Skip to content

Commit 79c33fe

Browse files
committed
wip emojiCandidatesMatching; TODO maybe use autocomplete machinery; TODO test
1 parent 4a2bd4a commit 79c33fe

File tree

2 files changed

+156
-5
lines changed

2 files changed

+156
-5
lines changed

lib/model/emoji.dart

Lines changed: 153 additions & 5 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
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].
81118
final 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) {

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)