Skip to content

Commit 27b3005

Browse files
committed
wip emojiCandidatesMatching; TODO maybe use autocomplete machinery; TODO test
1 parent 50d6604 commit 27b3005

File tree

2 files changed

+148
-1
lines changed

2 files changed

+148
-1
lines changed

lib/model/emoji.dart

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:collection/collection.dart';
2+
13
import '../api/model/events.dart';
24
import '../api/model/initial_snapshot.dart';
35
import '../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.
5590
mixin 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) {

lib/model/store.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
352352
notifyListeners();
353353
}
354354

355+
@override
356+
Iterable<EmojiCandidate> emojiCandidatesMatching(String query) =>
357+
_emoji.emojiCandidatesMatching(query);
355358

356359
EmojiStoreImpl _emoji;
357360

0 commit comments

Comments
 (0)