merged develop

This commit is contained in:
2026-09-27 14:58:07 +02:00
188 changed files with 7474 additions and 2558 deletions
+32 -34
View File
@@ -118,6 +118,7 @@ class PushRegistration {
Future<bool> register() async {
if (DemoMode.active) return false;
if (_nextcloudOrNull == null) return _direct.register();
final epoch = SessionManager().sessionEpoch;
final String? fcmToken;
try {
fcmToken = await FirebaseMessaging.instance.getToken();
@@ -167,6 +168,7 @@ class PushRegistration {
fcmToken: fcmToken,
pems: pems,
appVersion: appVersion,
epoch: epoch,
);
allOk = allOk && ok;
}
@@ -178,6 +180,7 @@ class PushRegistration {
required String fcmToken,
required PushKeypairPems pems,
required String? appVersion,
required int epoch,
}) async {
try {
final proxyServer = currentProxyServer;
@@ -193,6 +196,11 @@ class PushRegistration {
userAgent: isTalk ? _talkUserAgent : null,
);
// Signed out while registering: persisting or announcing this
// registration would keep delivering the previous account's pushes,
// and logoutCleanup already ran so nothing would unregister it.
if (!SessionManager().isCurrentSession(epoch)) return false;
await _store.save(
type: type,
deviceIdentifier: registration.deviceIdentifier,
@@ -355,9 +363,8 @@ class PushRegistration {
status != AuthorizationStatus.deniedPermanently;
/// Requests the OS notification permission (covers iOS + Android 13) and
/// returns whether registration should proceed. Errors from the plugin are
/// treated as usable — better a possibly-idle registration than silently
/// losing push over a transient failure.
/// returns whether it is usable (not explicitly denied). Errors from the
/// plugin are treated as usable so a transient failure never nags the user.
static Future<bool> requestOsPermission() async {
try {
final settings = await FirebaseMessaging.instance.requestPermission();
@@ -368,56 +375,47 @@ class PushRegistration {
}
}
/// True when the user has explicitly denied the OS notification permission.
/// Read-only (no prompt) — used by the settings UI to surface the state.
static Future<bool> isOsPermissionDenied() async {
/// Current OS notification permission, or null when the plugin can't tell.
/// Read-only — never triggers the OS prompt.
static Future<AuthorizationStatus?> osPermissionStatus() async {
try {
final settings = await FirebaseMessaging.instance
.getNotificationSettings();
return !isPermissionUsable(settings.authorizationStatus);
return settings.authorizationStatus;
} on Object {
return false;
return null;
}
}
/// True when the OS notification permission is already granted
/// (`authorized`/`provisional`). Read-only — never triggers the OS prompt.
/// Used by the cold-start/self-heal path so it registers only for devices
/// that already opted in, leaving the actual prompt to the first Talk visit.
static Future<bool> isOsPermissionGranted() async {
try {
final settings = await FirebaseMessaging.instance
.getNotificationSettings();
return settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional;
} on Object {
return false;
}
final status = await osPermissionStatus();
return status != null && canDisplay(status);
}
/// Whether the OS will actually show a visible notification in [status].
/// Stricter than [isPermissionUsable]: `notDetermined` shows nothing yet.
static bool canDisplay(AuthorizationStatus status) =>
status == AuthorizationStatus.authorized ||
status == AuthorizationStatus.provisional;
/// 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.
/// Deliberately independent of both the in-app notification toggle and the
/// OS notification permission: silent sync pushes (deletes, chat/badge
/// refresh, widget refresh) need no permission — data-only FCM on Android,
/// `content-available` on iOS — so every device stays in sync. Whether a
/// visible notification is wanted is reported separately via the telemetry
/// heartbeat (toggle + OS permission), from which the server picks silent
/// pushes on iOS. Never triggers the OS prompt. 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 false;
}
final registration = PushRegistration();
// register() below refreshes an unchanged subscription anyway; the check
// only surfaces the endpoint switch in the log for diagnosability.