Skip to content

Commit 51772ec

Browse files
committed
wip test, stateful logic; compose: Show special hint text for inputs if empty topic
Signed-off-by: Zixuan James Li <zixuan@zulip.com>
1 parent 6d1535f commit 51772ec

File tree

2 files changed

+77
-12
lines changed

2 files changed

+77
-12
lines changed

lib/widgets/compose_box.dart

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,34 @@ class ComposeTopicController extends ComposeController<TopicValidationError> {
157157
@override
158158
String _computeTextNormalized() {
159159
String trimmed = text.trim();
160-
return trimmed.isEmpty ? kNoTopicTopic : trimmed;
160+
// TODO(server-10): simplify
161+
if (store.connection.zulipFeatureLevel! < 334) {
162+
return trimmed.isEmpty ? kNoTopicTopic : trimmed;
163+
}
164+
165+
return trimmed;
161166
}
162167

163168
/// Whether [textNormalized] would fail a mandatory-topics check
164169
/// (see [mandatory]).
165170
///
166171
/// The term "Vacuous" draws distinction from [String.isEmpty], in the sense
167172
/// that certain strings are empty but also indicate the absence of a topic.
168-
bool get isTopicVacuous => textNormalized == kNoTopicTopic;
173+
bool get isTopicVacuous {
174+
bool result = textNormalized.isEmpty
175+
// We keep checking for '(no topic)' regardless of the feature level
176+
// because it remains equivalent to an empty topic even when FL >= 334.
177+
// This can change in the future:
178+
// https://chat.zulip.org/#narrow/channel/412-api-documentation/topic/.28realm_.29mandatory_topics.20behavior/near/2062391
179+
|| textNormalized == kNoTopicTopic;
180+
181+
// TODO(server-10): simplify
182+
if (store.connection.zulipFeatureLevel! >= 334) {
183+
result |= textNormalized == store.realmEmptyTopicDisplayName;
184+
}
185+
186+
return result;
187+
}
169188

170189
@override
171190
List<TopicValidationError> _computeValidationErrors() {
@@ -560,11 +579,18 @@ class _StreamContentInputState extends State<_StreamContentInput> {
560579
});
561580
}
562581

582+
void _focusChanged() {
583+
setState(() {
584+
// The actual state lives in `widget.controller.contentFocusNode`.
585+
});
586+
}
587+
563588
@override
564589
void initState() {
565590
super.initState();
566591
_topicTextNormalized = widget.controller.topic.textNormalized;
567592
widget.controller.topic.addListener(_topicChanged);
593+
widget.controller.contentFocusNode.addListener(_focusChanged);
568594
}
569595

570596
@override
@@ -574,11 +600,16 @@ class _StreamContentInputState extends State<_StreamContentInput> {
574600
oldWidget.controller.topic.removeListener(_topicChanged);
575601
widget.controller.topic.addListener(_topicChanged);
576602
}
603+
if (widget.controller.contentFocusNode != oldWidget.controller.contentFocusNode) {
604+
widget.controller.contentFocusNode.removeListener(_focusChanged);
605+
widget.controller.contentFocusNode.addListener(_focusChanged);
606+
}
577607
}
578608

579609
@override
580610
void dispose() {
581611
widget.controller.topic.removeListener(_topicChanged);
612+
widget.controller.contentFocusNode.removeListener(_focusChanged);
582613
super.dispose();
583614
}
584615

