Skip to content

Commit 3e102c0

Browse files
committed
compose: Support sending to empty topic
This behavior is designed to replace how sending to an empty topic is effectively sending to "(no topic)". The key difference being that the `TopicName.apiValue` is actually empty, instead of being converted to "(no topic)" with `_computeTextNormalized`. Signed-off-by: Zixuan James Li <[email protected]>
1 parent ab4bf20 commit 3e102c0

File tree

2 files changed

+85
-6
lines changed

2 files changed

+85
-6
lines changed

lib/widgets/compose_box.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,34 @@ class ComposeTopicController extends ComposeController<TopicValidationError> {
157157
@override
158158
String _computeTextNormalized() {
159159
String trimmed = text.trim();
160-
return trimmed.isEmpty ? kNoTopicTopic : trimmed;
160+
// TODO(server-10): simplify
161+
if (store.connection.zulipFeatureLevel! < 334) {
162+
return trimmed.isEmpty ? kNoTopicTopic : trimmed;
163+
}
164+
165+
return trimmed;
161166
}
162167

163168
/// Whether [textNormalized] would fail a mandatory-topics check
164169
/// (see [mandatory]).
165170
///
166171
/// The term "Vacuous" draws distinction from [String.isEmpty], in the sense
167172
/// that certain strings are not empty but also indicate the absence of a topic.
168-
bool get isTopicVacuous => textNormalized == kNoTopicTopic;
173+
bool get isTopicVacuous {
174+
bool result = textNormalized.isEmpty
175+
// We keep checking for '(no topic)' regardless of the feature level
176+
// because it remains equivalent to an empty topic even when FL >= 334.
177+
// This can change in the future:
178+
// https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/.28realm_.29mandatory_topics.20behavior/near/2062391
179+
|| textNormalized == kNoTopicTopic;
180+
181+
// TODO(server-10): simplify
182+
if (store.connection.zulipFeatureLevel! >= 334) {
183+
result |= textNormalized == store.realmEmptyTopicDisplayName;
184+
}
185+
186+
return result;
187+
}
169188

170189
/// The send destination as a string.
171190
///
@@ -181,6 +200,14 @@ class ComposeTopicController extends ComposeController<TopicValidationError> {
181200
if (mandatory && isTopicVacuous) {
182201
return '#$streamName';
183202
}
203+
204+
// TODO(server-10): simplify
205+
if (textNormalized.isEmpty) {
206+
// [textNormalized] cannot be empty prior to empty topics.
207+
assert(store.connection.zulipFeatureLevel! >= 334);
208+
return '#$streamName > ${store.realmEmptyTopicDisplayName}';
209+
}
210+
184211
return '#$streamName > $textNormalized';
185212
}
186213

test/widgets/compose_box_test.dart

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,18 @@ void main() {
4747
List<User> otherUsers = const [],
4848
List<ZulipStream> streams = const [],
4949
bool? mandatoryTopics,
50+
int? zulipFeatureLevel,
5051
}) async {
5152
if (narrow case ChannelNarrow(:var streamId) || TopicNarrow(: var streamId)) {
5253
assert(streams.any((stream) => stream.streamId == streamId),
5354
'Add a channel with "streamId" the same as of $narrow.streamId to the store.');
5455
}
5556
addTearDown(testBinding.reset);
5657
selfUser ??= eg.selfUser;
57-
final selfAccount = eg.account(user: selfUser);
58+
zulipFeatureLevel ??= eg.futureZulipFeatureLevel;
59+
final selfAccount = eg.account(user: selfUser, zulipFeatureLevel: zulipFeatureLevel);
5860
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot(
61+
zulipFeatureLevel: zulipFeatureLevel,
5962
realmMandatoryTopics: mandatoryTopics,
6063
));
6164

@@ -327,12 +330,14 @@ void main() {
327330
Future<void> prepare(WidgetTester tester, {
328331
required Narrow narrow,
329332
bool? mandatoryTopics,
333+
int? zulipFeatureLevel,
330334
}) async {
331335
await prepareComposeBox(tester,
332336
narrow: narrow,
333337
otherUsers: [eg.otherUser, eg.thirdUser],
334338
streams: [channel],
335-
mandatoryTopics: mandatoryTopics);
339+
mandatoryTopics: mandatoryTopics,
340+
zulipFeatureLevel: zulipFeatureLevel);
336341
}
337342

