use mutex when updating user

This commit is contained in:
otsmr 2025-06-15 21:25:54 +02:00
parent 6946164d1e
commit 20030ccd14
23 changed files with 194 additions and 136 deletions

View file

@ -44,6 +44,10 @@ class UserData {
DateTime? signalLastSignedPreKeyUpdated;
@JsonKey(defaultValue: false)
bool identityBackupEnabled = false;
DateTime? identityBackupLastBackupTime;
final int userId;
factory UserData.fromJson(Map<String, dynamic> json) =>

View file

@ -44,7 +44,12 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..signalLastSignedPreKeyUpdated =
json['signalLastSignedPreKeyUpdated'] == null
? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String);
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..identityBackupEnabled = json['identityBackupEnabled'] as bool? ?? false
..identityBackupLastBackupTime =
json['identityBackupLastBackupTime'] == null
? null
: DateTime.parse(json['identityBackupLastBackupTime'] as String);
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username,
@ -69,6 +74,9 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'myBestFriendContactId': instance.myBestFriendContactId,
'signalLastSignedPreKeyUpdated':
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
'identityBackupEnabled': instance.identityBackupEnabled,
'identityBackupLastBackupTime':
instance.identityBackupLastBackupTime?.toIso8601String(),
'userId': instance.userId,
};

View file

@ -21,10 +21,9 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
notifyListeners();
var user = await getUser();
if (user != null) {
await updateUserdata((user) {
user.themeMode = newThemeMode;
await updateUser(user);
}
return user;
});
}
}

View file

@ -13,7 +13,6 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/app.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
@ -347,11 +346,10 @@ class ApiService {
server.Response_Ok ok = result.value;
if (ok.hasAuthenticated()) {
server.Response_Authenticated authenticated = ok.authenticated;
UserData? user = await getUser();
if (user != null) {
updateUserdata((user) {
user.subscriptionPlan = authenticated.plan;
await updateUser(user);
}
return user;
});
}
Log.info("websocket is authenticated");
onAuthenticated();

View file

@ -34,20 +34,24 @@ Future<ErrorCode?> isAllowedToSend() async {
return ErrorCode.PlanNotAllowed;
}
if (user.subscriptionPlan == "Free") {
int? todaysImageCounter = user.todaysImageCounter;
if (user.lastImageSend != null && user.todaysImageCounter != null) {
if (isToday(user.lastImageSend!)) {
if (user.todaysImageCounter == 3) {
return ErrorCode.PlanLimitReached;
}
user.todaysImageCounter = user.todaysImageCounter! + 1;
todaysImageCounter = user.todaysImageCounter! + 1;
} else {
user.todaysImageCounter = 1;
todaysImageCounter = 1;
}
} else {
user.todaysImageCounter = 1;
todaysImageCounter = 1;
}
await updateUserdata((user) {
user.lastImageSend = DateTime.now();
await updateUser(user);
user.todaysImageCounter = todaysImageCounter;
return user;
});
}
return null;
}

View file

@ -0,0 +1,20 @@
import 'package:twonly/src/utils/storage.dart';
Future<bool> isIdentityBackupEnabled() async {
final user = await getUser();
if (user == null) return false;
return user.identityBackupEnabled;
}
Future<DateTime?> getLastIdentityBackup() async {
final user = await getUser();
if (user == null) return null;
return user.identityBackupLastBackupTime;
}
Future enableIdentityBackup() async {
await updateUserdata((user) {
user.identityBackupEnabled = false;
return user;
});
}

View file

