Skip to content

api: Add add-reaction and remove-reaction routes #272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/api/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ class ApiConnection {
..files.add(http.MultipartFile('file', content, length, filename: filename));
return send(routeName, fromJson, request);
}

Future<T> delete<T>(String routeName, T Function(Map<String, dynamic>) fromJson,
String path, Map<String, dynamic>? params) async {
final url = realmUrl.replace(path: "/api/v1/$path");
final request = http.Request('DELETE', url);
if (params != null) {
request.bodyFields = encodeParameters(params)!;
}
return send(routeName, fromJson, request);
}
}

ApiRequestException _makeApiException(String routeName, int httpStatus, Map<String, dynamic>? json) {
Expand Down
2 changes: 1 addition & 1 deletion lib/api/model/events.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -475,4 +475,6 @@ enum ReactionType {
unicodeEmoji,
realmEmoji,
zulipExtraEmoji;

String toJson() => _$ReactionTypeEnumMap[this]!;
}
2 changes: 1 addition & 1 deletion lib/api/model/model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions lib/api/route/messages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,31 @@ class UploadFileResult {

Map<String, dynamic> toJson() => _$UploadFileResultToJson(this);
}

/// https://zulip.com/api/add-reaction
Future<void> addReaction(ApiConnection connection, {
required int messageId,
required ReactionType reactionType,
required String emojiCode,
required String emojiName,
}) {
return connection.post('addReaction', (_) {}, 'messages/$messageId/reactions', {
'emoji_name': RawParameter(emojiName),
'emoji_code': RawParameter(emojiCode),
'reaction_type': RawParameter(reactionType.toJson()),
});
}

/// https://zulip.com/api/remove-reaction
Future<void> removeReaction(ApiConnection connection, {
required int messageId,
required ReactionType reactionType,
required String emojiCode,
required String emojiName,
}) {
return connection.delete('removeReaction', (_) {}, 'messages/$messageId/reactions', {
'emoji_name': RawParameter(emojiName),
'emoji_code': RawParameter(emojiCode),
'reaction_type': RawParameter(reactionType.toJson()),
});
}
30 changes: 30 additions & 0 deletions test/api/core_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,36 @@ void main() {
checkRequest(['asdf'.codeUnits], 100, null);
});

test('ApiConnection.delete', () async {
Future<void> checkRequest(Map<String, dynamic>? params, String expectedBody, {bool expectContentType = true}) {
return FakeApiConnection.with_(account: eg.selfAccount, (connection) async {
connection.prepare(json: {});
await connection.delete(kExampleRouteName, (json) => json, 'example/route', params);
check(connection.lastRequest!).isA<http.Request>()
..method.equals('DELETE')
..url.asString.equals('${eg.realmUrl.origin}/api/v1/example/route')
..headers.deepEquals({
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
if (expectContentType)
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
})
..body.equals(expectedBody);
});
}

checkRequest(null, '', expectContentType: false);
checkRequest({}, '');
checkRequest({'x': 3}, 'x=3');
checkRequest({'x': 3, 'y': 4}, 'x=3&y=4');
checkRequest({'x': null}, 'x=null');
checkRequest({'x': true}, 'x=true');
checkRequest({'x': 'foo'}, 'x=%22foo%22');
checkRequest({'x': [1, 2]}, 'x=%5B1%2C2%5D');
checkRequest({'x': {'y': 1}}, 'x=%7B%22y%22%3A1%7D');
checkRequest({'x': RawParameter('foo')}, 'x=foo');
checkRequest({'x': RawParameter('foo'), 'y': 'bar'}, 'x=foo&y=%22bar%22');
});

test('API success result', () async {
await FakeApiConnection.with_(account: eg.selfAccount, (connection) async {
connection.prepare(json: {'result': 'success', 'x': 3});
Expand Down
102 changes: 102 additions & 0 deletions test/api/route/messages_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,106 @@ void main() {
});
});
});

group('addReaction', () {
Future<void> checkAddReaction(FakeApiConnection connection, {
required int messageId,
required Reaction reaction,
required String expectedReactionType,
}) async {
connection.prepare(json: {});
await addReaction(connection,
messageId: messageId,
reactionType: reaction.reactionType,
emojiCode: reaction.emojiCode,
emojiName: reaction.emojiName,
);
check(connection.lastRequest).isNotNull().isA<http.Request>()
..method.equals('POST')
..url.path.equals('/api/v1/messages/$messageId/reactions')
..bodyFields.deepEquals({
'reaction_type': expectedReactionType,
'emoji_code': reaction.emojiCode,
'emoji_name': reaction.emojiName,
});
}

test('unicode emoji', () {
return FakeApiConnection.with_((connection) async {
await checkAddReaction(connection,
messageId: eg.streamMessage().id,
reaction: eg.unicodeEmojiReaction,
expectedReactionType: 'unicode_emoji');
});
});

test('realm emoji', () {
return FakeApiConnection.with_((connection) async {
await checkAddReaction(connection,
messageId: eg.streamMessage().id,
reaction: eg.realmEmojiReaction,
expectedReactionType: 'realm_emoji');
});
});

test('Zulip extra emoji', () {
return FakeApiConnection.with_((connection) async {
await checkAddReaction(connection,
messageId: eg.streamMessage().id,
reaction: eg.zulipExtraEmojiReaction,
expectedReactionType: 'zulip_extra_emoji');
});
});
});

