From e005d01177bc6e42d6a19cf888ae3a1eea08ecf8 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 7 Apr 2025 23:30:24 +0200 Subject: [PATCH] fix multiple race condition problems --- lib/main.dart | 1 - lib/src/app.dart | 2 ++ lib/src/providers/api/api.dart | 24 ++++++++------- lib/src/providers/api/media.dart | 3 +- lib/src/providers/api/server_messages.dart | 30 +++++++++++++------ lib/src/providers/hive.dart | 2 +- lib/src/services/notification_service.dart | 6 ++-- lib/src/utils/misc.dart | 4 +-- .../views/chats/chat_item_details_view.dart | 8 ++++- lib/src/views/chats/media_viewer_view.dart | 14 ++++++++- lib/src/views/chats/search_username_view.dart | 11 +++++++ lib/src/views/settings/diagnostics_view.dart | 2 +- 12 files changed, 76 insertions(+), 31 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index cb50f2f..fedb5eb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,7 +41,6 @@ void main() async { apiProvider = ApiProvider(); twonlyDatabase = TwonlyDatabase(); - setupNotificationWithUsers(); runApp( MultiProvider( diff --git a/lib/src/app.dart b/lib/src/app.dart index be49485..9ab0eef 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -2,6 +2,7 @@ import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/components/connection_state.dart'; import 'package:twonly/src/providers/settings_change_provider.dart'; +import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/onboarding/onboarding_view.dart'; import 'package:twonly/src/views/home_view.dart'; @@ -44,6 +45,7 @@ class _MyAppState extends State with WidgetsBindingObserver { setState(() { _isConnected = isConnected; }); + setupNotificationWithUsers(); }; // WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index 517e723..59edb6a 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -182,17 +182,19 @@ Future sendTextMessage( } Future notifyContactAboutOpeningMessage( - int fromUserId, int messageOtherId) async { - encryptAndSendMessage( - null, - fromUserId, - MessageJson( - kind: MessageKind.opened, - messageId: messageOtherId, - content: MessageContent(), - timestamp: DateTime.now(), - ), - ); + int fromUserId, List messageOtherIds) async { + for (final messageOtherId in messageOtherIds) { + await encryptAndSendMessage( + null, + fromUserId, + MessageJson( + kind: MessageKind.opened, + messageId: messageOtherId, + content: MessageContent(), + timestamp: DateTime.now(), + ), + ); + } } Future notifyContactsAboutProfileChange() async { diff --git a/lib/src/providers/api/media.dart b/lib/src/providers/api/media.dart index c827e86..297ceb1 100644 --- a/lib/src/providers/api/media.dart +++ b/lib/src/providers/api/media.dart @@ -462,7 +462,8 @@ Future getDownloadedMedia( if (media == null) return null; // await userOpenedOtherMessage(otherUserId, messageOtherId); - notifyContactAboutOpeningMessage(message.contactId, message.messageOtherId!); + notifyContactAboutOpeningMessage( + message.contactId, [message.messageOtherId!]); twonlyDatabase.messagesDao.updateMessageByMessageId( message.messageId, MessagesCompanion(openedAt: Value(DateTime.now()))); diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index 4f103c6..2e90367 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -23,8 +23,16 @@ import 'package:twonly/src/services/notification_service.dart'; // ignore: library_prefixes import 'package:twonly/src/utils/signal.dart' as SignalHelper; +bool isBlocked = false; + Future handleServerMessage(server.ServerToClient msg) async { client.Response? response; + int maxCounter = 0; // only block for 2 seconds + while (isBlocked && maxCounter < 200) { + await Future.delayed(Duration(milliseconds: 10)); + maxCounter += 1; + } + isBlocked = true; try { if (msg.v0.hasRequestNewPreKeys()) { @@ -38,12 +46,14 @@ Future handleServerMessage(server.ServerToClient msg) async { } else { Logger("handleServerMessage") .shout("Got a new message from the server: $msg"); - return; + response = client.Response()..error = ErrorCode.InternalError; } } catch (e) { response = client.Response()..error = ErrorCode.InternalError; } + isBlocked = false; + var v0 = client.V0() ..seq = msg.v0.seq ..response = response; @@ -68,17 +78,19 @@ Future handleDownloadData(DownloadData data) async { if (messageId == null) { Logger("server_messages") - .info("download data received, but unknown messageID"); + .shout("download data received, but unknown messageID"); // answers with ok, so the server will delete the message var ok = client.Response_Ok()..none = true; return client.Response()..ok = ok; } if (data.fin && data.data.isEmpty) { + Logger("server_messages") + .shout("Got an image message, but was already deleted by the server!"); // media file was deleted by the server. remove the media from device await twonlyDatabase.messagesDao.deleteMessageById(messageId); - box.delete(boxId); - box.delete("${data.downloadToken}_downloaded"); + await box.delete(boxId); + await box.delete("${data.downloadToken}_downloaded"); var ok = client.Response_Ok()..none = true; return client.Response()..ok = ok; } @@ -103,7 +115,7 @@ Future handleDownloadData(DownloadData data) async { if (!data.fin) { // download not finished, so waiting for more data... - box.put(boxId, downloadedBytes); + await box.put(boxId, downloadedBytes); var ok = client.Response_Ok()..none = true; return client.Response()..ok = ok; } @@ -138,7 +150,7 @@ Future handleDownloadData(DownloadData data) async { final rawBytes = await xchacha20.decrypt(secretBox, secretKey: secretKeyData); - box.put("${data.downloadToken}_downloaded", rawBytes); + await box.put("${data.downloadToken}_downloaded", rawBytes); } catch (e) { Logger("server_messages").info("Decryption error: $e"); // deleting message as this is an invalid image @@ -148,13 +160,14 @@ Future handleDownloadData(DownloadData data) async { return client.Response()..ok = ok; } + Logger("server_messages").info("Downloaded: $messageId"); await twonlyDatabase.messagesDao.updateMessageByOtherUser( msg.contactId, messageId, MessagesCompanion(downloadState: Value(DownloadState.downloaded)), ); - box.delete(boxId); + await box.delete(boxId); var ok = client.Response_Ok()..none = true; return client.Response()..ok = ok; @@ -201,7 +214,6 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { final update = ContactsCompanion(accepted: Value(true)); await twonlyDatabase.contactsDao.updateContact(fromUserId, update); notifyContactsAboutProfileChange(); - setupNotificationWithUsers(); break; case MessageKind.profileChange: @@ -344,7 +356,6 @@ Future handleContactRequest( Result username = await apiProvider.getUsername(fromUserId); if (username.isSuccess) { Uint8List name = username.value.userdata.username; - await twonlyDatabase.contactsDao.insertContact( ContactsCompanion( username: Value(utf8.decode(name)), @@ -353,6 +364,7 @@ Future handleContactRequest( ), ); } + await setupNotificationWithUsers(); var ok = client.Response_Ok()..none = true; return client.Response()..ok = ok; } diff --git a/lib/src/providers/hive.dart b/lib/src/providers/hive.dart index 8a9df54..306f007 100644 --- a/lib/src/providers/hive.dart +++ b/lib/src/providers/hive.dart @@ -16,7 +16,7 @@ Future initMediaStorage() async { value: base64UrlEncode(key), ); } - final dir = await getApplicationDocumentsDirectory(); + final dir = await getApplicationSupportDirectory(); Hive.init(dir.path); } diff --git a/lib/src/services/notification_service.dart b/lib/src/services/notification_service.dart index e2544ed..5cd1ac5 100644 --- a/lib/src/services/notification_service.dart +++ b/lib/src/services/notification_service.dart @@ -97,7 +97,7 @@ Future setupNotificationWithUsers({bool force = false}) async { key: List.generate(32, (index) => random.nextInt(256)), createdAt: DateTime.now(), ); - sendNewPushKey(contact.userId, pushKey); + await sendNewPushKey(contact.userId, pushKey); pushKeys[contact.userId]!.keys.add(pushKey); pushKeys[contact.userId]!.displayName = getContactDisplayName(contact); wasChanged = true; @@ -109,7 +109,7 @@ Future setupNotificationWithUsers({bool force = false}) async { key: List.generate(32, (index) => random.nextInt(256)), createdAt: DateTime.now(), ); - sendNewPushKey(contact.userId, pushKey); + await sendNewPushKey(contact.userId, pushKey); final pushUser = PushUser( displayName: getContactDisplayName(contact), keys: [pushKey], @@ -588,7 +588,7 @@ Future getAvatarIcon(Contact user) async { final Uint8List pngBytes = byteData!.buffer.asUint8List(); // Get the directory to save the image - final directory = await getApplicationDocumentsDirectory(); + final directory = await getApplicationCacheDirectory(); final avatarsDirectory = Directory('${directory.path}/avatars'); // Create the avatars directory if it does not exist diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index e193344..03ce93a 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -20,7 +20,7 @@ extension ShortCutsExtension on BuildContext { } Future writeLogToFile(LogRecord record) async { - final directory = await getApplicationDocumentsDirectory(); + final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); // Prepare the log message @@ -32,7 +32,7 @@ Future writeLogToFile(LogRecord record) async { } Future deleteLogFile() async { - final directory = await getApplicationDocumentsDirectory(); + final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); if (await logFile.exists()) { diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index 4377ab5..09a363b 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -235,12 +235,14 @@ class _ChatItemDetailsViewState extends State { // should be cleared Map> tmpReactionsToMyMessages = {}; Map> tmpTeactionsToOtherMessages = {}; + + List openedMessageOtherIds = []; for (Message msg in msgs) { if (msg.kind == MessageKind.textMessage && msg.messageOtherId != null && msg.openedAt == null) { updated = true; - notifyContactAboutOpeningMessage(widget.userid, msg.messageOtherId!); + openedMessageOtherIds.add(msg.messageOtherId!); } if (msg.responseToMessageId != null) { @@ -261,7 +263,11 @@ class _ChatItemDetailsViewState extends State { displayedMessages.add(msg); } } + if (openedMessageOtherIds.isNotEmpty) { + notifyContactAboutOpeningMessage(widget.userid, openedMessageOtherIds); + } twonlyDatabase.messagesDao.openedAllNonMediaMessages(widget.userid); + // should be fixed with that if (!updated) { // The stream should be get an update, so only update the UI when all are opened setState(() { diff --git a/lib/src/views/chats/media_viewer_view.dart b/lib/src/views/chats/media_viewer_view.dart index aacd26b..031d408 100644 --- a/lib/src/views/chats/media_viewer_view.dart +++ b/lib/src/views/chats/media_viewer_view.dart @@ -64,7 +64,19 @@ class _MediaViewerViewState extends State { _subscription = messages.listen((messages) { for (Message msg in messages) { - if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) { + // if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) { + // allMediaFiles.add(msg); + // } + // Find the index of the existing message with the same messageId + int index = + allMediaFiles.indexWhere((m) => m.messageId == msg.messageId); + + if (index >= 1) { + // to not modify the first message + // If the message exists, replace it + allMediaFiles[index] = msg; + } else { + // If the message does not exist, add it allMediaFiles.add(msg); } } diff --git a/lib/src/views/chats/search_username_view.dart b/lib/src/views/chats/search_username_view.dart index 95e8c7f..1733d5e 100644 --- a/lib/src/views/chats/search_username_view.dart +++ b/lib/src/views/chats/search_username_view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:twonly/src/components/alert_dialog.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; @@ -122,6 +123,16 @@ class _SearchUsernameView extends State { onSubmitted: (_) { _addNewUser(context); }, + onChanged: (value) { + searchUserName.text = value.toLowerCase(); + searchUserName.selection = TextSelection.fromPosition( + TextPosition(offset: searchUserName.text.length), + ); + }, + inputFormatters: [ + LengthLimitingTextInputFormatter(12), + FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9A-Z]')), + ], controller: searchUserName, decoration: getInputDecoration(context.lang.searchUsernameInput), diff --git a/lib/src/views/settings/diagnostics_view.dart b/lib/src/views/settings/diagnostics_view.dart index 90ff823..0f0d8cf 100644 --- a/lib/src/views/settings/diagnostics_view.dart +++ b/lib/src/views/settings/diagnostics_view.dart @@ -76,7 +76,7 @@ class DiagnosticsView extends StatelessWidget { } Future _loadLogFile() async { - final directory = await getApplicationDocumentsDirectory(); + final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); if (await logFile.exists()) {