From 1002a9ba3d8604c29756b339e2cdd89246ef583f Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Thu, 26 Jun 2025 20:17:21 -0700 Subject: [PATCH 1/2] msglist test [nfc]: Move a Finder helper outward for reuse --- test/widgets/message_list_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index fd8dd6f10b..026abcc62c 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -125,6 +125,9 @@ void main() { return findScrollView(tester).controller; } + final contentInputFinder = find.byWidgetPredicate( + (widget) => widget is TextField && widget.controller is ComposeContentController); + group('MessageListPage', () { testWidgets('ancestorOf finds page state from message', (tester) async { await setupMessageListPage(tester, @@ -1634,9 +1637,6 @@ void main() { final topicNarrow = eg.topicNarrow(stream.streamId, topic); const content = 'outbox message content'; - final contentInputFinder = find.byWidgetPredicate( - (widget) => widget is TextField && widget.controller is ComposeContentController); - Finder outboxMessageFinder = find.widgetWithText( OutboxMessageWithPossibleSender, content, skipOffstage: true); From d732415b40d3165bcce5c0701ded7c02755bf7fd Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Thu, 26 Jun 2025 21:23:28 -0700 Subject: [PATCH 2/2] compose: Don't show compose box for private, unsubscribed channels --- lib/widgets/compose_box.dart | 8 ++- test/widgets/compose_box_test.dart | 91 +++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index a53d628a2c..cee8270c95 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -2064,8 +2064,12 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM case ChannelNarrow(:final streamId): case TopicNarrow(:final streamId): final channel = store.streams[streamId]; - if (channel == null || !store.hasPostingPermission(inChannel: channel, - user: store.selfUser, byDate: DateTime.now())) { + if ( + channel == null + || (channel.inviteOnly && channel is! Subscription) + || !store.hasPostingPermission(inChannel: channel, + user: store.selfUser, byDate: DateTime.now()) + ) { return _ErrorBanner(getLabel: (zulipLocalizations) => zulipLocalizations.errorBannerCannotPostInChannelLabel); } diff --git a/test/widgets/compose_box_test.dart b/test/widgets/compose_box_test.dart index 76305610c6..b777ffa6f1 100644 --- a/test/widgets/compose_box_test.dart +++ b/test/widgets/compose_box_test.dart @@ -58,6 +58,7 @@ void main() { User? selfUser, List otherUsers = const [], List streams = const [], + List subscriptions = const [], List? messages, bool? mandatoryTopics, int? zulipFeatureLevel, @@ -81,6 +82,7 @@ void main() { await testBinding.globalStore.add(selfAccount, eg.initialSnapshot( realmUsers: [selfUser, ...otherUsers], streams: streams, + subscriptions: subscriptions, zulipFeatureLevel: zulipFeatureLevel, realmMandatoryTopics: mandatoryTopics, realmAllowMessageEditing: true, @@ -1281,7 +1283,7 @@ void main() { }); }); - group('in channel/topic narrow according to channel post policy', () { + group('in channel/topic narrow according to channel post policy and privacy/subscribed', () { void checkComposeBox({required bool isShown}) => checkComposeBoxIsShown(isShown, bannerLabel: zulipLocalizations.errorBannerCannotPostInChannelLabel); @@ -1300,7 +1302,7 @@ void main() { checkComposeBox(isShown: true); }); - testWidgets('error banner is shown in $narrowType narrow', (tester) async { + testWidgets('error banner is shown in $narrowType narrow without posting permission', (tester) async { await prepareComposeBox(tester, narrow: narrow, selfUser: eg.user(role: UserRole.moderator), @@ -1308,6 +1310,17 @@ void main() { channelPostPolicy: ChannelPostPolicy.administrators)]); checkComposeBox(isShown: false); }); + + testWidgets('error banner is shown in $narrowType when private and unsubscribed', (tester) async { + await prepareComposeBox(tester, + narrow: narrow, + selfUser: eg.user(role: UserRole.moderator), + streams: [eg.stream(streamId: 1, + channelPostPolicy: ChannelPostPolicy.any, + inviteOnly: true)]); + check(store.subscriptions[1]).isNull(); + checkComposeBox(isShown: false); + }); } testWidgets('user loses privilege -> compose box is replaced with the banner', (tester) async { @@ -1375,6 +1388,80 @@ void main() { await tester.pump(); checkComposeBox(isShown: true); }); + + testWidgets('unsubscribed private channel becomes public -> banner is replaced with the compose box', (tester) async { + final selfUser = eg.user(role: UserRole.moderator); + final channel = eg.stream(streamId: 1, inviteOnly: true, + channelPostPolicy: ChannelPostPolicy.any); + + await prepareComposeBox(tester, + narrow: const ChannelNarrow(1), + selfUser: selfUser, + streams: [channel]); + check(store.subscriptions[1]).isNull(); + checkComposeBox(isShown: false); + + await store.handleEvent(eg.channelUpdateEvent(channel, + property: ChannelPropertyName.inviteOnly, + value: false)); + await tester.pump(); + checkComposeBox(isShown: true); + }); + + testWidgets('unsubscribed public channel becomes private -> compose box is replaced with the banner', (tester) async { + final selfUser = eg.user(role: UserRole.moderator); + final channel = eg.stream(streamId: 1, inviteOnly: false, + channelPostPolicy: ChannelPostPolicy.any); + + await prepareComposeBox(tester, + narrow: const ChannelNarrow(1), + selfUser: selfUser, + streams: [channel]); + check(store.subscriptions[1]).isNull(); + checkComposeBox(isShown: true); + + await store.handleEvent(eg.channelUpdateEvent(channel, + property: ChannelPropertyName.inviteOnly, + value: true)); + await tester.pump(); + checkComposeBox(isShown: false); + }); + + testWidgets('unsubscribed private channel becomes subscribed -> banner is replaced with the compose box', (tester) async { + final selfUser = eg.user(role: UserRole.moderator); + final channel = eg.stream(streamId: 1, inviteOnly: true, + channelPostPolicy: ChannelPostPolicy.any); + + await prepareComposeBox(tester, + narrow: const ChannelNarrow(1), + selfUser: selfUser, + streams: [channel]); + check(store.subscriptions[1]).isNull(); + checkComposeBox(isShown: false); + + await store.handleEvent(SubscriptionAddEvent(id: 1, + subscriptions: [eg.subscription(channel)])); + await tester.pump(); + checkComposeBox(isShown: true); + }); + + testWidgets('subscribed private channel becomes unsubscribed -> compose box is replaced with the banner', (tester) async { + final selfUser = eg.user(role: UserRole.moderator); + final channel = eg.stream(streamId: 1, inviteOnly: true, + channelPostPolicy: ChannelPostPolicy.any); + + await prepareComposeBox(tester, + narrow: const ChannelNarrow(1), + selfUser: selfUser, + streams: [channel], + subscriptions: [eg.subscription(channel)]); + checkComposeBox(isShown: true); + + await store.handleEvent(SubscriptionRemoveEvent(id: 1, + streamIds: [channel.streamId])); + await tester.pump(); + checkComposeBox(isShown: false); + }); }); });