@@ -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<Q 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
}
@@ -464,6 +486,7 @@ class MentionAutocompleteQuery extends AutocompleteQuery {
464
486
465
487
class AutocompleteDataCache {
466
488
final Map <int , List <String >> _nameWordsByUser = {};
489
+ final Map <String , List <String >> _wordsOfTopics = {};
467
490
468
491
List <String > nameWordsForUser (User user) {
469
492
return _nameWordsByUser[user.userId] ?? = user.fullName.toLowerCase ().split (' ' );
@@ -472,6 +495,14 @@ class AutocompleteDataCache {
472
495
void invalidateUser (int userId) {
473
496
_nameWordsByUser.remove (userId);
474
497
}
498
+
499
+ List <String > wordsOfTopic (Topic topic) {
500
+ return _wordsOfTopics[topic.value] ?? = topic.value.toLowerCase ().split (' ' );
501
+ }
502
+
503
+ void invalidateTopic (String value) {
504
+ _wordsOfTopics.remove (value);
505
+ }
475
506
}
476
507
477
508
class AutocompleteResult {}
@@ -487,3 +518,73 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
487
518
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
488
519
489
520
// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
521
+
522
+ class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult , Topic > {
523
+ final int streamId;
524
+ Iterable <Topic > _topics = [];
525
+ bool _isFetching = false ;
526
+
527
+ TopicAutocompleteView .init ({required super .store, required this .streamId}) {
528
+ store.autocompleteViewManager.registerTopicAutocomplete (this );
529
+ fetch ();
530
+ }
531
+
532
+ /// Fetches topics of the current stream narrow, expected to fetch
533
+ /// only once per lifecycle.
534
+ ///
535
+ /// Starts fetching once the stream narrow is active, then when results
536
+ /// are fetched it notifies `autocompleteViewManager` to refresh UI
537
+ /// showing the newly fetched topics.
538
+ Future <void > fetch () async {
539
+ if (_isFetching) return ;
540
+ _isFetching = true ;
541
+ final result = await getStreamTopics (store.connection, streamId: streamId);
542
+ _topics = result.topics;
543
+ _isFetching = false ;
544
+ if (_query != null ) _startSearch (_query! );
545
+ }
546
+
547
+ @override
548
+ Iterable <Topic > getSortedItemsToTest (TopicAutocompleteQuery query) => _topics;
549
+
550
+ @override
551
+ TopicAutocompleteResult ? testItem (TopicAutocompleteQuery query, Topic item) {
552
+ if (query.testTopic (item, store.autocompleteViewManager.autocompleteDataCache)) {
553
+ return TopicAutocompleteResult (topic: item);
554
+ }
555
+ return null ;
556
+ }
557
+
558
+ @override
559
+ void dispose () {
560
+ store.autocompleteViewManager.unregisterTopicAutocomplete (this );
561
+ super .dispose ();
562
+ }
563
+ }
564
+
565
+ class TopicAutocompleteQuery extends AutocompleteQuery {
566
+ TopicAutocompleteQuery (super .raw);
567
+
568
+ bool testTopic (Topic topic, AutocompleteDataCache cache) {
569
+ return _testContainsQueryWords (cache.wordsOfTopic (topic));
570
+ }
571
+
572
+ @override
573
+ String toString () {
574
+ return '${objectRuntimeType (this , 'TopicAutocompleteQuery' )}(raw: $raw )' ;
575
+ }
576
+
577
+ @override
578
+ bool operator == (Object other) {
579
+ return other is TopicAutocompleteQuery && other.raw == raw;
580
+ }
581
+
582
+ @override
583
+ int get hashCode => Object .hash ('TopicAutocompleteQuery' , raw);
584
+ }
585
+
586
+ class TopicAutocompleteResult extends AutocompleteResult {
587
+ final Topic topic;
588
+
589
+ TopicAutocompleteResult ({required this .topic});
590
+ }
0 commit comments