@ -21,8 +21,10 @@ Future syncFlameCounters() async {
contacts.firstWhere((x) => x.totalMediaCounter == maxMessageCounter);
if (user.myBestFriendContactId != bestFriend.userId) {
await updateUserdata((user) {
user.myBestFriendContactId = bestFriend.userId;
await updateUser(user);
return user;
});
}
for (Contact contact in contacts) {

View file

@ -38,8 +38,10 @@ Future signalHandleNewServerConnection() async {
Log.error("could not generate a new signed pre key!");
return;
}
await updateUserdata((user) {
user.signalLastSignedPreKeyUpdated = DateTime.now();
await updateUser(user);
return user;
});
Result res = await apiService.updateSignedPreKey(
signedPreKey.id,
signedPreKey.getKeyPair().publicKey.serialize(),
@ -47,10 +49,10 @@ Future signalHandleNewServerConnection() async {
);
if (res.isError) {
Log.error("could not update the signed pre key: ${res.error}");
final UserData? user = await getUser();
if (user == null) return;
await updateUserdata((user) {
user.signalLastSignedPreKeyUpdated = null;
await updateUser(user);
return user;
});
} else {
Log.info("updated signed pre key");
}

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mutex/mutex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/model/json/userdata.dart';
@ -33,18 +34,27 @@ Future<UserData?> getUser() async {
Future updateUsersPlan(BuildContext context, String planId) async {
context.read<CustomChangeProvider>().plan = planId;
var user = await getUser();
if (user != null) {
await updateUserdata((user) {
user.subscriptionPlan = planId;
await updateUser(user);
}
return user;
});
if (!context.mounted) return;
context.read<CustomChangeProvider>().updatePlan(planId);
}
Future updateUser(UserData userData) async {
Mutex updateProtection = Mutex();
Future<UserData?> updateUserdata(Function(UserData userData) updateUser) async {
return await updateProtection.protect<UserData?>(() async {
final user = await getUser();
if (user == null) return null;
UserData updated = updateUser(user);
final storage = FlutterSecureStorage();
storage.write(key: "userData", value: jsonEncode(userData));
storage.write(key: "userData", value: jsonEncode(updated));
return user;
});
}
Future<bool> deleteLocalUserData() async {

View file

@ -558,11 +558,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
onPressed: () async {
useHighQuality = !useHighQuality;
setState(() {});
var user = await getUser();
if (user != null) {
await updateUserdata((user) {
user.useHighQuality = useHighQuality;
updateUser(user);
}
return user;
});
},
),
if (!hasAudioPermission)

View file

@ -29,8 +29,7 @@ class _EmojisState extends State<Emojis> {
}
Future selectEmojis(String emoji) async {
final user = await getUser();
if (user == null) return;
await updateUserdata((user) {
if (user.lastUsedEditorEmojis == null) {
user.lastUsedEditorEmojis = [emoji];
} else {
@ -43,7 +42,8 @@ class _EmojisState extends State<Emojis> {
}
user.lastUsedEditorEmojis!.toSet().toList();
}
await updateUser(user);
return user;
});
if (!mounted) return;
Navigator.pop(
context,

View file

@ -233,11 +233,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
maxShowTime = gMediaShowInfinite;
}
setState(() {});
var user = await getUser();
if (user != null) {
await updateUserdata((user) {
user.defaultShowTime = maxShowTime;
updateUser(user);
}
return user;
});
},
),
),

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/services/backup.identitiy.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/settings/backup/twonly_identity_backup.view.dart';
@ -22,10 +23,10 @@ class _BackupViewState extends State<BackupView> {
}
Future initAsync() async {
setState(() {
_twonlyIdBackupEnabled = true;
_twonlyIdBackupEnabled = await isIdentityBackupEnabled();
_twonlyIdLastBackup = await getLastIdentityBackup();
_dataBackupEnabled = false;
});
setState(() {});
}
@override
@ -37,7 +38,7 @@ class _BackupViewState extends State<BackupView> {
body: ListView(
children: [
BackupOption(
title: 'twonly-Identity Backup',
title: 'twonly Safe',
description:
'Back up your twonly identity, as this is the only way to restore your account if you uninstall or lose your phone.',
lastBackup: _twonlyIdLastBackup,
@ -51,7 +52,7 @@ class _BackupViewState extends State<BackupView> {
},
),
BackupOption(
title: 'Daten-Backup',
title: 'Daten-Backup (Coming Soon)',
description:
'This backup contains besides of your twonly-Identity also all of your media files. This backup will also be encrypted using a password chosen by the user but stored locally on the smartphone. You then have to ensure to manually copy it onto your laptop or device of your choice.',
autoBackupEnabled: _dataBackupEnabled,

View file

@ -13,12 +13,14 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("twonly-Identity Backup"),
title: Text("twonly Safe"),
),
body: ListView(children: [
body: ListView(
children: [
Text(
'Backup of your twonly-Identity. As twonly does not have any second factor like your phone number or email, this backup contains your twonly-Identity. If you lose your device, the only option to recover is with the twonly-ID Backup. This backup will be protected by a password chosen by you in the next step and anonymously uploaded to the twonly servers. Read more [here](https://twonly.eu/s/backup).'),
]),
],
),
);
}
}

View file

@ -36,11 +36,10 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
} else {
if (selectedEmojis.length < 12) {
selectedEmojis.add(emoji);
var user = await getUser();
if (user != null) {
await updateUserdata((user) {
user.preSelectedEmojies = selectedEmojis;
await updateUser(user);
}
return user;
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@ -98,11 +97,10 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
selectedEmojis =
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
setState(() {});
var user = await getUser();
if (user != null) {
await updateUserdata((user) {
user.preSelectedEmojies = selectedEmojis;
await updateUser(user);
}
return user;
});
},
child: Icon(Icons.settings_backup_restore_rounded),
),

View file

@ -48,10 +48,10 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
}
void toggleStoreInGallery() async {
final user = await getUser();
if (user == null) return;
user.storeMediaFilesInGallery = !storeMediaFilesInGallery;
await updateUser(user);
await updateUserdata((u) {
u.storeMediaFilesInGallery = !storeMediaFilesInGallery;
return u;
});
initAsync();
}
@ -179,10 +179,12 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
}
// Call the onUpdate callback to notify the parent widget
final user = await getUser();
if (user == null) return;
user.autoDownloadOptions = autoDownloadOptions;
await updateUser(user);
await updateUserdata((u) {
u.autoDownloadOptions = autoDownloadOptions;
return u;
});
widget.onUpdate();
setState(() {});
}

View file

@ -39,10 +39,10 @@ class HelpView extends StatelessWidget {
title: Text(context.lang.settingsResetTutorials),
subtitle: Text(context.lang.settingsResetTutorialsDesc),
onTap: () async {
final user = await getUser();
if (user == null) return;
updateUserdata((user) {
user.tutorialDisplayed = [];
await updateUser(user);
return user;
});
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(

View file

@ -2,7 +2,6 @@ import 'dart:math';
import 'package:avatar_maker/avatar_maker.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart';
import "package:get/get.dart";
@ -12,9 +11,7 @@ class ModifyAvatar extends StatelessWidget {
const ModifyAvatar({super.key});
Future updateUserAvatar(String json, String svg) async {
UserData? user = await getUser();
if (user == null) return null;
await updateUserdata((user) {
user.avatarJson = json;
user.avatarSvg = svg;
if (user.avatarCounter == null) {
@ -22,7 +19,8 @@ class ModifyAvatar extends StatelessWidget {
} else {
user.avatarCounter = user.avatarCounter! + 1;
}
await updateUser(user);
return user;
});
await notifyContactsAboutProfileChange();
}

View file

@ -29,16 +29,17 @@ class _ProfileViewState extends State<ProfileView> {
setState(() {});
}
Future updateUserDisplayname(String displayName) async {
UserData? user = await getUser();
if (user == null) return null;
Future updateUserDisplayName(String displayName) async {
await updateUserdata((user) {
user.displayName = displayName;
if (user.avatarCounter == null) {
user.avatarCounter = 1;
} else {
user.avatarCounter = user.avatarCounter! + 1;
}
await updateUser(user);
return user;
});
await notifyContactsAboutProfileChange();
initAsync();
}
@ -83,7 +84,7 @@ class _ProfileViewState extends State<ProfileView> {
final displayName =
await showDisplayNameChangeDialog(context, user!.displayName);
if (context.mounted && displayName != null && displayName != "") {
updateUserDisplayname(displayName);
updateUserDisplayName(displayName);
}
},
),

View file

@ -7,6 +7,7 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/account.view.dart';
import 'package:twonly/src/views/settings/appearance.view.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart';
import 'package:twonly/src/views/settings/chat/chat_settings.view.dart';
import 'package:twonly/src/views/settings/data_and_storage.view.dart';
import 'package:twonly/src/views/settings/notification.view.dart';
@ -120,16 +121,16 @@ class _SettingsMainViewState extends State<SettingsMainView> {
}));
},
),
// BetterListTile(
// icon: Icons.lock_clock_rounded,
// text: context.lang.settingsBackup,
// onTap: () {
// Navigator.push(context,
// MaterialPageRoute(builder: (context) {
// return BackupView();
// }));
// },
// ),
BetterListTile(
icon: Icons.lock_clock_rounded,
text: context.lang.settingsBackup,
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return BackupView();
}));
},
),
const Divider(),
BetterListTile(
icon: FontAwesomeIcons.sun,

View file

@ -14,24 +14,27 @@ import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
Future<List<Response_AddAccountsInvite>?> loadAdditionalUserInvites() async {
List<Response_AddAccountsInvite>? ballance;
final user = await getUser();
if (user == null) return ballance;
ballance = await apiService.getAdditionalUserInvites();
final ballance = await apiService.getAdditionalUserInvites();
if (ballance != null) {
user.additionalUserInvites =
await updateUserdata((u) {
u.additionalUserInvites =
jsonEncode(ballance.map((x) => x.writeToJson()).toList());
await updateUser(user);
} else if (user.lastPlanBallance != null) {
return u;
});
return ballance;
}
final user = await getUser();
if (user != null && user.lastPlanBallance != null) {
try {
List<String> decoded = jsonDecode(user.additionalUserInvites!);
ballance =
decoded.map((x) => Response_AddAccountsInvite.fromJson(x)).toList();
return decoded
.map((x) => Response_AddAccountsInvite.fromJson(x))
.toList();
} catch (e) {
Log.error("from json: $e");
}
}
return ballance;
return null;
}
class AdditionalUsersView extends StatefulWidget {

View file

@ -36,16 +36,18 @@ String localePrizing(BuildContext context, int cents) {
}
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
Response_PlanBallance? ballance;
final user = await getUser();
if (user == null) return ballance;
ballance = await apiService.getPlanBallance();
final ballance = await apiService.getPlanBallance();
if (ballance != null) {
user.lastPlanBallance = ballance.writeToJson();
await updateUser(user);
} else if (user.lastPlanBallance != null && useCache) {
updateUserdata((u) {
u.lastPlanBallance = ballance.writeToJson();
return u;
});
return ballance;
}
final user = await getUser();
if (user != null && user.lastPlanBallance != null && useCache) {
try {
ballance = Response_PlanBallance.fromJson(
return Response_PlanBallance.fromJson(
user.lastPlanBallance!,
);
} catch (e) {

View file

@ -47,7 +47,12 @@ Future<bool> checkIfTutorialAlreadyShown(String tutorialId) async {
return true;
}
user.tutorialDisplayed!.add(tutorialId);
await updateUser(user);
await updateUserdata((u) {
u.tutorialDisplayed = user.tutorialDisplayed;
return u;
});
return false;
}