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 Persistence
PubNub Chat Components for iOS 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.
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.
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 linesMessage Reactions API
"reaction"
is a message action type that's required by PubNub's Message Reactions 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:
- Define the new list of emojis.
let reactions = ["👌", "\u{1F923}", "\u{1F60A}"]
- Create
reactionTheme
to which you inject the new reactions list. Assign the new theme to the main theme inchatProvider
.
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 linesTo 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 linesdataProvider
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 Message Persistence 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 linesA 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