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,6 +98,8 @@ mixin EmojiStore {
6398    required  String  emojiName,
6499  });
65100
101+   Iterable <EmojiCandidate > emojiCandidatesMatching (String  query);
102+ 
66103  void  setServerEmojiData (ServerEmojiData  data);
67104}
68105
@@ -75,7 +112,7 @@ class EmojiStoreImpl with EmojiStore {
75112  EmojiStoreImpl ({
76113    required  this .realmUrl,
77114    required  this .realmEmoji,
78-   }) :  _serverEmojiData =  {} ; // TODO(#974) maybe start from a hard-coded baseline 
115+   }) :  _serverEmojiData =  null ; // TODO(#974) maybe start from a hard-coded baseline 
79116
80117  /// The same as [PerAccountStore.realmUrl] . 
81118final  Uri  realmUrl;
@@ -134,14 +171,125 @@ class EmojiStoreImpl with EmojiStore {
134171    );
135172  }
136173
137-   // (Note this may be out of date; [UpdateMachine.fetchEmojiData] 
138-   // sets it only after the store has been created.) 
139-   // ignore: unused_field 
140-   Map <String , List <String >> _serverEmojiData;
174+   /// The server's list of Unicode emoji and names for them, 
175+   /// from [ServerEmojiData] . 
176+   /// 
177+   /// This is null until [UpdateMachine.fetchEmojiData]  finishes 
178+   /// retrieving the data. 
179+ Map <String , List <String >>?  _serverEmojiData;
180+ 
181+   List <EmojiCandidate >?  _allEmojiCandidates;
182+ 
183+   EmojiCandidate  _emojiCandidateFor ({
184+     required  ReactionType  emojiType,
185+     required  String  emojiCode,
186+     required  String  emojiName,
187+     required  List <String >?  aliases,
188+   }) {
189+     return  EmojiCandidate (
190+       emojiType:  emojiType, emojiCode:  emojiCode, emojiName:  emojiName,
191+       aliases:  aliases,
192+       emojiDisplay:  emojiDisplayFor (
193+         emojiType:  emojiType, emojiCode:  emojiCode, emojiName:  emojiName));
194+   }
195+ 
196+   List <EmojiCandidate > _generateAllCandidates () {
197+     final  results =  < EmojiCandidate > [];
198+ 
199+     final  namesOverridden =  {
200+       for  (final  emoji in  realmEmoji.values) emoji.name,
201+       'zulip' ,
202+     };
203+     // TODO if _serverEmojiData missing, tell user data is missing 
204+     for  (final  entry in  (_serverEmojiData ??  {}).entries) {
205+       final  allNames =  entry.value;
206+       final  String  emojiName;
207+       final  List <String >?  aliases;
208+       if  (allNames.any (namesOverridden.contains)) {
209+         final  names =  allNames.whereNot (namesOverridden.contains).toList ();
210+         if  (names.isEmpty) continue ;
211+         emojiName =  names.removeAt (0 );
212+         aliases =  names;
213+       } else  {
214+         // Most emoji aren't overridden, so avoid copying the list. 
215+         emojiName =  allNames.first;
216+         aliases =  allNames.length >  1  ?  allNames.sublist (1 ) :  null ;
217+       }
218+       results.add (_emojiCandidateFor (
219+         emojiType:  ReactionType .unicodeEmoji,
220+         emojiCode:  entry.key, emojiName:  emojiName,
221+         aliases:  aliases));
222+     }
223+ 
224+     for  (final  entry in  realmEmoji.entries) {
225+       final  emojiName =  entry.value.name;
226+       if  (emojiName ==  'zulip' ) {
227+         // TODO does 'zulip' really override realm emoji? 
228+         //   (This is copied from zulip-mobile's behavior.) 
229+         continue ;
230+       }
231+       results.add (_emojiCandidateFor (
232+         emojiType:  ReactionType .realmEmoji,
233+         emojiCode:  entry.key, emojiName:  emojiName,
234+         aliases:  null ));
235+     }
236+ 
237+     results.add (_emojiCandidateFor (
238+       emojiType:  ReactionType .zulipExtraEmoji,
239+       emojiCode:  'zulip' , emojiName:  'zulip' ,
240+       aliases:  null ));
241+ 
242+     return  results;
243+   }
244+ 
245+   // Compare query_matches_string_in_order in Zulip web:shared/src/typeahead.ts . 
246+   bool  _nameMatches (String  emojiName, String  adjustedQuery) {
247+     // TODO this assumes emojiName is already lower-case (and no diacritics) 
248+     const  String  separator =  '_' ;
249+ 
250+     if  (! adjustedQuery.contains (separator)) {
251+       // If the query is a single token (doesn't contain a separator), 
252+       // the match can be anywhere in the string. 
253+       return  emojiName.contains (adjustedQuery);
254+     }
255+ 
256+     // If there is a separator in the query, then we 
257+     // require the match to start at the start of a token. 
258+     // (E.g. for 'ab_cd_ef', query could be 'ab_c' or 'cd_ef', 
259+     // but not 'b_cd_ef'.) 
260+     return  emojiName.startsWith (adjustedQuery)
261+       ||  emojiName.contains (separator +  adjustedQuery);
262+   }
263+ 
264+   // Compare get_emoji_matcher in Zulip web:shared/src/typeahead.ts . 
265+   bool  _candidateMatches (EmojiCandidate  candidate, String  adjustedQuery) {
266+     if  (candidate.emojiDisplay case  UnicodeEmojiDisplay (: var  emojiUnicode)) {
267+       if  (adjustedQuery ==  emojiUnicode) return  true ;
268+     }
269+     return  _nameMatches (candidate.emojiName, adjustedQuery)
270+       ||  candidate.aliases.any ((alias) =>  _nameMatches (alias, adjustedQuery));
271+   }
272+ 
273+   @override 
274+   Iterable <EmojiCandidate > emojiCandidatesMatching (String  query) {
275+     _allEmojiCandidates ?? =  _generateAllCandidates ();
276+     if  (query.isEmpty) {
277+       return  _allEmojiCandidates! ;
278+     }
279+ 
280+     final  adjustedQuery =  query.toLowerCase ().replaceAll (' ' , '_' ); // TODO remove diacritics too 
281+     final  results =  < EmojiCandidate > [];
282+     for  (final  candidate in  _allEmojiCandidates! ) {
283+       if  (! _candidateMatches (candidate, adjustedQuery)) continue ;
284+       results.add (candidate); // TODO reduce aliases to just matches 
285+     }
286+     return  results;
287+   }
141288
142289  @override 
143290  void  setServerEmojiData (ServerEmojiData  data) {
144291    _serverEmojiData =  data.codeToNames;
292+     _allEmojiCandidates =  null ;
145293  }
146294
147295  void  handleRealmEmojiUpdateEvent (RealmEmojiUpdateEvent  event) {
0 commit comments