refactor global user variable

This commit is contained in:
otsmr 2026-04-21 03:34:52 +02:00
parent e945e30991
commit 3d35615136
68 changed files with 864 additions and 886 deletions

View file

@ -131,9 +131,9 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (_isUserCreated) { if (_isUserCreated) {
if (_isTwonlyLocked) { if (_isTwonlyLocked) {
// do not change in case twonly was already unlocked at some point // do not change in case twonly was already unlocked at some point
_isTwonlyLocked = gUser.screenLockEnabled; _isTwonlyLocked = AppSession.currentUser.screenLockEnabled;
} }
if (gUser.appVersion < 62) { if (AppSession.currentUser.appVersion < 62) {
_showDatabaseMigration = true; _showDatabaseMigration = true;
} }
} }
@ -176,7 +176,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
_isTwonlyLocked = false; _isTwonlyLocked = false;
}), }),
); );
} else if (gUser.twonlySafeBackup == null && !_skipBackup) { } else if (AppSession.currentUser.twonlySafeBackup == null && !_skipBackup) {
child = SetupBackupView( child = SetupBackupView(
callBack: () { callBack: () {
_skipBackup = true; _skipBackup = true;

View file

@ -38,6 +38,13 @@ late TwonlyDB twonlyDB;
// Cached UserData in the memory. Every time the user data is changed the `updateUserdata` function is called, // Cached UserData in the memory. Every time the user data is changed the `updateUserdata` function is called,
// which will update this global variable. The variable is set in the main.dart and after the user has registered in the register.view.dart // which will update this global variable. The variable is set in the main.dart and after the user has registered in the register.view.dart
late UserData gUser; class AppSession {
static late UserData currentUser;
final userDataUpdateController = StreamController<void>.broadcast(); static final _userDataUpdateController = StreamController<void>.broadcast();
static Stream<void> get onUserUpdated => _userDataUpdateController.stream;
static void triggerUserUpdate() {
_userDataUpdateController.add(null);
}
}

View file

@ -60,7 +60,7 @@ void main() async {
} }
if (user != null) { if (user != null) {
gUser = user; AppSession.currentUser = user;
if (user.allowErrorTrackingViaSentry) { if (user.allowErrorTrackingViaSentry) {
AppState.allowErrorTrackingViaSentry = true; AppState.allowErrorTrackingViaSentry = true;
@ -91,20 +91,18 @@ void main() async {
twonlyDB = TwonlyDB(); twonlyDB = TwonlyDB();
if (user != null) { if (user != null) {
if (gUser.appVersion < 90) { if (AppSession.currentUser.appVersion < 90) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state... // BUG: Requested media files for reupload where not reuploaded because the wrong state...
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState(); await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
await updateUserdata((u) { await updateUser((u) {
u.appVersion = 90; u.appVersion = 90;
return u;
}); });
} }
if (gUser.appVersion < 91) { if (AppSession.currentUser.appVersion < 91) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state... // BUG: Requested media files for reupload where not reuploaded because the wrong state...
await makeMigrationToVersion91(); await makeMigrationToVersion91();
await updateUserdata((u) { await updateUser((u) {
u.appVersion = 91; u.appVersion = 91;
return u;
}); });
} }
} }

View file

@ -140,7 +140,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
t.userDiscoveryVersion.isNotNull() & t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) & t.userDiscoveryExcluded.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue( t.mediaSendCounter.isBiggerOrEqualValue(
gUser.minimumRequiredImagesExchanged, AppSession.currentUser.minimumRequiredImagesExchanged,
), ),
)) ))
.watch(); .watch();

View file

@ -113,7 +113,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
int contactId, int contactId,
GroupsCompanion group, GroupsCompanion group,
) async { ) async {
final groupIdDirectChat = getUUIDforDirectChat(contactId, gUser.userId); final groupIdDirectChat = getUUIDforDirectChat(contactId, AppSession.currentUser.userId);
final insertGroup = group.copyWith( final insertGroup = group.copyWith(
groupId: Value(groupIdDirectChat), groupId: Value(groupIdDirectChat),
isDirectChat: const Value(true), isDirectChat: const Value(true),
@ -209,7 +209,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
} }
Stream<Group?> watchDirectChat(int contactId) { Stream<Group?> watchDirectChat(int contactId) {
final groupId = getUUIDforDirectChat(contactId, gUser.userId); final groupId = getUUIDforDirectChat(contactId, AppSession.currentUser.userId);
return (select( return (select(
groups, groups,
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull(); )..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();

View file

@ -174,9 +174,8 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
// an ok authenticated which is processed in the apiProvider... // an ok authenticated which is processed in the apiProvider...
if (res.isSuccess) { if (res.isSuccess) {
if (Platform.isAndroid) { if (Platform.isAndroid) {
await updateUserdata((u) { await updateUser((u) {
u.subscriptionPlanIdStore = purchaseDetails.productID; u.subscriptionPlanIdStore = purchaseDetails.productID;
return u;
}); });
} }
} }

View file

@ -62,13 +62,15 @@ class ApiService {
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream; Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
final _connectionStateController = StreamController<bool>.broadcast(); final _connectionStateController = StreamController<bool>.broadcast();
Stream<bool> get onConnectionStateUpdated => _connectionStateController.stream; Stream<bool> get onConnectionStateUpdated =>
_connectionStateController.stream;
final _appOutdatedController = StreamController<void>.broadcast(); final _appOutdatedController = StreamController<void>.broadcast();
Stream<void> get onAppOutdated => _appOutdatedController.stream; Stream<void> get onAppOutdated => _appOutdatedController.stream;
final _newDeviceRegisteredController = StreamController<void>.broadcast(); final _newDeviceRegisteredController = StreamController<void>.broadcast();
Stream<void> get onNewDeviceRegistered => _newDeviceRegisteredController.stream; Stream<void> get onNewDeviceRegistered =>
_newDeviceRegisteredController.stream;
bool appIsOutdated = false; bool appIsOutdated = false;
bool isAuthenticated = false; bool isAuthenticated = false;
@ -124,7 +126,7 @@ class ApiService {
unawaited(UserDiscoveryService.checkForNewAnnouncedUsers()); unawaited(UserDiscoveryService.checkForNewAnnouncedUsers());
if (gUser.userStudyParticipantsToken != null) { if (AppSession.currentUser.userStudyParticipantsToken != null) {
// In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection // In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection
unawaited(handleUserStudyUpload()); unawaited(handleUserStudyUpload());
} }
@ -341,9 +343,8 @@ class ApiService {
final ok = res.value as server.Response_Ok; final ok = res.value as server.Response_Ok;
if (ok.hasAuthenticated()) { if (ok.hasAuthenticated()) {
final authenticated = ok.authenticated; final authenticated = ok.authenticated;
await updateUserdata((user) { await updateUser((user) {
user.subscriptionPlan = authenticated.plan; user.subscriptionPlan = authenticated.plan;
return user;
}); });
_planUpdateController.add(planFromString(authenticated.plan)); _planUpdateController.add(planFromString(authenticated.plan));
@ -782,9 +783,8 @@ class ApiService {
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async { Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
final ballance = await getPlanBallance(); final ballance = await getPlanBallance();
if (ballance != null) { if (ballance != null) {
await updateUserdata((u) { await updateUser((u) {
u.lastPlanBallance = ballance.writeToJson(); u.lastPlanBallance = ballance.writeToJson();
return u;
}); });
return ballance; return ballance;
} }

View file

@ -161,7 +161,7 @@ Future<void> handleGroupUpdate(
case GroupActionType.demoteToMember: case GroupActionType.demoteToMember:
int? affectedContactId = update.affectedContactId.toInt(); int? affectedContactId = update.affectedContactId.toInt();
if (affectedContactId == gUser.userId) { if (affectedContactId == AppSession.currentUser.userId) {
affectedContactId = null; affectedContactId = null;
if (actionType == GroupActionType.removedMember) { if (actionType == GroupActionType.removedMember) {
// Oh no, I just got removed from the group... // Oh no, I just got removed from the group...

View file

@ -34,17 +34,17 @@ Future<void> handleUserDiscoveryRequest(
) async { ) async {
Log.info('Got a user discovery request'); Log.info('Got a user discovery request');
if (!gUser.isUserDiscoveryEnabled) { if (!AppSession.currentUser.isUserDiscoveryEnabled) {
Log.warn('Got a user discovery request while it is disabled'); Log.warn('Got a user discovery request while it is disabled');
return; return;
} }
final contact = await twonlyDB.contactsDao.getContactById(fromUserId); final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
if (contact == null) return; if (contact == null) return;
if (contact.mediaSendCounter < gUser.minimumRequiredImagesExchanged || if (contact.mediaSendCounter < AppSession.currentUser.minimumRequiredImagesExchanged ||
contact.userDiscoveryExcluded) { contact.userDiscoveryExcluded) {
Log.warn( Log.warn(
'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${gUser.minimumRequiredImagesExchanged} or user is excluded ${contact.userDiscoveryExcluded}', 'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${AppSession.currentUser.minimumRequiredImagesExchanged} or user is excluded ${contact.userDiscoveryExcluded}',
); );
return; return;
} }
@ -72,7 +72,7 @@ Future<void> handleUserDiscoveryUpdate(
int fromUserId, int fromUserId,
EncryptedContent_UserDiscoveryUpdate update, EncryptedContent_UserDiscoveryUpdate update,
) async { ) async {
if (!gUser.isUserDiscoveryEnabled) { if (!AppSession.currentUser.isUserDiscoveryEnabled) {
Log.warn('Got a user discovery update while it is disabled'); Log.warn('Got a user discovery update while it is disabled');
return; return;
} }

View file

@ -96,7 +96,7 @@ Future<bool> isAllowedToDownload(MediaType type) async {
} }
final connectivityResult = await Connectivity().checkConnectivity(); final connectivityResult = await Connectivity().checkConnectivity();
final options = gUser.autoDownloadOptions ?? defaultAutoDownloadOptions; final options = AppSession.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
if (connectivityResult.contains(ConnectivityResult.mobile)) { if (connectivityResult.contains(ConnectivityResult.mobile)) {
if (type == MediaType.video) { if (type == MediaType.video) {

View file

@ -357,7 +357,7 @@ Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
// if the user has enabled auto storing and the file // if the user has enabled auto storing and the file
// was send with unlimited counter not in twonly-Mode then store the file // was send with unlimited counter not in twonly-Mode then store the file
if (gUser.autoStoreAllSendUnlimitedMediaFiles && if (AppSession.currentUser.autoStoreAllSendUnlimitedMediaFiles &&
!mediaService.mediaFile.requiresAuthentication && !mediaService.mediaFile.requiresAuthentication &&
!mediaService.storedPath.existsSync() && !mediaService.storedPath.existsSync() &&
mediaService.mediaFile.displayLimitInMilliseconds == null) { mediaService.mediaFile.displayLimitInMilliseconds == null) {

View file

@ -345,12 +345,12 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
return null; return null;
} }
} }
encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter); encryptedContent.senderProfileCounter = Int64(AppSession.currentUser.avatarCounter);
if (gUser.isUserDiscoveryEnabled && messageId != null) { if (AppSession.currentUser.isUserDiscoveryEnabled && messageId != null) {
final contact = await twonlyDB.contactsDao.getContactById(contactId); final contact = await twonlyDB.contactsDao.getContactById(contactId);
if (contact != null && if (contact != null &&
contact.mediaSendCounter >= gUser.minimumRequiredImagesExchanged && contact.mediaSendCounter >= AppSession.currentUser.minimumRequiredImagesExchanged &&
!contact.userDiscoveryExcluded) { !contact.userDiscoveryExcluded) {
final version = await UserDiscoveryService.getCurrentVersion(); final version = await UserDiscoveryService.getCurrentVersion();
if (version != null) { if (version != null) {
@ -406,7 +406,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
} }
Future<void> sendTypingIndication(String groupId, bool isTyping) async { Future<void> sendTypingIndication(String groupId, bool isTyping) async {
if (!gUser.typingIndicators) return; if (!AppSession.currentUser.typingIndicators) return;
await sendCipherTextToGroup( await sendCipherTextToGroup(
groupId, groupId,
pb.EncryptedContent( pb.EncryptedContent(
@ -462,15 +462,15 @@ Future<void> notifyContactAboutOpeningMessage(
Future<void> sendContactMyProfileData(int contactId) async { Future<void> sendContactMyProfileData(int contactId) async {
List<int>? avatarSvgCompressed; List<int>? avatarSvgCompressed;
if (gUser.avatarSvg != null) { if (AppSession.currentUser.avatarSvg != null) {
avatarSvgCompressed = gzip.encode(utf8.encode(gUser.avatarSvg!)); avatarSvgCompressed = gzip.encode(utf8.encode(AppSession.currentUser.avatarSvg!));
} }
final encryptedContent = pb.EncryptedContent( final encryptedContent = pb.EncryptedContent(
contactUpdate: pb.EncryptedContent_ContactUpdate( contactUpdate: pb.EncryptedContent_ContactUpdate(
type: pb.EncryptedContent_ContactUpdate_Type.UPDATE, type: pb.EncryptedContent_ContactUpdate_Type.UPDATE,
avatarSvgCompressed: avatarSvgCompressed, avatarSvgCompressed: avatarSvgCompressed,
displayName: gUser.displayName, displayName: AppSession.currentUser.displayName,
username: gUser.username, username: AppSession.currentUser.username,
), ),
); );
await sendCipherText(contactId, encryptedContent); await sendCipherText(contactId, encryptedContent);

View file

@ -263,7 +263,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
await twonlyDB.receiptsDao.markMessagesForRetry(fromUserId); await twonlyDB.receiptsDao.markMessagesForRetry(fromUserId);
final senderProfileCounter = await checkForProfileUpdate(fromUserId, content); final senderProfileCounter = await checkForProfileUpdate(fromUserId, content);
if (gUser.isUserDiscoveryEnabled && content.hasSenderUserDiscoveryVersion()) { if (AppSession.currentUser.isUserDiscoveryEnabled && content.hasSenderUserDiscoveryVersion()) {
await checkForUserDiscoveryChanges( await checkForUserDiscoveryChanges(
fromUserId, fromUserId,
content.senderUserDiscoveryVersion, content.senderUserDiscoveryVersion,
@ -351,7 +351,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
/// Verify that the user is (still) in that group... /// Verify that the user is (still) in that group...
if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) {
if (getUUIDforDirectChat(gUser.userId, fromUserId) == content.groupId) { if (getUUIDforDirectChat(AppSession.currentUser.userId, fromUserId) == content.groupId) {
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull(); .getSingleOrNull();

View file

@ -57,7 +57,7 @@ Future<bool> initBackgroundExecution() async {
// stay alive for multiple hours between task executions // stay alive for multiple hours between task executions
final user = await getUser(); final user = await getUser();
if (user == null) return false; if (user == null) return false;
gUser = user; AppSession.currentUser = user;
return true; return true;
} }
@ -69,7 +69,7 @@ Future<bool> initBackgroundExecution() async {
final user = await getUser(); final user = await getUser();
if (user == null) return false; if (user == null) return false;
gUser = user; AppSession.currentUser = user;
twonlyDB = TwonlyDB(); twonlyDB = TwonlyDB();
apiService = ApiService(); apiService = ApiService();

View file

@ -13,34 +13,35 @@ import 'package:twonly/src/utils/storage.dart';
Future<void> enableTwonlySafe(String password) async { Future<void> enableTwonlySafe(String password) async {
final (backupId, encryptionKey) = await getMasterKey( final (backupId, encryptionKey) = await getMasterKey(
password, password,
gUser.username, AppSession.currentUser.username,
); );
await updateUserdata((user) { await updateUser((user) {
user.twonlySafeBackup = TwonlySafeBackup( user.twonlySafeBackup = TwonlySafeBackup(
encryptionKey: encryptionKey, encryptionKey: encryptionKey,
backupId: backupId, backupId: backupId,
); );
return user;
}); });
unawaited(performTwonlySafeBackup(force: true)); unawaited(performTwonlySafeBackup(force: true));
} }
Future<void> removeTwonlySafeFromServer() async { Future<void> removeTwonlySafeFromServer() async {
final serverUrl = await getTwonlySafeBackupUrl(); final serverUrl = getTwonlySafeBackupUrl();
if (serverUrl != null) { if (serverUrl == null) {
try { Log.error('Could not remove twonly safe as serverUrl is null');
final response = await http.delete( return;
Uri.parse(serverUrl), }
headers: { try {
'Content-Type': 'application/json', // Set the content type if needed final response = await http.delete(
// Add any other headers if required Uri.parse(serverUrl),
}, headers: {
); 'Content-Type': 'application/json', // Set the content type if needed
Log.info('Download deleted with: ${response.statusCode}'); // Add any other headers if required
} catch (e) { },
Log.error('Could not connect upload the backup.'); );
} Log.info('Download deleted with: ${response.statusCode}');
} catch (e) {
Log.error('Could not connect upload the backup.');
} }
} }
@ -63,19 +64,18 @@ Future<(Uint8List, Uint8List)> getMasterKey(
return (key.sublist(0, 32), key.sublist(32, 64)); return (key.sublist(0, 32), key.sublist(32, 64));
} }
Future<String?> getTwonlySafeBackupUrl() async { String? getTwonlySafeBackupUrl() {
final user = await getUser(); if (AppSession.currentUser.twonlySafeBackup == null) return null;
if (user == null || user.twonlySafeBackup == null) return null;
return getTwonlySafeBackupUrlFromServer( return getTwonlySafeBackupUrlFromServer(
user.twonlySafeBackup!.backupId, AppSession.currentUser.twonlySafeBackup!.backupId,
user.backupServer, AppSession.currentUser.backupServer,
); );
} }
Future<String?> getTwonlySafeBackupUrlFromServer( String? getTwonlySafeBackupUrlFromServer(
List<int> backupId, List<int> backupId,
BackupServer? backupServer, BackupServer? backupServer,
) async { ) {
var backupServerUrl = 'https://safe.twonly.eu/'; var backupServerUrl = 'https://safe.twonly.eu/';
if (backupServer != null) { if (backupServer != null) {

View file

@ -19,20 +19,20 @@ import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart';
Future<void> performTwonlySafeBackup({bool force = false}) async { Future<void> performTwonlySafeBackup({bool force = false}) async {
if (gUser.twonlySafeBackup == null) { if (AppSession.currentUser.twonlySafeBackup == null) {
return; return;
} }
if (gUser.twonlySafeBackup!.backupUploadState == if (AppSession.currentUser.twonlySafeBackup!.backupUploadState ==
LastBackupUploadState.pending) { LastBackupUploadState.pending) {
Log.warn('Backup upload is already pending.'); Log.warn('Backup upload is already pending.');
return; return;
} }
final lastUpdateTime = gUser.twonlySafeBackup!.lastBackupDone; final lastUpdateTime =
AppSession.currentUser.twonlySafeBackup!.lastBackupDone;
if (!force && lastUpdateTime != null) { if (!force && lastUpdateTime != null) {
if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) { if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) {
return; return;
@ -120,8 +120,8 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes); final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
if (gUser.twonlySafeBackup!.lastBackupDone == null || if (AppSession.currentUser.twonlySafeBackup!.lastBackupDone == null ||
gUser.twonlySafeBackup!.lastBackupDone!.isAfter( AppSession.currentUser.twonlySafeBackup!.lastBackupDone!.isAfter(
clock.now().subtract(const Duration(days: 90)), clock.now().subtract(const Duration(days: 90)),
)) { )) {
force = true; force = true;
@ -149,7 +149,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
final secretBox = await chacha20.encrypt( final secretBox = await chacha20.encrypt(
backupBytes, backupBytes,
secretKey: SecretKey(gUser.twonlySafeBackup!.encryptionKey), secretKey: SecretKey(
AppSession.currentUser.twonlySafeBackup!.encryptionKey,
),
nonce: nonce, nonce: nonce,
); );
@ -171,12 +173,12 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.', 'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.',
); );
if (gUser.backupServer != null) { if (AppSession.currentUser.backupServer != null) {
if (encryptedBackupBytes.length > gUser.backupServer!.maxBackupBytes) { if (encryptedBackupBytes.length >
AppSession.currentUser.backupServer!.maxBackupBytes) {
Log.error('Backup is to big for the alternative backup server.'); Log.error('Backup is to big for the alternative backup server.');
await updateUserdata((user) { await updateUser((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
return user;
}); });
return; return;
} }
@ -186,7 +188,7 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
taskId: 'backup', taskId: 'backup',
file: encryptedBackupBytesFile, file: encryptedBackupBytesFile,
httpRequestMethod: 'PUT', httpRequestMethod: 'PUT',
url: (await getTwonlySafeBackupUrl())!, url: getTwonlySafeBackupUrl()!,
post: 'binary', post: 'binary',
retries: 2, retries: 2,
headers: { headers: {
@ -195,13 +197,11 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
); );
if (await FileDownloader().enqueue(task)) { if (await FileDownloader().enqueue(task)) {
Log.info('Starting upload from twonly Backup.'); Log.info('Starting upload from twonly Backup.');
await updateUserdata((user) { await updateUser((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending; user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
user.twonlySafeBackup!.lastBackupDone = clock.now(); user.twonlySafeBackup!.lastBackupDone = clock.now();
user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length; user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length;
return user;
}); });
gUpdateBackupView();
} else { } else {
Log.error('Error starting UploadTask for twonly Backup.'); Log.error('Error starting UploadTask for twonly Backup.');
} }
@ -210,26 +210,23 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async { Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) { update.status == TaskStatus.canceled) {
await updateUserdata((user) { await updateUser((user) {
if (user.twonlySafeBackup != null) { if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
} }
return user;
}); });
} else if (update.status == TaskStatus.complete) { } else if (update.status == TaskStatus.complete) {
Log.info( Log.info(
'twonly Backup uploaded with status code ${update.responseStatusCode}', 'twonly Backup uploaded with status code ${update.responseStatusCode}',
); );
await updateUserdata((user) { await updateUser((user) {
if (user.twonlySafeBackup != null) { if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = user.twonlySafeBackup!.backupUploadState =
LastBackupUploadState.success; LastBackupUploadState.success;
} }
return user;
}); });
} else { } else {
Log.info('Backup is in state: ${update.status}'); Log.info('Backup is in state: ${update.status}');
return; return;
} }
gUpdateBackupView();
} }

View file

@ -110,8 +110,7 @@ Future<void> handleBackupData(
key: SecureStorageKeys.userData, key: SecureStorageKeys.userData,
value: secureStorage[SecureStorageKeys.userData] as String, value: secureStorage[SecureStorageKeys.userData] as String,
); );
await updateUserdata((u) { await updateUser((u) {
u.deviceId += 1; u.deviceId += 1;
return u;
}); });
} }

View file

@ -17,10 +17,9 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
(x) => x.totalMediaCounter == maxMessageCounter, (x) => x.totalMediaCounter == maxMessageCounter,
); );
if (gUser.myBestFriendGroupId != bestFriend.groupId) { if (AppSession.currentUser.myBestFriendGroupId != bestFriend.groupId) {
await updateUserdata((user) { await updateUser((user) {
user.myBestFriendGroupId = bestFriend.groupId; user.myBestFriendGroupId = bestFriend.groupId;
return user;
}); });
} }

View file

@ -42,8 +42,8 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
final memberIds = members.map((x) => Int64(x.userId)).toList(); final memberIds = members.map((x) => Int64(x.userId)).toList();
final groupState = EncryptedGroupState( final groupState = EncryptedGroupState(
memberIds: [Int64(gUser.userId)] + memberIds, memberIds: [Int64(AppSession.currentUser.userId)] + memberIds,
adminIds: [Int64(gUser.userId)], adminIds: [Int64(AppSession.currentUser.userId)],
groupName: groupName, groupName: groupName,
deleteMessagesAfterMilliseconds: Int64( deleteMessagesAfterMilliseconds: Int64(
defaultDeleteMessagesAfterMilliseconds, defaultDeleteMessagesAfterMilliseconds,
@ -283,9 +283,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
final myPubKey = keyPair.getPublicKey().serialize().toList(); final myPubKey = keyPair.getPublicKey().serialize().toList();
if (listEquals(appendedPubKey, myPubKey)) { if (listEquals(appendedPubKey, myPubKey)) {
adminIds.remove(Int64(gUser.userId)); adminIds.remove(Int64(AppSession.currentUser.userId));
memberIds.remove( memberIds.remove(
Int64(gUser.userId), Int64(AppSession.currentUser.userId),
); // -> Will remove the user later... ); // -> Will remove the user later...
} else { } else {
Log.info('A non admin left the group!!!'); Log.info('A non admin left the group!!!');
@ -303,7 +303,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
} }
} }
if (!memberIds.contains(Int64(gUser.userId))) { if (!memberIds.contains(Int64(AppSession.currentUser.userId))) {
// OH no, I am no longer a member of this group... // OH no, I am no longer a member of this group...
// Return from the group... // Return from the group...
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
@ -316,7 +316,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
} }
final isGroupAdmin = final isGroupAdmin =
adminIds.firstWhereOrNull((t) => t.toInt() == gUser.userId) != null; adminIds.firstWhereOrNull((t) => t.toInt() == AppSession.currentUser.userId) != null;
if (!listEquals(memberIds, encryptedGroupState.memberIds)) { if (!listEquals(memberIds, encryptedGroupState.memberIds)) {
if (isGroupAdmin) { if (isGroupAdmin) {
@ -368,7 +368,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
// First find and insert NEW members // First find and insert NEW members
for (final memberId in memberIds) { for (final memberId in memberIds) {
if (memberId == Int64(gUser.userId)) { if (memberId == Int64(AppSession.currentUser.userId)) {
continue; continue;
} }
if (currentGroupMembers.any((t) => t.contactId == memberId.toInt())) { if (currentGroupMembers.any((t) => t.contactId == memberId.toInt())) {
@ -838,7 +838,7 @@ Future<bool> removeMemberFromGroup(
groupId: Value(group.groupId), groupId: Value(group.groupId),
type: const Value(GroupActionType.removedMember), type: const Value(GroupActionType.removedMember),
affectedContactId: Value( affectedContactId: Value(
removeContactId == gUser.userId ? null : removeContactId, removeContactId == AppSession.currentUser.userId ? null : removeContactId,
), ),
), ),
); );
@ -945,7 +945,7 @@ Future<bool> leaveAsNonAdminFromGroup(Group group) async {
EncryptedContent( EncryptedContent(
groupUpdate: EncryptedContent_GroupUpdate( groupUpdate: EncryptedContent_GroupUpdate(
groupActionType: groupActionType.name, groupActionType: groupActionType.name,
affectedContactId: Int64(gUser.userId), affectedContactId: Int64(AppSession.currentUser.userId),
), ),
), ),
); );

View file

@ -32,7 +32,7 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
if (!context.mounted) return false; if (!context.mounted) return false;
if (username == gUser.username) { if (username == AppSession.currentUser.username) {
await context.push(Routes.settingsPublicProfile); await context.push(Routes.settingsPublicProfile);
return true; return true;
} }
@ -115,7 +115,7 @@ Future<void> handleIntentMediaFile(
final newMediaService = await initializeMediaUpload( final newMediaService = await initializeMediaUpload(
type, type,
gUser.defaultShowTime, AppSession.currentUser.defaultShowTime,
); );
if (newMediaService == null) { if (newMediaService == null) {
Log.error('Could not create new media file for intent shared file'); Log.error('Could not create new media file for intent shared file');

View file

@ -237,7 +237,7 @@ class MediaFileService {
} }
if (tempPath.existsSync()) { if (tempPath.existsSync()) {
await tempPath.copy(storedPath.path); await tempPath.copy(storedPath.path);
if (gUser.storeMediaFilesInGallery) { if (AppSession.currentUser.storeMediaFilesInGallery) {
if (mediaFile.type == MediaType.video) { if (mediaFile.type == MediaType.video) {
await saveVideoToGallery(storedPath.path); await saveVideoToGallery(storedPath.path);
} else { } else {

View file

@ -48,9 +48,8 @@ Future<void> checkForTokenUpdates() async {
if (storedToken == null || fcmToken != storedToken) { if (storedToken == null || fcmToken != storedToken) {
Log.info('Got new FCM TOKEN.'); Log.info('Got new FCM TOKEN.');
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken); await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
await updateUserdata((u) { await updateUser((u) {
u.updateFCMToken = true; u.updateFCMToken = true;
return u;
}); });
} }
@ -61,9 +60,8 @@ Future<void> checkForTokenUpdates() async {
key: SecureStorageKeys.googleFcm, key: SecureStorageKeys.googleFcm,
value: fcmToken, value: fcmToken,
); );
await updateUserdata((u) { await updateUser((u) {
u.updateFCMToken = true; u.updateFCMToken = true;
return u;
}); });
}) })
.onError((err) { .onError((err) {
@ -75,16 +73,15 @@ Future<void> checkForTokenUpdates() async {
} }
Future<void> initFCMAfterAuthenticated({bool force = false}) async { Future<void> initFCMAfterAuthenticated({bool force = false}) async {
if (gUser.updateFCMToken || force) { if (AppSession.currentUser.updateFCMToken || force) {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm); final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
if (storedToken != null) { if (storedToken != null) {
final res = await apiService.updateFCMToken(storedToken); final res = await apiService.updateFCMToken(storedToken);
if (res.isSuccess) { if (res.isSuccess) {
Log.info('Uploaded new FCM token!'); Log.info('Uploaded new FCM token!');
await updateUserdata((u) { await updateUser((u) {
u.updateFCMToken = false; u.updateFCMToken = false;
return u;
}); });
} else { } else {
Log.error('Could not update FCM token!'); Log.error('Could not update FCM token!');

View file

@ -20,11 +20,12 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
// This function runs after the clients authenticated with the server. // This function runs after the clients authenticated with the server.
// It then checks if it should update a new session key // It then checks if it should update a new session key
Future<void> signalHandleNewServerConnection() async { Future<void> signalHandleNewServerConnection() async {
if (gUser.signalLastSignedPreKeyUpdated != null) { if (AppSession.currentUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48)); final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
final isYoungerThan48Hours = (gUser.signalLastSignedPreKeyUpdated!).isAfter( final isYoungerThan48Hours =
fortyEightHoursAgo, (AppSession.currentUser.signalLastSignedPreKeyUpdated!).isAfter(
); fortyEightHoursAgo,
);
if (isYoungerThan48Hours) { if (isYoungerThan48Hours) {
// The key does live for 48 hours then it expires and a new key is generated. // The key does live for 48 hours then it expires and a new key is generated.
return; return;
@ -35,9 +36,8 @@ Future<void> signalHandleNewServerConnection() async {
Log.error('could not generate a new signed pre key!'); Log.error('could not generate a new signed pre key!');
return; return;
} }
await updateUserdata((user) { await updateUser((user) {
user.signalLastSignedPreKeyUpdated = clock.now(); user.signalLastSignedPreKeyUpdated = clock.now();
return user;
}); });
final res = await apiService.updateSignedPreKey( final res = await apiService.updateSignedPreKey(
signedPreKey.id, signedPreKey.id,
@ -46,9 +46,8 @@ Future<void> signalHandleNewServerConnection() async {
); );
if (res.isError) { if (res.isError) {
Log.error('could not update the signed pre key: ${res.error}'); Log.error('could not update the signed pre key: ${res.error}');
await updateUserdata((user) { await updateUser((user) {
user.signalLastSignedPreKeyUpdated = null; user.signalLastSignedPreKeyUpdated = null;
return user;
}); });
} else { } else {
Log.info('updated signed pre key'); Log.info('updated signed pre key');
@ -60,10 +59,9 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
if (user == null) return []; if (user == null) return [];
final start = user.currentPreKeyIndexStart; final start = user.currentPreKeyIndexStart;
await updateUserdata((user) { await updateUser((user) {
user.currentPreKeyIndexStart = user.currentPreKeyIndexStart =
(user.currentPreKeyIndexStart + 200) % maxValue; (user.currentPreKeyIndexStart + 200) % maxValue;
return user;
}); });
final preKeys = generatePreKeys(start, 200); final preKeys = generatePreKeys(start, 200);
final signalStore = await getSignalStore(); final signalStore = await getSignalStore();
@ -138,9 +136,8 @@ Future<SignedPreKeyRecord?> _getNewSignalSignedPreKey() async {
} }
final signedPreKeyId = user.currentSignedPreKeyIndexStart; final signedPreKeyId = user.currentSignedPreKeyIndexStart;
await updateUserdata((user) { await updateUser((user) {
user.currentSignedPreKeyIndexStart += 1; user.currentSignedPreKeyIndexStart += 1;
return user;
}); });
final signedPreKey = generateSignedPreKey( final signedPreKey = generateSignedPreKey(

View file

@ -1,7 +1,5 @@
// ignore_for_file: constant_identifier_names // ignore_for_file: constant_identifier_names
import 'package:twonly/globals.dart';
enum SubscriptionPlan { enum SubscriptionPlan {
Free, Free,
Tester, Tester,
@ -41,7 +39,3 @@ SubscriptionPlan planFromString(String value) {
} }
return SubscriptionPlan.Free; return SubscriptionPlan.Free;
} }
SubscriptionPlan getCurrentPlan() {
return planFromString(gUser.subscriptionPlan);
}

View file

@ -53,15 +53,14 @@ class UserDiscoveryService {
try { try {
await FlutterUserDiscovery.initializeOrUpdate( await FlutterUserDiscovery.initializeOrUpdate(
threshold: threshold, threshold: threshold,
userId: gUser.userId, userId: AppSession.currentUser.userId,
publicKey: await getUserPublicKey(), publicKey: await getUserPublicKey(),
); );
await updateUserdata((u) { await updateUser(
u (u) => u
..isUserDiscoveryEnabled = true ..isUserDiscoveryEnabled = true
..minimumRequiredImagesExchanged = minimumRequiredImagesExchanged; ..minimumRequiredImagesExchanged = minimumRequiredImagesExchanged,
return u; );
});
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
} }
@ -142,9 +141,8 @@ class UserDiscoveryService {
} }
static Future<void> disable() async { static Future<void> disable() async {
await updateUserdata((u) { await updateUser((u) {
u.isUserDiscoveryEnabled = false; u.isUserDiscoveryEnabled = false;
return u;
}); });
} }
} }

View file

@ -47,13 +47,13 @@ File avatarPNGFile(int contactId) {
} }
Future<Uint8List> getUserAvatar() async { Future<Uint8List> getUserAvatar() async {
if (gUser.avatarSvg == null) { if (AppSession.currentUser.avatarSvg == null) {
final data = await rootBundle.load('assets/images/default_avatar.png'); final data = await rootBundle.load('assets/images/default_avatar.png');
return data.buffer.asUint8List(); return data.buffer.asUint8List();
} }
final pictureInfo = await vg.loadPicture( final pictureInfo = await vg.loadPicture(
SvgStringLoader(gUser.avatarSvg!), SvgStringLoader(AppSession.currentUser.avatarSvg!),
null, null,
); );
@ -62,7 +62,7 @@ Future<Uint8List> getUserAvatar() async {
final byteData = await image.toByteData(format: ui.ImageByteFormat.png); final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List(); final pngBytes = byteData!.buffer.asUint8List();
final file = avatarPNGFile(gUser.userId)..writeAsBytesSync(pngBytes); final file = avatarPNGFile(AppSession.currentUser.userId)..writeAsBytesSync(pngBytes);
pictureInfo.picture.dispose(); pictureInfo.picture.dispose();
return file.readAsBytesSync(); return file.readAsBytesSync();

View file

@ -17,8 +17,8 @@ Future<Uint8List> getProfileQrCodeData() async {
final signedPreKey = (await signalStore.loadSignedPreKeys())[0]; final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
final publicProfile = PublicProfile( final publicProfile = PublicProfile(
userId: Int64(gUser.userId), userId: Int64(AppSession.currentUser.userId),
username: gUser.username, username: AppSession.currentUser.username,
publicIdentityKey: (await signalStore.getIdentityKeyPair()) publicIdentityKey: (await signalStore.getIdentityKeyPair())
.getPublicKey() .getPublicKey()
.serialize(), .serialize(),

View file

@ -16,7 +16,7 @@ Future<bool> isUserCreated() async {
if (user == null) { if (user == null) {
return false; return false;
} }
gUser = user; AppSession.currentUser = user;
return true; return true;
} }
@ -43,9 +43,8 @@ Future<void> updateUsersPlan(
) async { ) async {
context.read<PurchasesProvider>().plan = plan; context.read<PurchasesProvider>().plan = plan;
await updateUserdata((user) { await updateUser((user) {
user.subscriptionPlan = plan.name; user.subscriptionPlan = plan.name;
return user;
}); });
if (!context.mounted) return; if (!context.mounted) return;
@ -54,27 +53,25 @@ Future<void> updateUsersPlan(
Mutex updateProtection = Mutex(); Mutex updateProtection = Mutex();
Future<UserData?> updateUserdata( Future<void> updateUser(
UserData Function(UserData userData) updateUser, void Function(UserData userData) updateUser,
) async { ) async {
final userData = await updateProtection.protect<UserData?>(() async { await updateProtection.protect(() async {
final user = await getUser(); final user = await getUser();
if (user == null) return null; if (user == null) return;
if (user.defaultShowTime == 999999) { if (user.defaultShowTime == 999999) {
// This was the old version for infinity -> change it to null // This was the old version for infinity -> change it to null
user.defaultShowTime = null; user.defaultShowTime = null;
} }
final updated = updateUser(user); updateUser(user);
await const FlutterSecureStorage().write( await const FlutterSecureStorage().write(
key: SecureStorageKeys.userData, key: SecureStorageKeys.userData,
value: jsonEncode(updated), value: jsonEncode(user),
); );
gUser = updated; AppSession.currentUser = user;
return updated;
}); });
userDataUpdateController.add(null);
return userData; AppSession.triggerUserUpdate();
} }
Future<bool> deleteLocalUserData() async { Future<bool> deleteLocalUserData() async {

View file

@ -208,10 +208,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Future<void> initAsync() async { Future<void> initAsync() async {
_hasAudioPermission = await Permission.microphone.isGranted; _hasAudioPermission = await Permission.microphone.isGranted;
if (!_hasAudioPermission && !gUser.requestedAudioPermission) { if (!_hasAudioPermission &&
await updateUserdata((u) { !AppSession.currentUser.requestedAudioPermission) {
await updateUser((u) {
u.requestedAudioPermission = true; u.requestedAudioPermission = true;
return u;
}); });
await requestMicrophonePermission(); await requestMicrophonePermission();
} }
@ -321,7 +321,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
((videoFilePath != null) ? MediaType.video : MediaType.image); ((videoFilePath != null) ? MediaType.video : MediaType.image);
final mediaFileService = await initializeMediaUpload( final mediaFileService = await initializeMediaUpload(
type, type,
gUser.defaultShowTime, AppSession.currentUser.defaultShowTime,
isDraftMedia: true, isDraftMedia: true,
); );
if (!mounted) return true; if (!mounted) return true;

View file

@ -135,7 +135,7 @@ class MainCameraController {
await cameraController?.initialize(); await cameraController?.initialize();
await cameraController?.startImageStream(_processCameraImage); await cameraController?.startImageStream(_processCameraImage);
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
if (gUser.videoStabilizationEnabled && !kDebugMode) { if (AppSession.currentUser.videoStabilizationEnabled && !kDebugMode) {
await cameraController?.setVideoStabilizationMode( await cameraController?.setVideoStabilizationMode(
VideoStabilizationMode.level1, VideoStabilizationMode.level1,
); );
@ -395,7 +395,7 @@ class MainCameraController {
} }
} }
} else { } else {
if (profile.username != gUser.username) { if (profile.username != AppSession.currentUser.username) {
if (scannedNewProfiles[profile.userId.toInt()] == null) { if (scannedNewProfiles[profile.userId.toInt()] == null) {
await HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile( scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile(

View file

@ -254,7 +254,7 @@ class _ShareImageView extends State<ShareImageView> {
children: [ children: [
if (widget.mediaFileService.mediaFile.type == MediaType.image && if (widget.mediaFileService.mediaFile.type == MediaType.image &&
_screenshotImage?.image != null && _screenshotImage?.image != null &&
gUser.showShowImagePreviewWhenSending) AppSession.currentUser.showShowImagePreviewWhenSending)
SizedBox( SizedBox(
height: 100, height: 100,
width: 100 * 9 / 16, width: 100 * 9 / 16,

View file

@ -158,9 +158,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
if (storeAsDefault) { if (storeAsDefault) {
await updateUserdata((user) { await updateUser((user) {
user.defaultShowTime = maxShowTime; user.defaultShowTime = maxShowTime;
return user;
}); });
} }
} }

View file

@ -92,7 +92,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
} }
Future<void> _requestNewUserByUsername(String username) async { Future<void> _requestNewUserByUsername(String username) async {
if (gUser.username == username) return; if (AppSession.currentUser.username == username) return;
setState(() { setState(() {
_isLoading = true; _isLoading = true;

View file

@ -27,6 +27,7 @@ class ChatListView extends StatefulWidget {
} }
class _ChatListViewState extends State<ChatListView> { class _ChatListViewState extends State<ChatListView> {
StreamSubscription<void>? _userSub;
late StreamSubscription<List<Group>> _contactsSub; late StreamSubscription<List<Group>> _contactsSub;
List<Group> _groupsNotPinned = []; List<Group> _groupsNotPinned = [];
List<Group> _groupsPinned = []; List<Group> _groupsPinned = [];
@ -43,6 +44,9 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
void initState() { void initState() {
initAsync(); initAsync();
_userSub = AppSession.onUserUpdated.listen((_) {
if (mounted) setState(() {});
});
super.initState(); super.initState();
} }
@ -85,11 +89,11 @@ class _ChatListViewState extends State<ChatListView> {
Sha256().hash, Sha256().hash,
changeLog.codeUnits, changeLog.codeUnits,
)).bytes; )).bytes;
if (!gUser.hideChangeLog && if (!AppSession.currentUser.hideChangeLog &&
gUser.lastChangeLogHash.toString() != changeLogHash.toString()) { AppSession.currentUser.lastChangeLogHash.toString() !=
await updateUserdata((u) { changeLogHash.toString()) {
await updateUser((u) {
u.lastChangeLogHash = changeLogHash; u.lastChangeLogHash = changeLogHash;
return u;
}); });
if (!mounted) return; if (!mounted) return;
// only show changelog to people who already have contacts // only show changelog to people who already have contacts
@ -109,6 +113,7 @@ class _ChatListViewState extends State<ChatListView> {
_contactsSub.cancel(); _contactsSub.cancel();
_countContactRequestStream.cancel(); _countContactRequestStream.cancel();
_countAnnouncedStream.cancel(); _countAnnouncedStream.cancel();
_userSub?.cancel();
super.dispose(); super.dispose();
} }
@ -122,9 +127,7 @@ class _ChatListViewState extends State<ChatListView> {
ConnectionStatusBadge( ConnectionStatusBadge(
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: () async {
await context.push(Routes.settingsProfile); context.push(Routes.settingsProfile);
if (!mounted) return;
setState(() {}); // gUser has updated
}, },
child: AvatarIcon( child: AvatarIcon(
myAvatar: true, myAvatar: true,
@ -199,8 +202,7 @@ class _ChatListViewState extends State<ChatListView> {
IconButton( IconButton(
onPressed: () async { onPressed: () async {
await context.push(Routes.settings); context.push(Routes.settings);
if (mounted) setState(() {}); // gUser may has changed...
}, },
icon: const FaIcon(FontAwesomeIcons.gear, size: 19), icon: const FaIcon(FontAwesomeIcons.gear, size: 19),
), ),

View file

@ -122,7 +122,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
_receiverDeletedAccount = groupContacts.first.accountDeleted; _receiverDeletedAccount = groupContacts.first.accountDeleted;
} }
if (gUser.typingIndicators) { if (AppSession.currentUser.typingIndicators) {
unawaited(sendTypingIndication(widget.groupId, false)); unawaited(sendTypingIndication(widget.groupId, false));
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 4), ( _nextTypingIndicator = Timer.periodic(const Duration(seconds: 4), (
_, _,
@ -287,7 +287,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
itemScrollController: itemScrollController, itemScrollController: itemScrollController,
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == 0) { if (i == 0) {
return gUser.typingIndicators return AppSession.currentUser.typingIndicators
? TypingIndicator(group: group) ? TypingIndicator(group: group)
: Container(); : Container();
} }

View file

@ -102,7 +102,7 @@ class _ContactRowState extends State<_ContactRow> {
bool _isLoading = false; bool _isLoading = false;
Future<void> _onContactClick(bool isAdded) async { Future<void> _onContactClick(bool isAdded) async {
if (widget.contact.userId.toInt() == gUser.userId) { if (widget.contact.userId.toInt() == AppSession.currentUser.userId) {
await context.push(Routes.settingsProfile); await context.push(Routes.settingsProfile);
return; return;
} }
@ -162,7 +162,7 @@ class _ContactRowState extends State<_ContactRow> {
final contactInDb = snapshot.data; final contactInDb = snapshot.data;
final isAdded = final isAdded =
contactInDb != null || contactInDb != null ||
widget.contact.userId.toInt() == gUser.userId; widget.contact.userId.toInt() == AppSession.currentUser.userId;
return GestureDetector( return GestureDetector(
onTap: _isLoading ? null : () => _onContactClick(isAdded), onTap: _isLoading ? null : () => _onContactClick(isAdded),

View file

@ -72,7 +72,7 @@ class _MessageInputState extends State<MessageInput> {
_textFieldController.text = widget.group.draftMessage!; _textFieldController.text = widget.group.draftMessage!;
} }
widget.textFieldFocus.addListener(_handleTextFocusChange); widget.textFieldFocus.addListener(_handleTextFocusChange);
if (gUser.typingIndicators) { if (AppSession.currentUser.typingIndicators) {
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 1), ( _nextTypingIndicator = Timer.periodic(const Duration(seconds: 1), (
_, _,
) async { ) async {

View file

@ -50,8 +50,8 @@ class _ReactionButtonsState extends State<ReactionButtons> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
if (gUser.preSelectedEmojies != null) { if (AppSession.currentUser.preSelectedEmojies != null) {
selectedEmojis = gUser.preSelectedEmojies!; selectedEmojis = AppSession.currentUser.preSelectedEmojies!;
} }
setState(() {}); setState(() {});
} }

View file

@ -33,7 +33,7 @@ class _AvatarIconState extends State<AvatarIcon> {
StreamSubscription<List<Contact>>? groupStream; StreamSubscription<List<Contact>>? groupStream;
StreamSubscription<List<Contact>>? contactsStream; StreamSubscription<List<Contact>>? contactsStream;
StreamSubscription<Contact?>? contactStream; StreamSubscription<Contact?>? contactStream;
StreamSubscription<void>? _userDataSub; StreamSubscription<void>? _userSub;
@override @override
void initState() { void initState() {
@ -46,7 +46,7 @@ class _AvatarIconState extends State<AvatarIcon> {
groupStream?.cancel(); groupStream?.cancel();
contactStream?.cancel(); contactStream?.cancel();
contactsStream?.cancel(); contactsStream?.cancel();
_userDataSub?.cancel(); _userSub?.cancel();
super.dispose(); super.dispose();
} }
@ -93,19 +93,19 @@ class _AvatarIconState extends State<AvatarIcon> {
setState(() {}); setState(() {});
}); });
} else if (widget.myAvatar) { } else if (widget.myAvatar) {
_userDataSub = userDataUpdateController.stream.listen((_) { _userSub = AppSession.onUserUpdated.listen((_) {
if (mounted) { if (mounted) {
setState(() { setState(() {
if (gUser.avatarSvg != null) { if (AppSession.currentUser.avatarSvg != null) {
_avatarSvg = gUser.avatarSvg; _avatarSvg = AppSession.currentUser.avatarSvg;
} else { } else {
_avatarContacts = []; _avatarContacts = [];
} }
}); });
} }
}); });
if (gUser.avatarSvg != null) { if (AppSession.currentUser.avatarSvg != null) {
_avatarSvg = gUser.avatarSvg; _avatarSvg = AppSession.currentUser.avatarSvg;
} }
} else if (widget.contactId != null) { } else if (widget.contactId != null) {
contactStream = twonlyDB.contactsDao contactStream = twonlyDB.contactsDao

View file

@ -48,7 +48,7 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
} }
if (groupId != null && group != null) { if (groupId != null && group != null) {
isBestFriend = isBestFriend =
gUser.myBestFriendGroupId == groupId && group.alsoBestFriend; AppSession.currentUser.myBestFriendGroupId == groupId && group.alsoBestFriend;
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId); final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
flameCounterSub = stream.listen((counter) { flameCounterSub = stream.listen((counter) {
if (mounted) { if (mounted) {

View file

@ -38,7 +38,10 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
@override @override
void initState() { void initState() {
_groupId = getUUIDforDirectChat(widget.contactId, gUser.userId); _groupId = getUUIDforDirectChat(
widget.contactId,
AppSession.currentUser.userId,
);
final stream = twonlyDB.groupsDao.watchGroup(_groupId); final stream = twonlyDB.groupsDao.watchGroup(_groupId);
_groupSub = stream.listen((update) { _groupSub = stream.listen((update) {
if (mounted) setState(() => _group = update); if (mounted) setState(() => _group = update);
@ -53,7 +56,8 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
} }
Future<void> _restoreFlames() async { Future<void> _restoreFlames() async {
if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames) && final currentPlan = planFromString(AppSession.currentUser.subscriptionPlan);
if (!isUserAllowed(currentPlan, PremiumFeatures.RestoreFlames) &&
kReleaseMode) { kReleaseMode) {
await context.push(Routes.settingsSubscription); await context.push(Routes.settingsSubscription);
return; return;

View file

@ -204,7 +204,7 @@ class _ContactViewState extends State<ContactView> {
}, },
), ),
SelectChatDeletionTimeListTitle( SelectChatDeletionTimeListTitle(
groupId: getUUIDforDirectChat(widget.userId, gUser.userId), groupId: getUUIDforDirectChat(widget.userId, AppSession.currentUser.userId),
), ),
const Divider(), const Divider(),
MaxFlameListTitle( MaxFlameListTitle(
@ -222,17 +222,17 @@ class _ContactViewState extends State<ContactView> {
setState(() {}); setState(() {});
}, },
), ),
if (gUser.isUserDiscoveryEnabled) if (AppSession.currentUser.isUserDiscoveryEnabled)
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.usersViewfinder, icon: FontAwesomeIcons.usersViewfinder,
text: context.lang.userDiscoverySettingsTitle, text: context.lang.userDiscoverySettingsTitle,
subtitle: subtitle:
!contact.userDiscoveryExcluded && !contact.userDiscoveryExcluded &&
contact.mediaSendCounter < contact.mediaSendCounter <
gUser.minimumRequiredImagesExchanged AppSession.currentUser.minimumRequiredImagesExchanged
? Text( ? Text(
context.lang.contactUserDiscoveryImagesLeft( context.lang.contactUserDiscoveryImagesLeft(
gUser.minimumRequiredImagesExchanged - AppSession.currentUser.minimumRequiredImagesExchanged -
contact.mediaSendCounter, contact.mediaSendCounter,
getContactDisplayName(contact), getContactDisplayName(contact),
), ),

View file

@ -142,7 +142,7 @@ class _GroupViewState extends State<GroupView> {
success = await removeMemberFromGroup( success = await removeMemberFromGroup(
_group!, _group!,
keyPair.getPublicKey().serialize(), keyPair.getPublicKey().serialize(),
gUser.userId, AppSession.currentUser.userId,
); );
} else { } else {
success = await leaveAsNonAdminFromGroup(_group!); success = await leaveAsNonAdminFromGroup(_group!);

View file

@ -135,7 +135,7 @@ class HomeViewState extends State<HomeView> {
_mainCameraController.setSharedLinkForPreview, _mainCameraController.setSharedLinkForPreview,
); );
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.initialPage == 1 && !gUser.startWithCameraOpen || if (widget.initialPage == 1 && !AppSession.currentUser.startWithCameraOpen ||
widget.initialPage == 0) { widget.initialPage == 0) {
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
} }

View file

@ -98,7 +98,7 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
final newMediaService = await initializeMediaUpload( final newMediaService = await initializeMediaUpload(
orgMediaService.mediaFile.type, orgMediaService.mediaFile.type,
gUser.defaultShowTime, AppSession.currentUser.defaultShowTime,
); );
if (newMediaService == null) { if (newMediaService == null) {
Log.error('Could not create new mediaFIle'); Log.error('Could not create new mediaFIle');

View file

@ -142,7 +142,7 @@ class _RegisterViewState extends State<RegisterView> {
value: jsonEncode(userData), value: jsonEncode(userData),
); );
gUser = userData; AppSession.currentUser = userData;
await apiService.authenticate(); await apiService.authenticate();
widget.callbackOnSuccess(); widget.callbackOnSuccess();

View file

@ -107,7 +107,7 @@ class _PublicProfileViewState extends State<PublicProfileView> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
gUser.username, AppSession.currentUser.username,
style: const TextStyle(fontSize: 24), style: const TextStyle(fontSize: 24),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@ -126,11 +126,11 @@ class _PublicProfileViewState extends State<PublicProfileView> {
text: context.lang.shareYourProfile, text: context.lang.shareYourProfile,
subtitle: (_publicKey == null) subtitle: (_publicKey == null)
? null ? null
: Text('https://me.twonly.eu/${gUser.username}'), : Text('https://me.twonly.eu/${AppSession.currentUser.username}'),
onTap: () { onTap: () {
final params = ShareParams( final params = ShareParams(
text: text:
'https://me.twonly.eu/${gUser.username}#${base64Url.encode(_publicKey!)}', 'https://me.twonly.eu/${AppSession.currentUser.username}#${base64Url.encode(_publicKey!)}',
); );
SharePlus.instance.share(params); SharePlus.instance.share(params);
}, },

View file

@ -73,32 +73,20 @@ class _AppearanceViewState extends State<AppearanceView> {
} }
Future<void> toggleShowFeedbackIcon() async { Future<void> toggleShowFeedbackIcon() async {
await updateUserdata((u) { await updateUser((u) {
u.showFeedbackShortcut = !u.showFeedbackShortcut; u.showFeedbackShortcut = !u.showFeedbackShortcut;
return u;
});
setState(() {
// gUser
}); });
} }
Future<void> toggleStartWithCameraOpen() async { Future<void> toggleStartWithCameraOpen() async {
await updateUserdata((u) { await updateUser((u) {
u.startWithCameraOpen = !u.startWithCameraOpen; u.startWithCameraOpen = !u.startWithCameraOpen;
return u;
});
setState(() {
// gUser
}); });
} }
Future<void> toggleShowImagePreviewWhenSending() async { Future<void> toggleShowImagePreviewWhenSending() async {
await updateUserdata((u) { await updateUser((u) {
u.showShowImagePreviewWhenSending = !u.showShowImagePreviewWhenSending; u.showShowImagePreviewWhenSending = !u.showShowImagePreviewWhenSending;
return u;
});
setState(() {
// gUser
}); });
} }
@ -109,43 +97,48 @@ class _AppearanceViewState extends State<AppearanceView> {
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsAppearance), title: Text(context.lang.settingsAppearance),
), ),
body: ListView( body: StreamBuilder<void>(
children: [ stream: AppSession.onUserUpdated,
ListTile( builder: (context, snapshot) {
title: Text(context.lang.settingsAppearanceTheme), return ListView(
subtitle: Text( children: [
selectedTheme.name, ListTile(
style: const TextStyle(color: Colors.grey), title: Text(context.lang.settingsAppearanceTheme),
), subtitle: Text(
onTap: () async { selectedTheme.name,
await _showSelectThemeMode(context); style: const TextStyle(color: Colors.grey),
}, ),
), onTap: () async {
ListTile( await _showSelectThemeMode(context);
title: Text(context.lang.contactUsShortcut), },
onTap: toggleShowFeedbackIcon, ),
trailing: Switch( ListTile(
value: !gUser.showFeedbackShortcut, title: Text(context.lang.contactUsShortcut),
onChanged: (a) => toggleShowFeedbackIcon(), onTap: toggleShowFeedbackIcon,
), trailing: Switch(
), value: !AppSession.currentUser.showFeedbackShortcut,
ListTile( onChanged: (a) => toggleShowFeedbackIcon(),
title: Text(context.lang.startWithCameraOpen), ),
onTap: toggleStartWithCameraOpen, ),
trailing: Switch( ListTile(
value: gUser.startWithCameraOpen, title: Text(context.lang.startWithCameraOpen),
onChanged: (a) => toggleStartWithCameraOpen(), onTap: toggleStartWithCameraOpen,
), trailing: Switch(
), value: AppSession.currentUser.startWithCameraOpen,
ListTile( onChanged: (a) => toggleStartWithCameraOpen(),
title: Text(context.lang.showImagePreviewWhenSending), ),
onTap: toggleShowImagePreviewWhenSending, ),
trailing: Switch( ListTile(
value: gUser.showShowImagePreviewWhenSending, title: Text(context.lang.showImagePreviewWhenSending),
onChanged: (a) => toggleShowImagePreviewWhenSending(), onTap: toggleShowImagePreviewWhenSending,
), trailing: Switch(
), value: AppSession.currentUser.showShowImagePreviewWhenSending,
], onChanged: (a) => toggleShowImagePreviewWhenSending(),
),
),
],
);
},
), ),
); );
} }

View file

@ -8,8 +8,6 @@ import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/backup/create.backup.dart'; import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
void Function() gUpdateBackupView = () {};
class BackupView extends StatefulWidget { class BackupView extends StatefulWidget {
const BackupView({super.key}); const BackupView({super.key});
@ -34,18 +32,9 @@ class _BackupViewState extends State<BackupView> {
void initState() { void initState() {
super.initState(); super.initState();
unawaited(initAsync()); unawaited(initAsync());
gUpdateBackupView = initAsync;
} }
@override Future<void> initAsync() async {}
void dispose() {
gUpdateBackupView = () {};
super.dispose();
}
Future<void> initAsync() async {
setState(() {});
}
String backupStatus(LastBackupUploadState status) { String backupStatus(LastBackupUploadState status) {
switch (status) { switch (status) {
@ -62,156 +51,170 @@ class _BackupViewState extends State<BackupView> {
Future<void> changeTwonlySafePassword() async { Future<void> changeTwonlySafePassword() async {
await context.push(Routes.settingsBackupSetup, extra: true); await context.push(Routes.settingsBackupSetup, extra: true);
setState(() {
// gUser was updated
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backupServer = gUser.backupServer ?? defaultBackupServer; return StreamBuilder<void>(
return Scaffold( stream: AppSession.onUserUpdated,
appBar: AppBar( builder: (context, _) {
title: Text(context.lang.settingsBackup), final backupServer =
), AppSession.currentUser.backupServer ?? defaultBackupServer;
body: PageView( return Scaffold(
controller: pageController, appBar: AppBar(
onPageChanged: (index) { title: Text(context.lang.settingsBackup),
setState(() { ),
activePageIdx = index; body: PageView(
}); controller: pageController,
}, onPageChanged: (index) {
children: [ setState(() {
BackupOption( activePageIdx = index;
title: 'twonly Backup', });
description: context.lang.backupTwonlySafeDesc, },
bottomButton: FilledButton( children: [
onPressed: changeTwonlySafePassword, BackupOption(
child: Text(context.lang.backupChangePassword), title: 'twonly Backup',
), description: context.lang.backupTwonlySafeDesc,
child: (gUser.twonlySafeBackup == null) bottomButton: FilledButton(
? null onPressed: changeTwonlySafePassword,
: Column( child: Text(context.lang.backupChangePassword),
children: [ ),
Table( child: (AppSession.currentUser.twonlySafeBackup == null)
defaultVerticalAlignment: ? null
TableCellVerticalAlignment.middle, : Column(
children: [ children: [
...[ Table(
( defaultVerticalAlignment:
context.lang.backupServer, TableCellVerticalAlignment.middle,
(backupServer.serverUrl.contains('@')) children: [
? backupServer.serverUrl.split('@')[1] ...[
: backupServer.serverUrl.replaceAll( (
'https://', context.lang.backupServer,
'', (backupServer.serverUrl.contains('@'))
), ? backupServer.serverUrl.split('@')[1]
), : backupServer.serverUrl.replaceAll(
( 'https://',
context.lang.backupMaxBackupSize, '',
formatBytes(backupServer.maxBackupBytes), ),
),
(
context.lang.backupStorageRetention,
'${backupServer.retentionDays} Days',
),
(
context.lang.backupLastBackupDate,
formatDateTime(
context,
gUser.twonlySafeBackup!.lastBackupDone,
),
),
(
context.lang.backupLastBackupSize,
formatBytes(
gUser.twonlySafeBackup!.lastBackupSize,
),
),
(
context.lang.backupLastBackupResult,
backupStatus(
gUser.twonlySafeBackup!.backupUploadState,
),
),
].map((pair) {
return TableRow(
children: [
TableCell(
// padding: EdgeInsets.all(4),
child: Text(pair.$1),
), ),
TableCell( (
child: Padding( context.lang.backupMaxBackupSize,
padding: const EdgeInsets.symmetric( formatBytes(backupServer.maxBackupBytes),
vertical: 4, ),
), (
child: Text( context.lang.backupStorageRetention,
pair.$2, '${backupServer.retentionDays} Days',
textAlign: TextAlign.right, ),
), (
context.lang.backupLastBackupDate,
formatDateTime(
context,
AppSession
.currentUser
.twonlySafeBackup!
.lastBackupDone,
), ),
), ),
], (
); context.lang.backupLastBackupSize,
}), formatBytes(
AppSession
.currentUser
.twonlySafeBackup!
.lastBackupSize,
),
),
(
context.lang.backupLastBackupResult,
backupStatus(
AppSession
.currentUser
.twonlySafeBackup!
.backupUploadState,
),
),
].map((pair) {
return TableRow(
children: [
TableCell(
// padding: EdgeInsets.all(4),
child: Text(pair.$1),
),
TableCell(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
),
child: Text(
pair.$2,
textAlign: TextAlign.right,
),
),
),
],
);
}),
],
),
const SizedBox(height: 10),
OutlinedButton(
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
await performTwonlySafeBackup(force: true);
setState(() {
isLoading = false;
});
},
child: Text(context.lang.backupTwonlySaveNow),
),
], ],
), ),
const SizedBox(height: 10), ),
OutlinedButton( BackupOption(
onPressed: isLoading title: '${context.lang.backupData} (Coming Soon)',
? null description: context.lang.backupDataDesc,
: () async { ),
setState(() { ],
isLoading = true;
});
await performTwonlySafeBackup(force: true);
setState(() {
isLoading = false;
});
},
child: Text(context.lang.backupTwonlySaveNow),
),
],
),
), ),
BackupOption( bottomNavigationBar: BottomNavigationBar(
title: '${context.lang.backupData} (Coming Soon)', showSelectedLabels: true,
description: context.lang.backupDataDesc, showUnselectedLabels: true,
unselectedIconTheme: IconThemeData(
color: Theme.of(
context,
).colorScheme.inverseSurface.withAlpha(150),
),
selectedIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.inverseSurface,
),
items: [
const BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.vault, size: 17),
label: 'twonly Backup',
),
BottomNavigationBarItem(
icon: const FaIcon(Icons.archive_outlined, size: 17),
label: context.lang.backupData,
),
],
onTap: (index) async {
activePageIdx = index;
await pageController.animateToPage(
index,
duration: const Duration(milliseconds: 100),
curve: Curves.bounceIn,
);
if (mounted) setState(() {});
},
currentIndex: activePageIdx,
// ),
), ),
], );
), },
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: true,
showUnselectedLabels: true,
unselectedIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150),
),
selectedIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.inverseSurface,
),
items: [
const BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.vault, size: 17),
label: 'twonly Backup',
),
BottomNavigationBarItem(
icon: const FaIcon(Icons.archive_outlined, size: 17),
label: context.lang.backupData,
),
],
onTap: (index) async {
activePageIdx = index;
await pageController.animateToPage(
index,
duration: const Duration(milliseconds: 100),
curve: Curves.bounceIn,
);
if (mounted) setState(() {});
},
currentIndex: activePageIdx,
// ),
),
); );
} }
} }

View file

@ -31,8 +31,8 @@ class _BackupServerViewState extends State<BackupServerView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
if (gUser.backupServer != null) { if (AppSession.currentUser.backupServer != null) {
final uri = Uri.parse(gUser.backupServer!.serverUrl); final uri = Uri.parse(AppSession.currentUser.backupServer!.serverUrl);
// remove user auth data // remove user auth data
final serverUrl = Uri( final serverUrl = Uri(
scheme: uri.scheme, scheme: uri.scheme,
@ -79,9 +79,8 @@ class _BackupServerViewState extends State<BackupServerView> {
retentionDays: data['retentionDays']! as int, retentionDays: data['retentionDays']! as int,
maxBackupBytes: data['maxBackupBytes']! as int, maxBackupBytes: data['maxBackupBytes']! as int,
); );
await updateUserdata((user) { await updateUser((user) {
user.backupServer = backupServer; user.backupServer = backupServer;
return user;
}); });
if (mounted) Navigator.pop(context, backupServer); if (mounted) Navigator.pop(context, backupServer);
} else { } else {
@ -166,9 +165,8 @@ class _BackupServerViewState extends State<BackupServerView> {
Center( Center(
child: OutlinedButton( child: OutlinedButton(
onPressed: () async { onPressed: () async {
await updateUserdata((user) { await updateUser((user) {
user.backupServer = null; user.backupServer = null;
return user;
}); });
if (context.mounted) Navigator.pop(context); if (context.mounted) Navigator.pop(context);
}, },

View file

@ -38,9 +38,8 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
} else { } else {
if (selectedEmojis.length < 12) { if (selectedEmojis.length < 12) {
selectedEmojis.add(emoji); selectedEmojis.add(emoji);
await updateUserdata((user) { await updateUser((user) {
user.preSelectedEmojies = selectedEmojis; user.preSelectedEmojies = selectedEmojis;
return user;
}); });
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -99,9 +98,8 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
6, 6,
); );
setState(() {}); setState(() {});
await updateUserdata((user) { await updateUser((user) {
user.preSelectedEmojies = selectedEmojis; user.preSelectedEmojies = selectedEmojis;
return user;
}); });
}, },
child: const Icon(Icons.settings_backup_restore_rounded), child: const Icon(Icons.settings_backup_restore_rounded),

View file

@ -27,110 +27,121 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
builder: (context) { builder: (context) {
return AutoDownloadOptionsDialog( return AutoDownloadOptionsDialog(
autoDownloadOptions: autoDownloadOptions:
gUser.autoDownloadOptions ?? defaultAutoDownloadOptions, AppSession.currentUser.autoDownloadOptions ??
defaultAutoDownloadOptions,
connectionMode: connectionMode, connectionMode: connectionMode,
onUpdate: () async { onUpdate: () {},
setState(() {});
},
); );
}, },
); );
} }
Future<void> toggleStoreInGallery() async { Future<void> toggleStoreInGallery() async {
await updateUserdata((u) { await updateUser((u) {
u.storeMediaFilesInGallery = !u.storeMediaFilesInGallery; u.storeMediaFilesInGallery = !u.storeMediaFilesInGallery;
return u;
}); });
setState(() {});
} }
Future<void> toggleAutoStoreMediaFiles() async { Future<void> toggleAutoStoreMediaFiles() async {
await updateUserdata((u) { await updateUser((u) {
u.autoStoreAllSendUnlimitedMediaFiles = u.autoStoreAllSendUnlimitedMediaFiles =
!u.autoStoreAllSendUnlimitedMediaFiles; !u.autoStoreAllSendUnlimitedMediaFiles;
return u;
}); });
setState(() {});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final autoDownloadOptions =
gUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsStorageData), title: Text(context.lang.settingsStorageData),
), ),
body: ListView( body: StreamBuilder<void>(
children: [ stream: AppSession.onUserUpdated,
ListTile( builder: (context, _) {
title: Text(context.lang.settingsStorageDataStoreInGTitle), final autoDownloadOptions =
subtitle: Text(context.lang.settingsStorageDataStoreInGSubtitle), AppSession.currentUser.autoDownloadOptions ??
onTap: toggleStoreInGallery, defaultAutoDownloadOptions;
trailing: Switch( return ListView(
value: gUser.storeMediaFilesInGallery, children: [
onChanged: (a) => toggleStoreInGallery(), ListTile(
), title: Text(context.lang.settingsStorageDataStoreInGTitle),
), subtitle: Text(
ListTile( context.lang.settingsStorageDataStoreInGSubtitle,
title: Text(context.lang.autoStoreAllSendUnlimitedMediaFiles), ),
subtitle: Text( onTap: toggleStoreInGallery,
context.lang.autoStoreAllSendUnlimitedMediaFilesSubtitle, trailing: Switch(
style: const TextStyle(fontSize: 9), value: AppSession.currentUser.storeMediaFilesInGallery,
), onChanged: (a) => toggleStoreInGallery(),
onTap: toggleAutoStoreMediaFiles, ),
trailing: Switch(
value: gUser.autoStoreAllSendUnlimitedMediaFiles,
onChanged: (a) => toggleAutoStoreMediaFiles(),
),
),
if (Platform.isAndroid)
ListTile(
title: Text(
context.lang.exportMemories,
), ),
onTap: () => context.push(Routes.settingsStorageExport), ListTile(
), title: Text(context.lang.autoStoreAllSendUnlimitedMediaFiles),
if (Platform.isAndroid) subtitle: Text(
ListTile( context.lang.autoStoreAllSendUnlimitedMediaFilesSubtitle,
title: Text( style: const TextStyle(fontSize: 9),
context.lang.importMemories, ),
onTap: toggleAutoStoreMediaFiles,
trailing: Switch(
value: AppSession
.currentUser
.autoStoreAllSendUnlimitedMediaFiles,
onChanged: (a) => toggleAutoStoreMediaFiles(),
),
), ),
onTap: () => context.push(Routes.settingsStorageImport), if (Platform.isAndroid)
), ListTile(
const Divider(), title: Text(
ListTile( context.lang.exportMemories,
title: Text( ),
context.lang.settingsStorageDataMediaAutoDownload, onTap: () => context.push(Routes.settingsStorageExport),
style: const TextStyle(fontSize: 13), ),
), if (Platform.isAndroid)
), ListTile(
ListTile( title: Text(
title: Text(context.lang.settingsStorageDataAutoDownMobile), context.lang.importMemories,
subtitle: Text( ),
autoDownloadOptions[ConnectivityResult.mobile.name]! onTap: () => context.push(Routes.settingsStorageImport),
.where((e) => e != 'audio') ),
.join(', '), const Divider(),
style: const TextStyle(color: Colors.grey), ListTile(
), title: Text(
onTap: () async { context.lang.settingsStorageDataMediaAutoDownload,
await showAutoDownloadOptions(context, ConnectivityResult.mobile); style: const TextStyle(fontSize: 13),
}, ),
), ),
ListTile( ListTile(
title: Text(context.lang.settingsStorageDataAutoDownWifi), title: Text(context.lang.settingsStorageDataAutoDownMobile),
subtitle: Text( subtitle: Text(
autoDownloadOptions[ConnectivityResult.wifi.name]! autoDownloadOptions[ConnectivityResult.mobile.name]!
.where((e) => e != 'audio') .where((e) => e != 'audio')
.join(', '), .join(', '),
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
onTap: () async { onTap: () async {
await showAutoDownloadOptions(context, ConnectivityResult.wifi); await showAutoDownloadOptions(
}, context,
), ConnectivityResult.mobile,
], );
},
),
ListTile(
title: Text(context.lang.settingsStorageDataAutoDownWifi),
subtitle: Text(
autoDownloadOptions[ConnectivityResult.wifi.name]!
.where((e) => e != 'audio')
.join(', '),
style: const TextStyle(color: Colors.grey),
),
onTap: () async {
await showAutoDownloadOptions(
context,
ConnectivityResult.wifi,
);
},
),
],
);
},
), ),
); );
} }
@ -215,9 +226,8 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
// Call the onUpdate callback to notify the parent widget // Call the onUpdate callback to notify the parent widget
await updateUserdata((u) { await updateUser((u) {
u.autoDownloadOptions = autoDownloadOptions; u.autoDownloadOptions = autoDownloadOptions;
return u;
}); });
widget.onUpdate(); widget.onUpdate();

View file

@ -26,19 +26,13 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
} }
Future<void> toggleDeveloperSettings() async { Future<void> toggleDeveloperSettings() async {
await updateUserdata((u) { await updateUser((u) => u.isDeveloper = !u.isDeveloper);
u.isDeveloper = !u.isDeveloper;
return u;
});
setState(() {});
} }
Future<void> toggleVideoStabilization() async { Future<void> toggleVideoStabilization() async {
await updateUserdata((u) { await updateUser(
u.videoStabilizationEnabled = !u.videoStabilizationEnabled; (u) => u.videoStabilizationEnabled = !u.videoStabilizationEnabled,
return u; );
});
setState(() {});
} }
@override @override
@ -47,80 +41,86 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
appBar: AppBar( appBar: AppBar(
title: const Text('Developer Settings'), title: const Text('Developer Settings'),
), ),
body: ListView( body: StreamBuilder<void>(
children: [ stream: AppSession.onUserUpdated,
ListTile( builder: (context, _) {
title: const Text('Show Developer Settings'), return ListView(
onTap: toggleDeveloperSettings, children: [
trailing: Switch( ListTile(
value: gUser.isDeveloper, title: const Text('Show Developer Settings'),
onChanged: (a) => toggleDeveloperSettings(), onTap: toggleDeveloperSettings,
), trailing: Switch(
), value: AppSession.currentUser.isDeveloper,
ListTile( onChanged: (_) => toggleDeveloperSettings(),
title: const Text('Show Retransmission Database'), ),
onTap: () => ),
context.push(Routes.settingsDeveloperRetransmissionDatabase), ListTile(
), title: const Text('Show Retransmission Database'),
ListTile( onTap: () => context.push(
title: const Text('Toggle Video Stabilization'), Routes.settingsDeveloperRetransmissionDatabase,
onTap: toggleVideoStabilization, ),
trailing: Switch( ),
value: gUser.videoStabilizationEnabled, ListTile(
onChanged: (a) => toggleVideoStabilization(), title: const Text('Toggle Video Stabilization'),
), onTap: toggleVideoStabilization,
), trailing: Switch(
ListTile( value: AppSession.currentUser.videoStabilizationEnabled,
title: const Text('Delete all (!) app data'), onChanged: (a) => toggleVideoStabilization(),
onTap: () async { ),
final ok = await showAlertDialog( ),
context, ListTile(
'Sure?', title: const Text('Delete all (!) app data'),
'If you do not have a backup, you have to register with a new account.', onTap: () async {
); final ok = await showAlertDialog(
if (ok) { context,
await deleteLocalUserData(); 'Sure?',
await Restart.restartApp( 'If you do not have a backup, you have to register with a new account.',
notificationTitle: 'Account successfully deleted',
notificationBody: 'Click here to open the app again',
forceKill: true,
);
}
},
),
ListTile(
title: const Text('Reduce flames'),
onTap: () => context.push(Routes.settingsDeveloperReduceFlames),
),
if (!kReleaseMode)
ListTile(
title: const Text('Make it possible to reset flames'),
onTap: () async {
final chats = await twonlyDB.groupsDao.getAllDirectChats();
for (final chat in chats) {
await twonlyDB.groupsDao.updateGroup(
chat.groupId,
GroupsCompanion(
flameCounter: const Value(0),
maxFlameCounter: const Value(365),
lastFlameCounterChange: Value(clock.now()),
maxFlameCounterFrom: Value(
clock.now().subtract(const Duration(days: 1)),
),
),
); );
} if (ok) {
await HapticFeedback.heavyImpact(); await deleteLocalUserData();
}, await Restart.restartApp(
), notificationTitle: 'Account successfully deleted',
if (!kReleaseMode) notificationBody: 'Click here to open the app again',
ListTile( forceKill: true,
title: const Text('Automated Testing'), );
onTap: () => }
context.push(Routes.settingsDeveloperAutomatedTesting), },
), ),
], ListTile(
title: const Text('Reduce flames'),
onTap: () => context.push(Routes.settingsDeveloperReduceFlames),
),
if (!kReleaseMode)
ListTile(
title: const Text('Make it possible to reset flames'),
onTap: () async {
final chats = await twonlyDB.groupsDao.getAllDirectChats();
for (final chat in chats) {
await twonlyDB.groupsDao.updateGroup(
chat.groupId,
GroupsCompanion(
flameCounter: const Value(0),
maxFlameCounter: const Value(365),
lastFlameCounterChange: Value(clock.now()),
maxFlameCounterFrom: Value(
clock.now().subtract(const Duration(days: 1)),
),
),
);
}
await HapticFeedback.heavyImpact();
},
),
if (!kReleaseMode)
ListTile(
title: const Text('Automated Testing'),
onTap: () =>
context.push(Routes.settingsDeveloperAutomatedTesting),
),
],
);
},
), ),
); );
} }

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -73,7 +74,6 @@ class ChangeLogView extends StatefulWidget {
class _ChangeLogViewState extends State<ChangeLogView> { class _ChangeLogViewState extends State<ChangeLogView> {
String changeLog = ''; String changeLog = '';
bool hideChangeLog = false;
@override @override
void initState() { void initState() {
@ -87,49 +87,41 @@ class _ChangeLogViewState extends State<ChangeLogView> {
Future<void> initAsync() async { Future<void> initAsync() async {
changeLog = await rootBundle.loadString('CHANGELOG.md'); changeLog = await rootBundle.loadString('CHANGELOG.md');
final user = await getUser(); if (mounted) setState(() {});
if (user != null) {
hideChangeLog = user.hideChangeLog;
}
setState(() {});
}
Future<void> _toggleAutoOpen(bool value) async {
await updateUserdata((u) {
u.hideChangeLog = !hideChangeLog;
return u;
});
setState(() {
hideChangeLog = !value;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return StreamBuilder<void>(
appBar: AppBar( stream: AppSession.onUserUpdated,
title: const Text('Changelog'), builder: (context, _) {
), return Scaffold(
body: SafeArea( appBar: AppBar(
child: Padding( title: const Text('Changelog'),
padding: const EdgeInsets.all(8),
child: ListView(
children: parseMarkdown(context, changeLog),
), ),
), body: SafeArea(
), child: Padding(
bottomNavigationBar: BottomAppBar( padding: const EdgeInsets.all(8),
child: Row( child: ListView(
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: parseMarkdown(context, changeLog),
children: [ ),
Text(context.lang.openChangeLog),
Switch(
value: !hideChangeLog,
onChanged: _toggleAutoOpen,
), ),
], ),
), bottomNavigationBar: BottomAppBar(
), child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(context.lang.openChangeLog),
Switch(
value: !AppSession.currentUser.hideChangeLog,
onChanged: (_) =>
updateUser((u) => u.hideChangeLog = !u.hideChangeLog),
),
],
),
),
);
},
); );
} }
} }

View file

@ -19,11 +19,9 @@ class HelpView extends StatefulWidget {
class _HelpViewState extends State<HelpView> { class _HelpViewState extends State<HelpView> {
Future<void> toggleAllowErrorTrackingViaSentry() async { Future<void> toggleAllowErrorTrackingViaSentry() async {
await updateUserdata((u) { await updateUser(
u.allowErrorTrackingViaSentry = !u.allowErrorTrackingViaSentry; (u) => u.allowErrorTrackingViaSentry = !u.allowErrorTrackingViaSentry,
return u; );
});
setState(() {});
} }
@override @override
@ -32,111 +30,115 @@ class _HelpViewState extends State<HelpView> {
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsHelp), title: Text(context.lang.settingsHelp),
), ),
body: ListView( body: StreamBuilder<void>(
children: [ stream: AppSession.onUserUpdated,
ListTile( builder: (context, _) {
title: Text(context.lang.settingsHelpFAQ), return ListView(
onTap: () => context.push(Routes.settingsHelpFaq), children: [
), ListTile(
ListTile( title: Text(context.lang.settingsHelpFAQ),
title: Text(context.lang.settingsHelpContactUs), onTap: () => context.push(Routes.settingsHelpFaq),
onTap: () => context.push(Routes.settingsHelpContactUs), ),
), ListTile(
const Divider(), title: Text(context.lang.settingsHelpContactUs),
ListTile( onTap: () => context.push(Routes.settingsHelpContactUs),
title: Text(context.lang.allowErrorTracking), ),
subtitle: Text( const Divider(),
context.lang.allowErrorTrackingSubtitle, ListTile(
style: const TextStyle(fontSize: 10), title: Text(context.lang.allowErrorTracking),
), subtitle: Text(
onTap: toggleAllowErrorTrackingViaSentry, context.lang.allowErrorTrackingSubtitle,
trailing: Switch( style: const TextStyle(fontSize: 10),
value: gUser.allowErrorTrackingViaSentry, ),
onChanged: (a) => toggleAllowErrorTrackingViaSentry(), onTap: toggleAllowErrorTrackingViaSentry,
), trailing: Switch(
), value: AppSession.currentUser.allowErrorTrackingViaSentry,
ListTile( onChanged: (a) => toggleAllowErrorTrackingViaSentry(),
title: Text(context.lang.settingsHelpDiagnostics), ),
onTap: () => context.push(Routes.settingsHelpDiagnostics), ),
), ListTile(
const Divider(), title: Text(context.lang.settingsHelpDiagnostics),
if (gUser.userStudyParticipantsToken == null || kDebugMode) onTap: () => context.push(Routes.settingsHelpDiagnostics),
ListTile( ),
title: const Text('Teilnahme an Nutzerstudie'), const Divider(),
onTap: () => context.push(Routes.settingsHelpUserStudy), if (AppSession.currentUser.userStudyParticipantsToken == null ||
), kDebugMode)
FutureBuilder( ListTile(
future: PackageInfo.fromPlatform(), title: const Text('Teilnahme an Nutzerstudie'),
builder: (context, snap) { onTap: () => context.push(Routes.settingsHelpUserStudy),
if (snap.hasData) { ),
return ListTile( FutureBuilder(
title: Text(context.lang.settingsHelpVersion), future: PackageInfo.fromPlatform(),
subtitle: Text(snap.data!.version), builder: (context, snap) {
); if (snap.hasData) {
} else { return ListTile(
return Container(); title: Text(context.lang.settingsHelpVersion),
} subtitle: Text(snap.data!.version),
}, );
), } else {
ListTile( return Container();
title: Text(context.lang.settingsHelpLicenses), }
onTap: () => showLicensePage(context: context), },
), ),
ListTile( ListTile(
title: Text(context.lang.settingsHelpCredits), title: Text(context.lang.settingsHelpLicenses),
onTap: () => context.push(Routes.settingsHelpCredits), onTap: () => showLicensePage(context: context),
), ),
ListTile( ListTile(
title: const Text('Changelog'), title: Text(context.lang.settingsHelpCredits),
onTap: () => context.push(Routes.settingsHelpChangelog), onTap: () => context.push(Routes.settingsHelpCredits),
), ),
ListTile( ListTile(
title: const Text('Open Source'), title: const Text('Changelog'),
onTap: () => launchUrl( onTap: () => context.push(Routes.settingsHelpChangelog),
Uri.parse('https://github.com/twonlyapp/twonly-app'), ),
), ListTile(
trailing: const FaIcon( title: const Text('Open Source'),
FontAwesomeIcons.arrowUpRightFromSquare, onTap: () => launchUrl(
size: 15, Uri.parse('https://github.com/twonlyapp/twonly-app'),
), ),
), trailing: const FaIcon(
ListTile( FontAwesomeIcons.arrowUpRightFromSquare,
title: Text(context.lang.settingsHelpImprint), size: 15,
onTap: () => launchUrl(Uri.parse('https://twonly.eu/de/legal/')), ),
trailing: const FaIcon( ),
FontAwesomeIcons.arrowUpRightFromSquare, ListTile(
size: 15, title: Text(context.lang.settingsHelpImprint),
), onTap: () =>
), launchUrl(Uri.parse('https://twonly.eu/de/legal/')),
ListTile( trailing: const FaIcon(
title: Text(context.lang.settingsHelpTerms), FontAwesomeIcons.arrowUpRightFromSquare,
onTap: () => size: 15,
launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html')), ),
trailing: const FaIcon( ),
FontAwesomeIcons.arrowUpRightFromSquare, ListTile(
size: 15, title: Text(context.lang.settingsHelpTerms),
), onTap: () =>
), launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html')),
ListTile( trailing: const FaIcon(
onLongPress: () async { FontAwesomeIcons.arrowUpRightFromSquare,
final okay = await showAlertDialog( size: 15,
context, ),
'Developer Settings', ),
'Do you want to enable the developer settings?', ListTile(
); onLongPress: () async {
if (okay) { final okay = await showAlertDialog(
await updateUserdata((u) { context,
u.isDeveloper = true; 'Developer Settings',
return u; 'Do you want to enable the developer settings?',
}); );
} if (okay) {
}, await updateUser((u) => u.isDeveloper = true);
title: const Text( }
'Copyright twonly', },
style: TextStyle(color: Colors.grey, fontSize: 13), title: const Text(
), 'Copyright twonly',
), style: TextStyle(color: Colors.grey, fontSize: 13),
], ),
),
],
);
},
), ),
); );
} }

View file

@ -20,22 +20,20 @@ class _PrivacyViewState extends State<PrivacyView> {
Future<void> toggleAuthRequirementOnStartup() async { Future<void> toggleAuthRequirementOnStartup() async {
final isAuth = await authenticateUser( final isAuth = await authenticateUser(
gUser.screenLockEnabled AppSession.currentUser.screenLockEnabled
? context.lang.settingsScreenLockAuthMessageDisable ? context.lang.settingsScreenLockAuthMessageDisable
: context.lang.settingsScreenLockAuthMessageEnable, : context.lang.settingsScreenLockAuthMessageEnable,
); );
if (!isAuth) return; if (!isAuth) return;
await updateUserdata((u) { await updateUser((u) {
u.screenLockEnabled = !u.screenLockEnabled; u.screenLockEnabled = !u.screenLockEnabled;
return u;
}); });
setState(() {}); setState(() {});
} }
Future<void> toggleTypingIndicators() async { Future<void> toggleTypingIndicators() async {
await updateUserdata((u) { await updateUser((u) {
u.typingIndicators = !u.typingIndicators; u.typingIndicators = !u.typingIndicators;
return u;
}); });
setState(() {}); setState(() {});
} }
@ -86,7 +84,7 @@ class _PrivacyViewState extends State<PrivacyView> {
subtitle: Text(context.lang.settingsTypingIndicationSubtitle), subtitle: Text(context.lang.settingsTypingIndicationSubtitle),
onTap: toggleTypingIndicators, onTap: toggleTypingIndicators,
trailing: Switch( trailing: Switch(
value: gUser.typingIndicators, value: AppSession.currentUser.typingIndicators,
onChanged: (a) => toggleTypingIndicators(), onChanged: (a) => toggleTypingIndicators(),
), ),
), ),
@ -96,7 +94,7 @@ class _PrivacyViewState extends State<PrivacyView> {
subtitle: Text(context.lang.settingsScreenLockSubtitle), subtitle: Text(context.lang.settingsScreenLockSubtitle),
onTap: toggleAuthRequirementOnStartup, onTap: toggleAuthRequirementOnStartup,
trailing: Switch( trailing: Switch(
value: gUser.screenLockEnabled, value: AppSession.currentUser.screenLockEnabled,
onChanged: (a) => toggleAuthRequirementOnStartup(), onChanged: (a) => toggleAuthRequirementOnStartup(),
), ),
), ),

View file

@ -18,9 +18,14 @@ class _UserDiscoverySettingsViewState extends State<UserDiscoverySettingsView> {
appBar: AppBar( appBar: AppBar(
title: const Text('Freunde finden'), title: const Text('Freunde finden'),
), ),
body: gUser.isUserDiscoveryEnabled body: StreamBuilder<void>(
? UserDiscoveryEnabledComponent(onUpdate: () => setState(() {})) stream: AppSession.onUserUpdated,
: UserDiscoveryDisabledComponent(onUpdate: () => setState(() {})), builder: (context, _) {
return AppSession.currentUser.isUserDiscoveryEnabled
? const UserDiscoveryEnabledComponent()
: const UserDiscoveryDisabledComponent();
},
),
); );
} }
} }

View file

@ -4,9 +4,7 @@ import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class UserDiscoveryDisabledComponent extends StatefulWidget { class UserDiscoveryDisabledComponent extends StatefulWidget {
const UserDiscoveryDisabledComponent({required this.onUpdate, super.key}); const UserDiscoveryDisabledComponent({super.key});
final VoidCallback onUpdate;
@override @override
State<UserDiscoveryDisabledComponent> createState() => State<UserDiscoveryDisabledComponent> createState() =>
@ -20,7 +18,6 @@ class _UserDiscoveryDisabledComponentState
threshold: 2, threshold: 2,
minimumRequiredImagesExchanged: 4, minimumRequiredImagesExchanged: 4,
); );
widget.onUpdate();
} }
@override @override

View file

@ -15,9 +15,7 @@ import 'package:twonly/src/views/components/user_context_menu.component.dart';
import 'package:twonly/src/views/settings/privacy/user_discovery/user_discovery_settings.view.dart'; import 'package:twonly/src/views/settings/privacy/user_discovery/user_discovery_settings.view.dart';
class UserDiscoveryEnabledComponent extends StatefulWidget { class UserDiscoveryEnabledComponent extends StatefulWidget {
const UserDiscoveryEnabledComponent({required this.onUpdate, super.key}); const UserDiscoveryEnabledComponent({super.key});
final VoidCallback onUpdate;
@override @override
State<UserDiscoveryEnabledComponent> createState() => State<UserDiscoveryEnabledComponent> createState() =>
@ -69,9 +67,6 @@ class _UserDiscoveryEnabledComponentState
if (ok) { if (ok) {
await UserDiscoveryService.disable(); await UserDiscoveryService.disable();
} }
// This will show the DisabledComponent as the gUser has been updated...
widget.onUpdate();
} }
@override @override
@ -119,7 +114,7 @@ class _UserDiscoveryEnabledComponentState
), ),
subtitle: subtitle:
(version != null && (version != null &&
(gUser.isDeveloper || !kReleaseMode)) (AppSession.currentUser.isDeveloper || !kReleaseMode))
? Text( ? Text(
context.lang.userDiscoveryEnabledVersion( context.lang.userDiscoveryEnabledVersion(
'${version.announcement}.${version.promotion}', '${version.announcement}.${version.promotion}',
@ -171,7 +166,7 @@ class _UserDiscoveryEnabledComponentState
title: Text(context.lang.userDiscoveryActionDisable), title: Text(context.lang.userDiscoveryActionDisable),
onTap: _disableUserDiscovery, onTap: _disableUserDiscovery,
), ),
if (_version != null && (gUser.isDeveloper || !kReleaseMode)) if (_version != null && (AppSession.currentUser.isDeveloper || !kReleaseMode))
ListTile( ListTile(
title: Text( title: Text(
context.lang.userDiscoveryEnabledYourVersion( context.lang.userDiscoveryEnabledYourVersion(

View file

@ -22,26 +22,28 @@ class _UserDiscoverySettingsViewState extends State<UserDiscoverySettingsView> {
@override @override
void initState() { void initState() {
_minimumRequiredImagesExchanged = gUser.minimumRequiredImagesExchanged; _minimumRequiredImagesExchanged =
_userDiscoveryThreshold = gUser.userDiscoveryThreshold; AppSession.currentUser.minimumRequiredImagesExchanged;
_userDiscoveryThreshold = AppSession.currentUser.userDiscoveryThreshold;
super.initState(); super.initState();
} }
Future<void> _saveChanges() async { Future<void> _saveChanges() async {
final requiresNewInitialization = final requiresNewInitialization =
gUser.userDiscoveryThreshold != _userDiscoveryThreshold; AppSession.currentUser.userDiscoveryThreshold !=
_userDiscoveryThreshold;
await updateUserdata((u) { await updateUser((u) {
u u
..minimumRequiredImagesExchanged = _minimumRequiredImagesExchanged ..minimumRequiredImagesExchanged = _minimumRequiredImagesExchanged
..userDiscoveryThreshold = _userDiscoveryThreshold; ..userDiscoveryThreshold = _userDiscoveryThreshold;
return u;
}); });
if (requiresNewInitialization) { if (requiresNewInitialization) {
await UserDiscoveryService.initializeOrUpdate( await UserDiscoveryService.initializeOrUpdate(
threshold: gUser.userDiscoveryThreshold, threshold: AppSession.currentUser.userDiscoveryThreshold,
minimumRequiredImagesExchanged: gUser.minimumRequiredImagesExchanged, minimumRequiredImagesExchanged:
AppSession.currentUser.minimumRequiredImagesExchanged,
); );
} }
if (mounted) Navigator.pop(context); if (mounted) Navigator.pop(context);
@ -113,8 +115,9 @@ class _UserDiscoverySettingsViewState extends State<UserDiscoverySettingsView> {
height: 30, height: 30,
), ),
if (_minimumRequiredImagesExchanged != if (_minimumRequiredImagesExchanged !=
gUser.minimumRequiredImagesExchanged || AppSession.currentUser.minimumRequiredImagesExchanged ||
_userDiscoveryThreshold != gUser.userDiscoveryThreshold) _userDiscoveryThreshold !=
AppSession.currentUser.userDiscoveryThreshold)
Padding( Padding(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
child: FilledButton( child: FilledButton(

View file

@ -23,13 +23,12 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
} }
Future<void> updateUserAvatar(String json, String svg) async { Future<void> updateUserAvatar(String json, String svg) async {
await updateUserdata((user) { await updateUser(
user (u) => u
..avatarJson = json ..avatarJson = json
..avatarSvg = svg ..avatarSvg = svg
..avatarCounter = user.avatarCounter + 1; ..avatarCounter = u.avatarCounter + 1,
return user; );
});
} }
AvatarMakerThemeData getAvatarMakerTheme(BuildContext context) { AvatarMakerThemeData getAvatarMakerTheme(BuildContext context) {
@ -121,7 +120,8 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
canPop: false, canPop: false,
onPopInvokedWithResult: (didPop, result) async { onPopInvokedWithResult: (didPop, result) async {
if (didPop) return; if (didPop) return;
if (_avatarMakerController.getJsonOptionsSync() != gUser.avatarJson) { if (_avatarMakerController.getJsonOptionsSync() !=
AppSession.currentUser.avatarJson) {
// there where changes // there where changes
final shouldPop = await _showBackDialog() ?? false; final shouldPop = await _showBackDialog() ?? false;
if (context.mounted && shouldPop) { if (context.mounted && shouldPop) {

View file

@ -47,13 +47,11 @@ class _ProfileViewState extends State<ProfileView> {
} }
Future<void> updateUserDisplayName(String displayName) async { Future<void> updateUserDisplayName(String displayName) async {
await updateUserdata((user) { await updateUser(
user (u) => u
..displayName = displayName ..displayName = displayName
..avatarCounter = user.avatarCounter + 1; ..avatarCounter = u.avatarCounter + 1,
return user; );
});
if (mounted) setState(() {}); // gUser has updated
} }
Future<void> _updateUsername(String username) async { Future<void> _updateUsername(String username) async {
@ -94,13 +92,11 @@ class _ProfileViewState extends State<ProfileView> {
await removeTwonlySafeFromServer(); await removeTwonlySafeFromServer();
unawaited(performTwonlySafeBackup(force: true)); unawaited(performTwonlySafeBackup(force: true));
await updateUserdata((user) { await updateUser(
user (u) => u
..username = username ..username = username
..avatarCounter = user.avatarCounter + 1; ..avatarCounter = u.avatarCounter + 1,
return user; );
});
setState(() {}); // gUser has updated
} }
@override @override
@ -109,99 +105,107 @@ class _ProfileViewState extends State<ProfileView> {
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsProfile), title: Text(context.lang.settingsProfile),
), ),
body: ListView( body: StreamBuilder<void>(
physics: const BouncingScrollPhysics(), stream: AppSession.onUserUpdated,
children: <Widget>[ builder: (context, _) {
const SizedBox(height: 25), return ListView(
AvatarMakerAvatar( physics: const BouncingScrollPhysics(),
backgroundColor: Colors.transparent, children: <Widget>[
radius: 80, const SizedBox(height: 25),
controller: _avatarMakerController, AvatarMakerAvatar(
), backgroundColor: Colors.transparent,
const SizedBox(height: 10), radius: 80,
Center( controller: _avatarMakerController,
child: SizedBox( ),
height: 35, const SizedBox(height: 10),
child: ElevatedButton.icon( Center(
icon: const Icon(Icons.edit), child: SizedBox(
label: Text(context.lang.settingsProfileCustomizeAvatar), height: 35,
onPressed: () async { child: ElevatedButton.icon(
await context.push(Routes.settingsProfileModifyAvatar); icon: const Icon(Icons.edit),
await _avatarMakerController.performRestore(); label: Text(context.lang.settingsProfileCustomizeAvatar),
setState(() {}); onPressed: () async {
await context.push(Routes.settingsProfileModifyAvatar);
await _avatarMakerController.performRestore();
},
),
),
),
const SizedBox(height: 20),
const Divider(),
BetterListTile(
leading: const Padding(
padding: EdgeInsets.only(right: 5, left: 1),
child: FaIcon(
FontAwesomeIcons.qrcode,
size: 20,
),
),
onTap: () => context.push(Routes.settingsPublicProfile),
text: context.lang.profileYourQrCode,
),
BetterListTile(
leading: const Padding(
padding: EdgeInsets.only(right: 5, left: 1),
child: FaIcon(
FontAwesomeIcons.at,
size: 20,
),
),
text: context.lang.registerUsernameDecoration,
subtitle: Text(AppSession.currentUser.username),
onTap: () async {
final username = await showDisplayNameChangeDialog(
context,
AppSession.currentUser.username,
context.lang.registerUsernameDecoration,
context.lang.registerUsernameDecoration,
maxLength: 12,
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(
RegExp('[a-z0-9A-Z._]'),
),
],
);
if (context.mounted && username != null && username != '') {
await _updateUsername(username);
}
}, },
), ),
), BetterListTile(
), icon: FontAwesomeIcons.userPen,
const SizedBox(height: 20), text: context.lang.settingsProfileEditDisplayName,
const Divider(), subtitle: Text(AppSession.currentUser.displayName),
BetterListTile( onTap: () async {
leading: const Padding( final displayName = await showDisplayNameChangeDialog(
padding: EdgeInsets.only(right: 5, left: 1), context,
child: FaIcon( AppSession.currentUser.displayName,
FontAwesomeIcons.qrcode, context.lang.settingsProfileEditDisplayName,
size: 20, context.lang.settingsProfileEditDisplayNameNew,
maxLength: 30,
);
if (context.mounted &&
displayName != null &&
displayName != '') {
await updateUserDisplayName(displayName);
}
},
), ),
), BetterListTile(
onTap: () => context.push(Routes.settingsPublicProfile), text: context.lang.yourTwonlyScore,
text: context.lang.profileYourQrCode, icon: FontAwesomeIcons.trophy,
), trailing: Text(
BetterListTile( twonlyScore.toString(),
leading: const Padding( style: TextStyle(
padding: EdgeInsets.only(right: 5, left: 1), color: context.color.primary,
child: FaIcon( fontSize: 18,
FontAwesomeIcons.at, ),
size: 20, ),
), ),
), ],
text: context.lang.registerUsernameDecoration, );
subtitle: Text(gUser.username), },
onTap: () async {
final username = await showDisplayNameChangeDialog(
context,
gUser.username,
context.lang.registerUsernameDecoration,
context.lang.registerUsernameDecoration,
maxLength: 12,
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z._]')),
],
);
if (context.mounted && username != null && username != '') {
await _updateUsername(username);
}
},
),
BetterListTile(
icon: FontAwesomeIcons.userPen,
text: context.lang.settingsProfileEditDisplayName,
subtitle: Text(gUser.displayName),
onTap: () async {
final displayName = await showDisplayNameChangeDialog(
context,
gUser.displayName,
context.lang.settingsProfileEditDisplayName,
context.lang.settingsProfileEditDisplayNameNew,
maxLength: 30,
);
if (context.mounted && displayName != null && displayName != '') {
await updateUserDisplayName(displayName);
}
},
),
BetterListTile(
text: context.lang.yourTwonlyScore,
icon: FontAwesomeIcons.trophy,
trailing: Text(
twonlyScore.toString(),
style: TextStyle(
color: context.color.primary,
fontSize: 18,
),
),
),
],
), ),
); );
} }

View file

@ -47,12 +47,12 @@ class _SettingsMainViewState extends State<SettingsMainView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
substringBy(gUser.displayName, 27), substringBy(AppSession.currentUser.displayName, 27),
style: const TextStyle(fontSize: 20), style: const TextStyle(fontSize: 20),
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
Text( Text(
gUser.username, AppSession.currentUser.username,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
), ),
@ -124,11 +124,11 @@ class _SettingsMainViewState extends State<SettingsMainView> {
onTap: () async { onTap: () async {
await context.push(Routes.settingsHelp); await context.push(Routes.settingsHelp);
setState(() { setState(() {
// gUser could have been changed // AppSession.currentUser could have been changed
}); });
}, },
), ),
if (gUser.isDeveloper) if (AppSession.currentUser.isDeveloper)
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.code, icon: FontAwesomeIcons.code,
text: 'Developer Settings', text: 'Developer Settings',

View file

@ -290,7 +290,7 @@ class _PlanCardState extends State<PlanCard> {
var url = 'https://apps.apple.com/account/subscriptions'; var url = 'https://apps.apple.com/account/subscriptions';
if (Platform.isAndroid) { if (Platform.isAndroid) {
url = url =
'https://play.google.com/store/account/subscriptions?sku=${gUser.subscriptionPlanIdStore}&package=eu.twonly'; 'https://play.google.com/store/account/subscriptions?sku=${AppSession.currentUser.subscriptionPlanIdStore}&package=eu.twonly';
} }
await launchUrl( await launchUrl(
Uri.parse(url), Uri.parse(url),

View file

@ -39,10 +39,7 @@ String localePrizing(BuildContext context, int cents) {
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async { Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
final ballance = await apiService.getPlanBallance(); final ballance = await apiService.getPlanBallance();
if (ballance != null) { if (ballance != null) {
await updateUserdata((u) { await updateUser((u) => u.lastPlanBallance = ballance.writeToJson());
u.lastPlanBallance = ballance.writeToJson();
return u;
});
return ballance; return ballance;
} }
final user = await getUser(); final user = await getUser();

View file

@ -15,7 +15,7 @@ const surveyUrlBase = 'https://survey.twonly.org/upload.php';
Future<void> handleUserStudyUpload() async { Future<void> handleUserStudyUpload() async {
try { try {
final token = gUser.userStudyParticipantsToken; final token = AppSession.currentUser.userStudyParticipantsToken;
if (token == null) return; if (token == null) return;
// in case the survey was taken offline try again // in case the survey was taken offline try again
@ -35,8 +35,8 @@ Future<void> handleUserStudyUpload() async {
await KeyValueStore.delete(userStudySurveyKey); await KeyValueStore.delete(userStudySurveyKey);
} }
if (gUser.lastUserStudyDataUpload != null && if (AppSession.currentUser.lastUserStudyDataUpload != null &&
isToday(gUser.lastUserStudyDataUpload!)) { isToday(AppSession.currentUser.lastUserStudyDataUpload!)) {
// Only send updates once a day. // Only send updates once a day.
// This enables to see if improvements to actually work. // This enables to see if improvements to actually work.
return; return;
@ -56,18 +56,16 @@ Future<void> handleUserStudyUpload() async {
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
await updateUserdata((u) { await updateUser((u) {
u.lastUserStudyDataUpload = DateTime.now(); u.lastUserStudyDataUpload = DateTime.now();
return u;
}); });
} }
if (response.statusCode == 404) { if (response.statusCode == 404) {
// Token is unknown to the server... // Token is unknown to the server...
await updateUserdata((u) { await updateUser((u) {
u u
..lastUserStudyDataUpload = null ..lastUserStudyDataUpload = null
..userStudyParticipantsToken = null; ..userStudyParticipantsToken = null;
return u;
}); });
} }
} catch (e) { } catch (e) {

View file

@ -50,12 +50,11 @@ class _UserStudyQuestionnaireViewState
Future<void> _submitData() async { Future<void> _submitData() async {
await KeyValueStore.put(userStudySurveyKey, _responses); await KeyValueStore.put(userStudySurveyKey, _responses);
await updateUserdata((u) { await updateUser((u) {
// generate a random participants id to identify data send later while keeping the user anonym // generate a random participants id to identify data send later while keeping the user anonym
u u
..userStudyParticipantsToken = getRandomString(25) ..userStudyParticipantsToken = getRandomString(25)
..askedForUserStudyPermission = true; ..askedForUserStudyPermission = true;
return u;
}); });
await handleUserStudyUpload(); await handleUserStudyUpload();

View file

@ -86,10 +86,9 @@ class _UserStudyWelcomeViewState extends State<UserStudyWelcomeView> {
Center( Center(
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: () async {
await updateUserdata((u) { await updateUser(
u.askedForUserStudyPermission = true; (u) => u.askedForUserStudyPermission = true,
return u; );
});
if (context.mounted) context.pop(); if (context.mounted) context.pop();
}, },
child: const Text( child: const Text(