Skip to content

Commit

Permalink
reactions: Add sheet to view who reacted to a message
Browse files Browse the repository at this point in the history
Fixed zulip#740
  • Loading branch information
chimnayajith committed Dec 23, 2024
1 parent bf64923 commit 8808abe
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 3 deletions.
4 changes: 4 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@
"@actionSheetOptionShare": {
"description": "Label for share button on action sheet."
},
"actionSheetOptionViewReactions": "View Reactions",
"@actionSheetOptionViewReactions": {
"description": "Label for View Reactions button on action sheet."
},
"actionSheetOptionQuoteAndReply": "Quote and reply",
"@actionSheetOptionQuoteAndReply": {
"description": "Label for Quote and reply button on action sheet."
Expand Down
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ abstract class ZulipLocalizations {
/// **'Share'**
String get actionSheetOptionShare;

/// Label for View Reactions button on action sheet.
///
/// In en, this message translates to:
/// **'View Reactions'**
String get actionSheetOptionViewReactions;

/// Label for Quote and reply button on action sheet.
///
/// In en, this message translates to:
Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_fr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Udostępnij';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Odpowiedz cytując';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Поделиться';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Ответить с цитированием';

Expand Down
20 changes: 20 additions & 0 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ void showMessageActionSheet({required BuildContext context, required Message mes

final optionButtons = [
ReactionButtons(message: message, pageContext: context),
if((message.reactions?.total ?? 0) > 0)
ViewReactionsButton(message: message, pageContext: context),
StarButton(message: message, pageContext: context),
if (isComposeBoxOffered)
QuoteAndReplyButton(message: message, pageContext: context),
Expand Down Expand Up @@ -688,6 +690,24 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
}
}

class ViewReactionsButton extends MessageActionSheetMenuItemButton {
ViewReactionsButton({super.key, required super.message, required super.pageContext});

@override IconData get icon => ZulipIcons.smile;

@override
String label(ZulipLocalizations zulipLocalizations) {
return zulipLocalizations.actionSheetOptionViewReactions;
}

@override void onPressed() async {
showReactionListSheet(
pageContext,
reactionList: message.reactions?.aggregated,
);
}
}

class MarkAsUnreadButton extends MessageActionSheetMenuItemButton {
MarkAsUnreadButton({super.key, required super.message, required super.pageContext});

Expand Down
201 changes: 198 additions & 3 deletions lib/widgets/emoji_reaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import '../api/route/messages.dart';
import '../generated/l10n/zulip_localizations.dart';
import '../model/autocomplete.dart';
import '../model/emoji.dart';
import '../model/store.dart';
import 'action_sheet.dart';
import 'color.dart';
import 'content.dart';
import 'dialog.dart';
import 'emoji.dart';
import 'inset_shadow.dart';
import 'profile.dart';
import 'store.dart';
import 'text.dart';
import 'theme.dart';
Expand Down Expand Up @@ -127,23 +131,36 @@ class ReactionChipsList extends StatelessWidget {
final showNames = displayEmojiReactionUsers && reactions.total <= 3;

return Wrap(spacing: 4, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center,
children: reactions.aggregated.map((reactionVotes) => ReactionChip(
children: reactions.aggregated.map((reactionVotes) {
final index = reactions.aggregated.indexOf(reactionVotes);
return ReactionChip(
showName: showNames,
messageId: messageId, reactionWithVotes: reactionVotes),
).toList());
messageId: messageId,
reactionWithVotes: reactionVotes,
onLongPress:(context){
showReactionListSheet(
context,
reactionList: reactions.aggregated,
initialTabIndex: index,
);
}
);
}).toList());
}
}

class ReactionChip extends StatelessWidget {
final bool showName;
final int messageId;
final ReactionWithVotes reactionWithVotes;
final void Function(BuildContext context)? onLongPress;

const ReactionChip({
super.key,
required this.showName,
required this.messageId,
required this.reactionWithVotes,
this.onLongPress,
});

@override
Expand Down Expand Up @@ -206,6 +223,11 @@ class ReactionChip extends StatelessWidget {
customBorder: shape,
splashColor: splashColor,
highlightColor: highlightColor,
onLongPress: (){
if (onLongPress != null) {
onLongPress!(context);
}
},
onTap: () {
(selfVoted ? removeReaction : addReaction).call(store.connection,
messageId: messageId,
Expand Down Expand Up @@ -266,6 +288,179 @@ class ReactionChip extends StatelessWidget {
}
}

void showReactionListSheet(
BuildContext context, {
required List<ReactionWithVotes>? reactionList,
int initialTabIndex = 0,
}) {
final store = PerAccountStoreWidget.of(context);

if (reactionList == null || reactionList.isEmpty) return;

showModalBottomSheet<void>(
context: context,
clipBehavior: Clip.antiAlias,
useSafeArea: true,
isScrollControlled: true,
builder: (BuildContext modalContext) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
child: SafeArea(
minimum: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: InsetShadowBox(
top: 8,
bottom: 8,
color: DesignVariables.of(context).bgContextMenu,
child: PerAccountStoreWidget(
accountId: store.accountId,
child: ReactionListContent(
store: store,
reactionList: reactionList,
initialTabIndex: initialTabIndex
),
),
),
),
const ActionSheetCancelButton(),
],
),
),
),
);
},
);
}
class ReactionListContent extends StatelessWidget {
final PerAccountStore store;
final List<ReactionWithVotes> reactionList;
final int initialTabIndex;

const ReactionListContent({
super.key,
required this.store,
required this.reactionList,
this.initialTabIndex = 0,
});

@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);

