Skip to content

Commit 1eae683

Browse files
committed
wip emojiCandidatesMatching; TODO maybe use autocomplete machinery; TODO test
1 parent 304b69d commit 1eae683

File tree

2 files changed

+150
-0
lines changed

2 files changed

+150
-0
lines changed

lib/model/emoji.dart

Lines changed: 146 additions & 0 deletions
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
// TODO cut debugServerEmojiData once we can query for lists of emoji;
67104
// have tests make those queries end-to-end
68105
Map<String, List<String>>? get debugServerEmojiData;
@@ -148,9 +185,118 @@ class EmojiStoreImpl with EmojiStore {
148185
/// retrieving the data.
149186
Map<String, List<String>>? _serverEmojiData;
150187

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+
}
286+
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+
}
295+
151296
@override
152297
void setServerEmojiData(ServerEmojiData data) {
153298
_serverEmojiData = data.codeToNames;
299+
_allEmojiCandidates = null;
154300
}
155301

156302
void handleRealmEmojiUpdateEvent(RealmEmojiUpdateEvent event) {

lib/model/store.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
355355
notifyListeners();
356356
}
357357

358+
@override
359+
Iterable<EmojiCandidate> emojiCandidatesMatching(String query) =>
360+
_emoji.emojiCandidatesMatching(query);
361+
358362
EmojiStoreImpl _emoji;
359363

360364
////////////////////////////////

0 commit comments

Comments
 (0)