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: []);