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(