diff --git a/lib/main.dart b/lib/main.dart index d9ac10b4c3..c049187fd6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,7 +38,7 @@ void main() async { // In the background fetch mode we do not want to waste ressources with // starting the Flutter engine but process incoming push notifications. - BackgroundPush.clientOnly(clients.first); + BackgroundPush.clientsOnly(clients); // To start the flutter engine afterwards we add an custom observer. WidgetsBinding.instance.addObserver(AppStarter(clients, store)); Logs().i( diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index fce1d38a4a..2bb1072f93 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -392,7 +392,9 @@ class ChatController extends State _setReadMarkerFuture = null; }); if (eventId == null || eventId == timeline.room.lastEvent?.eventId) { - Matrix.of(context).backgroundPush?.cancelNotification(roomId); + Matrix.of(context) + .backgroundPush + ?.cancelNotification(Matrix.of(context).client, roomId); } } diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 553c979df5..eda8a566ab 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -516,7 +516,9 @@ class ChatListController extends State if (mounted) { searchServer = Matrix.of(context).store.getString(_serverStoreNamespace); - Matrix.of(context).backgroundPush?.setupPush(); + Matrix.of(context) + .backgroundPush + ?.setupPush(Matrix.of(context).widget.clients); UpdateNotifier.showUpdateSnackBar(context); } diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 1ba2659a6b..3a4e3a55c4 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -49,7 +49,7 @@ class BackgroundPush { static BackgroundPush? _instance; final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - Client client; + List clients; MatrixState? matrix; String? _fcmToken; void Function(String errorMsg, {Uri? link})? onFcmError; @@ -81,22 +81,26 @@ class BackgroundPush { ); Logs().v('Flutter Local Notifications initialized'); firebase?.setListeners( - onMessage: (message) => pushHelper( + onMessage: (message) => PushHelper.pushHelper( PushNotification.fromJson( Map.from(message['data'] ?? message), ), - client: client, + clients: clients, + //TODO: figure out if firebase supports + // multiple instances + instance: clients.first.clientName, l10n: l10n, activeRoomId: matrix?.activeRoomId, + activeClient: matrix?.client, flutterLocalNotificationsPlugin: _flutterLocalNotificationsPlugin, ), ); if (Platform.isAndroid) { await UnifiedPush.initialize( - onNewEndpoint: _newUpEndpoint, - onRegistrationFailed: _upUnregistered, - onUnregistered: _upUnregistered, - onMessage: _onUpMessage, + onNewEndpoint: _newUPEndpoint, + onRegistrationFailed: _onUPUnregistered, + onUnregistered: _onUPUnregistered, + onMessage: _onUPMessage, ); } } catch (e, s) { @@ -104,26 +108,26 @@ class BackgroundPush { } } - BackgroundPush._(this.client) { + BackgroundPush._(this.clients) { _init(); } - factory BackgroundPush.clientOnly(Client client) { - return _instance ??= BackgroundPush._(client); + factory BackgroundPush.clientsOnly(List clients) { + return _instance ??= BackgroundPush._(clients); } factory BackgroundPush( MatrixState matrix, { final void Function(String errorMsg, {Uri? link})? onFcmError, }) { - final instance = BackgroundPush.clientOnly(matrix.client); + final instance = BackgroundPush.clientsOnly(matrix.widget.clients); instance.matrix = matrix; // ignore: prefer_initializing_formals instance.onFcmError = onFcmError; return instance; } - Future cancelNotification(String roomId) async { + Future cancelNotification(Client client, String roomId) async { Logs().v('Cancel notification for room', roomId); await _flutterLocalNotificationsPlugin.cancel(roomId.hashCode); @@ -146,6 +150,7 @@ class BackgroundPush { String? token, Set? oldTokens, bool useDeviceSpecificAppId = false, + required Client client, }) async { if (PlatformInfos.isIOS) { await firebase?.requestPermission(); @@ -192,7 +197,7 @@ class BackgroundPush { )) { Logs().i('[Push] Pusher already set'); } else { - Logs().i('Need to set new pusher'); + Logs().i('[Push] Need to set new pusher'); oldTokens.add(token); if (client.isLogged()) { setNewPusher = true; @@ -246,9 +251,32 @@ class BackgroundPush { static bool _wentToRoomOnStartup = false; - Future setupPush() async { + Future setupPush(List clients) async { Logs().d("SetupPush"); - if (client.onLoginStateChanged.value != LoginState.loggedIn || + + { + // migrate single client push settings to multiclient settings + final endpoint = matrix!.store.getString(SettingKeys.unifiedPushEndpoint); + if (endpoint != null) { + matrix!.store.setString( + clients.first.clientName + SettingKeys.unifiedPushEndpoint, + endpoint, + ); + matrix!.store.remove(SettingKeys.unifiedPushEndpoint); + } + + final registered = + matrix!.store.getBool(SettingKeys.unifiedPushRegistered); + if (registered != null) { + matrix!.store.setBool( + clients.first.clientName + SettingKeys.unifiedPushRegistered, + registered, + ); + matrix!.store.remove(SettingKeys.unifiedPushRegistered); + } + } + + if (clients.first.onLoginStateChanged.value != LoginState.loggedIn || !PlatformInfos.isMobile || matrix == null) { return; @@ -260,7 +288,7 @@ class BackgroundPush { } if (!PlatformInfos.isIOS && (await UnifiedPush.getDistributors()).isNotEmpty) { - await setupUp(); + await setupUP(clients); } else { await setupFirebase(); } @@ -316,42 +344,66 @@ class BackgroundPush { await setupPusher( gatewayUrl: AppConfig.pushNotificationsGatewayUrl, token: _fcmToken, + client: clients.first, // Workaround: see todo in _init ); } Future goToRoom(NotificationResponse? response) async { try { - final roomId = response?.payload; - Logs().v('[Push] Attempting to go to room $roomId...'); - if (roomId == null) { + final payloadEncoded = response?.payload; + if (payloadEncoded == null || payloadEncoded.isEmpty) { + return; + } + final payload = + NotificationResponsePayload.fromJson(jsonDecode(payloadEncoded)); + if (payload.roomId.isEmpty) { + return; + } + Logs().v('[Push] Attempting to go to room ${payload.roomId}...'); + + Client? client; + for (final c in clients) { + if (c.clientName == payload.clientName) { + client = c; + break; + } + } + if (client == null) { + Logs() + .w('[Push] No client could be found for room ${payload.roomId}...'); return; } + if (matrix!.client != client) { + matrix!.setActiveClient(client); + } + await client.roomsLoading; await client.accountDataLoading; - if (client.getRoomById(roomId) == null) { + if (client.getRoomById(payload.roomId) == null) { await client - .waitForRoomInSync(roomId) + .waitForRoomInSync(payload.roomId) .timeout(const Duration(seconds: 30)); } FluffyChatApp.router.go( - client.getRoomById(roomId)?.membership == Membership.invite + client.getRoomById(payload.roomId)?.membership == Membership.invite ? '/rooms' - : '/rooms/$roomId', + : '/rooms/${payload.roomId}', ); } catch (e, s) { Logs().e('[Push] Failed to open room', e, s); } } - Future setupUp() async { - await UnifiedPushUi(matrix!.context, ["default"], UPFunctions()) + Future setupUP(List clients) async { + final names = clients.map((c) => c.clientName); + await UnifiedPushUi(matrix!.context, List.from(names), UPFunctions()) .registerAppWithDialog(); } - Future _newUpEndpoint(String newEndpoint, String i) async { + Future _newUPEndpoint(String newEndpoint, String instance) async { upAction = true; if (newEndpoint.isEmpty) { - await _upUnregistered(i); + await _onUPUnregistered(instance); return; } var endpoint = @@ -377,48 +429,67 @@ class BackgroundPush { '[Push] No self-hosted unified push gateway present: $newEndpoint', ); } - Logs().i('[Push] UnifiedPush using endpoint $endpoint'); + Logs().i('[Push] UnifiedPush $instance using endpoint $endpoint'); final oldTokens = {}; try { final fcmToken = await firebase?.getToken(); oldTokens.add(fcmToken); } catch (_) {} + final client = clientFromInstance(instance, clients); + if (client == null) { + Logs().e("Not client found for $instance"); + return; + } await setupPusher( gatewayUrl: endpoint, token: newEndpoint, oldTokens: oldTokens, useDeviceSpecificAppId: true, + client: client, + ); + await matrix?.store.setString( + client.clientName + SettingKeys.unifiedPushEndpoint, + newEndpoint, ); - await matrix?.store.setString(SettingKeys.unifiedPushEndpoint, newEndpoint); - await matrix?.store.setBool(SettingKeys.unifiedPushRegistered, true); + await matrix?.store + .setBool(client.clientName + SettingKeys.unifiedPushRegistered, true); } - Future _upUnregistered(String i) async { + Future _onUPUnregistered(String instance) async { upAction = true; + final client = clientFromInstance(instance, clients); + if (client == null) { + return; + } Logs().i('[Push] Removing UnifiedPush endpoint...'); - final oldEndpoint = - matrix?.store.getString(SettingKeys.unifiedPushEndpoint); - await matrix?.store.setBool(SettingKeys.unifiedPushRegistered, false); - await matrix?.store.remove(SettingKeys.unifiedPushEndpoint); + final oldEndpoint = matrix?.store + .getString(client.clientName + SettingKeys.unifiedPushEndpoint); + await matrix?.store + .setBool(client.clientName + SettingKeys.unifiedPushRegistered, false); + await matrix?.store + .remove(client.clientName + SettingKeys.unifiedPushEndpoint); if (oldEndpoint?.isNotEmpty ?? false) { // remove the old pusher await setupPusher( oldTokens: {oldEndpoint}, + client: client, ); } } - Future _onUpMessage(Uint8List message, String i) async { + Future _onUPMessage(Uint8List message, String instance) async { upAction = true; final data = Map.from( json.decode(utf8.decode(message))['notification'], ); // UP may strip the devices list data['devices'] ??= []; - await pushHelper( + await PushHelper.pushHelper( PushNotification.fromJson(data), - client: client, + clients: clients, + instance: instance, l10n: l10n, + activeClient: matrix?.client, activeRoomId: matrix?.activeRoomId, flutterLocalNotificationsPlugin: _flutterLocalNotificationsPlugin, ); @@ -447,3 +518,12 @@ class UPFunctions extends UnifiedPushFunctions { await UnifiedPush.saveDistributor(distributor); } } + +Client? clientFromInstance(String? instance, List clients) { + for (final c in clients) { + if (c.clientName == instance) { + return c; + } + } + return null; +} diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index 05f0e90c71..d0e21b749a 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:ui'; import 'package:flutter/foundation.dart'; @@ -11,42 +13,134 @@ import 'package:matrix/matrix.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/background_push.dart' show clientFromInstance; import 'package:fluffychat/utils/client_download_content_extension.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/voip/callkeep_manager.dart'; -Future pushHelper( - PushNotification notification, { - Client? client, - L10n? l10n, - String? activeRoomId, - required FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, -}) async { - try { - await _tryPushHelper( - notification, - client: client, - l10n: l10n, - activeRoomId: activeRoomId, - flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin, - ); - } catch (e, s) { +class PushHelper { + final PushNotification notification; + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + late Client client; + late Event event; + late bool isBackgroundMessage; + L10n? l10n; + + PushHelper._(this.notification, this.flutterLocalNotificationsPlugin); + + static Future pushHelper( + PushNotification notification, { + List? clients, + L10n? l10n, + String? activeRoomId, + Client? activeClient, + required FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, + required String instance, + }) async { + try { + final handler = await _newPushHandler( + notification, + clients: clients, + l10n: l10n, + activeRoomId: activeRoomId, + activeClient: activeClient, + flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin, + instance: instance, + ); + await handler?._showNotification(); + } catch (e) { + rethrow; + } + } + + static FutureOr _newPushHandler( + PushNotification notification, { + List? clients, + L10n? l10n, + String? activeRoomId, + Client? activeClient, + required FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, + required String instance, + }) async { + final helper = PushHelper._(notification, flutterLocalNotificationsPlugin); + + try { + helper.isBackgroundMessage = clients == null; + Logs().v( + 'Push helper has been started (background=${helper.isBackgroundMessage}).', + notification.toJson(), + ); + + clients ??= (await ClientManager.getClients( + initialize: false, + store: await SharedPreferences.getInstance(), + )); + final client = clientFromInstance(instance, clients); + if (client == null) { + Logs().e("Not client could be found for $instance"); + return null; + } + helper.client = client; + + if (_isInForeground(notification, activeRoomId, activeClient, client)) { + Logs().v('Room is in foreground. Stop push helper here.'); + return null; + } + + final event = await client.getEventByPushNotification( + notification, + storeInDatabase: helper.isBackgroundMessage, + ); + + if (event == null) { + Logs().v('Notification is a clearing indicator.'); + if (notification.counts?.unread == null || + notification.counts?.unread == 0) { + await flutterLocalNotificationsPlugin.cancelAll(); + } else { + // Make sure client is fully loaded and synced before dismiss notifications: + await client.roomsLoading; + await client.oneShotSync(); + final activeNotifications = + await flutterLocalNotificationsPlugin.getActiveNotifications(); + for (final activeNotification in activeNotifications) { + final room = client.rooms.singleWhereOrNull( + (room) => room.id.hashCode == activeNotification.id, + ); + if (room == null || !room.isUnreadOrInvited) { + flutterLocalNotificationsPlugin.cancel(activeNotification.id!); + } + } + } + return null; + } + helper.event = event; + + Logs().v('Push helper got notification event of type ${event.type}.'); + return helper; + } catch (e, s) { + await helper._crashHandler(e, s); + rethrow; + } + } + + _crashHandler(e, s) async { Logs().v('Push Helper has crashed!', e, s); l10n ??= await lookupL10n(const Locale('en')); flutterLocalNotificationsPlugin.show( notification.roomId?.hashCode ?? 0, - l10n.newMessageInFluffyChat, - l10n.openAppToReadMessages, + l10n!.newMessageInFluffyChat, + l10n!.openAppToReadMessages, NotificationDetails( iOS: const DarwinNotificationDetails(), android: AndroidNotificationDetails( AppConfig.pushNotificationsChannelId, - l10n.incomingMessages, + l10n!.incomingMessages, number: notification.counts?.unread, - ticker: l10n.unreadChatsInApp( + ticker: l10n!.unreadChatsInApp( AppConfig.applicationName, (notification.counts?.unread ?? 0).toString(), ), @@ -56,271 +150,252 @@ Future pushHelper( ), ), ); - rethrow; } -} -Future _tryPushHelper( - PushNotification notification, { - Client? client, - L10n? l10n, - String? activeRoomId, - required FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, -}) async { - final isBackgroundMessage = client == null; - Logs().v( - 'Push helper has been started (background=$isBackgroundMessage).', - notification.toJson(), - ); - - if (notification.roomId != null && - activeRoomId == notification.roomId && - WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) { - Logs().v('Room is in foreground. Stop push helper here.'); - return; - } + Future _showNotification() async { + try { + if (event.type.startsWith('m.call')) { + // make sure bg sync is on (needed to update hold, unhold events) + // prevent over write from app life cycle change + client.backgroundSync = true; + } - client ??= (await ClientManager.getClients( - initialize: false, - store: await SharedPreferences.getInstance(), - )) - .first; - final event = await client.getEventByPushNotification( - notification, - storeInDatabase: isBackgroundMessage, - ); - - if (event == null) { - Logs().v('Notification is a clearing indicator.'); - if (notification.counts?.unread == null || - notification.counts?.unread == 0) { - await flutterLocalNotificationsPlugin.cancelAll(); - } else { - // Make sure client is fully loaded and synced before dismiss notifications: - await client.roomsLoading; - await client.oneShotSync(); - final activeNotifications = - await flutterLocalNotificationsPlugin.getActiveNotifications(); - for (final activeNotification in activeNotifications) { - final room = client.rooms.singleWhereOrNull( - (room) => room.id.hashCode == activeNotification.id, - ); - if (room == null || !room.isUnreadOrInvited) { - flutterLocalNotificationsPlugin.cancel(activeNotification.id!); - } + if (event.type == EventTypes.CallInvite) { + CallKeepManager().initialize(); + } else if (event.type == EventTypes.CallHangup) { + client.backgroundSync = false; } - } - return; - } - Logs().v('Push helper got notification event of type ${event.type}.'); - if (event.type.startsWith('m.call')) { - // make sure bg sync is on (needed to update hold, unhold events) - // prevent over write from app life cycle change - client.backgroundSync = true; - } + if ((event.type.startsWith('m.call') && + event.type != EventTypes.CallInvite) || + event.type == EventTypes.CallSDPStreamMetadataChangedPrefix) { + Logs().v('Push message was for a call, but not call invite.'); + return; + } - if (event.type == EventTypes.CallInvite) { - CallKeepManager().initialize(); - } else if (event.type == EventTypes.CallHangup) { - client.backgroundSync = false; - } + l10n ??= await L10n.delegate.load(PlatformDispatcher.instance.locale); + final locals = MatrixLocals(l10n!); - if (event.type.startsWith('m.call') && event.type != EventTypes.CallInvite) { - Logs().v('Push message is a m.call but not invite. Do not display.'); - return; - } + // Calculate the body + final body = event.type == EventTypes.Encrypted + ? l10n!.newMessageInFluffyChat + : await event.calcLocalizedBody( + locals, + plaintextBody: true, + withSenderNamePrefix: false, + hideReply: true, + hideEdit: true, + removeMarkdown: true, + ); - if ((event.type.startsWith('m.call') && - event.type != EventTypes.CallInvite) || - event.type == 'org.matrix.call.sdp_stream_metadata_changed') { - Logs().v('Push message was for a call, but not call invite.'); - return; - } + final id = notification.roomId.hashCode; + final title = event.room.getLocalizedDisplayname(locals); + final roomName = event.room.getLocalizedDisplayname(locals); + + var notificationGroupId = + event.room.isDirectChat ? 'directChats' : 'groupChats'; + notificationGroupId += client.clientName; + final groupName = + event.room.isDirectChat ? l10n!.directChats : l10n!.groups; + + final messageRooms = AndroidNotificationChannelGroup( + notificationGroupId, + groupName, + ); + final roomsChannel = AndroidNotificationChannel( + event.room.id, + roomName, + groupId: notificationGroupId, + ); - l10n ??= await L10n.delegate.load(PlatformDispatcher.instance.locale); - final matrixLocals = MatrixLocals(l10n); - - // Calculate the body - final body = event.type == EventTypes.Encrypted - ? l10n.newMessageInFluffyChat - : await event.calcLocalizedBody( - matrixLocals, - plaintextBody: true, - withSenderNamePrefix: false, - hideReply: true, - hideEdit: true, - removeMarkdown: true, - ); - - // The person object for the android message style notification - final avatar = event.room.avatar; - final senderAvatar = event.room.isDirectChat - ? avatar - : event.senderFromMemoryOrFallback.avatarUrl; - - Uint8List? roomAvatarFile, senderAvatarFile; - try { - roomAvatarFile = avatar == null - ? null - : await client - .downloadMxcCached( - avatar, - thumbnailMethod: ThumbnailMethod.scale, - width: 256, - height: 256, - animated: false, - isThumbnail: true, - ) - .timeout(const Duration(seconds: 3)); - } catch (e, s) { - Logs().e('Unable to get avatar picture', e, s); + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannelGroup(messageRooms); + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(roomsChannel); + + final platformChannelSpecifics = await _getPlatformChannelSpecifics( + id, + body, + title, + roomName, + ); + + await flutterLocalNotificationsPlugin.show( + id, + title, + body, + platformChannelSpecifics, + payload: jsonEncode( + NotificationResponsePayload( + event.roomId ?? "", + client.clientName, + ).toJson(), + ), + ); + Logs().v('Push helper has been completed!'); + } catch (e, s) { + await _crashHandler(e, s); + rethrow; + } } - try { - senderAvatarFile = event.room.isDirectChat + + Future _getPlatformChannelSpecifics( + int notificationId, + String notificationBody, + String notificationTitle, + String roomName, + ) async { + // The person object for the android message style notification + final avatar = event.room.avatar; + final senderAvatar = event.room.isDirectChat + ? avatar + : event.senderFromMemoryOrFallback.avatarUrl; + + final roomAvatarFile = await _getAvatarFile(client, avatar); + final senderAvatarFile = event.room.isDirectChat ? roomAvatarFile - : senderAvatar == null + : await _getAvatarFile(client, senderAvatar); + + // Show notification + final newMessage = Message( + notificationBody, + event.originServerTs, + Person( + bot: event.messageType == MessageTypes.Notice, + key: event.senderId, + name: event.senderFromMemoryOrFallback.calcDisplayname(), + icon: senderAvatarFile == null ? null - : await client - .downloadMxcCached( - senderAvatar, - thumbnailMethod: ThumbnailMethod.scale, - width: 256, - height: 256, - animated: false, - isThumbnail: true, - ) - .timeout(const Duration(seconds: 3)); - } catch (e, s) { - Logs().e('Unable to get avatar picture', e, s); - } + : ByteArrayAndroidIcon(senderAvatarFile), + ), + ); - final id = notification.roomId.hashCode; + final messagingStyleInformation = PlatformInfos.isAndroid + ? await AndroidFlutterLocalNotificationsPlugin() + .getActiveNotificationMessagingStyle(notificationId) + : null; + messagingStyleInformation?.messages?.add(newMessage); - // Show notification + if (PlatformInfos.isAndroid && messagingStyleInformation == null) { + await _setShortcut(notificationTitle, roomAvatarFile); + } - final newMessage = Message( - body, - event.originServerTs, - Person( - bot: event.messageType == MessageTypes.Notice, - key: event.senderId, - name: event.senderFromMemoryOrFallback.calcDisplayname(), - icon: senderAvatarFile == null - ? null - : ByteArrayAndroidIcon(senderAvatarFile), - ), - ); - - final messagingStyleInformation = PlatformInfos.isAndroid - ? await AndroidFlutterLocalNotificationsPlugin() - .getActiveNotificationMessagingStyle(id) - : null; - messagingStyleInformation?.messages?.add(newMessage); - - final roomName = event.room.getLocalizedDisplayname(MatrixLocals(l10n)); - - final notificationGroupId = - event.room.isDirectChat ? 'directChats' : 'groupChats'; - final groupName = event.room.isDirectChat ? l10n.directChats : l10n.groups; - - final messageRooms = AndroidNotificationChannelGroup( - notificationGroupId, - groupName, - ); - final roomsChannel = AndroidNotificationChannel( - event.room.id, - roomName, - groupId: notificationGroupId, - ); - - await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.createNotificationChannelGroup(messageRooms); - await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.createNotificationChannel(roomsChannel); - - final androidPlatformChannelSpecifics = AndroidNotificationDetails( - AppConfig.pushNotificationsChannelId, - l10n.incomingMessages, - number: notification.counts?.unread, - category: AndroidNotificationCategory.message, - shortcutId: event.room.id, - styleInformation: messagingStyleInformation ?? - MessagingStyleInformation( - Person( - name: event.senderFromMemoryOrFallback.calcDisplayname(), - icon: roomAvatarFile == null - ? null - : ByteArrayAndroidIcon(roomAvatarFile), - key: event.roomId, - important: event.room.isFavourite, + final androidPlatformChannelSpecifics = AndroidNotificationDetails( + AppConfig.pushNotificationsChannelId, + l10n!.incomingMessages, + number: notification.counts?.unread, + category: AndroidNotificationCategory.message, + shortcutId: event.room.id, + styleInformation: messagingStyleInformation ?? + MessagingStyleInformation( + Person( + name: event.senderFromMemoryOrFallback.calcDisplayname(), + icon: roomAvatarFile == null + ? null + : ByteArrayAndroidIcon(roomAvatarFile), + key: event.roomId, + important: event.room.isFavourite, + ), + conversationTitle: roomName, + groupConversation: !event.room.isDirectChat, + messages: [newMessage], ), - conversationTitle: roomName, - groupConversation: !event.room.isDirectChat, - messages: [newMessage], - ), - ticker: event.calcLocalizedBodyFallback( - matrixLocals, - plaintextBody: true, - withSenderNamePrefix: true, - hideReply: true, - hideEdit: true, - removeMarkdown: true, - ), - importance: Importance.high, - priority: Priority.max, - groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms', - ); - const iOSPlatformChannelSpecifics = DarwinNotificationDetails(); - final platformChannelSpecifics = NotificationDetails( - android: androidPlatformChannelSpecifics, - iOS: iOSPlatformChannelSpecifics, - ); - - final title = event.room.getLocalizedDisplayname(MatrixLocals(l10n)); - - if (PlatformInfos.isAndroid && messagingStyleInformation == null) { - await _setShortcut(event, l10n, title, roomAvatarFile); + ticker: event.calcLocalizedBodyFallback( + MatrixLocals(l10n!), + plaintextBody: true, + withSenderNamePrefix: true, + hideReply: true, + hideEdit: true, + removeMarkdown: true, + ), + importance: Importance.high, + priority: Priority.max, + groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms', + ); + const iOSPlatformChannelSpecifics = DarwinNotificationDetails(); + return NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); } - await flutterLocalNotificationsPlugin.show( - id, - title, - body, - platformChannelSpecifics, - payload: event.roomId, - ); - Logs().v('Push helper has been completed!'); -} + /// Creates a shortcut for Android platform but does not block displaying the + /// notification. This is optional but provides a nicer view of the + /// notification popup. + Future _setShortcut( + String title, + Uint8List? avatarFile, + ) async { + final flutterShortcuts = FlutterShortcuts(); + await flutterShortcuts.initialize(debug: !kReleaseMode); + await flutterShortcuts.pushShortcutItem( + shortcut: ShortcutItem( + id: event.room.id, + action: AppConfig.inviteLinkPrefix + event.room.id, + shortLabel: title, + conversationShortcut: true, + icon: avatarFile == null + ? null + : ShortcutMemoryIcon(jpegImage: avatarFile).toString(), + shortcutIconAsset: avatarFile == null + ? ShortcutIconAsset.androidAsset + : ShortcutIconAsset.memoryAsset, + isImportant: event.room.isFavourite, + ), + ); + } -/// Creates a shortcut for Android platform but does not block displaying the -/// notification. This is optional but provides a nicer view of the -/// notification popup. -Future _setShortcut( - Event event, - L10n l10n, - String title, - Uint8List? avatarFile, -) async { - final flutterShortcuts = FlutterShortcuts(); - await flutterShortcuts.initialize(debug: !kReleaseMode); - await flutterShortcuts.pushShortcutItem( - shortcut: ShortcutItem( - id: event.room.id, - action: AppConfig.inviteLinkPrefix + event.room.id, - shortLabel: title, - conversationShortcut: true, - icon: avatarFile == null + static Future _getAvatarFile(Client client, Uri? avatar) async { + try { + return avatar == null ? null - : ShortcutMemoryIcon(jpegImage: avatarFile).toString(), - shortcutIconAsset: avatarFile == null - ? ShortcutIconAsset.androidAsset - : ShortcutIconAsset.memoryAsset, - isImportant: event.room.isFavourite, - ), - ); + : await client + .downloadMxcCached( + avatar, + thumbnailMethod: ThumbnailMethod.scale, + width: 256, + height: 256, + animated: false, + isThumbnail: true, + ) + .timeout(const Duration(seconds: 3)); + } catch (e, s) { + Logs().e('Unable to get avatar picture', e, s); + return null; + } + } + + static bool _isInForeground( + PushNotification notification, + String? activeRoomId, + Client? activeClient, + Client notifiedClient, + ) { + return notification.roomId != null && + activeRoomId == notification.roomId && + activeClient == notifiedClient && + WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed; + } +} + +class NotificationResponsePayload { + final String roomId; + final String clientName; + + NotificationResponsePayload(this.roomId, this.clientName); + + NotificationResponsePayload.fromJson(Map json) + : roomId = json['roomId'], + clientName = json['clientName']; + + Map toJson() { + return { + 'roomId': roomId, + 'clientName': clientName, + }; + } }