Skip to content

Commit 973d830

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: zulip#569 Fixes: zulip#571
1 parent 6d14d06 commit 973d830

File tree

2 files changed

+67
-19
lines changed

2 files changed

+67
-19
lines changed

lib/notifications/display.dart

Lines changed: 22 additions & 5 deletions
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+
tag: groupKey,
150+
channelId: NotificationChannelManager.kChannelId,
151+
groupKey: groupKey,
152+
isGroupSummary: true,
153+
154+
color: kZulipBrandColor.value,
155+
// TODO vary notification icon for debug
156+
smallIconResourceName: 'zulip_notification', // This name must appear in keep.xml too: https://github.com/zulip/zulip-flutter/issues/528
157+
inboxStyle: InboxStyle(
158+
// TODO(#570) Show organization name, not URL
159+
summaryText: data.realmUri.toString()),
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

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,24 +111,47 @@ 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+
.deepEquals(<Condition<Object?>>[
122+
(it) => it.isA<AndroidNotificationHostApiNotifyCall>()
123+
..id.equals(expectedId)
124+
..tag.equals(expectedTag)
125+
..channelId.equals(NotificationChannelManager.kChannelId)
126+
..contentTitle.equals(expectedTitle)
127+
..contentText.equals(data.content)
128+
..color.equals(kZulipBrandColor.value)
129+
..smallIconResourceName.equals('zulip_notification')
130+
..extras.isNull()
131+
..groupKey.equals(expectedGroupKey)
132+
..isGroupSummary.isNull()
133+
..inboxStyle.isNull()
134+
..autoCancel.isNull()
135+
..contentIntent.which((it) => it.isNotNull()
136+
..requestCode.equals(expectedId)
137+
..flags.equals(expectedIntentFlags)
138+
..intentPayload.equals(jsonEncode(data.toJson()))),
139+
(it) => it.isA<AndroidNotificationHostApiNotifyCall>()
140+
..id.equals(NotificationDisplayManager.notificationIdAsHashOf(expectedGroupKey))
141+
..tag.equals(expectedGroupKey)
142+
..channelId.equals(NotificationChannelManager.kChannelId)
143+
..contentTitle.isNull()
144+
..contentText.isNull()
145+
..color.equals(kZulipBrandColor.value)
146+
..smallIconResourceName.equals('zulip_notification')
147+
..extras.isNull()
148+
..groupKey.equals(expectedGroupKey)
149+
..isGroupSummary.equals(true)
150+
..inboxStyle.which((it) => it.isNotNull()
151+
..summaryText.equals(data.realmUri.toString()))
152+
..autoCancel.isNull()
153+
..contentIntent.isNull()
154+
]);
132155
}
133156

134157
Future<void> checkNotifications(FakeAsync async, MessageFcmMessage data, {
@@ -369,12 +392,16 @@ extension AndroidNotificationChannelChecks on Subject<AndroidNotificationChannel
369392
extension on Subject<AndroidNotificationHostApiNotifyCall> {
370393
Subject<String?> get tag => has((x) => x.tag, 'tag');
371394
Subject<int> get id => has((x) => x.id, 'id');
395+
Subject<bool?> get autoCancel => has((x) => x.autoCancel, 'autoCancel');
372396
Subject<String> get channelId => has((x) => x.channelId, 'channelId');
373397
Subject<int?> get color => has((x) => x.color, 'color');
374398
Subject<PendingIntent?> get contentIntent => has((x) => x.contentIntent, 'contentIntent');
375399
Subject<String?> get contentText => has((x) => x.contentText, 'contentText');
376400
Subject<String?> get contentTitle => has((x) => x.contentTitle, 'contentTitle');
377401
Subject<Map<String?, String?>?> get extras => has((x) => x.extras, 'extras');
402+
Subject<String?> get groupKey => has((x) => x.groupKey, 'groupKey');
403+
Subject<InboxStyle?> get inboxStyle => has((x) => x.inboxStyle, 'inboxStyle');
404+
Subject<bool?> get isGroupSummary => has((x) => x.isGroupSummary, 'isGroupSummary');
378405
Subject<String?> get smallIconResourceName => has((x) => x.smallIconResourceName, 'smallIconResourceName');
379406
}
380407

@@ -383,3 +410,7 @@ extension on Subject<PendingIntent> {
383410
Subject<String> get intentPayload => has((x) => x.intentPayload, 'intentPayload');
384411
Subject<int> get flags => has((x) => x.flags, 'flags');
385412
}
413+
414+
extension on Subject<InboxStyle> {
415+
Subject<String> get summaryText => has((x) => x.summaryText, 'summaryText');
416+
}

0 commit comments

Comments
 (0)