diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 08854a6..f15a756 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -149,6 +149,8 @@
+
+
notificationTap(
+ response,
+ client: client,
+ router: FluffyChatApp.router,
+ l10n: l10n
+ ),
+ onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
Logs().v('Flutter Local Notifications initialized');
firebase?.setListeners(
@@ -278,7 +309,16 @@ class BackgroundPush {
return;
}
_wentToRoomOnStartup = true;
- goToRoom(details.notificationResponse);
+ final response = details.notificationResponse;
+
+ if (response != null) {
+ notificationTap(
+ response,
+ client: client,
+ router: FluffyChatApp.router,
+ l10n: l10n
+ );
+ }
});
}
@@ -351,7 +391,8 @@ class BackgroundPush {
final replyText = response?.input;
final room = client.getRoomById(roomId);
if (replyText != null && room != null) {
- await room.sendTextEvent(replyText, inReplyTo: await room.getEventById(eventId));
+ await room.sendTextEvent(replyText,
+ inReplyTo: await room.getEventById(eventId));
}
return;
}
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 7c5f2c1..f432bbe 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
@@ -109,6 +109,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 0000000..415c759
--- /dev/null
+++ b/lib/utils/notification_background_handler.dart
@@ -0,0 +1,237 @@
+import 'dart:convert';
+import 'dart:ui';
+
+import 'package:collection/collection.dart';
+import 'package:extera_next/config/app_config.dart';
+import 'package:extera_next/generated/l10n/l10n.dart';
+import 'package:extera_next/utils/client_download_content_extension.dart';
+import 'package:extera_next/utils/matrix_sdk_extensions/matrix_locals.dart';
+import 'package:extera_next/utils/platform_infos.dart';
+import 'package:extera_next/utils/push_helper.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:extera_next/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 store = await SharedPreferences.getInstance();
+ final client = (await ClientManager.getClients(
+ initialize: false,
+ store: store,
+ ))
+ .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,
+ L10n? l10n,
+ required Client client,
+}) async {
+ Logs().d(
+ 'Notification action handler started',
+ notificationResponse.notificationResponseType.name,
+ );
+ final payload =
+ NotificationPushPayload.fromString(notificationResponse.payload ?? '');
+ switch (notificationResponse.notificationResponseType) {
+ case NotificationResponseType.selectedNotification:
+ final roomId = payload.roomId;
+ 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(
+ payload!.eventId,
+ mRead: payload!.eventId,
+ public:
+ AppConfig.sendPublicReadReceipts, // 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',
+ );
+ }
+ final eventId = await room.sendTextEvent(input);
+
+ if (PlatformInfos.isAndroid) {
+ final ownProfile = await room.client.fetchOwnProfile();
+
+ final avatar = ownProfile.avatarUrl;
+ final avatarFile = avatar == null
+ ? null
+ : await client
+ .downloadMxcCached(
+ avatar,
+ thumbnailMethod: ThumbnailMethod.scale,
+ width: notificationAvatarDimension,
+ height: notificationAvatarDimension,
+ animated: false,
+ isThumbnail: true,
+ rounded: true,
+ )
+ .timeout(const Duration(seconds: 3));
+ final messagingStyleInformation =
+ await AndroidFlutterLocalNotificationsPlugin()
+ .getActiveNotificationMessagingStyle(room.id.hashCode);
+ if (messagingStyleInformation == null) return;
+ l10n ??= await lookupL10n(PlatformDispatcher.instance.locale);
+ messagingStyleInformation.messages?.add(
+ Message(
+ input,
+ DateTime.now(),
+ Person(
+ key: room.client.userID,
+ name: l10n.you,
+ icon: avatarFile == null
+ ? null
+ : ByteArrayAndroidIcon(avatarFile),
+ ),
+ ),
+ );
+
+ await FlutterLocalNotificationsPlugin().show(
+ room.id.hashCode,
+ room.getLocalizedDisplayname(MatrixLocals(l10n)),
+ input,
+ NotificationDetails(
+ android: AndroidNotificationDetails(
+ AppConfig.pushNotificationsChannelId,
+ l10n.incomingMessages,
+ category: AndroidNotificationCategory.message,
+ shortcutId: room.id,
+ styleInformation: messagingStyleInformation,
+ groupKey: room.id,
+ playSound: false,
+ enableVibration: false,
+ actions: [
+ AndroidNotificationAction(
+ FluffyChatNotificationActions.reply.name,
+ l10n.reply,
+ inputs: [
+ AndroidNotificationActionInput(
+ label: l10n.writeAMessage,
+ ),
+ ],
+ cancelNotification: false,
+ allowGeneratedReplies: true,
+ semanticAction: SemanticAction.reply,
+ ),
+ AndroidNotificationAction(
+ FluffyChatNotificationActions.markAsRead.name,
+ l10n.markAsRead,
+ semanticAction: SemanticAction.markAsRead,
+ ),
+ ],
+ ),
+ ),
+ payload: NotificationPushPayload(
+ client.clientName,
+ room.id,
+ eventId,
+ ).toString(),
+ );
+ }
+ }
+ }
+}
+
+enum FluffyChatNotificationActions { markAsRead, reply }
diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart
index f0c7bc2..de86db4 100644
--- a/lib/utils/push_helper.dart
+++ b/lib/utils/push_helper.dart
@@ -1,10 +1,10 @@
+import 'dart:convert';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
-import 'package:extera_next/generated/l10n/l10n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_shortcuts_new/flutter_shortcuts_new.dart';
import 'package:matrix/matrix.dart';
@@ -14,7 +14,11 @@ import 'package:extera_next/config/app_config.dart';
import 'package:extera_next/utils/client_download_content_extension.dart';
import 'package:extera_next/utils/client_manager.dart';
import 'package:extera_next/utils/matrix_sdk_extensions/matrix_locals.dart';
+import 'package:extera_next/utils/notification_background_handler.dart';
import 'package:extera_next/utils/platform_infos.dart';
+import 'package:extera_next/generated/l10n/l10n.dart';
+
+const notificationAvatarDimension = 128;
Future pushHelper(
PushNotification notification, {
@@ -32,9 +36,9 @@ Future pushHelper(
flutterLocalNotificationsPlugin: flutterLocalNotificationsPlugin,
);
} catch (e, s) {
- Logs().v('Push Helper has crashed!', e, s);
+ Logs().e('Push Helper has crashed! Writing into temporary file', e, s);
- l10n ??= await lookupL10n(const Locale('en'));
+ l10n ??= await lookupL10n(PlatformDispatcher.instance.locale);
flutterLocalNotificationsPlugin.show(
notification.roomId?.hashCode ?? 0,
l10n.newMessageInFluffyChat,
@@ -86,7 +90,7 @@ Future _tryPushHelper(
.first;
final event = await client.getEventByPushNotification(
notification,
- storeInDatabase: isBackgroundMessage,
+ storeInDatabase: false,
);
if (event == null) {
@@ -163,11 +167,12 @@ Future _tryPushHelper(
: await client
.downloadMxcCached(
avatar,
- thumbnailMethod: ThumbnailMethod.scale,
- width: 256,
- height: 256,
+ thumbnailMethod: ThumbnailMethod.crop,
+ width: notificationAvatarDimension,
+ height: notificationAvatarDimension,
animated: false,
isThumbnail: true,
+ rounded: true,
)
.timeout(const Duration(seconds: 3));
} catch (e, s) {
@@ -181,11 +186,12 @@ Future _tryPushHelper(
: await client
.downloadMxcCached(
senderAvatar,
- thumbnailMethod: ThumbnailMethod.scale,
- width: 256,
- height: 256,
+ thumbnailMethod: ThumbnailMethod.crop,
+ width: notificationAvatarDimension,
+ height: notificationAvatarDimension,
animated: false,
isThumbnail: true,
+ rounded: true,
)
.timeout(const Duration(seconds: 3));
} catch (e, s) {
@@ -247,13 +253,6 @@ Future _tryPushHelper(
number: notification.counts?.unread,
category: AndroidNotificationCategory.message,
shortcutId: event.room.id,
- actions: [
- AndroidNotificationAction("read", l10n.markAsRead),
- AndroidNotificationAction("reply",
- l10n.reply, inputs: [
- AndroidNotificationActionInput(label: l10n.writeAMessage)
- ])
- ],
styleInformation: messagingStyleInformation ??
MessagingStyleInformation(
Person(
@@ -279,6 +278,25 @@ 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,
+ semanticAction: SemanticAction.reply,
+ ),
+ AndroidNotificationAction(
+ FluffyChatNotificationActions.markAsRead.name,
+ l10n.markAsRead,
+ semanticAction: SemanticAction.markAsRead,
+ ),
+ ],
);
const iOSPlatformChannelSpecifics = DarwinNotificationDetails();
final platformChannelSpecifics = NotificationDetails(
@@ -297,11 +315,30 @@ Future _tryPushHelper(
title,
body,
platformChannelSpecifics,
- payload: '${event.roomId} ${event.eventId}',
+ payload:
+ NotificationPushPayload(client.clientName, event.room.id, event.eventId)
+ .toString(),
);
Logs().v('Push helper has been completed!');
}
+class NotificationPushPayload {
+ final String? clientName, roomId, eventId;
+
+ NotificationPushPayload(this.clientName, this.roomId, this.eventId);
+
+ factory NotificationPushPayload.fromString(String payload) {
+ final parts = payload.split('|');
+ if (parts.length != 3) {
+ return NotificationPushPayload(null, null, null);
+ }
+ return NotificationPushPayload(parts[0], parts[1], parts[2]);
+ }
+
+ @override
+ String toString() => '$clientName|$roomId|$eventId';
+}
+
/// 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.
@@ -319,13 +356,11 @@ Future _setShortcut(
action: AppConfig.inviteLinkPrefix + event.room.id,
shortLabel: title,
conversationShortcut: true,
- icon: avatarFile == null
- ? null
- : ShortcutMemoryIcon(jpegImage: avatarFile).toString(),
+ icon: avatarFile == null ? null : base64Encode(avatarFile),
shortcutIconAsset: avatarFile == null
? ShortcutIconAsset.androidAsset
: ShortcutIconAsset.memoryAsset,
isImportant: event.room.isFavourite,
),
);
-}
+}
\ No newline at end of file
diff --git a/pubspec.lock b/pubspec.lock
index 873cac5..feacc4c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -575,26 +575,34 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
- sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35"
+ sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
url: "https://pub.dev"
source: hosted
- version: "17.2.4"
+ version: "19.4.2"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
- sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af
+ sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
url: "https://pub.dev"
source: hosted
- version: "4.0.1"
+ version: "6.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
- sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66"
+ sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
url: "https://pub.dev"
source: hosted
- version: "7.2.0"
+ version: "9.1.0"
+ flutter_local_notifications_windows:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_windows
+ sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.3"
flutter_localizations:
dependency: "direct main"
description: flutter
@@ -2000,10 +2008,10 @@ packages:
dependency: transitive
description:
name: timezone
- sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
+ sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
- version: "0.9.4"
+ version: "0.10.1"
tint:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index cd0cccd..619a8e1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -34,7 +34,7 @@ dependencies:
flutter_foreground_task: ^6.1.3
flutter_highlighter: ^0.1.1
flutter_linkify: ^6.0.0
- flutter_local_notifications: ^17.2.3
+ flutter_local_notifications: ^19.4.2
flutter_localizations:
sdk: flutter
flutter_map: ^6.1.0
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index b701d30..7e3e64d 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -21,6 +21,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ flutter_local_notifications_windows
flutter_vodozemac
)