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.
Enable reactions
Message Persistence
PubNub Chat Components for Android use PubNub's Message Reactions API to store information on added and removed message reactions. To use it in your app, make sure you have Message Persistence 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.
Menu.kt
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 linesBottomMenu
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:
-
Add the
onReactionSelected
parameter to theContent
composable function.internal fun Content(
messages: Flow<PagingData<MessageUi>>,
presence: Presence? = null,
onMessageSelected: (MessageUi.Data) -> Unit,
// Add the below line
onReactionSelected: ((React) -> Unit)? = null,
) -
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),
) -
Initialize
reactionViewModel
in theView
function.
show all 17 lines@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()
}
} -
Add the
menuVisible
andselectedMessage
states to theView
function. These states are responsible for theBottomMenu
visibility and saving information on the last selected message.
show all 20 lines@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()
}
} -
Define the
onDismiss
method.
val onDismiss: () -> Unit = { menuVisible = false}
-
Add the whole
Menu
component underCompositionLocalProvider
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()
}
) -
Add the additional
onMessageSelected
andonReactionSelected
parameters to theContent
invoke function underCompositionLocalProvider
.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:
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 Reactions API
"reaction"
is a message action type that's required by PubNub's Message Reactions 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 thumbs 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 Message Persistence.
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,
...
)