fix multiple race condition problems

This commit is contained in:
otsmr 2025-04-07 23:30:24 +02:00
parent 6d67a16840
commit e005d01177
12 changed files with 76 additions and 31 deletions

View file

@ -41,7 +41,6 @@ void main() async {
apiProvider = ApiProvider(); apiProvider = ApiProvider();
twonlyDatabase = TwonlyDatabase(); twonlyDatabase = TwonlyDatabase();
setupNotificationWithUsers();
runApp( runApp(
MultiProvider( MultiProvider(

View file

@ -2,6 +2,7 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/components/connection_state.dart'; import 'package:twonly/src/components/connection_state.dart';
import 'package:twonly/src/providers/settings_change_provider.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/utils/storage.dart';
import 'package:twonly/src/views/onboarding/onboarding_view.dart'; import 'package:twonly/src/views/onboarding/onboarding_view.dart';
import 'package:twonly/src/views/home_view.dart'; import 'package:twonly/src/views/home_view.dart';
@ -44,6 +45,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
setState(() { setState(() {
_isConnected = isConnected; _isConnected = isConnected;
}); });
setupNotificationWithUsers();
}; };
// WidgetsBinding.instance.addPostFrameCallback((_) { // WidgetsBinding.instance.addPostFrameCallback((_) {

View file

@ -182,8 +182,9 @@ Future sendTextMessage(
} }
Future notifyContactAboutOpeningMessage( Future notifyContactAboutOpeningMessage(
int fromUserId, int messageOtherId) async { int fromUserId, List<int> messageOtherIds) async {
encryptAndSendMessage( for (final messageOtherId in messageOtherIds) {
await encryptAndSendMessage(
null, null,
fromUserId, fromUserId,
MessageJson( MessageJson(
@ -194,6 +195,7 @@ Future notifyContactAboutOpeningMessage(
), ),
); );
} }
}
Future notifyContactsAboutProfileChange() async { Future notifyContactsAboutProfileChange() async {
List<Contact> contacts = List<Contact> contacts =

View file

@ -462,7 +462,8 @@ Future<Uint8List?> getDownloadedMedia(
if (media == null) return null; if (media == null) return null;
// await userOpenedOtherMessage(otherUserId, messageOtherId); // await userOpenedOtherMessage(otherUserId, messageOtherId);
notifyContactAboutOpeningMessage(message.contactId, message.messageOtherId!); notifyContactAboutOpeningMessage(
message.contactId, [message.messageOtherId!]);
twonlyDatabase.messagesDao.updateMessageByMessageId( twonlyDatabase.messagesDao.updateMessageByMessageId(
message.messageId, MessagesCompanion(openedAt: Value(DateTime.now()))); message.messageId, MessagesCompanion(openedAt: Value(DateTime.now())));

View file

@ -23,8 +23,16 @@ import 'package:twonly/src/services/notification_service.dart';
// ignore: library_prefixes // ignore: library_prefixes
import 'package:twonly/src/utils/signal.dart' as SignalHelper; import 'package:twonly/src/utils/signal.dart' as SignalHelper;
bool isBlocked = false;
Future handleServerMessage(server.ServerToClient msg) async { Future handleServerMessage(server.ServerToClient msg) async {
client.Response? response; 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 { try {
if (msg.v0.hasRequestNewPreKeys()) { if (msg.v0.hasRequestNewPreKeys()) {
@ -38,12 +46,14 @@ Future handleServerMessage(server.ServerToClient msg) async {
} else { } else {
Logger("handleServerMessage") Logger("handleServerMessage")
.shout("Got a new message from the server: $msg"); .shout("Got a new message from the server: $msg");
return; response = client.Response()..error = ErrorCode.InternalError;
} }
} catch (e) { } catch (e) {
response = client.Response()..error = ErrorCode.InternalError; response = client.Response()..error = ErrorCode.InternalError;
} }
isBlocked = false;
var v0 = client.V0() var v0 = client.V0()
..seq = msg.v0.seq ..seq = msg.v0.seq
..response = response; ..response = response;
@ -68,17 +78,19 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
if (messageId == null) { if (messageId == null) {
Logger("server_messages") 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 // answers with ok, so the server will delete the message
var ok = client.Response_Ok()..none = true; var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
if (data.fin && data.data.isEmpty) { 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 // media file was deleted by the server. remove the media from device
await twonlyDatabase.messagesDao.deleteMessageById(messageId); await twonlyDatabase.messagesDao.deleteMessageById(messageId);
box.delete(boxId); await box.delete(boxId);
box.delete("${data.downloadToken}_downloaded"); await box.delete("${data.downloadToken}_downloaded");
var ok = client.Response_Ok()..none = true; var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
@ -103,7 +115,7 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
if (!data.fin) { if (!data.fin) {
// download not finished, so waiting for more data... // download not finished, so waiting for more data...
box.put(boxId, downloadedBytes); await box.put(boxId, downloadedBytes);
var ok = client.Response_Ok()..none = true; var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
@ -138,7 +150,7 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
final rawBytes = final rawBytes =
await xchacha20.decrypt(secretBox, secretKey: secretKeyData); await xchacha20.decrypt(secretBox, secretKey: secretKeyData);
box.put("${data.downloadToken}_downloaded", rawBytes); await box.put("${data.downloadToken}_downloaded", rawBytes);
} catch (e) { } catch (e) {
Logger("server_messages").info("Decryption error: $e"); Logger("server_messages").info("Decryption error: $e");
// deleting message as this is an invalid image // deleting message as this is an invalid image
@ -148,13 +160,14 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
Logger("server_messages").info("Downloaded: $messageId");
await twonlyDatabase.messagesDao.updateMessageByOtherUser( await twonlyDatabase.messagesDao.updateMessageByOtherUser(
msg.contactId, msg.contactId,
messageId, messageId,
MessagesCompanion(downloadState: Value(DownloadState.downloaded)), MessagesCompanion(downloadState: Value(DownloadState.downloaded)),
); );
box.delete(boxId); await box.delete(boxId);
var ok = client.Response_Ok()..none = true; var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
@ -201,7 +214,6 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
final update = ContactsCompanion(accepted: Value(true)); final update = ContactsCompanion(accepted: Value(true));
await twonlyDatabase.contactsDao.updateContact(fromUserId, update); await twonlyDatabase.contactsDao.updateContact(fromUserId, update);
notifyContactsAboutProfileChange(); notifyContactsAboutProfileChange();
setupNotificationWithUsers();
break; break;
case MessageKind.profileChange: case MessageKind.profileChange:
@ -344,7 +356,6 @@ Future<client.Response> handleContactRequest(
Result username = await apiProvider.getUsername(fromUserId); Result username = await apiProvider.getUsername(fromUserId);
if (username.isSuccess) { if (username.isSuccess) {
Uint8List name = username.value.userdata.username; Uint8List name = username.value.userdata.username;
await twonlyDatabase.contactsDao.insertContact( await twonlyDatabase.contactsDao.insertContact(
ContactsCompanion( ContactsCompanion(
username: Value(utf8.decode(name)), username: Value(utf8.decode(name)),
@ -353,6 +364,7 @@ Future<client.Response> handleContactRequest(
), ),
); );
} }
await setupNotificationWithUsers();
var ok = client.Response_Ok()..none = true; var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }

View file

@ -16,7 +16,7 @@ Future initMediaStorage() async {
value: base64UrlEncode(key), value: base64UrlEncode(key),
); );
} }
final dir = await getApplicationDocumentsDirectory(); final dir = await getApplicationSupportDirectory();
Hive.init(dir.path); Hive.init(dir.path);
} }

View file

@ -97,7 +97,7 @@ Future setupNotificationWithUsers({bool force = false}) async {
key: List<int>.generate(32, (index) => random.nextInt(256)), key: List<int>.generate(32, (index) => random.nextInt(256)),
createdAt: DateTime.now(), createdAt: DateTime.now(),
); );
sendNewPushKey(contact.userId, pushKey); await sendNewPushKey(contact.userId, pushKey);
pushKeys[contact.userId]!.keys.add(pushKey); pushKeys[contact.userId]!.keys.add(pushKey);
pushKeys[contact.userId]!.displayName = getContactDisplayName(contact); pushKeys[contact.userId]!.displayName = getContactDisplayName(contact);
wasChanged = true; wasChanged = true;
@ -109,7 +109,7 @@ Future setupNotificationWithUsers({bool force = false}) async {
key: List<int>.generate(32, (index) => random.nextInt(256)), key: List<int>.generate(32, (index) => random.nextInt(256)),
createdAt: DateTime.now(), createdAt: DateTime.now(),
); );
sendNewPushKey(contact.userId, pushKey); await sendNewPushKey(contact.userId, pushKey);
final pushUser = PushUser( final pushUser = PushUser(
displayName: getContactDisplayName(contact), displayName: getContactDisplayName(contact),
keys: [pushKey], keys: [pushKey],
@ -588,7 +588,7 @@ Future<String?> getAvatarIcon(Contact user) async {
final Uint8List pngBytes = byteData!.buffer.asUint8List(); final Uint8List pngBytes = byteData!.buffer.asUint8List();
// Get the directory to save the image // Get the directory to save the image
final directory = await getApplicationDocumentsDirectory(); final directory = await getApplicationCacheDirectory();
final avatarsDirectory = Directory('${directory.path}/avatars'); final avatarsDirectory = Directory('${directory.path}/avatars');
// Create the avatars directory if it does not exist // Create the avatars directory if it does not exist

View file

@ -20,7 +20,7 @@ extension ShortCutsExtension on BuildContext {
} }
Future<void> writeLogToFile(LogRecord record) async { Future<void> writeLogToFile(LogRecord record) async {
final directory = await getApplicationDocumentsDirectory(); final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log'); final logFile = File('${directory.path}/app.log');
// Prepare the log message // Prepare the log message
@ -32,7 +32,7 @@ Future<void> writeLogToFile(LogRecord record) async {
} }
Future<bool> deleteLogFile() async { Future<bool> deleteLogFile() async {
final directory = await getApplicationDocumentsDirectory(); final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log'); final logFile = File('${directory.path}/app.log');
if (await logFile.exists()) { if (await logFile.exists()) {

View file

@ -235,12 +235,14 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
// should be cleared // should be cleared
Map<int, List<Message>> tmpReactionsToMyMessages = {}; Map<int, List<Message>> tmpReactionsToMyMessages = {};
Map<int, List<Message>> tmpTeactionsToOtherMessages = {}; Map<int, List<Message>> tmpTeactionsToOtherMessages = {};
List<int> openedMessageOtherIds = [];
for (Message msg in msgs) { for (Message msg in msgs) {
if (msg.kind == MessageKind.textMessage && if (msg.kind == MessageKind.textMessage &&
msg.messageOtherId != null && msg.messageOtherId != null &&
msg.openedAt == null) { msg.openedAt == null) {
updated = true; updated = true;
notifyContactAboutOpeningMessage(widget.userid, msg.messageOtherId!); openedMessageOtherIds.add(msg.messageOtherId!);
} }
if (msg.responseToMessageId != null) { if (msg.responseToMessageId != null) {
@ -261,7 +263,11 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
displayedMessages.add(msg); displayedMessages.add(msg);
} }
} }
if (openedMessageOtherIds.isNotEmpty) {
notifyContactAboutOpeningMessage(widget.userid, openedMessageOtherIds);
}
twonlyDatabase.messagesDao.openedAllNonMediaMessages(widget.userid); twonlyDatabase.messagesDao.openedAllNonMediaMessages(widget.userid);
// should be fixed with that
if (!updated) { if (!updated) {
// The stream should be get an update, so only update the UI when all are opened // The stream should be get an update, so only update the UI when all are opened
setState(() { setState(() {

View file

@ -64,7 +64,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
_subscription = messages.listen((messages) { _subscription = messages.listen((messages) {
for (Message msg in 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); allMediaFiles.add(msg);
} }
} }

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:twonly/src/components/alert_dialog.dart'; import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
@ -122,6 +123,16 @@ class _SearchUsernameView extends State<SearchUsernameView> {
onSubmitted: (_) { onSubmitted: (_) {
_addNewUser(context); _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, controller: searchUserName,
decoration: decoration:
getInputDecoration(context.lang.searchUsernameInput), getInputDecoration(context.lang.searchUsernameInput),

View file

@ -76,7 +76,7 @@ class DiagnosticsView extends StatelessWidget {
} }
Future<String> _loadLogFile() async { Future<String> _loadLogFile() async {
final directory = await getApplicationDocumentsDirectory(); final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log'); final logFile = File('${directory.path}/app.log');
if (await logFile.exists()) { if (await logFile.exists()) {