group('removeReaction', () {
Future<void> checkRemoveReaction(FakeApiConnection connection, {
required int messageId,
required Reaction reaction,
required String expectedReactionType,
}) async {
connection.prepare(json: {});
await removeReaction(connection,
messageId: messageId,
reactionType: reaction.reactionType,
emojiCode: reaction.emojiCode,
emojiName: reaction.emojiName,
);
check(connection.lastRequest).isNotNull().isA<http.Request>()
..method.equals('DELETE')
..url.path.equals('/api/v1/messages/$messageId/reactions')
..bodyFields.deepEquals({
'reaction_type': expectedReactionType,
'emoji_code': reaction.emojiCode,
'emoji_name': reaction.emojiName,
});
}

test('unicode emoji', () {
return FakeApiConnection.with_((connection) async {
await checkRemoveReaction(connection,
messageId: eg.streamMessage().id,
reaction: eg.unicodeEmojiReaction,
expectedReactionType: 'unicode_emoji');
});
});

test('realm emoji', () {
return FakeApiConnection.with_((connection) async {
await checkRemoveReaction(connection,
messageId: eg.streamMessage().id,
reaction: eg.realmEmojiReaction,
expectedReactionType: 'realm_emoji');
});
});

test('Zulip extra emoji', () {
return FakeApiConnection.with_((connection) async {
await checkRemoveReaction(connection,
messageId: eg.streamMessage().id,
reaction: eg.zulipExtraEmojiReaction,
expectedReactionType: 'zulip_extra_emoji');
});
});
});
}
18 changes: 17 additions & 1 deletion test/example_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ StreamMessage streamMessage({
..._messagePropertiesFromContent(content, contentMarkdown),
'display_recipient': effectiveStream.name,
'stream_id': effectiveStream.streamId,
'reactions': reactions?.map((r) => r.toJson()).toList() ?? [],
'reactions': reactions?.map(
(r) => r.toJson()..['reaction_type'] = r.reactionType.toJson(),
).toList() ?? [],
'flags': flags ?? [],
'id': id ?? 1234567, // TODO generate example IDs
'last_edit_timestamp': lastEditTimestamp,
Expand Down Expand Up @@ -195,6 +197,20 @@ Reaction unicodeEmojiReaction = Reaction(
userId: selfUser.userId,
);

Reaction realmEmojiReaction = Reaction(
emojiName: 'twocents',
emojiCode: '181',
reactionType: ReactionType.realmEmoji,
userId: selfUser.userId,
);

Reaction zulipExtraEmojiReaction = Reaction(
emojiName: 'zulip',
emojiCode: 'zulip',
reactionType: ReactionType.zulipExtraEmoji,
userId: selfUser.userId,
);

// TODO example data for many more types

InitialSnapshot initialSnapshot({
Expand Down
15 changes: 7 additions & 8 deletions test/model/message_list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -396,23 +396,22 @@ void main() async {

test('remove reaction', () async {
final eventReaction = Reaction(reactionType: ReactionType.unicodeEmoji,
emojiName: 'wave', emojiCode: '1f44b', userId: 1);
emojiName: 'wave', emojiCode: '1f44b', userId: 1);

// Same emoji, different user. Not to be removed.
final reaction2 = Reaction.fromJson(eventReaction.toJson()
..['user_id'] = 2);
final reaction2 = Reaction(reactionType: ReactionType.unicodeEmoji,
emojiName: 'wave', emojiCode: '1f44b', userId: 2);

// Same user, different emoji. Not to be removed.
final reaction3 = Reaction.fromJson(eventReaction.toJson()
..['emoji_code'] = '1f6e0'
..['emoji_name'] = 'working_on_it');
final reaction3 = Reaction(reactionType: ReactionType.unicodeEmoji,
emojiName: 'working_on_it', emojiCode: '1f6e0', userId: 1);

// Same user, same emojiCode, different emojiName. To be removed: servers
// key on user, message, reaction type, and emoji code, but not emoji name.
// So we mimic that behavior; see discussion:
// https://github.com/zulip/zulip-flutter/pull/256#discussion_r1284865099
final reaction4 = Reaction.fromJson(eventReaction.toJson()
..['emoji_name'] = 'hello');
final reaction4 = Reaction(reactionType: ReactionType.unicodeEmoji,
emojiName: 'hello', emojiCode: '1f44b', userId: 1);

final originalMessage = eg.streamMessage(
reactions: [reaction2, reaction3, reaction4]);
Expand Down