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
@@ -136,12 +173,119 @@ class EmojiStoreImpl with EmojiStore {
136173
137174  // (Note this may be out of date; [UpdateMachine.fetchEmojiData] 
138175  // sets it only after the store has been created.) 
139-   // ignore: unused_field 
140176  Map <String , List <String >> _serverEmojiData;
141177
178+   List <EmojiCandidate >?  _allEmojiCandidates;
179+ 
180+   EmojiCandidate  _emojiCandidateFor ({
181+     required  ReactionType  emojiType,
182+     required  String  emojiCode,
183+     required  String  emojiName,
184+     required  List <String >?  aliases,
185+   }) {
186+     return  EmojiCandidate (
187+       emojiType:  emojiType, emojiCode:  emojiCode, emojiName:  emojiName,
188+       aliases:  aliases,
189+       emojiDisplay:  emojiDisplayFor (
190+         emojiType:  emojiType, emojiCode:  emojiCode, emojiName:  emojiName));
191+   }
192+ 
193+   List <EmojiCandidate > _generateAllCandidates () {
194+     final  results =  < EmojiCandidate > [];
195+ 
196+     final  namesOverridden =  {
197+       for  (final  emoji in  realmEmoji.values) emoji.name,
198+       'zulip' ,
199+     };
200+     for  (final  entry in  _serverEmojiData.entries) {
201+       final  allNames =  entry.value;
202+       final  String  emojiName;
203+       final  List <String >?  aliases;
204+       if  (allNames.any (namesOverridden.contains)) {
205+         final  names =  allNames.whereNot (namesOverridden.contains).toList ();
206+         if  (names.isEmpty) continue ;
207+         emojiName =  names.removeAt (0 );
208+         aliases =  names;
209+       } else  {
210+         // Most emoji aren't overridden, so avoid copying the list. 
211+         emojiName =  allNames.first;
212+         aliases =  allNames.length >  1  ?  allNames.sublist (1 ) :  null ;
213+       }
214+       results.add (_emojiCandidateFor (
215+         emojiType:  ReactionType .unicodeEmoji,
216+         emojiCode:  entry.key, emojiName:  emojiName,
217+         aliases:  aliases));
218+     }
219+ 
220+     for  (final  entry in  realmEmoji.entries) {
221+       final  emojiName =  entry.value.name;
222+       if  (emojiName ==  'zulip' ) {
223+         // TODO does 'zulip' really override realm emoji? 
224+         //   (This is copied from zulip-mobile's behavior.) 
225+         continue ;
226+       }
227+       results.add (_emojiCandidateFor (
228+         emojiType:  ReactionType .realmEmoji,
229+         emojiCode:  entry.key, emojiName:  emojiName,
230+         aliases:  null ));
231+     }
232+ 
233+     results.add (_emojiCandidateFor (
234+       emojiType:  ReactionType .zulipExtraEmoji,
235+       emojiCode:  'zulip' , emojiName:  'zulip' ,
236+       aliases:  null ));
237+ 
238+     return  results;
239+   }
240+ 
241+   // Compare query_matches_string_in_order in Zulip web:shared/src/typeahead.ts . 
242+   bool  _nameMatches (String  emojiName, String  adjustedQuery) {
243+     // TODO this assumes emojiName is already lower-case (and no diacritics) 
244+     const  String  separator =  '_' ;
245+ 
246+     if  (! adjustedQuery.contains (separator)) {
247+       // If the query is a single token (doesn't contain a separator), 
248+       // the match can be anywhere in the string. 
249+       return  emojiName.contains (adjustedQuery);
250+     }
251+ 
252+     // If there is a separator in the query, then we 
253+     // require the match to start at the start of a token. 
254+     // (E.g. for 'ab_cd_ef', query could be 'ab_c' or 'cd_ef', 
255+     // but not 'b_cd_ef'.) 
256+     return  emojiName.startsWith (adjustedQuery)
257+       ||  emojiName.contains (separator +  adjustedQuery);
258+   }
259+ 
260+   // Compare get_emoji_matcher in Zulip web:shared/src/typeahead.ts . 
261+   bool  _candidateMatches (EmojiCandidate  candidate, String  adjustedQuery) {
262+     if  (candidate.emojiDisplay case  UnicodeEmojiDisplay (: var  emojiUnicode)) {
263+       if  (adjustedQuery ==  emojiUnicode) return  true ;
264+     }
265+     return  _nameMatches (candidate.emojiName, adjustedQuery)
266+       ||  candidate.aliases.any ((alias) =>  _nameMatches (alias, adjustedQuery));
267+   }
268+ 
269+   @override 
270+   Iterable <EmojiCandidate > emojiCandidatesMatching (String  query) {
271+     _allEmojiCandidates ?? =  _generateAllCandidates ();
272+     if  (query.isEmpty) {
273+       return  _allEmojiCandidates! ;
274+     }
275+ 
276+     final  adjustedQuery =  query.toLowerCase ().replaceAll (' ' , '_' ); // TODO remove diacritics too 
277+     final  results =  < EmojiCandidate > [];
278+     for  (final  candidate in  _allEmojiCandidates! ) {
279+       if  (! _candidateMatches (candidate, adjustedQuery)) continue ;
280+       results.add (candidate); // TODO reduce aliases to just matches 
281+     }
282+     return  results;
283+   }
284+ 
142285  @override 
143286  void  setServerEmojiData (ServerEmojiData  data) {
144287    _serverEmojiData =  data.codeToNames;
288+     _allEmojiCandidates =  null ;
145289  }
146290
147291  void  handleRealmEmojiUpdateEvent (RealmEmojiUpdateEvent  event) {
0 commit comments