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:
@@ -1,16 +1,25 @@
|
||||
import 'push_registration_type.dart';
|
||||
import 'push_secure_storage.dart';
|
||||
|
||||
/// Persists the bookkeeping produced by a successful push registration:
|
||||
/// the Nextcloud device identifier, the per-user server public key (needed to
|
||||
/// verify incoming push signatures), the FCM token the registration was made
|
||||
/// with (so a token refresh can be detected) and the endpoints it was bound to
|
||||
/// (so an endpoint switch in the dev tools can be detected).
|
||||
/// Persists the bookkeeping produced by successful push registrations —
|
||||
/// per [PushRegistrationType]: the Nextcloud device identifier, the FCM token
|
||||
/// the registration was made with (so a token refresh can be detected), the
|
||||
/// endpoints it was bound to (so an endpoint switch in the dev tools can be
|
||||
/// detected) and the last attempt outcome. The server public key and the
|
||||
/// device keypair are shared between both registrations.
|
||||
///
|
||||
/// Key layout: the `general` type uses the pre-dual key names unchanged, so
|
||||
/// existing installs are implicitly migrated — their stored registration IS
|
||||
/// the general one; the missing talk registration is added by the next
|
||||
/// register-on-start self-heal.
|
||||
class PushRegistrationStore {
|
||||
static const _deviceIdentifierKey = 'push_device_identifier';
|
||||
static const _serverPublicKeyKey = 'push_server_public_key_pem';
|
||||
static const _registeredTokenKey = 'push_registered_fcm_token';
|
||||
static const _proxyServerKey = 'push_registered_proxy_server';
|
||||
static const _ncBaseUrlKey = 'push_registered_nc_base_url';
|
||||
static const _lastAttemptAtKey = 'push_last_registration_at';
|
||||
static const _lastAttemptErrorKey = 'push_last_registration_error';
|
||||
// Native-only context: the iOS AppDelegate answers Talk notification actions
|
||||
// (reply / mark-as-read) directly via URLSession while the Flutter engine is
|
||||
// not guaranteed to run. It needs the Nextcloud username and base URL from the
|
||||
@@ -19,26 +28,46 @@ class PushRegistrationStore {
|
||||
static const _usernameKey = 'nextcloud_username';
|
||||
static const _baseUrlKey = 'nextcloud_base_url';
|
||||
|
||||
const PushRegistrationStore();
|
||||
static const _perTypeKeys = [
|
||||
_deviceIdentifierKey,
|
||||
_registeredTokenKey,
|
||||
_proxyServerKey,
|
||||
_ncBaseUrlKey,
|
||||
_lastAttemptAtKey,
|
||||
_lastAttemptErrorKey,
|
||||
];
|
||||
|
||||
final FlutterSecureStorageLike _storage;
|
||||
|
||||
const PushRegistrationStore([this._storage = const PushSecureStorage()]);
|
||||
|
||||
/// Type-specific key: general keeps the legacy names (implicit migration of
|
||||
/// pre-dual installs), talk appends a suffix.
|
||||
static String keyFor(String baseKey, PushRegistrationType type) =>
|
||||
type == PushRegistrationType.general ? baseKey : '${baseKey}_talk';
|
||||
|
||||
Future<void> save({
|
||||
required PushRegistrationType type,
|
||||
required String deviceIdentifier,
|
||||
required String serverPublicKeyPem,
|
||||
required String fcmToken,
|
||||
required String proxyServer,
|
||||
required String ncBaseUrl,
|
||||
}) async {
|
||||
await pushSecureStorage.write(
|
||||
key: _deviceIdentifierKey,
|
||||
await _storage.write(
|
||||
key: keyFor(_deviceIdentifierKey, type),
|
||||
value: deviceIdentifier,
|
||||
);
|
||||
await pushSecureStorage.write(
|
||||
key: _serverPublicKeyKey,
|
||||
value: serverPublicKeyPem,
|
||||
await _storage.write(key: _serverPublicKeyKey, value: serverPublicKeyPem);
|
||||
await _storage.write(
|
||||
key: keyFor(_registeredTokenKey, type),
|
||||
value: fcmToken,
|
||||
);
|
||||
await pushSecureStorage.write(key: _registeredTokenKey, value: fcmToken);
|
||||
await pushSecureStorage.write(key: _proxyServerKey, value: proxyServer);
|
||||
await pushSecureStorage.write(key: _ncBaseUrlKey, value: ncBaseUrl);
|
||||
await _storage.write(
|
||||
key: keyFor(_proxyServerKey, type),
|
||||
value: proxyServer,
|
||||
);
|
||||
await _storage.write(key: keyFor(_ncBaseUrlKey, type), value: ncBaseUrl);
|
||||
}
|
||||
|
||||
/// Persists the username and Nextcloud base URL group-scoped so the native
|
||||
@@ -49,39 +78,73 @@ class PushRegistrationStore {
|
||||
required String username,
|
||||
required String baseUrl,
|
||||
}) async {
|
||||
await pushSecureStorage.write(key: _usernameKey, value: username);
|
||||
await pushSecureStorage.write(key: _baseUrlKey, value: baseUrl);
|
||||
await _storage.write(key: _usernameKey, value: username);
|
||||
await _storage.write(key: _baseUrlKey, value: baseUrl);
|
||||
}
|
||||
|
||||
Future<String?> deviceIdentifier() =>
|
||||
pushSecureStorage.read(key: _deviceIdentifierKey);
|
||||
Future<String?> deviceIdentifier(PushRegistrationType type) =>
|
||||
_storage.read(key: keyFor(_deviceIdentifierKey, type));
|
||||
|
||||
/// Per-user server public key — identical for both registrations.
|
||||
Future<String?> serverPublicKeyPem() =>
|
||||
pushSecureStorage.read(key: _serverPublicKeyKey);
|
||||
_storage.read(key: _serverPublicKeyKey);
|
||||
|
||||
Future<String?> registeredFcmToken() =>
|
||||
pushSecureStorage.read(key: _registeredTokenKey);
|
||||
Future<String?> registeredFcmToken(PushRegistrationType type) =>
|
||||
_storage.read(key: keyFor(_registeredTokenKey, type));
|
||||
|
||||
/// Proxy-server URL the current registration was made with.
|
||||
Future<String?> registeredProxyServer() =>
|
||||
pushSecureStorage.read(key: _proxyServerKey);
|
||||
/// Proxy-server URL the registration of [type] was made with.
|
||||
Future<String?> registeredProxyServer(PushRegistrationType type) =>
|
||||
_storage.read(key: keyFor(_proxyServerKey, type));
|
||||
|
||||
/// Nextcloud base URL the current registration was made against.
|
||||
Future<String?> registeredNcBaseUrl() =>
|
||||
pushSecureStorage.read(key: _ncBaseUrlKey);
|
||||
/// Nextcloud base URL the registration of [type] was made against.
|
||||
Future<String?> registeredNcBaseUrl(PushRegistrationType type) =>
|
||||
_storage.read(key: keyFor(_ncBaseUrlKey, type));
|
||||
|
||||
/// True when a registration has been persisted (used by the cold-start
|
||||
/// self-heal to decide whether to (re-)register).
|
||||
Future<bool> isRegistered() async =>
|
||||
(await registeredFcmToken())?.isNotEmpty ?? false;
|
||||
/// True when a registration of [type] has been persisted.
|
||||
Future<bool> isRegistered(PushRegistrationType type) async =>
|
||||
(await registeredFcmToken(type))?.isNotEmpty ?? false;
|
||||
|
||||
/// Records the outcome of the most recent registration attempt of [type] so
|
||||
/// the push status view can show when it ran and why it failed. [error]
|
||||
/// null = success (stored as empty string).
|
||||
Future<void> saveLastRegistrationAttempt({
|
||||
required PushRegistrationType type,
|
||||
required DateTime at,
|
||||
String? error,
|
||||
}) async {
|
||||
await _storage.write(
|
||||
key: keyFor(_lastAttemptAtKey, type),
|
||||
value: at.toIso8601String(),
|
||||
);
|
||||
await _storage.write(
|
||||
key: keyFor(_lastAttemptErrorKey, type),
|
||||
value: error ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// Timestamp of the last registration attempt of [type], or null when none
|
||||
/// ran yet.
|
||||
Future<DateTime?> lastRegistrationAt(PushRegistrationType type) async {
|
||||
final raw = await _storage.read(key: keyFor(_lastAttemptAtKey, type));
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
return DateTime.tryParse(raw);
|
||||
}
|
||||
|
||||
/// Error text of the last registration attempt of [type], or null when it
|
||||
/// succeeded (or never ran).
|
||||
Future<String?> lastRegistrationError(PushRegistrationType type) async {
|
||||
final raw = await _storage.read(key: keyFor(_lastAttemptErrorKey, type));
|
||||
return (raw == null || raw.isEmpty) ? null : raw;
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
await pushSecureStorage.delete(key: _deviceIdentifierKey);
|
||||
await pushSecureStorage.delete(key: _serverPublicKeyKey);
|
||||
await pushSecureStorage.delete(key: _registeredTokenKey);
|
||||
await pushSecureStorage.delete(key: _proxyServerKey);
|
||||
await pushSecureStorage.delete(key: _ncBaseUrlKey);
|
||||
await pushSecureStorage.delete(key: _usernameKey);
|
||||
await pushSecureStorage.delete(key: _baseUrlKey);
|
||||
for (final type in PushRegistrationType.values) {
|
||||
for (final key in _perTypeKeys) {
|
||||
await _storage.delete(key: keyFor(key, type));
|
||||
}
|
||||
}
|
||||
await _storage.delete(key: _serverPublicKeyKey);
|
||||
await _storage.delete(key: _usernameKey);
|
||||
await _storage.delete(key: _baseUrlKey);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user