implemented dual Nextcloud push registration with separate general and talk apptypes to ensure reliable Talk notification delivery; introduced stacked MessagingStyle notifications for chat threads with support for circular conversation avatars and disk caching

This commit is contained in:
2026-07-05 22:48:04 +02:00
parent 35e144799e
commit 483fea62ba
31 changed files with 2807 additions and 320 deletions
+31 -11
View File
@@ -4,6 +4,7 @@ import 'package:crypton/crypton.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import '../notification/notification_service.dart';
import 'chat_thread_store.dart';
import 'nid_store.dart';
import 'push_decryptor.dart';
import 'push_keypair.dart';
@@ -34,6 +35,17 @@ PushKind classifyPush(Map<String, dynamic> data) {
return PushKind.unknown;
}
/// FCM background isolate entry point. Must be a TOP-LEVEL function with the
/// entry-point pragma: AOT builds cannot invoke static class members from
/// native code unless the class itself is annotated too (DartVM error
/// "must be annotated"), so a plain function is the reliable form.
@pragma('vm:entry-point')
Future<void> pushOnBackgroundMessage(RemoteMessage message) async {
await NotificationService().initializeNotifications();
await PushRenderer.ensureChannels();
await PushMessageHandler().handle(message);
}
/// Verifies, decrypts, and renders incoming push messages. Delete-pushes cancel
/// the matching tray notification via [NidStore]. Works both in the FCM
/// background isolate and the foreground.
@@ -42,25 +54,19 @@ class PushMessageHandler {
final PushRegistrationStore _registrationStore;
final PushRenderer _renderer;
final NidStore _nidStore;
final ChatThreadStore _threadStore;
PushMessageHandler({
PushKeypair? keypair,
PushRegistrationStore? registrationStore,
PushRenderer? renderer,
NidStore? nidStore,
ChatThreadStore? threadStore,
}) : _keypair = keypair ?? const PushKeypair(),
_registrationStore = registrationStore ?? const PushRegistrationStore(),
_renderer = renderer ?? PushRenderer(),
_nidStore = nidStore ?? NidStore();
/// Background isolate entry point registered with
/// `FirebaseMessaging.onBackgroundMessage`.
@pragma('vm:entry-point')
static Future<void> onBackgroundMessage(RemoteMessage message) async {
await NotificationService().initializeNotifications();
await PushRenderer.ensureChannels();
await PushMessageHandler().handle(message);
}
_nidStore = nidStore ?? NidStore(),
_threadStore = threadStore ?? ChatThreadStore();
/// Processes [message]. In the foreground, pass [foreground] true and
/// [openChatToken] so a message for the currently open chat is suppressed
@@ -157,6 +163,7 @@ class PushMessageHandler {
await _cancel(entry);
}
await _nidStore.clear();
await _threadStore.clearAll();
return;
}
final nids = <int>[
@@ -165,7 +172,20 @@ class PushMessageHandler {
];
for (final nid in nids) {
final entry = await _nidStore.get(nid);
if (entry != null) await _cancel(entry);
final chatToken = entry?.chatToken;
if (chatToken != null && chatToken.isNotEmpty) {
// Stacked chat notification: drop only this message from the thread.
// Remaining messages re-render WITHOUT alerting again; the last one
// going away cancels the whole card.
final remaining = await _threadStore.removeNid(chatToken, nid);
if (remaining.isEmpty) {
await _cancel(entry!);
} else {
await _renderer.renderTalkThread(chatToken, remaining, alert: false);
}
} else if (entry != null) {
await _cancel(entry);
}
await _nidStore.delete(nid);
}
}