@@ -589,12 +620,17 @@ class _StreamContentInputState extends State<_StreamContentInput> {
589620
final streamName = store.streams[widget.narrow.streamId]?.name
590621
?? zulipLocalizations.unknownChannelName;
591622
final topic = TopicName(_topicTextNormalized);
623+
592624
final String? topicDisplayName;
593-
if (store.realmMandatoryTopics && widget.controller.topic.isTopicVacuous) {
594-
topicDisplayName = null;
625+
// ignore: unnecessary_null_comparison // null topic names soon to be enabled
626+
if (topic.displayName != null) {
627+
topicDisplayName = topic.displayName;
628+
} else if (widget.controller.contentFocusNode.hasFocus) {
629+
// The empty topic display name can only be shown when the user is
630+
// actively sending a message, i.e., when the content input is focused.
631+
topicDisplayName = store.realmEmptyTopicDisplayName;
595632
} else {
596-
// ignore: dead_null_aware_expression // null topic names soon to be enabled
597-
topicDisplayName = topic.displayName ?? store.realmEmptyTopicDisplayName;
633+
topicDisplayName = null;
598634
}
599635

600636
return _ContentInput(
@@ -623,6 +659,7 @@ class _TopicInputState extends State<_TopicInput> {
623659
void initState() {
624660
super.initState();
625661
widget.controller.topic.addListener(_topicChanged);
662+
widget.controller.contentFocusNode.addListener(_contentFocusChanged);
626663
}
627664

628665
@override
@@ -632,11 +669,16 @@ class _TopicInputState extends State<_TopicInput> {
632669
oldWidget.controller.topic.removeListener(_topicChanged);
633670
widget.controller.topic.addListener(_topicChanged);
634671
}
672+
if (widget.controller.contentFocusNode != oldWidget.controller.contentFocusNode) {
673+
oldWidget.controller.contentFocusNode.removeListener(_contentFocusChanged);
674+
widget.controller.contentFocusNode.addListener(_contentFocusChanged);
675+
}
635676
}
636677

637678
@override
638679
void dispose() {
639680
widget.controller.topic.removeListener(_topicChanged);
681+
widget.controller.contentFocusNode.removeListener(_contentFocusChanged);
640682
super.dispose();
641683
}
642684

@@ -646,6 +688,12 @@ class _TopicInputState extends State<_TopicInput> {
646688
});
647689
}
648690

691+
void _contentFocusChanged() {
692+
setState(() {
693+
// The actual state lives in `widget.controller.contentFocusNode`.
694+
});
695+
}
696+
649697
@override
650698
Widget build(BuildContext context) {
651699
final zulipLocalizations = ZulipLocalizations.of(context);
@@ -659,6 +707,15 @@ class _TopicInputState extends State<_TopicInput> {
659707

660708
final allowEmptyTopics =
661709
store.connection.zulipFeatureLevel! >= 334 && !store.realmMandatoryTopics;
710+
final decoration =
711+
allowEmptyTopics && widget.controller.contentFocusNode.hasFocus
712+
? InputDecoration(
713+
hintText: store.realmEmptyTopicDisplayName,
714+
hintStyle: topicTextStyle.copyWith(fontStyle: FontStyle.italic))
715+
: InputDecoration(
716+
hintText: zulipLocalizations.composeBoxTopicHintText,
717+
hintStyle: topicTextStyle.copyWith(
718+
color: designVariables.textInput.withFadedAlpha(0.5)));
662719

663720
return TopicAutocomplete(
664721
streamId: widget.streamId,
@@ -682,10 +739,7 @@ class _TopicInputState extends State<_TopicInput> {
682739
? FontStyle.italic
683740
: null,
684741
),
685-
decoration: InputDecoration(
686-
hintText: zulipLocalizations.composeBoxTopicHintText,
687-
hintStyle: topicTextStyle.copyWith(
688-
color: designVariables.textInput.withFadedAlpha(0.5))))));
742+
decoration: decoration)));
689743
}
690744
}
691745

test/widgets/compose_box_test.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,19 @@ void main() {
385385
}
386386

387387
group('to ChannelNarrow, topics not mandatory', () {
388-
testWidgets('with empty topic', (tester) async {
388+
testWidgets('with empty topic, content input has focus', (tester) async {
389+
await prepare(tester, narrow: ChannelNarrow(channel.streamId),
390+
mandatoryTopics: false);
391+
await enterContent(tester, '');
392+
checkComposeBoxHintTexts(tester,
393+
topicHintText: eg.defaultRealmEmptyTopicDisplayName,
394+
contentHintText: 'Message #${channel.name} > ${eg.defaultRealmEmptyTopicDisplayName}');
395+
}, skip: true); // null topic names soon to be enabled
396+
397+
testWidgets('with empty topic, topic input has focus', (tester) async {
389398
final narrow = ChannelNarrow(channel.streamId);
390399
await prepare(tester, narrow: narrow, mandatoryTopics: false);
400+
await enterTopic(tester, narrow: narrow, topic: '');
391401
await tester.pump();
392402
checkComposeBoxHintTexts(tester,
393403
topicHintText: 'Topic',
@@ -408,9 +418,10 @@ void main() {
408418
await prepare(tester, narrow: narrow,
409419
mandatoryTopics: false);
410420
await enterTopic(tester, narrow: narrow, topic: 'new topic');
421+
await enterContent(tester, '');
411422
await tester.pump();
412423
checkComposeBoxHintTexts(tester,
413-
topicHintText: 'Topic',
424+
topicHintText: eg.defaultRealmEmptyTopicDisplayName,
414425
contentHintText: 'Message #${channel.name} > new topic');
415426
});
416427
});

0 commit comments

Comments
 (0)