@@ -576,16 +576,26 @@ class _StreamContentInputState extends State<_StreamContentInput> {
576
576
});
577
577
}
578
578
579
+ void _hasChosenTopicChanged () {
580
+ setState (() {
581
+ // The relevant state lives on widget.controller.hasChosenTopic itself.
582
+ });
583
+ }
584
+
579
585
void _contentFocusChanged () {
580
586
setState (() {
581
587
// The relevant state lives on widget.controller.contentFocusNode itself.
582
588
});
589
+ if (widget.controller.contentFocusNode.hasFocus){
590
+ widget.controller.hasChosenTopic.value = true ;
591
+ }
583
592
}
584
593
585
594
@override
586
595
void initState () {
587
596
super .initState ();
588
597
widget.controller.topic.addListener (_topicChanged);
598
+ widget.controller.hasChosenTopic.addListener (_hasChosenTopicChanged);
589
599
widget.controller.contentFocusNode.addListener (_contentFocusChanged);
590
600
}
591
601
@@ -596,6 +606,10 @@ class _StreamContentInputState extends State<_StreamContentInput> {
596
606
oldWidget.controller.topic.removeListener (_topicChanged);
597
607
widget.controller.topic.addListener (_topicChanged);
598
608
}
609
+ if (widget.controller.hasChosenTopic != oldWidget.controller.hasChosenTopic) {
610
+ oldWidget.controller.hasChosenTopic.removeListener (_hasChosenTopicChanged);
611
+ widget.controller.hasChosenTopic.addListener (_hasChosenTopicChanged);
612
+ }
599
613
if (widget.controller.contentFocusNode != oldWidget.controller.contentFocusNode) {
600
614
oldWidget.controller.contentFocusNode.removeListener (_contentFocusChanged);
601
615
widget.controller.contentFocusNode.addListener (_contentFocusChanged);
@@ -605,6 +619,7 @@ class _StreamContentInputState extends State<_StreamContentInput> {
605
619
@override
606
620
void dispose () {
607
621
widget.controller.topic.removeListener (_topicChanged);
622
+ widget.controller.hasChosenTopic.removeListener (_hasChosenTopicChanged);
608
623
widget.controller.contentFocusNode.removeListener (_contentFocusChanged);
609
624
super .dispose ();
610
625
}
@@ -616,7 +631,7 @@ class _StreamContentInputState extends State<_StreamContentInput> {
616
631
// The chosen topic can't be sent to, so don't show it.
617
632
return null ;
618
633
}
619
- if (! widget.controller.contentFocusNode.hasFocus ) {
634
+ if (! widget.controller.hasChosenTopic.value ) {
620
635
// Do not fall back to a vacuous topic unless the user explicitly chooses
621
636
// to do so (by skipping topic input and moving focus to content input),
622
637
// so that the user is not encouraged to use vacuous topic when they
@@ -654,41 +669,121 @@ class _StreamContentInputState extends State<_StreamContentInput> {
654
669
}
655
670
}
656
671
657
- class _TopicInput extends StatelessWidget {
672
+ class _TopicInput extends StatefulWidget {
658
673
const _TopicInput ({required this .streamId, required this .controller});
659
674
660
675
final int streamId;
661
676
final StreamComposeBoxController controller;
662
677
678
+ @override
679
+ State <_TopicInput > createState () => _TopicInputState ();
680
+ }
681
+
682
+ class _TopicInputState extends State <_TopicInput > {
683
+ @override
684
+ void initState () {
685
+ super .initState ();
686
+ widget.controller.topicFocusNode.addListener (_topicFocusChanged);
687
+ widget.controller.hasChosenTopic.addListener (_hasChosenTopicChanged);
688
+ }
689
+
690
+ @override
691
+ void didUpdateWidget (covariant _TopicInput oldWidget) {
692
+ super .didUpdateWidget (oldWidget);
693
+ if (widget.controller.topicFocusNode != oldWidget.controller.topicFocusNode) {
694
+ oldWidget.controller.topicFocusNode.removeListener (_topicFocusChanged);
695
+ widget.controller.topicFocusNode.addListener (_topicFocusChanged);
696
+ }
697
+ if (widget.controller.hasChosenTopic != oldWidget.controller.hasChosenTopic) {
698
+ oldWidget.controller.hasChosenTopic.removeListener (_hasChosenTopicChanged);
699
+ widget.controller.hasChosenTopic.addListener (_hasChosenTopicChanged);
700
+ }
701
+ }
702
+
703
+ @override
704
+ void dispose () {
705
+ widget.controller.topicFocusNode.removeListener (_topicFocusChanged);
706
+ widget.controller.hasChosenTopic.removeListener (_hasChosenTopicChanged);
707
+ super .dispose ();
708
+ }
709
+
710
+ void _topicFocusChanged () {
711
+ setState (() {
712
+ // The relevant state lives on widget.controller.topicFocusNode itself.
713
+ });
714
+ if (widget.controller.topicFocusNode.hasFocus) {
715
+ widget.controller.hasChosenTopic.value = false ;
716
+ }
717
+ }
718
+
719
+ void _hasChosenTopicChanged () {
720
+ setState (() {
721
+ // The relevant state lives on widget.controller.hasChosenTopic itself.
722
+ });
723
+ }
724
+
725
+ /// Return the default topic display name to use, or `null` if topics are
726
+ /// required.
727
+ String ? _defaultTopicDisplayName () {
728
+ final store = PerAccountStoreWidget .of (context);
729
+ if (store.realmMandatoryTopics) {
730
+ return null ;
731
+ }
732
+
733
+ // TODO(server-10) simplify
734
+ return store.zulipFeatureLevel >= 334
735
+ ? store.realmEmptyTopicDisplayName : kNoTopicTopic;
736
+ }
737
+
663
738
@override
664
739
Widget build (BuildContext context) {
665
740
final zulipLocalizations = ZulipLocalizations .of (context);
666
741
final designVariables = DesignVariables .of (context);
742
+ final store = PerAccountStoreWidget .of (context);
667
743
TextStyle topicTextStyle = TextStyle (
668
744
fontSize: 20 ,
669
745
height: 22 / 20 ,
670
746
color: designVariables.textInput.withFadedAlpha (0.9 ),
671
747
).merge (weightVariableTextStyle (context, wght: 600 ));
672
748
749
+ String hintText = zulipLocalizations.composeBoxTopicHintText;
750
+ TextStyle hintStyle = topicTextStyle.copyWith (
751
+ color: designVariables.textInput.withFadedAlpha (0.5 ));
752
+ final defaultTopicDisplayName = _defaultTopicDisplayName ();
753
+ if (defaultTopicDisplayName != null ) {
754
+ if (widget.controller.topicFocusNode.hasFocus) {
755
+ // The user is actively interacting with the input.
756
+ // Show a long and engaging hint text.
757
+ hintText = zulipLocalizations.composeBoxEnterTopicOrSkipHintText (
758
+ defaultTopicDisplayName);
759
+ } else if (widget.controller.hasChosenTopic.value) {
760
+ // The topic has been chosen. Show the default topic display name in
761
+ // the input as if the user has entered that when they left it empty.
762
+ hintText = defaultTopicDisplayName;
763
+ hintStyle = topicTextStyle.copyWith (
764
+ // TODO(server-10) simplify
765
+ fontStyle: store.zulipFeatureLevel >= 334 ? FontStyle .italic : null );
766
+ }
767
+ }
768
+
673
769
return TopicAutocomplete (
674
- streamId: streamId,
675
- controller: controller.topic,
676
- focusNode: controller.topicFocusNode,
677
- contentFocusNode: controller.contentFocusNode,
770
+ streamId: widget. streamId,
771
+ controller: widget. controller.topic,
772
+ focusNode: widget. controller.topicFocusNode,
773
+ contentFocusNode: widget. controller.contentFocusNode,
678
774
fieldViewBuilder: (context) => Container (
679
775
padding: const EdgeInsets .only (top: 10 , bottom: 9 ),
680
776
decoration: BoxDecoration (border: Border (bottom: BorderSide (
681
777
width: 1 ,
682
778
color: designVariables.foreground.withFadedAlpha (0.2 )))),
683
779
child: TextField (
684
- controller: controller.topic,
685
- focusNode: controller.topicFocusNode,
780
+ controller: widget. controller.topic,
781
+ focusNode: widget. controller.topicFocusNode,
686
782
textInputAction: TextInputAction .next,
687
783
style: topicTextStyle,
688
784
decoration: InputDecoration (
689
- hintText: zulipLocalizations.composeBoxTopicHintText,
690
- hintStyle: topicTextStyle.copyWith (
691
- color: designVariables.textInput.withFadedAlpha (0.5 ))))));
785
+ hintText: hintText,
786
+ hintStyle: hintStyle))));
692
787
}
693
788
}
694
789
@@ -1366,10 +1461,18 @@ class StreamComposeBoxController extends ComposeBoxController {
1366
1461
final ComposeTopicController topic;
1367
1462
final topicFocusNode = FocusNode ();
1368
1463
1464
+ /// Whether the user has made up their mind choosing a topic.
1465
+ ///
1466
+ /// Empirically, this should be set to `false` whenever the user focuses on
1467
+ /// the topic input, and set to `true` whenever the user focuses on the
1468
+ /// content input.
1469
+ ValueNotifier <bool > hasChosenTopic = ValueNotifier (false );
1470
+
1369
1471
@override
1370
1472
void dispose () {
1371
1473
topic.dispose ();
1372
1474
topicFocusNode.dispose ();
1475
+ hasChosenTopic.dispose ();
1373
1476
super .dispose ();
1374
1477
}
1375
1478
}
0 commit comments