From 2e07b7bcf179033c654b5759bfd80e2604188e9c Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 3 Oct 2024 17:06:59 +0200 Subject: [PATCH 1/2] feat: Notification actions on android --- android/app/src/main/AndroidManifest.xml | 3 + lib/utils/background_push.dart | 41 ++++------ .../notification_background_handler.dart | 80 +++++++++++++++++++ lib/utils/push_helper.dart | 14 ++++ 4 files changed, 112 insertions(+), 26 deletions(-) create mode 100644 lib/utils/notification_background_handler.dart 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 +284,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 +336,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/notification_background_handler.dart b/lib/utils/notification_background_handler.dart new file mode 100644 index 000000000..e604ddcad --- /dev/null +++ b/lib/utils/notification_background_handler.dart @@ -0,0 +1,80 @@ +import 'package:collection/collection.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +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'; + +@pragma('vm:entry-point') +void notificationTapBackground( + NotificationResponse notificationResponse, +) async { + final client = (await ClientManager.getClients( + initialize: false, + store: await SharedPreferences.getInstance(), + )) + .first; + notificationTap(notificationResponse, client: client); +} + +void notificationTap( + NotificationResponse notificationResponse, { + GoRouter? router, + required Client client, +}) async { + 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'); + } + 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); + 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..078746c61 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,19 @@ Future _tryPushHelper( importance: Importance.high, priority: Priority.max, groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms', + actions: [ + AndroidNotificationAction( + FluffyChatNotificationActions.markAsRead.name, + l10n.markAsRead, + ), + AndroidNotificationAction( + FluffyChatNotificationActions.reply.name, + l10n.reply, + inputs: [ + const AndroidNotificationActionInput(), + ], + ), + ], ); const iOSPlatformChannelSpecifics = DarwinNotificationDetails(); final platformChannelSpecifics = NotificationDetails( From 04b5ecce54771702ca82244ec1b43a2e58f63d35 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Thu, 3 Oct 2024 17:33:44 +0200 Subject: [PATCH 2/2] feat: Add notification actions --- lib/utils/background_push.dart | 23 ++++++ .../builder.dart | 1 + .../notification_background_handler.dart | 72 ++++++++++++++++++- lib/utils/push_helper.dart | 14 ++-- 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index b62253ed9..ca45e6076 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -20,6 +20,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; +import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -73,6 +75,27 @@ class BackgroundPush { void _init() async { try { + if (PlatformInfos.isAndroid) { + final port = ReceivePort(); + IsolateNameServer.removePortNameMapping('background_tab_port'); + IsolateNameServer.registerPortWithName( + port.sendPort, + 'background_tab_port', + ); + port.listen( + (message) async { + try { + await notificationTap( + NotificationResponseJson.fromJsonString(message), + client: client, + router: FluffyChatApp.router, + ); + } catch (e, s) { + Logs().wtf('Main Notification Tap crashed', e, s); + } + }, + ); + } await _flutterLocalNotificationsPlugin.initialize( const InitializationSettings( android: AndroidInitializationSettings('notifications_icon'), 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 index e604ddcad..8c9a279ee 100644 --- a/lib/utils/notification_background_handler.dart +++ b/lib/utils/notification_background_handler.dart @@ -1,28 +1,87 @@ +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; - notificationTap(notificationResponse, client: client); + 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; } -void notificationTap( +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; @@ -56,6 +115,9 @@ void notificationTap( 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( @@ -64,7 +126,11 @@ void notificationTap( } switch (actionType) { case FluffyChatNotificationActions.markAsRead: - await room.setReadMarker(room.lastEvent!.eventId); + 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) { diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index 078746c61..08de28ba6 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -279,16 +279,20 @@ Future _tryPushHelper( priority: Priority.max, groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms', actions: [ - AndroidNotificationAction( - FluffyChatNotificationActions.markAsRead.name, - l10n.markAsRead, - ), AndroidNotificationAction( FluffyChatNotificationActions.reply.name, l10n.reply, inputs: [ - const AndroidNotificationActionInput(), + AndroidNotificationActionInput( + label: l10n.writeAMessage, + ), ], + cancelNotification: false, + allowGeneratedReplies: true, + ), + AndroidNotificationAction( + FluffyChatNotificationActions.markAsRead.name, + l10n.markAsRead, ), ], );