Skip to content

Commit 897788d

Browse files
committed
compose: Change topic hint text conditionally
This is implemented to match web's behavior. See CZO discussion for design details: https://chat.zulip.org/#narrow/channel/530-mobile-design/topic/general.20chat.20design.20.23F1297/near/2106736 Signed-off-by: Zixuan James Li <[email protected]>
1 parent a3a2e97 commit 897788d

12 files changed

+121
-16
lines changed

assets/l10n/app_en.arb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,13 @@
379379
"@composeBoxTopicHintText": {
380380
"description": "Hint text for topic input widget in compose box."
381381
},
382+
"composeBoxEnterTopicOrSkipHintText": "Enter a topic (skip for “{defaultTopicName}”)",
383+
"@composeBoxEnterTopicOrSkipHintText": {
384+
"description": "Hint text for topic input widget in compose box when topics are optional.",
385+
"placeholders": {
386+
"defaultTopicName": {"type": "String", "example": "general chat"}
387+
}
388+
},
382389
"composeBoxUploadingFilename": "Uploading {filename}…",
383390
"@composeBoxUploadingFilename": {
384391
"description": "Placeholder in compose box showing the specified file is currently uploading.",

lib/generated/l10n/zulip_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,12 @@ abstract class ZulipLocalizations {
609609
/// **'Topic'**
610610
String get composeBoxTopicHintText;
611611

612+
/// Hint text for topic input widget in compose box when topics are optional.
613+
///
614+
/// In en, this message translates to:
615+
/// **'Enter a topic (skip for “{defaultTopicName}”)'**
616+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName);
617+
612618
/// Placeholder in compose box showing the specified file is currently uploading.
613619
///
614620
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
298298
@override
299299
String get composeBoxTopicHintText => 'Topic';
300300

301+
@override
302+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
303+
return 'Enter a topic (skip for “$defaultTopicName”)';
304+
}
305+
301306
@override
302307
String composeBoxUploadingFilename(String filename) {
303308
return 'Uploading $filename…';

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
298298
@override
299299
String get composeBoxTopicHintText => 'Topic';
300300

301+
@override
302+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
303+
return 'Enter a topic (skip for “$defaultTopicName”)';
304+
}
305+
301306
@override
302307
String composeBoxUploadingFilename(String filename) {
303308
return 'Uploading $filename…';

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
298298
@override
299299
String get composeBoxTopicHintText => 'Topic';
300300

301+
@override
302+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
303+
return 'Enter a topic (skip for “$defaultTopicName”)';
304+
}
305+
301306
@override
302307
String composeBoxUploadingFilename(String filename) {
303308
return 'Uploading $filename…';

lib/generated/l10n/zulip_localizations_nb.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
298298
@override
299299
String get composeBoxTopicHintText => 'Topic';
300300

301+
@override
302+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
303+
return 'Enter a topic (skip for “$defaultTopicName”)';
304+
}
305+
301306
@override
302307
String composeBoxUploadingFilename(String filename) {
303308
return 'Uploading $filename…';

lib/generated/l10n/zulip_localizations_pl.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
298298
@override
299299
String get composeBoxTopicHintText => 'Wątek';
300300

301+
@override
302+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
303+
return 'Enter a topic (skip for “$defaultTopicName”)';
304+
}
305+
301306
@override
302307
String composeBoxUploadingFilename(String filename) {
303308
return 'Przekazywanie $filename…';

lib/generated/l10n/zulip_localizations_ru.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
298298
@override
299299
String get composeBoxTopicHintText => 'Тема';
300300

301+
@override
302+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
303+
return 'Enter a topic (skip for “$defaultTopicName”)';
304+
}
305+
301306
@override
302307
String composeBoxUploadingFilename(String filename) {
303308
return 'Загрузка $filename…';

lib/generated/l10n/zulip_localizations_sk.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
298298
@override
299299
String get composeBoxTopicHintText => 'Topic';
300300

301+
@override
302+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
303+
return 'Enter a topic (skip for “$defaultTopicName”)';
304+
}
305+
301306
@override
302307
String composeBoxUploadingFilename(String filename) {
303308
return 'Uploading $filename…';

lib/widgets/compose_box.dart

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ class _TopicInputState extends State<_TopicInput> {
686686
void initState() {
687687
super.initState();
688688
widget.controller.topicFocusNode.addListener(_topicFocusChanged);
689+
widget.controller.hasChosenTopic.addListener(_hasChosenTopicChanged);
689690
}
690691

691692
@override
@@ -695,11 +696,16 @@ class _TopicInputState extends State<_TopicInput> {
695696
oldWidget.controller.topicFocusNode.removeListener(_topicFocusChanged);
696697
widget.controller.topicFocusNode.addListener(_topicFocusChanged);
697698
}
699+
if (widget.controller.hasChosenTopic != oldWidget.controller.hasChosenTopic) {
700+
oldWidget.controller.hasChosenTopic.removeListener(_hasChosenTopicChanged);
701+
widget.controller.hasChosenTopic.addListener(_hasChosenTopicChanged);
702+
}
698703
}
699704

700705
@override
701706
void dispose() {
702707
widget.controller.topicFocusNode.removeListener(_topicFocusChanged);
708+
widget.controller.hasChosenTopic.removeListener(_hasChosenTopicChanged);
703709
super.dispose();
704710
}
705711

@@ -712,16 +718,56 @@ class _TopicInputState extends State<_TopicInput> {
712718
}
713719
}
714720

