@@ -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
0 commit comments