Skip to content

Commit 2e0e38a

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

File tree

3 files changed

+85
-17
lines changed

3 files changed

+85
-17
lines changed

lib/widgets/compose_box.dart

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

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

96-
// TODO: subscribe to this value:
97-
// https://zulip.com/help/require-topics
98-
final mandatory = true;
96+
PerAccountStore store;
97+
98+
// TODO(#668): listen to [PerAccountStore] once we subscribe to this value
99+
bool get mandatory => store.realmMandatoryTopics;
99100

100101
// TODO(#307) use `max_topic_length` instead of hardcoded limit
101102
@override final maxLengthUnicodeCodePoints = kMaxTopicLengthCodePoints;
@@ -1227,7 +1228,10 @@ sealed class ComposeBoxController {
12271228
}
12281229

12291230
class StreamComposeBoxController extends ComposeBoxController {
1230-
final topic = ComposeTopicController();
1231+
StreamComposeBoxController({required PerAccountStore store})
1232+
: topic = ComposeTopicController(store: store);
1233+
1234+
final ComposeTopicController topic;
12311235
final topicFocusNode = FocusNode();
12321236

12331237
@override
@@ -1308,16 +1312,17 @@ abstract class ComposeBoxState extends State<ComposeBox> {
13081312
ComposeBoxController get controller;
13091313
}
13101314

1311-
class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
1312-
@override ComposeBoxController get controller => _controller;
1313-
late final ComposeBoxController _controller;
1315+
class _ComposeBoxState extends State<ComposeBox> with PerAccountStoreAwareStateMixin<ComposeBox> implements ComposeBoxState {
1316+
@override ComposeBoxController get controller => _controller!;
1317+
ComposeBoxController? _controller;
13141318

13151319
@override
1316-
void initState() {
1317-
super.initState();
1320+
void onNewStore() {
13181321
switch (widget.narrow) {
13191322
case ChannelNarrow():
1320-
_controller = StreamComposeBoxController();
1323+
final store = PerAccountStoreWidget.of(context);
1324+
_controller ??= StreamComposeBoxController(store: store);
1325+
(controller as StreamComposeBoxController).topic.store = store;
13211326
case TopicNarrow():
13221327
case DmNarrow():
13231328
_controller = FixedDestinationComposeBoxController();
@@ -1330,7 +1335,7 @@ class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
13301335

13311336
@override
13321337
void dispose() {
1333-
_controller.dispose();
1338+
_controller!.dispose();
13341339
super.dispose();
13351340
}
13361341

@@ -1370,15 +1375,16 @@ class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
13701375
return _ComposeBoxContainer(body: null, errorBanner: errorBanner);
13711376
}
13721377

1378+
final controller = _controller!;
13731379
final narrow = widget.narrow;
1374-
switch (_controller) {
1380+
switch (controller) {
13751381
case StreamComposeBoxController(): {
13761382
narrow as ChannelNarrow;
1377-
body = _StreamComposeBoxBody(controller: _controller, narrow: narrow);
1383+
body = _StreamComposeBoxBody(controller: controller, narrow: narrow);
13781384
}
13791385
case FixedDestinationComposeBoxController(): {
13801386
narrow as SendableNarrow;
1381-
body = _FixedDestinationComposeBoxBody(controller: _controller, narrow: narrow);
1387+
body = _FixedDestinationComposeBoxBody(controller: controller, narrow: narrow);
13821388
}
13831389
}
13841390

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: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ void main() {
4646
User? selfUser,
4747
List<User> otherUsers = const [],
4848
List<ZulipStream> streams = const [],
49+
bool? mandatoryTopics,
4950
}) async {
5051
if (narrow case ChannelNarrow(:var streamId) || TopicNarrow(: var streamId)) {
5152
assert(streams.any((stream) => stream.streamId == streamId),
@@ -54,7 +55,9 @@ void main() {
5455
addTearDown(testBinding.reset);
5556
selfUser ??= eg.selfUser;
5657
final selfAccount = eg.account(user: selfUser);
57-
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot());
58+
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot(
59+
realmMandatoryTopics: mandatoryTopics,
60+
));
5861

5962
store = await testBinding.globalStore.perAccount(selfAccount.id);
6063

@@ -558,6 +561,64 @@ void main() {
558561
});
559562
});
560563

564+
group('sending to empty topic', () {
565+
late ZulipStream channel;
566+
567+
Future<void> setupAndTapSend(WidgetTester tester, {
568+
required String topicInputText,
569+
bool? mandatoryTopics,
570+
}) async {
571+
TypingNotifier.debugEnable = false;
572+
addTearDown(TypingNotifier.debugReset);
573+
574+
channel = eg.stream();
575+
final narrow = ChannelNarrow(channel.streamId);
576+
await prepareComposeBox(tester,
577+
narrow: narrow, streams: [channel],
578+
mandatoryTopics: mandatoryTopics);
579+
580+
await enterTopic(tester, narrow: narrow, topic: topicInputText);
581+
await tester.enterText(contentInputFinder, 'test content');
582+
await tester.tap(find.byIcon(ZulipIcons.send));
583+
await tester.pump();
584+
}
585+
586+
void checkMessageNotSent(WidgetTester tester) {
587+
check(connection.takeRequests()).isEmpty();
588+
checkErrorDialog(tester,
589+
expectedTitle: 'Message not sent',
590+
expectedMessage: 'Topics are required in this organization.');
591+
}
592+
593+
testWidgets('empty topic -> (no topic)', (tester) async {
594+
await setupAndTapSend(tester, topicInputText: '');
595+
check(connection.lastRequest).isA<http.Request>()
596+
..method.equals('POST')
597+
..url.path.equals('/api/v1/messages')
598+
..bodyFields.deepEquals({
599+
'type': 'stream',
600+
'to': channel.streamId.toString(),
601+
'topic': '(no topic)',
602+
'content': 'test content',
603+
'read_by_sender': 'true',
604+
});
605+
});
606+
607+
testWidgets('if topics are mandatory, reject empty topic', (tester) async {
608+
await setupAndTapSend(tester,
609+
topicInputText: '',
610+
mandatoryTopics: true);
611+
checkMessageNotSent(tester);
612+
});
613+
614+
testWidgets('if topics are mandatory, reject (no topic)', (tester) async {
615+
await setupAndTapSend(tester,
616+
topicInputText: '(no topic)',
617+
mandatoryTopics: true);
618+
checkMessageNotSent(tester);
619+
});
620+
});
621+
561622
group('uploads', () {
562623
void checkAppearsLoading(WidgetTester tester, bool expected) {
563624
final sendButtonElement = tester.element(find.ancestor(

0 commit comments

Comments
 (0)