==

Q-Consultation for every industry

Securely hold virtual meetings and video conferences

Learn More>

Want to learn more about our products and services?

Speak to us now

How to use Message Reactions to Enhance your Flutter Chat App

Hitesh Garg
4 Jan 2023
Use Message Reactions to enhance your chat app

If you want your Flutter chat app to stand out against the rest, consider adding additional features to make your app more fun and engaging. In our previous blog, we showed you How to Create Polls & Surveys in your Flutter Chat App using QuickBlox. In this blog, we’ll show you how to build another cool feature on top of the QuickBlox SDK. Read on to learn how to create facebook-like message reactions that will delight your users.

message reactions

Find out more about: How to Save and Share Files in your Flutter Chat App

Table of Contents

Initial Set-up

To set up the template application for building the react-to-message functionality, we have taken code from the official Flutter chat sample application of Quickblox. The Quickblox sample chat app makes it really easy to build chat fast.

Learn more about: Introducing the QuickBlox Flutter Code Sample

Check out our template application code.

This application has three screens: Splash, Login, and Chat. For this article, the _dialogId on the Chat screen has been hard-coded so that users are taken directly to the relevant Group chat.

To create a Flutter app using the QuickBlox SDK, you will new a QuickBlox developer account.
Create a new account, or sign-in to your existing account. You can also use your Google or GitHub accounts to sign in.

  1. Once you’re in your QuickBlox account, click the “New App” button to create an app.
  2. Then, input the information about your organization into the specified fields and click “Add”.
  3. Finally, navigate to the Dashboard, select your app and go to the Overview section to copy the Application.
  4. ID, Authorization Key, Authorization Secret, and Account Key.

After acquiring the application credentials, paste them into the main.dart file that is included in the template application. If the above steps have been followed correctly, the application can now be run with the following commands:

flutter packages get
flutter run

Note: We recommend that you add your credentials to the keys.env file and include it in the .gitignore for security purposes.

Congratulations! Your basic chat application is now up and running!

Working with QuickBlox Custom Objects and Data Models

To start with, let’s take a moment to understand the data models offered by QuickBlox. After that, we can create our models on top of them to extend functionalities and create reactions.

data model

QBMessageWrapper is a wrapper built around the default data object, QBMessage, provided by Quickblox. This wrapper contains additional fields such as senderName, and date that are useful when displaying the message data on a chat screen.

QBMessage is a default data object that consists of the message identifier(id), text message(body), and extra metadata (properties) about a message.

QBMessage is great for rendering static text messages, locations, and web links, but it cannot be used to add real-time updating react-to-message features. To accomplish this, Quickblox provides Custom Objects, which is a custom key-value database schema that can be updated in real-time, making it an ideal fit for the react-to-message feature.

Learn more about: What are Custom Objects and how they can Benefit your Application

Next, let’s go to our Quickblox Dashboard, select “Custom,” click “Add,” and select “Add New Class.” We will then create a custom schema class called “Reaction” as shown below.

Add class

Once created, open the “Edit Permissions” option, adjust the permission level, and select the appropriate checkboxes as shown below:

Adding react messages to chat

Note: If users do not have open permissions, they will not be able to modify the Reaction values from the app.

We will create two data models: MessageReactProperties and MessageActionReact.

  1. MessageReactProperties will contain a map of user ids and reaction ids that represent the reactions users have to a message. It will also have a toJson method which will return a map of the class needed to create the Reaction custom object.
  2. MessageActionReact will include messageReactId, reacts, currentUserId, and chosenReactionId. This will assist us in updating our custom object. A getter, updatedReacts, will recalculate the reactions with the user-chosen option and return the updated value.

Note: The values of the map are encoded as strings using jsonEncode, as Quickblox custom objects do not support the map data type.

import 'dart:convert';

class MessageReactProperties {
  const MessageReactProperties({
    required this.reacts,
  });
  final Map reacts;

  Map toJson() {
    return {
      "reacts": jsonEncode({}),
    };
  }

  factory MessageReactProperties.fromData() {
    return const MessageReactProperties(
      reacts: {},
    );
  }
}

class MessageActionReact {
  const MessageActionReact({
    required this.messageReactId,
    required this.reacts,
    required this.currentUserId,
    required this.chosenReactionId,
  });
  final String messageReactId;
  final Map reacts;
  final String chosenReactionId;
  final String currentUserId;

  Map get updatedReacts {
    reacts[currentUserId] = chosenReactionId;
    return {"reacts": jsonEncode(reacts)};
  }
}

