@@ -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' ;
@@ -43,6 +44,16 @@ extension ComposeContentAutocomplete on ComposeContentController {
43
44
}
44
45
}
45
46
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
+
46
57
final RegExp mentionAutocompleteMarkerRegex = (() {
47
58
// What's likely to come before an @-mention: the start of the string,
48
59
// whitespace, or punctuation. Letters are unlikely; in that case an email
@@ -112,6 +123,7 @@ class AutocompleteIntent<QueryT extends AutocompleteQuery> {
112
123
/// On reassemble, call [reassemble] .
113
124
class AutocompleteViewManager {
114
125
final Set <MentionAutocompleteView > _mentionAutocompleteViews = {};
126
+ final Set <TopicAutocompleteView > _topicAutocompleteViews = {};
115
127
116
128
AutocompleteDataCache autocompleteDataCache = AutocompleteDataCache ();
117
129
@@ -125,6 +137,16 @@ class AutocompleteViewManager {
125
137
assert (removed);
126
138
}
127
139
140
+ void registerTopicAutocomplete (TopicAutocompleteView view) {
141
+ final added = _topicAutocompleteViews.add (view);
142
+ assert (added);
143
+ }
144
+
145
+ void unregisterTopicAutocomplete (TopicAutocompleteView view) {
146
+ final removed = _topicAutocompleteViews.remove (view);
147
+ assert (removed);
148
+ }
149
+
128
150
void handleRealmUserRemoveEvent (RealmUserRemoveEvent event) {
129
151
autocompleteDataCache.invalidateUser (event.userId);
130
152
}
@@ -468,6 +490,7 @@ class MentionAutocompleteQuery extends AutocompleteQuery {
468
490
469
491
class AutocompleteDataCache {
470
492
final Map <int , List <String >> _nameWordsByUser = {};
493
+ final Map <String , List <String >> _wordsOfTopics = {};
471
494
472
495
List <String > nameWordsForUser (User user) {
473
496
return _nameWordsByUser[user.userId] ?? = user.fullName.toLowerCase ().split (' ' );
@@ -476,6 +499,14 @@ class AutocompleteDataCache {
476
499
void invalidateUser (int userId) {
477
500
_nameWordsByUser.remove (userId);
478
501
}
502
+
503
+ List <String > wordsOfTopic (Topic topic) {
504
+ return _wordsOfTopics[topic.value] ?? = topic.value.toLowerCase ().split (' ' );
505
+ }
506
+
507
+ void invalidateTopic (String value) {
508
+ _wordsOfTopics.remove (value);
509
+ }
479
510
}
480
511
481
512
class AutocompleteResult {}
@@ -491,3 +522,78 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
491
522
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
492
523
493
524
// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
525
+
526
+ class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult , Topic > {
527
+ TopicAutocompleteView ._({required super .store, required this .streamId});
528
+
529
+ factory TopicAutocompleteView .init ({required PerAccountStore store, required int streamId}) {
530
+ final view = TopicAutocompleteView ._(store: store, streamId: streamId);
531
+ store.autocompleteViewManager.registerTopicAutocomplete (view);
532
+ view.fetch ();
533
+ return view;
534
+ }
535
+
536
+ final int streamId;
537
+ Iterable <Topic > _topics = [];
538
+ bool _isFetching = false ;
539
+
540
+
541
+ /// Fetches topics of the current stream narrow, expected to fetch
542
+ /// only once per lifecycle.
543
+ ///
544
+ /// Starts fetching once the stream narrow is active, then when results
545
+ /// are fetched it notifies `autocompleteViewManager` to refresh UI
546
+ /// showing the newly fetched topics.
547
+ Future <void > fetch () async {
548
+ if (_isFetching) return ;
549
+ _isFetching = true ;
550
+ final result = await getStreamTopics (store.connection, streamId: streamId);
551
+ _topics = result.topics;
552
+ _isFetching = false ;
553
+ if (_query != null ) _startSearch (_query! );
554
+ }
555
+
556
+ @override
557
+ Iterable <Topic > getSortedItemsToTest (TopicAutocompleteQuery query) => _topics;
558
+
559
+ @override
560
+ TopicAutocompleteResult ? testItem (TopicAutocompleteQuery query, Topic item) {
561
+ if (query.testTopic (item, store.autocompleteViewManager.autocompleteDataCache)) {
562
+ return TopicAutocompleteResult (topic: item);
563
+ }
564
+ return null ;
565
+ }
566
+
567
+ @override
568
+ void dispose () {
569
+ store.autocompleteViewManager.unregisterTopicAutocomplete (this );
570
+ super .dispose ();
571
+ }
572
+ }
573
+
574
+ class TopicAutocompleteQuery extends AutocompleteQuery {
575
+ TopicAutocompleteQuery (super .raw);
576
+
577
+ bool testTopic (Topic topic, AutocompleteDataCache cache) {
578
+ return _testContainsQueryWords (cache.wordsOfTopic (topic));
579
+ }
580
+
581
+ @override
582
+ String toString () {
583
+ return '${objectRuntimeType (this , 'TopicAutocompleteQuery' )}(raw: $raw )' ;
584
+ }
585
+
586
+ @override
587
+ bool operator == (Object other) {
588
+ return other is TopicAutocompleteQuery && other.raw == raw;
589
+ }
590
+
591
+ @override
592
+ int get hashCode => Object .hash ('TopicAutocompleteQuery' , raw);
593
+ }
594
+
595
+ class TopicAutocompleteResult extends AutocompleteResult {
596
+ final Topic topic;
597
+
598
+ TopicAutocompleteResult ({required this .topic});
599
+ }
0 commit comments