Message menu for PubNub Chat Components for Android

MessageMenu allows you to perform actions on sent messages by long-tapping them. It allows you, for example, to copy a message to the clipboard.

Message Menu

MessageMenu uses the concept of Bottom Sheets from Material Design. By default, the Copy message option in MessageMenu is enabled, but you can easily disable or override it. MessageMenu appears on BottomMenu that pops up when you long-tap a message. A single message option available on MessageMenu consists of an icon and a text field that provides the name of the option.

BottomMenu

MessageMenu is defined in PubNub Chat Components for Android as part of the BottomMenu composable function:

@Composable
fun BottomMenu(
message: MessageUi.Data?,
onAction: (MenuAction) -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
visible: Boolean = true,
states: List<MenuItemState> = message?.let { MenuDefaults.items(message) } ?: emptyList(),
headerContent: @Composable ColumnScope.() -> Unit = {},
bodyContent: @Composable ColumnScope.() -> Unit = {
MessageMenu(items = states, onClick = { onAction(it) })
},
)
ParameterDescription
messageReference to the selected message, needed for all action types
onActionGlobal onClick handler with MenuAction passed
onDismissAction to call after closing or clicking outside of MessageMenu
modifierWay in which MessageMenu is drawn
visibleVisibility state
statesList of menu items
headerContentRendering function for the header, empty by default, used for drawing message reactions
bodyContentRendering function for the body, used for MessageMenu by default

BottomMenu describes the bottom drawer that pops up on the screen when you long-tap a sent message. The layout of BottomMenu itself is split into headerContent and bodyContent, both of which are modifiable. By default, headerContent is empty and must be used to pass message reactions while bodyContent contains the MessageMenu component which specifies options available in BottomMenu:

@Composable
fun MessageMenu(
items: List<MenuItemState>,
onClick: (MenuAction) -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.(MenuItemState) -> Unit = { state ->
MenuItem(
state = state,
onClick = onClick,
)
}
)

Check out a sample usage:

