11import 'dart:async' ;
22
3+ import 'package:collection/collection.dart' ;
34import 'package:flutter/foundation.dart' ;
45import 'package:flutter/material.dart' ;
56import 'package:flutter/services.dart' ;
@@ -9,13 +10,16 @@ import '../api/exception.dart';
910import '../api/model/model.dart' ;
1011import '../api/route/messages.dart' ;
1112import '../generated/l10n/zulip_localizations.dart' ;
13+ import '../model/emoji.dart' ;
1214import '../model/internal_link.dart' ;
1315import '../model/narrow.dart' ;
1416import 'actions.dart' ;
1517import 'clipboard.dart' ;
1618import 'color.dart' ;
1719import 'compose_box.dart' ;
1820import 'dialog.dart' ;
21+ import 'emoji.dart' ;
22+ import 'emoji_reaction.dart' ;
1923import 'icons.dart' ;
2024import 'inset_shadow.dart' ;
2125import 'message_list.dart' ;
@@ -25,7 +29,7 @@ import 'theme.dart';
2529
2630void _showActionSheet (
2731 BuildContext context, {
28- required List <ActionSheetMenuItemButton > optionButtons,
32+ required List <Widget > optionButtons,
2933}) {
3034 showModalBottomSheet <void >(
3135 context: context,
@@ -161,16 +165,8 @@ void showMessageActionSheet({required BuildContext context, required Message mes
161165 final markAsUnreadSupported = store.connection.zulipFeatureLevel! >= 155 ; // TODO(server-6)
162166 final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead;
163167
164- final hasThumbsUpReactionVote = message.reactions
165- ? .aggregated.any ((reactionWithVotes) =>
166- reactionWithVotes.reactionType == ReactionType .unicodeEmoji
167- && reactionWithVotes.emojiCode == '1f44d'
168- && reactionWithVotes.userIds.contains (store.selfUserId))
169- ?? false ;
170-
171168 final optionButtons = [
172- if (! hasThumbsUpReactionVote)
173- AddThumbsUpButton (message: message, pageContext: context),
169+ ReactionButtons (message: message, pageContext: context),
174170 StarButton (message: message, pageContext: context),
175171 if (isComposeBoxOffered)
176172 QuoteAndReplyButton (message: message, pageContext: context),
@@ -194,41 +190,94 @@ abstract class MessageActionSheetMenuItemButton extends ActionSheetMenuItemButto
194190 final Message message;
195191}
196192
197- // This button is very temporary, to complete #125 before we have a way to
198- // choose an arbitrary reaction (#388). So, skipping i18n.
199- class AddThumbsUpButton extends MessageActionSheetMenuItemButton {
200- AddThumbsUpButton ({super .key, required super .message, required super .pageContext});
193+ class ReactionButtons extends StatelessWidget {
194+ const ReactionButtons ({
195+ super .key,
196+ required this .message,
197+ required this .pageContext,
198+ });
201199
202- @override IconData get icon => ZulipIcons .smile ;
200+ final Message message ;
203201
204- @override
205- String label (ZulipLocalizations zulipLocalizations) {
206- return 'React with 👍' ; // TODO(i18n) skip translation for now
202+ /// A context within the [MessageListPage] this action sheet was
203+ /// triggered from.
204+ final BuildContext pageContext;
205+
206+ void _handleTapReaction ({
207+ required EmojiCandidate emoji,
208+ required bool isSelfVoted,
209+ }) {
210+ // Dismiss the enclosing action sheet immediately,
211+ // for swift UI feedback that the user's selection was received.
212+ Navigator .pop (pageContext);
213+
214+ final zulipLocalizations = ZulipLocalizations .of (pageContext);
215+ doAddOrRemoveReaction (
216+ context: pageContext,
217+ doRemoveReaction: isSelfVoted,
218+ messageId: message.id,
219+ emoji: emoji,
220+ errorDialogTitle: isSelfVoted
221+ ? zulipLocalizations.errorReactionRemovingFailedTitle
222+ : zulipLocalizations.errorReactionAddingFailedTitle);
207223 }
208224
209- @override void onPressed () async {
210- String ? errorMessage;
211- try {
212- await addReaction (PerAccountStoreWidget .of (pageContext).connection,
213- messageId: message.id,
214- reactionType: ReactionType .unicodeEmoji,
215- emojiCode: '1f44d' ,
216- emojiName: '+1' ,
217- );
218- } catch (e) {
219- if (! pageContext.mounted) return ;
225+ Widget _buildButton ({
226+ required BuildContext context,
227+ required EmojiCandidate emoji,
228+ required bool isSelfVoted,
229+ required bool isFirst,
230+ }) {
231+ final designVariables = DesignVariables .of (context);
232+ return Flexible (child: InkWell (
233+ onTap: () => _handleTapReaction (emoji: emoji, isSelfVoted: isSelfVoted),
234+ splashFactory: NoSplash .splashFactory,
235+ borderRadius: isFirst
236+ ? const BorderRadius .only (topLeft: Radius .circular (7 ))
237+ : null ,
238+ overlayColor: WidgetStateColor .resolveWith ((states) =>
239+ states.any ((e) => e == WidgetState .pressed)
240+ ? designVariables.contextMenuItemBg.withFadedAlpha (0.20 )
241+ : Colors .transparent),
242+ child: Container (
243+ width: double .infinity,
244+ padding: const EdgeInsets .symmetric (vertical: 12 , horizontal: 5 ),
245+ alignment: Alignment .center,
246+ color: isSelfVoted
247+ ? designVariables.contextMenuItemBg.withFadedAlpha (0.20 )
248+ : null ,
249+ child: UnicodeEmojiWidget (
250+ emojiDisplay: emoji.emojiDisplay as UnicodeEmojiDisplay ,
251+ notoColorEmojiTextSize: 20.1 ,
252+ size: 24 ))));
253+ }
220254
221- switch (e) {
222- case ZulipApiException ():
223- errorMessage = e.message;
224- // TODO(#741) specific messages for common errors, like network errors
225- // (support with reusable code)
226- default :
227- }
255+ @override
256+ Widget build (BuildContext context) {
257+ assert (EmojiStore .popularEmojiCandidates.every (
258+ (emoji) => emoji.emojiType == ReactionType .unicodeEmoji));
228259
229- showErrorDialog (context: pageContext,
230- title: 'Adding reaction failed' , message: errorMessage);
260+ final store = PerAccountStoreWidget .of (pageContext);
261+ final designVariables = DesignVariables .of (context);
262+
263+ bool hasSelfVote (EmojiCandidate emoji) {
264+ return message.reactions? .aggregated.any ((reactionWithVotes) {
265+ return reactionWithVotes.reactionType == ReactionType .unicodeEmoji
266+ && reactionWithVotes.emojiCode == emoji.emojiCode
267+ && reactionWithVotes.userIds.contains (store.selfUserId);
268+ }) ?? false ;
231269 }
270+
271+ return Container (
272+ decoration: BoxDecoration (
273+ color: designVariables.contextMenuItemBg.withFadedAlpha (0.12 )),
274+ child: Row (spacing: 1 , children: List .unmodifiable (
275+ EmojiStore .popularEmojiCandidates.mapIndexed ((index, emoji) =>
276+ _buildButton (
277+ context: context,
278+ emoji: emoji,
279+ isSelfVoted: hasSelfVote (emoji),
280+ isFirst: index == 0 )))));
232281 }
233282}
234283
0 commit comments