@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
2
2
3
3
import 'store.dart' ;
4
4
import '../model/autocomplete.dart' ;
5
+ import '../model/compose.dart' ;
5
6
import '../model/narrow.dart' ;
6
7
import 'compose_box.dart' ;
7
8
@@ -32,13 +33,14 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> {
32
33
final newAutocompleteIntent = widget.controller.autocompleteIntent ();
33
34
if (newAutocompleteIntent != null ) {
34
35
final store = PerAccountStoreWidget .of (context);
35
- _viewModel ?? = MentionAutocompleteView .init (
36
- store : store, narrow : widget.narrow );
36
+ _viewModel ?? = MentionAutocompleteView .init (store : store, narrow : widget.narrow)
37
+ .. addListener (_viewModelChanged );
37
38
_viewModel! .query = newAutocompleteIntent.query;
38
39
} else {
39
40
if (_viewModel != null ) {
40
- _viewModel! .dispose ();
41
+ _viewModel! .dispose (); // removes our listener
41
42
_viewModel = null ;
43
+ _resultsToDisplay = [];
42
44
}
43
45
}
44
46
}
@@ -61,12 +63,112 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> {
61
63
@override
62
64
void dispose () {
63
65
widget.controller.removeListener (_composeContentChanged);
64
- _viewModel? .dispose ();
66
+ _viewModel? .dispose (); // removes our listener
65
67
super .dispose ();
66
68
}
67
69
70
+ List <MentionAutocompleteResult > _resultsToDisplay = [];
71
+
72
+ void _viewModelChanged () {
73
+ setState (() {
74
+ _resultsToDisplay = _viewModel! .results.toList ();
75
+ });
76
+ }
77
+
78
+ void _onTapOption (MentionAutocompleteResult option) {
79
+ // Probably the same intent that brought up the option that was tapped.
80
+ // If not, it still shouldn't be off by more than the time it takes
81
+ // to compute the autocomplete results, which we do asynchronously.
82
+ final intent = widget.controller.autocompleteIntent ();
83
+ if (intent == null ) {
84
+ return ; // Shrug.
85
+ }
86
+
87
+ final store = PerAccountStoreWidget .of (context);
88
+ final String replacementString;
89
+ switch (option) {
90
+ case UserMentionAutocompleteResult (: var userId):
91
+ // TODO(i18n) language-appropriate space character; check active keyboard?
92
+ // (maybe handle centrally in `widget.controller`)
93
+ replacementString = '${mention (store .users [userId ]!, silent : intent .query .silent , users : store .users )} ' ;
94
+ case WildcardMentionAutocompleteResult ():
95
+ replacementString = '[unimplemented]' ; // TODO
96
+ case UserGroupMentionAutocompleteResult ():
97
+ replacementString = '[unimplemented]' ; // TODO
98
+ }
99
+
100
+ widget.controller.value = intent.textEditingValue.replaced (
101
+ TextRange (
102
+ start: intent.syntaxStart,
103
+ end: intent.textEditingValue.selection.end),
104
+ replacementString,
105
+ );
106
+ }
107
+
108
+ Widget _buildItem (BuildContext _, int index) {
109
+ final option = _resultsToDisplay[index];
110
+ String label;
111
+ switch (option) {
112
+ case UserMentionAutocompleteResult (: var userId):
113
+ // TODO avatar
114
+ label = PerAccountStoreWidget .of (context).users[userId]! .fullName;
115
+ case WildcardMentionAutocompleteResult ():
116
+ label = '[unimplemented]' ; // TODO
117
+ case UserGroupMentionAutocompleteResult ():
118
+ label = '[unimplemented]' ; // TODO
119
+ }
120
+ return InkWell (
121
+ onTap: () {
122
+ _onTapOption (option);
123
+ },
124
+ child: Padding (
125
+ padding: const EdgeInsets .all (16.0 ),
126
+ child: Text (label)));
127
+ }
128
+
68
129
@override
69
130
Widget build (BuildContext context) {
70
- return widget.fieldViewBuilder (context);
131
+ return RawAutocomplete <MentionAutocompleteResult >(
132
+ textEditingController: widget.controller,
133
+ focusNode: widget.focusNode,
134
+ optionsBuilder: (_) => _resultsToDisplay,
135
+ optionsViewOpenDirection: OptionsViewOpenDirection .up,
136
+ // RawAutocomplete passes these when it calls optionsViewBuilder:
137
+ // AutocompleteOnSelected<T> onSelected,
138
+ // Iterable<T> options,
139
+ //
140
+ // We ignore them:
141
+ // - `onSelected` would cause some behavior we don't want,
142
+ // such as moving the cursor to the end of the compose-input text.
143
+ // - `options` would be needed if we were delegating to RawAutocomplete
144
+ // the work of creating the list of options. We're not; the
145
+ // `optionsBuilder` we pass is just a function that returns
146
+ // _resultsToDisplay, which is computed with lots of help from
147
+ // MentionAutocompleteView.
148
+ optionsViewBuilder: (context, _, __) {
149
+ return Align (
150
+ alignment: Alignment .bottomLeft,
151
+ child: Material (
152
+ elevation: 4.0 ,
153
+ child: ConstrainedBox (
154
+ constraints: const BoxConstraints (maxHeight: 300 ), // TODO not hard-coded
155
+ child: ListView .builder (
156
+ padding: EdgeInsets .zero,
157
+ shrinkWrap: true ,
158
+ itemCount: _resultsToDisplay.length,
159
+ itemBuilder: _buildItem))));
160
+ },
161
+ // RawAutocomplete passes these when it calls fieldViewBuilder:
162
+ // TextEditingController textEditingController,
163
+ // FocusNode focusNode,
164
+ // VoidCallback onFieldSubmitted,
165
+ //
166
+ // We ignore them. For the first two, we've opted out of having
167
+ // RawAutocomplete create them for us; we create and manage them ourselves.
168
+ // The third isn't helpful; it lets us opt into behavior we don't actually
169
+ // want (see discussion:
170
+ // <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/autocomplete.20UI/near/1599994>)
171
+ fieldViewBuilder: (context, _, __, ___) => widget.fieldViewBuilder (context),
172
+ );
71
173
}
72
174
}
0 commit comments