@@ -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' ;
@@ -43,6 +44,16 @@ extension ComposeContentAutocomplete on ComposeContentController {
4344 }
4445}
4546
47+ extension ComposeTopicAutocomplete on ComposeTopicController {
48+ AutocompleteIntent <TopicAutocompleteQuery >? autocompleteIntent () {
49+ if (! selection.isValid || ! selection.isNormalized) return null ;
50+ return AutocompleteIntent (
51+ syntaxStart: 0 ,
52+ query: TopicAutocompleteQuery (value.text),
53+ textEditingValue: value);
54+ }
55+ }
56+
4657final RegExp mentionAutocompleteMarkerRegex = (() {
4758 // What's likely to come before an @-mention: the start of the string,
4859 // whitespace, or punctuation. Letters are unlikely; in that case an email
@@ -150,6 +161,12 @@ class AutocompleteViewManager {
150161 autocompleteDataCache.invalidateUser (event.userId);
151162 }
152163
164+ void handleTopicsFetchCompleted () {
165+ for (final view in _getViewsOfType <TopicAutocompleteView >()) {
166+ view.reassemble ();
167+ }
168+ }
169+
153170 /// Called when the app is reassembled during debugging, e.g. for hot reload.
154171 ///
155172 /// Calls [AutocompleteView.reassemble] for all that are registered.
@@ -407,6 +424,7 @@ class MentionAutocompleteQuery extends AutocompleteQuery {
407424
408425class AutocompleteDataCache {
409426 final Map <int , List <String >> _nameWordsByUser = {};
427+ final Map <int , List <String >> _nameWordsByTopic = {};
410428
411429 List <String > nameWordsForUser (User user) {
412430 return _nameWordsByUser[user.userId] ?? = user.fullName.toLowerCase ().split (' ' );
@@ -415,6 +433,14 @@ class AutocompleteDataCache {
415433 void invalidateUser (int userId) {
416434 _nameWordsByUser.remove (userId);
417435 }
436+
437+ List <String > nameWordsForTopic (Topic topic) {
438+ return _nameWordsByTopic[topic.maxId] ?? = topic.name.toLowerCase ().split (' ' );
439+ }
440+
441+ void invalidateTopic (int topicMaxId) {
442+ _nameWordsByTopic.remove (topicMaxId);
443+ }
418444}
419445
420446class AutocompleteResult {}
@@ -430,3 +456,77 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
430456// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
431457
432458// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
459+
460+ class TopicAutocompleteDataProvider extends AutocompleteDataProvider <Topic , TopicAutocompleteQuery , TopicAutocompleteResult > {
461+ final PerAccountStore store;
462+ final int streamId;
463+ Iterable <Topic >? _topics;
464+ bool _isFetching = false ;
465+
466+ TopicAutocompleteDataProvider ({required this .store, required this .streamId});
467+
468+ /// Fetches topics of the current stream narrow, expected to fetch
469+ /// only once per lifecycle.
470+ ///
471+ /// Starts fetching once the stream narrow is active, then when results
472+ /// are fetched it notifies `autocompleteViewManager` to refresh UI
473+ /// showing the newly fetched topics.
474+ Future <void > fetch () async {
475+ if (_topics != null && ! _isFetching) return ;
476+ _isFetching = true ;
477+ final result = await getStreamTopics (store.connection, streamId: streamId);
478+ _topics = result.topics;
479+ store.autocompleteViewManager.handleTopicsFetchCompleted ();
480+ _isFetching = false ;
481+ }
482+
483+ @override
484+ Iterable <Topic > getDataForQuery (TopicAutocompleteQuery query) {
485+ return _topics ?? [];
486+ }
487+
488+ @override
489+ TopicAutocompleteResult ? testItem (TopicAutocompleteQuery query, Topic item) {
490+ if (query.testTopic (item, store.autocompleteViewManager.autocompleteDataCache)) {
491+ return TopicAutocompleteResult (topic: item);
492+ }
493+ return null ;
494+ }
495+ }
496+
497+ class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult > {
498+ TopicAutocompleteView .init ({
499+ required super .store,
500+ required int streamId,
501+ }) : super (dataProvider: TopicAutocompleteDataProvider (
502+ store: store,
503+ streamId: streamId
504+ )..fetch ());
505+ }
506+
507+ class TopicAutocompleteQuery extends AutocompleteQuery {
508+ TopicAutocompleteQuery (super .raw);
509+
510+ bool testTopic (Topic topic, AutocompleteDataCache cache) {
511+ return _testContainsQueryWords (cache.nameWordsForTopic (topic));
512+ }
513+
514+ @override
515+ String toString () {
516+ return '${objectRuntimeType (this , 'TopicAutocompleteQuery' )}(raw: $raw )' ;
517+ }
518+
519+ @override
520+ bool operator == (Object other) {
521+ return other is TopicAutocompleteQuery && other.raw == raw;
522+ }
523+
524+ @override
525+ int get hashCode => Object .hash ('TopicAutocompleteQuery' , raw);
526+ }
527+
528+ class TopicAutocompleteResult extends AutocompleteResult {
529+ final Topic topic;
530+
531+ TopicAutocompleteResult ({required this .topic});
532+ }
0 commit comments