721+
void _hasChosenTopicChanged() {
722+
setState(() {
723+
// The relevant state lives on widget.controller.hasChosenTopic itself.
724+
});
725+
}
726+
727+
/// Return the default topic display name to use, or `null` if topics are
728+
/// required.
729+
String? _defaultTopicDisplayName() {
730+
final store = PerAccountStoreWidget.of(context);
731+
if (store.realmMandatoryTopics) {
732+
return null;
733+
}
734+
735+
// TODO(server-10) simplify
736+
return store.zulipFeatureLevel >= 334
737+
? store.realmEmptyTopicDisplayName : kNoTopicTopic;
738+
}
739+
715740
@override
716741
Widget build(BuildContext context) {
717742
final zulipLocalizations = ZulipLocalizations.of(context);
718743
final designVariables = DesignVariables.of(context);
744+
final store = PerAccountStoreWidget.of(context);
719745
TextStyle topicTextStyle = TextStyle(
720746
fontSize: 20,
721747
height: 22 / 20,
722748
color: designVariables.textInput.withFadedAlpha(0.9),
723749
).merge(weightVariableTextStyle(context, wght: 600));
724750

751+
String hintText = zulipLocalizations.composeBoxTopicHintText;
752+
TextStyle hintStyle = topicTextStyle.copyWith(
753+
color: designVariables.textInput.withFadedAlpha(0.5));
754+
final defaultTopicDisplayName = _defaultTopicDisplayName();
755+
if (defaultTopicDisplayName != null) {
756+
if (widget.controller.topicFocusNode.hasFocus) {
757+
// The user is actively interacting with the input.
758+
// Show a long and engaging hint text.
759+
hintText = zulipLocalizations.composeBoxEnterTopicOrSkipHintText(
760+
defaultTopicDisplayName);
761+
} else if (widget.controller.hasChosenTopic.value) {
762+
// The topic has been chosen. Show the default topic display name in
763+
// the input as if the user has entered that when they left it empty.
764+
hintText = defaultTopicDisplayName;
765+
hintStyle = topicTextStyle.copyWith(
766+
// TODO(server-10) simplify
767+
fontStyle: store.zulipFeatureLevel >= 334 ? FontStyle.italic : null);
768+
}
769+
}
770+
725771
return TopicAutocomplete(
726772
streamId: widget.streamId,
727773
controller: widget.controller.topic,
@@ -738,9 +784,8 @@ class _TopicInputState extends State<_TopicInput> {
738784
textInputAction: TextInputAction.next,
739785
style: topicTextStyle,
740786
decoration: InputDecoration(
741-
hintText: zulipLocalizations.composeBoxTopicHintText,
742-
hintStyle: topicTextStyle.copyWith(
743-
color: designVariables.textInput.withFadedAlpha(0.5))))));
787+
hintText: hintText,
788+
hintStyle: hintStyle))));
744789
}
745790
}
746791

