@@ -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
}
@@ -367,6 +389,7 @@ class MentionAutocompleteQuery extends AutocompleteQuery {
367
389
368
390
class AutocompleteDataCache {
369
391
final Map <int , List <String >> _nameWordsByUser = {};
392
+ final Map <String , List <String >> _wordsOfTopics = {};
370
393
371
394
List <String > nameWordsForUser (User user) {
372
395
return _nameWordsByUser[user.userId] ?? = user.fullName.toLowerCase ().split (' ' );
@@ -375,6 +398,14 @@ class AutocompleteDataCache {
375
398
void invalidateUser (int userId) {
376
399
_nameWordsByUser.remove (userId);
377
400
}
401
+
402
+ List <String > wordsOfTopic (Topic topic) {
403
+ return _wordsOfTopics[topic.value] ?? = topic.value.toLowerCase ().split (' ' );
404
+ }
405
+
406
+ void invalidateTopic (String value) {
407
+ _wordsOfTopics.remove (value);
408
+ }
378
409
}
379
410
380
411
class AutocompleteResult {}
@@ -390,3 +421,76 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
390
421
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
391
422
392
423
// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
424
+
425
+ class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult , Topic > {
426
+ final int streamId;
427
+ Iterable <Topic > _topics = [];
428
+ bool _isFetching = false ;
429
+
430
+ TopicAutocompleteView .init ({
431
+ required super .store,
432
+ required this .streamId,
433
+ }) {
434
+ store.autocompleteViewManager.registerTopicAutocomplete (this );
435
+ fetch ();
436
+ }
437
+
438
+ /// Fetches topics of the current stream narrow, expected to fetch
439
+ /// only once per lifecycle.
440
+ ///
441
+ /// Starts fetching once the stream narrow is active, then when results
442
+ /// are fetched it notifies `autocompleteViewManager` to refresh UI
443
+ /// showing the newly fetched topics.
444
+ Future <void > fetch () async {
445
+ if (_isFetching) return ;
446
+ _isFetching = true ;
447
+ final result = await getStreamTopics (store.connection, streamId: streamId);
448
+ _topics = result.topics;
449
+ _isFetching = false ;
450
+ if (_query != null ) _startSearch (_query! );
451
+ }
452
+
453
+ @override
454
+ Iterable <Topic > getSortedItemsToTest (TopicAutocompleteQuery query) => _topics;
455
+
456
+ @override
457
+ TopicAutocompleteResult ? testItem (TopicAutocompleteQuery query, Topic item) {
458
+ if (query.testTopic (item, store.autocompleteViewManager.autocompleteDataCache)) {
459
+ return TopicAutocompleteResult (topic: item);
460
+ }
461
+ return null ;
462
+ }
463
+
464
+ @override
465
+ void dispose () {
466
+ store.autocompleteViewManager.unregisterTopicAutocomplete (this );
467
+ super .dispose ();
468
+ }
469
+ }
470
+
471
+ class TopicAutocompleteQuery extends AutocompleteQuery {
472
+ TopicAutocompleteQuery (super .raw);
473
+
474
+ bool testTopic (Topic topic, AutocompleteDataCache cache) {
475
+ return _testContainsQueryWords (cache.wordsOfTopic (topic));
476
+ }
477
+
478
+ @override
479
+ String toString () {
480
+ return '${objectRuntimeType (this , 'TopicAutocompleteQuery' )}(raw: $raw )' ;
481
+ }
482
+
483
+ @override
484
+ bool operator == (Object other) {
485
+ return other is TopicAutocompleteQuery && other.raw == raw;
486
+ }
487
+
488
+ @override
489
+ int get hashCode => Object .hash ('TopicAutocompleteQuery' , raw);
490
+ }
491
+
492
+ class TopicAutocompleteResult extends AutocompleteResult {
493
+ final Topic topic;
494
+
495
+ TopicAutocompleteResult ({required this .topic});
496
+ }
0 commit comments