Skip to content

Commit f5d09c4

Browse files
committed
emoji: Fetch server emoji data (about Unicode emoji)
This completes the data we'll need for use in #669, which in turn will let us offer an emoji picker for reactions and for use inside message content. The doc on fetchEmojiData is based on a comment at the corresponding code in the legacy zulip-mobile app, in src/events/eventActions.js .
1 parent e963af5 commit f5d09c4

File tree

3 files changed

+163
-1
lines changed

3 files changed

+163
-1
lines changed

lib/model/emoji.dart

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import '../api/model/events.dart';
22
import '../api/model/initial_snapshot.dart';
33
import '../api/model/model.dart';
4+
import '../api/route/realm.dart';
45

56
/// An emoji, described by how to display it in the UI.
67
sealed class EmojiDisplay {
@@ -61,6 +62,12 @@ mixin EmojiStore {
6162
required String emojiCode,
6263
required String emojiName,
6364
});
65+
66+
// TODO cut debugServerEmojiData once we can query for lists of emoji;
67+
// have tests make those queries end-to-end
68+
Map<String, List<String>>? get debugServerEmojiData;
69+
70+
void setServerEmojiData(ServerEmojiData data);
6471
}
6572

6673
/// The implementation of [EmojiStore] that does the work.
@@ -72,7 +79,7 @@ class EmojiStoreImpl with EmojiStore {
7279
EmojiStoreImpl({
7380
required this.realmUrl,
7481
required this.realmEmoji,
75-
});
82+
}) : _serverEmojiData = null; // TODO(#974) maybe start from a hard-coded baseline
7683

7784
/// The same as [PerAccountStore.realmUrl].
7885
final Uri realmUrl;
@@ -131,6 +138,21 @@ class EmojiStoreImpl with EmojiStore {
131138
);
132139
}
133140

141+
@override
142+
Map<String, List<String>>? get debugServerEmojiData => _serverEmojiData;
143+
144+
/// The server's list of Unicode emoji and names for them,
145+
/// from [ServerEmojiData].
146+
///
147+
/// This is null until [UpdateMachine.fetchEmojiData] finishes
148+
/// retrieving the data.
149+
Map<String, List<String>>? _serverEmojiData;
150+
151+
@override
152+
void setServerEmojiData(ServerEmojiData data) {
153+
_serverEmojiData = data.codeToNames;
154+
}
155+
134156
void handleRealmEmojiUpdateEvent(RealmEmojiUpdateEvent event) {
135157
realmEmoji = event.realmEmoji;
136158
}

lib/model/store.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import '../api/model/model.dart';
1616
import '../api/route/events.dart';
1717
import '../api/route/messages.dart';
1818
import '../api/backoff.dart';
19+
import '../api/route/realm.dart';
1920
import '../log.dart';
2021
import '../notifications/receive.dart';
2122
import 'autocomplete.dart';
@@ -345,6 +346,15 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
345346
emojiType: emojiType, emojiCode: emojiCode, emojiName: emojiName);
346347
}
347348

349+
@override
350+
Map<String, List<String>>? get debugServerEmojiData => _emoji.debugServerEmojiData;
351+
352+
@override
353+
void setServerEmojiData(ServerEmojiData data) {
354+
_emoji.setServerEmojiData(data);
355+
notifyListeners();
356+
}
357+
348358
EmojiStoreImpl _emoji;
349359

350360
////////////////////////////////
@@ -746,6 +756,12 @@ class UpdateMachine {
746756
final updateMachine = UpdateMachine.fromInitialSnapshot(
747757
store: store, initialSnapshot: initialSnapshot);
748758
updateMachine.poll();
759+
if (initialSnapshot.serverEmojiDataUrl != null) {
760+
// TODO(server-6): If the server is ancient, just skip trying to have
761+
// a list of its emoji. (The old servers that don't provide
762+
// serverEmojiDataUrl are already unsupported at time of writing.)
763+
unawaited(updateMachine.fetchEmojiData(initialSnapshot.serverEmojiDataUrl!));
764+
}
749765
// TODO do registerNotificationToken before registerQueue:
750766
// https://github.com/zulip/zulip-flutter/pull/325#discussion_r1365982807
751767
unawaited(updateMachine.registerNotificationToken());
@@ -772,6 +788,43 @@ class UpdateMachine {
772788
}
773789
}
774790

791+
/// Fetch emoji data from the server, and update the store with the result.
792+
///
793+
/// This functions a lot like [registerQueue] and the surrounding logic
794+
/// in [load] above, but it's unusual in that we've separated it out.
795+
/// Effectively it's data that *would have* been in the [registerQueue]
796+
/// response, except that we pulled it out to its own endpoint as part of
797+
/// a caching strategy, because the data changes infrequently.
798+
///
799+
/// Conveniently (a) this deferred fetch doesn't cause any fetch/event race,
800+
/// because this data doesn't get updated by events anyway (it can change
801+
/// only on a server restart); and (b) we don't need this data for displaying
802+
/// messages or anything else, only for certain UIs like the emoji picker,
803+
/// so it's fine that we go without it for a while.
804+
Future<void> fetchEmojiData(Uri serverEmojiDataUrl) async {
805+
if (!debugEnableFetchEmojiData) return;
806+
BackoffMachine? backoffMachine;
807+
ServerEmojiData data;
808+
while (true) {
809+
try {
810+
data = await fetchServerEmojiData(store.connection,
811+
emojiDataUrl: serverEmojiDataUrl);
812+
assert(debugLog('Got emoji data: ${data.codeToNames.length} emoji'));
813+
break;
814+
} catch (e) {
815+
assert(debugLog('Error fetching emoji data: $e\n' // TODO(log)
816+
'Backing off, then will retry…'));
817+
// The emoji data is a lot less urgent than the initial fetch,
818+
// or even the event-queue poll request. So wait longer.
819+
backoffMachine ??= BackoffMachine(firstBound: const Duration(seconds: 2),
820+
maxBound: const Duration(minutes: 2));
821+
await backoffMachine.wait();
822+
}
823+
}
824+
825+
store.setServerEmojiData(data);
826+
}
827+
775828
Completer<void>? _debugLoopSignal;
776829

