1+ import  'package:collection/collection.dart' ;
2+ 
13import  '../api/model/events.dart' ;
24import  '../api/model/initial_snapshot.dart' ;
35import  '../api/model/model.dart' ;
@@ -51,6 +53,39 @@ class TextEmojiDisplay extends EmojiDisplay {
5153  TextEmojiDisplay ({required  super .emojiName});
5254}
5355
56+ /// An emoji that might be offered in an emoji picker UI. 
57+ final  class  EmojiCandidate  {
58+   /// The Zulip "emoji type" for this emoji. 
59+ final  ReactionType  emojiType;
60+ 
61+   /// The Zulip "emoji code" for this emoji. 
62+   /// 
63+   /// This is the value that would appear in [Reaction.emojiCode] . 
64+ final  String  emojiCode;
65+ 
66+   /// The Zulip "emoji name" to use for this emoji. 
67+   /// 
68+   /// This might not be the only name this emoji has; see [aliases] . 
69+ final  String  emojiName;
70+ 
71+   /// Additional Zulip "emoji name" values for this emoji, 
72+   /// to show in the emoji picker UI. 
73+ List <String > get  aliases =>  _aliases ??  const  [];
74+   List <String >?  _aliases;
75+ 
76+   void  addAlias (String  alias) =>  (_aliases ?? =  []).add (alias);
77+ 
78+   final  EmojiDisplay  emojiDisplay;
79+ 
80+   EmojiCandidate ({
81+     required  this .emojiType,
82+     required  this .emojiCode,
83+     required  this .emojiName,
84+     required  List <String >?  aliases,
85+     required  this .emojiDisplay,
86+   }) :  _aliases =  aliases;
87+ }
88+ 
5489/// The portion of [PerAccountStore]  describing what emoji exist. 
5590mixin  EmojiStore  {
5691  /// The realm's custom emoji (for [ReactionType.realmEmoji] , 
@@ -63,9 +98,11 @@ mixin EmojiStore {
6398    required  String  emojiName,
6499  });
65100
101+   Iterable <EmojiCandidate > emojiCandidatesMatching (String  query);
102+ 
66103  // TODO cut debugServerEmojiData once we can query for lists of emoji; 
67104  //   have tests make those queries end-to-end 
68-   Map <String , List <String >> get  debugServerEmojiData;
105+   Map <String , List <String >>?  get  debugServerEmojiData;
69106
70107  void  setServerEmojiData (ServerEmojiData  data);
71108}
@@ -79,7 +116,7 @@ class EmojiStoreImpl with EmojiStore {
79116  EmojiStoreImpl ({
80117    required  this .realmUrl,
81118    required  this .realmEmoji,
82-   }) :  _serverEmojiData =  {} ; // TODO(#974) maybe start from a hard-coded baseline 
119+   }) :  _serverEmojiData =  null ; // TODO(#974) maybe start from a hard-coded baseline 
83120
84121  /// The same as [PerAccountStore.realmUrl] . 
85122final  Uri  realmUrl;
@@ -139,16 +176,127 @@ class EmojiStoreImpl with EmojiStore {
139176  }
140177
141178  @override 
142-   Map <String , List <String >> get  debugServerEmojiData =>  _serverEmojiData;
179+   Map <String , List <String >>?  get  debugServerEmojiData =>  _serverEmojiData;
180+ 
181+   /// The server's list of Unicode emoji and names for them, 
182+   /// from [ServerEmojiData] . 
183+   /// 
184+   /// This is null until [UpdateMachine.fetchEmojiData]  finishes 
185+   /// retrieving the data. 
186+ Map <String , List <String >>?  _serverEmojiData;
187+ 
188+   List <EmojiCandidate >?  _allEmojiCandidates;
189+ 
190+   EmojiCandidate  _emojiCandidateFor ({
191+     required  ReactionType  emojiType,
192+     required  String  emojiCode,
193+     required  String  emojiName,
194+     required  List <String >?  aliases,
195+   }) {
196+     return  EmojiCandidate (
197+       emojiType:  emojiType, emojiCode:  emojiCode, emojiName:  emojiName,
198+       aliases:  aliases,
199+       emojiDisplay:  emojiDisplayFor (
200+         emojiType:  emojiType, emojiCode:  emojiCode, emojiName:  emojiName));
201+   }
202+ 
203+   List <EmojiCandidate > _generateAllCandidates () {
204+     final  results =  < EmojiCandidate > [];
205+ 
206+     final  namesOverridden =  {
207+       for  (final  emoji in  realmEmoji.values) emoji.name,
208+       'zulip' ,
209+     };
210+     // TODO if _serverEmojiData missing, tell user data is missing 
211+     for  (final  entry in  (_serverEmojiData ??  {}).entries) {
212+       final  allNames =  entry.value;
213+       final  String  emojiName;
214+       final  List <String >?  aliases;
215+       if  (allNames.any (namesOverridden.contains)) {
216+         final  names =  allNames.whereNot (namesOverridden.contains).toList ();
217+         if  (names.isEmpty) continue ;
218+         emojiName =  names.removeAt (0 );
219+         aliases =  names;
220+       } else  {
221+         // Most emoji aren't overridden, so avoid copying the list. 
222+         emojiName =  allNames.first;
223+         aliases =  allNames.length >  1  ?  allNames.sublist (1 ) :  null ;
224+       }
225+       results.add (_emojiCandidateFor (
226+         emojiType:  ReactionType .unicodeEmoji,
227+         emojiCode:  entry.key, emojiName:  emojiName,
228+         aliases:  aliases));
229+     }
230+ 
231+     for  (final  entry in  realmEmoji.entries) {
232+       final  emojiName =  entry.value.name;
233+       if  (emojiName ==  'zulip' ) {
234+         // TODO does 'zulip' really override realm emoji? 
235+         //   (This is copied from zulip-mobile's behavior.) 
236+         continue ;
237+       }
238+       results.add (_emojiCandidateFor (
239+         emojiType:  ReactionType .realmEmoji,
240+         emojiCode:  entry.key, emojiName:  emojiName,
241+         aliases:  null ));
242+     }
243+ 
244+     results.add (_emojiCandidateFor (
245+       emojiType:  ReactionType .zulipExtraEmoji,
246+       emojiCode:  'zulip' , emojiName:  'zulip' ,
247+       aliases:  null ));
248+ 
249+     return  results;
250+   }
251+ 
252+   // Compare query_matches_string_in_order in Zulip web:shared/src/typeahead.ts . 
253+   bool  _nameMatches (String  emojiName, String  adjustedQuery) {
254+     // TODO this assumes emojiName is already lower-case (and no diacritics) 
255+     const  String  separator =  '_' ;
256+ 
257+     if  (! adjustedQuery.contains (separator)) {
258+       // If the query is a single token (doesn't contain a separator), 
259+       // the match can be anywhere in the string. 
260+       return  emojiName.contains (adjustedQuery);
261+     }
262+ 
263+     // If there is a separator in the query, then we 
264+     // require the match to start at the start of a token. 
265+     // (E.g. for 'ab_cd_ef', query could be 'ab_c' or 'cd_ef', 
266+     // but not 'b_cd_ef'.) 
267+     return  emojiName.startsWith (adjustedQuery)
268+       ||  emojiName.contains (separator +  adjustedQuery);
269+   }
270+ 
271+   // Compare get_emoji_matcher in Zulip web:shared/src/typeahead.ts . 
272+   bool  _candidateMatches (EmojiCandidate  candidate, String  adjustedQuery) {
273+     if  (candidate.emojiDisplay case  UnicodeEmojiDisplay (: var  emojiUnicode)) {
274+       if  (adjustedQuery ==  emojiUnicode) return  true ;
275+     }
276+     return  _nameMatches (candidate.emojiName, adjustedQuery)
277+       ||  candidate.aliases.any ((alias) =>  _nameMatches (alias, adjustedQuery));
278+   }
279+ 
280+   @override 
281+   Iterable <EmojiCandidate > emojiCandidatesMatching (String  query) {
282+     _allEmojiCandidates ?? =  _generateAllCandidates ();
283+     if  (query.isEmpty) {
284+       return  _allEmojiCandidates! ;
285+     }
143286
144-   // (Note this may be out of date; [UpdateMachine.fetchEmojiData] 
145-   // sets it only after the store has been created.) 
146-   // ignore: unused_field 
147-   Map <String , List <String >> _serverEmojiData;
287+     final  adjustedQuery =  query.toLowerCase ().replaceAll (' ' , '_' ); // TODO remove diacritics too 
288+     final  results =  < EmojiCandidate > [];
289+     for  (final  candidate in  _allEmojiCandidates! ) {
290+       if  (! _candidateMatches (candidate, adjustedQuery)) continue ;
291+       results.add (candidate); // TODO reduce aliases to just matches 
292+     }
293+     return  results;
294+   }
148295
149296  @override 
150297  void  setServerEmojiData (ServerEmojiData  data) {
151298    _serverEmojiData =  data.codeToNames;
299+     _allEmojiCandidates =  null ;
152300  }
153301
154302  void  handleRealmEmojiUpdateEvent (RealmEmojiUpdateEvent  event) {
0 commit comments