copy notification actions feature from fluffychat

This commit is contained in:
OfficialDakari 2025-10-16 20:36:40 +05:00
parent 314ab88551
commit d4bbe15ed6
8 changed files with 359 additions and 34 deletions

View File

@ -149,6 +149,8 @@
</intent-filter>
</receiver>
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data

View File

@ -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';
@ -33,6 +35,7 @@ import 'package:unifiedpush/unifiedpush.dart';
import 'package:unifiedpush_ui/unifiedpush_ui.dart';
import 'package:extera_next/utils/push_helper.dart';
import 'package:extera_next/utils/notification_background_handler.dart';
import 'package:extera_next/widgets/fluffy_chat_app.dart';
import '../config/app_config.dart';
import '../config/setting_keys.dart';
@ -72,12 +75,40 @@ 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,
l10n: l10n,
);
} catch (e, s) {
Logs().wtf('Main Notification Tap crashed', e, s);
}
},
);
}
await _flutterLocalNotificationsPlugin.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('notifications_icon'),
iOS: DarwinInitializationSettings(),
),
onDidReceiveNotificationResponse: goToRoom,
onDidReceiveNotificationResponse: (response) => 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;
}

View File

@ -109,6 +109,7 @@ Future<MatrixSdkDatabase> _constructDatabase(String clientName) async {
version: 1,
// most important : apply encryption when opening the DB
onConfigure: helper?.applyPragmaKey,
singleInstance: false
),
);

View File

@ -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<String, Object?>;
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<String, dynamic>,
);
}
}
@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<void> 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>[
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 }

View File

@ -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<void> pushHelper(
PushNotification notification, {
@ -32,9 +36,9 @@ Future<void> 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<void> _tryPushHelper(
.first;
final event = await client.getEventByPushNotification(
notification,
storeInDatabase: isBackgroundMessage,
storeInDatabase: false,
);
if (event == null) {
@ -163,11 +167,12 @@ Future<void> _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<void> _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<void> _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<void> _tryPushHelper(
importance: Importance.high,
priority: Priority.max,
groupKey: event.room.spaceParents.firstOrNull?.roomId ?? 'rooms',
actions: <AndroidNotificationAction>[
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<void> _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<void> _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,
),
);
}
}

View File

@ -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:

View File

@ -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

View File

@ -21,6 +21,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
flutter_vodozemac
)