Skip to content

Commit f1646c4

Browse files
PIG208gnprice
authored andcommitted
compose: Respect realm setting for mandatory topics
Signed-off-by: Zixuan James Li <[email protected]>
1 parent f76b003 commit f1646c4

File tree

3 files changed

+84
-17
lines changed

3 files changed

+84
-17
lines changed

lib/widgets/compose_box.dart

Lines changed: 24 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,20 @@ 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+
if (_controller == null) {
1325+
_controller = StreamComposeBoxController(store: store);
1326+
} else {
1327+
(controller as StreamComposeBoxController).topic.store = store;
1328+
}
13211329
case TopicNarrow():
13221330
case DmNarrow():
13231331
_controller = FixedDestinationComposeBoxController();
@@ -1330,7 +1338,7 @@ class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
13301338

13311339
@override
13321340
void dispose() {
1333-
_controller.dispose();
1341+
controller.dispose();
13341342
super.dispose();
13351343
}
13361344

@@ -1370,15 +1378,16 @@ class _ComposeBoxState extends State<ComposeBox> implements ComposeBoxState {
13701378
return _ComposeBoxContainer(body: null, errorBanner: errorBanner);
13711379
}
13721380

1381+
final controller = this.controller;
13731382
final narrow = widget.narrow;
1374-
switch (_controller) {
1383+
switch (controller) {
13751384
case StreamComposeBoxController(): {
13761385
narrow as ChannelNarrow;
1377-
body = _StreamComposeBoxBody(controller: _controller, narrow: narrow);
1386+
body = _StreamComposeBoxBody(controller: controller, narrow: narrow);
13781387
}
13791388
case FixedDestinationComposeBoxController(): {
13801389
narrow as SendableNarrow;
1381-
body = _FixedDestinationComposeBoxBody(controller: _controller, narrow: narrow);
1390+
body = _FixedDestinationComposeBoxBody(controller: controller, narrow: narrow);
13821391
}
13831392
}
13841393

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: 58 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,60 @@ 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+
required 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,
595+
topicInputText: '',
596+
mandatoryTopics: false);
597+
check(connection.lastRequest).isA<http.Request>()
598+
..method.equals('POST')
599+
..url.path.equals('/api/v1/messages')
600+
..bodyFields['topic'].equals('(no topic)');
601+
});
602+
603+
testWidgets('if topics are mandatory, reject empty topic', (tester) async {
604+
await setupAndTapSend(tester,
605+
topicInputText: '',
606+
mandatoryTopics: true);
607+
checkMessageNotSent(tester);
608+
});
609+
610+
testWidgets('if topics are mandatory, reject "(no topic)"', (tester) async {
611+
await setupAndTapSend(tester,
612+
topicInputText: '(no topic)',
613+
mandatoryTopics: true);
614+
checkMessageNotSent(tester);
615+
});
616+
});
617+
561618
group('uploads', () {
562619
void checkAppearsLoading(WidgetTester tester, bool expected) {
563620
final sendButtonElement = tester.element(find.ancestor(

0 commit comments

Comments
 (0)