Skip to content

Commit 7c6e2ba

Browse files
committed
compose: Respect realm setting for mandatory topics
Signed-off-by: Zixuan James Li <[email protected]>
1 parent 2906475 commit 7c6e2ba

File tree

3 files changed

+87
-14
lines changed

3 files changed

+87
-14
lines changed

lib/widgets/compose_box.dart

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,15 @@ enum TopicValidationError {
8989
}
9090

9191
class ComposeTopicController extends ComposeController<TopicValidationError> {
92-
ComposeTopicController() {
92+
ComposeTopicController({required this.store}) {
9393
_update();
9494
}
9595

96+
PerAccountStore store;
97+
9698
// TODO: subscribe to this value:
9799
// https://zulip.com/help/require-topics
98-
final mandatory = true;
100+
bool get mandatory => store.realmMandatoryTopics;
99101

100102
// TODO(#307) use `max_topic_length` instead of hardcoded limit
101103
@override final maxLengthUnicodeCodePoints = kMaxTopicLengthCodePoints;
@@ -1227,7 +1229,10 @@ sealed class ComposeBoxController {
12271229
}
12281230

12291231
class StreamComposeBoxController extends ComposeBoxController {
1230-
final topic = ComposeTopicController();
1232+
StreamComposeBoxController({required PerAccountStore store})
1233+
: topic = ComposeTopicController(store: store);
1234+
1235+
final ComposeTopicController topic;
12311236
final topicFocusNode = FocusNode();
12321237

12331238
@override
@@ -1304,16 +1309,17 @@ abstract class ComposeBoxState extends State<ComposeBox> {
13041309
ComposeBoxController get controller;
13051310
}
13061311

1307-
class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
1308-
@override ComposeBoxController get controller => _controller;
1309-
late final ComposeBoxController _controller;
1312+
class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateMixin<ComposeBox> implements ComposeBoxState {
1313+
@override ComposeBoxController get controller => _controller!;
1314+
ComposeBoxController? _controller;
13101315

13111316
@override
1312-
void initState() {
1313-
super.initState();
1317+
void onNewStore() {
13141318
switch (widget.narrow) {
13151319
case ChannelNarrow():
1316-
_controller = StreamComposeBoxController();
1320+
final store = PerAccountStoreWidget.of(context);
1321+
_controller ??= StreamComposeBoxController(store: store);
1322+
(controller as StreamComposeBoxController).topic.store = store;
13171323
case TopicNarrow():
13181324
case DmNarrow():
13191325
_controller = FixedDestinationComposeBoxController();
@@ -1326,7 +1332,7 @@ class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
13261332

13271333
@override
13281334
void dispose() {
1329-
_controller.dispose();
1335+
_controller!.dispose();
13301336
super.dispose();
13311337
}
13321338

@@ -1366,15 +1372,16 @@ class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
13661372
return _ComposeBoxContainer(body: null, errorBanner: errorBanner);
13671373
}
13681374

1375+
final controller = _controller!;
13691376
final narrow = widget.narrow;
1370-
switch (_controller) {
1377+
switch (controller) {
13711378
case StreamComposeBoxController(): {
13721379
narrow as ChannelNarrow;
1373-
body = _StreamComposeBoxBody(controller: _controller, narrow: narrow);
1380+
body = _StreamComposeBoxBody(controller: controller, narrow: narrow);
13741381
}
13751382
case FixedDestinationComposeBoxController(): {
13761383
narrow as SendableNarrow;
1377-
body = _FixedDestinationComposeBoxBody(controller: _controller, narrow: narrow);
1384+
body = _FixedDestinationComposeBoxBody(controller: controller, narrow: narrow);
13781385
}
13791386
}
13801387

test/model/autocomplete_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,8 @@ void main() {
835835

836836
final description = 'topic-input with text: $markedText produces: ${expectedQuery?.raw ?? 'No Query!'}';
837837
test(description, () {
838-
final controller = ComposeTopicController();
838+
final store = eg.store();
839+
final controller = ComposeTopicController(store: store);
839840
controller.value = parsed.value;
840841
if (expectedQuery == null) {
841842
check(controller).autocompleteIntent.isNull();

test/widgets/compose_box_test.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,71 @@ void main() {
566566
});
567567
});
568568

569+
group('sending to empty topic', () {
570+
late ZulipStream channel;
571+
572+
Future<void> setupAndTapSend(WidgetTester tester, {
573+
bool? mandatoryTopics,
574+
required String topicInputText,
575+
}) async {
576+
TypingNotifier.debugEnable = false;
577+
addTearDown(TypingNotifier.debugReset);
578+
addTearDown(testBinding.reset);
579+
580+
channel = eg.stream();
581+
final account = eg.account(user: eg.selfUser);
582+
final initialSnapshot = eg.initialSnapshot(
583+
realmMandatoryTopics: mandatoryTopics);
584+
await testBinding.globalStore.add(account, initialSnapshot);
585+
586+
store = await testBinding.globalStore.perAccount(account.id);
587+
await store.addStream(channel);
588+
await store.addUser(eg.selfUser);
589+
connection = store.connection as FakeApiConnection;
590+
591+
final narrow = ChannelNarrow(channel.streamId);
592+
await prepareComposeBox(tester,
593+
narrow: narrow, account: account);
594+
await enterTopic(tester, narrow: narrow, topic: topicInputText);
595+
await tester.enterText(contentInputFinder, 'test content');
596+
await tester.tap(find.byIcon(ZulipIcons.send));
597+
await tester.pump();
598+
}
599+
600+
void checkMessageNotSent(WidgetTester tester) {
601+
check(connection.takeRequests()).isEmpty();
602+
checkErrorDialog(tester,
603+
expectedTitle: 'Message not sent',
604+
expectedMessage: 'Topics are required in this organization.');
605+
}
606+
607+
testWidgets('empty topic -> (no topic)', (tester) async {
608+
await setupAndTapSend(tester, topicInputText: '');
609+
check(connection.lastRequest).isA<http.Request>()
610+
..method.equals('POST')
611+
..url.path.equals('/api/v1/messages')
612+
..bodyFields.deepEquals({
613+
'type': 'stream',
614+
'to': channel.streamId.toString(),
615+
'topic': '(no topic)',
616+
'content': 'test content',
617+
'read_by_sender': 'true',
618+
});
619+
});
620+
621+
testWidgets('if topics are mandatory, reject empty topic', (tester) async {
622+
await setupAndTapSend(tester, topicInputText: '',
623+
mandatoryTopics: true);
624+
checkMessageNotSent(tester);
625+
});
626+
627+
testWidgets('if topics are mandatory, reject (no topic)', (tester) async {
628+
await setupAndTapSend(tester, topicInputText: '(no topic)',
629+
mandatoryTopics: true);
630+
checkMessageNotSent(tester);
631+
});
632+
});
633+
569634
group('uploads', () {
570635
void checkAppearsLoading(WidgetTester tester, bool expected) {
571636
final sendButtonElement = tester.element(find.ancestor(

0 commit comments

Comments
 (0)