From 68cf1b6b89c4e8ec5693dcde92f5e32f455ff484 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 15 Mar 2026 14:22:00 +0100 Subject: [PATCH 1/3] only force create avatars in case they where changed or did not yet exists --- .../api/client2client/contact.c2c.dart | 7 +++--- lib/src/utils/avatars.dart | 22 ++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index 217b745..3f7c0b7 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -124,14 +124,15 @@ Future handleContactUpdate( await twonlyDB.contactsDao.updateContact( fromUserId, ContactsCompanion( - avatarSvgCompressed: - Value(Uint8List.fromList(contactUpdate.avatarSvgCompressed)), + avatarSvgCompressed: Value( + Uint8List.fromList(contactUpdate.avatarSvgCompressed), + ), displayName: Value(contactUpdate.displayName), username: Value(contactUpdate.username), senderProfileCounter: Value(senderProfileCounter), ), ); - unawaited(createPushAvatars()); + unawaited(createPushAvatars(forceForUserId: fromUserId)); } } } diff --git a/lib/src/utils/avatars.dart b/lib/src/utils/avatars.dart index 08a903e..d1fcd69 100644 --- a/lib/src/utils/avatars.dart +++ b/lib/src/utils/avatars.dart @@ -6,12 +6,21 @@ import 'package:flutter_svg/svg.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart'; -Future createPushAvatars() async { +Future createPushAvatars({int? forceForUserId}) async { final contacts = await twonlyDB.contactsDao.getAllContacts(); for (final contact in contacts) { if (contact.avatarSvgCompressed == null) continue; + if (forceForUserId == null) { + if (avatarPNGFile(contact.userId).existsSync()) { + continue; // only create the avatar in case no avatar exists yet fot this user + } + } else if (contact.userId != forceForUserId) { + // only update the avatar for this specified contact + continue; + } + final avatarSvg = getAvatarSvg(contact.avatarSvgCompressed!); final pictureInfo = await vg.loadPicture(SvgStringLoader(avatarSvg), null); @@ -27,8 +36,9 @@ Future createPushAvatars() async { } File avatarPNGFile(int contactId) { - final avatarsDirectory = - Directory('$globalApplicationCacheDirectory/avatars'); + final avatarsDirectory = Directory( + '$globalApplicationCacheDirectory/avatars', + ); if (!avatarsDirectory.existsSync()) { avatarsDirectory.createSync(recursive: true); @@ -42,8 +52,10 @@ Future getUserAvatar() async { return data.buffer.asUint8List(); } - final pictureInfo = - await vg.loadPicture(SvgStringLoader(gUser.avatarSvg!), null); + final pictureInfo = await vg.loadPicture( + SvgStringLoader(gUser.avatarSvg!), + null, + ); final image = await pictureInfo.picture.toImage(270, 300); From 5204984f9cc2220f404ac378d29d1897a262a7f1 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 15 Mar 2026 19:59:29 +0100 Subject: [PATCH 2/3] fix issue with workmanager --- CHANGELOG.md | 2 +- .../callback_dispatcher.background.dart | 57 ++++++++++++------- lib/src/utils/exclusive_access.dart | 51 +++++++++++++++++ lib/src/utils/log.dart | 46 ++------------- pubspec.yaml | 2 +- 5 files changed, 94 insertions(+), 64 deletions(-) create mode 100644 lib/src/utils/exclusive_access.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 458a935..662859a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.0.99 +## 0.1.0 - New: Groups can now collect flames as well - New: Background execution to pre-load messages diff --git a/lib/src/services/background/callback_dispatcher.background.dart b/lib/src/services/background/callback_dispatcher.background.dart index 07297b2..15838fa 100644 --- a/lib/src/services/background/callback_dispatcher.background.dart +++ b/lib/src/services/background/callback_dispatcher.background.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:twonly/globals.dart'; @@ -6,6 +7,7 @@ import 'package:twonly/src/constants/keyvalue.keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/utils/exclusive_access.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -14,12 +16,14 @@ import 'package:workmanager/workmanager.dart'; // ignore: unreachable_from_main Future initializeBackgroundTaskManager() async { await Workmanager().initialize(callbackDispatcher); + await Workmanager().cancelByUniqueName('fetch_data_from_server'); await Workmanager().registerPeriodicTask( 'fetch_data_from_server', 'eu.twonly.periodic_task', frequency: const Duration(minutes: 20), initialDelay: const Duration(minutes: 5), + existingWorkPolicy: ExistingPeriodicWorkPolicy.update, constraints: Constraints( networkType: NetworkType.connected, ), @@ -64,26 +68,35 @@ Future initBackgroundExecution() async { return true; } -Future handlePeriodicTask() async { - final lastExecution = - await KeyValueStore.get(KeyValueKeys.lastPeriodicTaskExecution); - if (lastExecution == null || !lastExecution.containsKey('timestamp')) { - final lastExecutionTime = lastExecution?['timestamp'] as int?; - if (lastExecutionTime != null) { - final lastExecution = - DateTime.fromMillisecondsSinceEpoch(lastExecutionTime); - if (DateTime.now().difference(lastExecution).inMinutes < 2) { - Log.info( - 'eu.twonly.periodic_task not executed as last execution was within the last two minutes.', - ); - return true; - } - } - } +final Mutex _keyValueMutex = Mutex(); - await KeyValueStore.put(KeyValueKeys.lastPeriodicTaskExecution, { - 'timestamp': DateTime.now().millisecondsSinceEpoch, - }); +Future handlePeriodicTask() async { + final shouldBeExecuted = await exclusiveAccess( + lockName: 'periodic_task', + mutex: _keyValueMutex, + action: () async { + final lastExecution = await KeyValueStore.get( + KeyValueKeys.lastPeriodicTaskExecution, + ); + if (lastExecution != null && lastExecution.containsKey('timestamp')) { + final lastExecutionTime = lastExecution['timestamp'] as int?; + if (lastExecutionTime != null) { + final lastExecutionDate = DateTime.fromMillisecondsSinceEpoch( + lastExecutionTime, + ); + if (DateTime.now().difference(lastExecutionDate).inMinutes < 2) { + return false; + } + } + } + await KeyValueStore.put(KeyValueKeys.lastPeriodicTaskExecution, { + 'timestamp': DateTime.now().millisecondsSinceEpoch, + }); + return false; + }, + ); + + if (!shouldBeExecuted) return; Log.info('eu.twonly.periodic_task was called.'); @@ -91,12 +104,12 @@ Future handlePeriodicTask() async { if (!await apiService.connect()) { Log.info('Could not connect to the api. Returning early.'); - return false; + return; } if (!apiService.isAuthenticated) { Log.info('Api is not authenticated. Returning early.'); - return false; + return; } while (!globalGotMessageFromServer) { @@ -119,7 +132,7 @@ Future handlePeriodicTask() async { stopwatch.stop(); Log.info('eu.twonly.periodic_task finished after ${stopwatch.elapsed}.'); - return true; + return; } Future handleProcessingTask() async { diff --git a/lib/src/utils/exclusive_access.dart b/lib/src/utils/exclusive_access.dart new file mode 100644 index 0000000..d2a4253 --- /dev/null +++ b/lib/src/utils/exclusive_access.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:mutex/mutex.dart'; +import 'package:twonly/globals.dart'; + +Future exclusiveAccess({ + required String lockName, + required Future Function() action, + required Mutex mutex, +}) async { + final lockFile = File('$globalApplicationSupportDirectory/$lockName.lock'); + return mutex.protect(() async { + var lockAcquired = false; + + while (!lockAcquired) { + try { + lockFile.createSync(exclusive: true); + lockAcquired = true; + } on FileSystemException catch (e) { + final isExists = e is PathExistsException || e.osError?.errorCode == 17; + if (!isExists) { + break; + } + try { + final stat = lockFile.statSync(); + if (stat.type != FileSystemEntityType.notFound) { + final age = DateTime.now().difference(stat.modified).inMilliseconds; + if (age > 1000) { + lockFile.deleteSync(); + continue; + } + } + } catch (_) {} + await Future.delayed(const Duration(milliseconds: 50)); + } catch (_) { + break; + } + } + try { + return await action(); + } finally { + if (lockAcquired) { + try { + if (lockFile.existsSync()) { + lockFile.deleteSync(); + } + } catch (_) {} + } + } + }); +} diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index fcba8bb..84b5009 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -6,6 +6,7 @@ import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/exclusive_access.dart'; void initLogger() { // Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; @@ -91,46 +92,11 @@ Future readLast1000Lines() async { final Mutex _logMutex = Mutex(); Future _protectFileAccess(Future Function() action) async { - return _logMutex.protect(() async { - final lockFile = File('$globalApplicationSupportDirectory/app.log.lock'); - var lockAcquired = false; - - while (!lockAcquired) { - try { - lockFile.createSync(exclusive: true); - lockAcquired = true; - } on FileSystemException catch (e) { - final isExists = e is PathExistsException || e.osError?.errorCode == 17; - if (!isExists) { - break; - } - try { - final stat = lockFile.statSync(); - if (stat.type != FileSystemEntityType.notFound) { - final age = DateTime.now().difference(stat.modified).inMilliseconds; - if (age > 1000) { - lockFile.deleteSync(); - continue; - } - } - } catch (_) {} - await Future.delayed(const Duration(milliseconds: 50)); - } catch (_) { - break; - } - } - try { - return await action(); - } finally { - if (lockAcquired) { - try { - if (lockFile.existsSync()) { - lockFile.deleteSync(); - } - } catch (_) {} - } - } - }); + return exclusiveAccess( + lockName: 'app.log', + action: action, + mutex: _logMutex, + ); } Future _writeLogToFile(LogRecord record) async { diff --git a/pubspec.yaml b/pubspec.yaml index bddd5a9..7b07725 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.0.99+99 +version: 0.1.0+100 environment: sdk: ^3.11.0 From 18cce3ba3985d060bcfef11da2d16a71a27c16be Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 16 Mar 2026 00:03:46 +0100 Subject: [PATCH 3/3] bump version and fix issue --- CHANGELOG.md | 2 +- lib/src/database/daos/mediafiles.dao.dart | 116 +++++++++--------- .../api/mediafiles/upload.service.dart | 6 +- .../callback_dispatcher.background.dart | 10 +- pubspec.yaml | 2 +- 5 files changed, 68 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 662859a..fa4742a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.1.0 +## 0.1.1 - New: Groups can now collect flames as well - New: Background execution to pre-load messages diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 1bbcc54..7f4ff8c 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -26,8 +26,9 @@ class MediaFilesDao extends DatabaseAccessor final rowId = await into(mediaFiles).insert(insertMediaFile); - return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId))) - .getSingle(); + return await (select( + mediaFiles, + )..where((t) => t.rowId.equals(rowId))).getSingle(); } catch (e) { Log.error('Could not insert media file: $e'); return null; @@ -35,10 +36,9 @@ class MediaFilesDao extends DatabaseAccessor } Future deleteMediaFile(String mediaId) async { - await (delete(mediaFiles) - ..where( - (t) => t.mediaId.equals(mediaId), - )) + await (delete(mediaFiles)..where( + (t) => t.mediaId.equals(mediaId), + )) .go(); } @@ -46,8 +46,9 @@ class MediaFilesDao extends DatabaseAccessor String mediaId, MediaFilesCompanion updates, ) async { - await (update(mediaFiles)..where((c) => c.mediaId.equals(mediaId))) - .write(updates); + await (update( + mediaFiles, + )..where((c) => c.mediaId.equals(mediaId))).write(updates); } Future updateAllMediaFiles( @@ -57,14 +58,15 @@ class MediaFilesDao extends DatabaseAccessor } Future getMediaFileById(String mediaId) async { - return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) - .getSingleOrNull(); + return (select( + mediaFiles, + )..where((t) => t.mediaId.equals(mediaId))).getSingleOrNull(); } Future getDraftMediaFile() async { - final medias = await (select(mediaFiles) - ..where((t) => t.isDraftMedia.equals(true))) - .get(); + final medias = await (select( + mediaFiles, + )..where((t) => t.isDraftMedia.equals(true))).get(); if (medias.isEmpty) { return null; } @@ -72,65 +74,62 @@ class MediaFilesDao extends DatabaseAccessor } Stream watchMedia(String mediaId) { - return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) - .watchSingleOrNull(); + return (select( + mediaFiles, + )..where((t) => t.mediaId.equals(mediaId))).watchSingleOrNull(); } Future resetPendingDownloadState() async { - await (update(mediaFiles) - ..where( - (c) => c.downloadState.equals( - DownloadState.downloading.name, - ), - )) + await (update(mediaFiles)..where( + (c) => c.downloadState.equals( + DownloadState.downloading.name, + ), + )) .write( - const MediaFilesCompanion( - downloadState: Value(DownloadState.pending), - ), - ); + const MediaFilesCompanion( + downloadState: Value(DownloadState.pending), + ), + ); } Future> getAllMediaFilesPendingDownload() async { - return (select(mediaFiles) - ..where( - (t) => - t.downloadState.equals(DownloadState.pending.name) | - t.downloadState.equals(DownloadState.downloading.name), - )) + return (select(mediaFiles)..where( + (t) => + t.downloadState.equals(DownloadState.pending.name) | + t.downloadState.equals(DownloadState.downloading.name), + )) .get(); } Future> getAllMediaFilesReuploadRequested() async { - return (select(mediaFiles) - ..where( - (t) => t.downloadState.equals(DownloadState.reuploadRequested.name), - )) + return (select(mediaFiles)..where( + (t) => t.downloadState.equals(DownloadState.reuploadRequested.name), + )) .get(); } Future> getAllNonHashedStoredMediaFiles() async { - return (select(mediaFiles) - ..where( - (t) => t.stored.equals(true) & t.storedFileHash.isNull(), - )) + return (select(mediaFiles)..where( + (t) => t.stored.equals(true) & t.storedFileHash.isNull(), + )) .get(); } Future> getAllMediaFilesPendingUpload() async { - return (select(mediaFiles) - ..where( - (t) => (t.uploadState.equals(UploadState.initialized.name) | - t.uploadState.equals(UploadState.uploadLimitReached.name) | - t.uploadState.equals(UploadState.uploading.name) | - t.uploadState.equals(UploadState.preprocessing.name)), - )) + return (select(mediaFiles)..where( + (t) => + (t.uploadState.equals(UploadState.initialized.name) | + t.uploadState.equals(UploadState.uploadLimitReached.name) | + t.uploadState.equals(UploadState.uploading.name) | + t.uploadState.equals(UploadState.preprocessing.name)), + )) .get(); } Stream> watchAllStoredMediaFiles() { - final query = (select(mediaFiles)..where((t) => t.stored.equals(true))) - .join([]) - ..groupBy([mediaFiles.storedFileHash]); + final query = + (select(mediaFiles)..where((t) => t.stored.equals(true))).join([]) + ..groupBy([mediaFiles.storedFileHash]); return query.map((row) => row.readTable(mediaFiles)).watch(); } @@ -142,16 +141,15 @@ class MediaFilesDao extends DatabaseAccessor } Future updateAllRetransmissionUploadingState() async { - await (update(mediaFiles) - ..where( - (t) => - t.uploadState.equals(UploadState.uploading.name) & - t.reuploadRequestedBy.isNotNull(), - )) + await (update(mediaFiles)..where( + (t) => + t.uploadState.equals(UploadState.uploading.name) & + t.reuploadRequestedBy.isNotNull(), + )) .write( - const MediaFilesCompanion( - uploadState: Value(UploadState.preprocessing), - ), - ); + const MediaFilesCompanion( + uploadState: Value(UploadState.preprocessing), + ), + ); } } diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 468533c..c15ae6c 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -30,10 +30,9 @@ Future finishStartedPreprocessing() async { final mediaFiles = await twonlyDB.mediaFilesDao .getAllMediaFilesPendingUpload(); - Log.info('There are ${mediaFiles.length} media files pending'); - for (final mediaFile in mediaFiles) { if (mediaFile.isDraftMedia) { + Log.info('Ignoring media files as it is a draft'); continue; } try { @@ -51,6 +50,9 @@ Future finishStartedPreprocessing() async { await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); continue; } + Log.info( + 'Finishing started preprocessing of ${mediaFile.mediaId} in state ${mediaFile.uploadState}.', + ); await startBackgroundMediaUpload(service); } catch (e) { Log.warn(e); diff --git a/lib/src/services/background/callback_dispatcher.background.dart b/lib/src/services/background/callback_dispatcher.background.dart index 15838fa..94980df 100644 --- a/lib/src/services/background/callback_dispatcher.background.dart +++ b/lib/src/services/background/callback_dispatcher.background.dart @@ -51,16 +51,16 @@ void callbackDispatcher() { Future initBackgroundExecution() async { SentryWidgetsFlutterBinding.ensureInitialized(); + globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path; + globalApplicationSupportDirectory = + (await getApplicationSupportDirectory()).path; + initLogger(); final user = await getUser(); if (user == null) return false; gUser = user; - globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path; - globalApplicationSupportDirectory = - (await getApplicationSupportDirectory()).path; - twonlyDB = TwonlyDB(); apiService = ApiService(); globalIsInBackgroundTask = true; @@ -92,7 +92,7 @@ Future handlePeriodicTask() async { await KeyValueStore.put(KeyValueKeys.lastPeriodicTaskExecution, { 'timestamp': DateTime.now().millisecondsSinceEpoch, }); - return false; + return true; }, ); diff --git a/pubspec.yaml b/pubspec.yaml index 7b07725..db27196 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.1.0+100 +version: 0.1.1+101 environment: sdk: ^3.11.0