Message reactions
Reactions Overview
The SwiftUI chat SDK provides a default view that's displayed as an overlay of the message. When you long press on a message, the message reactions overlay is shown. By default, it shows a blurred background and a possibility to react to a message or remove a reaction. Additionally, the reactions overlay has a "message actions" slot, which allows you to perform actions on the message. Both the displayed reactions on a message and the reactions overlay can be replaced by your own views.
When reactions are added to a message, a view displaying the added reactions is shown above the message. When this view is tapped or long-pressed, a new overlay view displaying the list of people who reacted to a message is presented. That view can also be swapped with your own implementation.
Customizing the Message Reactions View
The simplest way to customize the message reactions view is to replace its reaction icons. Those are available under the availableReactions
property in the Images class, which is part of the Appearance class in the StreamChat object. The availableReactions
property is a dictionary, which contains mappings between MessageReactionType
and its corresponding ChatMessageReactionAppearanceType
, which consists of small and large icon for a reaction. If you change these properties, make sure to inject the updated Images
class in the StreamChat object.
let customReactions: [MessageReactionType: ChatMessageReactionAppearance] = [
.init(rawValue: "custom"): .init(smallIcon: smallIcon, largeIcon: largeIcon)
]
var images = Images()
images.availableReactions = customReactions
let appearance = Appearance(images: images)
streamChat = StreamChat(chatClient: chatClient, appearance: appearance)
You can also change the tint color of the reactions, by changing reactionCurrentUserColor
for the current user's reactions, or reactionOtherUserColor
for other users' reactions. Additionally, you can set a background for a selected reaction, with selectedReactionBackgroundColor
. These colors are optional, so if you don't want to tint the reactions, but want to use the original icon color, you can just pass nil
as a value. Here's an example how to change these values:
var colors = ColorPalette()
colors.reactionCurrentUserColor = UIColor.blue
colors.reactionOtherUserColor = UIColor.red
colors.selectedReactionBackgroundColor = UIColor.gray
let appearance = Appearance(colors: colors)
streamChat = StreamChat(chatClient: chatClient, appearance: appearance)
By default, the reactions are sorted by their raw value in an alphabetical order. You can change this logic by adding your own implementation of the sortReactions
closure in the Utils
class.
Here's an example how to achieve this:
let customReactionSort: (MessageReactionType, MessageReactionType) -> Bool = { lhs, rhs in
// Your custom sorting logic here
lhs.rawValue < rhs.rawValue
}
let utils = Utils(sortReactions: customReactionSort)
streamChat = StreamChat(chatClient: chatClient, utils: utils)
Changing the Message Reactions View
Alternatively, you can completely swap the ReactionsContainer
view with your own implementation. In order to do that, you need to implement the makeMessageReactionView
method from the ViewFactory
, which is called with the message as a parameter.
public func makeMessageReactionView(
message: ChatMessage
) -> some View {
CustomReactionsContainer(message: message)
}
Customizing the Reactions Overlay View
The reactions overlay view (shown on long press of a message), also provides access to the message actions. If you want to replace / extend the default message actions, you can do so via the supportedMessageActions
method in the ViewFactory
. As an inspiration, here's a glimpse on how the default message actions are configured.
public func supportedMessageActions(
for message: ChatMessage,
channel: ChatChannel,
onFinish: @escaping (MessageActionInfo) -> Void,
onError: @escaping (Error) -> Void
) -> [MessageAction] {
MessageAction.defaultActions(
factory: self,
for: message,
channel: channel,
chatClient: chatClient,
onFinish: onFinish,
onError: onError
)
}
extension MessageAction {
public static func defaultActions<Factory: ViewFactory>(
factory: Factory,
for message: ChatMessage,
channel: ChatChannel,
chatClient: ChatClient,
onFinish: @escaping (MessageActionInfo) -> Void,
onError: @escaping (Error) -> Void
) -> [MessageAction] {
var messageActions = [MessageAction]()
let replyAction = replyAction(
for: message,
channel: channel,
onFinish: onFinish
)
messageActions.append(replyAction)
if !message.isPartOfThread {
let replyThread = threadReplyAction(
factory: factory,
for: message,
channel: channel
)
messageActions.append(replyThread)
}
if message.isSentByCurrentUser {
let deleteAction = deleteMessageAction(
for: message,
channel: channel,
chatClient: chatClient,
onFinish: onFinish,
onError: onError
)
messageActions.append(deleteAction)
} else {
let flagAction = flagMessageAction(
for: message,
channel: channel,
chatClient: chatClient,
onFinish: onFinish,
onError: onError
)
messageActions.append(flagAction)
}
return messageActions
}
// MARK: - private
private static func replyAction(
for message: ChatMessage,
channel: ChatChannel,
onFinish: @escaping (MessageActionInfo) -> Void
) -> MessageAction {
let replyAction = MessageAction(
title: L10n.Message.Actions.inlineReply,
iconName: "icn_inline_reply",
action: {
onFinish(
MessageActionInfo(
message: message,
identifier: "inlineReply"
)
)
},
confirmationPopup: nil,
isDestructive: false
)
return replyAction
}
private static func threadReplyAction<Factory: ViewFactory>(
factory: Factory,
for message: ChatMessage,
channel: ChatChannel
) -> MessageAction {
var replyThread = MessageAction(
title: L10n.Message.Actions.threadReply,
iconName: "icn_thread_reply",
action: {},
confirmationPopup: nil,
isDestructive: false
)
let destination = factory.makeMessageThreadDestination()
replyThread.navigationDestination = AnyView(destination(channel, message))
return replyThread
}
private static func deleteMessageAction(
for message: ChatMessage,
channel: ChatChannel,
chatClient: ChatClient,
onFinish: @escaping (MessageActionInfo) -> Void,
onError: @escaping (Error) -> Void
) -> MessageAction {
let messageController = chatClient.messageController(
cid: channel.cid,
messageId: message.id
)
let deleteAction = {
messageController.deleteMessage { error in
if let error = error {
onError(error)
} else {
onFinish(
MessageActionInfo(
message: message,
identifier: "delete"
)
)
}
}
}
let confirmationPopup = ConfirmationPopup(
title: L10n.Message.Actions.Delete.confirmationTitle,
message: L10n.Message.Actions.Delete.confirmationMessage,
buttonTitle: L10n.Message.Actions.delete
)
let deleteMessage = MessageAction(
title: L10n.Message.Actions.delete,
iconName: "trash",
action: deleteAction,
confirmationPopup: confirmationPopup,
isDestructive: true
)
return deleteMessage
}
private static func flagMessageAction(
for message: ChatMessage,
channel: ChatChannel,
chatClient: ChatClient,
onFinish: @escaping (MessageActionInfo) -> Void,
onError: @escaping (Error) -> Void
) -> MessageAction {
let messageController = chatClient.messageController(
cid: channel.cid,
messageId: message.id
)
let flagAction = {
messageController.flag { error in
if let error = error {
onError(error)
} else {
onFinish(
MessageActionInfo(
message: message,
identifier: "flag"
)
)
}
}
}
let confirmationPopup = ConfirmationPopup(
title: L10n.Message.Actions.Flag.confirmationTitle,
message: L10n.Message.Actions.Flag.confirmationMessage,
buttonTitle: L10n.Message.Actions.flag
)
let flagMessage = MessageAction(
title: L10n.Message.Actions.flag,
iconName: "flag",
action: flagAction,
confirmationPopup: confirmationPopup,
isDestructive: false
)
return flagMessage
}
}
Alternatively, you can swap the whole MessageActionsView
with your own implementation, by implementing the makeMessageActionsView
method in the ViewFactory
.
public func makeMessageActionsView(
for message: ChatMessage,
channel: ChatChannel,
onFinish: @escaping (MessageActionInfo) -> Void,
onError: @escaping (Error) -> Void
) -> some View {
let messageActions = supportedMessageActions(
for: message,
channel: channel,
onFinish: onFinish,
onError: onError
)
return MessageActionsView(messageActions: messageActions)
}
As mentioned at the beginning, when the reactions are tapped or long-pressed, a view with the list of users who reacted to the message is displayed. In order to change this view with your own implementation, you will need to implement the makeReactionsUsersView
in the ViewFactory
. In this method, you receive the message which contains the reactions, as well as the maximum height available for this view.
func makeReactionsUsersView(
message: ChatMessage,
maxHeight: CGFloat
) -> some View {
ReactionsUsersView(
message: message,
maxHeight: maxHeight
)
}
The background of the reactions overlay is a blurred snapshot of the current channel view. You can customize it by implementing the makeReactionsBackgroundView
method in the ViewFactory
. For example, you can remove the blur, change the opacity, or even return an EmptyView
. Here's an example implementation of this method:
func makeReactionsBackgroundView(
currentSnapshot: UIImage,
popInAnimationInProgress: Bool
) -> some View {
Image(uiImage: currentSnapshot)
.overlay(Color.black.opacity(popInAnimationInProgress ? 0 : 0.1))
.blur(radius: popInAnimationInProgress ? 0 : 4)
}
The currentSnapshot
parameter returns the current snapshot of the whole view displaying the chat channel. The popInAnimationInProgress
parameter tells whether the animation is already popped in and can be used to transition between animation states.
You can customize the snapshot generation logic. In order to do this, you will need to provide your implementation of the SnapshotCreator
protocol, which has one method func makeSnapshot(for view: AnyView) -> UIImage
. The view
parameter is the SwiftUI view which invokes the reactions overlay presentation (the ChatChannelView
), while the generated UIImage
is used in the makeReactionsBackgroundView
method above.
In case you want to implement your own implementation of this protocol, you will need to inject it in our Utils
class.
let snapshotCreator = CustomSnapshotCreator()
let utils = Utils(snapshotCreator: snapshotCreator)
let streamChat = StreamChat(chatClient: chatClient, utils: utils)
Finally, you can swap the whole ReactionsOverlayView
with your own implementation. In order to do this, you need to implement the makeReactionsOverlayView
method in the ViewFactory
. The current snapshot of the message list is provided to you, in case you want to blur it or apply any other effects.
public func makeReactionsOverlayView(
channel: ChatChannel,
currentSnapshot: UIImage,
messageDisplayInfo: MessageDisplayInfo,
onBackgroundTap: @escaping () -> Void,
onActionExecuted: @escaping (MessageActionInfo) -> Void
) -> some View {
ReactionsOverlayView(
factory: self,
channel: channel,
currentSnapshot: currentSnapshot,
messageDisplayInfo: messageDisplayInfo,
onBackgroundTap: onBackgroundTap,
onActionExecuted: onActionExecuted
)
}