338343
/// This checks the input's configured hint text without regard to whether
@@ -357,6 +362,16 @@ void main() {
357362
testWidgets('with empty topic', (tester) async {
358363
await prepare(tester, narrow: ChannelNarrow(channel.streamId),
359364
mandatoryTopics: false);
365+
await tester.pump();
366+
checkComposeBoxHintTexts(tester,
367+
topicHintText: 'Topic',
368+
contentHintText: 'Message #${channel.name} > ${eg.defaultRealmEmptyTopicDisplayName}');
369+
}, skip: true); // null topic names soon to be enabled
370+
371+
testWidgets('legacy: with empty topic', (tester) async {
372+
await prepare(tester, narrow: ChannelNarrow(channel.streamId),
373+
mandatoryTopics: false,
374+
zulipFeatureLevel: 333);
360375
checkComposeBoxHintTexts(tester,
361376
topicHintText: 'Topic',
362377
contentHintText: 'Message #${channel.name} > (no topic)');
@@ -383,6 +398,15 @@ void main() {
383398
contentHintText: 'Message #${channel.name}');
384399
});
385400

401+
testWidgets('legacy: with empty topic', (tester) async {
402+
await prepare(tester, narrow: ChannelNarrow(channel.streamId),
403+
mandatoryTopics: true,
404+
zulipFeatureLevel: 333);
405+
checkComposeBoxHintTexts(tester,
406+
topicHintText: 'Topic',
407+
contentHintText: 'Message #${channel.name}');
408+
});
409+
386410
testWidgets('with non-empty topic', (tester) async {
387411
final narrow = ChannelNarrow(channel.streamId);
388412
await prepare(tester, narrow: narrow,
@@ -689,6 +713,7 @@ void main() {
689713
Future<void> setupAndTapSend(WidgetTester tester, {
690714
required String topicInputText,
691715
required bool mandatoryTopics,
716+
int? zulipFeatureLevel,
692717
}) async {
693718
TypingNotifier.debugEnable = false;
694719
addTearDown(TypingNotifier.debugReset);
@@ -697,7 +722,8 @@ void main() {
697722
final narrow = ChannelNarrow(channel.streamId);
698723
await prepareComposeBox(tester,
699724
narrow: narrow, streams: [channel],
700-
mandatoryTopics: mandatoryTopics);
725+
mandatoryTopics: mandatoryTopics,
726+
zulipFeatureLevel: zulipFeatureLevel);
701727

702728
await enterTopic(tester, narrow: narrow, topic: topicInputText);
703729
await tester.enterText(contentInputFinder, 'test content');
@@ -712,10 +738,21 @@ void main() {
712738
expectedMessage: 'Topics are required in this organization.');
713739
}
714740

715-
testWidgets('empty topic -> "(no topic)"', (tester) async {
741+
testWidgets('empty topic -> empty topic', (tester) async {
716742
await setupAndTapSend(tester,
717743
topicInputText: '',
718744
mandatoryTopics: false);
745+
check(connection.lastRequest).isA<http.Request>()
746+
..method.equals('POST')
747+
..url.path.equals('/api/v1/messages')
748+
..bodyFields['topic'].equals('');
749+
}, skip: true); // null topic names soon to be enabled
750+
751+
testWidgets('legacy: empty topic -> "(no topic)"', (tester) async {
752+
await setupAndTapSend(tester,
753+
topicInputText: '',
754+
mandatoryTopics: false,
755+
zulipFeatureLevel: 333);
719756
check(connection.lastRequest).isA<http.Request>()
720757
..method.equals('POST')
721758
..url.path.equals('/api/v1/messages')
@@ -729,12 +766,27 @@ void main() {
729766
checkMessageNotSent(tester);
730767
});
731768

769+
testWidgets('if topics are mandatory, reject `realmEmptyTopicDisplayName`', (tester) async {
770+
await setupAndTapSend(tester,
771+
topicInputText: eg.defaultRealmEmptyTopicDisplayName,
772+
mandatoryTopics: true);
773+
checkMessageNotSent(tester);
774+
}, skip: true); // null topic names soon to be enabled
775+
732776
testWidgets('if topics are mandatory, reject "(no topic)"', (tester) async {
733777
await setupAndTapSend(tester,
734778
topicInputText: '(no topic)',
735779
mandatoryTopics: true);
736780
checkMessageNotSent(tester);
737781
});
782+
783+
testWidgets('legacy: if topics are mandatory, reject "(no topic)"', (tester) async {
784+
await setupAndTapSend(tester,
785+
topicInputText: '(no topic)',
786+
mandatoryTopics: true,
787+
zulipFeatureLevel: 333);
788+
checkMessageNotSent(tester);
789+
});
738790
});
739791

740792
group('uploads', () {

0 commit comments

Comments
 (0)