multiple reactions possible and quoted message size improved

This commit is contained in:
otsmr 2025-10-29 09:32:07 +01:00
parent 37790aa304
commit f2bd80c2dc
11 changed files with 161 additions and 110 deletions

View file

@ -4,6 +4,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
part 'reactions.dao.g.dart';
@ -18,21 +19,29 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
int contactId,
String messageId,
String groupId,
String? emoji,
String emoji,
bool remove,
) async {
if (!isEmoji(emoji)) {
Log.error('Did not update reaction as it is not an emoji!');
return;
}
final msg =
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
if (msg == null || msg.groupId != groupId) return;
try {
if (remove) {
await (delete(reactions)
..where(
(t) =>
t.senderId.equals(contactId) & t.messageId.equals(messageId),
t.senderId.equals(contactId) &
t.messageId.equals(messageId) &
t.emoji.equals(emoji),
))
.go();
if (emoji != null) {
await into(reactions).insert(
} else {
await into(reactions).insertOnConflictUpdate(
ReactionsCompanion(
messageId: Value(messageId),
emoji: Value(emoji),
@ -45,6 +54,42 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
}
}
Future<void> updateMyReaction(
String messageId,
String emoji,
bool remove,
) async {
if (!isEmoji(emoji)) {
Log.error('Did not update reaction as it is not an emoji!');
return;
}
final msg =
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
if (msg == null) return;
try {
await (delete(reactions)
..where(
(t) =>
t.senderId.isNull() &
t.messageId.equals(messageId) &
t.emoji.equals(emoji),
))
.go();
if (!remove) {
await into(reactions).insert(
ReactionsCompanion(
messageId: Value(messageId),
emoji: Value(emoji),
senderId: const Value(null),
),
);
}
} catch (e) {
Log.error(e);
}
}
Stream<List<Reaction>> watchReactions(String messageId) {
return (select(reactions)
..where((t) => t.messageId.equals(messageId))
@ -81,25 +126,4 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
.map((row) => (row.readTable(reactions), row.readTableOrNull(contacts)))
.watch();
}
Future<void> updateMyReaction(String messageId, String? emoji) async {
try {
await (delete(reactions)
..where(
(t) => t.senderId.isNull() & t.messageId.equals(messageId),
))
.go();
if (emoji != null) {
await into(reactions).insert(
ReactionsCompanion(
messageId: Value(messageId),
emoji: Value(emoji),
senderId: const Value(null),
),
);
}
} catch (e) {
Log.error(e);
}
}
}

View file

@ -143,12 +143,8 @@ const EncryptedContent_Reaction$json = {
'1': 'Reaction',
'2': [
{'1': 'targetMessageId', '3': 1, '4': 1, '5': 9, '10': 'targetMessageId'},
{'1': 'emoji', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'emoji', '17': true},
{'1': 'remove', '3': 3, '4': 1, '5': 8, '9': 1, '10': 'remove', '17': true},
],
'8': [
{'1': '_emoji'},
{'1': '_remove'},
{'1': 'emoji', '3': 2, '4': 1, '5': 9, '10': 'emoji'},
{'1': 'remove', '3': 3, '4': 1, '5': 8, '10': 'remove'},
],
};
@ -333,45 +329,44 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'50ZW50LlRleHRNZXNzYWdlSAtSC3RleHRNZXNzYWdliAEBGqkBCgtUZXh0TWVzc2FnZRIoCg9z'
'ZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZX'
'h0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJ'
'SABSDnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBqBAQoIUmVhY3Rpb24SKA'
'oPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSGQoFZW1vamkYAiABKAlI'
'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3'
'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu'
'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3'
'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMYAyADKAlSGG11'
'bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX'
'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ'
'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg'
'9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu'
'RW5jcnlwdGVkQ29udGVudC5NZWRpYS5UeXBlUgR0eXBlEkMKGmRpc3BsYXlMaW1pdEluTWlsbG'
'lzZWNvbmRzGAMgASgDSABSGmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjYKFnJlcXVp'
'cmVzQXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZX'
'N0YW1wGAUgASgDUgl0aW1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBiABKAlIAVIOcXVvdGVN'
'ZXNzYWdlSWSIAQESKQoNZG93bmxvYWRUb2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEi'
'kKDWVuY3J5cHRpb25LZXkYCCABKAxIA1INZW5jcnlwdGlvbktleYgBARIpCg1lbmNyeXB0aW9u'
'TWFjGAkgASgMSARSDWVuY3J5cHRpb25NYWOIAQESLQoPZW5jcnlwdGlvbk5vbmNlGAogASgMSA'
'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ'
'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX'
'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu'
'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR'
'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJn'
'ZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEA'
'ASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkK'
'BHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cG'
'UiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa8AEKDUNvbnRh'
'Y3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS'
'5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29t'
'cHJlc3NlZIgBARIlCgtkaXNwbGF5TmFtZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeX'
'BlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3NlZEIOCgxf'
'ZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW'
'50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5'
'GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBF'
'R5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVh'
'dGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlch'
'I2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdl'
'Eh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmRCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZW'
'N0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21l'
'ZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3'
'RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2Fn'
'ZQ==');
'SABSDnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBpiCghSZWFjdGlvbhIoCg'
'90YXJnZXRNZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIF'
'ZW1vamkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZR'
'gBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3Nl'
'bmRlck1lc3NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYX'
'JnZXRNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgE'
'IAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCg'
'oGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJ'
'ZEIHCgVfdGV4dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZX'
'NzYWdlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlw'
'ZRJDChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk'
'1pbGxpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJl'
'c0F1dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTW'
'Vzc2FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByAB'
'KAxIAlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cH'
'Rpb25LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0K'
'D2VuY3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCg'
'hSRVVQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxp'
'bWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQh'
'AKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2Ua'
'pwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVX'
'BkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdl'
'SWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1'
'IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5D'
'b250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVB'
'ABEgoKBkFDQ0VQVBACGvABCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0'
'ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2'
'VkGAIgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZWSIAQESJQoLZGlzcGxheU5hbWUYAyABKAlI'
'AVILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFgoUX2'
'F2YXRhclN2Z0NvbXByZXNzZWRCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBl'
'GAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGA'
'IgASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQg'
'ASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICg'
'Zfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GocBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3Vu'
'dGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1'
'IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5k'
'QgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdENoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3VudGVyQh'
'AKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRpYUIOCgxfbWVkaWFVcGRhdGVCEAoOX2NvbnRhY3RV'
'cGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0QgwKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZXlzQgsKCV'
'9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2U=');

View file

@ -54,8 +54,8 @@ message EncryptedContent {
message Reaction {
string targetMessageId = 1;
optional string emoji = 2;
optional bool remove = 3;
string emoji = 2;
bool remove = 3;
}
message MessageUpdate {

View file

@ -7,21 +7,16 @@ Future<void> handleReaction(
String groupId,
EncryptedContent_Reaction reaction,
) async {
Log.info('Got a reaction from $fromUserId');
if (reaction.hasRemove()) {
if (reaction.remove) {
await twonlyDB.reactionsDao
.updateReaction(fromUserId, reaction.targetMessageId, groupId, null);
return;
}
}
if (reaction.hasEmoji()) {
Log.info('Got a reaction from $fromUserId (remove=${reaction.remove})');
await twonlyDB.reactionsDao.updateReaction(
fromUserId,
reaction.targetMessageId,
groupId,
reaction.emoji,
reaction.remove,
);
if (!reaction.remove) {
await twonlyDB.groupsDao
.increaseLastMessageExchange(groupId, DateTime.now());
}

View file

@ -8,6 +8,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
class AllReactionsView extends StatefulWidget {
@ -47,14 +48,18 @@ class _AllReactionsViewState extends State<AllReactionsView> {
setState(() {});
}
Future<void> removeReaction() async {
await twonlyDB.reactionsDao
.updateMyReaction(widget.message.messageId, null);
Future<void> removeReaction(String emoji) async {
await twonlyDB.reactionsDao.updateMyReaction(
widget.message.messageId,
emoji,
true,
);
await sendCipherTextToGroup(
widget.message.groupId,
pb.EncryptedContent(
reaction: pb.EncryptedContent_Reaction(
targetMessageId: widget.message.messageId,
emoji: emoji,
remove: true,
),
),
@ -97,12 +102,17 @@ class _AllReactionsViewState extends State<AllReactionsView> {
child: ListView(
children: reactionsUsers.map((entry) {
return GestureDetector(
onTap: (entry.$2 != null) ? null : removeReaction,
onTap: (entry.$2 != null)
? null
: () {
removeReaction(entry.$1.emoji);
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 5,
horizontal: 30,
),
color: Colors.transparent,
margin: const EdgeInsets.only(left: 4),
child: Row(
children: [
@ -130,10 +140,30 @@ class _AllReactionsViewState extends State<AllReactionsView> {
],
),
),
Text(
if (EmojiAnimation.animatedIcons
.containsKey(entry.$1.emoji))
SizedBox(
height: 25,
child: EmojiAnimation(emoji: entry.$1.emoji),
)
else
SizedBox(
height: 24,
child: Center(
child: Text(
entry.$1.emoji,
style: const TextStyle(fontSize: 25),
style: const TextStyle(fontSize: 22),
strutStyle: const StrutStyle(
forceStrutHeight: true,
height: 1.6,
),
),
),
),
// Text(
// entry.$1.emoji,
// style: const TextStyle(fontSize: 25),
// ),
],
),
),

View file

@ -133,6 +133,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
group: widget.group,
mediaService: mediaService!,
galleryItems: widget.galleryItems,
minWidth: reactionsForWidth * 43,
),
),
if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10),

View file

@ -22,10 +22,12 @@ class ChatMediaEntry extends StatefulWidget {
required this.group,
required this.galleryItems,
required this.mediaService,
required this.minWidth,
super.key,
});
final Message message;
final double minWidth;
final Group group;
final List<MemoryItem> galleryItems;
final MediaFileService mediaService;
@ -117,7 +119,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
onDoubleTap: onDoubleTap,
onTap: (widget.message.type == MessageType.media) ? onTap : null,
child: SizedBox(
width: 150,
width: (widget.minWidth > 150) ? widget.minWidth : 150,
height: (widget.message.mediaStored &&
widget.mediaService.imagePreviewAvailable)
? 271

View file

@ -113,9 +113,10 @@ class ReactionRow extends StatelessWidget {
child: Text(
entry.$2.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
color: Colors.black,
color:
isDarkMode(context) ? Colors.white : Colors.black,
decoration: TextDecoration.none,
fontWeight: FontWeight.normal,
),

View file

@ -53,8 +53,7 @@ class ChatTextEntry extends StatelessWidget {
if (message.isDeletedFromSender) {
color = context.color.surfaceBright;
displayTime = false;
} else if (measureTextWidth(text) > 270 ||
message.quotesMessageId != null) {
} else if (measureTextWidth(text) > 270) {
expanded = true;
}

View file

@ -50,8 +50,11 @@ class MessageContextMenu extends StatelessWidget {
) as EmojiLayerData?;
if (layer == null) return;
await twonlyDB.reactionsDao
.updateMyReaction(message.messageId, layer.text);
await twonlyDB.reactionsDao.updateMyReaction(
message.messageId,
layer.text,
false,
);
await sendCipherTextToGroup(
message.groupId,

View file

@ -36,7 +36,7 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
child: GestureDetector(
onTap: () async {
await twonlyDB.reactionsDao
.updateMyReaction(widget.messageId, widget.emoji);
.updateMyReaction(widget.messageId, widget.emoji, false);
await sendCipherTextToGroup(
widget.groupId,
@ -44,6 +44,7 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
reaction: EncryptedContent_Reaction(
targetMessageId: widget.messageId,
emoji: widget.emoji,
remove: false,
),
),
null,