@@ -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
@@ -156,6 +167,12 @@ class AutocompleteViewManager {
156
167
autocompleteDataCache.invalidateUser (event.userId);
157
168
}
158
169
170
+ void handleTopicsFetchCompleted () {
171
+ for (final view in _getViewsOfType <TopicAutocompleteView >()) {
172
+ view.reassemble ();
173
+ }
174
+ }
175
+
159
176
/// Called when the app is reassembled during debugging, e.g. for hot reload.
160
177
///
161
178
/// Calls [AutocompleteView.reassemble] for all that are registered.
@@ -431,6 +448,7 @@ class MentionAutocompleteQuery extends AutocompleteQuery {
431
448
432
449
class AutocompleteDataCache {
433
450
final Map <int , List <String >> _nameWordsByUser = {};
451
+ final Map <int , List <String >> _nameWordsByTopic = {};
434
452
435
453
List <String > nameWordsForUser (User user) {
436
454
return _nameWordsByUser[user.userId] ?? = user.fullName.toLowerCase ().split (' ' );
@@ -439,6 +457,14 @@ class AutocompleteDataCache {
439
457
void invalidateUser (int userId) {
440
458
_nameWordsByUser.remove (userId);
441
459
}
460
+
461
+ List <String > nameWordsForTopic (Topic topic) {
462
+ return _nameWordsByTopic[topic.maxId] ?? = topic.name.toLowerCase ().split (' ' );
463
+ }
464
+
465
+ void invalidateTopic (int topicMaxId) {
466
+ _nameWordsByTopic.remove (topicMaxId);
467
+ }
442
468
}
443
469
444
470
class AutocompleteResult {}
@@ -454,3 +480,101 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
454
480
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
455
481
456
482
// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
483
+
484
+ class TopicAutocompleteDataProvider extends AutocompleteDataProvider <Topic , TopicAutocompleteQuery , TopicAutocompleteResult > {
485
+ final PerAccountStore store;
486
+ final StreamNarrow narrow;
487
+ Iterable <Topic >? topics;
488
+ bool isFetching = false ;
489
+
490
+ TopicAutocompleteDataProvider ({required this .store, required this .topics, required this .narrow});
491
+
492
+ /// Fetches topics of the current stream narrow, expected to fetch
493
+ /// only once per lifecycle.
494
+ ///
495
+ /// Starts fetching once the stream narrow is active, then when results
496
+ /// are fetched it notifies `autocompleteViewManager` to refresh UI
497
+ /// showing the newly fetched topics.
498
+ Future <void > fetch () async {
499
+ if (topics != null && ! isFetching) return ;
500
+ isFetching = true ;
501
+ final result = await getStreamTopics (store.connection, streamId: narrow.streamId);
502
+ topics = result.topics;
503
+ store.autocompleteViewManager.handleTopicsFetchCompleted ();
504
+ isFetching = false ;
505
+ }
506
+
507
+ @override
508
+ Iterable <Topic > getDataForQuery (TopicAutocompleteQuery query) {
509
+ return topics ?? [];
510
+ }
511
+
512
+ @override
513
+ TopicAutocompleteResult ? testItem (TopicAutocompleteQuery query, Topic item) {
514
+ if (query.testTopic (item, store.autocompleteViewManager.autocompleteDataCache)) {
515
+ return TopicAutocompleteResult (topic: item);
516
+ }
517
+ return null ;
518
+ }
519
+ }
520
+
521
+ class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult > {
522
+ TopicAutocompleteView .init ({
523
+ required super .store,
524
+ required StreamNarrow narrow,
525
+ }) : super (dataProvider: TopicAutocompleteDataProvider (
526
+ store: store,
527
+ topics: null ,
528
+ narrow: narrow
529
+ )..fetch ());
530
+ }
531
+
532
+ class TopicAutocompleteQuery extends AutocompleteQuery {
533
+ TopicAutocompleteQuery (super .raw)
534
+ : _lowercaseWords = raw.toLowerCase ().split (' ' );
535
+
536
+ final List <String > _lowercaseWords;
537
+
538
+ bool testTopic (Topic topic, AutocompleteDataCache cache) {
539
+ return _testName (topic, cache);
540
+ }
541
+
542
+ bool _testName (Topic topic, AutocompleteDataCache cache) {
543
+ final List <String > nameWords = cache.nameWordsForTopic (topic);
544
+
545
+ int nameWordsIndex = 0 ;
546
+ int queryWordsIndex = 0 ;
547
+ while (true ) {
548
+ if (queryWordsIndex == _lowercaseWords.length) {
549
+ return true ;
550
+ }
551
+ if (nameWordsIndex == nameWords.length) {
552
+ return false ;
553
+ }
554
+
555
+ if (nameWords[nameWordsIndex].startsWith (_lowercaseWords[queryWordsIndex])) {
556
+ queryWordsIndex++ ;
557
+ }
558
+ nameWordsIndex++ ;
559
+ }
560
+ }
561
+
562
+ @override
563
+ String toString () {
564
+ return '${objectRuntimeType (this , 'TopicAutocompleteQuery' )}(raw: $raw )' ;
565
+ }
566
+
567
+ @override
568
+ bool operator == (Object other) {
569
+ return other is TopicAutocompleteQuery && other.raw == raw;
570
+ }
571
+
572
+ @override
573
+ int get hashCode => Object .hash ('TopicAutocompleteQuery' , raw);
574
+ }
575
+
576
+ class TopicAutocompleteResult extends AutocompleteResult {
577
+ final Topic topic;
578
+
579
+ TopicAutocompleteResult ({required this .topic});
580
+ }
0 commit comments