Skip to content

Commit 791c061

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 83ec831 commit 791c061

12 files changed

+230
-19
lines changed

assets/l10n/app_en.arb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,13 @@
367367
"@composeBoxTopicHintText": {
368368
"description": "Hint text for topic input widget in compose box."
369369
},
370+
"composeBoxEnterTopicOrSkipHintText": "Enter a topic (skip for \"{defaultTopicName}\")",
371+
"@composeBoxEnterTopicOrSkipHintText": {
372+
"description": "Hint text for topic input widget in compose box when topics are optional.",
373+
"placeholders": {
374+
"defaultTopicName": {"type": "String", "example": "general chat"}
375+
}
376+
},
370377
"composeBoxUploadingFilename": "Uploading {filename}…",
371378
"@composeBoxUploadingFilename": {
372379
"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
@@ -591,6 +591,12 @@ abstract class ZulipLocalizations {
591591
/// **'Topic'**
592592
String get composeBoxTopicHintText;
593593

594+
/// Hint text for topic input widget in compose box when topics are optional.
595+
///
596+
/// In en, this message translates to:
597+
/// **'Enter a topic (skip for \"{defaultTopicName}\")'**
598+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName);
599+
594600
/// Placeholder in compose box showing the specified file is currently uploading.
595601
///
596602
/// 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
@@ -289,6 +289,11 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
289289
@override
290290
String get composeBoxTopicHintText => 'Topic';
291291

292+
@override
293+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
294+
return 'Enter a topic (skip for \"$defaultTopicName\")';
295+
}
296+
292297
@override
293298
String composeBoxUploadingFilename(String filename) {
294299
return 'Uploading $filename…';

lib/generated/l10n/zulip_localizations_en.dart

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

292+
@override
293+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
294+
return 'Enter a topic (skip for \"$defaultTopicName\")';
295+
}
296+
292297
@override
293298
String composeBoxUploadingFilename(String filename) {
294299
return 'Uploading $filename…';

lib/generated/l10n/zulip_localizations_ja.dart

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

292+
@override
293+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
294+
return 'Enter a topic (skip for \"$defaultTopicName\")';
295+
}
296+
292297
@override
293298
String composeBoxUploadingFilename(String filename) {
294299
return 'Uploading $filename…';

lib/generated/l10n/zulip_localizations_nb.dart

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

292+
@override
293+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
294+
return 'Enter a topic (skip for \"$defaultTopicName\")';
295+
}
296+
292297
@override
293298
String composeBoxUploadingFilename(String filename) {
294299
return 'Uploading $filename…';

lib/generated/l10n/zulip_localizations_pl.dart

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

292+
@override
293+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
294+
return 'Enter a topic (skip for \"$defaultTopicName\")';
295+
}
296+
292297
@override
293298
String composeBoxUploadingFilename(String filename) {
294299
return 'Przekazywanie $filename…';

lib/generated/l10n/zulip_localizations_ru.dart

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

292+
@override
293+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
294+
return 'Enter a topic (skip for \"$defaultTopicName\")';
295+
}
296+
292297
@override
293298
String composeBoxUploadingFilename(String filename) {
294299
return 'Загрузка $filename…';

lib/generated/l10n/zulip_localizations_sk.dart

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

292+
@override
293+
String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) {
294+
return 'Enter a topic (skip for \"$defaultTopicName\")';
295+
}
296+
292297
@override
293298
String composeBoxUploadingFilename(String filename) {
294299
return 'Uploading $filename…';

lib/widgets/compose_box.dart

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ class ComposeTopicController extends ComposeController<TopicValidationError> {
202202
// https://github.com/zulip/zulip-flutter/pull/1148#discussion_r1941990585
203203
String getDestinationString({
204204
required String streamName,
205-
required bool contentHasFocus,
205+
required bool hasChosenTopic,
206206
}) {
207207
final textTrimmed = text.trim();
208208
if (textTrimmed.isNotEmpty) {
@@ -217,7 +217,7 @@ class ComposeTopicController extends ComposeController<TopicValidationError> {
217217
// Do not fall back to a vacuous topic unless the user explicitly chooses
218218
// to do so (by skipping topic input and moving focus to content input),
219219
// because we expect a call to action for the user to pick one first.
220-
|| !contentHasFocus
220+
|| !hasChosenTopic
221221
) {
222222
return '#$streamName';
223223
}
@@ -619,16 +619,26 @@ class _StreamContentInputState extends State<_StreamContentInput> {
619619
});
620620
}
621621

622+
void _hasChosenTopicChanged() {
623+
setState(() {
624+
// The relevant state lives on widget.controller.hasChosenTopic itself.
625+
});
626+
}
627+
622628
void _contentFocusChanged() {
623629
setState(() {
624630
// The relevant state lives on widget.controller.contentFocusNode itself.
625631
});
632+
if (widget.controller.contentFocusNode.hasFocus){
633+
widget.controller.hasChosenTopic.value = true;
634+
}
626635
}
627636

628637
@override
629638
void initState() {
630639
super.initState();
631640
widget.controller.topic.addListener(_topicChanged);
641+
widget.controller.hasChosenTopic.addListener(_hasChosenTopicChanged);
632642
widget.controller.contentFocusNode.addListener(_contentFocusChanged);
633643
}
634644

@@ -639,6 +649,10 @@ class _StreamContentInputState extends State<_StreamContentInput> {
639649
oldWidget.controller.topic.removeListener(_topicChanged);
640650
widget.controller.topic.addListener(_topicChanged);
641651
}
652+
if (widget.controller.hasChosenTopic != oldWidget.controller.hasChosenTopic) {
653+
oldWidget.controller.hasChosenTopic.removeListener(_hasChosenTopicChanged);
654+
widget.controller.hasChosenTopic.addListener(_hasChosenTopicChanged);
655+
}
642656
if (widget.controller.contentFocusNode != oldWidget.controller.contentFocusNode) {
643657
oldWidget.controller.contentFocusNode.removeListener(_contentFocusChanged);
644658
widget.controller.contentFocusNode.addListener(_contentFocusChanged);
@@ -648,6 +662,7 @@ class _StreamContentInputState extends State<_StreamContentInput> {
648662
@override
649663
void dispose() {
650664
widget.controller.topic.removeListener(_topicChanged);
665+
widget.controller.hasChosenTopic.removeListener(_hasChosenTopicChanged);
651666
widget.controller.contentFocusNode.removeListener(_contentFocusChanged);
652667
super.dispose();
653668
}
@@ -666,45 +681,113 @@ class _StreamContentInputState extends State<_StreamContentInput> {
666681
hintText: zulipLocalizations.composeBoxChannelContentHint(
667682
widget.controller.topic.getDestinationString(
668683
streamName: streamName,
669-
contentHasFocus: widget.controller.contentFocusNode.hasFocus)));
684+
hasChosenTopic: widget.controller.hasChosenTopic.value)));
670685
}
671686
}
672687

