Skip to content

Commit d7d0899

Browse files
rajveermalviyagnprice
authored andcommitted
notif ios: Add parser for iOS APNs payload
Introduces NotificationOpenPayload.parseIosApnsPayload which can parse the payload that Apple push notification service delivers to the app for displaying a notification. It retrieves the navigation data for the specific message notification.
1 parent 1911520 commit d7d0899

File tree

2 files changed

+161
-1
lines changed

2 files changed

+161
-1
lines changed

lib/notifications/open.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,72 @@ class NotificationOpenPayload {
100100
required this.narrow,
101101
});
102102

103+
/// Parses the iOS APNs payload and retrieves the information
104+
/// required for navigation.
105+
factory NotificationOpenPayload.parseIosApnsPayload(Map<Object?, Object?> payload) {
106+
if (payload case {
107+
'zulip': {
108+
'user_id': final int userId,
109+
'sender_id': final int senderId,
110+
} && final zulipData,
111+
}) {
112+
final eventType = zulipData['event'];
113+
if (eventType != null && eventType != 'message') {
114+
// On Android, we also receive "remove" notification messages, tagged
115+
// with an `event` field with value 'remove'. As of Zulip Server 10,
116+
// however, these are not yet sent to iOS devices, and we don't have a
117+
// way to handle them even if they were.
118+
//
119+
// The messages we currently do receive, and can handle, are analogous
120+
// to Android notification messages of event type 'message'. On the
121+
// assumption that some future version of the Zulip server will send
122+
// explicit event types in APNs messages, accept messages with that
123+
// `event` value, but no other.
124+
throw const FormatException();
125+
}
126+
127+
final realmUrl = switch (zulipData) {
128+
{'realm_url': final String value} => value,
129+
{'realm_uri': final String value} => value,
130+
_ => throw const FormatException(),
131+
};
132+
133+
final narrow = switch (zulipData) {
134+
{
135+
'recipient_type': 'stream',
136+
// TODO(server-5) remove this comment.
137+
// We require 'stream_id' here but that is new from Server 5.0,
138+
// resulting in failure on pre-5.0 servers.
139+
'stream_id': final int streamId,
140+
'topic': final String topic,
141+
} =>
142+
TopicNarrow(streamId, TopicName(topic)),
143+
144+
{'recipient_type': 'private', 'pm_users': final String pmUsers} =>
145+
DmNarrow(
146+
allRecipientIds: pmUsers
147+
.split(',')
148+
.map((e) => int.parse(e, radix: 10))
149+
.toList(growable: false)
150+
..sort(),
151+
selfUserId: userId),
152+
153+
{'recipient_type': 'private'} =>
154+
DmNarrow.withUser(senderId, selfUserId: userId),
155+
156+
_ => throw const FormatException(),
157+
};
158+
159+
return NotificationOpenPayload(
160+
realmUrl: Uri.parse(realmUrl),
161+
userId: userId,
162+
narrow: narrow);
163+
} else {
164+
// TODO(dart): simplify after https://github.com/dart-lang/language/issues/2537
165+
throw const FormatException();
166+
}
167+
}
168+
103169
/// Parses the internal Android notification url, that was created using
104170
/// [buildAndroidNotificationUrl], and retrieves the information required
105171
/// for navigation.

test/notifications/open_test.dart

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,50 @@ import '../widgets/message_list_checks.dart';
2525
import '../widgets/page_checks.dart';
2626
import 'display_test.dart';
2727

