@@ -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
}
@@ -133,6 +155,12 @@ class AutocompleteViewManager {
133
155
autocompleteDataCache.invalidateUser (event.userId);
134
156
}
135
157
158
+ void handleTopicsFetchCompleted () {
159
+ for (final view in _topicAutocompleteViews) {
160
+ view.reassemble ();
161
+ }
162
+ }
163
+
136
164
/// Called when the app is reassembled during debugging, e.g. for hot reload.
137
165
///
138
166
/// Calls [MentionAutocompleteView.reassemble] for all that are registered.
@@ -370,6 +398,7 @@ class MentionAutocompleteQuery extends AutocompleteQuery {
370
398
371
399
class AutocompleteDataCache {
372
400
final Map <int , List <String >> _nameWordsByUser = {};
401
+ final Map <String , List <String >> _nameWordsByTopic = {};
373
402
374
403
List <String > nameWordsForUser (User user) {
375
404
return _nameWordsByUser[user.userId] ?? = user.fullName.toLowerCase ().split (' ' );
@@ -378,6 +407,14 @@ class AutocompleteDataCache {
378
407
void invalidateUser (int userId) {
379
408
_nameWordsByUser.remove (userId);
380
409
}
410
+
411
+ List <String > nameWordsForTopic (Topic topic) {
412
+ return _nameWordsByTopic[topic.value] ?? = topic.value.toLowerCase ().split (' ' );
413
+ }
414
+
415
+ void invalidateTopic (String value) {
416
+ _nameWordsByTopic.remove (value);
417
+ }
381
418
}
382
419
383
420
class AutocompleteResult {}
@@ -393,3 +430,80 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
393
430
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
394
431
395
432
// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
433
+
434
+ class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult > {
435
+ final int streamId;
436
+ Iterable <Topic > _topics = [];
437
+ bool _isFetching = false ;
438
+
439
+ TopicAutocompleteView .init ({
440
+ required super .store,
441
+ required this .streamId,
442
+ }) {
443
+ store.autocompleteViewManager.registerTopicAutocomplete (this );
444
+ fetch ();
445
+ }
446
+
447
+ /// Fetches topics of the current stream narrow, expected to fetch
448
+ /// only once per lifecycle.
449
+ ///
450
+ /// Starts fetching once the stream narrow is active, then when results
451
+ /// are fetched it notifies `autocompleteViewManager` to refresh UI
452
+ /// showing the newly fetched topics.
453
+ Future <void > fetch () async {
454
+ if (_isFetching) return ;
455
+ _isFetching = true ;
456
+ final result = await getStreamTopics (store.connection, streamId: streamId);
457
+ _topics = result.topics;
458
+ store.autocompleteViewManager.handleTopicsFetchCompleted ();
459
+ _isFetching = false ;
460
+ }
461
+
462
+ @override
463
+ Iterable <Topic > getDataForQuery (TopicAutocompleteQuery query) {
464
+ return _topics;
465
+ }
466
+
467
+ @override
468
+ TopicAutocompleteResult ? testItem (TopicAutocompleteQuery query, Object item) {
469
+ if (item is Topic ) {
470
+ if (query.testTopic (item, store.autocompleteViewManager.autocompleteDataCache)) {
471
+ return TopicAutocompleteResult (topic: item);
472
+ }
473
+ }
474
+ return null ;
475
+ }
476
+
477
+ @override
478
+ void dispose () {
479
+ store.autocompleteViewManager.unregisterTopicAutocomplete (this );
480
+ super .dispose ();
481
+ }
482
+ }
483
+
484
+ class TopicAutocompleteQuery extends AutocompleteQuery {
485
+ TopicAutocompleteQuery (super .raw);
486
+
487
+ bool testTopic (Topic topic, AutocompleteDataCache cache) {
488
+ return _testContainsQueryWords (cache.nameWordsForTopic (topic));
489
+ }
490
+
491
+ @override
492
+ String toString () {
493
+ return '${objectRuntimeType (this , 'TopicAutocompleteQuery' )}(raw: $raw )' ;
494
+ }
495
+
496
+ @override
497
+ bool operator == (Object other) {
498
+ return other is TopicAutocompleteQuery && other.raw == raw;
499
+ }
500
+
501
+ @override
502
+ int get hashCode => Object .hash ('TopicAutocompleteQuery' , raw);
503
+ }
504
+
505
+ class TopicAutocompleteResult extends AutocompleteResult {
506
+ final Topic topic;
507
+
508
+ TopicAutocompleteResult ({required this .topic});
509
+ }
0 commit comments