673-
class _TopicInput extends StatelessWidget {
688+
class _TopicInput extends StatefulWidget {
674689
const _TopicInput({required this.streamId, required this.controller});
675690

676691
final int streamId;
677692
final StreamComposeBoxController controller;
678693

694+
@override
695+
State<_TopicInput> createState() => _TopicInputState();
696+
}
697+
698+
class _TopicInputState extends State<_TopicInput> {
699+
@override
700+
void initState() {
701+
super.initState();
702+
widget.controller.topicFocusNode.addListener(_topicFocusChanged);
703+
widget.controller.hasChosenTopic.addListener(_hasChosenTopicChanged);
704+
}
705+
706+
@override
707+
void didUpdateWidget(covariant _TopicInput oldWidget) {
708+
super.didUpdateWidget(oldWidget);
709+
if (widget.controller.topicFocusNode != oldWidget.controller.topicFocusNode) {
710+
oldWidget.controller.topicFocusNode.removeListener(_topicFocusChanged);
711+
widget.controller.topicFocusNode.addListener(_topicFocusChanged);
712+
}
713+
if (widget.controller.hasChosenTopic != oldWidget.controller.hasChosenTopic) {
714+
oldWidget.controller.hasChosenTopic.removeListener(_hasChosenTopicChanged);
715+
widget.controller.hasChosenTopic.addListener(_hasChosenTopicChanged);
716+
}
717+
}
718+
719+
@override
720+
void dispose() {
721+
widget.controller.topicFocusNode.removeListener(_topicFocusChanged);
722+
widget.controller.hasChosenTopic.removeListener(_hasChosenTopicChanged);
723+
super.dispose();
724+
}
725+
726+
void _topicFocusChanged() {
727+
setState(() {
728+
// The relevant state lives on widget.controller.topicFocusNode itself.
729+
});
730+
if (widget.controller.topicFocusNode.hasFocus) {
731+
widget.controller.hasChosenTopic.value = false;
732+
}
733+
}
734+
735+
void _hasChosenTopicChanged() {
736+
setState(() {
737+
// The relevant state lives on widget.controller.hasChosenTopic itself.
738+
});
739+
}
740+
679741
@override
680742
Widget build(BuildContext context) {
681743
final zulipLocalizations = ZulipLocalizations.of(context);
682744
final designVariables = DesignVariables.of(context);
745+
final store = PerAccountStoreWidget.of(context);
683746
TextStyle topicTextStyle = TextStyle(
684747
fontSize: 20,
685748
height: 22 / 20,
686749
color: designVariables.textInput.withFadedAlpha(0.9),
687750
).merge(weightVariableTextStyle(context, wght: 600));
751+
final hintStyle = topicTextStyle.copyWith(
752+
color: designVariables.textInput.withFadedAlpha(0.5));
753+
754+
final defaultTopicDisplayName = store.zulipFeatureLevel >= 334
755+
? store.realmEmptyTopicDisplayName : kNoTopicTopic;
756+
757+
final decoration = switch ((
758+
store.realmMandatoryTopics,
759+
widget.controller.hasChosenTopic.value,
760+
widget.controller.topicFocusNode.hasFocus,
761+
)) {
762+
(false, true, _) => InputDecoration(
763+
hintText: defaultTopicDisplayName,
764+
hintStyle: topicTextStyle.copyWith(
765+
fontStyle: store.zulipFeatureLevel >= 334 ? FontStyle.italic : null)),
766+
(false, false, true) => InputDecoration(
767+
hintText: zulipLocalizations.composeBoxEnterTopicOrSkipHintText(
768+
defaultTopicDisplayName),
769+
hintStyle: hintStyle),
770+
(_, _, _) => InputDecoration(
771+
hintText: zulipLocalizations.composeBoxTopicHintText,
772+
hintStyle: hintStyle),
773+
};
688774

689775
return TopicAutocomplete(
690-
streamId: streamId,
691-
controller: controller.topic,
692-
focusNode: controller.topicFocusNode,
693-
contentFocusNode: controller.contentFocusNode,
776+
streamId: widget.streamId,
777+
controller: widget.controller.topic,
778+
focusNode: widget.controller.topicFocusNode,
779+
contentFocusNode: widget.controller.contentFocusNode,
694780
fieldViewBuilder: (context) => Container(
695781
padding: const EdgeInsets.only(top: 10, bottom: 9),
696782
decoration: BoxDecoration(border: Border(bottom: BorderSide(
697783
width: 1,
698784
color: designVariables.foreground.withFadedAlpha(0.2)))),
699785
child: TextField(
700-
controller: controller.topic,
701-
focusNode: controller.topicFocusNode,
786+
controller: widget.controller.topic,
787+
focusNode: widget.controller.topicFocusNode,
702788
textInputAction: TextInputAction.next,
703789
style: topicTextStyle,
704-
decoration: InputDecoration(
705-
hintText: zulipLocalizations.composeBoxTopicHintText,
706-
hintStyle: topicTextStyle.copyWith(
707-
color: designVariables.textInput.withFadedAlpha(0.5))))));
790+
decoration: decoration)));
708791
}
709792
}
710793

