Skip to content

Commit 0e3d32c

Browse files
notif: Support messaging-style notifications
Fixes: #128
1 parent a646972 commit 0e3d32c

File tree

2 files changed

+70
-10
lines changed

2 files changed

+70
-10
lines changed

lib/notifications/display.dart

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import 'dart:convert';
22

3+
import 'package:http/http.dart' as http;
34
import 'package:collection/collection.dart';
45
import 'package:crypto/crypto.dart';
56
import 'package:flutter/foundation.dart';
67
import 'package:flutter/widgets.dart';
7-
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
8+
import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Person;
89

910
import '../api/notifications.dart';
1011
import '../host/android_notifications.dart';
@@ -92,7 +93,32 @@ class NotificationDisplayManager {
9293
static Future<void> _onMessageFcmMessage(MessageFcmMessage data, Map<String, dynamic> dataJson) async {
9394
assert(debugLog('notif message content: ${data.content}'));
9495
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
95-
final title = switch (data.recipient) {
96+
final groupKey = _groupKey(data);
97+
final conversationKey = _conversationKey(data, groupKey);
98+
99+
final oldMessagingStyle = await ZulipBinding.instance.androidNotificationHost
100+
.getActiveNotificationMessagingStyleByTag(conversationKey);
101+
102+
MessagingStyle messagingStyle;
103+
if (oldMessagingStyle != null) {
104+
messagingStyle = MessagingStyle(
105+
user: oldMessagingStyle.user,
106+
messages: oldMessagingStyle.messages?.toList() ?? [], // Clone a fixed-length list
107+
isGroupConversation: oldMessagingStyle.isGroupConversation);
108+
} else {
109+
messagingStyle = MessagingStyle(
110+
user: Person(
111+
key: data.userId.toString(),
112+
name: 'You'),
113+
messages: [],
114+
isGroupConversation: switch (data.recipient) {
115+
FcmMessageStreamRecipient() => true,
116+
FcmMessageDmRecipient(:var allRecipientIds) when allRecipientIds.length > 2 => true,
117+
FcmMessageDmRecipient() => false,
118+
});
119+
}
120+
121+
messagingStyle.conversationTitle = switch (data.recipient) {
96122
FcmMessageStreamRecipient(:var streamName?, :var topic) =>
97123
'#$streamName > $topic',
98124
FcmMessageStreamRecipient(:var topic) =>
@@ -103,8 +129,15 @@ class NotificationDisplayManager {
103129
FcmMessageDmRecipient() =>
104130
data.senderFullName,
105131
};
106-
final groupKey = _groupKey(data);
107-
final conversationKey = _conversationKey(data, groupKey);
132+
133+
messagingStyle.messages?.add(MessagingStyleMessage(
134+
text: data.content,
135+
timestampMs: data.time * 1000,
136+
person: Person(
137+
key: data.senderId.toString(),
138+
name: data.senderFullName,
139+
iconData: await _fetchBitmap(data.senderAvatarUrl))),
140+
);
108141

109142
await ZulipBinding.instance.androidNotificationHost.notify(
110143
// TODO the notification ID can be constant, instead of matching requestCode
@@ -114,8 +147,9 @@ class NotificationDisplayManager {
114147
channelId: NotificationChannelManager.kChannelId,
115148
groupKey: groupKey,
116149

117-
contentTitle: title,
118-
contentText: data.content,
150+
messagingStyle: messagingStyle,
151+
number: messagingStyle.messages?.length,
152+
119153
color: kZulipBrandColor.value,
120154
// TODO vary notification icon for debug
121155
smallIconResourceName: 'zulip_notification', // This name must appear in keep.xml too: https://github.com/zulip/zulip-flutter/issues/528
@@ -230,4 +264,14 @@ class NotificationDisplayManager {
230264
page: MessageListPage(narrow: narrow)));
231265
return;
232266
}
267+
268+
static Future<Uint8List?> _fetchBitmap(Uri url) async {
269+
try {
270+
final resp = await http.get(url);
271+
return resp.bodyBytes;
272+
} catch (e) {
273+
// TODO(log)
274+
return null;
275+
}
276+
}
233277
}

test/notifications/display_test.dart

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ void main() {
109109
void checkNotification(MessageFcmMessage data, {
110110
required String expectedTitle,
111111
required String expectedTagComponent,
112+
required bool expectedGroup,
112113
}) {
113114
final expectedTag = '${data.realmUri}|${data.userId}|$expectedTagComponent';
114115
final expectedGroupKey = '${data.realmUri}|${data.userId}';
@@ -121,15 +122,19 @@ void main() {
121122
..id.equals(expectedId)
122123
..tag.equals(expectedTag)
123124
..channelId.equals(NotificationChannelManager.kChannelId)
124-
..contentTitle.equals(expectedTitle)
125-
..contentText.equals(data.content)
126125
..color.equals(kZulipBrandColor.value)
127126
..smallIconResourceName.equals('zulip_notification')
128127
..extras.isNull()
129128
..groupKey.equals(expectedGroupKey)
130129
..isGroupSummary.isNull()
131130
..inboxStyle.isNull()
132131
..autoCancel.equals(true)
132+
..messagingStyle.which((it) => it.isNotNull()
133+
..isGroupConversation.equals(expectedGroup)
134+
..conversationTitle.equals(expectedTitle)
135+
..messages.which((it) => it.isNotNull()
136+
..last.which((it) => it.isNotNull()
137+
..text.equals(data.content))))
133138
..contentIntent.which((it) => it.isNotNull()
134139
..requestCode.equals(expectedId)
135140
..flags.equals(expectedIntentFlags)
@@ -156,6 +161,7 @@ void main() {
156161
Future<void> checkNotifications(FakeAsync async, MessageFcmMessage data, {
157162
required String expectedTitle,
158163
required String expectedTagComponent,
164+
required bool expectedGroup,
159165
}) async {
160166
// We could just call `NotificationDisplayManager.onFcmMessage`.
161167
// But this way is cheap, and it provides our test coverage of
@@ -164,13 +170,17 @@ void main() {
164170
testBinding.firebaseMessaging.onMessage.add(
165171
RemoteMessage(data: data.toJson()));
166172
async.flushMicrotasks();
167-
checkNotification(data, expectedTitle: expectedTitle,
173+
checkNotification(data,
174+
expectedGroup: expectedGroup,
175+
expectedTitle: expectedTitle,
168176
expectedTagComponent: expectedTagComponent);
169177

170178
testBinding.firebaseMessaging.onBackgroundMessage.add(
171179
RemoteMessage(data: data.toJson()));
172180
async.flushMicrotasks();
173-
checkNotification(data, expectedTitle: expectedTitle,
181+
checkNotification(data,
182+
expectedGroup: expectedGroup,
183+
expectedTitle: expectedTitle,
174184
expectedTagComponent: expectedTagComponent);
175185
}
176186

@@ -179,6 +189,7 @@ void main() {
179189
final stream = eg.stream();
180190
final message = eg.streamMessage(stream: stream);
181191
await checkNotifications(async, messageFcmMessage(message, streamName: stream.name),
192+
expectedGroup: true,
182193
expectedTitle: '#${stream.name} > ${message.subject}',
183194
expectedTagComponent: 'stream:${message.streamId}:${message.subject}');
184195
}));
@@ -188,6 +199,7 @@ void main() {
188199
final stream = eg.stream();
189200
final message = eg.streamMessage(stream: stream);
190201
await checkNotifications(async, messageFcmMessage(message, streamName: null),
202+
expectedGroup: true,
191203
expectedTitle: '#(unknown stream) > ${message.subject}',
192204
expectedTagComponent: 'stream:${message.streamId}:${message.subject}');
193205
}));
@@ -196,6 +208,7 @@ void main() {
196208
await init();
197209
final message = eg.dmMessage(from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);
198210
await checkNotifications(async, messageFcmMessage(message),
211+
expectedGroup: true,
199212
expectedTitle: "${eg.thirdUser.fullName} to you and 1 other",
200213
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
201214
}));
@@ -205,6 +218,7 @@ void main() {
205218
final message = eg.dmMessage(from: eg.thirdUser,
206219
to: [eg.otherUser, eg.selfUser, eg.fourthUser]);
207220
await checkNotifications(async, messageFcmMessage(message),
221+
expectedGroup: true,
208222
expectedTitle: "${eg.thirdUser.fullName} to you and 2 others",
209223
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
210224
}));
@@ -213,6 +227,7 @@ void main() {
213227
await init();
214228
final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]);
215229
await checkNotifications(async, messageFcmMessage(message),
230+
expectedGroup: false,
216231
expectedTitle: eg.otherUser.fullName,
217232
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
218233
}));
@@ -221,6 +236,7 @@ void main() {
221236
await init();
222237
final message = eg.dmMessage(from: eg.selfUser, to: []);
223238
await checkNotifications(async, messageFcmMessage(message),
239+
expectedGroup: false,
224240
expectedTitle: eg.selfUser.fullName,
225241
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
226242
}));

0 commit comments

Comments
 (0)