@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
33
44import  '../api/model/events.dart' ;
55import  '../api/model/model.dart' ;
6+ import  '../api/route/channels.dart' ;
67import  '../widgets/compose_box.dart' ;
78import  'narrow.dart' ;
89import  'store.dart' ;
@@ -43,6 +44,15 @@ extension ComposeContentAutocomplete on ComposeContentController {
4344  }
4445}
4546
47+ extension  ComposeTopicAutocomplete  on  ComposeTopicController  {
48+   AutocompleteIntent <TopicAutocompleteQuery >?  autocompleteIntent () {
49+     return  AutocompleteIntent (
50+       syntaxStart:  0 ,
51+       query:  TopicAutocompleteQuery (value.text),
52+       textEditingValue:  value);
53+   }
54+ }
55+ 
4656final  RegExp  mentionAutocompleteMarkerRegex =  (() {
4757  // What's likely to come before an @-mention: the start of the string, 
4858  // whitespace, or punctuation. Letters are unlikely; in that case an email 
@@ -112,6 +122,7 @@ class AutocompleteIntent<QueryT extends AutocompleteQuery> {
112122/// On reassemble, call [reassemble] . 
113123class  AutocompleteViewManager  {
114124  final  Set <MentionAutocompleteView > _mentionAutocompleteViews =  {};
125+   final  Set <TopicAutocompleteView > _topicAutocompleteViews =  {};
115126
116127  AutocompleteDataCache  autocompleteDataCache =  AutocompleteDataCache ();
117128
@@ -125,6 +136,16 @@ class AutocompleteViewManager {
125136    assert (removed);
126137  }
127138
139+   void  registerTopicAutocomplete (TopicAutocompleteView  view) {
140+     final  added =  _topicAutocompleteViews.add (view);
141+     assert (added);
142+   }
143+ 
144+   void  unregisterTopicAutocomplete (TopicAutocompleteView  view) {
145+     final  removed =  _topicAutocompleteViews.remove (view);
146+     assert (removed);
147+   }
148+ 
128149  void  handleRealmUserRemoveEvent (RealmUserRemoveEvent  event) {
129150    autocompleteDataCache.invalidateUser (event.userId);
130151  }
@@ -135,12 +156,15 @@ class AutocompleteViewManager {
135156
136157  /// Called when the app is reassembled during debugging, e.g. for hot reload. 
137158  /// 
138-   /// Calls [MentionAutocompleteView .reassemble]  for all that are registered. 
159+   /// Calls [AutocompleteView .reassemble]  for all that are registered. 
139160  /// 
140161   void  reassemble () {
141162    for  (final  view in  _mentionAutocompleteViews) {
142163      view.reassemble ();
143164    }
165+     for  (final  view in  _topicAutocompleteViews) {
166+       view.reassemble ();
167+     }
144168  }
145169
146170  // No `dispose` method, because there's nothing for it to do. 
@@ -531,3 +555,78 @@ class UserMentionAutocompleteResult extends MentionAutocompleteResult {
531555// TODO(#233): // class UserGroupMentionAutocompleteResult extends MentionAutocompleteResult { 
532556
533557// TODO(#234): // class WildcardMentionAutocompleteResult extends MentionAutocompleteResult { 
558+ 
559+ class  TopicAutocompleteView  extends  AutocompleteView <TopicAutocompleteQuery , TopicAutocompleteResult , String > {
560+   TopicAutocompleteView ._({required  super .store, required  this .streamId});
561+ 
562+   factory  TopicAutocompleteView .init ({required  PerAccountStore  store, required  int  streamId}) {
563+     final  view =  TopicAutocompleteView ._(store:  store, streamId:  streamId);
564+     store.autocompleteViewManager.registerTopicAutocomplete (view);
565+     view._fetch ();
566+     return  view;
567+   }
568+ 
569+   final  int  streamId;
570+   Iterable <String > _topics =  [];
571+   bool  _isFetching =  false ;
572+ 
573+   /// Fetches topics of the current stream narrow, expected to fetch 
574+   /// only once per lifecycle. 
575+   /// 
576+   /// Starts fetching once the stream narrow is active, then when results 
577+   /// are fetched it restarts search to refresh UI showing the newly 
578+   /// fetched topics. 
579+    Future <void > _fetch () async  {
580+      assert (! _isFetching);
581+     _isFetching =  true ;
582+     final  result =  await  getStreamTopics (store.connection, streamId:  streamId);
583+     _topics =  result.topics.map ((e) =>  e.name);
584+     _isFetching =  false ;
585+     if  (_query !=  null ) _startSearch (_query! );
586+   }
587+ 
588+   @override 
589+   Iterable <String > getSortedItemsToTest (TopicAutocompleteQuery  query) =>  _topics;
590+ 
591+   @override 
592+   TopicAutocompleteResult ?  testItem (TopicAutocompleteQuery  query, String  item) {
593+     if  (query.testTopic (item)) {
594+       return  TopicAutocompleteResult (topic:  item);
595+     }
596+     return  null ;
597+   }
598+ 
599+   @override 
600+   void  dispose () {
601+     store.autocompleteViewManager.unregisterTopicAutocomplete (this );
602+     super .dispose ();
603+   }
604+ }
605+ 
606+ class  TopicAutocompleteQuery  extends  AutocompleteQuery  {
607+   TopicAutocompleteQuery (super .raw);
608+ 
609+   bool  testTopic (String  topic) {
610+     // TODO(#881): Sort by match relevance, like web does. 
611+     return  topic !=  raw &&  topic.toLowerCase ().contains (raw.toLowerCase ());
612+   }
613+ 
614+   @override 
615+   String  toString () {
616+     return  '${objectRuntimeType (this , 'TopicAutocompleteQuery' )}(raw: $raw )' ;
617+   }
618+ 
619+   @override 
620+   bool  operator  == (Object  other) {
621+     return  other is  TopicAutocompleteQuery  &&  other.raw ==  raw;
622+   }
623+ 
624+   @override 
625+   int  get  hashCode =>  Object .hash ('TopicAutocompleteQuery' , raw);
626+ }
627+ 
628+ class  TopicAutocompleteResult  extends  AutocompleteResult  {
629+   final  String  topic;
630+ 
631+   TopicAutocompleteResult ({required  this .topic});
632+ }
0 commit comments