Skip to content

Commit 8c74a45

Browse files
committed
compose [nfc]: Store textNormalized and errors straight on controller
We're already computing this information eagerly on each edit, so we might as well do so in a way that lets us memoize centrally. This will also let us simplify, next, some of the stateful logic in the send button.
1 parent 7409f0b commit 8c74a45

File tree

1 file changed

+57
-20
lines changed

1 file changed

+57
-20
lines changed

lib/widgets/compose_box.dart

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,33 @@ import 'store.dart';
1313
const double _inputVerticalPadding = 8;
1414
const double _sendButtonSize = 36;
1515

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+
1643
enum TopicValidationError {
1744
mandatoryButEmpty,
1845
tooLong;
@@ -27,22 +54,27 @@ enum TopicValidationError {
2754
}
2855
}
2956

30-
class ComposeTopicController extends TextEditingController {
57+
class ComposeTopicController extends ComposeController<TopicValidationError> {
58+
ComposeTopicController() {
59+
_update();
60+
}
61+
3162
// TODO: subscribe to this value:
3263
// https://zulip.com/help/require-topics
3364
final mandatory = true;
3465

35-
String textNormalized() {
66+
@override
67+
String _computeTextNormalized() {
3668
String trimmed = text.trim();
3769
return trimmed.isEmpty ? kNoTopicTopic : trimmed;
3870
}
3971

40-
List<TopicValidationError> validationErrors() {
41-
final normalized = textNormalized();
72+
@override
73+
List<TopicValidationError> _computeValidationErrors() {
4274
return [
43-
if (mandatory && normalized == kNoTopicTopic)
75+
if (mandatory && textNormalized == kNoTopicTopic)
4476
TopicValidationError.mandatoryButEmpty,
45-
if (normalized.length > kMaxTopicLength)
77+
if (textNormalized.length > kMaxTopicLength)
4678
TopicValidationError.tooLong,
4779
];
4880
}
@@ -67,7 +99,11 @@ enum ContentValidationError {
6799
}
68100
}
69101

70-
class ComposeContentController extends TextEditingController {
102+
class ComposeContentController extends ComposeController<ContentValidationError> {
103+
ComposeContentController() {
104+
_update();
105+
}
106+
71107
int _nextUploadTag = 0;
72108

73109
final Map<int, ({String filename, String placeholder})> _uploads = {};
@@ -128,20 +164,21 @@ class ComposeContentController extends TextEditingController {
128164
notifyListeners(); // _uploads change could affect validationErrors
129165
}
130166

131-
String textNormalized() {
167+
@override
168+
String _computeTextNormalized() {
132169
return text.trim();
133170
}
134171

135-
List<ContentValidationError> validationErrors() {
136-
final normalized = textNormalized();
172+
@override
173+
List<ContentValidationError> _computeValidationErrors() {
137174
return [
138-
if (normalized.isEmpty)
175+
if (textNormalized.isEmpty)
139176
ContentValidationError.empty,
140177

141178
// normalized.length is the number of UTF-16 code units, while the server
142179
// API expresses the max in Unicode code points. So this comparison will
143180
// be conservative and may cut the user off shorter than necessary.
144-
if (normalized.length > kMaxMessageLengthCodePoints)
181+
if (textNormalized.length > kMaxMessageLengthCodePoints)
145182
ContentValidationError.tooLong,
146183

147184
if (_uploads.isNotEmpty)
@@ -252,14 +289,14 @@ class _StreamContentInputState extends State<_StreamContentInput> {
252289

253290
_topicChanged() {
254291
setState(() {
255-
_topicTextNormalized = widget.topicController.textNormalized();
292+
_topicTextNormalized = widget.topicController.textNormalized;
256293
});
257294
}
258295

259296
@override
260297
void initState() {
261298
super.initState();
262-
_topicTextNormalized = widget.topicController.textNormalized();
299+
_topicTextNormalized = widget.topicController.textNormalized;
263300
widget.topicController.addListener(_topicChanged);
264301
}
265302

@@ -542,7 +579,7 @@ class _StreamSendButtonState extends State<_StreamSendButton> {
542579

543580
_topicChanged() {
544581
final oldIsEmpty = _topicValidationErrors.isEmpty;
545-
final newErrors = widget.topicController.validationErrors();
582+
final newErrors = widget.topicController.validationErrors;
546583
final newIsEmpty = newErrors.isEmpty;
547584
_topicValidationErrors = newErrors;
548585
if (oldIsEmpty != newIsEmpty) {
@@ -554,7 +591,7 @@ class _StreamSendButtonState extends State<_StreamSendButton> {
554591

555592
_contentChanged() {
556593
final oldIsEmpty = _contentValidationErrors.isEmpty;
557-
final newErrors = widget.contentController.validationErrors();
594+
final newErrors = widget.contentController.validationErrors;
558595
final newIsEmpty = newErrors.isEmpty;
559596
_contentValidationErrors = newErrors;
560597
if (oldIsEmpty != newIsEmpty) {
@@ -567,8 +604,8 @@ class _StreamSendButtonState extends State<_StreamSendButton> {
567604
@override
568605
void initState() {
569606
super.initState();
570-
_topicValidationErrors = widget.topicController.validationErrors();
571-
_contentValidationErrors = widget.contentController.validationErrors();
607+
_topicValidationErrors = widget.topicController.validationErrors;
608+
_contentValidationErrors = widget.contentController.validationErrors;
572609
widget.topicController.addListener(_topicChanged);
573610
widget.contentController.addListener(_contentChanged);
574611
}
@@ -615,8 +652,8 @@ class _StreamSendButtonState extends State<_StreamSendButton> {
615652

616653
final store = PerAccountStoreWidget.of(context);
617654
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;
620657
store.sendMessage(destination: destination, content: content);
621658

622659
widget.contentController.clear();

0 commit comments

Comments
 (0)