---
source_url: https://www.pubnub.com/docs/chat/community-supported/ios/message-reactions
title: Message reactions (emojis) for PubNub Chat Components for iOS
updated_at: 2026-06-15T12:11:23.251Z
---

> Documentation Index
> For a curated overview of PubNub documentation, see: https://www.pubnub.com/docs/llms.txt
> For the full list of all documentation pages, see: https://www.pubnub.com/docs/llms-full.txt


# 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](https://www.pubnub.com/assets/images/ios-message-reactions-0dea825ec45392525e65bba556c87373.png)

:::note Message Persistence
PubNub Chat Components for iOS use PubNub's [Message Reactions API](https://www.pubnub.com/docs/sdks/swift/api-reference/message-actions) 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](https://admin.pubnub.com/).
:::

Message reactions are a part of the [Message List](https://www.pubnub.com/docs/chat/community-supported/ios/ui-components#messagelist) 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](https://www.pubnub.com/assets/images/reaction-picker-ios-e8c289c4924380817733558c9a3825b5.png)

Message reactions are based on [Unicode characters](https://unicode.org/emoji/charts/full-emoji-list.html). Their full default list is defined through the `DefaultReactionProvider` structure using the `ReactionProvider` protocol.

```swift
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.

```swift
  // 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 {
          PubNub.log.error("Message Action Tapped failed to convert Message Action while preparing to send Remove request: \(message)")
        }
      }
    } else {
      // Add message action
      do {
        viewModel?.provider.dataProvider
          .sendRemoteMessageAction(
            .init(
              parent: try message.convert(),
              actionType: "reaction",
              actionValue: messageActionView.reaction
            )
          ) { [weak messageActionView] result in
            completion?()
          }
      } catch {
        PubNub.log.error("Message Action Tapped failed to convert Message while preparing to send Add request: \(message)")
      }
    }
  }
```

:::tip Message Reactions API
`"reaction"` is a message action type that's required by PubNub's [Message Reactions API](https://www.pubnub.com/docs/sdks/swift/api-reference/message-actions) 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](https://www.pubnub.com/docs/chat/community-supported/ios/chat-provider)'s main theme using the dedicated [reactionTheme](#reaction-theme).

To change the default emojis:

1. Define the new list of emojis.

```swift
let reactions = ["👌", "\u{1F923}", "\u{1F60A}"]
```

1. Create `reactionTheme` to which you inject the new reactions list. Assign the new theme to the main theme in `chatProvider`.

```swift
provider.chatProvider.themeProvider.template.messageListComponent.reactionTheme = ReactionTheme(reactions: reactions)
```

:::tip 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`.

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

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

## Reaction theme

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

```swift
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.

```swift
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
  }
  
  /// - Parameters:
  ///   - provider: object providing list of symbols
  ///   - maxWidth: maximum picker width.
  public init(provider: ReactionProvider = DefaultReactionProvider(), maxWidth: CGFloat = 300) {
    self.provider = provider
    pickerMaxWidth = maxWidth
  }
}
```

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

```swift
$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`.

```swift
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](https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_ref-Asset_Catalog_Format/index.html) resource. To do it, [create a new color set](https://help.apple.com/xcode/mac/current/#/dev10510b1f7) 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.

```swift
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](https://www.pubnub.com/docs/chat/community-supported/ios/ui-components#messagelist) component and are managed by the `MessageListComponentViewModel` through the `MessageActionModel` property.

```swift
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.

```swift
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)")
    }
    }
} else {
    // Add the message action
    do {
    viewModel?.provider.dataProvider
        .sendRemoteMessageAction(
        .init(
            parent: try message.convert(),
            actionType: "reaction",
            actionValue: messageActionView.reaction
        )
        ) { [weak messageActionView] result in
        completion?()
        }
    } catch {
    PubNub.log.error("Message Action Tapped failed to convert Message while preparing to send Add request: \(message)")
    }
}
```

`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 Message Persistence or is removed from it.

```swift
  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))
        }
      }
  }

  public func removeRemoteMessageAction(
    _ request: MessageActionRequest<ModelData>,
    completion: ((Result<Void, Error>) -> Void)?
  ) {
    provider.pubnubProvider
      .removeMessageAction(request) { [weak self] result in
        switch result {
        case .success(let action):
          PubNub.log.debug("Send Message Success \(action)")
          self?.removeStoredMessageAction(
            messageActionId: action.id,
            completion: { error in
              if let error = error {
                completion?(.failure(error))
              } else {
                completion?(.success(Void()))
              }
            }
          )
        case .failure(let error):
          PubNub.log.error("Send Message Action Error \(error)")
          completion?(.failure(error))
        }
      }
  }
```

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

```swift
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 {
        ...
        case .messageActionAdded(let messageAction):
          PubNub.log.debug("Listener: Message Action added \(messageAction)")
          do {
            messageActions.append(try ChatMessageAction<ModelData>(from: messageAction))
          } catch {
            PubNub.log.error("Listener Message Action received conversion error \(error)")
          }
        case .messageActionRemoved(let messageAction):
          PubNub.log.debug("Listener: Message Action removed \(messageAction)")
          self.removeStoredMessageAction(messageActionId: messageAction.pubnubId)
        ...
        }
      }
      ...
    }
  }
}
```