Skip to content

Commit e217cc7

Browse files
action_sheet: Support reacting with popular emojis
1 parent 3ac750f commit e217cc7

9 files changed

+271
-58
lines changed

assets/l10n/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,5 +603,13 @@
603603
"errorNotificationOpenAccountMissing": "The account associated with this notification no longer exists.",
604604
"@errorNotificationOpenAccountMissing": {
605605
"description": "Error message when the account associated with the notification is not found"
606+
},
607+
"errorReactionAddingFailedTitle": "Adding reaction failed",
608+
"@errorReactionAddingFailedTitle": {
609+
"description": "Error title when adding a message reaction fails"
610+
},
611+
"errorReactionRemovingFailedTitle": "Removing reaction failed",
612+
"@errorReactionRemovingFailedTitle": {
613+
"description": "Error title when removing a message reaction fails"
606614
}
607615
}

lib/generated/l10n/zulip_localizations.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,18 @@ abstract class ZulipLocalizations {
894894
/// In en, this message translates to:
895895
/// **'The account associated with this notification no longer exists.'**
896896
String get errorNotificationOpenAccountMissing;
897+
898+
/// Error title when adding a message reaction fails
899+
///
900+
/// In en, this message translates to:
901+
/// **'Adding reaction failed'**
902+
String get errorReactionAddingFailedTitle;
903+
904+
/// Error title when removing a message reaction fails
905+
///
906+
/// In en, this message translates to:
907+
/// **'Removing reaction failed'**
908+
String get errorReactionRemovingFailedTitle;
897909
}
898910

899911
class _ZulipLocalizationsDelegate extends LocalizationsDelegate<ZulipLocalizations> {

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,4 +478,10 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
478478

479479
@override
480480
String get errorNotificationOpenAccountMissing => 'The account associated with this notification no longer exists.';
481+
482+
@override
483+
String get errorReactionAddingFailedTitle => 'Adding reaction failed';
484+
485+
@override
486+
String get errorReactionRemovingFailedTitle => 'Removing reaction failed';
481487
}

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,4 +478,10 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
478478

479479
@override
480480
String get errorNotificationOpenAccountMissing => 'The account associated with this notification no longer exists.';
481+
482+
@override
483+
String get errorReactionAddingFailedTitle => 'Adding reaction failed';
484+
485+
@override
486+
String get errorReactionRemovingFailedTitle => 'Removing reaction failed';
481487
}

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,4 +478,10 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
478478

479479
@override
480480
String get errorNotificationOpenAccountMissing => 'The account associated with this notification no longer exists.';
481+
482+
@override
483+
String get errorReactionAddingFailedTitle => 'Adding reaction failed';
484+
485+
@override
486+
String get errorReactionRemovingFailedTitle => 'Removing reaction failed';
481487
}

lib/model/emoji.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,60 @@ final class EmojiCandidate {
8888
}) : _aliases = aliases;
8989
}
9090