@Composable
fun MessageMenuExample() {
val items = listOf(
MenuItemState(
title = R.string.menu_copy,
iconPainter = rememberVectorPainter(image = Icons.Rounded.ContentCopy),
action = Copy(dummyMessageData()),
),
MenuItemState(
title = R.string.menu_delete,
iconPainter = rememberVectorPainter(image = Icons.Rounded.Delete),
action = Delete(dummyMessageData()),
),
)
MenuItemTheme(DefaultMenuItemTheme) {
show all 18 lines

The MessageMenu composable function consists of:

items

items stands for the list of menu items referred to in the MenuItemState class. This call contains a string resource reference to the title of the menu option, iconPainter for drawing an icon (you can use either local or remote files), and the type of action on which the option is triggered (set to onClick by default).

data class MenuItemState(
@StringRes val title: Int,
val iconPainter: Painter,
val action: MenuAction,
)

Default values for those parameters are stored in MenuDefaults that contains both the default MessageMenu options and message reactions:

object MenuDefaults {
@Composable
fun items(message: MessageUi.Data) = listOf(
MenuItemState(
title = R.string.menu_copy,
iconPainter = rememberVectorPainter(image = Icons.Rounded.ContentCopy),
action = Copy(message),
),
)

onClick

onClick stands for the callback handler for selecting an action. It calls MenuAction which is a sealed class for defining multiple actions.

sealed class MenuAction(val message: MessageUi.Data)

class Copy(message: MessageUi.Data) : MenuAction(message)

modifier

modifier is an obligatory Compose parameter that defines how the component is drawn. We can use it to define the animation for BottomMenu, its position, or its size.

content

content is a composable function used for rendering a single item (MenuItem).

Check out a sample usage:

@Composable
fun MenuItemExample() {
val message: MessageUi.Data = dummyMessageData()
MenuItemTheme(DefaultMenuItemTheme) {
MenuItem(
MenuItemState(
title = R.string.menu_copy,
iconPainter = rememberVectorPainter(image = Icons.Rounded.ContentCopy),
action = Copy(message),
)
)
}
}

Disable message options

MessageMenu with the Copy message option is enabled by default. To disable it, replace the bodyContent parameter with an empty function:

BottomMenu(
onAction = { action ->
onAction(action)
},
onDismiss = onDismiss,
visible = visible && message != null,
modifier = modifier,
// Add the below line
bodyContent = {},
)

Override message options

You can define your own message options to be displayed in MessageMenu or replace the existing option with a different one. To add additional options:

  1. Define a new MessageMenu option, such as Delete:

    val menuItems = listOf(
    MenuItemState(
    title = R.string.menu_copy,
    iconPainter = rememberVectorPainter(image = Icons.Rounded.ContentCopy),
    action = Copy(message),
    ),
    MenuItemState(
    title = R.string.menu_delete,
    iconPainter = rememberVectorPainter(image = Icons.Rounded.Delete),
    action = Delete(message),
    )
    )
  2. Pass menuItems in the states parameter to BottomMenu in your app code:

    BottomMenu(

    states = menuItems,

    )
  3. Consume the new option by overriding the onAction parameter and adding the additional Delete class:

    onAction = { action ->
    when (action) {
    is Copy -> {
    action.message.text?.let { content ->
    messageViewModel.copy(AnnotatedString(content))
    }
    }
    is Delete -> handleDelete(action.customData, action.message)
    is React -> reactionViewModel.reactionSelected(action)
    else -> {}
    }
    }

Theming

As already mentioned, a single MessageMenu option (like Copy message) is defined by the MenuItem composable function that applies the theming outlined in MenuItemTheme.

class MenuItemTheme(
modifier: Modifier = Modifier,
text: TextTheme,
icon: IconTheme,
horizontalArrangement: Arrangement.Horizontal,
verticalAlignment: Alignment.Vertical,
)

Its default implementation looks as follows:

@Composable
fun MenuItem(
state: MenuItemState,
onClick: ((MenuAction) -> Unit)? = null,
theme: MenuItemTheme = LocalMenuItemTheme.current,
) {
Row(
modifier = Modifier
.clickable(
onClick = { onClick?.invoke(state.action) },
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
)
.then(theme.modifier),
horizontalArrangement = theme.horizontalArrangement,
show all 31 lines

Customize parameters

MenuItemTheme allows you to customize these parameters for MessageMenu:

  • Background color of the whole list and a single option
  • Text color of an option title
  • Icon tint

Background color of the list

To change the background color of the whole option list to a different one, pass the modifier parameter with a selected background color like Red:

BottomMenu(
onAction = { action ->
onAction(action)
onDismiss()
},
onDismiss = onDismiss,
visible = visible && message != null,
// Add the below line
modifier = Modifier.background(Color.Red),
...
)

Since in the default implementation there's no padding set to modifier, add extra padding to show the difference better:

modifier = Modifier
.background(Color.Red)
.padding(12.dp),

Background color of an option

To change the background color of a single option, override the background parameter to a selected color like DarkGray:

val theme = ThemeDefaults.menuItem(modifier = Modifier.fillMaxWidth().background(Color.DarkGray))
MenuItemTheme(theme) {
BottomMenu(...)
}

Color of the option title

To change the color of an option title to a different one, override the menuItemTitle parameter to a selected color like Red:

val theme = ThemeDefaults.menuItem(text = TextThemeDefaults.menuItemTitle(color = Color.Red))
MenuItemTheme(theme) {
BottomMenu(...)
}

Icon tint

To change the color tint of an icon to a different one, override the menuIcon parameter to a selected color like Red:

val theme = ThemeDefaults.menuItem(icon = IconThemeDefaults.menuIcon(tint = Color.Red))
MenuItemTheme(theme) {
BottomMenu(...)
}