From f0dccf5809ab427e6e769dba1f7d1b14e77f5725 Mon Sep 17 00:00:00 2001 From: Zixuan James Li Date: Tue, 21 Jan 2025 15:39:26 -0500 Subject: [PATCH] compose: Respect realm setting for mandatory topics Signed-off-by: Zixuan James Li --- lib/widgets/compose_box.dart | 33 +++++++++------ test/model/autocomplete_test.dart | 3 +- test/widgets/compose_box_test.dart | 65 ++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 83d923b6b8..ac11de8142 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -66,13 +66,15 @@ enum TopicValidationError { } class ComposeTopicController extends ComposeController { - ComposeTopicController() { + ComposeTopicController({required this.store}) { _update(); } + PerAccountStore store; + // TODO: subscribe to this value: // https://zulip.com/help/require-topics - final mandatory = true; + bool get mandatory => store.realmMandatoryTopics; @override String _computeTextNormalized() { @@ -1194,7 +1196,10 @@ sealed class ComposeBoxController { } class StreamComposeBoxController extends ComposeBoxController { - final topic = ComposeTopicController(); + StreamComposeBoxController({required PerAccountStore store}) + : topic = ComposeTopicController(store: store); + + final ComposeTopicController topic; final topicFocusNode = FocusNode(); @override @@ -1271,16 +1276,17 @@ abstract class ComposeBoxState extends State { ComposeBoxController get controller; } -class _ComposeBoxState extends State implements ComposeBoxState { - @override ComposeBoxController get controller => _controller; - late final ComposeBoxController _controller; +class _ComposeBoxState extends State with PerAccountStoreAwareStateMixin implements ComposeBoxState { + @override ComposeBoxController get controller => _controller!; + ComposeBoxController? _controller; @override - void initState() { - super.initState(); + void onNewStore() { switch (widget.narrow) { case ChannelNarrow(): - _controller = StreamComposeBoxController(); + final store = PerAccountStoreWidget.of(context); + _controller ??= StreamComposeBoxController(store: store); + (controller as StreamComposeBoxController).topic.store = store; case TopicNarrow(): case DmNarrow(): _controller = FixedDestinationComposeBoxController(); @@ -1293,7 +1299,7 @@ class _ComposeBoxState extends State implements ComposeBoxState { @override void dispose() { - _controller.dispose(); + _controller!.dispose(); super.dispose(); } @@ -1333,15 +1339,16 @@ class _ComposeBoxState extends State implements ComposeBoxState { return _ComposeBoxContainer(body: null, errorBanner: errorBanner); } + final controller = _controller!; final narrow = widget.narrow; - switch (_controller) { + switch (controller) { case StreamComposeBoxController(): { narrow as ChannelNarrow; - body = _StreamComposeBoxBody(controller: _controller, narrow: narrow); + body = _StreamComposeBoxBody(controller: controller, narrow: narrow); } case FixedDestinationComposeBoxController(): { narrow as SendableNarrow; - body = _FixedDestinationComposeBoxBody(controller: _controller, narrow: narrow); + body = _FixedDestinationComposeBoxBody(controller: controller, narrow: narrow); } } diff --git a/test/model/autocomplete_test.dart b/test/model/autocomplete_test.dart index d6ed9574e0..3d680aca0e 100644 --- a/test/model/autocomplete_test.dart +++ b/test/model/autocomplete_test.dart @@ -835,7 +835,8 @@ void main() { final description = 'topic-input with text: $markedText produces: ${expectedQuery?.raw ?? 'No Query!'}'; test(description, () { - final controller = ComposeTopicController(); + final store = eg.store(); + final controller = ComposeTopicController(store: store); controller.value = parsed.value; if (expectedQuery == null) { check(controller).autocompleteIntent.isNull(); diff --git a/test/widgets/compose_box_test.dart b/test/widgets/compose_box_test.dart index f9c411ae2e..23615b1023 100644 --- a/test/widgets/compose_box_test.dart +++ b/test/widgets/compose_box_test.dart @@ -456,6 +456,71 @@ void main() { }); }); + group('sending to empty topic', () { + late ZulipStream channel; + + Future setupAndTapSend(WidgetTester tester, { + bool? mandatoryTopics, + required String topicInputText, + }) async { + TypingNotifier.debugEnable = false; + addTearDown(TypingNotifier.debugReset); + addTearDown(testBinding.reset); + + channel = eg.stream(); + final account = eg.account(user: eg.selfUser); + final initialSnapshot = eg.initialSnapshot( + realmMandatoryTopics: mandatoryTopics); + await testBinding.globalStore.add(account, initialSnapshot); + + store = await testBinding.globalStore.perAccount(account.id); + await store.addStream(channel); + await store.addUser(eg.selfUser); + connection = store.connection as FakeApiConnection; + + final narrow = ChannelNarrow(channel.streamId); + await prepareComposeBox(tester, + narrow: narrow, account: account); + await enterTopic(tester, narrow: narrow, topic: topicInputText); + await tester.enterText(contentInputFinder, 'test content'); + await tester.tap(find.byIcon(ZulipIcons.send)); + await tester.pump(); + } + + void checkMessageNotSent(WidgetTester tester) { + check(connection.takeRequests()).isEmpty(); + checkErrorDialog(tester, + expectedTitle: 'Message not sent', + expectedMessage: 'Topics are required in this organization.'); + } + + testWidgets('empty topic -> (no topic)', (tester) async { + await setupAndTapSend(tester, topicInputText: ''); + check(connection.lastRequest).isA() + ..method.equals('POST') + ..url.path.equals('/api/v1/messages') + ..bodyFields.deepEquals({ + 'type': 'stream', + 'to': channel.streamId.toString(), + 'topic': '(no topic)', + 'content': 'test content', + 'read_by_sender': 'true', + }); + }); + + testWidgets('if topics are mandatory, reject empty topic', (tester) async { + await setupAndTapSend(tester, topicInputText: '', + mandatoryTopics: true); + checkMessageNotSent(tester); + }); + + testWidgets('if topics are mandatory, reject (no topic)', (tester) async { + await setupAndTapSend(tester, topicInputText: '(no topic)', + mandatoryTopics: true); + checkMessageNotSent(tester); + }); + }); + group('uploads', () { void checkAppearsLoading(WidgetTester tester, bool expected) { final sendButtonElement = tester.element(find.ancestor(