@@ -13,6 +13,33 @@ import 'store.dart';
13
13
const double _inputVerticalPadding = 8 ;
14
14
const double _sendButtonSize = 36 ;
15
15
16
+ /// A [TextEditingController] for use in the compose box.
17
+ ///
18
+ /// Subclasses must ensure that [_update] is called in all exposed constructors.
19
+ abstract class ComposeController <ErrorT > extends TextEditingController {
20
+ String get textNormalized => _textNormalized;
21
+ late String _textNormalized;
22
+ String _computeTextNormalized ();
23
+
24
+ List <ErrorT > get validationErrors => _validationErrors;
25
+ late List <ErrorT > _validationErrors;
26
+ List <ErrorT > _computeValidationErrors ();
27
+
28
+ ValueNotifier <bool > hasValidationErrors = ValueNotifier (false );
29
+
30
+ void _update () {
31
+ _textNormalized = _computeTextNormalized ();
32
+ _validationErrors = _computeValidationErrors ();
33
+ hasValidationErrors.value = _validationErrors.isNotEmpty;
34
+ }
35
+
36
+ @override
37
+ void notifyListeners () {
38
+ _update ();
39
+ super .notifyListeners ();
40
+ }
41
+ }
42
+
16
43
enum TopicValidationError {
17
44
mandatoryButEmpty,
18
45
tooLong;
@@ -27,22 +54,27 @@ enum TopicValidationError {
27
54
}
28
55
}
29
56
30
- class ComposeTopicController extends TextEditingController {
57
+ class ComposeTopicController extends ComposeController <TopicValidationError > {
58
+ ComposeTopicController () {
59
+ _update ();
60
+ }
61
+
31
62
// TODO: subscribe to this value:
32
63
// https://zulip.com/help/require-topics
33
64
final mandatory = true ;
34
65
35
- String textNormalized () {
66
+ @override
67
+ String _computeTextNormalized () {
36
68
String trimmed = text.trim ();
37
69
return trimmed.isEmpty ? kNoTopicTopic : trimmed;
38
70
}
39
71
40
- List < TopicValidationError > validationErrors () {
41
- final normalized = textNormalized ();
72
+ @override
73
+ List < TopicValidationError > _computeValidationErrors () {
42
74
return [
43
- if (mandatory && normalized == kNoTopicTopic)
75
+ if (mandatory && textNormalized == kNoTopicTopic)
44
76
TopicValidationError .mandatoryButEmpty,
45
- if (normalized .length > kMaxTopicLength)
77
+ if (textNormalized .length > kMaxTopicLength)
46
78
TopicValidationError .tooLong,
47
79
];
48
80
}
@@ -67,7 +99,11 @@ enum ContentValidationError {
67
99
}
68
100
}
69
101
70
- class ComposeContentController extends TextEditingController {
102
+ class ComposeContentController extends ComposeController <ContentValidationError > {
103
+ ComposeContentController () {
104
+ _update ();
105
+ }
106
+
71
107
int _nextUploadTag = 0 ;
72
108
73
109
final Map <int , ({String filename, String placeholder})> _uploads = {};
@@ -128,20 +164,21 @@ class ComposeContentController extends TextEditingController {
128
164
notifyListeners (); // _uploads change could affect validationErrors
129
165
}
130
166
131
- String textNormalized () {
167
+ @override
168
+ String _computeTextNormalized () {
132
169
return text.trim ();
133
170
}
134
171
135
- List < ContentValidationError > validationErrors () {
136
- final normalized = textNormalized ();
172
+ @override
173
+ List < ContentValidationError > _computeValidationErrors () {
137
174
return [
138
- if (normalized .isEmpty)
175
+ if (textNormalized .isEmpty)
139
176
ContentValidationError .empty,
140
177
141
178
// normalized.length is the number of UTF-16 code units, while the server
142
179
// API expresses the max in Unicode code points. So this comparison will
143
180
// be conservative and may cut the user off shorter than necessary.
144
- if (normalized .length > kMaxMessageLengthCodePoints)
181
+ if (textNormalized .length > kMaxMessageLengthCodePoints)
145
182
ContentValidationError .tooLong,
146
183
147
184
if (_uploads.isNotEmpty)
@@ -252,14 +289,14 @@ class _StreamContentInputState extends State<_StreamContentInput> {
252
289
253
290
_topicChanged () {
254
291
setState (() {
255
- _topicTextNormalized = widget.topicController.textNormalized () ;
292
+ _topicTextNormalized = widget.topicController.textNormalized;
256
293
});
257
294
}
258
295
259
296
@override
260
297
void initState () {
261
298
super .initState ();
262
- _topicTextNormalized = widget.topicController.textNormalized () ;
299
+ _topicTextNormalized = widget.topicController.textNormalized;
263
300
widget.topicController.addListener (_topicChanged);
264
301
}
265
302
@@ -542,7 +579,7 @@ class _StreamSendButtonState extends State<_StreamSendButton> {
542
579
543
580
_topicChanged () {
544
581
final oldIsEmpty = _topicValidationErrors.isEmpty;
545
- final newErrors = widget.topicController.validationErrors () ;
582
+ final newErrors = widget.topicController.validationErrors;
546
583
final newIsEmpty = newErrors.isEmpty;
547
584
_topicValidationErrors = newErrors;
548
585
if (oldIsEmpty != newIsEmpty) {
@@ -554,7 +591,7 @@ class _StreamSendButtonState extends State<_StreamSendButton> {
554
591
555
592
_contentChanged () {
556
593
final oldIsEmpty = _contentValidationErrors.isEmpty;
557
- final newErrors = widget.contentController.validationErrors () ;
594
+ final newErrors = widget.contentController.validationErrors;
558
595
final newIsEmpty = newErrors.isEmpty;
559
596
_contentValidationErrors = newErrors;
560
597
if (oldIsEmpty != newIsEmpty) {
@@ -567,8 +604,8 @@ class _StreamSendButtonState extends State<_StreamSendButton> {
567
604
@override
568
605
void initState () {
569
606
super .initState ();
570
- _topicValidationErrors = widget.topicController.validationErrors () ;
571
- _contentValidationErrors = widget.contentController.validationErrors () ;
607
+ _topicValidationErrors = widget.topicController.validationErrors;
608
+ _contentValidationErrors = widget.contentController.validationErrors;
572
609
widget.topicController.addListener (_topicChanged);
573
610
widget.contentController.addListener (_contentChanged);
574
611
}
@@ -615,8 +652,8 @@ class _StreamSendButtonState extends State<_StreamSendButton> {
615
652
616
653
final store = PerAccountStoreWidget .of (context);
617
654
final destination = StreamDestination (
618
- widget.narrow.streamId, widget.topicController.textNormalized () );
619
- final content = widget.contentController.textNormalized () ;
655
+ widget.narrow.streamId, widget.topicController.textNormalized);
656
+ final content = widget.contentController.textNormalized;
620
657
store.sendMessage (destination: destination, content: content);
621
658
622
659
widget.contentController.clear ();
0 commit comments