@@ -682,15 +682,101 @@ class _TopicInput extends StatefulWidget {
682
682
}
683
683
684
684
class _TopicInputState extends State <_TopicInput > {
685
+ void _topicFocusChanged () {
686
+ setState (() {
687
+ if (widget.controller.topicFocusNode.hasFocus) {
688
+ widget.controller.topicEditStatus.value = ComposeTopicEditStatus .isEditing;
689
+ } else if (! widget.controller.contentFocusNode.hasFocus) {
690
+ widget.controller.topicEditStatus.value = ComposeTopicEditStatus .none;
691
+ }
692
+ });
693
+ }
694
+
695
+ void _contentFocusChanged () {
696
+ setState (() {
697
+ if (widget.controller.contentFocusNode.hasFocus) {
698
+ widget.controller.topicEditStatus.value = ComposeTopicEditStatus .hasChosen;
699
+ }
700
+ });
701
+ }
702
+
703
+ void _topicEditStatusChanged () {
704
+ setState (() {
705
+ // The actual state lives in widget.controller.topicEditStatus
706
+ });
707
+ }
708
+
709
+ @override
710
+ void initState () {
711
+ super .initState ();
712
+ widget.controller.topicFocusNode.addListener (_topicFocusChanged);
713
+ widget.controller.contentFocusNode.addListener (_contentFocusChanged);
714
+ widget.controller.topicEditStatus.addListener (_topicEditStatusChanged);
715
+ }
716
+
717
+ @override
718
+ void didUpdateWidget (covariant _TopicInput oldWidget) {
719
+ super .didUpdateWidget (oldWidget);
720
+ if (oldWidget.controller != widget.controller) {
721
+ oldWidget.controller.topicFocusNode.removeListener (_topicFocusChanged);
722
+ widget.controller.topicFocusNode.addListener (_topicFocusChanged);
723
+ oldWidget.controller.contentFocusNode.removeListener (_contentFocusChanged);
724
+ widget.controller.contentFocusNode.addListener (_contentFocusChanged);
725
+ oldWidget.controller.topicEditStatus.removeListener (_topicEditStatusChanged);
726
+ widget.controller.topicEditStatus.addListener (_topicEditStatusChanged);
727
+ }
728
+ }
729
+
730
+ @override
731
+ void dispose () {
732
+ widget.controller.topicFocusNode.removeListener (_topicFocusChanged);
733
+ widget.controller.contentFocusNode.removeListener (_contentFocusChanged);
734
+ widget.controller.topicEditStatus.removeListener (_topicEditStatusChanged);
735
+ super .dispose ();
736
+ }
737
+
685
738
@override
686
739
Widget build (BuildContext context) {
687
740
final zulipLocalizations = ZulipLocalizations .of (context);
688
741
final designVariables = DesignVariables .of (context);
689
- TextStyle topicTextStyle = TextStyle (
742
+ final store = PerAccountStoreWidget .of (context);
743
+
744
+ final topicTextStyle = TextStyle (
690
745
fontSize: 20 ,
691
746
height: 22 / 20 ,
692
747
color: designVariables.textInput.withFadedAlpha (0.9 ),
693
748
).merge (weightVariableTextStyle (context, wght: 600 ));
749
+ final hintStyle = topicTextStyle.copyWith (
750
+ color: designVariables.textInput.withFadedAlpha (0.5 ));
751
+ final defaultTopicDisplayName = store.zulipFeatureLevel >= 334
752
+ ? store.realmEmptyTopicDisplayName : kNoTopicTopic;
753
+
754
+ final decoration = switch ((
755
+ store.realmMandatoryTopics,
756
+ widget.controller.topicEditStatus.value,
757
+ )) {
758
+ (false , ComposeTopicEditStatus .hasChosen) => InputDecoration (
759
+ // The topic has likely been chosen. Since topics are not mandaotry,
760
+ // show the default topic display name as if the user has entered that
761
+ // when they left the input empty.
762
+ hintText: defaultTopicDisplayName,
763
+ hintStyle: topicTextStyle.copyWith (
764
+ fontStyle: store.zulipFeatureLevel >= 334 ? FontStyle .italic : null )),
765
+
766
+ (false , ComposeTopicEditStatus .isEditing) => InputDecoration (
767
+ // The user is actively interacting with the input. Since topics are
768
+ // not mandatory, show a long hint text mentioning that they can be
769
+ // left empty.
770
+ hintText: zulipLocalizations.composeBoxEnterTopicOrSkipHintText (
771
+ defaultTopicDisplayName),
772
+ hintStyle: hintStyle),
773
+
774
+ (false , ComposeTopicEditStatus .none) ||
775
+ (true , _) => InputDecoration (
776
+ // Otherwise, show a short hint text for less distraction.
777
+ hintText: zulipLocalizations.composeBoxTopicHintText,
778
+ hintStyle: hintStyle),
779
+ };
694
780
695
781
return TopicAutocomplete (
696
782
streamId: widget.streamId,
@@ -707,10 +793,7 @@ class _TopicInputState extends State<_TopicInput> {
707
793
focusNode: widget.controller.topicFocusNode,
708
794
textInputAction: TextInputAction .next,
709
795
style: topicTextStyle,
710
- decoration: InputDecoration (
711
- hintText: zulipLocalizations.composeBoxTopicHintText,
712
- hintStyle: topicTextStyle.copyWith (
713
- color: designVariables.textInput.withFadedAlpha (0.5 ))))));
796
+ decoration: decoration)));
714
797
}
715
798
}
716
799
@@ -1383,17 +1466,57 @@ sealed class ComposeBoxController {
1383
1466
}
1384
1467
}
1385
1468
1469
+ /// Represent how a user has edited the topic, by tracking their previous
1470
+ /// interactions with topic and content inputs.
1471
+ ///
1472
+ /// State-transition diagram:
1473
+ ///
1474
+ /// ```
1475
+ /// content input
1476
+ /// gains focus
1477
+ /// none ─────────────► hasEdited
1478
+ /// ▲ │ │ ▲
1479
+ /// │ └────────────────┤ │
1480
+ /// │ topic input │ │ content input
1481
+ /// │ gains focus │ │ gains focus
1482
+ /// │ ▼ │
1483
+ /// └────────────────── isEditing
1484
+ /// topic input loses focus
1485
+ /// and content input has no focus
1486
+ /// ```
1487
+ enum ComposeTopicEditStatus {
1488
+ /// The topic has likely not been chosen if left empty,
1489
+ /// and is not being actively edited.
1490
+ ///
1491
+ /// When in this status neither the topic input nor the content input has focus.
1492
+ none,
1493
+
1494
+ /// The topic has likely been chosen, even if it is left empty.
1495
+ ///
1496
+ /// When in this status, the topic input must have no focus;
1497
+ /// the content input might have focus.
1498
+ hasChosen,
1499
+
1500
+ /// The topic is being actively edited.
1501
+ ///
1502
+ /// When in this status, the topic input must have focus.
1503
+ isEditing,
1504
+ }
1505
+
1386
1506
class StreamComposeBoxController extends ComposeBoxController {
1387
1507
StreamComposeBoxController ({required PerAccountStore store})
1388
1508
: topic = ComposeTopicController (store: store);
1389
1509
1390
1510
final ComposeTopicController topic;
1391
1511
final topicFocusNode = FocusNode ();
1512
+ final ValueNotifier <ComposeTopicEditStatus > topicEditStatus =
1513
+ ValueNotifier (ComposeTopicEditStatus .none);
1392
1514
1393
1515
@override
1394
1516
void dispose () {
1395
1517
topic.dispose ();
1396
1518
topicFocusNode.dispose ();
1519
+ topicEditStatus.dispose ();
1397
1520
super .dispose ();
1398
1521
}
1399
1522
}
0 commit comments