@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
3
3
4
4
import '../api/model/events.dart' ;
5
5
import '../api/model/model.dart' ;
6
+ import '../api/route/streams.dart' ;
6
7
import '../widgets/compose_box.dart' ;
7
8
import 'narrow.dart' ;
8
9
import 'store.dart' ;
@@ -112,8 +113,10 @@ class AutocompleteIntent {
112
113
/// On reassemble, call [reassemble] .
113
114
class AutocompleteViewManager {
114
115
final Set <MentionAutocompleteView > _mentionAutocompleteViews = {};
116
+ final Set <TopicAutocompleteView > _topicAutocompleteViews = {};
115
117
116
118
AutocompleteDataCache autocompleteDataCache = AutocompleteDataCache ();
119
+ TopicAutocompleteDataCache topicAutocompleteDataCache = TopicAutocompleteDataCache ();
117
120
118
121
void registerMentionAutocomplete (MentionAutocompleteView view) {
119
122
final added = _mentionAutocompleteViews.add (view);
@@ -145,6 +148,16 @@ class AutocompleteViewManager {
145
148
autocompleteDataCache.invalidateUser (event.userId);
146
149
}
147
150
151
+ void registerTopicAutocomplete (TopicAutocompleteView view) {
152
+ final added = _topicAutocompleteViews.add (view);
153
+ assert (added);
154
+ }
155
+
156
+ void unregisterTopicAutocomplete (TopicAutocompleteView view) {
157
+ final removed = _topicAutocompleteViews.remove (view);
158
+ assert (removed);
159
+ }
160
+
148
161
/// Called when the app is reassembled during debugging, e.g. for hot reload.
149
162
///
150
163
/// Calls [MentionAutocompleteView.reassemble] for all that are registered.
@@ -183,7 +196,7 @@ abstract class AutocompleteView<Q, R> extends ChangeNotifier {
183
196
/// Called in particular when we get a [RealmUserEvent] .
184
197
void refreshStaleUserResults () {
185
198
if (_query != null ) {
186
- _startSearch (_query! );
199
+ _startSearch (_query as Q );
187
200
}
188
201
}
189
202
@@ -192,7 +205,7 @@ abstract class AutocompleteView<Q, R> extends ChangeNotifier {
192
205
/// This will redo the search from scratch for the current query, if any.
193
206
void reassemble () {
194
207
if (_query != null ) {
195
- _startSearch (_query! );
208
+ _startSearch (_query as Q );
196
209
}
197
210
}
198
211
@@ -363,3 +376,133 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
363
376
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
364
377
365
378
// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
379
+
380
+ /// A per-account manager for stream topics autocomplete interactions.
381
+ class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , Topic > {
382
+ TopicAutocompleteView ._({
383
+ required this .topics,
384
+ required this .store,
385
+ required this .narrow,
386
+ });
387
+
388
+ List <Topic >? topics;
389
+ final PerAccountStore store;
390
+ final StreamNarrow narrow;
391
+
392
+ static Future <TopicAutocompleteView > init ({
393
+ required PerAccountStore store,
394
+ required StreamNarrow narrow
395
+ }) async {
396
+ final view = TopicAutocompleteView ._(
397
+ store: store,
398
+ narrow: narrow,
399
+ topics: null ,
400
+ );
401
+ store.autocompleteViewManager.registerTopicAutocomplete (view);
402
+ return view;
403
+ }
404
+
405
+ @override
406
+ void dispose () {
407
+ store.autocompleteViewManager.unregisterTopicAutocomplete (this );
408
+ super .dispose ();
409
+ }
410
+
411
+ Future <void > fetch () async {
412
+ final result = await getStreamTopics (store.connection, streamId: narrow.streamId);
413
+ topics = result.topics;
414
+ notifyListeners ();
415
+ }
416
+
417
+ @override
418
+ Future <List <Topic >?> _computeResults (TopicAutocompleteQuery query) async {
419
+ if (topics == null ) return [];
420
+
421
+ final List <Topic > results = [];
422
+
423
+ final iterator = topics! .iterator;
424
+ bool isDone = false ;
425
+ while (! isDone) {
426
+ // CPU perf: End this task; enqueue a new one for resuming this work
427
+ await Future (() {});
428
+
429
+ if (query != _query || ! hasListeners) { // false if [dispose] has been called.
430
+ return null ;
431
+ }
432
+
433
+ for (int i = 0 ; i < 1000 ; i++ ) {
434
+ if (! iterator.moveNext ()) { // Can throw ConcurrentModificationError
435
+ isDone = true ;
436
+ break ;
437
+ }
438
+
439
+ final Topic topic = iterator.current;
440
+ if (query.testTopic (topic, store.autocompleteViewManager.topicAutocompleteDataCache)) {
441
+ results.add (topic);
442
+ }
443
+ }
444
+ }
445
+ return results; // TODO(#228) sort for most relevant first
446
+ }
447
+ }
448
+
449
+ class TopicAutocompleteQuery {
450
+ TopicAutocompleteQuery (this .raw)
451
+ : _lowercaseWords = raw.toLowerCase ().split (' ' );
452
+
453
+ final String raw;
454
+
455
+
456
+ final List <String > _lowercaseWords;
457
+
458
+ bool testTopic (Topic topic, TopicAutocompleteDataCache cache) {
459
+ return _testName (topic, cache);
460
+ }
461
+
462
+ bool _testName (Topic topic, TopicAutocompleteDataCache cache) {
463
+ // TODO(#237) test with diacritics stripped, where appropriate
464
+
465
+ final List <String > nameWords = cache.nameWordsForTopic (topic);
466
+
467
+ int nameWordsIndex = 0 ;
468
+ int queryWordsIndex = 0 ;
469
+ while (true ) {
470
+ if (queryWordsIndex == _lowercaseWords.length) {
471
+ return true ;
472
+ }
473
+ if (nameWordsIndex == nameWords.length) {
474
+ return false ;
475
+ }
476
+
477
+ if (nameWords[nameWordsIndex].startsWith (_lowercaseWords[queryWordsIndex])) {
478
+ queryWordsIndex++ ;
479
+ }
480
+ nameWordsIndex++ ;
481
+ }
482
+ }
483
+
484
+ @override
485
+ String toString () {
486
+ return '${objectRuntimeType (this , 'MentionAutocompleteQuery' )}(raw: $raw )' ;
487
+ }
488
+
489
+ @override
490
+ bool operator == (Object other) {
491
+ return other is TopicAutocompleteQuery && other.raw == raw;
492
+ }
493
+
494
+ @override
495
+ int get hashCode => Object .hash ('MentionAutocompleteQuery' , raw);
496
+ }
497
+
498
+ class TopicAutocompleteDataCache {
499
+ final Map <int , List <String >> _nameWordsByTopic = {};
500
+
501
+ List <String > nameWordsForTopic (Topic topic) {
502
+ return _nameWordsByTopic[topic.maxId] ?? = topic.name.toLowerCase ().split (' ' );
503
+ }
504
+
505
+ void invalidateTopic (int topicMaxId) {
506
+ _nameWordsByTopic.remove (topicMaxId);
507
+ }
508
+ }
0 commit comments