28+
Map<String, Object?> messageApnsPayload(
29+
Message zulipMessage, {
30+
String? streamName,
31+
Account? account,
32+
}) {
33+
account ??= eg.selfAccount;
34+
return {
35+
"aps": {
36+
"alert": {
37+
"title": "test",
38+
"subtitle": "test",
39+
"body": zulipMessage.content,
40+
},
41+
"sound": "default",
42+
"badge": 0,
43+
},
44+
"zulip": {
45+
"server": "zulip.example.cloud",
46+
"realm_id": 4,
47+
"realm_uri": account.realmUrl.toString(),
48+
"realm_url": account.realmUrl.toString(),
49+
"realm_name": "Test",
50+
"user_id": account.userId,
51+
"sender_id": zulipMessage.senderId,
52+
"sender_email": zulipMessage.senderEmail,
53+
"time": zulipMessage.timestamp,
54+
"message_ids": [zulipMessage.id],
55+
...(switch (zulipMessage) {
56+
StreamMessage(:var streamId, :var topic) => {
57+
"recipient_type": "stream",
58+
"stream_id": streamId,
59+
if (streamName != null) "stream": streamName,
60+
"topic": topic,
61+
},
62+
DmMessage(allRecipientIds: [_, _, _, ...]) => {
63+
"recipient_type": "private",
64+
"pm_users": zulipMessage.allRecipientIds.join(","),
65+
},
66+
DmMessage() => {"recipient_type": "private"},
67+
}),
68+
},
69+
};
70+
}
71+
2872
void main() {
2973
TestZulipBinding.ensureInitialized();
3074
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
@@ -249,7 +293,7 @@ void main() {
249293
});
250294

251295
group('NotificationOpenPayload', () {
252-
test('smoke round-trip', () {
296+
test('android: smoke round-trip', () {
253297
// DM narrow
254298
var payload = NotificationOpenPayload(
255299
realmUrl: Uri.parse('http://chat.example'),
@@ -275,6 +319,56 @@ void main() {
275319
..narrow.equals(payload.narrow);
276320
});
277321

322+
group('parseIosApnsPayload', () {
323+
test('smoke one-one DM', () {
324+
final userA = eg.user(userId: 1001);
325+
final userB = eg.user(userId: 1002);
326+
final account = eg.account(
327+
realmUrl: Uri.parse('http://chat.example'),
328+
user: userA);
329+
final payload = messageApnsPayload(eg.dmMessage(from: userB, to: [userA]),
330+
account: account);
331+
check(NotificationOpenPayload.parseIosApnsPayload(payload))
332+
..realmUrl.equals(Uri.parse('http://chat.example'))
333+
..userId.equals(1001)
334+
..narrow.which((it) => it.isA<DmNarrow>()
335+
..otherRecipientIds.deepEquals([1002]));
336+
});
337+
338+
test('smoke group DM', () {
339+
final userA = eg.user(userId: 1001);
340+
final userB = eg.user(userId: 1002);
341+
final userC = eg.user(userId: 1003);
342+
final account = eg.account(
343+
realmUrl: Uri.parse('http://chat.example'),
344+
user: userA);
345+
final payload = messageApnsPayload(eg.dmMessage(from: userC, to: [userA, userB]),
346+
account: account);
347+
check(NotificationOpenPayload.parseIosApnsPayload(payload))
348+
..realmUrl.equals(Uri.parse('http://chat.example'))
349+
..userId.equals(1001)
350+
..narrow.which((it) => it.isA<DmNarrow>()
351+
..otherRecipientIds.deepEquals([1002, 1003]));
352+
});
353+
354+
test('smoke topic message', () {
355+
final userA = eg.user(userId: 1001);
356+
final account = eg.account(
357+
realmUrl: Uri.parse('http://chat.example'),
358+
user: userA);
359+
final payload = messageApnsPayload(eg.streamMessage(
360+
stream: eg.stream(streamId: 1),
361+
topic: 'topic A'),
362+
account: account);
363+
check(NotificationOpenPayload.parseIosApnsPayload(payload))
364+
..realmUrl.equals(Uri.parse('http://chat.example'))
365+
..userId.equals(1001)
366+
..narrow.which((it) => it.isA<TopicNarrow>()
367+
..streamId.equals(1)
368+
..topic.equals(TopicName('topic A')));
369+
});
370+
});
371+
278372
group('buildAndroidNotificationUrl', () {
279373
test('smoke DM', () {
280374
final url = NotificationOpenPayload(

0 commit comments

Comments
 (0)