diff --git a/lib/notifications/display.dart b/lib/notifications/display.dart index 8dc096284..70a757160 100644 --- a/lib/notifications/display.dart +++ b/lib/notifications/display.dart @@ -1,10 +1,11 @@ import 'dart:convert'; +import 'package:http/http.dart' as http; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Person; import '../api/notifications.dart'; import '../host/android_notifications.dart'; @@ -92,7 +93,32 @@ class NotificationDisplayManager { static Future _onMessageFcmMessage(MessageFcmMessage data, Map dataJson) async { assert(debugLog('notif message content: ${data.content}')); final zulipLocalizations = GlobalLocalizations.zulipLocalizations; - final title = switch (data.recipient) { + final groupKey = _groupKey(data); + final conversationKey = _conversationKey(data, groupKey); + + final oldMessagingStyle = await ZulipBinding.instance.androidNotificationHost + .getActiveNotificationMessagingStyleByTag(conversationKey); + + MessagingStyle messagingStyle; + if (oldMessagingStyle != null) { + messagingStyle = MessagingStyle( + user: oldMessagingStyle.user, + messages: oldMessagingStyle.messages?.toList() ?? [], // Clone a fixed-length list + isGroupConversation: oldMessagingStyle.isGroupConversation); + } else { + messagingStyle = MessagingStyle( + user: Person( + key: data.userId.toString(), + name: 'You'), + messages: [], + isGroupConversation: switch (data.recipient) { + FcmMessageStreamRecipient() => true, + FcmMessageDmRecipient(:var allRecipientIds) when allRecipientIds.length > 2 => true, + FcmMessageDmRecipient() => false, + }); + } + + messagingStyle.conversationTitle = switch (data.recipient) { FcmMessageStreamRecipient(:var streamName?, :var topic) => '#$streamName > $topic', FcmMessageStreamRecipient(:var topic) => @@ -103,8 +129,15 @@ class NotificationDisplayManager { FcmMessageDmRecipient() => data.senderFullName, }; - final groupKey = _groupKey(data); - final conversationKey = _conversationKey(data, groupKey); + + messagingStyle.messages?.add(MessagingStyleMessage( + text: data.content, + timestampMs: data.time * 1000, + person: Person( + key: data.senderId.toString(), + name: data.senderFullName, + iconData: await _fetchBitmap(data.senderAvatarUrl))), + ); await ZulipBinding.instance.androidNotificationHost.notify( // TODO the notification ID can be constant, instead of matching requestCode @@ -114,8 +147,9 @@ class NotificationDisplayManager { channelId: NotificationChannelManager.kChannelId, groupKey: groupKey, - contentTitle: title, - contentText: data.content, + messagingStyle: messagingStyle, + number: messagingStyle.messages?.length, + color: kZulipBrandColor.value, // TODO vary notification icon for debug 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 { page: MessageListPage(narrow: narrow))); return; } + + static Future _fetchBitmap(Uri url) async { + try { + final resp = await http.get(url); + return resp.bodyBytes; + } catch (e) { + // TODO(log) + return null; + } + } } diff --git a/test/notifications/display_test.dart b/test/notifications/display_test.dart index 13d4f5d06..4c5e26d14 100644 --- a/test/notifications/display_test.dart +++ b/test/notifications/display_test.dart @@ -109,6 +109,7 @@ void main() { void checkNotification(MessageFcmMessage data, { required String expectedTitle, required String expectedTagComponent, + required bool expectedGroup, }) { final expectedTag = '${data.realmUri}|${data.userId}|$expectedTagComponent'; final expectedGroupKey = '${data.realmUri}|${data.userId}'; @@ -121,8 +122,6 @@ void main() { ..id.equals(expectedId) ..tag.equals(expectedTag) ..channelId.equals(NotificationChannelManager.kChannelId) - ..contentTitle.equals(expectedTitle) - ..contentText.equals(data.content) ..color.equals(kZulipBrandColor.value) ..smallIconResourceName.equals('zulip_notification') ..extras.isNull() @@ -130,6 +129,12 @@ void main() { ..isGroupSummary.isNull() ..inboxStyle.isNull() ..autoCancel.equals(true) + ..messagingStyle.which((it) => it.isNotNull() + ..isGroupConversation.equals(expectedGroup) + ..conversationTitle.equals(expectedTitle) + ..messages.which((it) => it.isNotNull() + ..last.which((it) => it.isNotNull() + ..text.equals(data.content)))) ..contentIntent.which((it) => it.isNotNull() ..requestCode.equals(expectedId) ..flags.equals(expectedIntentFlags) @@ -156,6 +161,7 @@ void main() { Future checkNotifications(FakeAsync async, MessageFcmMessage data, { required String expectedTitle, required String expectedTagComponent, + required bool expectedGroup, }) async { // We could just call `NotificationDisplayManager.onFcmMessage`. // But this way is cheap, and it provides our test coverage of @@ -164,13 +170,17 @@ void main() { testBinding.firebaseMessaging.onMessage.add( RemoteMessage(data: data.toJson())); async.flushMicrotasks(); - checkNotification(data, expectedTitle: expectedTitle, + checkNotification(data, + expectedGroup: expectedGroup, + expectedTitle: expectedTitle, expectedTagComponent: expectedTagComponent); testBinding.firebaseMessaging.onBackgroundMessage.add( RemoteMessage(data: data.toJson())); async.flushMicrotasks(); - checkNotification(data, expectedTitle: expectedTitle, + checkNotification(data, + expectedGroup: expectedGroup, + expectedTitle: expectedTitle, expectedTagComponent: expectedTagComponent); } @@ -179,6 +189,7 @@ void main() { final stream = eg.stream(); final message = eg.streamMessage(stream: stream); await checkNotifications(async, messageFcmMessage(message, streamName: stream.name), + expectedGroup: true, expectedTitle: '#${stream.name} > ${message.subject}', expectedTagComponent: 'stream:${message.streamId}:${message.subject}'); })); @@ -188,6 +199,7 @@ void main() { final stream = eg.stream(); final message = eg.streamMessage(stream: stream); await checkNotifications(async, messageFcmMessage(message, streamName: null), + expectedGroup: true, expectedTitle: '#(unknown stream) > ${message.subject}', expectedTagComponent: 'stream:${message.streamId}:${message.subject}'); })); @@ -196,6 +208,7 @@ void main() { await init(); final message = eg.dmMessage(from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]); await checkNotifications(async, messageFcmMessage(message), + expectedGroup: true, expectedTitle: "${eg.thirdUser.fullName} to you and 1 other", expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}'); })); @@ -205,6 +218,7 @@ void main() { final message = eg.dmMessage(from: eg.thirdUser, to: [eg.otherUser, eg.selfUser, eg.fourthUser]); await checkNotifications(async, messageFcmMessage(message), + expectedGroup: true, expectedTitle: "${eg.thirdUser.fullName} to you and 2 others", expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}'); })); @@ -213,6 +227,7 @@ void main() { await init(); final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]); await checkNotifications(async, messageFcmMessage(message), + expectedGroup: false, expectedTitle: eg.otherUser.fullName, expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}'); })); @@ -221,6 +236,7 @@ void main() { await init(); final message = eg.dmMessage(from: eg.selfUser, to: []); await checkNotifications(async, messageFcmMessage(message), + expectedGroup: false, expectedTitle: eg.selfUser.fullName, expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}'); }));