Skip to content

Get compose box hint texts ready for general chat #1342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 0 additions & 14 deletions assets/l10n/app_pl.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
14 changes: 0 additions & 14 deletions assets/l10n/app_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
6 changes: 3 additions & 3 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_nb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/generated/l10n/zulip_localizations_sk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 21 additions & 6 deletions lib/widgets/compose_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,17 @@ class ComposeTopicController extends ComposeController<TopicValidationError> {
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<TopicValidationError> _computeValidationErrors() {
return [
if (mandatory && textNormalized == kNoTopicTopic)
if (mandatory && isTopicVacuous)
TopicValidationError.mandatoryButEmpty,

if (
Expand Down Expand Up @@ -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}'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think this is a case where much of the information in the commit message is also relevant to understanding the resulting code (not only to understanding its history). So:

Suggested change
'#$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}'));

}
}

Expand Down Expand Up @@ -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;
Expand Down
88 changes: 85 additions & 3 deletions test/widgets/compose_box_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,17 @@ void main() {
controller = tester.state<ComposeBoxState>(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<void> 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);
Expand Down Expand Up @@ -318,6 +321,85 @@ void main() {
});
});

group('ComposeBox hintText', () {
final channel = eg.stream();

Future<void> 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.
Comment on lines +336 to +337
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is a bit unsatisfying because it's checking a condition the user doesn't see. Ideally we'd be checking whether the user actually sees a hint, and if so then what it is.

From the thread at #1342 (comment) it sounds like getting that ideal behavior might be more complicated, though. In that case it's fine to let this be — the question of whether a hint is visible isn't critical for us to test, particularly as it's mostly the responsibility of the framework's underlying text-input code anyway.

///
/// 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<TextField>(topicInputFinder))
.decoration.isNotNull().hintText.equals(topicHintText);
} else {
check(topicInputFinder).findsNothing();
}
check(tester.widget<TextField>(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',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I'm surprised this test passes. When using the app, when the topic input is empty, I see the hint text "Topic" there—but I don't see (or expect to see) hint text after I've typed something; I just see the text I've typed. Do you know why this is passing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper checks TextField.decoration.hintText directly, but does not test if hintText is invisible when the input is non-empty (it does check if the input exists or not at all). We could figure out a way to construct the finder that distinguishes the content and the hint text, but I felt that upstream covers this aspect already. Maybe we can add a dartdoc for this helper?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper checks TextField.decoration.hintText directly

Does it? I don't see .hintText in its implementation:

    void checkComposeBoxHintTexts(WidgetTester tester, {
      String? topicHintText,
      required String contentHintText,
    }) {
      if (topicHintText != null) {
        check(find.descendant(
          of: topicInputFinder, matching: find.text(topicHintText))).findsOne();
      } else {
        check(topicInputFinder).findsNothing();
      }
      check(find.descendant(
        of: contentInputFinder, matching: find.text(contentHintText))).findsOne();
    }

Copy link
Member Author

@PIG208 PIG208 Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I was mistaken. It doesn't check for the controller but find.text still ignores that the hint text can be invisible.

I was probably thinking of a different revision commenting on this.

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,
Expand Down
Loading