Skip to content

Commit

Permalink
notif: Create group summary notification
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rajveermalviya committed Jun 19, 2024
1 parent 6d14d06 commit 973d830
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 19 deletions.
27 changes: 22 additions & 5 deletions lib/notifications/display.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class NotificationDisplayManager {
}
}

static void _onMessageFcmMessage(MessageFcmMessage data, Map<String, dynamic> dataJson) {
static Future<void> _onMessageFcmMessage(MessageFcmMessage data, Map<String, dynamic> dataJson) async {
assert(debugLog('notif message content: ${data.content}'));
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
final title = switch (data.recipient) {
Expand All @@ -103,13 +103,16 @@ class NotificationDisplayManager {
FcmMessageDmRecipient() =>
data.senderFullName,
};
final conversationKey = _conversationKey(data);
ZulipBinding.instance.androidNotificationHost.notify(
final groupKey = _groupKey(data);
final conversationKey = _conversationKey(data, groupKey);

await ZulipBinding.instance.androidNotificationHost.notify(
// TODO the notification ID can be constant, instead of matching requestCode
// (This is a legacy of `flutter_local_notifications`.)
id: notificationIdAsHashOf(conversationKey),
tag: conversationKey,
channelId: NotificationChannelManager.kChannelId,
groupKey: groupKey,

contentTitle: title,
contentText: data.content,
Expand Down Expand Up @@ -140,6 +143,21 @@ class NotificationDisplayManager {
// (This is a legacy of `flutter_local_notifications`.)
),
);

await ZulipBinding.instance.androidNotificationHost.notify(
id: notificationIdAsHashOf(groupKey),
tag: groupKey,
channelId: NotificationChannelManager.kChannelId,
groupKey: groupKey,
isGroupSummary: true,

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
inboxStyle: InboxStyle(
// TODO(#570) Show organization name, not URL
summaryText: data.realmUri.toString()),
);
}

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

static String _conversationKey(MessageFcmMessage data) {
final groupKey = _groupKey(data);
static String _conversationKey(MessageFcmMessage data, String groupKey) {
final conversation = switch (data.recipient) {
FcmMessageStreamRecipient(:var streamId, :var topic) => 'stream:$streamId:$topic',
FcmMessageDmRecipient(:var allRecipientIds) => 'dm:${allRecipientIds.join(',')}',
Expand Down
59 changes: 45 additions & 14 deletions test/notifications/display_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,24 +111,47 @@ void main() {
required String expectedTagComponent,
}) {
final expectedTag = '${data.realmUri}|${data.userId}|$expectedTagComponent';
final expectedGroupKey = '${data.realmUri}|${data.userId}';
final expectedId =
NotificationDisplayManager.notificationIdAsHashOf(expectedTag);
const expectedIntentFlags =
PendingIntentFlag.immutable | PendingIntentFlag.updateCurrent;
check(testBinding.androidNotificationHost.takeNotifyCalls()).single
..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()
..contentIntent.which((it) => it.isNotNull()
..requestCode.equals(expectedId)
..flags.equals(expectedIntentFlags)
..intentPayload.equals(jsonEncode(data.toJson()))
);

check(testBinding.androidNotificationHost.takeNotifyCalls())
.deepEquals(<Condition<Object?>>[
(it) => it.isA<AndroidNotificationHostApiNotifyCall>()
..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()
..groupKey.equals(expectedGroupKey)
..isGroupSummary.isNull()
..inboxStyle.isNull()
..autoCancel.isNull()
..contentIntent.which((it) => it.isNotNull()
..requestCode.equals(expectedId)
..flags.equals(expectedIntentFlags)
..intentPayload.equals(jsonEncode(data.toJson()))),
(it) => it.isA<AndroidNotificationHostApiNotifyCall>()
..id.equals(NotificationDisplayManager.notificationIdAsHashOf(expectedGroupKey))
..tag.equals(expectedGroupKey)
..channelId.equals(NotificationChannelManager.kChannelId)
..contentTitle.isNull()
..contentText.isNull()
..color.equals(kZulipBrandColor.value)
..smallIconResourceName.equals('zulip_notification')
..extras.isNull()
..groupKey.equals(expectedGroupKey)
..isGroupSummary.equals(true)
..inboxStyle.which((it) => it.isNotNull()
..summaryText.equals(data.realmUri.toString()))
..autoCancel.isNull()
..contentIntent.isNull()
]);
}

Future<void> checkNotifications(FakeAsync async, MessageFcmMessage data, {
Expand Down Expand Up @@ -369,12 +392,16 @@ extension AndroidNotificationChannelChecks on Subject<AndroidNotificationChannel
extension on Subject<AndroidNotificationHostApiNotifyCall> {
Subject<String?> get tag => has((x) => x.tag, 'tag');
Subject<int> get id => has((x) => x.id, 'id');
Subject<bool?> get autoCancel => has((x) => x.autoCancel, 'autoCancel');
Subject<String> get channelId => has((x) => x.channelId, 'channelId');
Subject<int?> get color => has((x) => x.color, 'color');
Subject<PendingIntent?> get contentIntent => has((x) => x.contentIntent, 'contentIntent');
Subject<String?> get contentText => has((x) => x.contentText, 'contentText');
Subject<String?> get contentTitle => has((x) => x.contentTitle, 'contentTitle');
Subject<Map<String?, String?>?> get extras => has((x) => x.extras, 'extras');
Subject<String?> get groupKey => has((x) => x.groupKey, 'groupKey');
Subject<InboxStyle?> get inboxStyle => has((x) => x.inboxStyle, 'inboxStyle');
Subject<bool?> get isGroupSummary => has((x) => x.isGroupSummary, 'isGroupSummary');
Subject<String?> get smallIconResourceName => has((x) => x.smallIconResourceName, 'smallIconResourceName');
}

Expand All @@ -383,3 +410,7 @@ extension on Subject<PendingIntent> {
Subject<String> get intentPayload => has((x) => x.intentPayload, 'intentPayload');
Subject<int> get flags => has((x) => x.flags, 'flags');
}

extension on Subject<InboxStyle> {
Subject<String> get summaryText => has((x) => x.summaryText, 'summaryText');
}

0 comments on commit 973d830

Please sign in to comment.