From f553713ff84006e087e7bded3c30b742df2cec03 Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 1 May 2026 23:37:29 +0200 Subject: [PATCH] improve startup --- CHANGELOG.md | 4 + analysis_options.yaml | 1 + lib/main.dart | 45 ++- lib/src/callbacks/logging.callbacks.dart | 1 - lib/src/database/daos/mediafiles.dao.dart | 5 + lib/src/database/daos/messages.dao.dart | 21 +- .../services/api/mediafiles/upload.api.dart | 323 +++++++++--------- .../callback_dispatcher.background.dart | 3 +- .../mediafiles/mediafile.service.dart | 56 ++- .../components/verification_badge.comp.dart | 2 - .../views/settings/help/diagnostics.view.dart | 38 ++- pubspec.yaml | 2 +- rust/src/bridge/callbacks/user_discovery.rs | 1 - 13 files changed, 294 insertions(+), 208 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f53e8a8c..2fb92fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.2.3 + +- Fix: App did not launch sometimes on Android + ## 0.2.0 - New: Feature to find friends without a phone number diff --git a/analysis_options.yaml b/analysis_options.yaml index ecfa2298..229bcedc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -18,6 +18,7 @@ analyzer: - "lib/generated/**" - "lib/core/**" - "lib/src/localization/**" + - "rust_builder/" - "dependencies/**" - "pubspec.yaml" - "**.arb" diff --git a/lib/main.dart b/lib/main.dart index 86ac2d78..dfde69c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -53,6 +53,7 @@ Future twonlyMinimumInitialization() async { } void main() async { + final stopwatch = Stopwatch()..start(); await twonlyMinimumInitialization(); unawaited(initFCMService()); @@ -67,12 +68,9 @@ void main() async { storageError = true; } - final dbExists = File( - '${AppEnvironment.supportDir}/twonly.sqlite', - ).existsSync(); - if (Platform.isIOS && userExists) { - if (!dbExists) { + final dbFile = File('${AppEnvironment.supportDir}/twonly.sqlite'); + if (!dbFile.existsSync()) { Log.error('[twonly] IOS: App was removed and then reinstalled again...'); await SecureStorage.instance.deleteAll(); userExists = false; @@ -81,7 +79,7 @@ void main() async { final settingsController = SettingsChangeProvider()..loadSettings(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - await initFileDownloader(); + unawaited(initFileDownloader()); if (userExists) { if (userService.currentUser.allowErrorTrackingViaSentry) { @@ -96,23 +94,16 @@ void main() async { } await runMigrations(); - - await twonlyDB.messagesDao.purgeMessageTable(); - await twonlyDB.receiptsDao.purgeReceivedReceipts(); - await UserDiscoveryService.removeDeletedContacts(); - - unawaited(MediaFileService.purgeTempFolder()); - - unawaited(setupPushNotification()); - unawaited(finishStartedPreprocessing()); - unawaited(createPushAvatars()); - unawaited(performTwonlySafeBackup()); - unawaited(initializeBackgroundTaskManager()); + unawaited(postStartupTasks()); } await apiService.listenToNetworkChanges(); unawaited(apiService.connect()); + stopwatch.stop(); + + Log.info('Initialization finished after ${stopwatch.elapsed}.'); + runApp( MultiProvider( providers: [ @@ -161,3 +152,21 @@ Future runMigrations() async { }); } } + +Future postStartupTasks() async { + // 1. Immediate background cleanup (Non-blocking for UI) + await twonlyDB.messagesDao.purgeMessageTable(); + unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts()); + unawaited(UserDiscoveryService.removeDeletedContacts()); + unawaited(MediaFileService.purgeTempFolder()); + + // 2. Service initializations + unawaited(setupPushNotification()); + unawaited(finishStartedPreprocessing()); + unawaited(createPushAvatars()); + unawaited(initializeBackgroundTaskManager()); + + // 3. Delayed tasks (Wait for app to settle) + await Future.delayed(const Duration(minutes: 2)); + unawaited(performTwonlySafeBackup()); +} diff --git a/lib/src/callbacks/logging.callbacks.dart b/lib/src/callbacks/logging.callbacks.dart index dff9b1ad..bba3d194 100644 --- a/lib/src/callbacks/logging.callbacks.dart +++ b/lib/src/callbacks/logging.callbacks.dart @@ -19,7 +19,6 @@ class LoggingCallbacks { print(log); } }, - onDone: () => Log.info('Log stream closed'), ); timer.cancel(); } catch (e) { diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 5ff7cb67..3afade5f 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -65,6 +65,11 @@ class MediaFilesDao extends DatabaseAccessor )..where((t) => t.mediaId.equals(mediaId))).getSingleOrNull(); } + Future> getMediaFilesByIds(List mediaIds) async { + return (select(mediaFiles)..where((t) => t.mediaId.isIn(mediaIds))).get(); + } + + Future getDraftMediaFile() async { final medias = await (select( mediaFiles, diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 29edbc03..ace070ea 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -140,15 +140,22 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { Future purgeMessageTable() async { final allGroups = await select(groups).get(); - for (final group in allGroups) { + final groupedByTime = >{}; + for (final g in allGroups) { + groupedByTime + .putIfAbsent(g.deleteMessagesAfterMilliseconds, () => []) + .add(g.groupId); + } + + for (final entry in groupedByTime.entries) { final deletionTime = clock.now().subtract( - Duration( - milliseconds: group.deleteMessagesAfterMilliseconds, - ), + Duration(milliseconds: entry.key), ); + final groupIds = entry.value; + await (delete(messages)..where( (m) => - m.groupId.equals(group.groupId) & + m.groupId.isIn(groupIds) & (m.mediaStored.equals(true) & m.isDeletedFromSender.equals(true) | m.mediaStored.equals(false)) & @@ -404,6 +411,10 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get(); } + Future> getMessagesByMediaIds(List mediaIds) async { + return (select(messages)..where((t) => t.mediaId.isIn(mediaIds))).get(); + } + Stream> watchMessageActions(String messageId) { final query = (select(messageActions).join([ leftOuterJoin( diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index 7689365b..e84a77dc 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -23,6 +23,7 @@ import 'package:twonly/src/services/api/mediafiles/media_background.api.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/secure_storage.dart'; @@ -31,124 +32,128 @@ import 'package:workmanager/workmanager.dart' hide TaskStatus; final lockRetransmission = Mutex(); Future reuploadMediaFiles() async { - return lockRetransmission.protect(() async { - final receipts = await twonlyDB.receiptsDao - .getReceiptsForMediaRetransmissions(); + return exclusiveAccess( + lockName: 'reupload_maintenance', + mutex: lockRetransmission, + action: () async { + final receipts = await twonlyDB.receiptsDao + .getReceiptsForMediaRetransmissions(); - if (receipts.isEmpty) return; + if (receipts.isEmpty) return; - Log.info('Reuploading ${receipts.length} media files to the server.'); + Log.info('Reuploading ${receipts.length} media files to the server.'); - final contacts = {}; + final contacts = {}; - for (final receipt in receipts) { - if (receipt.retryCount > 1 && receipt.lastRetry != null) { - final twentyFourHoursAgo = DateTime.now().subtract( - const Duration(hours: 24), - ); - if (receipt.lastRetry!.isAfter(twentyFourHoursAgo)) { - Log.info( - 'Ignoring ${receipt.receiptId} as it was retried in the last 24h', + for (final receipt in receipts) { + if (receipt.retryCount > 1 && receipt.lastRetry != null) { + final twentyFourHoursAgo = DateTime.now().subtract( + const Duration(hours: 24), ); - continue; - } - } - var messageId = receipt.messageId; - if (receipt.messageId == null) { - Log.info('Message not in receipt. Loading it from the content.'); - try { - final content = EncryptedContent.fromBuffer(receipt.message); - if (content.hasMedia()) { - messageId = content.media.senderMessageId; - final messageExists = await twonlyDB.messagesDao - .getMessageById(messageId) - .getSingleOrNull(); - if (messageExists != null) { - await twonlyDB.receiptsDao.updateReceipt( - receipt.receiptId, - ReceiptsCompanion( - messageId: Value(messageId), - ), - ); - } else { - Log.info( - 'Message $messageId not found in DB for receipt recovery. Deleting stale receipt.', - ); - await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); - continue; - } - } - } catch (e) { - Log.error(e); - } - } - if (messageId == null) { - Log.error('MessageId is empty for media file receipts'); - continue; - } - if (receipt.markForRetryAfterAccepted != null) { - if (!contacts.containsKey(receipt.contactId)) { - final contact = await twonlyDB.contactsDao - .getContactByUserId(receipt.contactId) - .getSingleOrNull(); - if (contact == null) { - Log.error( - 'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.', + if (receipt.lastRetry!.isAfter(twentyFourHoursAgo)) { + Log.info( + 'Ignoring ${receipt.receiptId} as it was retried in the last 24h', ); continue; } - contacts[receipt.contactId] = contact; } - if (!(contacts[receipt.contactId]?.accepted ?? true)) { - Log.warn( - 'Could not send message as contact has still not yet accepted.', - ); - continue; - } - } - - if (receipt.ackByServerAt == null) { - // media file must be reuploaded again in case the media files - // was deleted by the server, the receiver will request a new media reupload - - final message = await twonlyDB.messagesDao - .getMessageById(messageId) - .getSingleOrNull(); - if (message == null || message.mediaId == null) { - // The message or media file does not exists any more, so delete the receipt... - if (message != null) { - // The media file of the message does not exist anymore. Removing it... - await twonlyDB.messagesDao.deleteMessagesById(messageId); + var messageId = receipt.messageId; + if (receipt.messageId == null) { + Log.info('Message not in receipt. Loading it from the content.'); + try { + final content = EncryptedContent.fromBuffer(receipt.message); + if (content.hasMedia()) { + messageId = content.media.senderMessageId; + final messageExists = await twonlyDB.messagesDao + .getMessageById(messageId) + .getSingleOrNull(); + if (messageExists != null) { + await twonlyDB.receiptsDao.updateReceipt( + receipt.receiptId, + ReceiptsCompanion( + messageId: Value(messageId), + ), + ); + } else { + Log.info( + 'Message $messageId not found in DB for receipt recovery. Deleting stale receipt.', + ); + await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); + continue; + } + } + } catch (e) { + Log.error(e); } - await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); - Log.warn( - 'Message not found for reupload of the receipt, likely deleted from sender (${message == null} - ${message?.mediaId}).', - ); + } + if (messageId == null) { + Log.error('MessageId is empty for media file receipts'); continue; } + if (receipt.markForRetryAfterAccepted != null) { + if (!contacts.containsKey(receipt.contactId)) { + final contact = await twonlyDB.contactsDao + .getContactByUserId(receipt.contactId) + .getSingleOrNull(); + if (contact == null) { + Log.error( + 'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.', + ); + continue; + } + contacts[receipt.contactId] = contact; + } + if (!(contacts[receipt.contactId]?.accepted ?? true)) { + Log.warn( + 'Could not send message as contact has still not yet accepted.', + ); + continue; + } + } - final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( - message.mediaId!, - ); - if (mediaFile == null) { - Log.error( - 'Mediafile not found for reupload of the receipt (${message.messageId} - ${message.mediaId}).', + if (receipt.ackByServerAt == null) { + // media file must be reuploaded again in case the media files + // was deleted by the server, the receiver will request a new media reupload + + final message = await twonlyDB.messagesDao + .getMessageById(messageId) + .getSingleOrNull(); + if (message == null || message.mediaId == null) { + // The message or media file does not exists any more, so delete the receipt... + if (message != null) { + // The media file of the message does not exist anymore. Removing it... + await twonlyDB.messagesDao.deleteMessagesById(messageId); + } + await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); + Log.warn( + 'Message not found for reupload of the receipt, likely deleted from sender (${message == null} - ${message?.mediaId}).', + ); + continue; + } + + final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( + message.mediaId!, ); - continue; + if (mediaFile == null) { + Log.error( + 'Mediafile not found for reupload of the receipt (${message.messageId} - ${message.mediaId}).', + ); + continue; + } + await reuploadMediaFile( + receipt.contactId, + mediaFile, + message.messageId, + ); + } else { + Log.info('Reuploading media file $messageId'); + // the media file should be still on the server, so it should be enough + // to just resend the message containing the download token. + await tryToSendCompleteMessage(receipt: receipt); } - await reuploadMediaFile( - receipt.contactId, - mediaFile, - message.messageId, - ); - } else { - Log.info('Reuploading media file $messageId'); - // the media file should be still on the server, so it should be enough - // to just resend the message containing the download token. - await tryToSendCompleteMessage(receipt: receipt); } - } - }); + }, + ); } Future reuploadMediaFile( @@ -187,69 +192,77 @@ Future reuploadMediaFile( } } +final Mutex _lockPreprocessing = Mutex(); + Future finishStartedPreprocessing() async { - final mediaFiles = await twonlyDB.mediaFilesDao - .getAllMediaFilesPendingUpload(); + return exclusiveAccess( + lockName: 'preprocessing_maintenance', + mutex: _lockPreprocessing, + action: () async { + final mediaFiles = await twonlyDB.mediaFilesDao + .getAllMediaFilesPendingUpload(); - for (final mediaFile in mediaFiles) { - if (mediaFile.isDraftMedia) { - Log.info('Ignoring media files as it is a draft'); - continue; - } - try { - final service = MediaFileService(mediaFile); - if (!service.originalPath.existsSync() && - !service.uploadRequestPath.existsSync()) { - if (service.storedPath.existsSync()) { - // media files was just stored.. - continue; - } - if (mediaFile.reuploadRequestedBy != null) { - Log.warn( - 'Reupload requested for ${mediaFile.mediaId} but files are missing. Cancelling reupload but keeping record.', - ); - await twonlyDB.mediaFilesDao.updateMedia( - mediaFile.mediaId, - const MediaFilesCompanion( - uploadState: Value(UploadState.uploaded), - reuploadRequestedBy: Value(null), - ), - ); + for (final mediaFile in mediaFiles) { + if (mediaFile.isDraftMedia) { + Log.info('Ignoring media files as it is a draft'); continue; } + try { + final service = MediaFileService(mediaFile); + if (!service.originalPath.existsSync() && + !service.uploadRequestPath.existsSync()) { + if (service.storedPath.existsSync()) { + // media files was just stored.. + continue; + } + if (mediaFile.reuploadRequestedBy != null) { + Log.warn( + 'Reupload requested for ${mediaFile.mediaId} but files are missing. Cancelling reupload but keeping record.', + ); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + uploadState: Value(UploadState.uploaded), + reuploadRequestedBy: Value(null), + ), + ); + continue; + } - final messages = await twonlyDB.messagesDao.getMessagesByMediaId( - mediaFile.mediaId, - ); + final messages = await twonlyDB.messagesDao.getMessagesByMediaId( + mediaFile.mediaId, + ); - if (messages.isEmpty) { + if (messages.isEmpty) { + Log.info( + 'Deleted media files ${mediaFile.mediaId} as originalPath and uploadRequestPath both do not exists and no messages reference it.', + ); + // the file does not exists anymore and no messages reference it. + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); + } else { + Log.warn( + 'Media files ${mediaFile.mediaId} missing but messages still reference it. Keeping record to avoid broken chat history.', + ); + // Just mark as uploaded to stop preprocessing attempts + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + uploadState: Value(UploadState.uploaded), + ), + ); + } + continue; + } Log.info( - 'Deleted media files ${mediaFile.mediaId} as originalPath and uploadRequestPath both do not exists and no messages reference it.', - ); - // the file does not exists anymore and no messages reference it. - await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); - } else { - Log.warn( - 'Media files ${mediaFile.mediaId} missing but messages still reference it. Keeping record to avoid broken chat history.', - ); - // Just mark as uploaded to stop preprocessing attempts - await twonlyDB.mediaFilesDao.updateMedia( - mediaFile.mediaId, - const MediaFilesCompanion( - uploadState: Value(UploadState.uploaded), - ), + 'Finishing started preprocessing of ${mediaFile.mediaId} in state ${mediaFile.uploadState}.', ); + await startBackgroundMediaUpload(service); + } catch (e) { + Log.warn(e); } - continue; } - Log.info( - 'Finishing started preprocessing of ${mediaFile.mediaId} in state ${mediaFile.uploadState}.', - ); - await startBackgroundMediaUpload(service); - } catch (e) { - Log.warn(e); - } - } + }, + ); } /// It can happen, that a media files is uploaded but not yet marked for been uploaded. diff --git a/lib/src/services/background/callback_dispatcher.background.dart b/lib/src/services/background/callback_dispatcher.background.dart index 249a10fe..7d2260a4 100644 --- a/lib/src/services/background/callback_dispatcher.background.dart +++ b/lib/src/services/background/callback_dispatcher.background.dart @@ -31,6 +31,7 @@ Future initializeBackgroundTaskManager() async { @pragma('vm:entry-point') void callbackDispatcher() { Workmanager().executeTask((task, inputData) async { + AppState.isInBackgroundTask = true; switch (task) { case 'eu.twonly.periodic_task': if (await initBackgroundExecution()) { @@ -63,8 +64,6 @@ Future initBackgroundExecution() async { return false; } - AppState.isInBackgroundTask = true; - _isInitialized = true; return true; } diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 7f08c4f4..ec914ade 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -33,39 +33,58 @@ class MediaFileService { ); final files = tempDirectory.listSync(); + if (files.isEmpty) return; + + final mediaIdToFile = >{}; for (final file in files) { final mediaId = basename(file.path).split('.').first; + mediaIdToFile.putIfAbsent(mediaId, () => []).add(file); + } + final mediaIds = mediaIdToFile.keys.toList(); + + // Bulk fetch media files and messages + final allMediaFiles = await twonlyDB.mediaFilesDao.getMediaFilesByIds( + mediaIds, + ); + final allMessages = await twonlyDB.messagesDao.getMessagesByMediaIds( + mediaIds, + ); + + final mediaFileMap = {for (final m in allMediaFiles) m.mediaId: m}; + final messageMap = >{}; + for (final msg in allMessages) { + if (msg.mediaId != null) { + messageMap.putIfAbsent(msg.mediaId!, () => []).add(msg); + } + } + + for (final mediaId in mediaIds) { // in case the mediaID is unknown the file will be deleted var delete = true; - final service = await MediaFileService.fromMediaId(mediaId); + final mediaFile = mediaFileMap[mediaId]; - if (service != null) { - if (service.mediaFile.isDraftMedia) { + if (mediaFile != null) { + if (mediaFile.isDraftMedia) { delete = false; } - final messages = await twonlyDB.messagesDao.getMessagesByMediaId( - mediaId, - ); + final messages = messageMap[mediaId] ?? []; // in case messages in empty the file will be deleted, as delete is true by default for (final message in messages) { - if (service.mediaFile.type == MediaType.audio) { + if (mediaFile.type == MediaType.audio) { delete = false; // do not delete voice messages } if (message.openedAt == null) { // Message was not yet opened from all persons, so wait... delete = false; - } else if (service.mediaFile.requiresAuthentication || - service.mediaFile.displayLimitInMilliseconds != null) { + } else if (mediaFile.requiresAuthentication || + mediaFile.displayLimitInMilliseconds != null) { // Message was opened by all persons, and they can not reopen the image. - // This branch will prevent to reach the next if condition, with would otherwise store the image for two days - // delete = true; // do not overwrite a previous delete = false - // this is just to make it easier to understand :) } else if (message.openedAt!.isAfter( clock.now().subtract(const Duration(days: 2)), )) { @@ -89,11 +108,20 @@ class MediaFileService { if (delete) { Log.info('Purging media file $mediaId'); - file.deleteSync(); + final filesToPurge = mediaIdToFile[mediaId] ?? []; + for (final file in filesToPurge) { + try { + if (file.existsSync()) { + file.deleteSync(); + } + } catch (e) { + Log.error('Error deleting file ${file.path}: $e'); + } + } } } } catch (e) { - Log.error(e); + Log.error('Error in purgeTempFolder: $e'); } } diff --git a/lib/src/visual/components/verification_badge.comp.dart b/lib/src/visual/components/verification_badge.comp.dart index 8a7bc422..ec655b44 100644 --- a/lib/src/visual/components/verification_badge.comp.dart +++ b/lib/src/visual/components/verification_badge.comp.dart @@ -6,7 +6,6 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/key_verification.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/visual/components/verification_badge_info.comp.dart'; import 'package:twonly/src/visual/elements/svg_icon.element.dart'; @@ -65,7 +64,6 @@ class _VerificationBadgeCompState extends State { .listen((update) { if (!mounted) return; setState(() { - Log.info('Update: ${update.length}'); _isVerified = update.isNotEmpty; }); }); diff --git a/lib/src/visual/views/settings/help/diagnostics.view.dart b/lib/src/visual/views/settings/help/diagnostics.view.dart index 75550424..0af711d7 100644 --- a/lib/src/visual/views/settings/help/diagnostics.view.dart +++ b/lib/src/visual/views/settings/help/diagnostics.view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/utils/log.dart'; @@ -157,25 +158,39 @@ class _LogViewerWidgetState extends State { final tsStyle = TextStyle( color: isDarkMode(context) ? Colors.white : Colors.black, fontFamily: 'monospace', + fontSize: 12, ); - final levelStyle = TextStyle( - color: Colors.blueGrey.shade600, + final fileNameStyle = TextStyle( + color: Colors.blueGrey.shade400, fontWeight: FontWeight.bold, fontFamily: 'monospace', + fontSize: 11, ); final msgStyle = TextStyle( color: isDarkMode(context) ? Colors.white : Colors.black, fontFamily: 'monospace', + fontSize: 13, ); return TextSpan( children: [ if (_showTimestamps && e.timestamp != null) TextSpan( - text: '${e.timestamp} '.replaceAll('.000', ''), + text: '${e.timestamp.toString().split(' ')[1].split('.')[0]} ', style: tsStyle, ), - TextSpan(text: '${e.fileName}\n', style: levelStyle), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: FaIcon( + e.isBackground ? FontAwesomeIcons.clock : FontAwesomeIcons.mobile, + size: 12, + color: Colors.grey, + ), + ), + ), + TextSpan(text: '${e.fileName}\n', style: fileNameStyle), TextSpan(text: e.message, style: msgStyle), ], ); @@ -274,17 +289,18 @@ class _LogViewerWidgetState extends State { class _LogEntry { _LogEntry({ + required this.timestamp, + required this.level, required this.message, required this.line, required this.fileName, - this.timestamp, - this.level, + required this.isBackground, }); // Minimal parser based on the sample log format factory _LogEntry.parse(String raw) { // Example line: - // 2025-12-25 23:36:52 WARNING [twonly] api.service.dart:189) > websocket error: ... + // 2025-12-25 23:36:52 WARNING [f] [twonly] api.service.dart:189) > websocket error: ... final trimmed = raw.trim(); DateTime? ts; String? level; @@ -318,6 +334,8 @@ class _LogEntry { } } + final isBackground = msg.contains('[b] '); + msg = msg .trim() .replaceAll('[twonly] ', '') @@ -326,8 +344,7 @@ class _LogEntry { final fileNameS = msg.split(' > '); final fileName = fileNameS[0]; - - msg = fileNameS.sublist(1).join(); + msg = fileNameS.sublist(1).join(' > '); return _LogEntry( timestamp: ts, @@ -335,11 +352,14 @@ class _LogEntry { message: msg, line: raw, fileName: fileName, + isBackground: isBackground, ); } + final DateTime? timestamp; final String? level; final String message; final String line; final String fileName; + final bool isBackground; } diff --git a/pubspec.yaml b/pubspec.yaml index 7b75d1a6..80297902 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.2+111 +version: 0.2.3+112 environment: sdk: ^3.11.0 diff --git a/rust/src/bridge/callbacks/user_discovery.rs b/rust/src/bridge/callbacks/user_discovery.rs index 68fd03cf..5d57b4bc 100644 --- a/rust/src/bridge/callbacks/user_discovery.rs +++ b/rust/src/bridge/callbacks/user_discovery.rs @@ -54,7 +54,6 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter { return Err(UserDiscoveryError::NotInitialized); } - tracing::debug!("Loading Config from {}", config_path.display()); Ok(std::fs::read_to_string(&config_path)?) }