Files
Client/lib/push/push_renderer.dart
T

197 lines
6.0 KiB
Dart

import 'dart:convert';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../notification/notification_service.dart';
import '../notification/notification_tasks.dart';
import 'nid_store.dart';
import 'push_actions.dart';
import 'push_subject.dart';
/// Renders decrypted push subjects (and plaintext Connect pushes) as local
/// notifications. Talk messages get a [MessagingStyleInformation] with inline
/// reply + mark-as-read actions and a per-chat tag; everything else renders in
/// a generic channel.
class PushRenderer {
static const talkChannelId = 'talk_messages';
static const talkChannelName = 'Talk-Nachrichten';
static const generalChannelId = 'nextcloud_general';
static const generalChannelName = 'Benachrichtigungen';
static const String iosTalkCategory = 'TALK_MESSAGE';
final NidStore _nidStore;
PushRenderer({NidStore? nidStore}) : _nidStore = nidStore ?? NidStore();
FlutterLocalNotificationsPlugin get _plugin =>
NotificationService().flutterLocalNotificationsPlugin;
/// Creates the Android notification channels. Safe to call repeatedly.
static Future<void> ensureChannels() async {
final android = NotificationService().flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (android == null) return;
await android.createNotificationChannel(
const AndroidNotificationChannel(
talkChannelId,
talkChannelName,
description: 'Neue Nachrichten aus Nextcloud Talk',
importance: Importance.high,
),
);
await android.createNotificationChannel(
const AndroidNotificationChannel(
generalChannelId,
generalChannelName,
description: 'Allgemeine Benachrichtigungen',
),
);
}
/// Renders a decrypted Nextcloud push subject.
Future<void> render(PushSubject subject) async {
if (subject.isTalk) {
await _renderTalk(subject);
} else {
await _renderGeneric(subject);
}
}
Future<void> _renderTalk(PushSubject subject) async {
final nid = subject.nid ?? _fallbackId(subject.id);
final chatToken = subject.id;
final tag = chatToken != null
? NotificationTasks.chatTag(chatToken)
: 'talk_$nid';
final text = subject.subject ?? 'Neue Nachricht';
final (senderName, messageText) = _splitSender(text);
final payload = _payload(chatToken: chatToken, nid: nid);
final messagingStyle = MessagingStyleInformation(
const Person(key: 'self', name: 'Ich'),
conversationTitle: senderName,
groupConversation: false,
messages: [
Message(messageText, DateTime.now(), Person(name: senderName)),
],
);
final androidDetails = AndroidNotificationDetails(
talkChannelId,
talkChannelName,
importance: Importance.high,
priority: Priority.high,
category: AndroidNotificationCategory.message,
tag: tag,
styleInformation: messagingStyle,
actions: const [
AndroidNotificationAction(
kTalkReplyActionId,
'Antworten',
showsUserInterface: false,
cancelNotification: false,
inputs: [AndroidNotificationActionInput(label: 'Nachricht')],
),
AndroidNotificationAction(
kTalkMarkReadActionId,
'Gelesen',
showsUserInterface: false,
),
],
);
final iosDetails = DarwinNotificationDetails(
threadIdentifier: tag,
categoryIdentifier: iosTalkCategory,
);
await _nidStore.put(
NidEntry(nid: nid, notificationId: nid, tag: tag, chatToken: chatToken),
);
await _plugin.show(
id: nid,
title: senderName,
body: messageText,
notificationDetails: NotificationDetails(
android: androidDetails,
iOS: iosDetails,
),
payload: payload,
);
}
Future<void> _renderGeneric(PushSubject subject) async {
final nid = subject.nid ?? _fallbackId(subject.subject);
const androidDetails = AndroidNotificationDetails(
generalChannelId,
generalChannelName,
);
const iosDetails = DarwinNotificationDetails();
await _nidStore.put(
NidEntry(nid: nid, notificationId: nid, tag: 'nc_$nid'),
);
await _plugin.show(
id: nid,
title: subject.subject ?? 'Neue Benachrichtigung',
body: null,
notificationDetails: const NotificationDetails(
android: androidDetails,
iOS: iosDetails,
),
payload: _payload(chatToken: null, nid: nid),
);
}
/// Renders a plaintext MarianumConnect direct push (Android only — iOS shows
/// the native alert itself).
Future<void> renderConnect({
required String title,
required String body,
Map<String, String>? data,
}) async {
final id = _fallbackId('$title$body');
const androidDetails = AndroidNotificationDetails(
generalChannelId,
generalChannelName,
importance: Importance.high,
priority: Priority.high,
);
await _plugin.show(
id: id,
title: title,
body: body,
notificationDetails: const NotificationDetails(android: androidDetails),
payload: data == null ? null : jsonEncode(data),
);
}
String _payload({required String? chatToken, required int nid}) =>
jsonEncode({'chatToken': ?chatToken, 'nid': nid});
/// Splits a `"Sender: message"` subject into its parts, falling back to a
/// generic sender label when there's no delimiter.
(String, String) _splitSender(String subject) {
final idx = subject.indexOf(': ');
if (idx > 0 && idx < subject.length - 2) {
return (subject.substring(0, idx), subject.substring(idx + 2));
}
return ('Talk', subject);
}
/// Deterministic non-negative 31-bit id from a string, used when the push
/// carries no `nid`.
int _fallbackId(String? seed) {
if (seed == null || seed.isEmpty) return 0;
var hash = 0;
for (final unit in seed.codeUnits) {
hash = (hash * 31 + unit) & 0x7fffffff;
}
return hash;
}
}