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
+58 -1
View File
@@ -11,9 +11,13 @@ import '../push/push_secure_storage.dart';
class AccountData {
static const _usernameField = 'username';
static const _passwordField = 'password';
// App password lives in the push-shared (group-scoped) keystore so the iOS
// App passwords live in the push-shared (group-scoped) keystore so the iOS
// Notification Service Extension can authenticate Nextcloud calls too.
// The talk password authenticates the second (apptype=talk) push
// registration — Nextcloud binds each push subscription to its session
// token, so two registrations need two app passwords.
static const _appPasswordField = 'nextcloud_app_password';
static const _appPasswordTalkField = 'nextcloud_app_password_talk';
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage();
@@ -29,6 +33,7 @@ class AccountData {
String? _username;
String? _password;
String? _appPassword;
String? _appPasswordTalk;
String getUsername() {
if (_username == null) throw Exception('Username not initialized');
@@ -65,9 +70,11 @@ class AccountData {
_username = null;
_password = null;
_appPassword = null;
_appPasswordTalk = null;
await _secureStorage.delete(key: _usernameField);
await _secureStorage.delete(key: _passwordField);
await _clearAppPasswordStorage();
await _clearAppPasswordTalkStorage();
}
/// Persists a freshly minted Nextcloud app password. After this every
@@ -90,6 +97,28 @@ class AccountData {
bool hasAppPassword() => _appPassword != null && _appPassword!.isNotEmpty;
/// Persists the app password backing the Talk push registration.
Future<void> setAppPasswordTalk(String appPassword) async {
_appPasswordTalk = appPassword;
try {
await pushSecureStorage.write(
key: _appPasswordTalkField,
value: appPassword,
);
} on Object {
// Group-scoped keystore may be unavailable — in-memory still works for
// this session, matching setAppPassword.
}
}
Future<void> clearAppPasswordTalk() async {
_appPasswordTalk = null;
await _clearAppPasswordTalkStorage();
}
bool hasAppPasswordTalk() =>
_appPasswordTalk != null && _appPasswordTalk!.isNotEmpty;
Future<void> _clearAppPasswordStorage() async {
try {
await pushSecureStorage.delete(key: _appPasswordField);
@@ -98,14 +127,26 @@ class AccountData {
}
}
Future<void> _clearAppPasswordTalkStorage() async {
try {
await pushSecureStorage.delete(key: _appPasswordTalkField);
} on Object {
// ignore — nothing stored or keystore unavailable
}
}
Future<void> _migrateAndLoad() async {
await _migrateFromLegacyStorage();
_username = await _secureStorage.read(key: _usernameField);
_password = await _secureStorage.read(key: _passwordField);
try {
_appPassword = await pushSecureStorage.read(key: _appPasswordField);
_appPasswordTalk = await pushSecureStorage.read(
key: _appPasswordTalkField,
);
} on Object {
_appPassword = null;
_appPasswordTalk = null;
}
if (!_populated.isCompleted) _populated.complete();
}
@@ -149,6 +190,22 @@ class AccountData {
return 'Basic ${base64Encode(utf8.encode('$_username:$secret'))}';
}
/// Basic-auth header using the Talk app password — authenticates the
/// apptype=talk push registration (and its unregister). Throws when the
/// talk password has not been minted yet; callers treat that as a failed
/// talk registration and retry on the next start.
String getTalkBasicAuthHeader() {
if (!isPopulated()) {
throw Exception(
'AccountData (e.g. username or password) is not initialized!',
);
}
if (!hasAppPasswordTalk()) {
throw StateError('Talk app password not available yet');
}
return 'Basic ${base64Encode(utf8.encode('$_username:$_appPasswordTalk'))}';
}
/// Basic-auth header that always uses the real password. Needed exactly once,
/// to mint the app password via `core/getapppassword` (an app password cannot
/// mint another).