diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf
index 15fce5b25a..8c1ec23db8 100644
Binary files a/assets/icons/ZulipIcons.ttf and b/assets/icons/ZulipIcons.ttf differ
diff --git a/assets/icons/reactions.svg b/assets/icons/reactions.svg
new file mode 100644
index 0000000000..78c2a48063
--- /dev/null
+++ b/assets/icons/reactions.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb
index 1060027553..4e83b1fb3e 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -104,6 +104,10 @@
"@actionSheetOptionShare": {
"description": "Label for share button on action sheet."
},
+ "actionSheetOptionViewReactions": "See who reacted",
+ "@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."
@@ -116,6 +120,10 @@
"@actionSheetOptionUnstarMessage": {
"description": "Label for unstar button on action sheet."
},
+ "reactionSheetEmptyReactions": "No reactions yet",
+ "@reactionSheetEmptyReactions": {
+ "description": "Text to display when the reactions sheet is open, but there are no reactions to show."
+ },
"errorWebAuthOperationalErrorTitle": "Something went wrong",
"@errorWebAuthOperationalErrorTitle": {
"description": "Error title when third-party authentication has an operational error (not necessarily caused by invalid credentials)."
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index 501eb577bf..9adcff63e5 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -261,6 +261,12 @@ abstract class ZulipLocalizations {
/// **'Share'**
String get actionSheetOptionShare;
+ /// Label for View Reactions button on action sheet.
+ ///
+ /// In en, this message translates to:
+ /// **'See who reacted'**
+ String get actionSheetOptionViewReactions;
+
/// Label for Quote and reply button on action sheet.
///
/// In en, this message translates to:
@@ -279,6 +285,12 @@ abstract class ZulipLocalizations {
/// **'Unstar message'**
String get actionSheetOptionUnstarMessage;
+ /// Text to display when the reactions sheet is open, but there are no reactions to show.
+ ///
+ /// In en, this message translates to:
+ /// **'No reactions yet'**
+ String get reactionSheetEmptyReactions;
+
/// Error title when third-party authentication has an operational error (not necessarily caused by invalid credentials).
///
/// In en, this message translates to:
diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart
index 721b20ac02..d4ddbc7716 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -88,6 +88,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';
+ @override
+ String get actionSheetOptionViewReactions => 'See who reacted';
+
@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';
@@ -97,6 +100,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Unstar message';
+ @override
+ String get reactionSheetEmptyReactions => 'No reactions yet';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Something went wrong';
diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart
index 6936cfe736..f38037c9b9 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -88,6 +88,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';
+ @override
+ String get actionSheetOptionViewReactions => 'See who reacted';
+
@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';
@@ -97,6 +100,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Unstar message';
+ @override
+ String get reactionSheetEmptyReactions => 'No reactions yet';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Something went wrong';
diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart
index c431471645..aa77a37c82 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -88,6 +88,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';
+ @override
+ String get actionSheetOptionViewReactions => 'See who reacted';
+
@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';
@@ -97,6 +100,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Unstar message';
+ @override
+ String get reactionSheetEmptyReactions => 'No reactions yet';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Something went wrong';
diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart
index fc530fccaa..9b0d078a03 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -88,6 +88,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';
+ @override
+ String get actionSheetOptionViewReactions => 'See who reacted';
+
@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';
@@ -97,6 +100,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Unstar message';
+ @override
+ String get reactionSheetEmptyReactions => 'No reactions yet';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Something went wrong';
diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart
index f817d400a8..648b2cd1a6 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -88,6 +88,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Udostępnij';
+ @override
+ String get actionSheetOptionViewReactions => 'See who reacted';
+
@override
String get actionSheetOptionQuoteAndReply => 'Odpowiedz cytując';
@@ -97,6 +100,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Odbierz gwiazdkę';
+ @override
+ String get reactionSheetEmptyReactions => 'No reactions yet';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Coś poszło nie tak';
diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart
index f6d8f1e41c..8f2a454edb 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -88,6 +88,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Поделиться';
+ @override
+ String get actionSheetOptionViewReactions => 'See who reacted';
+
@override
String get actionSheetOptionQuoteAndReply => 'Ответить с цитированием';
@@ -97,6 +100,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Снять отметку с сообщения';
+ @override
+ String get reactionSheetEmptyReactions => 'No reactions yet';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Что-то пошло не так';
diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart
index d6e04126d3..7057453786 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -88,6 +88,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Zdielať';
+ @override
+ String get actionSheetOptionViewReactions => 'See who reacted';
+
@override
String get actionSheetOptionQuoteAndReply => 'Citovať a odpovedať';
@@ -97,6 +100,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
@override
String get actionSheetOptionUnstarMessage => 'Odhviezdičkovať správu';
+ @override
+ String get reactionSheetEmptyReactions => 'No reactions yet';
+
@override
String get errorWebAuthOperationalErrorTitle => 'Niečo sa pokazilo';
diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart
index 6a90b61e64..588882176c 100644
--- a/lib/widgets/action_sheet.dart
+++ b/lib/widgets/action_sheet.dart
@@ -392,6 +392,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),
@@ -692,6 +694,25 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
}
}
+class ViewReactionsButton extends MessageActionSheetMenuItemButton {
+ ViewReactionsButton({super.key, required super.message, required super.pageContext});
+
+ @override IconData get icon => ZulipIcons.reactions;
+
+ @override
+ String label(ZulipLocalizations zulipLocalizations) {
+ return zulipLocalizations.actionSheetOptionViewReactions;
+ }
+
+ @override void onPressed() async {
+ showReactionListSheet(
+ pageContext,
+ messageId: message.id,
+ messageListView: MessageListPage.ancestorOf(pageContext).model,
+ );
+ }
+}
+
class MarkAsUnreadButton extends MessageActionSheetMenuItemButton {
MarkAsUnreadButton({super.key, required super.message, required super.pageContext});
diff --git a/lib/widgets/emoji_reaction.dart b/lib/widgets/emoji_reaction.dart
index 3d69e3d0d1..a25ea70725 100644
--- a/lib/widgets/emoji_reaction.dart
+++ b/lib/widgets/emoji_reaction.dart
@@ -6,10 +6,15 @@ import '../api/route/messages.dart';
import '../generated/l10n/zulip_localizations.dart';
import '../model/autocomplete.dart';
import '../model/emoji.dart';
+import '../model/message_list.dart';
+import '../model/store.dart';
import 'color.dart';
+import 'content.dart';
import 'dialog.dart';
import 'emoji.dart';
import 'inset_shadow.dart';
+import 'message_list.dart';
+import 'profile.dart';
import 'store.dart';
import 'text.dart';
import 'theme.dart';
@@ -125,10 +130,14 @@ 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(
- showName: showNames,
- messageId: messageId, reactionWithVotes: reactionVotes),
- ).toList());
+ children: reactions.aggregated.map((reactionVotes) {
+ return ReactionChip(
+ showName: showNames,
+ messageId: messageId,
+ reactionWithVotes: reactionVotes,
+ );
+ }).toList()
+ );
}
}
@@ -204,6 +213,14 @@ class ReactionChip extends StatelessWidget {
customBorder: shape,
splashColor: splashColor,
highlightColor: highlightColor,
+ onLongPress: (){
+ showReactionListSheet(
+ context,
+ messageId: messageId,
+ messageListView: MessageListPage.ancestorOf(context).model,
+ initialTab: reactionWithVotes,
+ );
+ },
onTap: () {
(selfVoted ? removeReaction : addReaction).call(store.connection,
messageId: messageId,
@@ -264,6 +281,269 @@ class ReactionChip extends StatelessWidget {
}
}
+void showReactionListSheet(
+ BuildContext context, {
+ required int messageId,
+ required MessageListView? messageListView,
+ ReactionWithVotes? initialTab,
+}) {
+ final store = PerAccountStoreWidget.of(context);
+
+ showModalBottomSheet(
+ 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,
+ messageId: messageId,
+ initialTab: initialTab,
+ messageListView: messageListView,
+ ),
+ ),
+ ),
+ ),
+ const ReactionSheetCloseButton(),
+ ],
+ ),
+ ),
+ ),
+ );
+ },
+ );
+}
+
+class ReactionListContent extends StatefulWidget {
+ final PerAccountStore store;
+ final int messageId;
+ final ReactionWithVotes? initialTab;
+ final MessageListView? messageListView;
+
+ const ReactionListContent({
+ super.key,
+ required this.store,
+ required this.messageListView,
+ required this.messageId,
+ this.initialTab,
+ });
+
+ @override
+ State createState() => _ReactionListContentState();
+}
+class _ReactionListContentState extends State {
+ late MessageListView? model;
+ List reactionList = [];
+ bool isLoading = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _initModel();
+ }
+
+ void _initModel() {
+ model = widget.messageListView;
+ model!.addListener(_onMessageListChanged);
+ _updateReactionList();
+ }
+
+ void _onMessageListChanged() {
+ _updateReactionList();
+ }
+
+ void _updateReactionList() {
+ setState(() {
+ reactionList = widget.store.messages[widget.messageId]?.reactions?.aggregated
+ .where((reaction) => reaction.userIds.isNotEmpty)
+ .toList() ??
+ [];
+ isLoading = false;
+ });
+ }
+
+ @override
+ void dispose() {
+ if (model != null) {
+ model!.removeListener(_onMessageListChanged);
+ }
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final designVariables = DesignVariables.of(context);
+ final zulipLocalizations = ZulipLocalizations.of(context);
+
+ if (isLoading) {
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+
+ if (reactionList.isEmpty) {
+ return Center(
+ child: Text(
+ zulipLocalizations.reactionSheetEmptyReactions,
+ style: TextStyle(
+ color: designVariables.foreground.withFadedAlpha(0.6),
+ fontSize: 16,
+ ).merge(weightVariableTextStyle(context, wght: 500)),
+ ),
+ );
+ }
+
+ final tabs = reactionList.map((reaction) {
+ final emojiDisplay = widget.store.emojiDisplayFor(
+ emojiType: reaction.reactionType,
+ emojiCode: reaction.emojiCode,
+ emojiName: reaction.emojiName,
+ ).resolve(widget.store.userSettings);
+
+ final emoji = switch (emojiDisplay) {
+ UnicodeEmojiDisplay() => _UnicodeEmoji(emojiDisplay: emojiDisplay),
+ ImageEmojiDisplay() => _ImageEmoji(
+ emojiDisplay: emojiDisplay,
+ emojiName: reaction.emojiName,
+ selected: reaction.userIds.contains(widget.store.selfUserId),
+ ),
+ TextEmojiDisplay() => _TextEmoji(
+ emojiDisplay: emojiDisplay,
+ selected: reaction.userIds.contains(widget.store.selfUserId),
+ ),
+ };
+
+ return Tab(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ emoji,
+ const SizedBox(height: 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 ListTile(
+ leading: Avatar(userId: userId, size: 32.0, borderRadius: 3),
+ title: Text(
+ userId == widget.store.selfUserId
+ ? 'You'
+ : widget.store.users[userId]?.fullName ?? zulipLocalizations.unknownUserName,
+ style: TextStyle(
+ color: designVariables.foreground.withFadedAlpha(0.80),
+ fontSize: 17,
+ ).merge(weightVariableTextStyle(context, wght: 500)),
+ ),
+ onTap: () {
+ Navigator.push(
+ context,
+ ProfilePage.buildRoute(context: context, userId: userId),
+ );
+ },
+ );
+ },
+ );
+ }).toList();
+
+ return DefaultTabController(
+ length: tabs.length,
+ initialIndex: widget.initialTab != null
+ ? reactionList.indexOf(widget.initialTab as ReactionWithVotes)
+ : 0,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 16.0),
+ child: TabBar(
+ isScrollable: true,
+ tabAlignment: TabAlignment.start,
+ dividerColor: Colors.transparent,
+ indicator: BoxDecoration(
+ color: designVariables.background,
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(
+ color: designVariables.foreground.withFadedAlpha(0.2),
+ width: 1,
+ ),
+ ),
+ splashFactory: NoSplash.splashFactory,
+ indicatorSize: TabBarIndicatorSize.tab,
+ labelColor: designVariables.foreground,
+ unselectedLabelColor: designVariables.foreground,
+ labelStyle: const TextStyle(fontSize: 14)
+ .merge(weightVariableTextStyle(context, wght: 400)),
+ unselectedLabelStyle: const TextStyle(fontSize: 14)
+ .merge(weightVariableTextStyle(context, wght: 400)),
+ tabs: tabs,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Flexible(
+ child: TabBarView(children: tabViews),
+ ),
+ ],
+ ),
+ );
+ }
+}
+class ReactionSheetCloseButton extends StatelessWidget {
+ const ReactionSheetCloseButton({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final designVariables = DesignVariables.of(context);
+ return TextButton(
+ style: TextButton.styleFrom(
+ minimumSize: const Size.fromHeight(44),
+ padding: const EdgeInsets.all(10),
+ foregroundColor: designVariables.contextMenuCancelText,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
+ splashFactory: NoSplash.splashFactory,
+ ).copyWith(backgroundColor: WidgetStateColor.fromMap({
+ WidgetState.pressed: designVariables.contextMenuCancelPressedBg,
+ ~WidgetState.pressed: designVariables.contextMenuCancelBg,
+ })),
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ child: Text(ZulipLocalizations.of(context).dialogClose,
+ style: const TextStyle(fontSize: 20, height: 24 / 20)
+ .merge(weightVariableTextStyle(context, wght: 600))));
+ }
+}
/// The size of a square emoji (Unicode or image).
///
/// Should be scaled by [_emojiTextScalerClamped].
diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart
index 7d58305fb4..9b29e6c555 100644
--- a/lib/widgets/icons.dart
+++ b/lib/widgets/icons.dart
@@ -99,38 +99,41 @@ abstract final class ZulipIcons {
/// The Zulip custom icon "mute".
static const IconData mute = IconData(0xf119, fontFamily: "Zulip Icons");
+ /// The Zulip custom icon "reactions".
+ static const IconData reactions = IconData(0xf11a, fontFamily: "Zulip Icons");
+
/// The Zulip custom icon "read_receipts".
- static const IconData read_receipts = IconData(0xf11a, fontFamily: "Zulip Icons");
+ static const IconData read_receipts = IconData(0xf11b, fontFamily: "Zulip Icons");
/// The Zulip custom icon "send".
- static const IconData send = IconData(0xf11b, fontFamily: "Zulip Icons");
+ static const IconData send = IconData(0xf11c, fontFamily: "Zulip Icons");
/// The Zulip custom icon "share".
- static const IconData share = IconData(0xf11c, fontFamily: "Zulip Icons");
+ static const IconData share = IconData(0xf11d, fontFamily: "Zulip Icons");
/// The Zulip custom icon "share_ios".
- static const IconData share_ios = IconData(0xf11d, fontFamily: "Zulip Icons");
+ static const IconData share_ios = IconData(0xf11e, fontFamily: "Zulip Icons");
/// The Zulip custom icon "smile".
- static const IconData smile = IconData(0xf11e, fontFamily: "Zulip Icons");
+ static const IconData smile = IconData(0xf11f, fontFamily: "Zulip Icons");
/// The Zulip custom icon "star".
- static const IconData star = IconData(0xf11f, fontFamily: "Zulip Icons");
+ static const IconData star = IconData(0xf120, fontFamily: "Zulip Icons");
/// The Zulip custom icon "star_filled".
- static const IconData star_filled = IconData(0xf120, fontFamily: "Zulip Icons");
+ static const IconData star_filled = IconData(0xf121, fontFamily: "Zulip Icons");
/// The Zulip custom icon "three_person".
- static const IconData three_person = IconData(0xf121, fontFamily: "Zulip Icons");
+ static const IconData three_person = IconData(0xf122, fontFamily: "Zulip Icons");
/// The Zulip custom icon "topic".
- static const IconData topic = IconData(0xf122, fontFamily: "Zulip Icons");
+ static const IconData topic = IconData(0xf123, fontFamily: "Zulip Icons");
/// The Zulip custom icon "unmute".
- static const IconData unmute = IconData(0xf123, fontFamily: "Zulip Icons");
+ static const IconData unmute = IconData(0xf124, fontFamily: "Zulip Icons");
/// The Zulip custom icon "user".
- static const IconData user = IconData(0xf124, fontFamily: "Zulip Icons");
+ static const IconData user = IconData(0xf125, fontFamily: "Zulip Icons");
// END GENERATED ICON DATA
}
diff --git a/packages/zulip_plugin/pubspec.lock b/packages/zulip_plugin/pubspec.lock
new file mode 100644
index 0000000000..e4de75859b
--- /dev/null
+++ b/packages/zulip_plugin/pubspec.lock
@@ -0,0 +1,6 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages: {}
+sdks:
+ dart: ">=3.4.0-256.0.dev <4.0.0"
+ flutter: ">=3.3.0"
diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart
index e6b48384b8..ee369660af 100644
--- a/test/widgets/action_sheet_test.dart
+++ b/test/widgets/action_sheet_test.dart
@@ -25,6 +25,7 @@ import 'package:zulip/widgets/app_bar.dart';
import 'package:zulip/widgets/compose_box.dart';
import 'package:zulip/widgets/content.dart';
import 'package:zulip/widgets/emoji.dart';
+import 'package:zulip/widgets/emoji_reaction.dart';
import 'package:zulip/widgets/home.dart';
import 'package:zulip/widgets/icons.dart';
import 'package:zulip/widgets/inbox.dart';
@@ -526,18 +527,104 @@ void main() {
}
});
- group('StarButton', () {
- Future tapButton(WidgetTester tester, {bool starred = false}) async {
- // Starred messages include the same icon so we need to
- // match only by descendants of [BottomSheet].
- await tester.ensureVisible(find.descendant(
+ group('ViewReaction', () {
+ final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
+
+ Future tapButton(WidgetTester tester) async {
+ await tester.ensureVisible(find.descendant(
+ of: find.byType(BottomSheet),
+ matching: find.byIcon(ZulipIcons.reactions, skipOffstage: false)));
+ await tester.tap(find.descendant(
+ of: find.byType(BottomSheet),
+ matching: find.byIcon(ZulipIcons.reactions)));
+ await tester.pump();
+ }
+
+ testWidgets('reaction option is absent when reactions list is empty', (tester) async {
+ final message = eg.streamMessage(reactions: []);
+
+ await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
+ connection.prepare(json: {});
+
+ expect(
+ find.descendant(
of: find.byType(BottomSheet),
- matching: find.byIcon(starred ? ZulipIcons.star_filled : ZulipIcons.star, skipOffstage: false)));
- await tester.tap(find.descendant(
+ matching: find.byIcon(ZulipIcons.reactions),
+ ),
+ findsNothing,
+ );
+ });
+
+ testWidgets('reaction sheet displays reactions correctly', (tester) async {
+
+ final message = eg.streamMessage(
+ reactions:[ eg.unicodeEmojiReaction , eg.realmEmojiReaction]
+ );
+
+ await setupToMessageActionSheet(tester, message:message, narrow: TopicNarrow.ofMessage(message));
+ connection.prepare(json: {});
+
+ expect(
+ find.descendant(
of: find.byType(BottomSheet),
- matching: find.byIcon(starred ? ZulipIcons.star_filled : ZulipIcons.star)));
- await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e
- }
+ matching: find.byIcon(ZulipIcons.reactions),
+ ),
+ findsOneWidget,
+ );
+
+ await tapButton(tester);
+ await tester.pumpAndSettle();
+
+ // Verify tabs and reaction content
+ expect(find.byType(ReactionListContent), findsOneWidget);
+ expect(find.byType(Tab), findsNWidgets(2));
+ expect(
+ find.widgetWithText(ReactionListContent, '👍'),
+ findsOneWidget,
+ );
+
+ final reactionListFinder = find.byType(ReactionListContent);
+ await tester.drag(reactionListFinder, const Offset(-300, 0));
+ await tester.pumpAndSettle();
+ expect(find.widgetWithText(ListTile, 'You'), findsOneWidget);
+ });
+
+ testWidgets('close button dismisses reaction list sheet', (tester) async {
+ final message = eg.streamMessage(
+ reactions:[ eg.unicodeEmojiReaction , eg.realmEmojiReaction]
+ );
+ await setupToMessageActionSheet(tester, message:message, narrow: TopicNarrow.ofMessage(message));
+ connection.prepare(json: {});
+ expect(
+ find.descendant(
+ of: find.byType(BottomSheet),
+ matching: find.byIcon(ZulipIcons.reactions),
+ ),
+ findsOneWidget,
+ );
+ // opening the reaction sheet
+ await tapButton(tester);
+ await tester.pumpAndSettle();
+
+ final findCloseButton = find.text(zulipLocalizations.dialogClose);
+ await tester.tap(findCloseButton);
+ await tester.pumpAndSettle();
+ expect(find.byType(ReactionListContent), findsNothing);
+ });
+
+ });
+ group('StarButton', () {
+ Future tapButton(WidgetTester tester, {bool starred = false}) async {
+ // Starred messages include the same icon so we need to
+ // match only by descendants of [BottomSheet].
+ await tester.ensureVisible(find.descendant(
+ of: find.byType(BottomSheet),
+ matching: find.byIcon(starred ? ZulipIcons.star_filled : ZulipIcons.star, skipOffstage: false)));
+ await tester.tap(find.descendant(
+ of: find.byType(BottomSheet),
+ matching: find.byIcon(starred ? ZulipIcons.star_filled : ZulipIcons.star)));
+ await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e
+ }
testWidgets('star success', (tester) async {
final message = eg.streamMessage(flags: []);