diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart index ef7b8496ae..c5a93eb31f 100644 --- a/lib/widgets/icons.dart +++ b/lib/widgets/icons.dart @@ -127,3 +127,21 @@ IconData iconDataForStream(ZulipStream stream) { ZulipStream() => ZulipIcons.hash_sign, }; } + +IconData? iconDataForTopic(UserTopicVisibilityPolicy policy) { + switch (policy) { + case UserTopicVisibilityPolicy.muted: + return ZulipIcons.mute; + case UserTopicVisibilityPolicy.unmuted: + return ZulipIcons.unmute; + case UserTopicVisibilityPolicy.followed: + return ZulipIcons.follow; + case UserTopicVisibilityPolicy.none: + return null; + case UserTopicVisibilityPolicy.unknown: + // This case is unreachable (or should be) because we keep `unknown` out + // of our data structures. We plan to remove the `unknown` case in #1074. + assert(false); + return null; + } +} diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index e996104e6d..0e9a2d612d 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -313,20 +313,42 @@ class MessageListAppBarTitle extends StatelessWidget { Widget _buildStreamRow(BuildContext context, { ZulipStream? stream, - required String text, }) { - // A null [Icon.icon] makes a blank space. - final icon = (stream != null) ? iconDataForStream(stream) : null; - return Row( + final icon = (stream == null) ? ZulipIcons.hash_sign : iconDataForStream(stream); + Widget result = Row( mainAxisSize: MainAxisSize.min, // TODO(design): The vertical alignment of the stream privacy icon is a bit ad hoc. // For screenshots of some experiments, see: // https://github.com/zulip/zulip-flutter/pull/219#discussion_r1281024746 crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(size: 16, icon), - const SizedBox(width: 4), - Flexible(child: Text(text)), + Padding(padding: const EdgeInsetsDirectional.only(end: 8.0), + child: Icon(size: 20, icon)), + Flexible(child: Text(stream?.name ?? '(unknown stream)', + style: const TextStyle( + fontSize: 20, + ).merge(weightVariableTextStyle(context)))), + ]); + + return result; + } + + Widget _buildTopicRow(BuildContext context, { + required ZulipStream? stream, + required String topic, + }) { + final store = PerAccountStoreWidget.of(context); + final icon = (stream == null) ? null + : iconDataForTopic(store.topicVisibilityPolicy(stream.streamId, topic)); + return Row( + children: [ + Flexible(child: Text(topic, style: const TextStyle( + fontSize: 13, + ).merge(weightVariableTextStyle(context)))), + if (icon != null) + Padding( + padding: const EdgeInsetsDirectional.only(start: 4), + child: Opacity(opacity: 0.4, child: Icon(icon, size: 14))), ]); } @@ -347,21 +369,24 @@ class MessageListAppBarTitle extends StatelessWidget { case ChannelNarrow(:var streamId): final store = PerAccountStoreWidget.of(context); final stream = store.streams[streamId]; - final streamName = stream?.name ?? '(unknown channel)'; - return _buildStreamRow(context, stream: stream, text: streamName); + return _buildStreamRow(context, stream: stream); case TopicNarrow(:var streamId, :var topic): final store = PerAccountStoreWidget.of(context); final stream = store.streams[streamId]; - final streamName = stream?.name ?? '(unknown channel)'; + return SizedBox( width: double.infinity, child: GestureDetector( behavior: HitTestBehavior.translucent, onLongPress: () => showTopicActionSheet(context, channelId: streamId, topic: topic), - child: _buildStreamRow( - context, stream: stream, text: "$streamName > $topic"))); + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStreamRow(context, stream: stream), + _buildTopicRow(context, stream: stream, topic: topic), + ]))); case DmNarrow(:var otherRecipientIds): final store = PerAccountStoreWidget.of(context); diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 27b270249f..8fde189c39 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -152,6 +152,22 @@ void main() { .page.isA().initNarrow .equals(ChannelNarrow(channel.streamId)); }); + + testWidgets('show topic visibility policy for topic narrows', (tester) async { + final channel = eg.stream(); + const topic = 'topic'; + await setupMessageListPage(tester, + narrow: TopicNarrow(channel.streamId, topic), + streams: [channel], subscriptions: [eg.subscription(channel)], + messageCount: 1); + await store.handleEvent(eg.userTopicEvent( + channel.streamId, topic, UserTopicVisibilityPolicy.muted)); + await tester.pump(); + + check(find.descendant( + of: find.byType(MessageListAppBarTitle), + matching: find.byIcon(ZulipIcons.mute))).findsOne(); + }); }); group('presents message content appropriately', () { @@ -725,7 +741,7 @@ void main() { ).length.equals(1); check(find.descendant( of: find.byType(MessageListAppBarTitle), - matching: find.text('${channel.name} > new topic')).evaluate() + matching: find.text('new topic')).evaluate() ).length.equals(1); }); });