1
1
import 'package:flutter/material.dart' ;
2
2
3
+ import '../api/model/model.dart' ;
3
4
import 'content.dart' ;
4
5
import 'store.dart' ;
5
6
import '../model/autocomplete.dart' ;
6
7
import '../model/compose.dart' ;
7
8
import '../model/narrow.dart' ;
8
9
import 'compose_box.dart' ;
9
10
10
- class ComposeAutocomplete extends StatefulWidget {
11
- const ComposeAutocomplete ({
11
+ abstract class AutocompleteField < QueryT extends AutocompleteQuery , ResultT extends AutocompleteResult , CandidateT > extends StatefulWidget {
12
+ const AutocompleteField ({
12
13
super .key,
13
- required this .narrow,
14
14
required this .controller,
15
15
required this .focusNode,
16
16
required this .fieldViewBuilder,
17
17
});
18
18
19
- /// The message list's narrow.
20
- final Narrow narrow;
21
-
22
- final ComposeContentController controller;
19
+ final TextEditingController controller;
23
20
final FocusNode focusNode;
24
21
final WidgetBuilder fieldViewBuilder;
25
22
23
+ AutocompleteIntent <QueryT >? autocompleteIntent ();
24
+
25
+ Widget buildItem (BuildContext context, int index, ResultT option);
26
+
27
+ AutocompleteView <QueryT , ResultT , CandidateT > initViewModel (BuildContext context);
28
+
26
29
@override
27
- State <ComposeAutocomplete > createState () => _ComposeAutocompleteState ();
30
+ State <AutocompleteField < QueryT , ResultT , CandidateT >> createState () => _AutocompleteFieldState < QueryT , ResultT , CandidateT > ();
28
31
}
29
32
30
- class _ComposeAutocompleteState extends State <ComposeAutocomplete > with PerAccountStoreAwareStateMixin <ComposeAutocomplete > {
31
- MentionAutocompleteView ? _viewModel; // TODO different autocomplete view types
33
+ class _AutocompleteFieldState < QueryT extends AutocompleteQuery , ResultT extends AutocompleteResult , CandidateT > extends State <AutocompleteField < QueryT , ResultT , CandidateT >> with PerAccountStoreAwareStateMixin <AutocompleteField < QueryT , ResultT , CandidateT > > {
34
+ AutocompleteView < QueryT , ResultT , CandidateT > ? _viewModel;
32
35
33
36
void _initViewModel () {
34
- final store = PerAccountStoreWidget .of (context);
35
- _viewModel = MentionAutocompleteView .init (store: store, narrow: widget.narrow)
37
+ _viewModel = widget.initViewModel (context)
36
38
..addListener (_viewModelChanged);
37
39
}
38
40
39
- void _composeContentChanged () {
40
- final newAutocompleteIntent = widget.controller. autocompleteIntent ();
41
+ void _handleControllerChange () {
42
+ final newAutocompleteIntent = widget.autocompleteIntent ();
41
43
if (newAutocompleteIntent != null ) {
42
44
if (_viewModel == null ) {
43
45
_initViewModel ();
@@ -55,7 +57,7 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
55
57
@override
56
58
void initState () {
57
59
super .initState ();
58
- widget.controller.addListener (_composeContentChanged );
60
+ widget.controller.addListener (_handleControllerChange );
59
61
}
60
62
61
63
@override
@@ -69,88 +71,43 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
69
71
}
70
72
71
73
@override
72
- void didUpdateWidget (covariant ComposeAutocomplete oldWidget) {
74
+ void didUpdateWidget (covariant AutocompleteField < QueryT , ResultT , CandidateT > oldWidget) {
73
75
super .didUpdateWidget (oldWidget);
74
76
if (widget.controller != oldWidget.controller) {
75
- oldWidget.controller.removeListener (_composeContentChanged );
76
- widget.controller.addListener (_composeContentChanged );
77
+ oldWidget.controller.removeListener (_handleControllerChange );
78
+ widget.controller.addListener (_handleControllerChange );
77
79
}
78
80
}
79
81
80
82
@override
81
83
void dispose () {
82
- widget.controller.removeListener (_composeContentChanged );
84
+ widget.controller.removeListener (_handleControllerChange );
83
85
_viewModel? .dispose (); // removes our listener
84
86
super .dispose ();
85
87
}
86
88
87
- List <MentionAutocompleteResult > _resultsToDisplay = [];
89
+ List <ResultT > _resultsToDisplay = [];
88
90
89
91
void _viewModelChanged () {
90
92
setState (() {
91
93
_resultsToDisplay = _viewModel! .results.toList ();
92
94
});
93
95
}
94
96
95
- void _onTapOption (MentionAutocompleteResult option) {
96
- // Probably the same intent that brought up the option that was tapped.
97
- // If not, it still shouldn't be off by more than the time it takes
98
- // to compute the autocomplete results, which we do asynchronously.
99
- final intent = widget.controller.autocompleteIntent ();
100
- if (intent == null ) {
101
- return ; // Shrug.
102
- }
103
-
104
- final store = PerAccountStoreWidget .of (context);
105
- final String replacementString;
106
- switch (option) {
107
- case UserMentionAutocompleteResult (: var userId):
108
- // TODO(i18n) language-appropriate space character; check active keyboard?
109
- // (maybe handle centrally in `widget.controller`)
110
- replacementString = '${mention (store .users [userId ]!, silent : intent .query .silent , users : store .users )} ' ;
111
- }
112
-
113
- widget.controller.value = intent.textEditingValue.replaced (
114
- TextRange (
115
- start: intent.syntaxStart,
116
- end: intent.textEditingValue.selection.end),
117
- replacementString,
118
- );
119
- }
120
-
121
- Widget _buildItem (BuildContext _, int index) {
122
- final option = _resultsToDisplay[index];
123
- Widget avatar;
124
- String label;
125
- switch (option) {
126
- case UserMentionAutocompleteResult (: var userId):
127
- avatar = Avatar (userId: userId, size: 32 , borderRadius: 3 );
128
- label = PerAccountStoreWidget .of (context).users[userId]! .fullName;
129
- }
130
- return InkWell (
131
- onTap: () {
132
- _onTapOption (option);
133
- },
134
- child: Padding (
135
- padding: const EdgeInsets .symmetric (horizontal: 16.0 , vertical: 8.0 ),
136
- child: Row (
137
- children: [
138
- avatar,
139
- const SizedBox (width: 8 ),
140
- Text (label),
141
- ])));
97
+ Widget _buildItem (BuildContext context, int index) {
98
+ return widget.buildItem (context, index, _resultsToDisplay[index]);
142
99
}
143
100
144
101
@override
145
102
Widget build (BuildContext context) {
146
- return RawAutocomplete <MentionAutocompleteResult >(
103
+ return RawAutocomplete <ResultT >(
147
104
textEditingController: widget.controller,
148
105
focusNode: widget.focusNode,
149
106
optionsBuilder: (_) => _resultsToDisplay,
150
107
optionsViewOpenDirection: OptionsViewOpenDirection .up,
151
108
// RawAutocomplete passes these when it calls optionsViewBuilder:
152
- // AutocompleteOnSelected<T > onSelected,
153
- // Iterable<T > options,
109
+ // AutocompleteOnSelected<CandidateT > onSelected,
110
+ // Iterable<CandidateT > options,
154
111
//
155
112
// We ignore them:
156
113
// - `onSelected` would cause some behavior we don't want,
@@ -159,7 +116,7 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
159
116
// the work of creating the list of options. We're not; the
160
117
// `optionsBuilder` we pass is just a function that returns
161
118
// _resultsToDisplay, which is computed with lots of help from
162
- // MentionAutocompleteView .
119
+ // AutocompleteView .
163
120
optionsViewBuilder: (context, _, __) {
164
121
return Align (
165
122
alignment: Alignment .bottomLeft,
@@ -187,3 +144,76 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
187
144
);
188
145
}
189
146
}
147
+
148
+ class ComposeAutocomplete extends AutocompleteField <MentionAutocompleteQuery , MentionAutocompleteResult , User > {
149
+ const ComposeAutocomplete ({
150
+ super .key,
151
+ required this .narrow,
152
+ required super .focusNode,
153
+ required super .fieldViewBuilder,
154
+ required ComposeContentController super .controller,
155
+ });
156
+
157
+ final Narrow narrow;
158
+
159
+ @override
160
+ ComposeContentController get controller => super .controller as ComposeContentController ;
161
+
162
+ @override
163
+ AutocompleteIntent <MentionAutocompleteQuery >? autocompleteIntent () => controller.autocompleteIntent ();
164
+
165
+ @override
166
+ AutocompleteView <MentionAutocompleteQuery , MentionAutocompleteResult , User > initViewModel (BuildContext context) {
167
+ final store = PerAccountStoreWidget .of (context);
168
+ return MentionAutocompleteView .init (store: store, narrow: narrow);
169
+ }
170
+
171
+ void _onTapOption (BuildContext context, MentionAutocompleteResult option) {
172
+ // Probably the same intent that brought up the option that was tapped.
173
+ // If not, it still shouldn't be off by more than the time it takes
174
+ // to compute the autocomplete results, which we do asynchronously.
175
+ final intent = autocompleteIntent ();
176
+ if (intent == null ) {
177
+ return ; // Shrug.
178
+ }
179
+
180
+ final store = PerAccountStoreWidget .of (context);
181
+ final String replacementString;
182
+ switch (option) {
183
+ case UserMentionAutocompleteResult (: var userId):
184
+ // TODO(i18n) language-appropriate space character; check active keyboard?
185
+ // (maybe handle centrally in `controller`)
186
+ replacementString = '${mention (store .users [userId ]!, silent : intent .query .silent , users : store .users )} ' ;
187
+ }
188
+
189
+ controller.value = intent.textEditingValue.replaced (
190
+ TextRange (
191
+ start: intent.syntaxStart,
192
+ end: intent.textEditingValue.selection.end),
193
+ replacementString,
194
+ );
195
+ }
196
+
197
+ @override
198
+ Widget buildItem (BuildContext context, int index, MentionAutocompleteResult option) {
199
+ Widget avatar;
200
+ String label;
201
+ switch (option) {
202
+ case UserMentionAutocompleteResult (: var userId):
203
+ avatar = Avatar (userId: userId, size: 32 , borderRadius: 3 );
204
+ label = PerAccountStoreWidget .of (context).users[userId]! .fullName;
205
+ }
206
+ return InkWell (
207
+ onTap: () {
208
+ _onTapOption (context, option);
209
+ },
210
+ child: Padding (
211
+ padding: const EdgeInsets .symmetric (horizontal: 16.0 , vertical: 8.0 ),
212
+ child: Row (
213
+ children: [
214
+ avatar,
215
+ const SizedBox (width: 8 ),
216
+ Text (label),
217
+ ])));
218
+ }
219
+ }
0 commit comments