@@ -3,7 +3,9 @@ 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' ;
8
+ import 'narrow.dart' ;
7
9
import 'store.dart' ;
8
10
9
11
extension ComposeContentAutocomplete on ComposeContentController {
@@ -42,6 +44,16 @@ extension ComposeContentAutocomplete on ComposeContentController {
42
44
}
43
45
}
44
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
+
45
57
final RegExp mentionAutocompleteMarkerRegex = (() {
46
58
// What's likely to come before an @-mention: the start of the string,
47
59
// whitespace, or punctuation. Letters are unlikely; in that case an email
@@ -158,6 +170,12 @@ class AutocompleteViewManager {
158
170
autocompleteDataCache.invalidateUser (event.userId);
159
171
}
160
172
173
+ void handleTopicsFetchCompleted () {
174
+ for (final view in getTypedViews <TopicAutocompleteView >()) {
175
+ view.reassemble ();
176
+ }
177
+ }
178
+
161
179
/// Called when the app is reassembled during debugging, e.g. for hot reload.
162
180
///
163
181
/// Calls [AutocompleteView.reassemble] for all that are registered.
@@ -393,6 +411,7 @@ class MentionAutocompleteQuery extends AutocompleteQuery {
393
411
394
412
class AutocompleteDataCache {
395
413
final Map <int , List <String >> _nameWordsByUser = {};
414
+ final Map <int , List <String >> _nameWordsByTopic = {};
396
415
397
416
List <String > nameWordsForUser (User user) {
398
417
return _nameWordsByUser[user.userId] ?? = user.fullName.toLowerCase ().split (' ' );
@@ -401,6 +420,14 @@ class AutocompleteDataCache {
401
420
void invalidateUser (int userId) {
402
421
_nameWordsByUser.remove (userId);
403
422
}
423
+
424
+ List <String > nameWordsForTopic (Topic topic) {
425
+ return _nameWordsByTopic[topic.maxId] ?? = topic.name.toLowerCase ().split (' ' );
426
+ }
427
+
428
+ void invalidateTopic (int topicMaxId) {
429
+ _nameWordsByTopic.remove (topicMaxId);
430
+ }
404
431
}
405
432
406
433
class AutocompleteResult {}
@@ -416,3 +443,102 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
416
443
// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult {
417
444
418
445
// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult {
446
+
447
+ class TopicAutocompleteDataProvider extends AutocompleteDataProvider <Topic , TopicAutocompleteQuery , TopicAutocompleteResult > {
448
+ final PerAccountStore store;
449
+ final StreamNarrow narrow;
450
+
451
+ TopicAutocompleteDataProvider ({required this .store, required this .topics, required this .narrow});
452
+
453
+ Iterable <Topic >? topics;
454
+
455
+ Future <void > fetch () async {
456
+ if (topics == null ) {
457
+ final result = await getStreamTopics (store.connection, streamId: narrow.streamId);
458
+ topics = result.topics;
459
+ store.autocompleteViewManager.handleTopicsFetchCompleted ();
460
+ }
461
+ }
462
+
463
+ @override
464
+ Iterable <Topic > getDataForQuery (TopicAutocompleteQuery query) {
465
+ return topics ?? [];
466
+ }
467
+
468
+ @override
469
+ TopicAutocompleteResult ? testItem (TopicAutocompleteQuery query, Topic item) {
470
+ if (query.testTopic (item, store.autocompleteViewManager.autocompleteDataCache)) {
471
+ return TopicAutocompleteResult (topic: item);
472
+ }
473
+ return null ;
474
+ }
475
+ }
476
+
477
+ class TopicAutocompleteView extends AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult > {
478
+ TopicAutocompleteView ._({
479
+ required Iterable <Topic >? topics,
480
+ required super .store,
481
+ required StreamNarrow narrow,
482
+ }) : super (dataProvider: TopicAutocompleteDataProvider (
483
+ store: store,
484
+ topics: topics,
485
+ narrow: narrow
486
+ )..fetch ());
487
+
488
+ TopicAutocompleteView .init ({
489
+ required PerAccountStore store,
490
+ required StreamNarrow narrow,
491
+ }) : this ._(topics: [], store: store, narrow: narrow);
492
+ }
493
+
494
+ class TopicAutocompleteQuery extends AutocompleteQuery {
495
+ TopicAutocompleteQuery (super .raw)
496
+ : _lowercaseWords = raw.toLowerCase ().split (' ' );
497
+
498
+ final List <String > _lowercaseWords;
499
+
500
+ bool testTopic (Topic topic, AutocompleteDataCache cache) {
501
+ return _testName (topic, cache);
502
+ }
503
+
504
+ bool _testName (Topic topic, AutocompleteDataCache cache) {
505
+ // TODO(#237) test with diacritics stripped, where appropriate
506
+
507
+ final List <String > nameWords = cache.nameWordsForTopic (topic);
508
+
509
+ int nameWordsIndex = 0 ;
510
+ int queryWordsIndex = 0 ;
511
+ while (true ) {
512
+ if (queryWordsIndex == _lowercaseWords.length) {
513
+ return true ;
514
+ }
515
+ if (nameWordsIndex == nameWords.length) {
516
+ return false ;
517
+ }
518
+
519
+ if (nameWords[nameWordsIndex].startsWith (_lowercaseWords[queryWordsIndex])) {
520
+ queryWordsIndex++ ;
521
+ }
522
+ nameWordsIndex++ ;
523
+ }
524
+ }
525
+
526
+ @override
527
+ String toString () {
528
+ return '${objectRuntimeType (this , 'TopicAutocompleteQuery' )}(raw: $raw )' ;
529
+ }
530
+
531
+ @override
532
+ bool operator == (Object other) {
533
+ return other is TopicAutocompleteQuery && other.raw == raw;
534
+ }
535
+
536
+ @override
537
+ int get hashCode => Object .hash ('TopicAutocompleteQuery' , raw);
538
+ }
539
+
540
+ class TopicAutocompleteResult extends AutocompleteResult {
541
+ final Topic topic;
542
+
543
+ TopicAutocompleteResult ({required this .topic});
544
+ }
0 commit comments