Skip to content

Commit 31d7122

Browse files
committed
emoji: Add view-model for autocomplete, EmojiAutocompleteView
As of this commit, it's not yet possible in the app to initiate an emoji autocomplete interaction. So in the widgets code that would consume the results of such an interaction, we just throw for now, leaving that to be implemented in a later commit.
1 parent cc41c08 commit 31d7122

File tree

4 files changed

+123
-0
lines changed

4 files changed

+123
-0
lines changed

lib/model/autocomplete.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import '../api/model/events.dart';
77
import '../api/model/model.dart';
88
import '../api/route/channels.dart';
99
import '../widgets/compose_box.dart';
10+
import 'emoji.dart';
1011
import 'narrow.dart';
1112
import 'store.dart';
1213

@@ -142,6 +143,7 @@ class AutocompleteIntent<QueryT extends AutocompleteQuery> {
142143
class AutocompleteViewManager {
143144
final Set<MentionAutocompleteView> _mentionAutocompleteViews = {};
144145
final Set<TopicAutocompleteView> _topicAutocompleteViews = {};
146+
final Set<EmojiAutocompleteView> _emojiAutocompleteViews = {};
145147

146148
AutocompleteDataCache autocompleteDataCache = AutocompleteDataCache();
147149

@@ -165,6 +167,16 @@ class AutocompleteViewManager {
165167
assert(removed);
166168
}
167169

170+
void registerEmojiAutocomplete(EmojiAutocompleteView view) {
171+
final added = _emojiAutocompleteViews.add(view);
172+
assert(added);
173+
}
174+
175+
void unregisterEmojiAutocomplete(EmojiAutocompleteView view) {
176+
final removed = _emojiAutocompleteViews.remove(view);
177+
assert(removed);
178+
}
179+
168180
void handleRealmUserRemoveEvent(RealmUserRemoveEvent event) {
169181
autocompleteDataCache.invalidateUser(event.userId);
170182
}
@@ -683,6 +695,13 @@ class AutocompleteResult {}
683695
/// and managed by some [ComposeAutocompleteView].
684696
sealed class ComposeAutocompleteResult extends AutocompleteResult {}
685697

698+
/// An emoji chosen in an autocomplete interaction, via [EmojiAutocompleteView].
699+
class EmojiAutocompleteResult extends ComposeAutocompleteResult {
700+
EmojiAutocompleteResult(this.candidate);
701+
702+
final EmojiCandidate candidate;
703+
}
704+
686705
/// A result from an @-mention autocomplete interaction,
687706
/// managed by a [MentionAutocompleteView].
688707
///

lib/model/emoji.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,34 @@ class EmojiStoreImpl with EmojiStore {
268268
}
269269
}
270270

271+
class EmojiAutocompleteView extends AutocompleteView<EmojiAutocompleteQuery, EmojiAutocompleteResult> {
272+
EmojiAutocompleteView._({required super.store, required super.query});
273+
274+
factory EmojiAutocompleteView.init({
275+
required PerAccountStore store,
276+
required EmojiAutocompleteQuery query,
277+
}) {
278+
final view = EmojiAutocompleteView._(store: store, query: query);
279+
store.autocompleteViewManager.registerEmojiAutocomplete(view);
280+
return view;
281+
}
282+
283+
@override
284+
Future<List<EmojiAutocompleteResult>?> computeResults() async {
285+
// TODO(#1068): rank emoji results (popular, realm, other; exact match, prefix, other)
286+
final results = <EmojiAutocompleteResult>[];
287+
if (await filterCandidates(filter: _testCandidate,
288+
candidates: store.allEmojiCandidates(), results: results)) {
289+
return null;
290+
}
291+
return results;
292+
}
293+
294+
EmojiAutocompleteResult? _testCandidate(EmojiAutocompleteQuery query, EmojiCandidate candidate) {
295+
return query.matches(candidate) ? EmojiAutocompleteResult(candidate) : null;
296+
}
297+
}
298+
271299
class EmojiAutocompleteQuery extends ComposeAutocompleteQuery {
272300
EmojiAutocompleteQuery(super.raw)
273301
: _adjusted = _adjustQuery(raw);

lib/widgets/autocomplete.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ class ComposeAutocomplete extends AutocompleteField<ComposeAutocompleteQuery, Co
187187
final store = PerAccountStoreWidget.of(context);
188188
final String replacementString;
189189
switch (option) {
190+
case EmojiAutocompleteResult():
191+
throw UnimplementedError(); // TODO(#670)
190192
case UserMentionAutocompleteResult(:var userId):
191193
if (query is! MentionAutocompleteQuery) {
192194
return; // Shrug; similar to `intent == null` case above.
@@ -208,6 +210,7 @@ class ComposeAutocomplete extends AutocompleteField<ComposeAutocompleteQuery, Co
208210
Widget buildItem(BuildContext context, int index, ComposeAutocompleteResult option) {
209211
final child = switch (option) {
210212
MentionAutocompleteResult() => _MentionAutocompleteItem(option: option),
213+
EmojiAutocompleteResult() => throw UnimplementedError(), // TODO(#670)
211214
};
212215
return InkWell(
213216
onTap: () {

test/model/emoji_test.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:test/scaffolding.dart';
33
import 'package:zulip/api/model/events.dart';
44
import 'package:zulip/api/model/model.dart';
55
import 'package:zulip/api/route/realm.dart';
6+
import 'package:zulip/model/autocomplete.dart';
67
import 'package:zulip/model/emoji.dart';
78
import 'package:zulip/model/store.dart';
89

@@ -203,6 +204,74 @@ void main() {
203204
});
204205
});
205206

207+
group('EmojiAutocompleteView', () {
208+
Condition<Object?> isUnicodeResult({String? emojiCode, List<String>? names}) {
209+
return (it) => it.isA<EmojiAutocompleteResult>().candidate.which(
210+
isUnicodeCandidate(emojiCode, names));
211+
}
212+
213+
Condition<Object?> isRealmResult({String? emojiCode, String? emojiName}) {
214+
return (it) => it.isA<EmojiAutocompleteResult>().candidate.which(
215+
isRealmCandidate(emojiCode: emojiCode, emojiName: emojiName));
216+
}
217+
218+
Condition<Object?> isZulipResult() {
219+
return (it) => it.isA<EmojiAutocompleteResult>().candidate.which(
220+
isZulipCandidate());
221+
}
222+
223+
PerAccountStore prepare({
224+
Map<String, String> realmEmoji = const {},
225+
Map<String, List<String>>? unicodeEmoji,
226+
}) {
227+
final store = eg.store(
228+
initialSnapshot: eg.initialSnapshot(realmEmoji: {
229+
for (final MapEntry(:key, :value) in realmEmoji.entries)
230+
key: eg.realmEmojiItem(emojiCode: key, emojiName: value),
231+
}));
232+
if (unicodeEmoji != null) {
233+
store.setServerEmojiData(ServerEmojiData(codeToNames: unicodeEmoji));
234+
}
235+
return store;
236+
}
237+
238+
test('results can include all three emoji types', () async {
239+
final store = prepare(
240+
realmEmoji: {'1': 'happy'}, unicodeEmoji: {'1f642': ['smile']});
241+
final view = EmojiAutocompleteView.init(store: store,
242+
query: EmojiAutocompleteQuery(''));
243+
bool done = false;
244+
view.addListener(() { done = true; });
245+
await Future(() {});
246+
check(done).isTrue();
247+
check(view.results).deepEquals([
248+
isUnicodeResult(names: ['smile']),
249+
isRealmResult(emojiName: 'happy'),
250+
isZulipResult(),
251+
]);
252+
});
253+
254+
test('results update after query change', () async {
255+
final store = prepare(
256+
realmEmoji: {'1': 'happy'}, unicodeEmoji: {'1f642': ['smile']});
257+
final view = EmojiAutocompleteView.init(store: store,
258+
query: EmojiAutocompleteQuery('h'));
259+
bool done = false;
260+
view.addListener(() { done = true; });
261+
await Future(() {});
262+
check(done).isTrue();
263+
check(view.results).single.which(
264+
isRealmResult(emojiName: 'happy'));
265+
266+
done = false;
267+
view.query = EmojiAutocompleteQuery('s');
268+
await Future(() {});
269+
check(done).isTrue();
270+
check(view.results).single.which(
271+
isUnicodeResult(names: ['smile']));
272+
});
273+
});
274+
206275
group('EmojiAutocompleteQuery.matches', () {
207276
EmojiCandidate unicode(List<String> names, {String? emojiCode}) {
208277
emojiCode ??= '10ffff';
@@ -369,3 +438,7 @@ extension EmojiCandidateChecks on Subject<EmojiCandidate> {
369438
Subject<Iterable<String>> get aliases => has((x) => x.aliases, 'aliases');
370439
Subject<EmojiDisplay> get emojiDisplay => has((x) => x.emojiDisplay, 'emojiDisplay');
371440
}
441+
442+
extension EmojiAutocompleteResultChecks on Subject<EmojiAutocompleteResult> {
443+
Subject<EmojiCandidate> get candidate => has((x) => x.candidate, 'candidate');
444+
}

0 commit comments

Comments
 (0)