test/flutter_checks.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ extension TextStyleChecks on Subject<TextStyle> {
9999
Subject<bool> get inherit => has((t) => t.inherit, 'inherit');
100100
Subject<Color?> get color => has((t) => t.color, 'color');
101101
Subject<double?> get fontSize => has((t) => t.fontSize, 'fontSize');
102+
Subject<FontStyle?> get fontStyle => has((t) => t.fontStyle, 'fontStyle');
102103
Subject<FontWeight?> get fontWeight => has((t) => t.fontWeight, 'fontWeight');
103104
Subject<double?> get letterSpacing => has((t) => t.letterSpacing, 'letterSpacing');
104105
Subject<List<FontVariation>?> get fontVariations => has((t) => t.fontVariations, 'fontVariations');
@@ -170,6 +171,7 @@ extension MaterialChecks on Subject<Material> {
170171

171172
extension InputDecorationChecks on Subject<InputDecoration> {
172173
Subject<String?> get hintText => has((x) => x.hintText, 'hintText');
174+
Subject<TextStyle?> get hintStyle => has((x) => x.hintStyle, 'hintStyle');
173175
}
174176

175177
extension RadioListTileChecks<T> on Subject<RadioListTile<T>> {

test/widgets/compose_box_test.dart

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,8 @@ void main() {
366366
await enterTopic(tester, narrow: narrow, topic: '');
367367
await tester.pump();
368368
checkComposeBoxHintTexts(tester,
369-
topicHintText: 'Topic',
369+
topicHintText: 'Enter a topic '
370+
'(skip for “${eg.defaultRealmEmptyTopicDisplayName}”)',
370371
contentHintText: 'Message #${channel.name}');
371372
});
372373

@@ -376,7 +377,7 @@ void main() {
376377
await enterTopic(tester, narrow: narrow, topic: '');
377378
await tester.pump();
378379
checkComposeBoxHintTexts(tester,
379-
topicHintText: 'Topic',
380+
topicHintText: 'Enter a topic (skip for “(no topic)”)',
380381
contentHintText: 'Message #${channel.name}');
381382
});
382383

@@ -386,7 +387,8 @@ void main() {
386387
topic: eg.defaultRealmEmptyTopicDisplayName);
387388
await tester.pump();
388389
checkComposeBoxHintTexts(tester,
389-
topicHintText: 'Topic',
390+
topicHintText: 'Enter a topic '
391+
'(skip for “${eg.defaultRealmEmptyTopicDisplayName}”)',
390392
contentHintText: 'Message #${channel.name}');
391393
});
392394

