Message reactions (emojis) for PubNub Chat Components for Android

One emoji is sometimes worth a thousand words. Adding emojis under messages in chat apps makes your conversations way more engaging and emotional. It improves the overall user experience by adding a visual touch to it. It's particularly useful in group and social chat use cases to boost community engagement.

PubNub Chat Components for Android support emojis in conversations through the so-called message reactions. This out-of-the-box feature is available for both 1:1 and group chats and comes with six default emojis. After enabling message reactions in your application, you can customize them to your liking.

Message Reactions

Enable reactions

Message persistence

PubNub Chat Components for Android use PubNub's Message Actions API to store information on added and removed message reactions. To use it in your app, make sure you have the Message persistence feature enabled on your app's keyset in the Admin Portal.

By default, message reactions are disabled. To enable them in your chat app, you have to add proper logic to your application code.

Check how to do that based on the Getting Started with Message Reactions app. To enable reactions, you must add a new Menu.kt file that defines the BottomMenu behavior and modify the existing Chat.kt file to update your chat application code.

Follow the steps in each file.

Create a Menu.kt file under the ui.view package and define the Menu composable function.

@Composable
fun Menu(
visible: Boolean,
message: MessageUi.Data?,
onAction: (MenuAction) -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
BottomMenu(
message = message,
headerContent = {
DefaultReactionsPickerRenderer.ReactionsPicker { reaction ->
message?.let { onAction(React(reaction, message)) }
}
},
show all 23 lines

BottomMenu is the drawable box that pops up when you tap the message you want to react to. It is enabled by default in the PubNub Chat Components for Android implementation and you can call it from any place in the application layout. To enable message reactions on BottomMenu, you need to pass the DefaultReactionsPickerRenderer implementation to the headerContent parameter as in the provided example.

Menu.kt

For reference, check the final version of the Menu.kt file in the Getting Started with Message Reactions app.

Chat.kt

Open the Chat.kt file and follow these steps:

  1. Add the onReactionSelected parameter to the Content composable function.

        internal fun Content(
    messages: Flow<PagingData<MessageUi>>,
    presence: Presence? = null,
    onMessageSelected: (MessageUi.Data) -> Unit,
    // Add the below line
    onReactionSelected: ((React) -> Unit)? = null,
    )
  2. Pass this new parameter to the MessageList component.

                MessageList(
    messages = messages,
    presence = presence,
    onMessageSelected = onMessageSelected,
    // Add the below line
    onReactionSelected = onReactionSelected,
    modifier = Modifier
    .fillMaxSize()
    .weight(1f, true),
    )
  3. Initialize reactionViewModel in the View function.

    @Composable
    fun View(
    channelId: ChannelId,
    ) {
    val messageViewModel: MessageViewModel = MessageViewModel.defaultWithMediator()
    val messages = remember(channelId) { messageViewModel.getAll(channelId) }

    // Add the below lines
    val reactionViewModel: ReactionViewModel = ReactionViewModel.default()
    DisposableEffect(channelId){
    reactionViewModel.bind(channelId)
    onDispose {
    reactionViewModel.unbind()
    }
    }
    show all 17 lines
  4. Add the menuVisible and selectedMessage states to the View function. These states are responsible for the BottomMenu visibility and saving information on the last selected message.

    @Composable
    fun View(
    channelId: ChannelId,
    ) {
    val messageViewModel: MessageViewModel = MessageViewModel.defaultWithMediator(channelId)
    val messages = remember { messageViewModel.getAll() }

    val reactionViewModel: ReactionViewModel = ReactionViewModel.default()
    DisposableEffect(channelId){
    reactionViewModel.bind(channelId)
    onDispose {
    reactionViewModel.unbind()
    }
    }

    show all 20 lines
  5. Define the onDismiss method.

    val onDismiss: () -> Unit = { menuVisible = false}
  6. Add the whole Menu component under CompositionLocalProvider and define the actions for it.

    Menu(
    visible = menuVisible,
    message = selectedMessage,
    onDismiss = onDismiss,
    onAction = { action ->
    when (action) {
    is Copy -> {
    messageViewModel.copy(AnnotatedString(action.message.text))
    }
    is React -> reactionViewModel.reactionSelected(action)
    else -> {}
    }
    onDismiss()
    }
    )
  7. Add the additional onMessageSelected and onReactionSelected parameters to the Content invoke function under CompositionLocalProvider.

    Content(
    messages = messages,
    // Add the "onMessageSelected" parameter
    onMessageSelected = {
    selectedMessage = it
    menuVisible = true
    },
    // Add the "onReactionSelected" parameter
    onReactionSelected = reactionViewModel::reactionSelected,
    )
Chat.kt

For reference, check the final version of the Chat.kt file in the Getting Started with Message Reactions app.

Default reactions

We provide six default message reactions to select from:

Bottom Menu

Message reactions are based on Unicode characters as surrogate pairs. Their full default list is defined in MenuDefaults.kt that's referenced in DefaultReactionsPickerRenderer.kt.

object DefaultReactionsPickerRenderer : ReactionsRenderer {
...
var emojis: List<Emoji> = MenuDefaults.reactions()
...
}

Check the default values in MenuDefaults.kt:

fun reactions() = listOf(
UnicodeEmoji("\uD83D\uDC4D"), // 👍 thumbs up
UnicodeEmoji("\u2764"), // ❤ red heart U+2764
UnicodeEmoji("\uD83D\uDE02"), // 😂 face with tears of joy U+1F602
UnicodeEmoji("\uD83D\uDE32"), // 😲 astonished face U+1F632
UnicodeEmoji("\uD83D\uDE22"), // 😢 crying face U+1F622
UnicodeEmoji("\uD83D\uDD25"), // 🔥 fire U+1F525
)

The UnicodeEmoji key enforces that a given emoji must be of the "reaction" type and the String value (for example, "\uD83D\uDC4D" for "thumbs up"), as defined in the Reaction.kt file.

data class UnicodeEmoji(override val type: String, override val value: String) : Emoji() {
constructor(value: String) : this("reaction", value)
}
Message Actions API

"reaction" is a message action type that's required by PubNub's Message Actions API that PubNub Chat Components for Android communicate with to store information on added and removed reactions.

Replace default reactions

You can override the default emojis provided by MenuDefaults.kt by defining a new set of emojis in your application code. You should run this code in your app during initialization, before calling DefaultReactionsPickerRenderer.

Check this example:

setContent {
AppTheme(pubNub = pubnub) {
DefaultReactionsPickerRenderer.emojis = listOf(
UnicodeEmoji("\uD83D\uDE4A"), // 🙊
UnicodeEmoji("\uD83D\uDE49"), // 🙉
UnicodeEmoji("\uD83D\uDE48"), // 🙈
UnicodeEmoji("\uD83D\uDC12"), // 🐒
UnicodeEmoji("\uD83E\uDD8D"), // 🦍
UnicodeEmoji("\uD83E\uDD84"), // 🦄
)
}
}

Change the default reactions count

PubNub Chat Components for Android provide six default emojis that appear in the BottomMenu row.

object DefaultReactionsPickerRenderer : ReactionsRenderer {
...
var visibleItemsCount: Int = 6
...
}

You can change this default number. To set it to 4, override the visibleItemsCount parameter in your application code.

setContent {
AppTheme(pubNub = pubnub) {
DefaultReactionsPickerRenderer.visibleItemsCount = 4
}
}

There is no limitation as to the total emojis count that can be shown in BottomMenu. If you provide a number larger than the default 6, the rest of the icons will be rendered under the default BottomMenu row and you can access them upon scrolling.

Limitations for negative values

The visibleItemsCount parameter is not validated for negative values so don't set it to <= 0.

Add reactions

If message reactions are enabled in your app, you can add them to a message in one of these ways:

  • Long press the chosen message and select one of the six predefined emojis from a drawer that pops up at the bottom of the screen (BottomMenu). This way you can also remove a previously added reaction.

  • Tap a reaction added by another user and the total count of reactions will increment in the reaction box under the message. This way you can also remove a previously added reaction.

Cumulative reactions

There's a counter next to every added message reaction that's incremented or decremented in real-time as users tap the emojis to either add or remove them. This list is sorted in the reaction box by the message reaction timestamp.

If the reaction counter is more than 99, it displays 99+.

This configuration is defined in the DefaultReactionsPickerRenderer.kt file.

text = if (reaction.members.size < 100) "${reaction.members.size}" else "99+"

Reaction theme

To distinguish your reactions from those added by others, their background is highlighted in red.

DefaultReactionsPickerRenderer defines this behavior.

val reactionTheme =
if (reaction.members.any { it.id == currentUserId }) theme.selectedReaction else theme.notSelectedReaction

The choice of the appropriate theme for your and other reactions is defined in ReactionTheme.

class ReactionTheme(
...
selectedReaction: ButtonTheme,
notSelectedReaction: ButtonTheme,
...
)

Reaction size

The size of reactions in BottomMenu depends on the device width and the number of available reactions. ReactionsPicker calculates it by dividing the maximum width of the device by the total number of selected message reactions.

itemWidth = floor(this.maxWidth.value / visibleItemsCount).dp

ReactionsPicker (in DefaultReactionsPickerRenderer.kt) sets the 1:1 ratio for the message reactions, so if you set one item (icon) in a row, this item will be a square with a width equal to the device width.

Communication with PubNub

Message reactions interact with the MessageList component. The logic that defines what is happening upon long-tapping a message is invoked in MessageRenderer through these four methods in the Message composable function.

interface MessageRenderer {
@Composable
fun Message(
...
reactions: List<ReactionUi>,
onMessageSelected: (() -> Unit)?,
onReactionSelected: ((Reaction) -> Unit)?,
reactionsPickerRenderer: ReactionsRenderer,
)
...
}
  • reactions stands for the list of reactions to a specific message.
  • onMessageSelected opens up BottomMenu with available reactions to pick when you long-tap a message.
  • onReactionSelected stands for the action that's to be called when you choose a reaction.
  • reactionsPickerRenderer is the default renderer that defines how the selected message reaction is displayed.

Long-tapping a message and choosing a reaction triggers reactionViewModel which acts as an intermediary between the UI layer, services, and repositories. reactionViewModel uses messageActionRepository to retrieve information on the selected reaction from the local Room database to verify who, when, and where (on which channel) selected a given reaction.

fun reactionSelected(reaction: PickedReaction) {
viewModelScope.launch {
val storedReaction = messageActionRepository.get(
userId,
react.message.channel,
react.message.timetoken,
react.reaction.type,
react.reaction.value,
)
...
}

If such a reaction, like "thumps up", hasn't been previously selected by this user and is not present in the local database, it needs to be added. messageReactionService calls actionService to add the given action entry to the PubNub storage.

    override suspend fun add(
channel: ChannelId,
messageTimetoken: Long,
type: String,
value: String,
) {
logger.i("Add message action '$type:$value' on channel '$channel'")
try {
val result = actionService.add(channel, PNMessageAction(type, value, messageTimetoken))
.toResult(channel)
addAction(result)
} catch (e: Exception) {
logger.e(e, "Cannot add message action")
}
}

If the action entry gets stored in PubNub successfully, messageReactionService saves this action information in the local repository.

private suspend fun addAction(result: PNMessageActionResult) {
messageActionRepository.insertOrUpdate(mapper.map(result))
}

The local database holds these details about each message reaction, where published stands for the time when the message action was added (check DBMessageAction.kt for details).

data class DBMessageAction(
override val channel: ChannelId,
override val user: UserId,
override val messageTimestamp: Timetoken,
override val published: Timetoken,
override val type: String,
override val value: String,
...
)