Skip to content

Commit 38fd31d

Browse files
committed
emoji: Generate popular candidates using names from server data
The server change in zulip/zulip#34177, renaming `:smile:` to `:slight_smile:`, broke the corresponding reaction button in the message action sheet. We've been sending the add/remove-reaction request with the old name 'smile', which modern servers reject. To fix, take the popular emoji names from ServerEmojiData, so that we'll use the correct name on servers before and after the change. API-design discussion: https://chat.zulip.org/#narrow/channel/378-api-design/topic/.23F1495.20smile.2Fslight_smile.20change.20broke.20reaction.20button/near/2170354 Fixes: zulip#1495
1 parent 6965d4d commit 38fd31d

File tree

4 files changed

+119
-79
lines changed

4 files changed

+119
-79
lines changed

lib/model/emoji.dart

+13-12
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,9 @@ class EmojiStoreImpl extends PerAccountStoreBase with EmojiStore {
212212
/// retrieving the data.
213213
Map<String, List<String>>? _serverEmojiData;
214214

215-
static final _popularCandidates = _generatePopularCandidates();
215+
List<EmojiCandidate>? _popularCandidates;
216216

217-
static List<EmojiCandidate> _generatePopularCandidates() {
217+
List<EmojiCandidate> _generatePopularCandidates() {
218218
EmojiCandidate candidate(String emojiCode, List<String> names) {
219219
final [emojiName, ...aliases] = names;
220220
final emojiUnicode = tryParseEmojiCodeToUnicode(emojiCode);
@@ -224,20 +224,20 @@ class EmojiStoreImpl extends PerAccountStoreBase with EmojiStore {
224224
emojiDisplay: UnicodeEmojiDisplay(
225225
emojiName: emojiName, emojiUnicode: emojiUnicode!));
226226
}
227-
final list = _popularEmojiCodesList;
228-
return [
229-
candidate(list[0], ['+1', 'thumbs_up', 'like']),
230-
candidate(list[1], ['tada']),
231-
candidate(list[2], ['smile']),
232-
candidate(list[3], ['heart', 'love', 'love_you']),
233-
candidate(list[4], ['working_on_it', 'hammer_and_wrench', 'tools']),
234-
candidate(list[5], ['octopus']),
235-
];
227+
if (_serverEmojiData == null) return [];
228+
229+
final result = <EmojiCandidate>[];
230+
for (final emojiCode in _popularEmojiCodesList) {
231+
final names = _serverEmojiData![emojiCode];
232+
if (names == null) continue;
233+
result.add(candidate(emojiCode, names));
234+
}
235+
return result;
236236
}
237237

238238
@override
239239
Iterable<EmojiCandidate> popularEmojiCandidates() {
240-
return _popularCandidates;
240+
return _popularCandidates ??= _generatePopularCandidates();
241241
}
242242

243243
/// Codes for the popular emoji, in order; all are Unicode emoji.
@@ -378,6 +378,7 @@ class EmojiStoreImpl extends PerAccountStoreBase with EmojiStore {
378378
@override
379379
void setServerEmojiData(ServerEmojiData data) {
380380
_serverEmojiData = data.codeToNames;
381+
_popularCandidates = null;
381382
_allEmojiCandidates = null;
382383
}
383384

lib/widgets/action_sheet.dart

+10-1
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,8 @@ void showMessageActionSheet({required BuildContext context, required Message mes
556556
final pageContext = PageRoot.contextOf(context);
557557
final store = PerAccountStoreWidget.of(pageContext);
558558

559+
final popularEmojiLoaded = store.popularEmojiCandidates().isNotEmpty;
560+
559561
// The UI that's conditioned on this won't live-update during this appearance
560562
// of the action sheet (we avoid calling composeBoxControllerOf in a build
561563
// method; see its doc).
@@ -569,7 +571,8 @@ void showMessageActionSheet({required BuildContext context, required Message mes
569571
final showMarkAsUnreadButton = markAsUnreadSupported && isMessageRead;
570572

571573
final optionButtons = [
572-
ReactionButtons(message: message, pageContext: pageContext),
574+
if (popularEmojiLoaded)
575+
ReactionButtons(message: message, pageContext: pageContext),
573576
StarButton(message: message, pageContext: pageContext),
574577
if (isComposeBoxOffered)
575578
QuoteAndReplyButton(message: message, pageContext: pageContext),
@@ -671,6 +674,12 @@ class ReactionButtons extends StatelessWidget {
671674
final popularEmojiCandidates = store.popularEmojiCandidates();
672675
assert(popularEmojiCandidates.every(
673676
(emoji) => emoji.emojiType == ReactionType.unicodeEmoji));
677+
// (if this is empty, the widget isn't built in the first place)
678+
assert(popularEmojiCandidates.isNotEmpty);
679+
// UI not designed to handle more than 6 popular emoji.
680+
// (We might have fewer if ServerEmojiData is lacking expected data,
681+
// but that looks fine in manual testing, even when there's just one.)
682+
assert(popularEmojiCandidates.length <= 6);
674683

675684
final zulipLocalizations = ZulipLocalizations.of(context);
676685
final designVariables = DesignVariables.of(context);

test/example_data.dart

+14
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@ ServerEmojiData serverEmojiDataPopularPlus(ServerEmojiData data) {
137137
return result;
138138
}
139139

140+
/// Like [serverEmojiDataPopular], but with the modern '1f642': ['slight_smile']
141+
/// instead of '1f642': ['smile']; see zulip/zulip@9feba0f16f.
142+
///
143+
/// zulip/zulip@9feba0f16f is a Server 11 commit.
144+
// TODO(server-11) can drop legacy data
145+
ServerEmojiData serverEmojiDataPopularModern = ServerEmojiData(codeToNames: {
146+
'1f44d': ['+1', 'thumbs_up', 'like'],
147+
'1f389': ['tada'],
148+
'1f642': ['slight_smile'],
149+
'2764': ['heart', 'love', 'love_you'],
150+
'1f6e0': ['working_on_it', 'hammer_and_wrench', 'tools'],
151+
'1f419': ['octopus'],
152+
});
153+
140154
RealmEmojiItem realmEmojiItem({
141155
required String emojiCode,
142156
required String emojiName,

test/widgets/action_sheet_test.dart

+82-66
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ late FakeApiConnection connection;
5252
Future<void> setupToMessageActionSheet(WidgetTester tester, {
5353
required Message message,
5454
required Narrow narrow,
55+
bool useLegacy = false,
5556
}) async {
5657
addTearDown(testBinding.reset);
5758
assert(narrow.containsMessage(message));
@@ -70,7 +71,9 @@ Future<void> setupToMessageActionSheet(WidgetTester tester, {
7071
await store.addSubscription(eg.subscription(stream));
7172
}
7273
connection = store.connection as FakeApiConnection;
73-
store.setServerEmojiData(eg.serverEmojiDataPopular);
74+
store.setServerEmojiData(useLegacy
75+
? eg.serverEmojiDataPopular
76+
: eg.serverEmojiDataPopularModern);
7477

7578
connection.prepare(json: eg.newestGetMessagesResult(
7679
foundOldest: true, messages: [message]).toJson());
@@ -812,74 +815,87 @@ void main() {
812815

813816
group('message action sheet', () {
814817
group('ReactionButtons', () {
815-
final popularCandidates =
816-
(eg.store()..setServerEmojiData(eg.serverEmojiDataPopular))
817-
.popularEmojiCandidates();
818+
for (final useLegacy in [false, true]) {
819+
final popularCandidates =
820+
(eg.store()..setServerEmojiData(
821+
useLegacy
822+
? eg.serverEmojiDataPopular
823+
: eg.serverEmojiDataPopularModern))
824+
.popularEmojiCandidates();
825+
for (final emoji in popularCandidates) {
826+
final emojiDisplay = emoji.emojiDisplay as UnicodeEmojiDisplay;
827+
828+
Future<void> tapButton(WidgetTester tester) async {
829+
await tester.tap(find.descendant(
830+
of: find.byType(BottomSheet),
831+
matching: find.text(emojiDisplay.emojiUnicode)));
832+
}
833+
834+
testWidgets('${emoji.emojiName} adding success; useLegacy: $useLegacy', (tester) async {
835+
final message = eg.streamMessage();
836+
await setupToMessageActionSheet(tester,
837+
message: message,
838+
narrow: TopicNarrow.ofMessage(message),
839+
useLegacy: useLegacy);
840+
841+
connection.prepare(json: {});
842+
await tapButton(tester);
843+
await tester.pump(Duration.zero);
844+
845+
check(connection.lastRequest).isA<http.Request>()
846+
..method.equals('POST')
847+
..url.path.equals('/api/v1/messages/${message.id}/reactions')
848+
..bodyFields.deepEquals({
849+
'reaction_type': 'unicode_emoji',
850+
'emoji_code': emoji.emojiCode,
851+
'emoji_name': emoji.emojiName,
852+
});
853+
});
818854

819-
for (final emoji in popularCandidates) {
820-
final emojiDisplay = emoji.emojiDisplay as UnicodeEmojiDisplay;
855+
testWidgets('${emoji.emojiName} removing success', (tester) async {
856+
final message = eg.streamMessage(
857+
reactions: [Reaction(
858+
emojiName: emoji.emojiName,
859+
emojiCode: emoji.emojiCode,
860+
reactionType: ReactionType.unicodeEmoji,
861+
userId: eg.selfAccount.userId)]
862+
);
863+
await setupToMessageActionSheet(tester,
864+
message: message,
865+
narrow: TopicNarrow.ofMessage(message),
866+
useLegacy: useLegacy);
867+
868+
connection.prepare(json: {});
869+
await tapButton(tester);
870+
await tester.pump(Duration.zero);
871+
872+
check(connection.lastRequest).isA<http.Request>()
873+
..method.equals('DELETE')
874+
..url.path.equals('/api/v1/messages/${message.id}/reactions')
875+
..bodyFields.deepEquals({
876+
'reaction_type': 'unicode_emoji',
877+
'emoji_code': emoji.emojiCode,
878+
'emoji_name': emoji.emojiName,
879+
});
880+
});
821881

822-
Future<void> tapButton(WidgetTester tester) async {
823-
await tester.tap(find.descendant(
824-
of: find.byType(BottomSheet),
825-
matching: find.text(emojiDisplay.emojiUnicode)));
882+
testWidgets('${emoji.emojiName} request has an error', (tester) async {
883+
final message = eg.streamMessage();
884+
await setupToMessageActionSheet(tester,
885+
message: message,
886+
narrow: TopicNarrow.ofMessage(message),
887+
useLegacy: useLegacy);
888+
889+
connection.prepare(
890+
apiException: eg.apiBadRequest(message: 'Invalid message(s)'));
891+
await tapButton(tester);
892+
await tester.pump(Duration.zero); // error arrives; error dialog shows
893+
894+
await tester.tap(find.byWidget(checkErrorDialog(tester,
895+
expectedTitle: 'Adding reaction failed',
896+
expectedMessage: 'Invalid message(s)')));
897+
});
826898
}
827-
828-
testWidgets('${emoji.emojiName} adding success', (tester) async {
829-
final message = eg.streamMessage();
830-
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
831-
832-
connection.prepare(json: {});
833-
await tapButton(tester);
834-
await tester.pump(Duration.zero);
835-
836-
check(connection.lastRequest).isA<http.Request>()
837-
..method.equals('POST')
838-
..url.path.equals('/api/v1/messages/${message.id}/reactions')
839-
..bodyFields.deepEquals({
840-
'reaction_type': 'unicode_emoji',
841-
'emoji_code': emoji.emojiCode,
842-
'emoji_name': emoji.emojiName,
843-
});
844-
});
845-
846-
testWidgets('${emoji.emojiName} removing success', (tester) async {
847-
final message = eg.streamMessage(
848-
reactions: [Reaction(
849-
emojiName: emoji.emojiName,
850-
emojiCode: emoji.emojiCode,
851-
reactionType: ReactionType.unicodeEmoji,
852-
userId: eg.selfAccount.userId)]
853-
);
854-
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
855-
856-
connection.prepare(json: {});
857-
await tapButton(tester);
858-
await tester.pump(Duration.zero);
859-
860-
check(connection.lastRequest).isA<http.Request>()
861-
..method.equals('DELETE')
862-
..url.path.equals('/api/v1/messages/${message.id}/reactions')
863-
..bodyFields.deepEquals({
864-
'reaction_type': 'unicode_emoji',
865-
'emoji_code': emoji.emojiCode,
866-
'emoji_name': emoji.emojiName,
867-
});
868-
});
869-
870-
testWidgets('${emoji.emojiName} request has an error', (tester) async {
871-
final message = eg.streamMessage();
872-
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
873-
874-
connection.prepare(
875-
apiException: eg.apiBadRequest(message: 'Invalid message(s)'));
876-
await tapButton(tester);
877-
await tester.pump(Duration.zero); // error arrives; error dialog shows
878-
879-
await tester.tap(find.byWidget(checkErrorDialog(tester,
880-
expectedTitle: 'Adding reaction failed',
881-
expectedMessage: 'Invalid message(s)')));
882-
});
883899
}
884900
});
885901

0 commit comments

Comments
 (0)