Skip to content

Commit 1273a93

Browse files
notif: Create group summary notification
Make use of Group Summary Notifications to group notifications based on different realms and also display the respective group label (which is currently the realm URL). See: https://developer.android.com/develop/ui/views/notifications/group#group-summary This change is a port of implementation in zulip-mobile: https://github.com/zulip/zulip-mobile/blob/6d5d56d175644cd0cdf47f3cd30ffadf6756bbdc/android/app/src/main/java/com/zulipmobile/notifications/NotificationUiManager.kt#L299-L382 Fixes: #569 Fixes: #571
1 parent 435bf86 commit 1273a93

File tree

2 files changed

+69
-19
lines changed

2 files changed

+69
-19
lines changed

lib/notifications/display.dart

+22-5
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class NotificationDisplayManager {
8989
}
9090
}
9191

92-
static void _onMessageFcmMessage(MessageFcmMessage data, Map<String, dynamic> dataJson) {
92+
static Future<void> _onMessageFcmMessage(MessageFcmMessage data, Map<String, dynamic> dataJson) async {
9393
assert(debugLog('notif message content: ${data.content}'));
9494
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
9595
final title = switch (data.recipient) {
@@ -103,13 +103,16 @@ class NotificationDisplayManager {
103103
FcmMessageDmRecipient() =>
104104
data.senderFullName,
105105
};
106-
final conversationKey = _conversationKey(data);
107-
ZulipBinding.instance.androidNotificationHost.notify(
106+
final groupKey = _groupKey(data);
107+
final conversationKey = _conversationKey(data, groupKey);
108+
109+
await ZulipBinding.instance.androidNotificationHost.notify(
108110
// TODO the notification ID can be constant, instead of matching requestCode
109111
// (This is a legacy of `flutter_local_notifications`.)
110112
id: notificationIdAsHashOf(conversationKey),
111113
tag: conversationKey,
112114
channelId: NotificationChannelManager.kChannelId,
115+
groupKey: groupKey,
113116

114117
contentTitle: title,
115118
contentText: data.content,
@@ -140,6 +143,21 @@ class NotificationDisplayManager {
140143
// (This is a legacy of `flutter_local_notifications`.)
141144
),
142145
);
146+
147+
await ZulipBinding.instance.androidNotificationHost.notify(
148+
id: notificationIdAsHashOf(groupKey),
149+
channelId: NotificationChannelManager.kChannelId,
150+
tag: groupKey,
151+
groupKey: groupKey,
152+
isGroupSummary: true,
153+
color: kZulipBrandColor.value,
154+
// TODO vary notification icon for debug
155+
smallIconResourceName: 'zulip_notification', // This name must appear in keep.xml too: https://github.com/zulip/zulip-flutter/issues/528
156+
inboxStyle: InboxStyle(
157+
// TODO(#570) Show organization name, not URL
158+
summaryText: data.realmUri.toString()),
159+
autoCancel: true,
160+
);
143161
}
144162

145163
/// A notification ID, derived as a hash of the given string key.
@@ -157,8 +175,7 @@ class NotificationDisplayManager {
157175
| ((bytes[3] & 0x7f) << 24);
158176
}
159177

160-
static String _conversationKey(MessageFcmMessage data) {
161-
final groupKey = _groupKey(data);
178+
static String _conversationKey(MessageFcmMessage data, String groupKey) {
162179
final conversation = switch (data.recipient) {
163180
FcmMessageStreamRecipient(:var streamId, :var topic) => 'stream:$streamId:$topic',
164181
FcmMessageDmRecipient(:var allRecipientIds) => 'dm:${allRecipientIds.join(',')}',

test/notifications/display_test.dart

+47-14
Original file line numberDiff line numberDiff line change
@@ -111,24 +111,49 @@ void main() {
111111
required String expectedTagComponent,
112112
}) {
113113
final expectedTag = '${data.realmUri}|${data.userId}|$expectedTagComponent';
114+
final expectedGroupKey = '${data.realmUri}|${data.userId}';
114115
final expectedId =
115116
NotificationDisplayManager.notificationIdAsHashOf(expectedTag);
116117
const expectedIntentFlags =
117118
PendingIntentFlag.immutable | PendingIntentFlag.updateCurrent;
118-
check(testBinding.androidNotificationHost.takeNotifyCalls()).single
119-
..id.equals(expectedId)
120-
..tag.equals(expectedTag)
121-
..channelId.equals(NotificationChannelManager.kChannelId)
122-
..contentTitle.equals(expectedTitle)
123-
..contentText.equals(data.content)
124-
..color.equals(kZulipBrandColor.value)
125-
..smallIconResourceName.equals('zulip_notification')
126-
..extras.isNull()
127-
..contentIntent.which((it) => it.isNotNull()
128-
..requestCode.equals(expectedId)
129-
..flags.equals(expectedIntentFlags)
130-
..intentPayload.equals(jsonEncode(data.toJson()))
131-
);
119+
120+
check(testBinding.androidNotificationHost.takeNotifyCalls())
121+
..length.equals(2)
122+
..containsInOrder([
123+
(Subject<AndroidNotificationHostApiNotifyCall> it) => it
124+
..id.equals(expectedId)
125+
..tag.equals(expectedTag)
126+
..channelId.equals(NotificationChannelManager.kChannelId)
127+
..contentTitle.equals(expectedTitle)
128+
..contentText.equals(data.content)
129+
..color.equals(kZulipBrandColor.value)
130+
..smallIconResourceName.equals('zulip_notification')
131+
..extras.isNull()
132+
..groupKey.equals(expectedGroupKey)
133+
..isGroupSummary.isNull()
134+
..inboxStyle.isNull()
135+
..autoCancel.isNull()
136+
..contentIntent.which((it) => it.isNotNull()
137+
..requestCode.equals(expectedId)
138+
..flags.equals(expectedIntentFlags)
139+
..intentPayload.equals(jsonEncode(data.toJson()))),
140+
(Subject<AndroidNotificationHostApiNotifyCall> it) => it
141+
..id.equals(NotificationDisplayManager.notificationIdAsHashOf(expectedGroupKey))
142+
..tag.equals(expectedGroupKey)
143+
..channelId.equals(NotificationChannelManager.kChannelId)
144+
..contentTitle.isNull()
145+
..contentText.isNull()
146+
..color.equals(kZulipBrandColor.value)
147+
..smallIconResourceName.equals('zulip_notification')
148+
..extras.isNull()
149+
..groupKey.equals(expectedGroupKey)
150+
..isGroupSummary.equals(true)
151+
..inboxStyle.which((it) => it.isNotNull()
152+
..summaryText.equals(data.realmUri.toString())
153+
)
154+
..autoCancel.equals(true)
155+
..contentIntent.isNull()
156+
]);
132157
}
133158

134159
Future<void> checkNotifications(FakeAsync async, MessageFcmMessage data, {
@@ -376,10 +401,18 @@ extension on Subject<AndroidNotificationHostApiNotifyCall> {
376401
Subject<String?> get contentTitle => has((x) => x.contentTitle, 'contentTitle');
377402
Subject<Map<String?, String?>?> get extras => has((x) => x.extras, 'extras');
378403
Subject<String?> get smallIconResourceName => has((x) => x.smallIconResourceName, 'smallIconResourceName');
404+
Subject<String?> get groupKey => has((x) => x.groupKey, 'groupKey');
405+
Subject<bool?> get isGroupSummary => has((x) => x.isGroupSummary, 'isGroupSummary');
406+
Subject<InboxStyle?> get inboxStyle => has((x) => x.inboxStyle, 'inboxStyle');
407+
Subject<bool?> get autoCancel => has((x) => x.autoCancel, 'autoCancel');
379408
}
380409

381410
extension on Subject<PendingIntent> {
382411
Subject<int> get requestCode => has((x) => x.requestCode, 'requestCode');
383412
Subject<String> get intentPayload => has((x) => x.intentPayload, 'intentPayload');
384413
Subject<int> get flags => has((x) => x.flags, 'flags');
385414
}
415+
416+
extension on Subject<InboxStyle> {
417+
Subject<String> get summaryText => has((x) => x.summaryText, 'summaryText');
418+
}

0 commit comments

Comments
 (0)