@@ -395,13 +397,14 @@ void main() {
395397
await enterTopic(tester, narrow: narrow, topic: '');
396398
await tester.pump();
397399
checkComposeBoxHintTexts(tester,
398-
topicHintText: 'Topic',
400+
topicHintText: 'Enter a topic '
401+
'(skip for “${eg.defaultRealmEmptyTopicDisplayName}”)',
399402
contentHintText: 'Message #${channel.name}');
400403

401404
await enterContent(tester, '');
402405
await tester.pump();
403406
checkComposeBoxHintTexts(tester,
404-
topicHintText: 'Topic',
407+
topicHintText: eg.defaultRealmEmptyTopicDisplayName,
405408
contentHintText: 'Message #${channel.name} > '
406409
'${eg.defaultRealmEmptyTopicDisplayName}');
407410
});
@@ -411,7 +414,8 @@ void main() {
411414
await enterTopic(tester, narrow: narrow, topic: '');
412415
await tester.pump();
413416
checkComposeBoxHintTexts(tester,
414-
topicHintText: 'Topic',
417+
topicHintText: 'Enter a topic '
418+
'(skip for “${eg.defaultRealmEmptyTopicDisplayName}”)',
415419
contentHintText: 'Message #${channel.name}');
416420

417421
FocusManager.instance.primaryFocus!.unfocus();
@@ -426,9 +430,11 @@ void main() {
426430
await enterContent(tester, '');
427431
await tester.pump();
428432
checkComposeBoxHintTexts(tester,
429-
topicHintText: 'Topic',
433+
topicHintText: eg.defaultRealmEmptyTopicDisplayName,
430434
contentHintText: 'Message #${channel.name} > '
431435
'${eg.defaultRealmEmptyTopicDisplayName}');
436+
check(tester.widget<TextField>(topicInputFinder)).decoration.isNotNull()
437+
.hintStyle.isNotNull().fontStyle.equals(FontStyle.italic);
432438
});
433439

434440
testWidgets('legacy: with empty topic, content input has focus', (tester) async {
@@ -437,23 +443,26 @@ void main() {
437443
await enterContent(tester, '');
438444
await tester.pump();
439445
checkComposeBoxHintTexts(tester,
440-
topicHintText: 'Topic',
446+
topicHintText: '(no topic)',
441447
contentHintText: 'Message #${channel.name} > (no topic)');
448+
check(tester.widget<TextField>(topicInputFinder)).decoration.isNotNull()
449+
.hintStyle.isNotNull().fontStyle.isNull();
442450
});
443451

444452
testWidgets('with empty topic, content input has focus, then topic input gains focus', (tester) async {
445453
await prepare(tester, narrow: narrow, mandatoryTopics: false);
446454
await enterContent(tester, '');
447455
await tester.pump();
448456
checkComposeBoxHintTexts(tester,
449-
topicHintText: 'Topic',
457+
topicHintText: eg.defaultRealmEmptyTopicDisplayName,
450458
contentHintText: 'Message #${channel.name} > '
451459
'${eg.defaultRealmEmptyTopicDisplayName}');
452460

453461
await enterTopic(tester, narrow: narrow, topic: '');
454462
await tester.pump();
455463
checkComposeBoxHintTexts(tester,
456-
topicHintText: 'Topic',
464+
topicHintText: 'Enter a topic '
465+
'(skip for “${eg.defaultRealmEmptyTopicDisplayName}”)',
457466
contentHintText: 'Message #${channel.name}');
458467
});
459468

@@ -462,14 +471,14 @@ void main() {
462471
await enterContent(tester, '');
463472
await tester.pump();
464473
checkComposeBoxHintTexts(tester,
465-
topicHintText: 'Topic',
474+
topicHintText: eg.defaultRealmEmptyTopicDisplayName,
466475
contentHintText: 'Message #${channel.name} > '
467476
'${eg.defaultRealmEmptyTopicDisplayName}');
468477

469478
FocusManager.instance.primaryFocus!.unfocus();
470479
await tester.pump();
471480
checkComposeBoxHintTexts(tester,
472-
topicHintText: 'Topic',
481+
topicHintText: eg.defaultRealmEmptyTopicDisplayName,
473482
contentHintText: 'Message #${channel.name} > '
474483
'${eg.defaultRealmEmptyTopicDisplayName}');
475484
});
@@ -479,7 +488,8 @@ void main() {
479488
await enterTopic(tester, narrow: narrow, topic: 'new topic');
480489
await tester.pump();
481490
checkComposeBoxHintTexts(tester,
482-
topicHintText: 'Topic',
491+
topicHintText: 'Enter a topic '
492+
'(skip for “${eg.defaultRealmEmptyTopicDisplayName}”)',
483493
contentHintText: 'Message #${channel.name} > new topic');
484494
});
485495
});

0 commit comments

Comments
 (0)