Repository: zulip/zulip-flutter (language: dart)

Issue:
Add channel feed button to bottom sheet
To improve navigation, we should add a "Channel feed" link to the bottom sheet for channels.

CZO: [#mobile > channel feed link in bottom sheet?](https://chat.zulip.org/#narrow/channel/48-mobile/topic/channel.20feed.20link.20in.20bottom.20sheet.3F/with/2219497)


Discussion hints:
It should probably be omitted if you're already on the channel's channel feed.
On web, we also omit it when just clicking on the channel name would take you to the channel feed. Might be fine to include the link on mobile, though, at least while the menu is short like it currently is.

Pull request: action_sheet: Add 'Channel feed' button to channel action sheet
Fixes #1705.

## Screenshots

### From channel feed (no button)

<img width="559" height="1062" alt="image" src="https://github.com/user-attachments/assets/afb32f2a-1864-40ee-bb72-5768a655a84d" />

### Not from channel feed (button)

<img width="559" height="1062" alt="image" src="https://github.com/user-attachments/assets/1c4f5710-b113-46b9-8ec2-d13448153244" />

<img width="559" height="1062" alt="image" src="https://github.com/user-attachments/assets/f92deb93-58f2-41de-aaf0-e4c1a0262e99" />


Test command: flutter test test/widgets/action_sheet_test.dart --reporter expanded 2>&1

Fix patch:
diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb
index 56d906e233..776a866d3b 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -116,6 +116,10 @@
   "@actionSheetOptionListOfTopics": {
     "description": "Label for navigating to a channel's topic-list page."
   },
+  "actionSheetOptionChannelFeed": "Channel feed",
+  "@actionSheetOptionChannelFeed": {
+    "description": "Label for navigating to a channel's channel-feed page."
+  },
   "actionSheetOptionUnsubscribe": "Unsubscribe",
   "@actionSheetOptionUnsubscribe": {
     "description": "Label in the channel action sheet for unsubscribing from the channel."
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index 6ec90bba8c..238d336f39 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -305,6 +305,12 @@ abstract class ZulipLocalizations {
   /// **'List of topics'**
   String get actionSheetOptionListOfTopics;
 
+  /// Label for navigating to a channel's channel-feed page.
+  ///
+  /// In en, this message translates to:
+  /// **'Channel feed'**
+  String get actionSheetOptionChannelFeed;
+
   /// Label in the channel action sheet for unsubscribing from the channel.
   ///
   /// 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 1037cd6cdd..0ff35c4b5b 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -102,6 +102,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'List of topics';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart
index 3a4c7da9a5..fb47405921 100644
--- a/lib/generated/l10n/zulip_localizations_de.dart
+++ b/lib/generated/l10n/zulip_localizations_de.dart
@@ -104,6 +104,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'Themenliste';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Deabonnieren';
 
diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart
index e3d201ba4f..2e94965a87 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -102,6 +102,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'List of topics';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart
index e0244f4b43..3766c7051e 100644
--- a/lib/generated/l10n/zulip_localizations_fr.dart
+++ b/lib/generated/l10n/zulip_localizations_fr.dart
@@ -104,6 +104,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'Liste des sujets';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart
index 0f1b6b350e..028f8680fb 100644
--- a/lib/generated/l10n/zulip_localizations_it.dart
+++ b/lib/generated/l10n/zulip_localizations_it.dart
@@ -103,6 +103,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'Elenco degli argomenti';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart
index 2bd2d6c68f..647b1f155d 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -101,6 +101,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'トピック一覧';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart
index 4e7b862eeb..36411b5274 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -102,6 +102,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'List of topics';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart
index db93e8f9ba..e4fdc40e60 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -104,6 +104,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'Lista wątków';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart
index 30bb44aa98..3b0d3e8cd8 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -104,6 +104,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'Список тем';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Отписаться';
 
diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart
index 3da8dbfa25..26be66502e 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -102,6 +102,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'List of topics';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart
index 86a7c52b06..782288eb49 100644
--- a/lib/generated/l10n/zulip_localizations_sl.dart
+++ b/lib/generated/l10n/zulip_localizations_sl.dart
@@ -102,6 +102,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'Seznam tem';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart
index 1c9bbe8f93..d263a8a9c0 100644
--- a/lib/generated/l10n/zulip_localizations_uk.dart
+++ b/lib/generated/l10n/zulip_localizations_uk.dart
@@ -105,6 +105,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'Список тем';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Скасувати підписку';
 
diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart
index 3133bc58f4..59fbc4b5d5 100644
--- a/lib/generated/l10n/zulip_localizations_zh.dart
+++ b/lib/generated/l10n/zulip_localizations_zh.dart
@@ -102,6 +102,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
   @override
   String get actionSheetOptionListOfTopics => 'List of topics';
 
+  @override
+  String get actionSheetOptionChannelFeed => 'Channel feed';
+
   @override
   String get actionSheetOptionUnsubscribe => 'Unsubscribe';
 
diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart
index cc56dedf08..56729e8bfc 100644
--- a/lib/widgets/action_sheet.dart
+++ b/lib/widgets/action_sheet.dart
@@ -334,12 +334,19 @@ enum BottomSheetDismissButtonStyle {
 /// Show a sheet of actions you can take on a channel.
 ///
 /// Needs a [PageRoot] ancestor.
+/// May or may not have a [MessageListPage] ancestor;
+/// some callers are on that page and some aren't.
 void showChannelActionSheet(BuildContext context, {
   required int channelId,
   bool showTopicListButton = true,
 }) {
   final pageContext = PageRoot.contextOf(context);
   final store = PerAccountStoreWidget.of(pageContext);
+  final messageListPageState = MessageListPage.maybeAncestorOf(pageContext);
+
+  final messageListPageNarrow = messageListPageState?.narrow;
+  final isOnChannelFeed = messageListPageNarrow is ChannelNarrow
+    && messageListPageNarrow.streamId == channelId;
 
   final unreadCount = store.unreads.countInChannelNarrow(channelId);
   final isSubscribed = store.subscriptions[channelId] != null;
@@ -352,6 +359,8 @@ void showChannelActionSheet(BuildContext context, {
         MarkChannelAsReadButton(pageContext: pageContext, channelId: channelId),
       if (showTopicListButton)
         TopicListButton(pageContext: pageContext, channelId: channelId),
+      if (!isOnChannelFeed)
+        ChannelFeedButton(pageContext: pageContext, channelId: channelId),
       CopyChannelLinkButton(channelId: channelId, pageContext: pageContext)
     ],
     if (isSubscribed)
@@ -452,6 +461,30 @@ class TopicListButton extends ActionSheetMenuItemButton {
   }
 }
 
+class ChannelFeedButton extends ActionSheetMenuItemButton {
+  const ChannelFeedButton({
+    super.key,
+    required this.channelId,
+    required super.pageContext,
+  });
+
+  final int channelId;
+
+  @override
+  IconData get icon => ZulipIcons.message_feed;
+
+  @override
+  String label(ZulipLocalizations zulipLocalizations) {
+    return zulipLocalizations.actionSheetOptionChannelFeed;
+  }
+
+  @override
+  void onPressed() {
+    Navigator.push(pageContext,
+      MessageListPage.buildRoute(context: pageContext, narrow: ChannelNarrow(channelId)));
+  }
+}
+
 class CopyChannelLinkButton extends ActionSheetMenuItemButton {
   const CopyChannelLinkButton({
     super.key,
diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart
index dab793392b..6316655705 100644
--- a/lib/widgets/message_list.dart
+++ b/lib/widgets/message_list.dart
@@ -206,14 +206,30 @@ class MessageListPage extends StatefulWidget {
   ///
   /// Uses the inefficient [BuildContext.findAncestorStateOfType];
   /// don't call this in a build method.
-  // If we do find ourselves wanting this in a build method, it won't be hard
-  // to enable that: we'd just need to add an [InheritedWidget] here.
+  ///
+  /// See also:
+  ///  * [maybeAncestorOf], which returns null instead of throwing
+  ///    when an ancestor [MessageListPageState] is not found.
   static MessageListPageState ancestorOf(BuildContext context) {
-    final state = context.findAncestorStateOfType<_MessageListPageState>();
+    final state = maybeAncestorOf(context);
     assert(state != null, 'No MessageListPage ancestor');
     return state!;
   }
 
+  /// The [MessageListPageState] above this context in the tree, if any.
+  ///
+  /// Uses the inefficient [BuildContext.findAncestorStateOfType];
+  /// don't call this in a build method.
+  ///
+  /// See also:
+  ///  * [ancestorOf], which throws instead of returning null
+  ///    when an ancestor [MessageListPageState] is not found.
+  // If we do find ourselves wanting this in a build method, it won't be hard
+  // to enable that: we'd just need to add an [InheritedWidget] here.
+  static MessageListPageState? maybeAncestorOf(BuildContext context) {
+    return context.findAncestorStateOfType<_MessageListPageState>();
+  }
+
   final Narrow initNarrow;
   final int? initAnchorMessageId; // TODO(#1564) highlight target upon load
 
diff --git a/lib/widgets/topic_list.dart b/lib/widgets/topic_list.dart
index 61e16df6ec..16111fc678 100644
--- a/lib/widgets/topic_list.dart
+++ b/lib/widgets/topic_list.dart
@@ -103,7 +103,10 @@ class _TopicListAppBarTitle extends StatelessWidget {
       child: GestureDetector(
         behavior: HitTestBehavior.translucent,
         onLongPress: () {
-          showChannelActionSheet(context, channelId: streamId);
+          showChannelActionSheet(context,
+            channelId: streamId,
+            // We're already on the topic list.
+            showTopicListButton: false);
         },
         child: Align(alignment: alignment,
           child: _buildStreamRow(context))));


Test patch:
diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart
index 83574786a7..8f4d614220 100644
--- a/test/widgets/action_sheet_test.dart
+++ b/test/widgets/action_sheet_test.dart
@@ -34,6 +34,7 @@ import 'package:zulip/widgets/message_list.dart';
 import 'package:share_plus_platform_interface/method_channel/method_channel_share.dart';
 import 'package:zulip/widgets/read_receipts.dart';
 import 'package:zulip/widgets/subscription_list.dart';
+import 'package:zulip/widgets/topic_list.dart';
 import 'package:zulip/widgets/user.dart';
 import '../api/fake_api.dart';
 
@@ -180,7 +181,9 @@ void main() {
     }
 
     Future<void> showFromInbox(WidgetTester tester) async {
+      transitionDurationObserver = TransitionDurationObserver();
       await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
+        navigatorObservers: [transitionDurationObserver],
         child: const HomePage()));
       await tester.pump();
       check(find.byType(InboxPageBody)).findsOne();
@@ -201,16 +204,19 @@ void main() {
       await tester.pump(const Duration(milliseconds: 250));
     }
 
-    Future<void> showFromAppBar(WidgetTester tester, {
+    Future<void> showFromMsglistAppBar(WidgetTester tester, {
       ZulipStream? channel,
-      List<StreamMessage>? messages,
       required Narrow narrow,
     }) async {
       channel ??= someChannel;
-      messages ??= [someMessage];
 
       connection.prepare(json: eg.newestGetMessagesResult(
-        foundOldest: true, messages: messages).toJson());
+        foundOldest: true, messages: []).toJson());
+      if (narrow case ChannelNarrow()) {
+        // We auto-focus the topic input when there are no messages;
+        // this is for topic autocomplete.
+        connection.prepare(json: GetStreamTopicsResult(topics: []).toJson());
+      }
       await tester.pumpWidget(TestZulipApp(
         accountId: eg.selfAccount.id,
         child: MessageListPage(
@@ -240,6 +246,22 @@ void main() {
       await tester.pump(const Duration(milliseconds: 250));
     }
 
+    Future<void> showFromTopicListAppBar(WidgetTester tester) async {
+      final transitionDurationObserver = TransitionDurationObserver();
+
+      connection.prepare(json: GetStreamTopicsResult(topics: []).toJson());
+      await tester.pumpWidget(TestZulipApp(
+        navigatorObservers: [transitionDurationObserver],
+        accountId: eg.selfAccount.id,
+        child: TopicListPage(streamId: someChannel.streamId)));
+      await tester.pump();
+
+      await tester.longPress(find.descendant(
+        of: find.byType(ZulipAppBar),
+        matching: find.text(someChannel.name)));
+      await transitionDurationObserver.pumpPastTransition(tester);
+    }
+
     final actionSheetFinder = find.byType(BottomSheet);
     Finder findButtonForLabel(String label) =>
       find.descendant(of: actionSheetFinder, matching: find.text(label));
@@ -256,7 +278,6 @@ void main() {
       void checkButtons() {
         check(actionSheetFinder).findsOne();
         checkButton('Mark channel as read');
-        checkButton('List of topics');
         checkButton('Copy link to channel');
       }
 
@@ -278,17 +299,17 @@ void main() {
         check(findButtonForLabel('Mark channel as read')).findsNothing();
       });
 
-      testWidgets('show from app bar in channel narrow', (tester) async {
+      testWidgets('show from message-list app bar in channel narrow', (tester) async {
         await prepare();
         final narrow = ChannelNarrow(someChannel.streamId);
-        await showFromAppBar(tester, narrow: narrow);
+        await showFromMsglistAppBar(tester, narrow: narrow);
         checkButtons();
       });
 
-      testWidgets('show from app bar in topic narrow', (tester) async {
+      testWidgets('show from message-list app bar in topic narrow', (tester) async {
         await prepare();
         final narrow = eg.topicNarrow(someChannel.streamId, someTopic);
-        await showFromAppBar(tester, narrow: narrow);
+        await showFromMsglistAppBar(tester, narrow: narrow);
         checkButtons();
       });
 
@@ -297,6 +318,12 @@ void main() {
         await showFromRecipientHeader(tester, message: someMessage);
         checkButtons();
       });
+
+      testWidgets('show from topic-list app bar', (tester) async {
+        await prepare();
+        await showFromTopicListAppBar(tester);
+        checkButtons();
+      });
     });
 
     group('SubscribeButton', () {
@@ -309,7 +336,7 @@ void main() {
         await prepare();
         final narrow = ChannelNarrow(someChannel.streamId);
         await store.removeSubscription(narrow.streamId);
-        await showFromAppBar(tester, narrow: narrow);
+        await showFromMsglistAppBar(tester, narrow: narrow);
         checkButton('Subscribe');
       });
 
@@ -317,7 +344,7 @@ void main() {
         await prepare();
         final narrow = ChannelNarrow(someChannel.streamId);
         check(store.subscriptions[narrow.streamId]).isNotNull();
-        await showFromAppBar(tester, narrow: narrow);
+        await showFromMsglistAppBar(tester, narrow: narrow);
         checkNoButton('Subscribe');
       });
 
@@ -325,7 +352,7 @@ void main() {
         await prepare();
         final narrow = ChannelNarrow(someChannel.streamId);
         await store.removeSubscription(narrow.streamId);
-        await showFromAppBar(tester, narrow: narrow);
+        await showFromMsglistAppBar(tester, narrow: narrow);
 
         connection.prepare(json: {});
         await tapButton(tester);
@@ -386,17 +413,80 @@ void main() {
       });
     });
 
-    testWidgets('TopicListButton', (tester) async {
-      await prepare();
-      await showFromAppBar(tester,
-        narrow: ChannelNarrow(someChannel.streamId));
+    group('TopicListButton', () {
+      testWidgets('not visible from app bar on topic list', (tester) async {
+        await prepare();
+        await showFromTopicListAppBar(tester);
+        checkNoButton('List of topics');
+      });
 
-      connection.prepare(json: GetStreamTopicsResult(topics: [
-        eg.getStreamTopicsEntry(name: 'some topic foo'),
-      ]).toJson());
-      await tester.tap(findButtonForLabel('List of topics'));
-      await tester.pumpAndSettle();
-      check(find.text('some topic foo')).findsOne();
+      testWidgets('happy path from msglist app bar', (tester) async {
+        await prepare();
+        await showFromMsglistAppBar(tester,
+          narrow: ChannelNarrow(someChannel.streamId));
+
+        connection.prepare(json: GetStreamTopicsResult(topics: [
+          eg.getStreamTopicsEntry(name: 'some topic foo'),
+        ]).toJson());
+        await tester.tap(findButtonForLabel('List of topics'));
+        await tester.pumpAndSettle();
+        check(find.text('some topic foo')).findsOne();
+      });
+    });
+
+    group('ChannelFeedButton', () {
+      Future<void> tapButtonAndPump(WidgetTester tester) async {
+        await tester.tap(findButtonForLabel('Channel feed'));
+        await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e
+      }
+
+      testWidgets('from inbox: visible', (tester) async {
+        await prepare();
+        await showFromInbox(tester);
+        checkButton('Channel feed');
+      });
+
+      testWidgets('from subscription list: visible', (tester) async {
+        await prepare();
+        await showFromSubscriptionList(tester);
+        checkButton('Channel feed');
+      });
+
+      testWidgets('from recipient header in combined feed: visible', (tester) async {
+        await prepare();
+        await showFromRecipientHeader(tester);
+        checkButton('Channel feed');
+      });
+
+      testWidgets('from app bar on topic list: visible', (tester) async {
+        await prepare();
+        await showFromTopicListAppBar(tester);
+        checkButton('Channel feed');
+      });
+
+      testWidgets('from msglist app bar on channel feed: not visible', (tester) async {
+        await prepare();
+        await showFromMsglistAppBar(tester, narrow: ChannelNarrow(someChannel.streamId));
+        checkNoButton('Channel feed');
+      });
+
+      // (The channel action sheet isn't reached from a recipient header
+      // in the channel feed.)
+
+      testWidgets('navigates to channel feed', (tester) async {
+        await prepare();
+        await showFromInbox(tester);
+
+        connection.prepare(json: eg.newestGetMessagesResult(
+          foundOldest: true, messages: []).toJson());
+        // for topic autocomplete
+        connection.prepare(json: GetStreamTopicsResult(topics: []).toJson());
+        await tapButtonAndPump(tester);
+        await transitionDurationObserver.pumpPastTransition(tester);
+
+        final appBar = tester.widget(find.byType(MessageListAppBarTitle)) as MessageListAppBarTitle;
+        check(appBar.narrow).equals(ChannelNarrow(someChannel.streamId));
+      });
     });
 
     group('CopyChannelLinkButton', () {
@@ -416,7 +506,7 @@ void main() {
       testWidgets('copies channel link to clipboard', (tester) async {
         await prepare();
         final narrow = ChannelNarrow(someChannel.streamId);
-        await showFromAppBar(tester, narrow: narrow);
+        await showFromMsglistAppBar(tester, narrow: narrow);
 
         await tapCopyChannelLinkButton(tester);
         await tester.pump(Duration.zero);
@@ -436,7 +526,7 @@ void main() {
         await prepare();
         final narrow = ChannelNarrow(someChannel.streamId);
         check(store.subscriptions[narrow.streamId]).isNotNull();
-        await showFromAppBar(tester, narrow: narrow);
+        await showFromMsglistAppBar(tester, narrow: narrow);
         checkButton('Unsubscribe');
       });
 
@@ -444,19 +534,17 @@ void main() {
         await prepare();
         final narrow = ChannelNarrow(someChannel.streamId);
         await store.removeSubscription(narrow.streamId);
-        await showFromAppBar(tester, narrow: narrow);
+        await showFromMsglistAppBar(tester, narrow: narrow);
         checkNoButton('Unsubscribe');
       });
 
       testWidgets('smoke, public channel', (tester) async {
         final channel = eg.stream(inviteOnly: false);
-        final message = eg.streamMessage(stream: channel);
         await prepare();
         await store.addStream(channel);
         await store.addSubscription(eg.subscription(channel));
         final narrow = ChannelNarrow(channel.streamId);
-        await showFromAppBar(tester,
-          channel: channel, narrow: narrow, messages: [message]);
+        await showFromMsglistAppBar(tester, channel: channel, narrow: narrow);
 
         connection.prepare(json: {});
         await tapButton(tester);
@@ -474,13 +562,11 @@ void main() {
 
       testWidgets('smoke, private channel', (tester) async {
         final channel = eg.stream(inviteOnly: true);
-        final message = eg.streamMessage(stream: channel);
         await prepare();
         await store.addStream(channel);
         await store.addSubscription(eg.subscription(channel));
         final narrow = ChannelNarrow(channel.streamId);
-        await showFromAppBar(tester,
-          channel: channel, narrow: narrow, messages: [message]);
+        await showFromMsglistAppBar(tester, channel: channel, narrow: narrow);
         connection.takeRequests();
 
         connection.prepare(json: {});