diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 118ab83c70..ec0de4dd8f 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -348,12 +348,11 @@ "@composeBoxSelfDmContentHint": { "description": "Hint text for content input when sending a message to yourself." }, - "composeBoxChannelContentHint": "Message #{channel} > {topic}", + "composeBoxChannelContentHint": "Message {destination}", "@composeBoxChannelContentHint": { - "description": "Hint text for content input when sending a message to a channel", + "description": "Hint text for content input when sending a message to a channel.", "placeholders": { - "channel": {"type": "String", "example": "channel name"}, - "topic": {"type": "String", "example": "topic name"} + "destination": {"type": "String", "example": "#channel name > topic name"} } }, "composeBoxSendTooltip": "Send", diff --git a/assets/l10n/app_pl.arb b/assets/l10n/app_pl.arb index 770a670212..c43e57e573 100644 --- a/assets/l10n/app_pl.arb +++ b/assets/l10n/app_pl.arb @@ -263,20 +263,6 @@ "@composeBoxSelfDmContentHint": { "description": "Hint text for content input when sending a message to yourself." }, - "composeBoxChannelContentHint": "Wiadomość #{channel} > {topic}", - "@composeBoxChannelContentHint": { - "description": "Hint text for content input when sending a message to a channel", - "placeholders": { - "channel": { - "type": "String", - "example": "channel name" - }, - "topic": { - "type": "String", - "example": "topic name" - } - } - }, "composeBoxTopicHintText": "Wątek", "@composeBoxTopicHintText": { "description": "Hint text for topic input widget in compose box." diff --git a/assets/l10n/app_ru.arb b/assets/l10n/app_ru.arb index ef38533bb2..5df7840006 100644 --- a/assets/l10n/app_ru.arb +++ b/assets/l10n/app_ru.arb @@ -373,20 +373,6 @@ "@composeBoxGenericContentHint": { "description": "Hint text for content input when sending a message." }, - "composeBoxChannelContentHint": "Сообщение для #{channel} > {topic}", - "@composeBoxChannelContentHint": { - "description": "Hint text for content input when sending a message to a channel", - "placeholders": { - "channel": { - "type": "String", - "example": "channel name" - }, - "topic": { - "type": "String", - "example": "topic name" - } - } - }, "composeBoxSendTooltip": "Отправить", "@composeBoxSendTooltip": { "description": "Tooltip for send button in compose box." diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 9579683908..1b339ce823 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -567,11 +567,11 @@ abstract class ZulipLocalizations { /// **'Jot down something'** String get composeBoxSelfDmContentHint; - /// Hint text for content input when sending a message to a channel + /// Hint text for content input when sending a message to a channel. /// /// In en, this message translates to: - /// **'Message #{channel} > {topic}'** - String composeBoxChannelContentHint(String channel, String topic); + /// **'Message {destination}'** + String composeBoxChannelContentHint(String destination); /// Tooltip for send button in compose box. /// diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart index 71bf06d8ce..890bf68596 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -276,8 +276,8 @@ class ZulipLocalizationsAr extends ZulipLocalizations { String get composeBoxSelfDmContentHint => 'Jot down something'; @override - String composeBoxChannelContentHint(String channel, String topic) { - return 'Message #$channel > $topic'; + String composeBoxChannelContentHint(String destination) { + return 'Message $destination'; } @override diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 7a33e33567..72b7e9ad69 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -276,8 +276,8 @@ class ZulipLocalizationsEn extends ZulipLocalizations { String get composeBoxSelfDmContentHint => 'Jot down something'; @override - String composeBoxChannelContentHint(String channel, String topic) { - return 'Message #$channel > $topic'; + String composeBoxChannelContentHint(String destination) { + return 'Message $destination'; } @override diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index 137883e5e9..5bfacf60d9 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -276,8 +276,8 @@ class ZulipLocalizationsJa extends ZulipLocalizations { String get composeBoxSelfDmContentHint => 'Jot down something'; @override - String composeBoxChannelContentHint(String channel, String topic) { - return 'Message #$channel > $topic'; + String composeBoxChannelContentHint(String destination) { + return 'Message $destination'; } @override diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index 3dec7d9b5a..9698d16c96 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -276,8 +276,8 @@ class ZulipLocalizationsNb extends ZulipLocalizations { String get composeBoxSelfDmContentHint => 'Jot down something'; @override - String composeBoxChannelContentHint(String channel, String topic) { - return 'Message #$channel > $topic'; + String composeBoxChannelContentHint(String destination) { + return 'Message $destination'; } @override diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index 83a777bfc4..01bee756da 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -276,8 +276,8 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get composeBoxSelfDmContentHint => 'Zanotuj coś na przyszłość'; @override - String composeBoxChannelContentHint(String channel, String topic) { - return 'Wiadomość #$channel > $topic'; + String composeBoxChannelContentHint(String destination) { + return 'Message $destination'; } @override diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 827aaf0155..2c01dddb61 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -276,8 +276,8 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get composeBoxSelfDmContentHint => 'Сделать заметку'; @override - String composeBoxChannelContentHint(String channel, String topic) { - return 'Сообщение для #$channel > $topic'; + String composeBoxChannelContentHint(String destination) { + return 'Message $destination'; } @override diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 38a3f8a240..a69cfc2d8a 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -276,8 +276,8 @@ class ZulipLocalizationsSk extends ZulipLocalizations { String get composeBoxSelfDmContentHint => 'Jot down something'; @override - String composeBoxChannelContentHint(String channel, String topic) { - return 'Message #$channel > $topic'; + String composeBoxChannelContentHint(String destination) { + return 'Message $destination'; } @override diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 6dcb1ac5fe..0a060a6bcf 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -160,10 +160,17 @@ class ComposeTopicController extends ComposeController { return trimmed.isEmpty ? kNoTopicTopic : trimmed; } + /// Whether [textNormalized] would fail a mandatory-topics check + /// (see [mandatory]). + /// + /// The term "Vacuous" draws distinction from [String.isEmpty], in the sense + /// that certain strings are not empty but also indicate the absence of a topic. + bool get isTopicVacuous => textNormalized == kNoTopicTopic; + @override List _computeValidationErrors() { return [ - if (mandatory && textNormalized == kNoTopicTopic) + if (mandatory && isTopicVacuous) TopicValidationError.mandatoryButEmpty, if ( @@ -577,13 +584,17 @@ class _StreamContentInputState extends State<_StreamContentInput> { final zulipLocalizations = ZulipLocalizations.of(context); final streamName = store.streams[widget.narrow.streamId]?.name ?? zulipLocalizations.unknownChannelName; + final topic = TopicName(widget.controller.topic.textNormalized); return _ContentInput( narrow: widget.narrow, - destination: TopicNarrow(widget.narrow.streamId, - TopicName(widget.controller.topic.textNormalized)), + destination: TopicNarrow(widget.narrow.streamId, topic), controller: widget.controller, - hintText: zulipLocalizations.composeBoxChannelContentHint(streamName, - widget.controller.topic.textNormalized)); + hintText: zulipLocalizations.composeBoxChannelContentHint( + // No i18n of this use of "#" and ">" string; those are part of how + // Zulip expresses channels and topics, not any normal English punctuation, + // so don't make sense to translate. See: + // https://github.com/zulip/zulip-flutter/pull/1148#discussion_r1941990585 + '#$streamName > ${topic.displayName}')); } } @@ -642,7 +653,11 @@ class _FixedDestinationContentInput extends StatelessWidget { final streamName = store.streams[streamId]?.name ?? zulipLocalizations.unknownChannelName; return zulipLocalizations.composeBoxChannelContentHint( - streamName, topic.displayName); + // No i18n of this use of "#" and ">" string; those are part of how + // Zulip expresses channels and topics, not any normal English punctuation, + // so don't make sense to translate. See: + // https://github.com/zulip/zulip-flutter/pull/1148#discussion_r1941990585 + '#$streamName > ${topic.displayName}'); case DmNarrow(otherRecipientIds: []): // The self-1:1 thread. return zulipLocalizations.composeBoxSelfDmContentHint; diff --git a/test/widgets/compose_box_test.dart b/test/widgets/compose_box_test.dart index 896dec7efe..52d2d1c851 100644 --- a/test/widgets/compose_box_test.dart +++ b/test/widgets/compose_box_test.dart @@ -78,14 +78,17 @@ void main() { controller = tester.state(find.byType(ComposeBox)).controller; } + /// A [Finder] for the topic input. + /// + /// To enter some text, use [enterTopic]. + final topicInputFinder = find.byWidgetPredicate( + (widget) => widget is TextField && widget.controller is ComposeTopicController); + /// Set the topic input's text to [topic], using [WidgetTester.enterText]. Future enterTopic(WidgetTester tester, { required ChannelNarrow narrow, required String topic, }) async { - final topicInputFinder = find.byWidgetPredicate( - (widget) => widget is TextField && widget.controller is ComposeTopicController); - connection.prepare(body: jsonEncode(GetStreamTopicsResult(topics: [eg.getStreamTopicsEntry()]).toJson())); await tester.enterText(topicInputFinder, topic); @@ -318,6 +321,85 @@ void main() { }); }); + group('ComposeBox hintText', () { + final channel = eg.stream(); + + Future prepare(WidgetTester tester, { + required Narrow narrow, + }) async { + await prepareComposeBox(tester, + narrow: narrow, + otherUsers: [eg.otherUser, eg.thirdUser], + streams: [channel]); + } + + /// This checks the input's configured hint text without regard to whether + /// it's currently visible, as it won't be if the user has entered some text. + /// + /// If `topicHintText` is `null`, check that the topic input is not present. + void checkComposeBoxHintTexts(WidgetTester tester, { + String? topicHintText, + required String contentHintText, + }) { + if (topicHintText != null) { + check(tester.widget(topicInputFinder)) + .decoration.isNotNull().hintText.equals(topicHintText); + } else { + check(topicInputFinder).findsNothing(); + } + check(tester.widget(contentInputFinder)) + .decoration.isNotNull().hintText.equals(contentHintText); + } + + group('to ChannelNarrow', () { + testWidgets('with empty topic', (tester) async { + await prepare(tester, narrow: ChannelNarrow(channel.streamId)); + checkComposeBoxHintTexts(tester, + topicHintText: 'Topic', + contentHintText: 'Message #${channel.name} > (no topic)'); + }); + + testWidgets('with non-empty topic', (tester) async { + final narrow = ChannelNarrow(channel.streamId); + await prepare(tester, narrow: narrow); + await enterTopic(tester, narrow: narrow, topic: 'new topic'); + await tester.pump(); + checkComposeBoxHintTexts(tester, + topicHintText: 'Topic', + contentHintText: 'Message #${channel.name} > new topic'); + }); + }); + + testWidgets('to TopicNarrow', (tester) async { + await prepare(tester, + narrow: TopicNarrow(channel.streamId, TopicName('topic'))); + checkComposeBoxHintTexts(tester, + contentHintText: 'Message #${channel.name} > topic'); + }); + + testWidgets('to DmNarrow with self', (tester) async { + await prepare(tester, narrow: DmNarrow.withUser( + eg.selfUser.userId, selfUserId: eg.selfUser.userId)); + checkComposeBoxHintTexts(tester, + contentHintText: 'Jot down something'); + }); + + testWidgets('to 1:1 DmNarrow', (tester) async { + await prepare(tester, narrow: DmNarrow.withUser( + eg.otherUser.userId, selfUserId: eg.selfUser.userId)); + checkComposeBoxHintTexts(tester, + contentHintText: 'Message @${eg.otherUser.fullName}'); + }); + + testWidgets('to group DmNarrow', (tester) async { + await prepare(tester, narrow: DmNarrow.withOtherUsers( + [eg.otherUser.userId, eg.thirdUser.userId], + selfUserId: eg.selfUser.userId)); + checkComposeBoxHintTexts(tester, + contentHintText: 'Message group'); + }); + }); + group('ComposeBox textCapitalization', () { void checkComposeBoxTextFields(WidgetTester tester, { required bool expectTopicTextField,