777830
/// In debug mode, causes the polling loop to pause before the next
@@ -890,6 +943,26 @@ class UpdateMachine {
890943
NotificationService.instance.token.removeListener(_registerNotificationToken);
891944
}
892945

946+
/// In debug mode, controls whether [fetchEmojiData] should
947+
/// have its normal effect.
948+
///
949+
/// Outside of debug mode, this is always true and the setter has no effect.
950+
static bool get debugEnableFetchEmojiData {
951+
bool result = true;
952+
assert(() {
953+
result = _debugEnableFetchEmojiData;
954+
return true;
955+
}());
956+
return result;
957+
}
958+
static bool _debugEnableFetchEmojiData = true;
959+
static set debugEnableFetchEmojiData(bool value) {
960+
assert(() {
961+
_debugEnableFetchEmojiData = value;
962+
return true;
963+
}());
964+
}
965+
893966
/// In debug mode, controls whether [registerNotificationToken] should
894967
/// have its normal effect.
895968
///

test/model/store_test.dart

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import 'package:checks/checks.dart';
44
import 'package:flutter/foundation.dart';
55
import 'package:http/http.dart' as http;
66
import 'package:test/scaffolding.dart';
7+
import 'package:zulip/api/core.dart';
78
import 'package:zulip/api/model/events.dart';
89
import 'package:zulip/api/model/model.dart';
910
import 'package:zulip/api/route/events.dart';
1011
import 'package:zulip/api/route/messages.dart';
12+
import 'package:zulip/api/route/realm.dart';
1113
import 'package:zulip/model/message_list.dart';
1214
import 'package:zulip/model/narrow.dart';
1315
import 'package:zulip/model/store.dart';
@@ -230,6 +232,8 @@ void main() {
230232
await globalStore.insertAccount(account.toCompanion(false));
231233
connection = (globalStore.apiConnectionFromAccount(account)
232234
as FakeApiConnection);
235+
UpdateMachine.debugEnableFetchEmojiData = false;
236+
addTearDown(() => UpdateMachine.debugEnableFetchEmojiData = true);
233237
UpdateMachine.debugEnableRegisterNotificationToken = false;
234238
addTearDown(() => UpdateMachine.debugEnableRegisterNotificationToken = true);
235239
}
@@ -317,6 +321,69 @@ void main() {
317321
// TODO test UpdateMachine.load calls registerNotificationToken
318322
});
319323

324+
group('UpdateMachine.fetchEmojiData', () {
325+
late UpdateMachine updateMachine;
326+
late PerAccountStore store;
327+
late FakeApiConnection connection;
328+
329+
void prepareStore() {
330+
updateMachine = eg.updateMachine();
331+
store = updateMachine.store;
332+
connection = store.connection as FakeApiConnection;
333+
}
334+
335+
final emojiDataUrl = Uri.parse('https://cdn.example/emoji.json');
336+
final data = {
337+
'1f642': ['smile'],
338+
'1f34a': ['orange', 'tangerine', 'mandarin'],
339+
};
340+
341+
void checkLastRequest() {
342+
check(connection.takeRequests()).single.isA<http.Request>()
343+
..method.equals('GET')
344+
..url.equals(emojiDataUrl)
345+
..headers.deepEquals(kFallbackUserAgentHeader);
346+
}
347+
348+
test('happy case', () => awaitFakeAsync((async) async {
349+
prepareStore();
350+
check(store.debugServerEmojiData).isNull();
351+
352+
connection.prepare(json: ServerEmojiData(codeToNames: data).toJson());
353+
await updateMachine.fetchEmojiData(emojiDataUrl);
354+
checkLastRequest();
355+
check(store.debugServerEmojiData).deepEquals(data);
356+
}));
357+
358+
test('retries on failure', () => awaitFakeAsync((async) async {
359+
prepareStore();
360+
check(store.debugServerEmojiData).isNull();
361+
362+
// Try to fetch, inducing an error in the request.
363+
connection.prepare(exception: Exception('failed'));
364+
final future = updateMachine.fetchEmojiData(emojiDataUrl);
365+
bool complete = false;
366+
future.whenComplete(() => complete = true);
367+
async.flushMicrotasks();
368+
checkLastRequest();
369+
check(complete).isFalse();
370+
check(store.debugServerEmojiData).isNull();
371+
372+
// The retry doesn't happen immediately; there's a timer.
373+
check(async.pendingTimers).length.equals(1);
374+
async.elapse(Duration.zero);
375+
check(connection.lastRequest).isNull();
376+
check(async.pendingTimers).length.equals(1);
377+
378+
// After a timer, we retry.
379+
connection.prepare(json: ServerEmojiData(codeToNames: data).toJson());
380+
await future;
381+
check(complete).isTrue();
382+
checkLastRequest();
383+
check(store.debugServerEmojiData).deepEquals(data);
384+
}));
385+
});
386+
320387
group('UpdateMachine.poll', () {
321388
late TestGlobalStore globalStore;
322389
late UpdateMachine updateMachine;

0 commit comments

Comments
 (0)