open class ChatMessageContentView: _View, ThemeProvider { public private(set) var deliveryStatusView: ChatMessageDeliveryStatusView? open func layout(options: ChatMessageLayoutOptions) { if options.contains(.deliveryStatusIndicator) { metadataSubviews.append(createDeliveryStatusView()) } ... } }
Customize Message Delivery Status
This guide explains how to manipulate delivery status UI part, specifically:
- how to show avatars of members who have seen the message
- how to hide status delivery indicator
If message delivery status term is not familiar to you - check out Message Delivery Status guide first.
UI component showing delivery status
The UI component responsible for showing delivery status indicator is ChatMessageDeliveryStatusView
. This component is shown inside ChatMessageContentView
when the message view is configured with layout options that include deliveryStatusIndicator
option.
Show avatars of members who’s seen a message
By default, when message from the current user sent to a group channel has read
delivery state, it shows the number of members who have seen it. To customize this behaviour and show member avatars, a custom ChatMessageDeliveryStatusView
subclass is needed.
The implementation of a subclass might be the following:
// 1. Subclass `ChatMessageDeliveryStatusView` final class CustomChatMessageDeliveryStatusView: ChatMessageDeliveryStatusView { // 2. Declare configuration variables for max # of avatars shown & the avatar size private let maxNumberOfAvatars = 3 private let avatarSize = CGSize(width: 15, height: 15) // 3. Declare a container for avatars and array of `ChatAvatarView` to have easy access to avatar views. private let avatarsStackView = UIStackView() private var avatarViews: [ChatAvatarView] = [] // 4. Override `setUpLayout` to make layout customizations. override func setUpLayout() { // Call `super` to get default layout in place. super.setUpLayout() // Setup the stack view showing avatars. avatarsStackView.translatesAutoresizingMaskIntoConstraints = false avatarsStackView.axis = .horizontal avatarsStackView.distribution = .fill avatarsStackView.alignment = .fill stackView.addArrangedSubview(avatarsStackView) // Create avatar views and add them to both the container and the array. (0..<maxNumberOfAvatars).forEach { _ in let avatarView = components.avatarView.init() avatarView.translatesAutoresizingMaskIntoConstraints = false avatarView.heightAnchor.constraint(equalToConstant: avatarSize.height).isActive = true avatarView.widthAnchor.constraint(equalToConstant: avatarSize.width).isActive = true avatarsStackView.addArrangedSubview(avatarView) avatarViews.append(avatarView) } // Make `messageReadСountsLabel` the last in the root container. // This label will be used to show `and N more` and should go after the avatars in the UI. stackView.addArrangedSubview(messageReadСountsLabel) } // 5. Override `updateContent` to configure avatars when content is updated. override func updateContent() { super.updateContent() // Get array of members who have seen the message. Sort it to fix the order. let readBy = content?.message.readBy.sorted { $0.id < $1.id } ?? [] avatarsStackView.isHidden = readBy.count == 0 // Iterate and configure avatar views, hide those ones that don't have a user to show. for index in 0..<avatarViews.count { let user = readBy.indices.contains(index) ? readBy[index] : nil let avatarView = avatarViews[index] avatarView.isHidden = user == nil components.imageLoader.loadImage( into: avatarView.imageView, url: user?.imageURL, imageCDN: components.imageCDN, placeholder: appearance.images.userAvatarPlaceholder4, preferredSize: avatarSize ) } // Calculate how many member are not shown. Show `and more` label if not users are shown. let leftUsers = readBy.count - avatarViews.count messageReadСountsLabel.text = leftUsers > 0 ? "and \(leftUsers) more" : nil messageReadСountsLabel.isHidden = messageReadСountsLabel.text == nil } }
Let’s take some final touches and make the UI a bit nicer:
override func setUpLayout() { // 1. Use negative spacing in the stack view making the next avatar overlapping the previous one. avatarsStackView.spacing = -avatarSize.width / 2 (0..<maxNumberOfAvatars).forEach { _ in // 2. Add a border to each avatar view. avatarView.imageView.layer.borderColor = appearance.colorPalette.background6.cgColor avatarView.imageView.layer.borderWidth = 1 } }
The last step is to register custom subclass in Components
so it is injected into the UI SDK and used instead of the default component:
Components.default.messageDeliveryStatusView = CustomChatMessageDeliveryStatusView.self
Run the app and see the outcome 🎉
Before | After |
---|---|
![]() | ![]() |
Handle taps
When only a subset of members is displayed in the message cell, there can be a requirement to show the entire list. Let’s see how to handle taps on delivery indicator and bring custom tap handler in place.
Steps:
- Enable user interaction for custom delivery indicator
- Have a custom
ChatMessageListVC
subclass and overridemessageContentViewDidTapOnDeliveryStatusIndicator
- Register
ChatMessageListVC
component
Luckily, the CustomChatMessageDeliveryStatusView
is a UIControl
so it can be made tappable just by setting isUserInteractionEnabled = true
:
// 1. final class CustomChatMessageDeliveryStatusView: ChatMessageDeliveryStatusView { override func updateContent() { ... // Allow interaction when message is in `read` state. isUserInteractionEnabled = readBy.count > 0 } }
With enabled interaction, taps on the delivery component trigger messageContentViewDidTapOnDeliveryStatusIndicator
on ChatMessageListVC
. Nothing happens by default but it’s possible to have custom logic triggered by subclassing ChatMessageListVC
and providing an override for this method.
// 2. final class CustomChatMessageListVC: ChatMessageListVC { override func messageContentViewDidTapOnDeliveryStatusIndicator(_ indexPath: IndexPath?) { guard let indexPath = indexPath, let message = dataSource?.chatMessageListVC(self, messageAt: indexPath) else { return } // Create and present custom screen showing `message.readBy` } }
Don’t forget to inject it:
// 3. Components.default.messageListVC = CustomChatMessageListVC.self
Running the app and making a tap on custom delivery status indicator triggers messageContentViewDidTapOnDeliveryStatusIndicator
on custom message list subclass 🎉
Hide delivery status
The delivery status indicator can be hidden in the UI by creating custom layout options resolver which would exclude the deliveryStatusIndicator
from calculated options.
Steps:
- Subclass
ChatMessageLayoutOptionsResolver
type - Override
optionsForMessage
, excludedeliveryStatusIndicator
option fromsuper.optionsForMessage
- Inject custom subclass into
Components
// 1. final class CustomChatMessageLayoutOptionsResolver: ChatMessageLayoutOptionsResolver { // 2. override func optionsForMessage( at indexPath: IndexPath, in channel: ChatChannel, with messages: AnyRandomAccessCollection<ChatMessage>, appearance: Appearance ) -> ChatMessageLayoutOptions { var layoutOptions = super.optionsForMessage(at: indexPath, in: channel, with: messages, appearance: appearance) layoutOptions.remove(.deliveryStatusIndicator) return layoutOptions } } // 3. Components.default.messageLayoutOptionsResolver = CustomChatMessageLayoutOptionsResolver()
Before | After |
---|---|
![]() | ![]() |
You can find more information on how the components configuration works here.