1
1
import 'dart:async' ;
2
2
3
+ import 'package:collection/collection.dart' ;
3
4
import 'package:flutter/foundation.dart' ;
4
5
import 'package:flutter/material.dart' ;
5
6
import 'package:flutter/services.dart' ;
@@ -10,13 +11,16 @@ import '../api/model/model.dart';
10
11
import '../api/route/channels.dart' ;
11
12
import '../api/route/messages.dart' ;
12
13
import '../generated/l10n/zulip_localizations.dart' ;
14
+ import '../model/emoji.dart' ;
13
15
import '../model/internal_link.dart' ;
14
16
import '../model/narrow.dart' ;
15
17
import 'actions.dart' ;
16
18
import 'clipboard.dart' ;
17
19
import 'color.dart' ;
18
20
import 'compose_box.dart' ;
19
21
import 'dialog.dart' ;
22
+ import 'emoji.dart' ;
23
+ import 'emoji_reaction.dart' ;
20
24
import 'icons.dart' ;
21
25
import 'inset_shadow.dart' ;
22
26
import 'message_list.dart' ;
@@ -26,7 +30,7 @@ import 'theme.dart';
26
30
27
31
void _showActionSheet (
28
32
BuildContext context, {
29
- required List <ActionSheetMenuItemButton > optionButtons,
33
+ required List <Widget > optionButtons,
30
34
}) {
31
35
showModalBottomSheet <void >(
32
36
context: context,
@@ -383,16 +387,8 @@ void showMessageActionSheet({required BuildContext context, required Message mes
383
387
final markAsUnreadSupported = store.connection.zulipFeatureLevel! >= 155 ; // TODO(server-6)
384
388
final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead;
385
389
386
- final hasThumbsUpReactionVote = message.reactions
387
- ? .aggregated.any ((reactionWithVotes) =>
388
- reactionWithVotes.reactionType == ReactionType .unicodeEmoji
389
- && reactionWithVotes.emojiCode == '1f44d'
390
- && reactionWithVotes.userIds.contains (store.selfUserId))
391
- ?? false ;
392
-
393
390
final optionButtons = [
394
- if (! hasThumbsUpReactionVote)
395
- AddThumbsUpButton (message: message, pageContext: context),
391
+ ReactionButtons (message: message, pageContext: context),
396
392
StarButton (message: message, pageContext: context),
397
393
if (isComposeBoxOffered)
398
394
QuoteAndReplyButton (message: message, pageContext: context),
@@ -416,41 +412,94 @@ abstract class MessageActionSheetMenuItemButton extends ActionSheetMenuItemButto
416
412
final Message message;
417
413
}
418
414
419
- // This button is very temporary, to complete #125 before we have a way to
420
- // choose an arbitrary reaction (#388). So, skipping i18n.
421
- class AddThumbsUpButton extends MessageActionSheetMenuItemButton {
422
- AddThumbsUpButton ({super .key, required super .message, required super .pageContext});
415
+ class ReactionButtons extends StatelessWidget {
416
+ const ReactionButtons ({
417
+ super .key,
418
+ required this .message,
419
+ required this .pageContext,
420
+ });
423
421
424
- @override IconData get icon => ZulipIcons .smile ;
422
+ final Message message ;
425
423
426
- @override
427
- String label (ZulipLocalizations zulipLocalizations) {
428
- return 'React with 👍' ; // TODO(i18n) skip translation for now
424
+ /// A context within the [MessageListPage] this action sheet was
425
+ /// triggered from.
426
+ final BuildContext pageContext;
427
+
428
+ void _handleTapReaction ({
429
+ required EmojiCandidate emoji,
430
+ required bool isSelfVoted,
431
+ }) {
432
+ // Dismiss the enclosing action sheet immediately,
433
+ // for swift UI feedback that the user's selection was received.
434
+ Navigator .pop (pageContext);
435
+
436
+ final zulipLocalizations = ZulipLocalizations .of (pageContext);
437
+ doAddOrRemoveReaction (
438
+ context: pageContext,
439
+ doRemoveReaction: isSelfVoted,
440
+ messageId: message.id,
441
+ emoji: emoji,
442
+ errorDialogTitle: isSelfVoted
443
+ ? zulipLocalizations.errorReactionRemovingFailedTitle
444
+ : zulipLocalizations.errorReactionAddingFailedTitle);
429
445
}
430
446
431
- @override void onPressed () async {
432
- String ? errorMessage;
433
- try {
434
- await addReaction (PerAccountStoreWidget .of (pageContext).connection,
435
- messageId: message.id,
436
- reactionType: ReactionType .unicodeEmoji,
437
- emojiCode: '1f44d' ,
438
- emojiName: '+1' ,
439
- );
440
- } catch (e) {
441
- if (! pageContext.mounted) return ;
447
+ Widget _buildButton ({
448
+ required BuildContext context,
449
+ required EmojiCandidate emoji,
450
+ required bool isSelfVoted,
451
+ required bool isFirst,
452
+ }) {
453
+ final designVariables = DesignVariables .of (context);
454
+ return Flexible (child: InkWell (
455
+ onTap: () => _handleTapReaction (emoji: emoji, isSelfVoted: isSelfVoted),
456
+ splashFactory: NoSplash .splashFactory,
457
+ borderRadius: isFirst
458
+ ? const BorderRadius .only (topLeft: Radius .circular (7 ))
459
+ : null ,
460
+ overlayColor: WidgetStateColor .resolveWith ((states) =>
461
+ states.any ((e) => e == WidgetState .pressed)
462
+ ? designVariables.contextMenuItemBg.withFadedAlpha (0.20 )
463
+ : Colors .transparent),
464
+ child: Container (
465
+ width: double .infinity,
466
+ padding: const EdgeInsets .symmetric (vertical: 12 , horizontal: 5 ),
467
+ alignment: Alignment .center,
468
+ color: isSelfVoted
469
+ ? designVariables.contextMenuItemBg.withFadedAlpha (0.20 )
470
+ : null ,
471
+ child: UnicodeEmojiWidget (
472
+ emojiDisplay: emoji.emojiDisplay as UnicodeEmojiDisplay ,
473
+ notoColorEmojiTextSize: 20.1 ,
474
+ size: 24 ))));
475
+ }
442
476
443
- switch (e) {
444
- case ZulipApiException ():
445
- errorMessage = e.message;
446
- // TODO(#741) specific messages for common errors, like network errors
447
- // (support with reusable code)
448
- default :
449
- }
477
+ @override
478
+ Widget build (BuildContext context) {
479
+ assert (EmojiStore .popularEmojiCandidates.every (
480
+ (emoji) => emoji.emojiType == ReactionType .unicodeEmoji));
450
481
451
- showErrorDialog (context: pageContext,
452
- title: 'Adding reaction failed' , message: errorMessage);
482
+ final store = PerAccountStoreWidget .of (pageContext);
483
+ final designVariables = DesignVariables .of (context);
484
+
485
+ bool hasSelfVote (EmojiCandidate emoji) {
486
+ return message.reactions? .aggregated.any ((reactionWithVotes) {
487
+ return reactionWithVotes.reactionType == ReactionType .unicodeEmoji
488
+ && reactionWithVotes.emojiCode == emoji.emojiCode
489
+ && reactionWithVotes.userIds.contains (store.selfUserId);
490
+ }) ?? false ;
453
491
}
492
+
493
+ return Container (
494
+ decoration: BoxDecoration (
495
+ color: designVariables.contextMenuItemBg.withFadedAlpha (0.12 )),
496
+ child: Row (spacing: 1 , children: List .unmodifiable (
497
+ EmojiStore .popularEmojiCandidates.mapIndexed ((index, emoji) =>
498
+ _buildButton (
499
+ context: context,
500
+ emoji: emoji,
501
+ isSelfVoted: hasSelfVote (emoji),
502
+ isFirst: index == 0 )))));
454
503
}
455
504
}
456
505
0 commit comments