Message reactions (emojis) for PubNub Chat Components for iOS

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 iOS 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.

Message Reactions

Message persistence

PubNub Chat Components for iOS 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.

Message reactions are a part of the Message List component and are enabled by default.

Default reactions

Message reactions consist of:

  • MessageReactionListComponent that allows for displaying message reactions under selected messages.
  • AddMessageReactionComponent that, upon long-tapping a message, displays a drawer view (Reaction Picker) with these six default message reactions to select from.

Reaction Picker

Message reactions are based on Unicode characters. Their full default list is defined through the DefaultReactionProvider structure using the ReactionProvider protocol.

public struct DefaultReactionProvider: ReactionProvider {
public let reactions: [String]

public init() {
reactions = ["👍", "❤️", "😂", "😲", "😢", "🔥"]
}
}

Message reactions are actions of the "reaction" type (actionType: "reaction") that are assigned a given emoji value (for example, 👍 for "thumbs up"). The whole logic defining what type of reactions are added or removed upon which user actions is defined by MessageListComponentViewModel through the messageActionTapped function.

  // Message action tapped
var messageActionTapped: ((MessageListComponentViewModel<ModelData, ManagedEntities>?, MessageReactionButtonComponent?, ManagedEntities.Message, (() -> Void)?) -> Void)? = { (viewModel, messageActionView, message, completion) in
guard let messageActionView = messageActionView else { return }

if messageActionView.isSelected {
// Remove message action
if let messageAction = message.messageActionViewModels.first(
where: { $0.pubnubUserId == viewModel?.author.pubnubUserID && $0.value == messageActionView.reaction }
) {
do {
viewModel?.provider.dataProvider
.removeRemoteMessageAction(.init(messageAction: try messageAction.convert())) { [weak messageActionView] _ in
completion?()
}
} catch {
show all 36 lines
Message Actions API

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

Replace default reactions

You can override the default emojis by providing a new set of emojis and passing them to the Chat Provider's main theme using the dedicated reactionTheme.

To change the default emojis:

  1. Define the new list of emojis.

    let reactions = ["👌", "\u{1F923}", "\u{1F60A}]
  2. Create reactionTheme to which you inject the new reactions list. Assign the new theme to the main theme in chatProvider.

    provider.chatProvider.themeProvider.template.messageListComponent.reactionTheme = ReactionTheme(reactions: reactions)
Reactions count

There is no limitation as to the total emojis count that can be shown in the Reaction Picker. If you provide a number larger than the default six, the rest of the icons are rendered further in the row and you can access them upon scrolling the reactions vertically.

Reactions list

The order in which reactions are displayed on screen in the Reaction Picker upon long-tapping a message is defined by the configure() method in MessageReactionListView.swift.

  open func configure<Message>(
_ message: Message,
currentUserId: String,
reactionProvider: ReactionProvider,
onMessageActionTap: ((MessageReactionButtonComponent?, Message, (() -> Void)?) -> Void)?
) where Message : ManagedMessageViewModel {
configure(
reactionButtons,
message: message,
currentUserId: currentUserId,
reactionProvider: reactionProvider,
onMessageActionTap: onMessageActionTap
)
}

Add reactions

You can add reactions to a message in one of these ways:

  • Long press the chosen message and select one of the six predefined emojis from the Reaction Picker that pops up under the tapped message. 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 inline message reaction list 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 inline message reaction list by the message reaction timestamp.

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

This configuration is defined in MessageReactionComponent.

currentCountPublisher.map({
$0 > 99 ? "99+" : String($0)
}).eraseToAnyPublisher(),

Reaction theme

MessageListComponentTheme is responsible for using ReactionProvider. However, this requires the reactionTheme property.

public class MessageListComponentTheme: ViewControllerComponentTheme {
...
@Published public var reactionTheme: ReactionTheme?
...
}

ReactionTheme is dedicated specifically to the Reaction Picker and the inline list of reactions under the message.

public struct ReactionTheme {
public var reactions: [String] {
provider.reactions
}

public let provider: ReactionProvider
public let pickerMaxWidth: CGFloat

/// - Parameters:
/// - reactions: reaction list.
/// - maxWidth: maximum picker width.
public init(reactions: [String], maxWidth: CGFloat = 300) {
provider = CustomReactionProvider(reactions: reactions)
pickerMaxWidth = maxWidth
}
show all 24 lines

To distinguish your own reactions from those added by others, their background is highlighted in red. This logic is defined in MessageReactionView.

$isHighlighted
.sink { [weak self] status in
if status {
self?.backgroundColor = AppearanceTemplate.Color.messageActionActive
} else {
self?.backgroundColor = .clear
}
}
.store(in: &cancellables)

The red color itself is hardcoded for the AppearanceTemplate.Color.messageActionActive variable in AppearanceTemplate.

public struct AppearanceTemplate {
...
public struct Color {
...
public static var messageActionActive: UIColor = UIColor(named: "messageActionActive") ?? UIColor(0xef3a43, alpha: 0.24)
}
}

You can override the background color for your own reactions using the Asset Catalog resource. To do it, create a new color set inside this catalog and name it messageActionActive.

Reaction size

The size of the whole Reaction Picker is defined in the ReactionTheme and it's set by default to 300 points.

public init(provider: ReactionProvider = DefaultReactionProvider(), maxWidth: CGFloat = 300) {
self.provider = provider
pickerMaxWidth = maxWidth
}

Communication with PubNub

Message reactions are a part of the Message List component and are managed by the MessageListComponentViewModel through the MessageActionModel property.

open class MessageListComponentViewModel<ModelData, ManagedEntities>:
ManagedEntityListViewModel<ModelData, ManagedEntities>,
ReloadDatasourceItemDelegate
where ModelData: ChatCustomData,
ManagedEntities: ChatViewModels,
ManagedEntities: ManagedChatEntities,
ManagedEntities.Channel.MemberViewModel == ManagedEntities.Member,
ManagedEntities.Message.MessageActionModel == ManagedEntities.MessageAction
{
...
}

The logic that defines what's happening upon long-tapping a message is invoked through the messageActionTapped function. This function calls dataProvider to either add a new reaction or remove an existing one.

var messageActionTapped: ((MessageListComponentViewModel<ModelData, ManagedEntities>?, MessageReactionButtonComponent?, ManagedEntities.Message, (() -> Void)?) -> Void)? = { (viewModel, messageActionView, message, completion) in
guard let messageActionView = messageActionView else { return }

if messageActionView.isSelected {
// Remove the message action
if let messageAction = message.messageActionViewModels.first(
where: { $0.pubnubUserId == viewModel?.author.pubnubUserID && $0.value == messageActionView.reaction }
) {
do {
viewModel?.provider.dataProvider
.removeRemoteMessageAction(.init(messageAction: try messageAction.convert())) { [weak messageActionView] _ in
completion?()
}
} catch {
PubNub.log.error("Message Action Tapped failed to convert Message Action while preparing to send Remove request: \(message)")
show all 34 lines

dataProvider passes the message reaction details (parent, actionType, and actionValue) to the server (pubnubProvider) through the respective sendRemoteMessageAction or removeRemoteMessageAction methods. If they're passed successfully, the data gets loaded to the PubNub storage or is removed from it.

  public func sendRemoteMessageAction(
_ request: MessageActionSendRequest<ModelData>,
completion: ((Result<ChatMessageAction<ModelData>, Error>) -> Void)?
) {
provider.pubnubProvider
.sendMessageAction(request) { [weak self] result in
switch result {
case .success(let action):
PubNub.log.debug("Send Message Success \(action)")
self?.load(messageActions: [action], completion: {
completion?(.success(action))
})
case .failure(let error):
PubNub.log.error("Send Message Action Error \(error)")
completion?(.failure(error))
show all 44 lines

A reaction added or removed on one device is automatically synchronized on other devices by ChatDataProvider.

extension ChatDataProvider {
open func syncPubnubListeners(
coreListener: CoreListener,
...
) {
...
coreListener.didReceiveBatchSubscription = { [weak self] events in
guard let self = self else { return }

var messages = [ChatMessage<ModelData>]()
var presenceChanges = [ChatMember<ModelData>]()
var messageActions = [ChatMessageAction<ModelData>]()

for event in events {
switch event {
show all 33 lines