@@ -8,7 +8,7 @@ import 'narrow.dart';
8
8
import 'store.dart' ;
9
9
10
10
extension ComposeContentAutocomplete on ComposeContentController {
11
- AutocompleteIntent ? autocompleteIntent () {
11
+ AutocompleteIntent < MentionAutocompleteQuery > ? autocompleteIntent () {
12
12
if (! selection.isValid || ! selection.isNormalized) {
13
13
// We don't require [isCollapsed] to be true because we've seen that
14
14
// autocorrect and even backspace involve programmatically expanding the
@@ -69,7 +69,7 @@ final RegExp mentionAutocompleteMarkerRegex = (() {
69
69
})();
70
70
71
71
/// The content controller's recognition that the user might want autocomplete UI.
72
- class AutocompleteIntent {
72
+ class AutocompleteIntent < Q extends AutocompleteQuery > {
73
73
AutocompleteIntent ({
74
74
required this .syntaxStart,
75
75
required this .query,
@@ -91,7 +91,7 @@ class AutocompleteIntent {
91
91
// that use a custom/subclassed [TextEditingValue], so that's not convenient.
92
92
final int syntaxStart;
93
93
94
- final MentionAutocompleteQuery query; // TODO other autocomplete query types
94
+ final Q query;
95
95
96
96
/// The [TextEditingValue] whose text [syntaxStart] refers to.
97
97
final TextEditingValue textEditingValue;
@@ -151,38 +151,115 @@ class AutocompleteViewManager {
151
151
// void dispose() { … }
152
152
}
153
153
154
- /// A view-model for a mention- autocomplete interaction.
154
+ /// A view-model for an autocomplete interaction.
155
155
///
156
156
/// The owner of one of these objects must call [dispose] when the object
157
157
/// will no longer be used, in order to free resources on the [PerAccountStore] .
158
158
///
159
159
/// Lifecycle:
160
- /// * Create with [init] .
160
+ /// * Create an instance of a concrete subtype .
161
161
/// * Add listeners with [addListener] .
162
162
/// * Use the [query] setter to start a search for a query.
163
163
/// * On reassemble, call [reassemble] .
164
164
/// * When the object will no longer be used, call [dispose] to free
165
165
/// resources on the [PerAccountStore].
166
- class MentionAutocompleteView extends ChangeNotifier {
167
- MentionAutocompleteView ._({
168
- required this .store,
169
- required this .narrow,
170
- required this .sortedUsers,
171
- });
166
+ abstract class AutocompleteView <Q extends AutocompleteQuery , R extends AutocompleteResult > extends ChangeNotifier {
172
167
173
- factory MentionAutocompleteView .init ({
174
- required PerAccountStore store,
175
- required Narrow narrow,
176
- }) {
177
- final view = MentionAutocompleteView ._(
178
- store: store,
179
- narrow: narrow,
180
- sortedUsers: _usersByRelevance (store: store, narrow: narrow),
181
- );
182
- store.autocompleteViewManager.registerMentionAutocomplete (view);
183
- return view;
168
+ /// This could be used to transform results after they've been
169
+ /// computed for sorting, filtering etc.
170
+ final List <R > Function (List <R > results)? resultsFilter;
171
+ final PerAccountStore store;
172
+
173
+ AutocompleteView ({this .resultsFilter, required this .store});
174
+
175
+ Iterable <Object > getDataForQuery (Q query);
176
+
177
+ R ? testItem (Q query, Object item);
178
+
179
+ Q ? get query => _query;
180
+ Q ? _query;
181
+ set query (Q ? query) {
182
+ _query = query;
183
+ if (query != null ) {
184
+ _startSearch (query);
185
+ }
186
+ }
187
+
188
+ /// Called when the app is reassembled during debugging, e.g. for hot reload.
189
+ ///
190
+ /// This will redo the search from scratch for the current query, if any.
191
+ void reassemble () {
192
+ if (_query != null ) {
193
+ _startSearch (_query! );
194
+ }
195
+ }
196
+
197
+ Iterable <R > get results => _results;
198
+ List <R > _results = [];
199
+
200
+ Future <void > _startSearch (Q query) async {
201
+ List <R >? newResults;
202
+
203
+ while (true ) {
204
+ try {
205
+ newResults = await _computeResults (query);
206
+ break ;
207
+ } on ConcurrentModificationError {
208
+ // Retry
209
+ // TODO backoff?
210
+ }
211
+ }
212
+
213
+ if (newResults == null ) {
214
+ // Query was old; new search is in progress. Or, no listeners to notify.
215
+ return ;
216
+ }
217
+
218
+ _results = newResults;
219
+ notifyListeners ();
220
+ }
221
+
222
+ Future <List <R >?> _computeResults (Q query) async {
223
+ final List <R > results = [];
224
+ final Iterable <Object > data = getDataForQuery (query);
225
+
226
+ final iterator = data.iterator;
227
+ bool isDone = false ;
228
+ while (! isDone) {
229
+ // CPU perf: End this task; enqueue a new one for resuming this work
230
+ await Future (() {});
231
+
232
+ if (query != _query || ! hasListeners) { // false if [dispose] has been called.
233
+ return null ;
234
+ }
235
+
236
+ for (int i = 0 ; i < 1000 ; i++ ) {
237
+ if (! iterator.moveNext ()) {
238
+ isDone = true ;
239
+ break ;
240
+ }
241
+ final Object item = iterator.current;
242
+ final result = testItem (query, item);
243
+ if (result != null ) results.add (result);
244
+ }
245
+ }
246
+ return resultsFilter? .call (results) ?? results;
247
+ }
248
+ }
249
+
250
+ class MentionAutocompleteView extends AutocompleteView <MentionAutocompleteQuery , MentionAutocompleteResult > {
251
+ final Narrow narrow;
252
+
253
+ MentionAutocompleteView .init ({
254
+ required super .store,
255
+ required this .narrow,
256
+ }) : sortedUsers = _usersByRelevance (store: store, narrow: narrow)
257
+ {
258
+ store.autocompleteViewManager.registerMentionAutocomplete (this );
184
259
}
185
260
261
+ final List <User > sortedUsers;
262
+
186
263
static List <User > _usersByRelevance ({
187
264
required PerAccountStore store,
188
265
required Narrow narrow,
@@ -192,6 +269,21 @@ class MentionAutocompleteView extends ChangeNotifier {
192
269
..sort ((userA, userB) => compareByDms (userA, userB, store: store));
193
270
}
194
271
272
+ @override
273
+ Iterable <Object > getDataForQuery (MentionAutocompleteQuery query) {
274
+ return sortedUsers;
275
+ }
276
+
277
+ @override
278
+ MentionAutocompleteResult ? testItem (MentionAutocompleteQuery query, Object item) {
279
+ if (item is User ) {
280
+ if (query.testUser (item, store.autocompleteViewManager.autocompleteDataCache)) {
281
+ return UserMentionAutocompleteResult (userId: item.userId);
282
+ }
283
+ }
284
+ return null ;
285
+ }
286
+
195
287
/// Determines which of the two users is more recent in DM conversations.
196
288
///
197
289
/// Returns a negative number if [userA] is more recent than [userB] ,
@@ -221,76 +313,39 @@ class MentionAutocompleteView extends ChangeNotifier {
221
313
super .dispose ();
222
314
}
223
315
224
- final PerAccountStore store;
225
- final Narrow narrow;
226
- final List <User > sortedUsers;
227
-
228
- MentionAutocompleteQuery ? get query => _query;
229
- MentionAutocompleteQuery ? _query;
230
- set query (MentionAutocompleteQuery ? query) {
231
- _query = query;
232
- if (query != null ) {
233
- _startSearch (query);
234
- }
235
- }
236
-
237
- /// Called when the app is reassembled during debugging, e.g. for hot reload.
316
+ /// Recompute user results for the current query, if any.
238
317
///
239
- /// This will redo the search from scratch for the current query, if any .
240
- void reassemble () {
318
+ /// Called in particular when we get a [RealmUserEvent] .
319
+ void refreshStaleUserResults () {
241
320
if (_query != null ) {
242
321
_startSearch (_query! );
243
322
}
244
323
}
324
+ }
245
325
246
- Iterable < MentionAutocompleteResult > get results => _results;
247
- List < MentionAutocompleteResult > _results = [] ;
326
+ abstract class AutocompleteQuery {
327
+ AutocompleteQuery ( this .raw) ;
248
328
249
- Future <void > _startSearch (MentionAutocompleteQuery query) async {
250
- final newResults = await _computeResults (query);
251
- if (newResults == null ) {
252
- // Query was old; new search is in progress. Or, no listeners to notify.
253
- return ;
254
- }
329
+ final String raw;
255
330
256
- _results = newResults;
257
- notifyListeners ();
331
+ @override
332
+ String toString () {
333
+ return '${objectRuntimeType (this , 'AutocompleteQuery' )}(raw: $raw })' ;
258
334
}
259
335
260
- Future <List <MentionAutocompleteResult >?> _computeResults (MentionAutocompleteQuery query) async {
261
- final List <MentionAutocompleteResult > results = [];
262
- final iterator = sortedUsers.iterator;
263
- bool isDone = false ;
264
- while (! isDone) {
265
- // CPU perf: End this task; enqueue a new one for resuming this work
266
- await Future (() {});
267
-
268
- if (query != _query || ! hasListeners) { // false if [dispose] has been called.
269
- return null ;
270
- }
271
-
272
- for (int i = 0 ; i < 1000 ; i++ ) {
273
- if (! iterator.moveNext ()) {
274
- isDone = true ;
275
- break ;
276
- }
277
-
278
- final User user = iterator.current;
279
- if (query.testUser (user, store.autocompleteViewManager.autocompleteDataCache)) {
280
- results.add (UserMentionAutocompleteResult (userId: user.userId));
281
- }
282
- }
283
- }
284
- return results;
336
+ @override
337
+ bool operator == (Object other) {
338
+ return other is AutocompleteQuery && other.raw == raw;
285
339
}
340
+
341
+ @override
342
+ int get hashCode => Object .hash ('AutocompleteQuery' , raw);
286
343
}
287
344
288
- class MentionAutocompleteQuery {
289
- MentionAutocompleteQuery (this .raw, {this .silent = false })
345
+ class MentionAutocompleteQuery extends AutocompleteQuery {
346
+ MentionAutocompleteQuery (super .raw, {this .silent = false })
290
347
: _lowercaseWords = raw.toLowerCase ().split (' ' );
291
348
292
- final String raw;
293
-
294
349
/// Whether the user wants a silent mention (@_query, vs. @query).
295
350
final bool silent;
296
351
@@ -352,7 +407,9 @@ class AutocompleteDataCache {
352
407
}
353
408
}
354
409
355
- sealed class MentionAutocompleteResult {}
410
+ class AutocompleteResult {}
411
+
412
+ sealed class MentionAutocompleteResult extends AutocompleteResult {}
356
413
357
414
class UserMentionAutocompleteResult extends MentionAutocompleteResult {
358
415
UserMentionAutocompleteResult ({required this .userId});
0 commit comments