final tabs = reactionList.map((reaction) {
final emojiDisplay = store.emojiDisplayFor(
emojiType: reaction.reactionType,
emojiCode: reaction.emojiCode,
emojiName: reaction.emojiName,
).resolve(store.userSettings);

final emoji = switch (emojiDisplay) {
UnicodeEmojiDisplay() => _UnicodeEmoji(emojiDisplay: emojiDisplay),
ImageEmojiDisplay() => _ImageEmoji(
emojiDisplay: emojiDisplay,
emojiName: reaction.emojiName,
selected: reaction.userIds.contains(store.selfUserId),
),
TextEmojiDisplay() => _TextEmoji(
emojiDisplay: emojiDisplay,
selected: reaction.userIds.contains(store.selfUserId),
),
};

return Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
emoji,
const SizedBox(width: 4),
Text(
'${reaction.userIds.length}',
style: const TextStyle()
.merge(weightVariableTextStyle(context, wght: 600)),
),
],
),
);
}).toList();

final tabViews = reactionList.map((reaction) {
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: reaction.userIds.length,
itemBuilder: (context, index) {
final userId = reaction.userIds.elementAt(index);
return Container(
decoration: BoxDecoration(
color: designVariables.contextMenuItemBg.withFadedAlpha(0.20),
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.symmetric(vertical: 2),
child: ListTile(
leading: Avatar(userId: userId, size: 36.0, borderRadius: 4),
title: Text(
userId == store.selfUserId
? 'You'
: store.users[userId]?.fullName ?? '(unknown user)',
style: TextStyle(
color: designVariables.contextMenuItemText,
fontSize: 18,
).merge(weightVariableTextStyle(context, wght: 600)),
),
onTap: () {
Navigator.push(
context,
ProfilePage.buildRoute(context: context, userId: userId),
);
},
),
);
},
);
}).toList();

return DefaultTabController(
length: tabs.length,
initialIndex: initialTabIndex,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 16.0), // Add space above the TabBar
child: TabBar(
isScrollable: true,
indicator: BoxDecoration(
color: designVariables.contextMenuItemBg.withFadedAlpha(0.1),
borderRadius: BorderRadius.circular(8),
),
splashFactory: NoSplash.splashFactory,
indicatorSize: TabBarIndicatorSize.tab,
labelColor: designVariables.contextMenuItemText,
unselectedLabelColor: designVariables.contextMenuItemText,
labelStyle: const TextStyle(fontSize: 14)
.merge(weightVariableTextStyle(context, wght: 700)),
unselectedLabelStyle: const TextStyle(fontSize: 14)
.merge(weightVariableTextStyle(context, wght: 400)),
tabs: tabs,
),
),
const SizedBox(height: 8), // Space between TabBar and TabBarView
Flexible(
child: TabBarView(children: tabViews),
),
],
),
);
}
}
/// The size of a square emoji (Unicode or image).
///
/// Should be scaled by [_emojiTextScalerClamped].
Expand Down

0 comments on commit 8808abe

Please sign in to comment.