diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0239aa..307ce68f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.2.9 + +- Improved: Make contact avatars clickable +- Fix: Messages occasionally not received until app restart +- Fix: Complete setup would sometimes get stuck + ## 0.2.8 - Fix: App did not launch sometimes on Android diff --git a/lib/main.dart b/lib/main.dart index 89789de3..ddd6f46f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -193,6 +193,21 @@ Future postStartupTasks() async { unawaited(finishStartedPreprocessing()); unawaited(createPushAvatars()); + if (userService.currentUser.userDiscoveryInitializationError) { + unawaited(() async { + try { + await UserDiscoveryService.initializeOrUpdate( + threshold: userService.currentUser.userDiscoveryThreshold, + sharePromotion: userService.currentUser.userDiscoverySharePromotion, + ); + } catch (e) { + Log.error( + 'Failed to retry UserDiscovery initialization on startup: $e', + ); + } + }()); + } + await Future.delayed(const Duration(seconds: 10)); unawaited(initializeBackgroundTaskManager()); // 3. Delayed tasks (Wait for app to settle) diff --git a/lib/src/callbacks/user_discovery.callbacks.dart b/lib/src/callbacks/user_discovery.callbacks.dart index 8542616b..3b72cc66 100644 --- a/lib/src/callbacks/user_discovery.callbacks.dart +++ b/lib/src/callbacks/user_discovery.callbacks.dart @@ -16,8 +16,12 @@ class UserDiscoveryCallbacks { static Future signData( Uint8List inputData, ) async { + Log.info('UserDiscoveryCallbacks: signData started'); var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey(); - if (privKey == null) return null; + if (privKey == null) { + Log.error('UserDiscoveryCallbacks: signData failed, privKey is null'); + return null; + } final random = getRandomUint8List(32); final signature = sign( privKey.serialize(), @@ -25,6 +29,7 @@ class UserDiscoveryCallbacks { random, ); privKey = null; + Log.info('UserDiscoveryCallbacks: signData finished'); return signature; } diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 67812253..28fe0883 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -332,4 +332,18 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return query.map((row) => row.readTable(groups)).watch(); } + + Future> getGroupsForMember(int contactId) { + final query = + select(groups).join([ + innerJoin( + groupMembers, + groupMembers.groupId.equalsExp(groups.groupId), + ), + ])..where( + groupMembers.contactId.equals(contactId), + ); + + return query.map((row) => row.readTable(groups)).get(); + } } diff --git a/lib/src/database/daos/key_verification.dao.dart b/lib/src/database/daos/key_verification.dao.dart index f4693ff4..cfcbaf84 100644 --- a/lib/src/database/daos/key_verification.dao.dart +++ b/lib/src/database/daos/key_verification.dao.dart @@ -103,6 +103,25 @@ class KeyVerificationDao extends DatabaseAccessor }); } + Future getTransferredTrustVerificationsCount() async { + final kv = keyVerifications; + final ur = userDiscoveryUserRelations; + + final query = selectOnly(ur, distinct: true) + ..addColumns([ur.announcedUserId]) + ..join([ + innerJoin(contacts, contacts.userId.equalsExp(ur.fromContactId)), + innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)), + ]) + ..where( + ur.publicKeyVerifiedTimestamp.isNotNull() & + ur.announcedUserId.equalsExp(ur.fromContactId).not(), + ); + + final rows = await query.get(); + return rows.length; + } + Stream watchAllGroupMembersVerified(String groupId) { final gm = groupMembers; final directKv = alias(keyVerifications, 'directKv'); diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index ffe01b40..ab42b249 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -83,6 +83,8 @@ enum GroupActionType { demoteToMember, updatedGroupName, changeDisplayMaxTime, + updatedContactUsername, + updatedContactDisplayName, } @DataClassName('GroupHistory') diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 47f5438e..a18d6f96 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2965,6 +2965,102 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Approve'** String get contactUserDiscoveryManualApprovalApprove; + + /// No description provided for @exampleUserName1. + /// + /// In en, this message translates to: + /// **'james'** + String get exampleUserName1; + + /// No description provided for @exampleUserName2. + /// + /// In en, this message translates to: + /// **'mary'** + String get exampleUserName2; + + /// No description provided for @exampleUserName3. + /// + /// In en, this message translates to: + /// **'john'** + String get exampleUserName3; + + /// No description provided for @exampleUserName4. + /// + /// In en, this message translates to: + /// **'patricia'** + String get exampleUserName4; + + /// No description provided for @exampleUserName5. + /// + /// In en, this message translates to: + /// **'robert'** + String get exampleUserName5; + + /// No description provided for @exampleUserName6. + /// + /// In en, this message translates to: + /// **'jennifer'** + String get exampleUserName6; + + /// No description provided for @exampleUserName7. + /// + /// In en, this message translates to: + /// **'michael'** + String get exampleUserName7; + + /// No description provided for @exampleUserName8. + /// + /// In en, this message translates to: + /// **'linda'** + String get exampleUserName8; + + /// No description provided for @exampleUserName9. + /// + /// In en, this message translates to: + /// **'william'** + String get exampleUserName9; + + /// No description provided for @exampleUserName10. + /// + /// In en, this message translates to: + /// **'lena'** + String get exampleUserName10; + + /// No description provided for @exampleUserName11. + /// + /// In en, this message translates to: + /// **'david'** + String get exampleUserName11; + + /// No description provided for @exampleJane. + /// + /// In en, this message translates to: + /// **'jane'** + String get exampleJane; + + /// No description provided for @back. + /// + /// In en, this message translates to: + /// **'Back'** + String get back; + + /// No description provided for @onboardingExampleLabel. + /// + /// In en, this message translates to: + /// **'Example'** + String get onboardingExampleLabel; + + /// No description provided for @makerChangedUsername. + /// + /// In en, this message translates to: + /// **'{maker} changed their username from {oldName} to {newName}.'** + String makerChangedUsername(Object maker, Object oldName, Object newName); + + /// No description provided for @makerChangedDisplayName. + /// + /// In en, this message translates to: + /// **'{maker} changed their display name from {oldName} to {newName}.'** + String makerChangedDisplayName(Object maker, Object oldName, Object newName); } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 4e247819..d1cfd1c8 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1669,4 +1669,56 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contactUserDiscoveryManualApprovalApprove => 'Freigeben'; + + @override + String get exampleUserName1 => 'max_mustermann'; + + @override + String get exampleUserName2 => 'erika_musterfrau'; + + @override + String get exampleUserName3 => 'hans'; + + @override + String get exampleUserName4 => 'petra'; + + @override + String get exampleUserName5 => 'klaus'; + + @override + String get exampleUserName6 => 'sabine'; + + @override + String get exampleUserName7 => 'stefan'; + + @override + String get exampleUserName8 => 'monika'; + + @override + String get exampleUserName9 => 'christian'; + + @override + String get exampleUserName10 => 'lena'; + + @override + String get exampleUserName11 => 'david'; + + @override + String get exampleJane => 'erika'; + + @override + String get back => 'Zurück'; + + @override + String get onboardingExampleLabel => 'Beispiel'; + + @override + String makerChangedUsername(Object maker, Object oldName, Object newName) { + return '$maker hat seinen Benutzernamen von $oldName zu $newName geändert.'; + } + + @override + String makerChangedDisplayName(Object maker, Object oldName, Object newName) { + return '$maker hat seinen Anzeigenamen von $oldName zu $newName geändert.'; + } } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 0f4bf7fa..555d59c5 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1654,4 +1654,56 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contactUserDiscoveryManualApprovalApprove => 'Approve'; + + @override + String get exampleUserName1 => 'james'; + + @override + String get exampleUserName2 => 'mary'; + + @override + String get exampleUserName3 => 'john'; + + @override + String get exampleUserName4 => 'patricia'; + + @override + String get exampleUserName5 => 'robert'; + + @override + String get exampleUserName6 => 'jennifer'; + + @override + String get exampleUserName7 => 'michael'; + + @override + String get exampleUserName8 => 'linda'; + + @override + String get exampleUserName9 => 'william'; + + @override + String get exampleUserName10 => 'lena'; + + @override + String get exampleUserName11 => 'david'; + + @override + String get exampleJane => 'jane'; + + @override + String get back => 'Back'; + + @override + String get onboardingExampleLabel => 'Example'; + + @override + String makerChangedUsername(Object maker, Object oldName, Object newName) { + return '$maker changed their username from $oldName to $newName.'; + } + + @override + String makerChangedDisplayName(Object maker, Object oldName, Object newName) { + return '$maker changed their display name from $oldName to $newName.'; + } } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 03bf2204..781626f6 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 03bf220400bf35002c77e153768bd0f963a97d89 +Subproject commit 781626f66c5f992ffad861abb7b4937f82319392 diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 07a44b81..52b9857b 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -109,6 +109,9 @@ class UserData { @JsonKey(defaultValue: true) bool userDiscoverySharePromotion = true; + @JsonKey(defaultValue: false) + bool userDiscoveryInitializationError = false; + // -- Custom DATA -- @JsonKey(defaultValue: 100_000) diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 85d7d049..7cebbbad 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -145,6 +145,7 @@ class ApiService { } Future onClosed() async { + Log.info('websocket connection closed'); _channel = null; isAuthenticated = false; _connectionStateController.add(false); @@ -249,7 +250,7 @@ class ApiService { completer.complete(msg); } } else { - await handleServerMessage(msg); + unawaited(handleServerMessage(msg)); } } catch (e) { Log.error('Error parsing the servers message: $e'); diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index d074f2eb..2d300bcd 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart' hide Message; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.api.dart'; @@ -126,6 +127,44 @@ Future handleContactUpdate( if (contactUpdate.hasDisplayName() && contactUpdate.hasUsername() && senderProfileCounter != null) { + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + + if (contact != null) { + final sharedGroups = await twonlyDB.groupsDao.getGroupsForMember( + fromUserId, + ); + + if (contact.username != contactUpdate.username) { + for (final group in sharedGroups) { + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.updatedContactUsername), + contactId: Value(fromUserId), + oldGroupName: Value('@${contact.username}'), + newGroupName: Value('@${contactUpdate.username}'), + ), + ); + } + } + + if (contact.displayName != contactUpdate.displayName) { + for (final group in sharedGroups) { + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.updatedContactDisplayName), + contactId: Value(fromUserId), + oldGroupName: Value(contact.displayName ?? ''), + newGroupName: Value(contactUpdate.displayName), + ), + ); + } + } + } + await twonlyDB.contactsDao.updateContact( fromUserId, ContactsCompanion( diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index a6a1c463..cf3be4a0 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -179,6 +179,8 @@ Future handleGroupUpdate( ), ); case GroupActionType.createdGroup: + case GroupActionType.updatedContactUsername: + case GroupActionType.updatedContactDisplayName: break; } } diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index 22e7534a..93885682 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -67,6 +67,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ Receipt? receipt, bool onlyReturnEncryptedData = false, bool blocking = true, + bool useLock = true, }) async { try { if (receiptId == null && receipt == null) return null; @@ -132,6 +133,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ final cipherText = await signalEncryptMessage( receipt.contactId, Uint8List.fromList(message.encryptedContent), + useLock: useLock, ); if (cipherText == null) { Log.error('Could not encrypt the message. Aborting and trying again.'); @@ -336,6 +338,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( bool blocking = true, String? messageId, bool onlySendIfNoReceiptsAreOpen = false, + bool useLock = true, }) async { if (onlySendIfNoReceiptsAreOpen) { final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact( @@ -397,6 +400,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( receipt: receipt, onlyReturnEncryptedData: onlyReturnEncryptedData, blocking: blocking, + useLock: useLock, ); if (!blocking) { return null; diff --git a/lib/src/services/api/server_messages.api.dart b/lib/src/services/api/server_messages.api.dart index f9e3136a..b3f68a11 100644 --- a/lib/src/services/api/server_messages.api.dart +++ b/lib/src/services/api/server_messages.api.dart @@ -40,6 +40,8 @@ final lockHandleServerMessage = Mutex(); Future handleServerMessage(server.ServerToClient msg) async { return lockHandleServerMessage.protect(() async { + Log.info('Processing a message from the server.'); + /// Returns means, that the server can delete the message from the server. final ok = client.Response_Ok()..none = true; var response = client.Response()..ok = ok; @@ -48,8 +50,12 @@ Future handleServerMessage(server.ServerToClient msg) async { if (msg.v0.hasRequestNewPreKeys()) { response = await handleRequestNewPreKey(); } else if (msg.v0.hasNewMessage()) { + Log.info('Got 1 message from the server.'); await handleClient2ClientMessage(msg.v0.newMessage); } else if (msg.v0.hasNewMessages()) { + Log.info( + 'Got ${msg.v0.newMessages.newMessages.length} messages from the server.', + ); for (final newMessage in msg.v0.newMessages.newMessages) { try { await handleClient2ClientMessage(newMessage); @@ -70,13 +76,12 @@ Future handleServerMessage(server.ServerToClient msg) async { await apiService.sendResponse(ClientToServer()..v0 = v0); AppState.gotMessageFromServer = true; + Log.info('Message from server proccessed.'); }); } DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); -Mutex protectReceiptCheck = Mutex(); - Future handleClient2ClientMessage(NewMessage newMessage) async { final body = Uint8List.fromList(newMessage.body); final fromUserId = newMessage.fromUserId.toInt(); @@ -84,15 +89,15 @@ Future handleClient2ClientMessage(NewMessage newMessage) async { final message = Message.fromBuffer(body); final receiptId = message.receiptId; - final isDuplicated = await protectReceiptCheck.protect(() async { - if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) { - return true; - } - await twonlyDB.receiptsDao.gotReceipt(receiptId); - return false; - }); + if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) { + return; + } - if (isDuplicated) { + try { + await twonlyDB.receiptsDao.gotReceipt(receiptId); + Log.info('Got a message with receiptId $receiptId'); + } catch (e) { + Log.error(e); return; } diff --git a/lib/src/services/background/callback_dispatcher.background.dart b/lib/src/services/background/callback_dispatcher.background.dart index 4dcc68f2..6aba3078 100644 --- a/lib/src/services/background/callback_dispatcher.background.dart +++ b/lib/src/services/background/callback_dispatcher.background.dart @@ -86,6 +86,7 @@ Future initBackgroundExecution() async { final Mutex _keyValueMutex = Mutex(); +// ignore: unreachable_from_main Future handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async { final shouldBeExecuted = await exclusiveAccess( lockName: 'periodic_task', diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart index d0c8afb4..9f2796a2 100644 --- a/lib/src/services/mediafiles/compression.service.dart +++ b/lib/src/services/mediafiles/compression.service.dart @@ -77,7 +77,8 @@ Future compressAndOverlayVideo(MediaFileService media) async { VideoSegment(video: EditorVideo.file(media.originalPath)), ], imageLayers: [ - ImageLayer(image: EditorLayerImage.file(media.overlayImagePath)), + if (media.overlayImagePath.existsSync()) + ImageLayer(image: EditorLayerImage.file(media.overlayImagePath)), ], enableAudio: !media.removeAudio, ); diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index ec914ade..edd82990 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -97,7 +97,7 @@ class MediaFileService { final group = await twonlyDB.groupsDao.getGroup( message.groupId, ); - if (group != null) { + if (group != null && !group.isDirectChat) { delete = false; } } diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index a50b7ca5..d8b4e1b5 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -11,20 +11,31 @@ import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; Future signalEncryptMessage( + int target, + Uint8List plaintextContent, { + bool useLock = true, +}) async { + if (useLock) { + return lockingSignalProtocol.protect(() async { + return _signalEncryptMessage(target, plaintextContent); + }); + } + return _signalEncryptMessage(target, plaintextContent); +} + +Future _signalEncryptMessage( int target, Uint8List plaintextContent, ) async { - return lockingSignalProtocol.protect(() async { - try { - final signalStore = (await getSignalStore())!; - final address = getSignalAddress(target); - final session = SessionCipher.fromStore(signalStore, address); - return await session.encrypt(plaintextContent); - } catch (e) { - Log.error(e.toString()); - return null; - } - }); + try { + final signalStore = (await getSignalStore())!; + final address = getSignalAddress(target); + final session = SessionCipher.fromStore(signalStore, address); + return await session.encrypt(plaintextContent); + } catch (e) { + Log.error(e.toString()); + return null; + } } Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)> @@ -67,8 +78,9 @@ signalDecryptMessage( Log.info(e.toString()); return (null, null); } on InvalidMessageException catch (e) { + Log.warn(e); if (!resyncedUsers.contains(fromUserId)) { - if (await handleSessionResync(fromUserId)) { + if (await handleSessionResync(fromUserId, useLock: false)) { // This flag prevents from resyncing the session the client received multiple new // messages from the server he could not decrypt resyncedUsers.add(fromUserId); @@ -81,10 +93,10 @@ signalDecryptMessage( type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC, ), ), + useLock: false, ); } } - Log.warn(e); return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); } catch (e) { Log.error(e); diff --git a/lib/src/services/signal/identity.signal.dart b/lib/src/services/signal/identity.signal.dart index 722f844c..6aadec4c 100644 --- a/lib/src/services/signal/identity.signal.dart +++ b/lib/src/services/signal/identity.signal.dart @@ -91,9 +91,14 @@ Future getSignalIdentity() async { } Future getUserPublicKey() async { + Log.info('getUserPublicKey: getting identity'); final signalIdentity = (await getSignalIdentity())!; + Log.info('getUserPublicKey: getting signal store'); final signalStore = await getSignalStoreFromIdentity(signalIdentity); - return (await signalStore.getIdentityKeyPair()).getPublicKey().serialize(); + Log.info('getUserPublicKey: getting key pair'); + final keyPair = await signalStore.getIdentityKeyPair(); + Log.info('getUserPublicKey: serializing public key'); + return keyPair.getPublicKey().serialize(); } Future createIfNotExistsSignalIdentity() async { diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index 03554d32..f23236b5 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -8,73 +8,83 @@ import 'package:twonly/src/services/signal/protocol_state.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; -Future processSignalUserData(Response_UserData userData) async { - return lockingSignalProtocol.protect(() async { - final SignalProtocolStore? signalStore = await getSignalStore(); +Future processSignalUserData( + Response_UserData userData, { + bool useLock = true, +}) async { + if (useLock) { + return lockingSignalProtocol.protect(() async { + return _processSignalUserData(userData); + }); + } + return _processSignalUserData(userData); +} - if (signalStore == null) { - return false; - } +Future _processSignalUserData(Response_UserData userData) async { + final SignalProtocolStore? signalStore = await getSignalStore(); - final targetAddress = getSignalAddress(userData.userId.toInt()); + if (signalStore == null) { + return false; + } - final sessionBuilder = SessionBuilder.fromSignalStore( - signalStore, - targetAddress, - ); + final targetAddress = getSignalAddress(userData.userId.toInt()); - ECPublicKey? tempPrePublicKey; - int? tempPreKeyId; + final sessionBuilder = SessionBuilder.fromSignalStore( + signalStore, + targetAddress, + ); - if (userData.prekeys.isNotEmpty) { - tempPrePublicKey = Curve.decodePoint( - DjbECPublicKey( - Uint8List.fromList(userData.prekeys.first.prekey), - ).serialize(), - 1, - ); - tempPreKeyId = userData.prekeys.first.id.toInt(); - } + ECPublicKey? tempPrePublicKey; + int? tempPreKeyId; - final tempSignedPreKeyId = userData.signedPrekeyId.toInt(); - - final tempSignedPreKeyPublic = Curve.decodePoint( - DjbECPublicKey(Uint8List.fromList(userData.signedPrekey)).serialize(), + if (userData.prekeys.isNotEmpty) { + tempPrePublicKey = Curve.decodePoint( + DjbECPublicKey( + Uint8List.fromList(userData.prekeys.first.prekey), + ).serialize(), 1, ); + tempPreKeyId = userData.prekeys.first.id.toInt(); + } - final tempSignedPreKeySignature = Uint8List.fromList( - userData.signedPrekeySignature, - ); + final tempSignedPreKeyId = userData.signedPrekeyId.toInt(); - final tempIdentityKey = IdentityKey( - Curve.decodePoint( - DjbECPublicKey( - Uint8List.fromList(userData.publicIdentityKey), - ).serialize(), - 1, - ), - ); + final tempSignedPreKeyPublic = Curve.decodePoint( + DjbECPublicKey(Uint8List.fromList(userData.signedPrekey)).serialize(), + 1, + ); - final preKeyBundle = PreKeyBundle( - userData.registrationId.toInt(), - defaultDeviceId, - tempPreKeyId, - tempPrePublicKey, - tempSignedPreKeyId, - tempSignedPreKeyPublic, - tempSignedPreKeySignature, - tempIdentityKey, - ); + final tempSignedPreKeySignature = Uint8List.fromList( + userData.signedPrekeySignature, + ); - try { - await sessionBuilder.processPreKeyBundle(preKeyBundle); - return true; - } catch (e) { - Log.error('could not process pre key bundle: $e'); - return false; - } - }); + final tempIdentityKey = IdentityKey( + Curve.decodePoint( + DjbECPublicKey( + Uint8List.fromList(userData.publicIdentityKey), + ).serialize(), + 1, + ), + ); + + final preKeyBundle = PreKeyBundle( + userData.registrationId.toInt(), + defaultDeviceId, + tempPreKeyId, + tempPrePublicKey, + tempSignedPreKeyId, + tempSignedPreKeyPublic, + tempSignedPreKeySignature, + tempIdentityKey, + ); + + try { + await sessionBuilder.processPreKeyBundle(preKeyBundle); + return true; + } catch (e) { + Log.error('could not process pre key bundle: $e'); + return false; + } } Future getPublicKeyFromContact(int contactId) async { @@ -96,11 +106,14 @@ Future getPublicKeyFromContact(int contactId) async { } } -Future handleSessionResync(int fromUserId) async { +Future handleSessionResync( + int fromUserId, { + bool useLock = true, +}) async { final userData = await apiService.getUserById(fromUserId); if (userData != null) { Log.info('Got new session data from the server to re-sync the session'); - return processSignalUserData(userData); + return processSignalUserData(userData, useLock: useLock); } Log.info('Could not download userdata from the server.'); return false; diff --git a/lib/src/services/user_discovery.service.dart b/lib/src/services/user_discovery.service.dart index a9d0015b..0161c1f0 100644 --- a/lib/src/services/user_discovery.service.dart +++ b/lib/src/services/user_discovery.service.dart @@ -75,22 +75,27 @@ class UserDiscoveryService { required int threshold, required bool sharePromotion, }) async { - try { - await FlutterUserDiscovery.initializeOrUpdate( - threshold: threshold, - userId: userService.currentUser.userId, - publicKey: await getUserPublicKey(), - sharePromotion: sharePromotion, - ); - await UserService.update( - (u) => u - ..isUserDiscoveryEnabled = true - ..userDiscoverySharePromotion = sharePromotion - ..userDiscoveryThreshold = threshold, - ); - } catch (e) { - Log.error(e); - } + Log.info('UserDiscoveryService: initializeOrUpdate started'); + final userId = userService.currentUser.userId; + final publicKey = await getUserPublicKey(); + Log.info('UserDiscoveryService: initializing Rust bridge'); + await FlutterUserDiscovery.initializeOrUpdate( + threshold: threshold, + userId: userId, + publicKey: publicKey, + sharePromotion: sharePromotion, + ).timeout(const Duration(seconds: 8)); + Log.info( + 'UserDiscoveryService: Rust bridge initialized, updating UserService', + ); + await UserService.update( + (u) => u + ..isUserDiscoveryEnabled = true + ..userDiscoverySharePromotion = sharePromotion + ..userDiscoveryThreshold = threshold + ..userDiscoveryInitializationError = false, + ); + Log.info('UserDiscoveryService: initializeOrUpdate finished'); } static Future getCurrentVersion() async { diff --git a/lib/src/services/user_study.service.dart b/lib/src/services/user_study.service.dart index 4507add8..79b9cb07 100644 --- a/lib/src/services/user_study.service.dart +++ b/lib/src/services/user_study.service.dart @@ -46,6 +46,8 @@ Future handleUserStudyUpload() async { final contacts = await twonlyDB.contactsDao.getAllContacts(); final verifications = await twonlyDB.keyVerificationDao .getFirstVerificationTypeByContacts(); + final udVerifiedByContactsCount = await twonlyDB.keyVerificationDao + .getTransferredTrustVerificationsCount(); final udFriendsShared = await twonlyDB.contactsDao .getContactsAnnouncedViaUserDiscovery(); @@ -81,6 +83,7 @@ Future handleUserStudyUpload() async { 'user_study_count_new_friends_via_suggestion': userService.currentUser.userStudyCountNewFriendsViaSuggestion, + 'user_discovery_count_verified_by_contacts': udVerifiedByContactsCount, 'accepted_contacts': contacts.where((c) => c.accepted).length, 'verified_contacts': verifications.length, diff --git a/lib/src/visual/themes/dark.dart b/lib/src/visual/themes/dark.dart index 621e0b68..ec302fab 100644 --- a/lib/src/visual/themes/dark.dart +++ b/lib/src/visual/themes/dark.dart @@ -5,7 +5,9 @@ final ThemeData darkTheme = ThemeData.dark().copyWith( brightness: Brightness.dark, seedColor: const Color(0xFF57CC99), surface: const Color.fromARGB(255, 20, 18, 23), - surfaceContainer: const Color.fromARGB(255, 33, 30, 39), + surfaceContainer: const Color.fromARGB(255, 45, 41, 54), + surfaceContainerLow: const Color.fromARGB(255, 38, 34, 45), + surfaceContainerHigh: const Color.fromARGB(255, 52, 48, 62), ), inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart index 0a522354..0e4c856e 100644 --- a/lib/src/visual/views/chats/chat_messages.view.dart +++ b/lib/src/visual/views/chats/chat_messages.view.dart @@ -77,7 +77,6 @@ class _ChatMessagesViewState extends State contactSub?.cancel(); groupActionsSub?.cancel(); _nextTypingIndicator?.cancel(); - textFieldFocus?.dispose(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -108,7 +107,7 @@ class _ChatMessagesViewState extends State }); protectMessageUpdating.protect(() async { - if (groupActionsSub == null && !newGroup.isDirectChat) { + if (groupActionsSub == null) { final actionsStream = twonlyDB.groupsDao.watchGroupActions( newGroup.groupId, ); diff --git a/lib/src/visual/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/visual/views/chats/chat_messages_components/chat_group_action.dart index 167a6ea5..9ed5d44a 100644 --- a/lib/src/visual/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/visual/views/chats/chat_messages_components/chat_group_action.dart @@ -105,6 +105,24 @@ class _ChatGroupActionState extends State { text = (contact == null) ? context.lang.youLeftGroup : context.lang.makerLeftGroup(maker); + case GroupActionType.updatedContactUsername: + if (contact != null) { + icon = FontAwesomeIcons.userPen; + text = context.lang.makerChangedUsername( + maker, + widget.action.oldGroupName!, + widget.action.newGroupName!, + ); + } + case GroupActionType.updatedContactDisplayName: + if (contact != null) { + icon = FontAwesomeIcons.userPen; + text = context.lang.makerChangedDisplayName( + maker, + widget.action.oldGroupName!, + widget.action.newGroupName!, + ); + } } // switch (widget.action.type) { diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index 4e3f187d..61633e1f 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; @@ -180,9 +180,31 @@ class _ContactViewState extends State { body: ListView( key: ValueKey(contact.userId), children: [ - Padding( - padding: const EdgeInsets.all(10), - child: AvatarIcon(contactId: contact.userId, fontSize: 30), + Center( + child: GestureDetector( + onTap: () { + // ignore: inference_failure_on_function_invocation + showDialog( + context: context, + builder: (context) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AvatarIcon(contactId: contact.userId, fontSize: 200), + ], + ), + ); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.all(10), + child: AvatarIcon(contactId: contact.userId, fontSize: 30), + ), + ), ), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -331,7 +353,9 @@ class _ContactViewState extends State { ), ); }, - child: Text(context.lang.contactUserDiscoveryManualApprovalApprove), + child: Text( + context.lang.contactUserDiscoveryManualApprovalApprove, + ), ), ) else diff --git a/lib/src/visual/views/onboarding/setup.view.dart b/lib/src/visual/views/onboarding/setup.view.dart index 048f8b23..847cdc50 100644 --- a/lib/src/visual/views/onboarding/setup.view.dart +++ b/lib/src/visual/views/onboarding/setup.view.dart @@ -43,6 +43,14 @@ extension SetupPagesExtension on SetupPages { } return null; } + + SetupPages? previous() { + final prevIndex = index - 1; + if (prevIndex >= 0) { + return SetupPages.values[prevIndex]; + } + return null; + } } class SetupView extends StatefulWidget { @@ -119,31 +127,51 @@ class _SetupViewState extends State { ), body: ListView( key: ValueKey(currentPage.name), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), children: [ _buildPage(currentPage, state), - if (!currentPage.isLast) - SizedBox( - height: 50, - child: Center( - child: TextButton( - onPressed: () async { - await UserService.update( - (u) => u.skipSetupPages = true, - ); - widget.onUpdate?.call(); - }, - child: Text( - context.lang.onboardingFinishLater, - style: TextStyle( - color: context.color.primary, - fontWeight: FontWeight.bold, + if (currentPage.index > 0 || !currentPage.isLast) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (currentPage.index > 0) + TextButton( + onPressed: () async { + await UserService.update((u) { + u.currentSetupPage = currentPage.previous()?.name; + }); + }, + child: Text( + context.lang.back, + style: TextStyle( + color: context.color.primary, + fontWeight: FontWeight.bold, + ), + ), ), - ), - ), + if (currentPage.index > 0 && !currentPage.isLast) + const SizedBox(width: 24), + if (!currentPage.isLast) + TextButton( + onPressed: () async { + await UserService.update( + (u) => u.skipSetupPages = true, + ); + widget.onUpdate?.call(); + }, + child: Text( + context.lang.onboardingFinishLater, + style: TextStyle( + color: context.color.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], ), ), - const SizedBox(height: 60), ], ), ); diff --git a/lib/src/visual/views/onboarding/setup/add_new_contacts.setup.dart b/lib/src/visual/views/onboarding/setup/add_new_contacts.setup.dart index f50687d4..b28da67f 100644 --- a/lib/src/visual/views/onboarding/setup/add_new_contacts.setup.dart +++ b/lib/src/visual/views/onboarding/setup/add_new_contacts.setup.dart @@ -65,7 +65,7 @@ class AddNewContactsPage extends StatelessWidget { const SizedBox(height: 20), Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), + padding: const EdgeInsets.symmetric(horizontal: 35), child: Column( children: [ diff --git a/lib/src/visual/views/onboarding/setup/backup.setup.dart b/lib/src/visual/views/onboarding/setup/backup.setup.dart index 1ad0ab41..659c0357 100644 --- a/lib/src/visual/views/onboarding/setup/backup.setup.dart +++ b/lib/src/visual/views/onboarding/setup/backup.setup.dart @@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; -import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/services/backup/common.backup.dart'; import 'package:twonly/src/services/user.service.dart'; @@ -24,19 +23,6 @@ class _BackupSetupPageState extends State { final TextEditingController passwordCtrl = TextEditingController(); final TextEditingController repeatedPasswordCtrl = TextEditingController(); - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (userService.currentUser.twonlySafeBackup != null) { - // twonly safe is already configured... - UserService.update((user) { - user.currentSetupPage = SetupPages.backup.next()?.name; - }); - } - }); - } - Future onPressedEnableTwonlySafe() async { setState(() { isLoading = true; diff --git a/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart b/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart index 5599da39..155fbf54 100644 --- a/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart +++ b/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart @@ -8,53 +8,101 @@ class MockContactRequestActionsComp extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - // width: 125, + return IgnorePointer( + child: SizedBox( + // width: 125, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 4), + SizedBox( + height: 20, + // width: 45, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 2, left: 4), + backgroundColor: context.color.surfaceContainerHigh, + foregroundColor: context.color.onSurface, + ), + onPressed: () {}, + child: Row( + children: [ + const Icon( + Icons.person_off_rounded, + color: Color.fromARGB(164, 244, 67, 54), + size: 12, + ), + Text( + context.lang.contactActionBlock, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + const SizedBox(width: 6), + SizedBox( + height: 20, + // width: 50, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 2, left: 4), + backgroundColor: context.color.surfaceContainerHigh, + foregroundColor: context.color.onSurface, + ), + onPressed: () {}, + child: Row( + children: [ + const Icon(Icons.check, color: Colors.green, size: 12), + Text( + context.lang.contactActionAccept, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 2), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + constraints: const BoxConstraints(), + icon: const Icon(Icons.close, size: 12), + onPressed: () {}, + ), + ], + ), + ), + ); + } +} + +class MockContactSuggestedActionsComp extends StatelessWidget { + const MockContactSuggestedActionsComp({super.key}); + + @override + Widget build(BuildContext context) { + return IgnorePointer( child: Row( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(width: 4), SizedBox( height: 20, - // width: 45, child: FilledButton( style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 2, left: 4), - backgroundColor: context.color.surfaceContainerHigh, - foregroundColor: context.color.onSurface, - ), + padding: const EdgeInsets.only(right: 8, left: 4), + ).merge(secondaryGreyButtonStyle(context)), onPressed: () {}, child: Row( children: [ - const Icon( - Icons.person_off_rounded, - color: Color.fromARGB(164, 244, 67, 54), - size: 12, + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: FaIcon(FontAwesomeIcons.userPlus, size: 10), ), Text( - context.lang.contactActionBlock, - style: const TextStyle(fontSize: 8), - ), - ], - ), - ), - ), - const SizedBox(width: 6), - SizedBox( - height: 20, - // width: 50, - child: FilledButton( - style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 2, left: 4), - backgroundColor: context.color.surfaceContainerHigh, - foregroundColor: context.color.onSurface, - ), - onPressed: () {}, - child: Row( - children: [ - const Icon(Icons.check, color: Colors.green, size: 12), - Text( - context.lang.contactActionAccept, + context.lang.friendSuggestionsRequest, style: const TextStyle(fontSize: 8), ), ], @@ -75,47 +123,3 @@ class MockContactRequestActionsComp extends StatelessWidget { ); } } - -class MockContactSuggestedActionsComp extends StatelessWidget { - const MockContactSuggestedActionsComp({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 4), - SizedBox( - height: 20, - child: FilledButton( - style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 8, left: 4), - ).merge(secondaryGreyButtonStyle(context)), - onPressed: () {}, - child: Row( - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 6), - child: FaIcon(FontAwesomeIcons.userPlus, size: 10), - ), - Text( - context.lang.friendSuggestionsRequest, - style: const TextStyle(fontSize: 8), - ), - ], - ), - ), - ), - IconButton( - style: IconButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 2), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - constraints: const BoxConstraints(), - icon: const Icon(Icons.close, size: 12), - onPressed: () {}, - ), - ], - ); - } -} diff --git a/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart b/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart index 40f22be4..fbf9eb80 100644 --- a/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart +++ b/lib/src/visual/views/onboarding/setup/components/next_button.comp.dart @@ -22,7 +22,7 @@ class NextButtonComp extends StatelessWidget { userService.currentUser.currentSetupPage, ); return ElevatedButton( - onPressed: canSubmit + onPressed: (canSubmit && !isLoading) ? () async { if (onPressed != null) { final error = await onPressed?.call(); diff --git a/lib/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart b/lib/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart index 3f7275ef..05e60f50 100644 --- a/lib/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart +++ b/lib/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart'; @@ -17,20 +15,7 @@ class LetYourFriendsFindYou extends StatefulWidget { } class _LetYourFriendsFindYouState extends State { - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (userService.currentUser.isUserDiscoveryEnabled && - userService.currentUser.userDiscoverySharePromotion) { - // feature is already configured... - UserService.update((user) { - user.currentSetupPage = SetupPages.letYourFriendsFindYou.next()?.name; - }); - } - }); - } + bool _isLoading = false; @override Widget build(BuildContext context) { @@ -77,8 +62,19 @@ class _LetYourFriendsFindYouState extends State { ), const SizedBox(height: 50), NextButtonComp( + isLoading: _isLoading, onPressed: () async { - return !(await widget.state.initializeOrUpdate()); + setState(() { + _isLoading = true; + }); + try { + final result = await widget.state.initializeOrUpdate(); + return !result; + } finally { + setState(() { + _isLoading = false; + }); + } }, ), ], diff --git a/lib/src/visual/views/onboarding/setup/share_your_friends.setup.dart b/lib/src/visual/views/onboarding/setup/share_your_friends.setup.dart index c2b6220e..27971806 100644 --- a/lib/src/visual/views/onboarding/setup/share_your_friends.setup.dart +++ b/lib/src/visual/views/onboarding/setup/share_your_friends.setup.dart @@ -1,7 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart'; @@ -16,20 +13,6 @@ class ShareYourFriendsSetupPage extends StatefulWidget { } class _ShareYourFriendsSetupPageState extends State { - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (userService.currentUser.isUserDiscoveryEnabled) { - // feature is already configured... - UserService.update((user) { - user.currentSetupPage = SetupPages.shareYourFriends.next()?.name; - }); - } - }); - } - @override Widget build(BuildContext context) { return Center( @@ -41,11 +24,7 @@ class _ShareYourFriendsSetupPageState extends State { showOnlySpecificPage: UserDiscoveryPages.shareYourFriends, ), const SizedBox(height: 60), - const NextButtonComp( - // onPressed: () async { - // return !(await widget.state.initializeOrUpdate()); - // }, - ), + const NextButtonComp(), ], ), ); diff --git a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart index 17c7f8c7..11fde673 100644 --- a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart +++ b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; @@ -9,18 +10,18 @@ import 'package:twonly/src/visual/views/contact/add_new_contact_components/frien import 'package:twonly/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/setup_switch_card.comp.dart'; -const exampleUsers = [ - 'james', - 'mary', - 'john', - 'patricia', - 'robert', - 'jennifer', - 'michael', - 'linda', - 'william', - 'lena', - 'david', +List getExampleUsers(BuildContext context) => [ + context.lang.exampleUserName1, + context.lang.exampleUserName2, + context.lang.exampleUserName3, + context.lang.exampleUserName4, + context.lang.exampleUserName5, + context.lang.exampleUserName6, + context.lang.exampleUserName7, + context.lang.exampleUserName8, + context.lang.exampleUserName9, + context.lang.exampleUserName10, + context.lang.exampleUserName11, ]; class UserDiscoverySetupState { @@ -52,21 +53,39 @@ class UserDiscoverySetupState { } Future initializeOrUpdate() async { - if (isUserDiscoveryEnabled) { - await UserDiscoveryService.initializeOrUpdate( - threshold: threshold, - sharePromotion: sharePromotion, - ); + try { + Log.info('UserDiscoverySetupState: initializeOrUpdate started'); + var hasError = false; + if (isUserDiscoveryEnabled) { + Log.info('UserDiscoverySetupState: initializing UserDiscoveryService'); + try { + await UserDiscoveryService.initializeOrUpdate( + threshold: threshold, + sharePromotion: sharePromotion, + ); + } catch (e) { + Log.error( + 'UserDiscoverySetupState: UserDiscoveryService failed or timed out: $e', + ); + hasError = true; + } + } + + Log.info('UserDiscoverySetupState: updating UserService'); + await UserService.update((u) { + u + ..isUserDiscoveryEnabled = isUserDiscoveryEnabled + ..requiredSendImages = requiredSendImages + ..userDiscoveryRequiresManualApproval = isManualApprovalEnabled + ..userDiscoveryInitializationError = hasError; + }); + + Log.info('UserDiscoverySetupState: initializeOrUpdate finished'); + return true; + } catch (e) { + Log.error('UserDiscoverySetupState: initializeOrUpdate failed: $e'); + return false; } - - await UserService.update((u) { - u - ..isUserDiscoveryEnabled = isUserDiscoveryEnabled - ..requiredSendImages = requiredSendImages - ..userDiscoveryRequiresManualApproval = isManualApprovalEnabled; - }); - - return true; } } @@ -107,15 +126,12 @@ class UserDiscoverySetupComp extends StatelessWidget { textAlign: TextAlign.center, ), - const SizedBox(height: 32), + const SizedBox(height: 24), SetupSwitchCard( value: state.isUserDiscoveryEnabled, onChanged: (val) => state.update(() { state.isUserDiscoveryEnabled = val; - if (!val) { - state.sharePromotion = false; - } }), title: context.lang.onboardingUserDiscoveryShareFriends, expandedChild: Column( @@ -143,48 +159,10 @@ class UserDiscoverySetupComp extends StatelessWidget { ), ), const Padding( - padding: EdgeInsets.symmetric(vertical: 8), + padding: EdgeInsets.only(bottom: 8), child: Divider(), ), - Text( - context.lang.onboardingUserDiscoveryContactsVerifiedBadge, - style: TextStyle( - color: context.color.onSurfaceVariant, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - Center( - child: Container( - width: 100, - height: 40, - decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 0.5), - borderRadius: BorderRadius.circular(12), - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AvatarIcon(fontSize: 12), - SizedBox(width: 5), - Text( - 'jane', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - SizedBox(width: 5), - VerificationBadgeComp( - isVerifiedByTransferredTrust: true, - size: 14, - clickable: false, - ), - ], - ), - ), - ), - const SizedBox(height: 24), + const _ExampleLabel(), Text( context.lang.onboardingUserDiscoveryWhoIsRequesting, style: TextStyle( @@ -214,9 +192,9 @@ class UserDiscoverySetupComp extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'jane', - style: TextStyle( + Text( + context.lang.exampleJane, + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 13, ), @@ -226,8 +204,8 @@ class UserDiscoverySetupComp extends StatelessWidget { children: buildFriendsListTextString( context, [ - 'mary', - 'james', + context.lang.exampleUserName2, + context.lang.exampleUserName1, ], ), style: const TextStyle(fontSize: 10), @@ -241,6 +219,47 @@ class UserDiscoverySetupComp extends StatelessWidget { ), ), ), + + const SizedBox(height: 24), + Text( + context.lang.onboardingUserDiscoveryContactsVerifiedBadge, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Center( + child: Container( + width: 100, + height: 40, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const AvatarIcon(fontSize: 12), + const SizedBox(width: 5), + Text( + context.lang.exampleJane, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 5), + const VerificationBadgeComp( + isVerifiedByTransferredTrust: true, + size: 14, + clickable: false, + ), + ], + ), + ), + ), + const SizedBox(height: 16), ], ), @@ -275,18 +294,15 @@ class UserDiscoverySetupComp extends StatelessWidget { SetupSwitchCard( value: state.sharePromotion, onChanged: (val) => state.update(() { - if (val) { - state.isUserDiscoveryEnabled = true; - } state.sharePromotion = val; }), title: context.lang.onboardingUserDiscoveryBeRecommended, - expandedChild: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( + expandedChild: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( children: [ Expanded( child: Text( @@ -319,121 +335,128 @@ class UserDiscoverySetupComp extends StatelessWidget { ), ], ), - const SizedBox(height: 16), - Text( - context.lang.onboardingUserDiscoveryWhatOthersSee, - style: TextStyle( - color: context.color.onSurfaceVariant, - fontSize: 12, - ), - textAlign: TextAlign.center, + ), + const Padding( + padding: EdgeInsets.only(bottom: 8, top: 8), + child: Divider(), + ), + const _ExampleLabel(), + Text( + context.lang.onboardingUserDiscoveryWhatOthersSee, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 12, ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 3, - ), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 0.5), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const AvatarIcon(fontSize: 14), - const SizedBox(width: 5), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - userService.currentUser.username, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const AvatarIcon(fontSize: 14), + const SizedBox(width: 5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userService.currentUser.username, + style: const TextStyle( + fontWeight: FontWeight.bold, ), - RichText( - text: TextSpan( - children: buildFriendsListTextString( - context, - exampleUsers.sublist( - 0, - state.threshold, - ), + ), + RichText( + text: TextSpan( + children: buildFriendsListTextString( + context, + getExampleUsers(context).sublist( + 0, + state.threshold, ), - style: const TextStyle(fontSize: 11), ), + style: const TextStyle(fontSize: 11), ), - ], - ), + ), + ], ), - const MockContactSuggestedActionsComp(), - ], - ), + ), + const MockContactSuggestedActionsComp(), + ], ), ), - const SizedBox(height: 16), - Text( - context.lang.onboardingUserDiscoveryWhatYouSee, - style: TextStyle( - color: context.color.onSurfaceVariant, - fontSize: 12, - ), - textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + context.lang.onboardingUserDiscoveryWhatYouSee, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 12, ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 3, - ), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 0.5), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const AvatarIcon(fontSize: 14), - const SizedBox(width: 5), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'jane', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 13, - ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const AvatarIcon(fontSize: 14), + const SizedBox(width: 5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.exampleJane, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, ), - RichText( - text: TextSpan( - children: buildFriendsListTextString( - context, - exampleUsers.sublist( - 0, - state.threshold, - ), + ), + RichText( + text: TextSpan( + children: buildFriendsListTextString( + context, + getExampleUsers(context).sublist( + 0, + state.threshold, ), - style: const TextStyle(fontSize: 10), ), + style: const TextStyle(fontSize: 10), ), - ], - ), + ), + ], ), - const MockContactRequestActionsComp(), - ], - ), + ), + const MockContactRequestActionsComp(), + ], ), ), - ], - ), + ), + const SizedBox(height: 16), + ], ), ), ], @@ -442,3 +465,28 @@ class UserDiscoverySetupComp extends StatelessWidget { ); } } + +class _ExampleLabel extends StatelessWidget { + const _ExampleLabel(); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + context.lang.onboardingExampleLabel, + style: const TextStyle(fontSize: 10), + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d43bc94a..378fba9a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.2.8+117 +version: 0.2.9+118 environment: sdk: ^3.11.0 diff --git a/rust/src/bridge/callbacks/user_discovery.rs b/rust/src/bridge/callbacks/user_discovery.rs index 5d57b4bc..1a24fb94 100644 --- a/rust/src/bridge/callbacks/user_discovery.rs +++ b/rust/src/bridge/callbacks/user_discovery.rs @@ -46,7 +46,7 @@ impl UserDiscoveryUtils for UserDiscoveryUtilsFlutter { impl UserDiscoveryStore for UserDiscoveryStoreFlutter { async fn get_config(&self) -> Result { - let ws = get_twonly_flutter().unwrap(); + let ws = get_twonly_flutter()?; let config_path = PathBuf::from(&ws.config.data_directory).join("user_discovery_config.json"); @@ -59,7 +59,7 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter { async fn update_config(&self, update: String) -> Result<()> { tracing::debug!("Updating configuration file."); - let ws = get_twonly_flutter().unwrap(); + let ws = get_twonly_flutter()?; let config_path = PathBuf::from(&ws.config.data_directory).join("user_discovery_config.json"); std::fs::write(config_path, &update)?; diff --git a/rust/src/bridge/wrapper/user_discovery.rs b/rust/src/bridge/wrapper/user_discovery.rs index 175184dd..6c50a7f8 100644 --- a/rust/src/bridge/wrapper/user_discovery.rs +++ b/rust/src/bridge/wrapper/user_discovery.rs @@ -10,12 +10,16 @@ impl FlutterUserDiscovery { public_key: Vec, share_promotion: bool, ) -> Result<()> { - Ok(get_twonly_flutter()? - .user_discovery - .get() - .await + tracing::info!("Rust bridge: initialize_or_update started"); + let twonly = get_twonly_flutter()?; + tracing::info!("Rust bridge: getting user_discovery lock"); + let user_discovery = twonly.user_discovery.get().await; + tracing::info!("Rust bridge: calling initialize_or_update on protocols"); + let res = user_discovery .initialize_or_update(threshold, user_id, public_key, share_promotion) - .await?) + .await; + tracing::info!("Rust bridge: initialize_or_update on protocols finished"); + Ok(res?) } pub async fn get_current_version() -> Result> { diff --git a/rust_dependencies/protocols/src/user_discovery.rs b/rust_dependencies/protocols/src/user_discovery.rs index a7df228c..68bccd63 100644 --- a/rust_dependencies/protocols/src/user_discovery.rs +++ b/rust_dependencies/protocols/src/user_discovery.rs @@ -91,8 +91,8 @@ impl UserDiscovery, share_promotion: bool, ) -> Result<()> { - let config_lock = self.config_lock.lock().await; - let mut config = match self.store.get_config().await { + tracing::info!("Protocols: initialize_or_update started, getting config from store"); + let config = match self.store.get_config().await { Ok(config) => { let mut config: UserDiscoveryConfig = serde_json::from_str(&config)?; config.threshold = threshold; @@ -113,23 +113,39 @@ impl UserDiscovery serde_json::from_str(&c)?, + Err(_) => UserDiscoveryConfig { + threshold, + user_id, + ..Default::default() + }, + }; - self.update_config(config, config_lock).await?; + final_config.public_id = public_id; + final_config.announcement_version += 1; + final_config.verification_shares = verification_shares; + final_config.share_promotion = share_promotion; + final_config.threshold = threshold; + self.update_config(final_config, config_lock).await?; + + tracing::info!("Protocols: initialize_or_update finished"); Ok(()) }