Skip to content

Commit bb5c955

Browse files
author
chimnayajith
committed
reactions: Add sheet to view who reacted to a message
Fixes zulip#740
1 parent 3ff7096 commit bb5c955

11 files changed

+272
-3
lines changed

assets/icons/reactions.svg

+3
Loading

assets/l10n/app_en.arb

+4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@
104104
"@actionSheetOptionShare": {
105105
"description": "Label for share button on action sheet."
106106
},
107+
"actionSheetOptionViewReactions": "View Reactions",
108+
"@actionSheetOptionViewReactions": {
109+
"description": "Label for View Reactions button on action sheet."
110+
},
107111
"actionSheetOptionQuoteAndReply": "Quote and reply",
108112
"@actionSheetOptionQuoteAndReply": {
109113
"description": "Label for Quote and reply button on action sheet."

lib/generated/l10n/zulip_localizations.dart

+6
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,12 @@ abstract class ZulipLocalizations {
259259
/// **'Share'**
260260
String get actionSheetOptionShare;
261261

262+
/// Label for View Reactions button on action sheet.
263+
///
264+
/// In en, this message translates to:
265+
/// **'View Reactions'**
266+
String get actionSheetOptionViewReactions;
267+
262268
/// Label for Quote and reply button on action sheet.
263269
///
264270
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
8888
@override
8989
String get actionSheetOptionShare => 'Share';
9090

91+
@override
92+
String get actionSheetOptionViewReactions => 'View Reactions';
93+
9194
@override
9295
String get actionSheetOptionQuoteAndReply => 'Quote and reply';
9396

lib/generated/l10n/zulip_localizations_en.dart

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
8888
@override
8989
String get actionSheetOptionShare => 'Share';
9090

91+
@override
92+
String get actionSheetOptionViewReactions => 'View Reactions';
93+
9194
@override
9295
String get actionSheetOptionQuoteAndReply => 'Quote and reply';
9396

lib/generated/l10n/zulip_localizations_fr.dart

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
8888
@override
8989
String get actionSheetOptionShare => 'Share';
9090

91+
@override
92+
String get actionSheetOptionViewReactions => 'View Reactions';
93+
9194
@override
9295
String get actionSheetOptionQuoteAndReply => 'Quote and reply';
9396

lib/generated/l10n/zulip_localizations_ja.dart

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
8888
@override
8989
String get actionSheetOptionShare => 'Share';
9090

91+
@override
92+
String get actionSheetOptionViewReactions => 'View Reactions';
93+
9194
@override
9295
String get actionSheetOptionQuoteAndReply => 'Quote and reply';
9396

lib/generated/l10n/zulip_localizations_pl.dart

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
8888
@override
8989
String get actionSheetOptionShare => 'Udostępnij';
9090

91+
@override
92+
String get actionSheetOptionViewReactions => 'View Reactions';
93+
9194
@override
9295
String get actionSheetOptionQuoteAndReply => 'Odpowiedz cytując';
9396

lib/generated/l10n/zulip_localizations_ru.dart

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
8888
@override
8989
String get actionSheetOptionShare => 'Поделиться';
9090

91+
@override
92+
String get actionSheetOptionViewReactions => 'View Reactions';
93+
9194
@override
9295
String get actionSheetOptionQuoteAndReply => 'Ответить с цитированием';
9396

lib/widgets/action_sheet.dart

+20
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,8 @@ void showMessageActionSheet({required BuildContext context, required Message mes
389389

390390
final optionButtons = [
391391
ReactionButtons(message: message, pageContext: context),
392+
if((message.reactions?.total ?? 0) > 0)
393+
ViewReactionsButton(message: message, pageContext: context),
392394
StarButton(message: message, pageContext: context),
393395
if (isComposeBoxOffered)
394396
QuoteAndReplyButton(message: message, pageContext: context),
@@ -688,6 +690,24 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
688690
}
689691
}
690692

693+
class ViewReactionsButton extends MessageActionSheetMenuItemButton {
694+
ViewReactionsButton({super.key, required super.message, required super.pageContext});
695+
696+
@override IconData get icon => ZulipIcons.reactions;
697+
698+
@override
699+
String label(ZulipLocalizations zulipLocalizations) {
700+
return zulipLocalizations.actionSheetOptionViewReactions;
701+
}
702+
703+
@override void onPressed() async {
704+
showReactionListSheet(
705+
pageContext,
706+
reactionList: message.reactions?.aggregated,
707+
);
708+
}
709+
}
710+
691711
class MarkAsUnreadButton extends MessageActionSheetMenuItemButton {
692712
MarkAsUnreadButton({super.key, required super.message, required super.pageContext});
693713

lib/widgets/emoji_reaction.dart

+221-3
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import '../api/route/messages.dart';
66
import '../generated/l10n/zulip_localizations.dart';
77
import '../model/autocomplete.dart';
88
import '../model/emoji.dart';
9+
import '../model/store.dart';
910
import 'color.dart';
11+
import 'content.dart';
1012
import 'dialog.dart';
1113
import 'emoji.dart';
1214
import 'inset_shadow.dart';
15+
import 'profile.dart';
1316
import 'store.dart';
1417
import 'text.dart';
1518
import 'theme.dart';
@@ -127,23 +130,36 @@ class ReactionChipsList extends StatelessWidget {
127130
final showNames = displayEmojiReactionUsers && reactions.total <= 3;
128131

129132
return Wrap(spacing: 4, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center,
130-
children: reactions.aggregated.map((reactionVotes) => ReactionChip(
133+
children: reactions.aggregated.map((reactionVotes) {
134+
final index = reactions.aggregated.indexOf(reactionVotes);
135+
return ReactionChip(
131136
showName: showNames,
132-
messageId: messageId, reactionWithVotes: reactionVotes),
133-
).toList());
137+
messageId: messageId,
138+
reactionWithVotes: reactionVotes,
139+
onLongPress:(context){
140+
showReactionListSheet(
141+
context,
142+
reactionList: reactions.aggregated,
143+
initialTabIndex: index,
144+
);
145+
}
146+
);
147+
}).toList());
134148
}
135149
}
136150

137151
class ReactionChip extends StatelessWidget {
138152
final bool showName;
139153
final int messageId;
140154
final ReactionWithVotes reactionWithVotes;
155+
final void Function(BuildContext context)? onLongPress;
141156

142157
const ReactionChip({
143158
super.key,
144159
required this.showName,
145160
required this.messageId,
146161
required this.reactionWithVotes,
162+
this.onLongPress,
147163
});
148164

149165
@override
@@ -206,6 +222,11 @@ class ReactionChip extends StatelessWidget {
206222
customBorder: shape,
207223
splashColor: splashColor,
208224
highlightColor: highlightColor,
225+
onLongPress: (){
226+
if (onLongPress != null) {
227+
onLongPress!(context);
228+
}
229+
},
209230
onTap: () {
210231
(selfVoted ? removeReaction : addReaction).call(store.connection,
211232
messageId: messageId,
@@ -266,6 +287,203 @@ class ReactionChip extends StatelessWidget {
266287
}
267288
}
268289

290+
void showReactionListSheet(
291+
BuildContext context, {
292+
required List<ReactionWithVotes>? reactionList,
293+
int initialTabIndex = 0,
294+
}) {
295+
final store = PerAccountStoreWidget.of(context);
296+
297+
if (reactionList == null || reactionList.isEmpty) return;
298+
299+
showModalBottomSheet<void>(
300+
context: context,
301+
clipBehavior: Clip.antiAlias,
302+
useSafeArea: true,
303+
isScrollControlled: true,
304+
builder: (BuildContext modalContext) {
305+
return ConstrainedBox(
306+
constraints: BoxConstraints(
307+
maxHeight: MediaQuery.of(context).size.height * 0.7,
308+
),
309+
child: SafeArea(
310+
minimum: const EdgeInsets.only(bottom: 16),
311+
child: Padding(
312+
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
313+
child: Column(
314+
crossAxisAlignment: CrossAxisAlignment.stretch,
315+
mainAxisSize: MainAxisSize.min,
316+
children: [
317+
Flexible(
318+
child: InsetShadowBox(
319+
top: 8,
320+
bottom: 8,
321+
color: DesignVariables.of(context).bgContextMenu,
322+
child: PerAccountStoreWidget(
323+
accountId: store.accountId,
324+
child: ReactionListContent(
325+
store: store,
326+
reactionList: reactionList,
327+
initialTabIndex: initialTabIndex
328+
),
329+
),
330+
),
331+
),
332+
const ReactionSheetCloseButton(),
333+
],
334+
),
335+
),
336+
),
337+
);
338+
},
339+
);
340+
}
341+
class ReactionListContent extends StatelessWidget {
342+
final PerAccountStore store;
343+
final List<ReactionWithVotes> reactionList;
344+
final int initialTabIndex;
345+
346+
const ReactionListContent({
347+
super.key,
348+
required this.store,
349+
required this.reactionList,
350+
this.initialTabIndex = 0,
351+
});
352+
353+
@override
354+
Widget build(BuildContext context) {
355+
final designVariables = DesignVariables.of(context);
356+
357+
final tabs = reactionList.map((reaction) {
358+
final emojiDisplay = store.emojiDisplayFor(
359+
emojiType: reaction.reactionType,
360+
emojiCode: reaction.emojiCode,
361+
emojiName: reaction.emojiName,
362+
).resolve(store.userSettings);
363+
364+
final emoji = switch (emojiDisplay) {
365+
UnicodeEmojiDisplay() => _UnicodeEmoji(emojiDisplay: emojiDisplay),
366+
ImageEmojiDisplay() => _ImageEmoji(
367+
emojiDisplay: emojiDisplay,
368+
emojiName: reaction.emojiName,
369+
selected: reaction.userIds.contains(store.selfUserId),
370+
),
371+
TextEmojiDisplay() => _TextEmoji(
372+
emojiDisplay: emojiDisplay,
373+
selected: reaction.userIds.contains(store.selfUserId),
374+
),
375+
};
376+
377+
return Tab(
378+
child: Column(
379+
mainAxisSize: MainAxisSize.min,
380+
mainAxisAlignment: MainAxisAlignment.center,
381+
children: [
382+
emoji,
383+
const SizedBox(height: 4),
384+
Text(
385+
'${reaction.userIds.length}',
386+
style: const TextStyle()
387+
.merge(weightVariableTextStyle(context, wght: 600)),
388+
),
389+
],
390+
),
391+
);
392+
}).toList();
393+
394+
final tabViews = reactionList.map((reaction) {
395+
return ListView.builder(
396+
padding: EdgeInsets.zero,
397+
itemCount: reaction.userIds.length,
398+
itemBuilder: (context, index) {
399+
final userId = reaction.userIds.elementAt(index);
400+
return ListTile(
401+
leading: Avatar(userId: userId, size: 32.0, borderRadius: 3),
402+
title: Text(
403+
userId == store.selfUserId
404+
? 'You'
405+
: store.users[userId]?.fullName ?? '(unknown user)',
406+
style: TextStyle(
407+
color: designVariables.foreground.withFadedAlpha(0.80),
408+
fontSize: 17,
409+
).merge(weightVariableTextStyle(context, wght: 500)),
410+
),
411+
onTap: () {
412+
Navigator.push(
413+
context,
414+
ProfilePage.buildRoute(context: context, userId: userId),
415+
);
416+
},
417+
);
418+
},
419+
);
420+
}).toList();
421+
422+
return DefaultTabController(
423+
length: tabs.length,
424+
initialIndex: initialTabIndex,
425+
child: Column(
426+
mainAxisSize: MainAxisSize.min,
427+
children: [
428+
Padding(
429+
padding: const EdgeInsets.only(top: 16.0),
430+
child: TabBar(
431+
isScrollable: true,
432+
tabAlignment: TabAlignment.start,
433+
dividerColor: Colors.transparent,
434+
indicator: BoxDecoration(
435+
color: designVariables.background,
436+
borderRadius: BorderRadius.circular(10),
437+
border: Border.all(
438+
color: designVariables.foreground.withFadedAlpha(0.2),
439+
width:1
440+
)
441+
),
442+
splashFactory: NoSplash.splashFactory,
443+
indicatorSize: TabBarIndicatorSize.tab,
444+
labelColor: designVariables.foreground,
445+
unselectedLabelColor: designVariables.foreground,
446+
labelStyle: const TextStyle(fontSize: 14)
447+
.merge(weightVariableTextStyle(context, wght: 400)),
448+
unselectedLabelStyle: const TextStyle(fontSize: 14)
449+
.merge(weightVariableTextStyle(context, wght: 400)),
450+
tabs: tabs,
451+
),
452+
),
453+
const SizedBox(height: 8),
454+
Flexible(
455+
child: TabBarView(children: tabViews),
456+
),
457+
],
458+
),
459+
);
460+
}
461+
}
462+
class ReactionSheetCloseButton extends StatelessWidget {
463+
const ReactionSheetCloseButton({super.key});
464+
465+
@override
466+
Widget build(BuildContext context) {
467+
final designVariables = DesignVariables.of(context);
468+
return TextButton(
469+
style: TextButton.styleFrom(
470+
minimumSize: const Size.fromHeight(44),
471+
padding: const EdgeInsets.all(10),
472+
foregroundColor: designVariables.contextMenuCancelText,
473+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
474+
splashFactory: NoSplash.splashFactory,
475+
).copyWith(backgroundColor: WidgetStateColor.fromMap({
476+
WidgetState.pressed: designVariables.contextMenuCancelPressedBg,
477+
~WidgetState.pressed: designVariables.contextMenuCancelBg,
478+
})),
479+
onPressed: () {
480+
Navigator.pop(context);
481+
},
482+
child: Text(ZulipLocalizations.of(context).dialogClose,
483+
style: const TextStyle(fontSize: 20, height: 24 / 20)
484+
.merge(weightVariableTextStyle(context, wght: 600))));
485+
}
486+
}
269487
/// The size of a square emoji (Unicode or image).
270488
///
271489
/// Should be scaled by [_emojiTextScalerClamped].

0 commit comments

Comments
 (0)