91+
// Zulip's hand selected "popular" emojis, currently used as list of
92+
// quick emoji reactions available in the message context menu.
93+
// See: https://github.com/zulip/zulip/blob/3bad36ef8cf07cd57d0b257a739bae635a8527ac/web/shared/src/typeahead.ts#L22-L29
94+
const zulipPopularEmojis = [
95+
EmojiCandidate(
96+
emojiType: ReactionType.unicodeEmoji,
97+
emojiCode: '1f44d',
98+
emojiName: '+1',
99+
aliases: ['thumbs_up', 'like'],
100+
emojiDisplay: UnicodeEmojiDisplay(
101+
emojiName: '+1',
102+
emojiUnicode: '\u{1f44d}')), // '👍'
103+
EmojiCandidate(
104+
emojiType: ReactionType.unicodeEmoji,
105+
emojiCode: '1f389',
106+
emojiName: 'tada',
107+
aliases: null,
108+
emojiDisplay: UnicodeEmojiDisplay(
109+
emojiName: 'tada',
110+
emojiUnicode: '\u{1f389}')), // '🎉'
111+
EmojiCandidate(
112+
emojiType: ReactionType.unicodeEmoji,
113+
emojiCode: '1f642',
114+
emojiName: 'smile',
115+
aliases: null,
116+
emojiDisplay: UnicodeEmojiDisplay(
117+
emojiName: 'smile',
118+
emojiUnicode: '\u{1f642}')), // '🙂'
119+
EmojiCandidate(
120+
emojiType: ReactionType.unicodeEmoji,
121+
emojiCode: '2764',
122+
emojiName: 'heart',
123+
aliases: ['love', 'love_you'],
124+
emojiDisplay: UnicodeEmojiDisplay(
125+
emojiName: 'heart',
126+
emojiUnicode: '\u{2764}')), // '❤'
127+
EmojiCandidate(
128+
emojiType: ReactionType.unicodeEmoji,
129+
emojiCode: '1f6e0',
130+
emojiName: 'working_on_it',
131+
aliases: ['hammer_and_wrench', 'tools'],
132+
emojiDisplay: UnicodeEmojiDisplay(
133+
emojiName: 'working_on_it',
134+
emojiUnicode: '\u{1f6e0}')), // '🛠'
135+
EmojiCandidate(
136+
emojiType: ReactionType.unicodeEmoji,
137+
emojiCode: '1f419',
138+
emojiName: 'octopus',
139+
aliases: null,
140+
emojiDisplay: UnicodeEmojiDisplay(
141+
emojiName: 'octopus',
142+
emojiUnicode: '\u{1f419}')), // '🐙'
143+
];
144+
91145
/// The portion of [PerAccountStore] describing what emoji exist.
92146
mixin EmojiStore {
93147
/// The realm's custom emoji (for [ReactionType.realmEmoji],

lib/widgets/action_sheet.dart

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import '../api/exception.dart';
99
import '../api/model/model.dart';
1010
import '../api/route/messages.dart';
1111
import '../generated/l10n/zulip_localizations.dart';
12+
import '../model/emoji.dart';
1213
import '../model/internal_link.dart';
1314
import '../model/narrow.dart';
1415
import 'actions.dart';
1516
import 'clipboard.dart';
1617
import 'color.dart';
1718
import 'compose_box.dart';
1819
import 'dialog.dart';
20+
import 'emoji.dart';
1921
import 'icons.dart';
2022
import 'inset_shadow.dart';
2123
import 'message_list.dart';
@@ -41,16 +43,8 @@ void showMessageActionSheet({required BuildContext context, required Message mes
4143
final markAsUnreadSupported = store.connection.zulipFeatureLevel! >= 155; // TODO(server-6)
4244
final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead;
4345

44-
final hasThumbsUpReactionVote = message.reactions
45-
?.aggregated.any((reactionWithVotes) =>
46-
reactionWithVotes.reactionType == ReactionType.unicodeEmoji
47-
&& reactionWithVotes.emojiCode == '1f44d'
48-
&& reactionWithVotes.userIds.contains(store.selfUserId))
49-
?? false;
50-
5146
final optionButtons = [
52-
if (!hasThumbsUpReactionVote)
53-
AddThumbsUpButton(message: message, pageContext: context),
47+
ReactionButtons(message: message, pageContext: context, popularEmojis: zulipPopularEmojis),
5448
StarButton(message: message, pageContext: context),
5549
if (isComposeBoxOffered)
5650
QuoteAndReplyButton(message: message, pageContext: context),
@@ -183,27 +177,39 @@ class MessageActionSheetCancelButton extends StatelessWidget {
183177
}
184178
}
185179

186-
// This button is very temporary, to complete #125 before we have a way to
187-
// choose an arbitrary reaction (#388). So, skipping i18n.
188-
class AddThumbsUpButton extends MessageActionSheetMenuItemButton {
189-
AddThumbsUpButton({super.key, required super.message, required super.pageContext});
180+
class ReactionButtons extends StatelessWidget {
181+
const ReactionButtons({
182+
super.key,
183+
required this.message,
184+
required this.pageContext,
185+
required this.popularEmojis,
186+
});
190187

191-
@override IconData get icon => ZulipIcons.smile;
188+
final Message message;
192189

193-
@override
194-
String label(ZulipLocalizations zulipLocalizations) {
195-
return 'React with 👍'; // TODO(i18n) skip translation for now
196-
}
190+
/// A context within the [MessageListPage] this action sheet was
191+
/// triggered from.
192+
final BuildContext pageContext;
197193

198-
@override void onPressed() async {
194+
/// List of popular emoji reaction buttons to display.
195+
/// Each emoji must be a unicode emoji.
196+
final List<EmojiCandidate> popularEmojis;
197+
198+
void _onPressed(
199+
EmojiCandidate emoji,
200+
bool selfVoted,
201+
ZulipLocalizations zulipLocalizations,
202+
) async {
199203
String? errorMessage;
200204
try {
201-
await addReaction(PerAccountStoreWidget.of(pageContext).connection,
205+
await (selfVoted ? removeReaction : addReaction).call(
206+
PerAccountStoreWidget.of(pageContext).connection,
202207
messageId: message.id,
203-
reactionType: ReactionType.unicodeEmoji,
204-
emojiCode: '1f44d',
205-
emojiName: '+1',
208+
reactionType: emoji.emojiType,
209+
emojiCode: emoji.emojiCode,
210+
emojiName: emoji.emojiName,
206211
);
212+
if (pageContext.mounted) Navigator.pop(pageContext);
207213
} catch (e) {
208214
if (!pageContext.mounted) return;
209215

@@ -216,9 +222,57 @@ class AddThumbsUpButton extends MessageActionSheetMenuItemButton {
216222
}
217223

218224
showErrorDialog(context: pageContext,
219-
title: 'Adding reaction failed', message: errorMessage);
225+
title: selfVoted
226+
? zulipLocalizations.errorReactionRemovingFailedTitle
227+
: zulipLocalizations.errorReactionAddingFailedTitle,
228+
message: errorMessage);
220229
}
221230
}
231+
232+
@override
233+
Widget build(BuildContext context) {
234+
assert(popularEmojis.every(
235+
(emoji) => emoji.emojiType == ReactionType.unicodeEmoji));
236+
237+
final zulipLocalizations = ZulipLocalizations.of(context);
238+
final store = PerAccountStoreWidget.of(pageContext);
239+
final designVariables = DesignVariables.of(context);
240+
241+
bool hasSelfVote(EmojiCandidate emoji) {
242+
return message.reactions?.aggregated.any((reactionWithVotes) {
243+
return reactionWithVotes.reactionType == ReactionType.unicodeEmoji
244+
&& reactionWithVotes.emojiCode == emoji.emojiCode
245+
&& reactionWithVotes.userIds.contains(store.selfUserId);
246+
}) ?? false;
247+
}
248+
249+
return Container(
250+
padding: const EdgeInsets.all(8),
251+
decoration: BoxDecoration(color: designVariables.contextMenuItemBg.withFadedAlpha(0.12)),
252+
child: Row(
253+
mainAxisAlignment: MainAxisAlignment.spaceAround,
254+
children: List.unmodifiable(popularEmojis.map((emoji) {
255+
final selfVoted = hasSelfVote(emoji);
256+
return IconButton(
257+
onPressed: () => _onPressed(emoji, selfVoted, zulipLocalizations),
258+
isSelected: selfVoted,
259+
style: IconButton.styleFrom(
260+
padding: EdgeInsets.zero,
261+
splashFactory: NoSplash.splashFactory,
262+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3.5)),
263+
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
264+
visualDensity: VisualDensity.compact,
265+
).copyWith(backgroundColor: WidgetStateColor.resolveWith((states) =>
266+
states.any((e) => e == WidgetState.pressed || e == WidgetState.selected)
267+
? designVariables.contextMenuItemBg.withFadedAlpha(0.20)
268+
: Colors.transparent)),
269+
icon: UnicodeEmojiWidget(
270+
emojiDisplay: emoji.emojiDisplay as UnicodeEmojiDisplay,
271+
notoColorEmojiTextSize: 20.1,
272+
size: 24));
273+
})))
274+
);
275+
}
222276
}
223277

224278
class StarButton extends MessageActionSheetMenuItemButton {

test/flutter_checks.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,7 @@ extension TableRowChecks on Subject<TableRow> {
158158
extension TableChecks on Subject<Table> {
159159
Subject<List<TableRow>> get children => has((x) => x.children, 'children');
160160
}
161+
162+
extension IconButtonChecks on Subject<IconButton> {
163+
Subject<bool?> get isSelected => has((x) => x.isSelected, 'isSelected');
164+
}

0 commit comments

Comments
 (0)