diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 167602f73..f7688fc0e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -135,6 +135,9 @@ + + + notificationTap( + response, + client: client, + router: FluffyChatApp.router, + ), + onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); Logs().v('Flutter Local Notifications initialized'); //firebase.setListeners( @@ -278,7 +307,14 @@ class BackgroundPush { return; } _wentToRoomOnStartup = true; - goToRoom(details.notificationResponse); + final response = details.notificationResponse; + if (response != null) { + notificationTap( + response, + client: client, + router: FluffyChatApp.router, + ); + } }); } @@ -323,30 +359,6 @@ class BackgroundPush { ); } - Future goToRoom(NotificationResponse? response) async { - try { - final roomId = response?.payload; - Logs().v('[Push] Attempting to go to room $roomId...'); - if (roomId == null) { - return; - } - await client.roomsLoading; - await client.accountDataLoading; - if (client.getRoomById(roomId) == null) { - await client - .waitForRoomInSync(roomId) - .timeout(const Duration(seconds: 30)); - } - FluffyChatApp.router.go( - client.getRoomById(roomId)?.membership == Membership.invite - ? '/rooms' - : '/rooms/$roomId', - ); - } catch (e, s) { - Logs().e('[Push] Failed to open room', e, s); - } - } - Future setupUp() async { await UnifiedPushUi(matrix!.context, ["default"], UPFunctions()) .registerAppWithDialog(); diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart index 3387fdf38..09738b518 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart @@ -105,6 +105,7 @@ Future _constructDatabase(String clientName) async { version: 1, // most important : apply encryption when opening the DB onConfigure: helper?.applyPragmaKey, + singleInstance: false, ), ); diff --git a/lib/utils/notification_background_handler.dart b/lib/utils/notification_background_handler.dart new file mode 100644 index 000000000..8c9a279ee --- /dev/null +++ b/lib/utils/notification_background_handler.dart @@ -0,0 +1,146 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:collection/collection.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:fluffychat/utils/client_manager.dart'; + +bool _vodInitialized = false; + +extension NotificationResponseJson on NotificationResponse { + String toJsonString() => jsonEncode({ + 'type': notificationResponseType.name, + 'id': id, + 'actionId': actionId, + 'input': input, + 'payload': payload, + 'data': data, + }); + + static NotificationResponse fromJsonString(String jsonString) { + final json = jsonDecode(jsonString) as Map; + return NotificationResponse( + notificationResponseType: NotificationResponseType.values + .singleWhere((t) => t.name == json['type']), + id: json['id'] as int?, + actionId: json['actionId'] as String?, + input: json['input'] as String?, + payload: json['payload'] as String?, + data: json['data'] as Map, + ); + } +} + +@pragma('vm:entry-point') +void notificationTapBackground( + NotificationResponse notificationResponse, +) async { + Logs().i('Notification tap in background'); + + final sendPort = IsolateNameServer.lookupPortByName('background_tab_port'); + if (sendPort != null) { + sendPort.send(notificationResponse.toJsonString()); + return; + } + + if (!_vodInitialized) { + await vod.init(); + _vodInitialized = true; + } + final client = (await ClientManager.getClients( + initialize: false, + store: await SharedPreferences.getInstance(), + )) + .first; + await client.abortSync(); + await client.init( + waitForFirstSync: false, + waitUntilLoadCompletedLoaded: false, + ); + if (!client.isLogged()) { + throw Exception('Notification tab in background but not logged in!'); + } + try { + await notificationTap(notificationResponse, client: client); + } finally { + await client.dispose(closeDatabase: false); + } + return; +} + +Future notificationTap( + NotificationResponse notificationResponse, { + GoRouter? router, + required Client client, +}) async { + Logs().d( + 'Notification action handler started', + notificationResponse.notificationResponseType.name, + ); + switch (notificationResponse.notificationResponseType) { + case NotificationResponseType.selectedNotification: + final roomId = notificationResponse.payload; + if (roomId == null) return; + + if (router == null) { + Logs().v('Ignore select notification action in background mode'); + return; + } + Logs().v('Open room from notification tap', roomId); + await client.roomsLoading; + await client.accountDataLoading; + if (client.getRoomById(roomId) == null) { + await client + .waitForRoomInSync(roomId) + .timeout(const Duration(seconds: 30)); + } + router.go( + client.getRoomById(roomId)?.membership == Membership.invite + ? '/rooms' + : '/rooms/$roomId', + ); + case NotificationResponseType.selectedNotificationAction: + final actionType = FluffyChatNotificationActions.values.singleWhereOrNull( + (action) => action.name == notificationResponse.actionId, + ); + if (actionType == null) { + throw Exception('Selected notification with action but no action ID'); + } + final roomId = notificationResponse.payload; + if (roomId == null) { + throw Exception('Selected notification with action but no payload'); + } + await client.roomsLoading; + await client.accountDataLoading; + await client.userDeviceKeysLoading; + final room = client.getRoomById(roomId); + if (room == null) { + throw Exception( + 'Selected notification with action but unknown room $roomId', + ); + } + switch (actionType) { + case FluffyChatNotificationActions.markAsRead: + await room.setReadMarker( + room.lastEvent!.eventId, + mRead: room.lastEvent!.eventId, + public: false, // TODO: Load preference here + ); + case FluffyChatNotificationActions.reply: + final input = notificationResponse.input; + if (input == null || input.isEmpty) { + throw Exception( + 'Selected notification with reply action but without input', + ); + } + await room.sendTextEvent(input); + } + } +} + +enum FluffyChatNotificationActions { markAsRead, reply } diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index 39f6bd90f..08de28ba6 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/l10n/l10n.dart'; 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/notification_background_handler.dart'; import 'package:fluffychat/utils/platform_infos.dart'; const notificationAvatarDimension = 128; @@ -277,6 +278,23 @@ Future _tryPushHelper( importance: Importance.high, priority: Priority.max, groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms', + actions: [ + AndroidNotificationAction( + FluffyChatNotificationActions.reply.name, + l10n.reply, + inputs: [ + AndroidNotificationActionInput( + label: l10n.writeAMessage, + ), + ], + cancelNotification: false, + allowGeneratedReplies: true, + ), + AndroidNotificationAction( + FluffyChatNotificationActions.markAsRead.name, + l10n.markAsRead, + ), + ], ); const iOSPlatformChannelSpecifics = DarwinNotificationDetails(); final platformChannelSpecifics = NotificationDetails(