Skip to content

Commit 8808abe

Browse files
author
chimnayajith
committed
reactions: Add sheet to view who reacted to a message
Fixed zulip#740
1 parent bf64923 commit 8808abe

10 files changed

+246
-3
lines changed

Diff for: 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."

Diff for: 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:

Diff for: 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

Diff for: 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

Diff for: 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

Diff for: 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

Diff for: 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

Diff for: 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

Diff for: 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.smile;
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

Diff for: lib/widgets/emoji_reaction.dart

+198-3
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ 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';
10+
import 'action_sheet.dart';
911
import 'color.dart';
12+
import 'content.dart';
1013
import 'dialog.dart';
1114
import 'emoji.dart';
1215
import 'inset_shadow.dart';
16+
import 'profile.dart';
1317
import 'store.dart';
1418
import 'text.dart';
1519
import 'theme.dart';
@@ -127,23 +131,36 @@ class ReactionChipsList extends StatelessWidget {
127131
final showNames = displayEmojiReactionUsers && reactions.total <= 3;
128132

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

137152
class ReactionChip extends StatelessWidget {
138153
final bool showName;
139154
final int messageId;
140155
final ReactionWithVotes reactionWithVotes;
156+
final void Function(BuildContext context)? onLongPress;
141157

142158
const ReactionChip({
143159
super.key,
144160
required this.showName,
145161
required this.messageId,
146162
required this.reactionWithVotes,
163+
this.onLongPress,
147164
});
148165

149166
@override
@@ -206,6 +223,11 @@ class ReactionChip extends StatelessWidget {
206223
customBorder: shape,
207224
splashColor: splashColor,
208225
highlightColor: highlightColor,
226+
onLongPress: (){
227+
if (onLongPress != null) {
228+
onLongPress!(context);
229+
}
230+
},
209231
onTap: () {
210232
(selfVoted ? removeReaction : addReaction).call(store.connection,
211233
messageId: messageId,
@@ -266,6 +288,179 @@ class ReactionChip extends StatelessWidget {
266288
}
267289
}
268290

291+
void showReactionListSheet(
292+
BuildContext context, {
293+
required List<ReactionWithVotes>? reactionList,
294+
int initialTabIndex = 0,
295+
}) {
296+
final store = PerAccountStoreWidget.of(context);
297+
298+
if (reactionList == null || reactionList.isEmpty) return;
299+
300+
showModalBottomSheet<void>(
301+
context: context,
302+
clipBehavior: Clip.antiAlias,
303+
useSafeArea: true,
304+
isScrollControlled: true,
305+
builder: (BuildContext modalContext) {
306+
return ConstrainedBox(
307+
constraints: BoxConstraints(
308+
maxHeight: MediaQuery.of(context).size.height * 0.7,
309+
),
310+
child: SafeArea(
311+
minimum: const EdgeInsets.only(bottom: 16),
312+
child: Padding(
313+
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
314+
child: Column(
315+
crossAxisAlignment: CrossAxisAlignment.stretch,
316+
mainAxisSize: MainAxisSize.min,
317+
children: [
318+
Flexible(
319+
child: InsetShadowBox(
320+
top: 8,
321+
bottom: 8,
322+
color: DesignVariables.of(context).bgContextMenu,
323+
child: PerAccountStoreWidget(
324+
accountId: store.accountId,
325+
child: ReactionListContent(
326+
store: store,
327+
reactionList: reactionList,
328+
initialTabIndex: initialTabIndex
329+
),
330+
),
331+
),
332+
),
333+
const ActionSheetCancelButton(),
334+
],
335+
),
336+
),
337+
),
338+
);
339+
},
340+
);
341+
}
342+
class ReactionListContent extends StatelessWidget {
343+
final PerAccountStore store;
344+
final List<ReactionWithVotes> reactionList;
345+
final int initialTabIndex;
346+
347+
const ReactionListContent({
348+
super.key,
349+
required this.store,
350+
required this.reactionList,
351+
this.initialTabIndex = 0,
352+
});
353+
354+
@override
355+
Widget build(BuildContext context) {
356+
final designVariables = DesignVariables.of(context);
357+
358+
final tabs = reactionList.map((reaction) {
359+
final emojiDisplay = store.emojiDisplayFor(
360+
emojiType: reaction.reactionType,
361+
emojiCode: reaction.emojiCode,
362+
emojiName: reaction.emojiName,
363+
).resolve(store.userSettings);
364+
365+
final emoji = switch (emojiDisplay) {
366+
UnicodeEmojiDisplay() => _UnicodeEmoji(emojiDisplay: emojiDisplay),
367+
ImageEmojiDisplay() => _ImageEmoji(
368+
emojiDisplay: emojiDisplay,
369+
emojiName: reaction.emojiName,
370+
selected: reaction.userIds.contains(store.selfUserId),
371+
),
372+
TextEmojiDisplay() => _TextEmoji(
373+
emojiDisplay: emojiDisplay,
374+
selected: reaction.userIds.contains(store.selfUserId),
375+
),
376+
};
377+
378+
return Tab(
379+
child: Row(
380+
mainAxisSize: MainAxisSize.min,
381+
mainAxisAlignment: MainAxisAlignment.center,
382+
children: [
383+
emoji,
384+
const SizedBox(width: 4),
385+
Text(
386+
'${reaction.userIds.length}',
387+
style: const TextStyle()
388+
.merge(weightVariableTextStyle(context, wght: 600)),
389+
),
390+
],
391+
),
392+
);
393+
}).toList();
394+
395+
final tabViews = reactionList.map((reaction) {
396+
return ListView.builder(
397+
padding: EdgeInsets.zero,
398+
itemCount: reaction.userIds.length,
399+
itemBuilder: (context, index) {
400+
final userId = reaction.userIds.elementAt(index);
401+
return Container(
402+
decoration: BoxDecoration(
403+
color: designVariables.contextMenuItemBg.withFadedAlpha(0.20),
404+
borderRadius: BorderRadius.circular(8),
405+
),
406+
margin: const EdgeInsets.symmetric(vertical: 2),
407+
child: ListTile(
408+
leading: Avatar(userId: userId, size: 36.0, borderRadius: 4),
409+
title: Text(
410+
userId == store.selfUserId
411+
? 'You'
412+
: store.users[userId]?.fullName ?? '(unknown user)',
413+
style: TextStyle(
414+
color: designVariables.contextMenuItemText,
415+
fontSize: 18,
416+
).merge(weightVariableTextStyle(context, wght: 600)),
417+
),
418+
onTap: () {
419+
Navigator.push(
420+
context,
421+
ProfilePage.buildRoute(context: context, userId: userId),
422+
);
423+
},
424+
),
425+
);
426+
},
427+
);
428+
}).toList();
429+
430+
return DefaultTabController(
431+
length: tabs.length,
432+
initialIndex: initialTabIndex,
433+
child: Column(
434+
mainAxisSize: MainAxisSize.min,
435+
children: [
436+
Padding(
437+
padding: const EdgeInsets.only(top: 16.0), // Add space above the TabBar
438+
child: TabBar(
439+
isScrollable: true,
440+
indicator: BoxDecoration(
441+
color: designVariables.contextMenuItemBg.withFadedAlpha(0.1),
442+
borderRadius: BorderRadius.circular(8),
443+
),
444+
splashFactory: NoSplash.splashFactory,
445+
indicatorSize: TabBarIndicatorSize.tab,
446+
labelColor: designVariables.contextMenuItemText,
447+
unselectedLabelColor: designVariables.contextMenuItemText,
448+
labelStyle: const TextStyle(fontSize: 14)
449+
.merge(weightVariableTextStyle(context, wght: 700)),
450+
unselectedLabelStyle: const TextStyle(fontSize: 14)
451+
.merge(weightVariableTextStyle(context, wght: 400)),
452+
tabs: tabs,
453+
),
454+
),
455+
const SizedBox(height: 8), // Space between TabBar and TabBarView
456+
Flexible(
457+
child: TabBarView(children: tabViews),
458+
),
459+
],
460+
),
461+
);
462+
}
463+
}
269464
/// The size of a square emoji (Unicode or image).
270465
///
271466
/// Should be scaled by [_emojiTextScalerClamped].

0 commit comments

Comments
 (0)