@@ -202,7 +202,7 @@ class ComposeTopicController extends ComposeController<TopicValidationError> {
202
202
// https://github.com/zulip/zulip-flutter/pull/1148#discussion_r1941990585
203
203
String getDestinationString ({
204
204
required String streamName,
205
- required bool contentHasFocus ,
205
+ required bool hasChosenTopic ,
206
206
}) {
207
207
final textTrimmed = text.trim ();
208
208
if (textTrimmed.isNotEmpty) {
@@ -217,7 +217,7 @@ class ComposeTopicController extends ComposeController<TopicValidationError> {
217
217
// Do not fall back to a vacuous topic unless the user explicitly chooses
218
218
// to do so (by skipping topic input and moving focus to content input),
219
219
// because we expect a call to action for the user to pick one first.
220
- || ! contentHasFocus
220
+ || ! hasChosenTopic
221
221
) {
222
222
return '#$streamName ' ;
223
223
}
@@ -618,16 +618,26 @@ class _StreamContentInputState extends State<_StreamContentInput> {
618
618
});
619
619
}
620
620
621
+ void _hasChosenTopicChanged () {
622
+ setState (() {
623
+ // The relevant state lives on widget.controller.hasChosenTopic itself.
624
+ });
625
+ }
626
+
621
627
void _contentFocusChanged () {
622
628
setState (() {
623
629
// The relevant state lives on widget.controller.contentFocusNode itself.
624
630
});
631
+ if (widget.controller.contentFocusNode.hasFocus){
632
+ widget.controller.hasChosenTopic.value = true ;
633
+ }
625
634
}
626
635
627
636
@override
628
637
void initState () {
629
638
super .initState ();
630
639
widget.controller.topic.addListener (_topicChanged);
640
+ widget.controller.hasChosenTopic.addListener (_hasChosenTopicChanged);
631
641
widget.controller.contentFocusNode.addListener (_contentFocusChanged);
632
642
}
633
643
@@ -638,6 +648,10 @@ class _StreamContentInputState extends State<_StreamContentInput> {
638
648
oldWidget.controller.topic.removeListener (_topicChanged);
639
649
widget.controller.topic.addListener (_topicChanged);
640
650
}
651
+ if (widget.controller.hasChosenTopic != oldWidget.controller.hasChosenTopic) {
652
+ oldWidget.controller.hasChosenTopic.removeListener (_hasChosenTopicChanged);
653
+ widget.controller.hasChosenTopic.addListener (_hasChosenTopicChanged);
654
+ }
641
655
if (widget.controller.contentFocusNode != oldWidget.controller.contentFocusNode) {
642
656
oldWidget.controller.contentFocusNode.removeListener (_contentFocusChanged);
643
657
widget.controller.contentFocusNode.addListener (_contentFocusChanged);
@@ -647,6 +661,7 @@ class _StreamContentInputState extends State<_StreamContentInput> {
647
661
@override
648
662
void dispose () {
649
663
widget.controller.topic.removeListener (_topicChanged);
664
+ widget.controller.hasChosenTopic.removeListener (_hasChosenTopicChanged);
650
665
widget.controller.contentFocusNode.removeListener (_contentFocusChanged);
651
666
super .dispose ();
652
667
}
@@ -665,45 +680,113 @@ class _StreamContentInputState extends State<_StreamContentInput> {
665
680
hintText: zulipLocalizations.composeBoxChannelContentHint (
666
681
widget.controller.topic.getDestinationString (
667
682
streamName: streamName,
668
- contentHasFocus : widget.controller.contentFocusNode.hasFocus )));
683
+ hasChosenTopic : widget.controller.hasChosenTopic.value )));
669
684
}
670
685
}
671
686
672
- class _TopicInput extends StatelessWidget {
687
+ class _TopicInput extends StatefulWidget {
673
688
const _TopicInput ({required this .streamId, required this .controller});
674
689
675
690
final int streamId;
676
691
final StreamComposeBoxController controller;
677
692
693
+ @override
694
+ State <_TopicInput > createState () => _TopicInputState ();
695
+ }
696
+
697
+ class _TopicInputState extends State <_TopicInput > {
698
+ @override
699
+ void initState () {
700
+ super .initState ();
701
+ widget.controller.topicFocusNode.addListener (_topicFocusChanged);
702
+ widget.controller.hasChosenTopic.addListener (_hasChosenTopicChanged);
703
+ }
704
+
705
+ @override
706
+ void didUpdateWidget (covariant _TopicInput oldWidget) {
707
+ super .didUpdateWidget (oldWidget);
708
+ if (widget.controller.topicFocusNode != oldWidget.controller.topicFocusNode) {
709
+ oldWidget.controller.topicFocusNode.removeListener (_topicFocusChanged);
710
+ widget.controller.topicFocusNode.addListener (_topicFocusChanged);
711
+ }
712
+ if (widget.controller.hasChosenTopic != oldWidget.controller.hasChosenTopic) {
713
+ oldWidget.controller.hasChosenTopic.removeListener (_hasChosenTopicChanged);
714
+ widget.controller.hasChosenTopic.addListener (_hasChosenTopicChanged);
715
+ }
716
+ }
717
+
718
+ @override
719
+ void dispose () {
720
+ widget.controller.topicFocusNode.removeListener (_topicFocusChanged);
721
+ widget.controller.hasChosenTopic.removeListener (_hasChosenTopicChanged);
722
+ super .dispose ();
723
+ }
724
+
725
+ void _topicFocusChanged () {
726
+ setState (() {
727
+ // The relevant state lives on widget.controller.topicFocusNode itself.
728
+ });
729
+ if (widget.controller.topicFocusNode.hasFocus) {
730
+ widget.controller.hasChosenTopic.value = false ;
731
+ }
732
+ }
733
+
734
+ void _hasChosenTopicChanged () {
735
+ setState (() {
736
+ // The relevant state lives on widget.controller.hasChosenTopic itself.
737
+ });
738
+ }
739
+
678
740
@override
679
741
Widget build (BuildContext context) {
680
742
final zulipLocalizations = ZulipLocalizations .of (context);
681
743
final designVariables = DesignVariables .of (context);
744
+ final store = PerAccountStoreWidget .of (context);
682
745
TextStyle topicTextStyle = TextStyle (
683
746
fontSize: 20 ,
684
747
height: 22 / 20 ,
685
748
color: designVariables.textInput.withFadedAlpha (0.9 ),
686
749
).merge (weightVariableTextStyle (context, wght: 600 ));
750
+ final hintStyle = topicTextStyle.copyWith (
751
+ color: designVariables.textInput.withFadedAlpha (0.5 ));
752
+
753
+ final defaultTopicDisplayName = store.zulipFeatureLevel >= 334
754
+ ? store.realmEmptyTopicDisplayName : kNoTopicTopic;
755
+
756
+ final decoration = switch ((
757
+ store.realmMandatoryTopics,
758
+ widget.controller.hasChosenTopic.value,
759
+ widget.controller.topicFocusNode.hasFocus,
760
+ )) {
761
+ (false , true , _) => InputDecoration (
762
+ hintText: defaultTopicDisplayName,
763
+ hintStyle: topicTextStyle.copyWith (
764
+ fontStyle: store.zulipFeatureLevel >= 334 ? FontStyle .italic : null )),
765
+ (false , false , true ) => InputDecoration (
766
+ hintText: zulipLocalizations.composeBoxEnterTopicOrSkipHintText (
767
+ defaultTopicDisplayName),
768
+ hintStyle: hintStyle),
769
+ (_, _, _) => InputDecoration (
770
+ hintText: zulipLocalizations.composeBoxTopicHintText,
771
+ hintStyle: hintStyle),
772
+ };
687
773
688
774
return TopicAutocomplete (
689
- streamId: streamId,
690
- controller: controller.topic,
691
- focusNode: controller.topicFocusNode,
692
- contentFocusNode: controller.contentFocusNode,
775
+ streamId: widget. streamId,
776
+ controller: widget. controller.topic,
777
+ focusNode: widget. controller.topicFocusNode,
778
+ contentFocusNode: widget. controller.contentFocusNode,
693
779
fieldViewBuilder: (context) => Container (
694
780
padding: const EdgeInsets .only (top: 10 , bottom: 9 ),
695
781
decoration: BoxDecoration (border: Border (bottom: BorderSide (
696
782
width: 1 ,
697
783
color: designVariables.foreground.withFadedAlpha (0.2 )))),
698
784
child: TextField (
699
- controller: controller.topic,
700
- focusNode: controller.topicFocusNode,
785
+ controller: widget. controller.topic,
786
+ focusNode: widget. controller.topicFocusNode,
701
787
textInputAction: TextInputAction .next,
702
788
style: topicTextStyle,
703
- decoration: InputDecoration (
704
- hintText: zulipLocalizations.composeBoxTopicHintText,
705
- hintStyle: topicTextStyle.copyWith (
706
- color: designVariables.textInput.withFadedAlpha (0.5 ))))));
789
+ decoration: decoration)));
707
790
}
708
791
}
709
792
@@ -1381,10 +1464,18 @@ class StreamComposeBoxController extends ComposeBoxController {
1381
1464
final ComposeTopicController topic;
1382
1465
final topicFocusNode = FocusNode ();
1383
1466
1467
+ /// Whether the user has made up their mind choosing a topic.
1468
+ ///
1469
+ /// Empirically, this should be set to `false` whenever the user focuses on
1470
+ /// the topic input, and set to `true` whenever the user focuses on the
1471
+ /// content input.
1472
+ ValueNotifier <bool > hasChosenTopic = ValueNotifier (false );
1473
+
1384
1474
@override
1385
1475
void dispose () {
1386
1476
topic.dispose ();
1387
1477
topicFocusNode.dispose ();
1478
+ hasChosenTopic.dispose ();
1388
1479
super .dispose ();
1389
1480
}
1390
1481
}
0 commit comments