migrated custom timetable events to Marianum-Connect and refactored push notification handling

This commit is contained in:
2026-07-12 14:31:48 +02:00
parent c444ed54a5
commit 91a6216f66
28 changed files with 474 additions and 212 deletions
+20 -1
View File
@@ -77,15 +77,24 @@ class PushMessageHandler {
String? openChatToken,
}) async {
final data = message.data;
// The device stays registered even when the user turns notifications off,
// so silent sync pushes (deletes, data refresh) keep arriving. When off we
// still process the message but skip raising a visible notification.
final notificationsEnabled = await _registrationStore.notificationsEnabled();
switch (classifyPush(data)) {
case PushKind.connect:
await _handleConnect(message, foreground: foreground);
await _handleConnect(
message,
foreground: foreground,
notificationsEnabled: notificationsEnabled,
);
break;
case PushKind.nextcloud:
await _handleNextcloud(
data,
foreground: foreground,
openChatToken: openChatToken,
notificationsEnabled: notificationsEnabled,
);
break;
case PushKind.unknown:
@@ -96,7 +105,11 @@ class PushMessageHandler {
Future<void> _handleConnect(
RemoteMessage message, {
required bool foreground,
required bool notificationsEnabled,
}) async {
// Connect pushes carry no silent side effects, so nothing to do when the
// user has notifications off.
if (!notificationsEnabled) return;
// On iOS the alert is delivered natively by the system; only Android needs
// to render the plaintext payload locally.
final data = message.data;
@@ -114,6 +127,7 @@ class PushMessageHandler {
Map<String, dynamic> data, {
required bool foreground,
required String? openChatToken,
required bool notificationsEnabled,
}) async {
final subjectBase64 = data['subject'] as String;
final signatureBase64 = data['signature'] as String;
@@ -153,6 +167,11 @@ class PushMessageHandler {
return;
}
// Notifications turned off: the push was still processed (deletes above,
// plus the foreground badge/provider refresh in NotificationController) —
// only the visible tray notification is suppressed.
if (!notificationsEnabled) return;
await _renderer.render(subject);
}
+19 -13
View File
@@ -350,21 +350,26 @@ class PushRegistration {
}
}
/// Registers this device when push is both user-enabled and backend-capable.
/// Only registers when the OS notification permission is *already* granted —
/// it never triggers the OS prompt itself. Requesting the permission is the
/// job of the first Talk visit (see `maybePromptTalkNotifications`), which
/// keeps the prompt out of the cold-start path. Safe to call on every start —
/// Nextcloud dedups an unchanged registration — which also self-heals a
/// device whose registration was lost.
static Future<void> syncSubscription({
required bool enabled,
required bool capable,
}) async {
if (!(enabled && capable)) return;
/// Registers this device whenever the backend advertises the push capability.
/// Deliberately independent of the in-app notification toggle: a user who
/// turned notifications off stays registered so silent sync pushes keep
/// flowing — the display is suppressed downstream via the mirrored flag (see
/// [PushRegistrationStore.notificationsEnabled]). Only registers when the OS
/// notification permission is *already* granted — it never triggers the OS
/// prompt itself. Requesting the permission is the job of the first Talk visit
/// (see `maybePromptTalkNotifications`), which keeps the prompt out of the
/// cold-start path. Safe to call on every start — Nextcloud dedups an
/// unchanged registration — which also self-heals a device whose registration
/// was lost.
/// Returns whether registration was actually *attempted* (all gates passed).
/// Even a partial success persists the `general` device identifier, so the
/// caller re-emits telemetry on `true` to reflect the fresh registration in
/// the same session instead of lagging until the next launch.
static Future<bool> syncSubscription({required bool capable}) async {
if (!capable) return false;
if (!await isOsPermissionGranted()) {
log('Push: OS notification permission not granted, skipping registration');
return;
return false;
}
final registration = PushRegistration();
// register() below refreshes an unchanged subscription anyway; the check
@@ -373,6 +378,7 @@ class PushRegistration {
log('Push: registered endpoints outdated, re-registering');
}
await registration.register();
return true;
}
/// Re-registers after an FCM token refresh. The Nextcloud device identifier
+18
View File
@@ -27,6 +27,11 @@ class PushRegistrationStore {
// (AccountData writes `nextcloud_app_password` group-scoped).
static const _usernameKey = 'nextcloud_username';
static const _baseUrlKey = 'nextcloud_base_url';
// Mirror of the in-app notification toggle (`notificationSettings.enabled`),
// written group-scoped so the FCM background isolate (no bloc access) and the
// iOS NSE can gate rendering. The device stays *registered* when off so silent
// sync pushes keep flowing — only the visible alert is suppressed.
static const _notificationsEnabledKey = 'push_notifications_enabled';
static const _perTypeKeys = [
_deviceIdentifierKey,
@@ -82,6 +87,19 @@ class PushRegistrationStore {
await _storage.write(key: _baseUrlKey, value: baseUrl);
}
/// Mirrors the in-app notification toggle so the background isolate / iOS NSE
/// can read it without bloc access.
Future<void> setNotificationsEnabled(bool enabled) =>
_storage.write(
key: _notificationsEnabledKey,
value: enabled ? '1' : '0',
);
/// The mirrored notification toggle. Defaults to `true` when unset (fresh
/// install / pre-mirror build) so a missing mirror never silences pushes.
Future<bool> notificationsEnabled() async =>
await _storage.read(key: _notificationsEnabledKey) != '0';
Future<String?> deviceIdentifier(PushRegistrationType type) =>
_storage.read(key: keyFor(_deviceIdentifierKey, type));