@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
33
44import '../api/model/events.dart' ;
55import '../api/model/model.dart' ;
6+ import '../api/route/streams.dart' ;
67import '../widgets/compose_box.dart' ;
78import 'narrow.dart' ;
89import 'store.dart' ;
@@ -112,8 +113,10 @@ class AutocompleteIntent {
112113/// On reassemble, call [reassemble] .
113114class AutocompleteViewManager {
114115 final Set <MentionAutocompleteView > _mentionAutocompleteViews = {};
116+ final Set <TopicAutocompleteView > _topicAutocompleteViews = {};
115117
116118 AutocompleteDataCache autocompleteDataCache = AutocompleteDataCache ();
119+ TopicAutocompleteDataCache topicAutocompleteDataCache = TopicAutocompleteDataCache ();
117120
118121 void registerMentionAutocomplete (MentionAutocompleteView view) {
119122 final added = _mentionAutocompleteViews.add (view);
@@ -145,6 +148,16 @@ class AutocompleteViewManager {
145148 autocompleteDataCache.invalidateUser (event.userId);
146149 }
147150
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+
148161 /// Called when the app is reassembled during debugging, e.g. for hot reload.
149162 ///
150163 /// Calls [MentionAutocompleteView.reassemble] for all that are registered.
@@ -363,3 +376,133 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
363376// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
364377
365378// 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