Skip to content

Commit 3f09922

Browse files
notif: Handle remove notification message
This change is similar to existing implementation in zulip-mobile: https://github.com/zulip/zulip-mobile/blob/2217c858e207f9f092651dd853051843c3f04422/android/app/src/main/java/com/zulipmobile/notifications/NotificationUiManager.kt#L116-L168 Fixes: #341 Co-authored-by: Greg Price <[email protected]>
1 parent f230423 commit 3f09922

File tree

3 files changed

+325
-11
lines changed

3 files changed

+325
-11
lines changed

lib/notifications/display.dart

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import 'package:http/http.dart' as http;
55
import 'package:collection/collection.dart';
66
import 'package:crypto/crypto.dart';
77
import 'package:flutter/foundation.dart';
8-
import 'package:flutter/widgets.dart';
8+
import 'package:flutter/widgets.dart' hide Notification;
99
import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Person;
1010

1111
import '../api/notifications.dart';
@@ -84,7 +84,7 @@ class NotificationDisplayManager {
8484
static void onFcmMessage(FcmMessage data, Map<String, dynamic> dataJson) {
8585
switch (data) {
8686
case MessageFcmMessage(): _onMessageFcmMessage(data, dataJson);
87-
case RemoveFcmMessage(): break; // TODO(#341) handle
87+
case RemoveFcmMessage(): _onRemoveFcmMessage(data);
8888
case UnexpectedFcmMessage(): break; // TODO(log)
8989
}
9090
}
@@ -155,6 +155,10 @@ class NotificationDisplayManager {
155155

156156
messagingStyle: messagingStyle,
157157
number: messagingStyle.messages.length,
158+
extras: {
159+
// Used to decide when a `RemoveFcmMessage` event should clear this notification.
160+
kExtraZulipMessageId: data.zulipMessageId.toString(),
161+
},
158162

159163
contentIntent: PendingIntent(
160164
// TODO make intent URLs distinct, instead of requestCode
@@ -202,6 +206,67 @@ class NotificationDisplayManager {
202206
);
203207
}
204208

209+
static void _onRemoveFcmMessage(RemoveFcmMessage data) async {
210+
assert(debugLog('notif remove zulipMessageIds: ${data.zulipMessageIds}'));
211+
212+
final groupKey = _groupKey(data);
213+
final activeNotifications =
214+
await ZulipBinding.instance.androidNotificationHost.getActiveNotifications(
215+
desiredExtras: [kExtraZulipMessageId]);
216+
217+
var haveRemaining = false;
218+
for (final statusBarNotification in activeNotifications) {
219+
if (statusBarNotification == null) continue; // TODO(pigeon) eliminate this case
220+
final notification = statusBarNotification.notification;
221+
222+
// Sadly we don't get toString on Pigeon data classes: flutter#59027
223+
assert(debugLog(' existing notif'
224+
' id: ${statusBarNotification.id}, tag: ${statusBarNotification.tag},'
225+
' notification: (group: ${notification.group}, extras: ${notification.extras}))'));
226+
227+
// Don't act on notifications that are for other Zulip accounts/identities.
228+
if (notification.group != groupKey) continue;
229+
230+
// Don't act on the summary notification for the group.
231+
if (statusBarNotification.tag == groupKey) continue;
232+
233+
final lastMessageIdStr = notification.extras[kExtraZulipMessageId];
234+
assert(lastMessageIdStr != null);
235+
if (lastMessageIdStr == null) continue; // TODO(log)
236+
final lastMessageId = int.parse(lastMessageIdStr, radix: 10);
237+
if (data.zulipMessageIds.contains(lastMessageId)) {
238+
// The latest Zulip message in this conversation was read.
239+
// That's our cue to cancel the notification for the conversation.
240+
await ZulipBinding.instance.androidNotificationHost
241+
.cancel(tag: statusBarNotification.tag, id: statusBarNotification.id);
242+
assert(debugLog(' … notif cancelled.'));
243+
} else {
244+
// This notification is for another conversation that's still unread.
245+
// We won't cancel the summary notification.
246+
haveRemaining = true;
247+
}
248+
}
249+
250+
if (!haveRemaining) {
251+
// The notification group is now empty; it had no notifications we didn't
252+
// just cancel, except the summary notification. Cancel that one too.
253+
//
254+
// Even though we enable the `autoCancel` flag for summary notification
255+
// during creation, the summary notification doesn't get auto canceled if
256+
// child notifications are canceled programatically as done above.
257+
await ZulipBinding.instance.androidNotificationHost
258+
.cancel(tag: groupKey, id: notificationIdAsHashOf(groupKey));
259+
}
260+
}
261+
262+
/// The key for the message-id entry in [Notification.extras] metadata.
263+
///
264+
/// Currently, it is used to store the message-id in the respective
265+
/// notification which is later fetched to determine if a [RemoveFcmMessage]
266+
/// event should clear that specific notification.
267+
@visibleForTesting
268+
static const kExtraZulipMessageId = 'zulipMessageId';
269+
205270
/// A notification ID, derived as a hash of the given string key.
206271
///
207272
/// The result fits in 31 bits, the size of a nonnegative Java `int`,

test/model/binding.dart

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -555,10 +555,14 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi {
555555
}
556556
List<AndroidNotificationHostApiNotifyCall> _notifyCalls = [];
557557

558+
Iterable<StatusBarNotification> get activeNotifications => _activeNotifications.values;
559+
final Map<(int, String?), StatusBarNotification> _activeNotifications = {};
560+
558561
final Map<String, MessagingStyle?> _activeNotificationsMessagingStyle = {};
559562

560563
/// Clears all active notifications that have been created via [notify].
561564
void clearActiveNotifications() {
565+
_activeNotifications.clear();
562566
_activeNotificationsMessagingStyle.clear();
563567
}
564568

@@ -599,6 +603,11 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi {
599603
));
600604

601605
if (tag != null) {
606+
_activeNotifications[(id, tag)] = StatusBarNotification(
607+
id: id,
608+
notification: Notification(group: groupKey ?? '', extras: extras ?? {}),
609+
tag: tag);
610+
602611
_activeNotificationsMessagingStyle[tag] = messagingStyle == null
603612
? null
604613
: MessagingStyle(
@@ -622,15 +631,21 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi {
622631
_activeNotificationsMessagingStyle[tag];
623632

624633
@override
625-
Future<List<StatusBarNotification?>> getActiveNotifications({required List<String?> desiredExtras}) {
626-
// TODO: implement getActiveNotifications
627-
throw UnimplementedError();
634+
Future<List<StatusBarNotification?>> getActiveNotifications({required List<String?> desiredExtras}) async {
635+
return _activeNotifications.values.map((statusNotif) {
636+
final notificationExtras = statusNotif.notification.extras;
637+
statusNotif.notification.extras = Map.fromEntries(
638+
desiredExtras
639+
.map((key) => MapEntry(key, notificationExtras[key]))
640+
.where((entry) => entry.value != null)
641+
);
642+
return statusNotif;
643+
}).toList(growable: false);
628644
}
629645

630646
@override
631-
Future<void> cancel({String? tag, required int id}) {
632-
// TODO: implement cancel
633-
throw UnimplementedError();
647+
Future<void> cancel({String? tag, required int id}) async {
648+
_activeNotifications.remove((id, tag));
634649
}
635650
}
636651

0 commit comments

Comments
 (0)