We need a model to handle incoming data, in addition to the models we use to parse and send data.

To do this, let’s create a ReactionMessage class that extends QBMessageWrapper and will contain the necessary properties to handle the reaction-related data.

import 'dart:convert';

import 'package:quickblox_polls_feature/models/message_wrapper.dart';
import 'package:quickblox_sdk/models/qb_custom_object.dart';
import 'package:quickblox_sdk/models/qb_message.dart';

class ReactionMessage extends QBMessageWrapper {
  ReactionMessage(
    super.senderName,
    super.message,
    super.currentUserId, {
    required this.messageReactId,
    required this.reacts,
  });

  final String messageReactId;
  final Map reacts;

  factory ReactionMessage.fromCustomObject(String senderName, QBMessage message,
      int currentUserId, QBCustomObject object) {
    return ReactionMessage(
      senderName,
      message,
      currentUserId,
      messageReactId: message.properties!['messageReactId']!,
      reacts: Map.from(
        jsonDecode(object.fields!['reacts'] as String),
      ),
    );
  }
  ReactionMessage copyWith({Map? reacts}) {
    return ReactionMessage(
      senderName!,
      qbMessage,
      currentUserId,
      messageReactId: messageReactId,
      reacts: reacts ?? this.reacts,
    );
  }
}

const REACTION_ID_MAP = {
  "#001": "assets/images/love.png",
  "#002": "assets/images/laugh.png",
  "#003": "assets/images/sad.png",
  "#004": "assets/images/angry.png",
  "#005": "assets/images/wow.png",
};

Now that we have finished setting up the models, let’s move on to the logic portion of our project. You may have already noticed that we have a constant map that stores the reaction ids and maps them to the image path of the reactions. This will be useful when we get the ids and the corresponding reaction.

Writing the logic to support reactions

In the previous section, we created data models to support reactions. Now, we will write the logic to utilize these models.

Let’s map out the steps necessary to incorporate reactions on text messages.

  • First, we need to create a custom object for every message that is sent, with the object’s ID being passed as a property of the message.
  • Then, upon receiving a reaction, we should send a reaction message and update the custom object accordingly.
  • Lastly, on the receiver’s end, when a reaction message is received, the total number of reactions should be retrieved and converted into a ReactionMessage.

Let’s go to _sendTextMessage in chat_screen_bloc for our first point. This method is invoked when we want to transmit a text message; we will adjust it to accept extra properties

Futurec _sendTextMessage(
  String text, {
  required MessageReactProperties reactProperties,
}) async {
  if (text.length > TEXT_MESSAGE_MAX_SIZE) {
    text = text.substring(0, TEXT_MESSAGE_MAX_SIZE);
  }

  await _chatRepository.sendMessage(
    _dialogId,
    text,
    reactProperties: reactProperties,
  );
}

We have added the named parameter reactProperties, so let’s incorporate it into the sendMessage function in chat_repository and alter the method according to the second point outlined in the steps above.


Future<void> sendMessage(
  String? dialogId,
  String messageBody, {
  Map? properties,
  required MessageReactProperties reactProperties,
}) async {
  if (dialogId == null) {
    throw RepositoryException(_parameterIsNullException,
        affectedParams: ["dialogId"]);
  }
  
  //Create custom object to hold reactions for the message
  final List reactObject = await QB.data.create(
    className: 'Reaction',
    fields: reactProperties.toJson(),
  );
  
  //Get the id of custom object
  final messageReactId = reactObject.first!.id!;

  //Add the id to message properties
  properties ??= {};
  properties['messageReactId'] = messageReactId;

  //Send message
  await QB.chat.sendMessage(
    dialogId,
    body: messageBody,
    saveToHistory: true,
    markable: true,
    properties: properties,
  );
}

We will create a custom object to hold reactions and pass its ID as a property to the message. After that, we’ll send a reaction message and update the custom object when a user reacts.

To do this, we’ll create a ReactMessageEvent in the chat_screen_events and push it from the UI to the BLoC, which will trigger a repository call to communicate with Quickblox servers.


class ReactMessageEvent extends ChatScreenEvents {
  final MessageActionReact data;

  ReactMessageEvent(this.data);
}

Now in the chat_screen_bloc, let’s check for ReactMessageEvent:

if (receivedEvent is ReactMessageEvent) {
  try {
    await Future.delayed(const Duration(milliseconds: 300), () async {
      await _sendReactMessage(
        data: receivedEvent.data,
      );
    });
  } on PlatformException catch (e) {
    states?.add(
      SendMessageErrorState(makeErrorMessage(e), 'Can\'t react to message'),
    );
  } on RepositoryException catch (e) {
    states
        ?.add(SendMessageErrorState(e.message, 'Can\'t react to message'));
  }
}

Future<void> _sendReactMessage({required MessageActionReact data}) async {
  await _chatRepository.sendReactMessage(
    _dialogId,
    data: data,
  );
}

In the chat_repository, create the sendReactMessage method and update the reactions in the custom object accordingly.

Future<void> sendReactMessage(
  String? dialogId, {
  required MessageActionReact data,
}) async {
  if (dialogId == null) {
    throw RepositoryException(_parameterIsNullException,
        affectedParams: ["dialogId"]);
  }

  await QB.data.update(
    "Reaction",
    id: data.messageReactId,
    fields: data.updatedReacts,
  );

  await QB.chat.sendMessage(
    dialogId,
    markable: true,
    properties: {
      "action": "messageActionReact",
      "messageReactId": data.messageReactId
    },
  );
}

We are not saving the reaction message to history. This is because the sole purpose of sendMessage is to inform the current clients that their reactions to a message have changed.

Note: saveToHistory should be used carefully, as pagination multiple times for groups of over 100 people can lead to a collection of superfluous React messages.

To learn more about parameters such as markable and saveToHistory, consult the Quickblox official documentation.

On the receiver’s end, we need to check if a reaction message has been received and update the reactions on the text message. To do this, we can write the following steps:

In the chat_screen_bloc file, we have the HashSet<QBMessageWrapper> _wrappedMessageSet, which stores all messages sorted by time.

We also have a method _wrapMessages(), which is called every time we receive new messages and is responsible for wrapping the QBMesssage(s) in the List<QBMessageWrappers>. We will now update this method to also handle reactions.

  • Upon receiving a Text Message, check if its properties contain a messageReactId. Fetch the corresponding reaction custom object for that id, add the reactions to the message, and convert it into a ReactionMessage object.
  • Upon receiving a React message with properties having action as messageActionReact, get the custom object with the id, get the reactions for the custom object, find the corresponding text message with the id, update the reactions, remove it from the list, and add the updated ReactionMessage to the list.
Future messages) async {
  List wrappedMessages = [];
  for (QBMessage? message in messages) {
    if (message == null) {
      break;
    }

    QBUser? sender = _getParticipantById(message.senderId);
    if (sender == null && message.senderId != null) {
      List users =
          await _usersRepository.getUsersByIds([message.senderId!]);
      if (users.isNotEmpty) {
        sender = users[0];
        _saveParticipants(users);
      }
    }
    String senderName = sender?.fullName ?? sender?.login ?? "DELETED User";

    if (message.properties?['action'] == 'pollActionVote') {
      //SOME CODE HERE
    } else if (message.properties?['action'] == 'pollActionCreate') {
      //SOME CODE HERE
      
      //OUR CODE HERE
    } else if (message.properties?['action'] == 'messageActionReact') {
      
      //Get the ID out of react message.
      final id = message.properties!['messageReactId']!;
      try {
      //Get the custom object.
        final reactObject = await _chatRepository
            .getCustomObject(ids: [id], className: 'Reaction');

      //Get updated reactions and update the message.
        if (reactObject != null) {
          final reacts = Map.from(
            jsonDecode(reactObject.first!.fields!['reacts'] as String),
          );
          final reactMessage = _wrappedMessageSet.firstWhere((element) =>
                  element is ReactionMessage && element.messageReactId == id)
              as ReactionMessage;
          _wrappedMessageSet.removeWhere((element) =>
              element is ReactionMessage && element.messageReactId == id);
          wrappedMessages.add(
            reactMessage.copyWith(reacts: reacts),
          );
        }
      } catch (e) {
        wrappedMessages
            .add(QBMessageWrapper(senderName, message, _localUserId!));
      }
    } else {
      if (message.properties?['messageReactId'] != null) {

        //Get the ID out of react message.
        final id = message.properties!['messageReactId']!;
        try {
        //Get the custom object and add the Reaction Message.
          final reactObject = await _chatRepository
              .getCustomObject(ids: [id], className: 'Reaction');
          if (reactObject != null) {
            wrappedMessages.add(
              ReactionMessage.fromCustomObject(
                senderName,
                message,
                _localUserId!,
                reactObject.first!,
              ),
            );
          }
        } catch (e) {
          wrappedMessages
              .add(QBMessageWrapper(senderName, message, _localUserId!));
        }
      } else {
        wrappedMessages
            .add(QBMessageWrapper(senderName, message, _localUserId!));
      }
    }
  }
  return wrappedMessages;
}