@@ -1382,6 +1465,10 @@ class StreamComposeBoxController extends ComposeBoxController {
13821465
final ComposeTopicController topic;
13831466
final topicFocusNode = FocusNode();
13841467

1468+
// True if auto-completing to "general chat", moving focus to content input
1469+
// from topic input (or as long as focus has been on content input?)
1470+
ValueNotifier<bool> hasChosenTopic = ValueNotifier(false);
1471+
13851472
@override
13861473
void dispose() {
13871474
topic.dispose();

test/flutter_checks.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ extension TextStyleChecks on Subject<TextStyle> {
8686
Subject<bool> get inherit => has((t) => t.inherit, 'inherit');
8787
Subject<Color?> get color => has((t) => t.color, 'color');
8888
Subject<double?> get fontSize => has((t) => t.fontSize, 'fontSize');
89+
Subject<FontStyle?> get fontStyle => has((t) => t.fontStyle, 'fontStyle');
8990
Subject<FontWeight?> get fontWeight => has((t) => t.fontWeight, 'fontWeight');
9091
Subject<double?> get letterSpacing => has((t) => t.letterSpacing, 'letterSpacing');
9192
Subject<List<FontVariation>?> get fontVariations => has((t) => t.fontVariations, 'fontVariations');
@@ -152,6 +153,7 @@ extension MaterialChecks on Subject<Material> {
152153

153154
extension InputDecorationChecks on Subject<InputDecoration> {
154155
Subject<String?> get hintText => has((x) => x.hintText, 'hintText');
156+
Subject<TextStyle?> get hintStyle => has((x) => x.hintStyle, 'hintStyle');
155157
}
156158

157159
extension BoxDecorationChecks on Subject<BoxDecoration> {

0 commit comments

Comments
 (0)