From f466d2ecff0a92795fe79c2a20ecde6d31a9201b Mon Sep 17 00:00:00 2001 From: rajveermalviya Date: Fri, 22 Mar 2024 02:48:01 +0530 Subject: [PATCH] notif: Messaging style notifications on Android Fixes: #128 Fixes: #569 Fixes: #579 --- lib/notifications.dart | 84 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/lib/notifications.dart b/lib/notifications.dart index d81e511038..f845fecdd7 100644 --- a/lib/notifications.dart +++ b/lib/notifications.dart @@ -6,6 +6,7 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:http/http.dart' as http; import 'api/core.dart'; import 'api/notifications.dart'; @@ -314,8 +315,64 @@ class NotificationDisplayManager { } static Future _showNotificationForAndroid(String title, MessageFcmMessage data, Map dataJson) async { + final groupKey = _groupKey(data); final conversationKey = _conversationKey(data); - ZulipBinding.instance.notifications.show( + + final oldNotification = await _getActiveNotificationByTag(conversationKey); + final oldStyleInfo = oldNotification?.id != null + ? await ZulipBinding.instance.notifications + .resolvePlatformSpecificImplementation() + ?.getActiveNotificationMessagingStyle(oldNotification!.id!) + : null; + + final messages = oldStyleInfo?.messages ?? []; + final bitmap = await _fetchBitmap(data.senderAvatarUrl); + messages.add(Message( + data.content, + DateTime.fromMillisecondsSinceEpoch(data.time * 1000), + Person( + icon: bitmap != null ? ByteArrayAndroidIcon(bitmap) : null, + key: data.senderId.toString(), + name: data.senderFullName, + ), + )); + + final messagingStyle = MessagingStyleInformation( + const Person(name: 'You'), // TODO(i18n) + conversationTitle: title, + groupConversation: switch (data.recipient) { + FcmMessageStreamRecipient() => true, + FcmMessageDmRecipient(:var allRecipientIds) when allRecipientIds.length > 2 => true, + FcmMessageDmRecipient() => false, + }, + messages: messages, + ); + + // Show the summary notification + await ZulipBinding.instance.notifications.show( + notificationIdAsHashOf(groupKey), + null, + null, + NotificationDetails(android: AndroidNotificationDetails( + NotificationChannelManager.kChannelId, + // This [FlutterLocalNotificationsPlugin.show] call can potentially create + // a new channel, if our channel doesn't already exist. That *shouldn't* + // happen; if it does, it won't get the right settings. Set the channel + // name in that case to something that has a chance of warning the user, + // and that can serve as a signature to diagnose the situation in support. + // But really we should fix flutter_local_notifications to not do that + // (see issue linked below), or replace that package entirely (#351). + '(Zulip internal error)', // TODO never implicitly create channel: https://github.com/MaikuB/flutter_local_notifications/issues/2135 + tag: groupKey, + color: kZulipBrandColor, + // TODO vary notification icon for debug + icon: 'zulip_notification', // This name must appear in keep.xml too: https://github.com/zulip/zulip-flutter/issues/528 + groupKey: groupKey, + setAsGroupSummary: true, + styleInformation: InboxStyleInformation([], summaryText: data.realmUri.toString()), + ))); + + await ZulipBinding.instance.notifications.show( // When creating the PendingIntent for the user to open the notification, // the plugin makes the underlying Intent objects look the same. // They differ in their extras, but that doesn't count: @@ -327,8 +384,8 @@ class NotificationDisplayManager { // notifications can lead to the right conversations when opened. // So, use a hash of the conversation key. notificationIdAsHashOf(conversationKey), - title, - data.content, + null, + null, payload: jsonEncode(dataJson), NotificationDetails(android: AndroidNotificationDetails( NotificationChannelManager.kChannelId, @@ -344,13 +401,32 @@ class NotificationDisplayManager { color: kZulipBrandColor, // TODO vary notification icon for debug icon: 'zulip_notification', // This name must appear in keep.xml too: https://github.com/zulip/zulip-flutter/issues/528 - // TODO(#128) inbox-style + groupKey: groupKey, + styleInformation: messagingStyle, + number: messagingStyle.messages?.length, // TODO plugin sets PendingIntent.FLAG_UPDATE_CURRENT; is that OK? // TODO plugin doesn't set our Intent flags; is that OK? ))); } + static Future _getActiveNotificationByTag(String tag) async { + final activeNotifications = await ZulipBinding.instance.notifications.getActiveNotifications(); + return activeNotifications.firstWhereOrNull((activeNotification) { + return activeNotification.tag == tag; + }); + } + + static Future _fetchBitmap(Uri url) async { + try { + final resp = await http.get(url); + return resp.bodyBytes; + } catch (e) { + // TODO(log) + return null; + } + } + /// A notification ID, derived as a hash of the given string key. /// /// The result fits in 31 bits, the size of a nonnegative Java `int`,