diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 64d867891a..bd31bdd015 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -102,16 +102,24 @@ class ComposeTopicController extends ComposeController { // TODO(#307) use `max_topic_length` instead of hardcoded limit @override final maxLengthUnicodeCodePoints = kMaxTopicLengthCodePoints; + // TODO(server-10): simplify + String get noTopicTopic => store.connection.zulipFeatureLevel! >= 334 + ? '' : kNoTopicTopic; + @override String _computeTextNormalized() { String trimmed = text.trim(); - return trimmed.isEmpty ? kNoTopicTopic : trimmed; + return trimmed.isEmpty ? noTopicTopic : trimmed; } @override List _computeValidationErrors() { return [ - if (mandatory && textNormalized == kNoTopicTopic) + if (mandatory && ( + // While this may appear repetitive, the latter is not guarded behind + // a `zulipFeatureLevel` check. "(no topic)" still gets rejected + // by newer servers when topics are mandatory. + textNormalized == noTopicTopic || textNormalized == kNoTopicTopic)) TopicValidationError.mandatoryButEmpty, if ( diff --git a/test/widgets/compose_box_test.dart b/test/widgets/compose_box_test.dart index 46d5fbe635..550fff8d0e 100644 --- a/test/widgets/compose_box_test.dart +++ b/test/widgets/compose_box_test.dart @@ -680,6 +680,7 @@ void main() { late ZulipStream channel; Future setupAndTapSend(WidgetTester tester, { + Account? account, bool? mandatoryTopics, required String topicInputText, }) async { @@ -688,8 +689,10 @@ void main() { addTearDown(testBinding.reset); channel = eg.stream(); - final account = eg.account(user: eg.selfUser); + account ??= eg.account( + user: eg.selfUser, zulipFeatureLevel: eg.futureZulipFeatureLevel); final initialSnapshot = eg.initialSnapshot( + zulipFeatureLevel: account.zulipFeatureLevel, realmMandatoryTopics: mandatoryTopics); await testBinding.globalStore.add(account, initialSnapshot); @@ -714,8 +717,23 @@ void main() { expectedMessage: 'Topics are required in this organization.'); } - testWidgets('empty topic -> (no topic)', (tester) async { + testWidgets('empty topic -> general chat', (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': eg.defaultRealmEmptyTopicDisplayName, + 'content': 'test content', + 'read_by_sender': 'true', + }); + }); + + testWidgets('legacy: empty topic -> (no topic)', (tester) async { + await setupAndTapSend(tester, topicInputText: '', + account: eg.account(user: eg.selfUser, zulipFeatureLevel: 333)); check(connection.lastRequest).isA() ..method.equals('POST') ..url.path.equals('/api/v1/messages') @@ -734,11 +752,24 @@ void main() { checkMessageNotSent(tester); }); + testWidgets('if topics are mandatory, reject `realmEmptyTopicDisplayName`', (tester) async { + await setupAndTapSend(tester, topicInputText: eg.defaultRealmEmptyTopicDisplayName, + mandatoryTopics: true); + checkMessageNotSent(tester); + }); + testWidgets('if topics are mandatory, reject (no topic)', (tester) async { await setupAndTapSend(tester, topicInputText: '(no topic)', mandatoryTopics: true); checkMessageNotSent(tester); }); + + testWidgets('legacy: if topics are mandatory, reject (no topic)', (tester) async { + await setupAndTapSend(tester, topicInputText: '(no topic)', + account: eg.account(user: eg.selfUser, zulipFeatureLevel: 333), + mandatoryTopics: true); + checkMessageNotSent(tester); + }); }); group('uploads', () {