visit
This article is in the continuation of our series of articles, in which we are building cool chat features on top of
This section is all about setting up the template application on which we can start building the polls functionality. We have taken some code from the official
For simplicity of this article, We only have 3 screens. Splash, Login, and Chat screen. We have also hard-coded the _dialogId
on the Chat screen so that we are directly taken to the required Group chat.
The template application code can be found
Once you have the application credentials, You can paste them into a the main.dart
file, which is present in the template application.
If you have followed the above steps correctly, you can now run the application by using the following commands:
We have put our credentials in the keys.env
file and added them to the .gitignore
for security reasons, we recommend you do the same too.
flutter packages get
flutter run
QBMessage
is a default data object provided by Quickblox that stores id(message identifier), body(text message), properties(extra metadata) etc.
In this project, we have also created a QBMessageWrapper
which is a wrapper around the QBMessage with additional fields like senderName, date etc that come in handy to display the messages data in the chat screen.
While QBMessage
with its properties
parameter is great to render static text messages, location, weblink etc, it cannot be used to host interactive polls that update over time.
For this, Quickblox provides us with which is basically a custom schema key-value database that can be updated in realtime and hence is a perfect fit to host polls.
To set it up, let’s head over to our Quickblox Dashboard > Custom > Add > Add New Class and prepare a custom schema class Reaction as below.
Once created, open Edit Permission and change the permission level and checkboxes as follows.
MessageReactProperties
to hold the reacts
properties, which is a map of user ids of users and the reaction ids of the reactions which they reacted with on a message. we also have a toJson
method, which will return us a Map of this class which is required to create our Reaction
custom object.MessageActionReact
which holds messageReactId
, reacts
, currentUserId
, and chosenReactionId
. These properties will help us in updating our custom object. we also have a getter updatedReacts
that will recalculate
the reacts with the user-chosen option and returns the updated value.
import 'dart:convert';
class MessageReactProperties {
const MessageReactProperties({
required this.reacts,
});
final Map<String, String> reacts;
Map<String, String> 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<String, String> reacts;
final String chosenReactionId;
final String currentUserId;
Map<String, String> get updatedReacts {
reacts[currentUserId] = chosenReactionId;
return {"reacts": jsonEncode(reacts)};
}
}
Let’s create a ReactionMessage
class extending QBMessageWrapper
to hold the reaction-specific properties.
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<String, String> reacts;
factory ReactionMessage.fromCustomObject(String senderName, QBMessage message,
int currentUserId, QBCustomObject object) {
return ReactionMessage(
senderName,
message,
currentUserId,
messageReactId: message.properties!['messageReactId']!,
reacts: Map<String, String>.from(
jsonDecode(object.fields!['reacts'] as String),
),
);
}
ReactionMessage copyWith({Map<String, String>? 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",
};
Let’s create a roadmap of steps that are required for this section
reacts
.ReactionMessage
.
For the first point, let’s head over to _sendTextMessage
in chat_screen_bloc
. This method is called when we send a text message, we will modify it to take additional properties.
Future<void> _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
, let’s add it to sendMessage
function in chat_repository
and modify the method according to the second point on the roadmap.
Future<void> sendMessage(
String? dialogId,
String messageBody, {
Map<String, String>? properties,
required MessageReactProperties reactProperties,
}) async {
if (dialogId == null) {
throw RepositoryException(_parameterIsNullException,
affectedParams: ["dialogId"]);
}
//Create custom object to hold reactions for the message
final List<QBCustomObject?> 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 ??= <String, String>{};
properties['messageReactId'] = messageReactId;
//Send message
await QB.chat.sendMessage(
dialogId,
body: messageBody,
saveToHistory: true,
markable: true,
properties: properties,
);
}
Let’s create a ReactMessageEvent
in chat_screen_events
. we will push this event from our UI to the BLoC which will trigger the 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,
);
}
Now in chat_repository
, create the sendReactMessage
method and update the reactions in our custom object.
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
},
);
}
One thing to note here is, we are not saving this reaction message to history. This is because the only purpose of sendMessage
here is to notify the current clients that the reactions on a message has been updated.
Important note: saveToHistroy
should be used sensibly. Otherwise, for larger groups of more than 100 people, we can find ourselves paginating multiple times only to find a series of useless react messages.
To know more about params like markable
and saveToHistory
you can refer to the official
In the chat_screen_bloc
file, we have a HashSet <QBMessageWrapper
> _wrappedMessageSet
which stores all the messages sorted by time.
We also have a method _wrapMessages()
, which is called every time when we receive new messages and is responsible for wrapping the QBMesssage
(s) in the List<QBMessageWrappers
>. We will now update this method to handle the reactions.
messageReactId
. Fetch the corresponding reaction custom object for that id, add the reactions to the message and convert it into a ReactionMessage
object.action
as messageActionReact
, then we will 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<List<QBMessageWrapper>> _wrapMessages(
List<QBMessage?> messages) async {
List<QBMessageWrapper> wrappedMessages = [];
for (QBMessage? message in messages) {
if (message == null) {
break;
}
QBUser? sender = _getParticipantById(message.senderId);
if (sender == null && message.senderId != null) {
List<QBUser?> 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<String, String>.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;
}
Let’s start by building a way to long press on a message and have various reactions to react with. when we long press on a message, we already have a list of PopupMenuItem
, which contains options like Forward
, Delivered to
, etc.
Now, PopupMenuItem
is basically a widget that holds a child widget and a value to later identify it in the list, but we want to make it hold a list of reaction images placed horizontally. So let’s create our own widget for that by extending the base class of PopupMenuItem
i.e. PopupMenuEntry
.
import 'package:flutter/material.dart';
class PopupMenuWidget<T> extends PopupMenuEntry<T> {
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<PopupMenuWidget> {
@override
Widget build(BuildContext context) => widget.child;
}
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,
),
);
}
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
);
We are checking if the message is a reaction message, and showing the reactions on the bottom right corner of the message. With this our message with some reactions looks like this:
//Map to hold unique reactions and their count
var reactionCountMap = <String, int>{};
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
);
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,
() {
//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();
},
),
],
),
),
We are pushing the reaction event to the BLoC, which will basically inform the system that a reaction has been made. Rest is already handled by our logic part, where we are just detecting the event and calling the method from chat_repository
class to communicate with Quckblox servers.
With this, we are finally done and if you have followed all the steps correctly, you should have the reaction feature ready and working. In any case, if you see any errors or any left-out pieces, you can always match your code with the full source code available in our