The above code has enabled us to create a list of messages with reactions, which can now be displayed in the UI.

Note: We have used a placeholder “SOME CODE HERE” in order to keep the code snippet short and concise.

Constructing the UI

We have the data models and reaction logic in place, so now let’s focus on creating an attractive UI for our reactions feature. We can start by enabling users to long press on a message to view a list of possible reactions.

Currently, we have a list of PopupMenuItem options such as Forward, Delivered to, etc. However, we want to make PopupMenuItem display a list of reaction images horizontally instead.

To achieve this, we will create our own widget by extending the base class of PopupMenuItem, e.g.PopupMenuEntry.

import 'package:flutter/material.dart';

class PopupMenuWidget extends PopupMenuEntry {
  const PopupMenuWidget({
    Key? key,
    required this.height,
    required this.child,
  }) : super(key: key);
  final Widget child;

  @override
  final double height;

  @override
  PopupMenuWidgetState createState() => PopupMenuWidgetState();

  @override
  bool represents(T? value) => true;
}

class PopupMenuWidgetState extends State {
  @override
  Widget build(BuildContext context) => widget.child;
}

In the chat_screen, a GestureDetector is wrapped around the ChatListItem to detect long presses on the message. Now, let’s create a new list of reactions here.

onLongPress: () {
  RenderBox? overlay = Overlay.of(context)
      ?.context
      .findRenderObject() as RenderBox;
  
  //OUR CODE
  List<PopupMenuEntry> messageMenuItems = [
    PopupMenuWidget(
      height: 20,
      child: Row(
        mainAxisAlignment:
            MainAxisAlignment.spaceEvenly,
        children: [
          for (var reaction
              in REACTION_ID_MAP.entries)
            _reactWidget(
              reaction.value,
              () {
              //TODO: FIGURE OUT LATER.
              },
            ),
        ],
      ),
    ),
//SOME CODE HERE
}

Widget _reactWidget(
  String imagePath,
  VoidCallback onPressed,
) {
  return InkWell(
    onTap: onPressed,
    child: Ink.image(
      image: AssetImage(
        imagePath,
      ),
      height: 25,
      width: 25,
    ),
  );
}

Note: The assets used for this project can be found in the GitHub repository linked.

We will work on the TODO (listed in the code snippet above) in the next section and create a simple, yet attractive, popup menu with reactions that will look similar to this.

react messages

We need to create a minimal UI that will make it easy for both the sender and the receiver to know when a message has been reacted to.

return GestureDetector(
child: message is ReactionMessage
  ? Stack(
      children: [
        ChatListItem(
          Key(
            RandomUtil.getRandomString(
                10),
          ),
          message,
          _dialogType,
        ),
        Positioned(
          right: message.isIncoming
              ? null
              : 20,
          left: message.isIncoming
              ? 70
              : null,
          bottom: 0,
          child: message.reacts.isEmpty
              ? const SizedBox.shrink()
              : Container(
                  decoration:
                      BoxDecoration(
                    color: Colors.grey,
                    borderRadius:
                        const BorderRadius
                            .all(
                      Radius.circular(10),
                    ),
                    border: Border.all(
                      width: 3,
                      color: Colors.grey,
                      style: BorderStyle
                          .solid,
                    ),
                  ),
                  child: Row(
                    children: [
                      for (var reaction
                          in reactionCountMap
                              .entries)
                        Row(
                          children: [
                            Image.asset(
                              REACTION_ID_MAP[
                                  reaction
                                      .key]!,
                              height: 13,
                              width: 13,
                            ),
                            Text(
                              '${reaction.value} ',
                              style:
                                  const TextStyle(
                                fontSize:
                                    12.0,
                              ),
                            ),
                          ],
                        ),
                    ],
                  ),
                ),
        ),
      ],
    )
  : ChatListItem(
      Key(
        RandomUtil.getRandomString(10),
      ),
      message,
      _dialogType,
    ),
// SOME CODE HERE
);

If the message is a reaction message, the reactions will be displayed on the bottom right corner of the message. This will make the message appear as follows:

react-to-message

Although this looks great, we don’t yet know how many people reacted to it with what reaction. Let’s make a map to store the one-of-a-kind reactions and the count of reactions, and adjust the loop to use that map.

//Map to hold unique reactions and their count
var reactionCountMap = {};
if (message is ReactionMessage) {
var elements = message.reacts.values.toList();

//Populating the map
for (var x in elements) {
reactionCountMap[x] =
    !reactionCountMap.containsKey(x)
        ? (1)
        : (reactionCountMap[x]! + 1);
  }
}

return GestureDetector(
  child: Stack(
    children: [
      ChatListItem(
        Key(
          RandomUtil.getRandomString(10),
        ),
        message,
        _dialogType,
      ),
      if (message is ReactionMessage)
        Positioned(
          right:
              message.isIncoming ? null : 20,
          left:
              message.isIncoming ? 70 : null,
          bottom: 0,
          child: message.reacts.isEmpty
              ? const SizedBox.shrink()
              : Container(
                  decoration: BoxDecoration(
                    color: Colors.grey,
                    borderRadius:
                        const BorderRadius
                            .all(
                      Radius.circular(10),
                    ),
                    border: Border.all(
                      width: 3,
                      color: Colors.grey,
                      style:
                          BorderStyle.solid,
                    ),
                  ),

                  //Iterating over map and displaying reactions and counts.
                  child: Row(
                    children: [
                      for (var reaction
                          in reactionCountMap
                              .entries)
                        Row(
                          children: [
                            Image.asset(
                              REACTION_ID_MAP[
                                  reaction
                                      .key]!,
                              height: 13,
                              width: 13,
                            ),
                            Text(
                              '${reaction.value} ',
                              style:
                                  const TextStyle(
                                fontSize:
                                    12.0,
                              ),
                            ),
                          ],
                        ),
                    ],
                  ),
                ),
        ),
    ],
  ),
// SOME CODE HERE
);

hello reactions

Doesn’t that look even better? With this, we have finished the UI part.

Putting it all together

It’s finally time to integrate our UI with our logic. To make this as straightforward as possible, let’s jot down the steps we need to take.

  • When a user presses a reaction on a message, we must push an event to our BLoC in order to update the custom object and send the reaction message.
  • Additionally, we must make sure to retain the old reactions so that they are not replaced.
  • If a user double-reacts, the corresponding entry must be updated to reflect only the latest reaction.

In the GestureDetector, let’s figure out the TODO:

onLongPress: () {
  RenderBox? overlay = Overlay.of(context)
      ?.context
      .findRenderObject() as RenderBox;
  
  //OUR CODE
  List messageMenuItems = [
    PopupMenuWidget(
      height: 20,
      child: Row(
        mainAxisAlignment:
            MainAxisAlignment.spaceEvenly,
        children: [
          for (var reaction
              in REACTION_ID_MAP.entries)
            _reactWidget(
              reaction.value,
              () {
               //Add ReactMessageEvent to the BLoC with chosen reaction.
               //Pass the old reactions map to preserve them.
               bloc?.events?.add(
                ReactMessageEvent(
                  MessageActionReact(
                    chosenReactionId:
                        reaction.key,
                    currentUserId: message
                        .currentUserId
                        .toString(),
                    messageReactId: message
                            .qbMessage
                            .properties![
                        "messageReactId"]!,
                    reacts: (message
                            as ReactionMessage)
                        .reacts,
                  ),
                ),
              );
              //Dismiss the popup menu
              Navigator.of(context).pop();
              },
            ),
        ],
      ),
    ),

By pushing the reaction event to the BLoC, we are essentially informing the system that a reaction has been made. The rest of the process is handled by our logic part, which detects the event and calls the method from the chat_repository class to communicate with Quickblox servers.

Demo
Adding message reactions to chat

If you have followed all the steps correctly, you should now have the react-to-message functionality working. However, if you encounter any errors or missing pieces, you can always compare your code to the full source code available in our repository.

With this, we are finally finished. Well done!

What’s next

In this article, we focused on building the functionality of the basic reaction, rather than refining the UI/UX and focusing on small details.

Here are some ways it could be improved:

  • adding animations to the reactions (similar to the ones on Facebook)
  • displaying a list of users and their reactions when the reaction is tapped
  • highlighting the user’s previous reaction when they try to react again
  • allowing the user to remove their reaction.

What do you think? Add your ideas to the comments below.

Have Questions? Need Support?

Join the QuickBlox Developer Discord Community, where you can share ideas, learn about our software, & get support.

Join QuickBlox Discord

Leave a Comment

Your email address will not be published. Required fields are marked *

Read More

Ready to get started?

QUICKBLOX
QuickBlox post-box