@@ -7,37 +7,101 @@ import '../model/compose.dart';
7
7
import '../model/narrow.dart' ;
8
8
import 'compose_box.dart' ;
9
9
10
- class ComposeAutocomplete extends StatefulWidget {
11
- const ComposeAutocomplete ({
10
+ class ComposeAutocomplete extends AutocompleteField <MentionAutocompleteQuery , MentionAutocompleteResult > {
11
+ ComposeAutocomplete ({
12
+ super .key,
13
+ required Narrow narrow,
14
+ required ComposeContentController controller,
15
+ required super .focusNode,
16
+ required super .fieldViewBuilder
17
+ }) : super (
18
+ controller: controller,
19
+ getAutocompleteIntent: () => controller.autocompleteIntent (),
20
+ viewModelBuilder: (context) {
21
+ final store = PerAccountStoreWidget .of (context);
22
+ return MentionAutocompleteView .init (store: store, narrow: narrow);
23
+ },
24
+ itemBuilder: (context, index, option) {
25
+ Widget avatar;
26
+ String label;
27
+ switch (option) {
28
+ case UserMentionAutocompleteResult (: var userId):
29
+ avatar = Avatar (userId: userId, size: 32 , borderRadius: 3 );
30
+ label = PerAccountStoreWidget .of (context).users[userId]! .fullName;
31
+ default :
32
+ avatar = const SizedBox ();
33
+ label = '' ;
34
+ }
35
+ return InkWell (
36
+ onTap: () {
37
+ // Probably the same intent that brought up the option that was tapped.
38
+ // If not, it still shouldn't be off by more than the time it takes
39
+ // to compute the autocomplete results, which we do asynchronously.
40
+ final intent = controller.autocompleteIntent ();
41
+ if (intent == null ) {
42
+ return ; // Shrug.
43
+ }
44
+
45
+ final store = PerAccountStoreWidget .of (context);
46
+ final String replacementString;
47
+ switch (option) {
48
+ case UserMentionAutocompleteResult (: var userId):
49
+ // TODO(i18n) language-appropriate space character; check active keyboard?
50
+ // (maybe handle centrally in `controller`)
51
+ replacementString = '${mention (store .users [userId ]!, silent : intent .query .silent , users : store .users )} ' ;
52
+ default :
53
+ replacementString = '' ;
54
+ }
55
+
56
+ controller.value = intent.textEditingValue.replaced (
57
+ TextRange (
58
+ start: intent.syntaxStart,
59
+ end: intent.textEditingValue.selection.end),
60
+ replacementString,
61
+ );
62
+ },
63
+ child: Padding (
64
+ padding: const EdgeInsets .symmetric (horizontal: 16.0 , vertical: 8.0 ),
65
+ child: Row (
66
+ children: [
67
+ avatar,
68
+ const SizedBox (width: 8 ),
69
+ Text (label)])));
70
+ });
71
+ }
72
+
73
+ class AutocompleteField <Q extends AutocompleteQuery , R extends AutocompleteResult > extends StatefulWidget {
74
+ const AutocompleteField ({
12
75
super .key,
13
- required this .narrow,
14
76
required this .controller,
15
77
required this .focusNode,
16
78
required this .fieldViewBuilder,
79
+ required this .itemBuilder,
80
+ required this .viewModelBuilder,
81
+ required this .getAutocompleteIntent,
17
82
});
18
83
19
- /// The message list's narrow.
20
- final Narrow narrow;
21
-
22
- final ComposeContentController controller;
84
+ final TextEditingController controller;
23
85
final FocusNode focusNode;
24
86
final WidgetBuilder fieldViewBuilder;
87
+ final Widget ? Function (BuildContext , int , R ) itemBuilder;
88
+ final AutocompleteView <Q , R > Function (BuildContext ) viewModelBuilder;
89
+ final AutocompleteIntent <Q >? Function () getAutocompleteIntent;
25
90
26
91
@override
27
- State <ComposeAutocomplete > createState () => _ComposeAutocompleteState ();
92
+ State <AutocompleteField < Q , R >> createState () => _AutocompleteFieldState < Q , R > ();
28
93
}
29
94
30
- class _ComposeAutocompleteState extends State <ComposeAutocomplete > with PerAccountStoreAwareStateMixin <ComposeAutocomplete > {
31
- MentionAutocompleteView ? _viewModel; // TODO different autocomplete view types
95
+ class _AutocompleteFieldState < Q extends AutocompleteQuery , R extends AutocompleteResult > extends State <AutocompleteField < Q , R >> with PerAccountStoreAwareStateMixin <AutocompleteField < Q , R > > {
96
+ AutocompleteView < Q , R > ? _viewModel;
32
97
33
98
void _initViewModel () {
34
- final store = PerAccountStoreWidget .of (context);
35
- _viewModel = MentionAutocompleteView .init (store: store, narrow: widget.narrow)
99
+ _viewModel = widget.viewModelBuilder (context)
36
100
..addListener (_viewModelChanged);
37
101
}
38
102
39
- void _composeContentChanged () {
40
- final newAutocompleteIntent = widget.controller. autocompleteIntent ();
103
+ void _onChanged () {
104
+ final AutocompleteIntent < Q > ? newAutocompleteIntent = widget.getAutocompleteIntent ();
41
105
if (newAutocompleteIntent != null ) {
42
106
if (_viewModel == null ) {
43
107
_initViewModel ();
@@ -55,7 +119,7 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
55
119
@override
56
120
void initState () {
57
121
super .initState ();
58
- widget.controller.addListener (_composeContentChanged );
122
+ widget.controller.addListener (_onChanged );
59
123
}
60
124
61
125
@override
@@ -69,81 +133,32 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
69
133
}
70
134
71
135
@override
72
- void didUpdateWidget (covariant ComposeAutocomplete oldWidget) {
136
+ void didUpdateWidget (covariant AutocompleteField < Q , R > oldWidget) {
73
137
super .didUpdateWidget (oldWidget);
74
138
if (widget.controller != oldWidget.controller) {
75
- oldWidget.controller.removeListener (_composeContentChanged );
76
- widget.controller.addListener (_composeContentChanged );
139
+ oldWidget.controller.removeListener (_onChanged );
140
+ widget.controller.addListener (_onChanged );
77
141
}
78
142
}
79
143
80
144
@override
81
145
void dispose () {
82
- widget.controller.removeListener (_composeContentChanged );
146
+ widget.controller.removeListener (_onChanged );
83
147
_viewModel? .dispose (); // removes our listener
84
148
super .dispose ();
85
149
}
86
150
87
- List <MentionAutocompleteResult > _resultsToDisplay = [];
151
+ List <R > _resultsToDisplay = [];
88
152
89
153
void _viewModelChanged () {
90
154
setState (() {
91
155
_resultsToDisplay = _viewModel! .results.toList ();
92
156
});
93
157
}
94
158
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
- ])));
142
- }
143
-
144
159
@override
145
160
Widget build (BuildContext context) {
146
- return RawAutocomplete <MentionAutocompleteResult >(
161
+ return RawAutocomplete <R >(
147
162
textEditingController: widget.controller,
148
163
focusNode: widget.focusNode,
149
164
optionsBuilder: (_) => _resultsToDisplay,
@@ -159,20 +174,20 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
159
174
// the work of creating the list of options. We're not; the
160
175
// `optionsBuilder` we pass is just a function that returns
161
176
// _resultsToDisplay, which is computed with lots of help from
162
- // MentionAutocompleteView .
163
- optionsViewBuilder: (context, _, __) {
164
- return Align (
165
- alignment : Alignment .bottomLeft,
166
- child : Material (
167
- elevation : 4.0 ,
168
- child : ConstrainedBox (
169
- constraints : const BoxConstraints (maxHeight : 300 ), // TODO not hard-coded
170
- child : ListView . builder (
171
- padding : EdgeInsets .zero ,
172
- shrinkWrap : true ,
173
- itemCount : _resultsToDisplay.length,
174
- itemBuilder: _buildItem))) );
175
- } ,
177
+ // AutocompleteView .
178
+ optionsViewBuilder: (context, _, __) => Align (
179
+ alignment : Alignment .bottomLeft,
180
+ child : Material (
181
+ elevation : 4.0 ,
182
+ child : ConstrainedBox (
183
+ constraints : const BoxConstraints (maxHeight : 300 ), // TODO not hard-coded
184
+ child : ListView . builder (
185
+ padding : EdgeInsets .zero,
186
+ shrinkWrap : true ,
187
+ itemCount : _resultsToDisplay.length ,
188
+ itemBuilder : (context, index) {
189
+ return widget. itemBuilder (context, index, _resultsToDisplay[index] );
190
+ })))) ,
176
191
// RawAutocomplete passes these when it calls fieldViewBuilder:
177
192
// TextEditingController textEditingController,
178
193
// FocusNode focusNode,
0 commit comments