From f5cbcf154b09a9b08fd2ebfd34453e9fa04f66d1 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 22 Oct 2025 00:01:55 +0200 Subject: [PATCH] fixing issues with media_download --- lib/main.dart | 4 +- lib/src/database/daos/mediafiles.dao.dart | 6 + lib/src/database/daos/messages.dao.dart | 6 +- lib/src/database/tables/mediafiles.table.dart | 11 +- lib/src/database/twonly.db.g.dart | 97 ++--- lib/src/services/api/media_download.dart | 403 +++++------------- .../media.server_messages.dart | 19 +- lib/src/services/mediafile.service.dart | 123 +++++- lib/src/services/thumbnail.service.dart | 52 +-- .../create_backup.twonly_safe.dart | 13 +- .../twonly_safe/restore.twonly_safe.dart | 41 +- .../views/camera/share_image_editor_view.dart | 4 + 12 files changed, 332 insertions(+), 447 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1d1ba15..8751c01 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -45,6 +45,8 @@ void main() async { apiService = ApiService(); twonlyDB = TwonlyDB(); + await initFileDownloader(); + // await twonlyDB.messagesDao.resetPendingDownloadState(); // await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days(); // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); @@ -56,8 +58,6 @@ void main() async { // unawaited(performTwonlySafeBackup()); - await initFileDownloader(); - runApp( MultiProvider( providers: [ diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 057f59e..a6832f7 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -51,4 +51,10 @@ class MediaFilesDao extends DatabaseAccessor ), ); } + + Future> getAllMediaFilesPendingDownload() async { + return (select(mediaFiles) + ..where((t) => t.downloadState.equals(DownloadState.pending.name))) + .get(); + } } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 963ef02..ed29987 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -177,7 +177,11 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { if (msg.mediaId != null) { await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!))) .go(); - await removeMediaFile(msg.mediaId!); + + final mediaService = await MediaFileService.fromMediaId(msg.mediaId!); + if (mediaService != null) { + mediaService.fullMediaRemoval(); + } } await (delete(messageHistories) ..where((t) => t.messageId.equals(messageId))) diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 34990a4..8b44793 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -16,7 +16,13 @@ enum UploadState { receiverNotified, } -enum DownloadState { pending, downloading, reuploadRequested } +enum DownloadState { + pending, + downloading, + downloaded, + ready, + reuploadRequested +} @DataClassName('MediaFile') class MediaFiles extends Table { @@ -31,8 +37,7 @@ class MediaFiles extends Table { BoolColumn get reopenByContact => boolean().withDefault(const Constant(false))(); - BoolColumn get storedByContact => - boolean().withDefault(const Constant(false))(); + BoolColumn get stored => boolean().withDefault(const Constant(false))(); TextColumn get reuploadRequestedBy => text().map(IntListTypeConverter()).nullable()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 55b6fe6..d69d580 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1473,15 +1473,14 @@ class $MediaFilesTable extends MediaFiles defaultConstraints: GeneratedColumn.constraintIsAlways( 'CHECK ("reopen_by_contact" IN (0, 1))'), defaultValue: const Constant(false)); - static const VerificationMeta _storedByContactMeta = - const VerificationMeta('storedByContact'); + static const VerificationMeta _storedMeta = const VerificationMeta('stored'); @override - late final GeneratedColumn storedByContact = GeneratedColumn( - 'stored_by_contact', aliasedName, false, + late final GeneratedColumn stored = GeneratedColumn( + 'stored', aliasedName, false, type: DriftSqlType.bool, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("stored_by_contact" IN (0, 1))'), + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("stored" IN (0, 1))'), defaultValue: const Constant(false)); @override late final GeneratedColumnWithTypeConverter?, String> @@ -1536,7 +1535,7 @@ class $MediaFilesTable extends MediaFiles downloadState, requiresAuthentication, reopenByContact, - storedByContact, + stored, reuploadRequestedBy, displayLimitInMilliseconds, downloadToken, @@ -1573,11 +1572,9 @@ class $MediaFilesTable extends MediaFiles reopenByContact.isAcceptableOrUnknown( data['reopen_by_contact']!, _reopenByContactMeta)); } - if (data.containsKey('stored_by_contact')) { - context.handle( - _storedByContactMeta, - storedByContact.isAcceptableOrUnknown( - data['stored_by_contact']!, _storedByContactMeta)); + if (data.containsKey('stored')) { + context.handle(_storedMeta, + stored.isAcceptableOrUnknown(data['stored']!, _storedMeta)); } if (data.containsKey('display_limit_in_milliseconds')) { context.handle( @@ -1638,8 +1635,8 @@ class $MediaFilesTable extends MediaFiles data['${effectivePrefix}requires_authentication'])!, reopenByContact: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!, - storedByContact: attachedDatabase.typeMapping.read( - DriftSqlType.bool, data['${effectivePrefix}stored_by_contact'])!, + stored: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}stored'])!, reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}reupload_requested_by'])), @@ -1690,7 +1687,7 @@ class MediaFile extends DataClass implements Insertable { final DownloadState? downloadState; final bool requiresAuthentication; final bool reopenByContact; - final bool storedByContact; + final bool stored; final List? reuploadRequestedBy; final int? displayLimitInMilliseconds; final Uint8List? downloadToken; @@ -1705,7 +1702,7 @@ class MediaFile extends DataClass implements Insertable { this.downloadState, required this.requiresAuthentication, required this.reopenByContact, - required this.storedByContact, + required this.stored, this.reuploadRequestedBy, this.displayLimitInMilliseconds, this.downloadToken, @@ -1731,7 +1728,7 @@ class MediaFile extends DataClass implements Insertable { } map['requires_authentication'] = Variable(requiresAuthentication); map['reopen_by_contact'] = Variable(reopenByContact); - map['stored_by_contact'] = Variable(storedByContact); + map['stored'] = Variable(stored); if (!nullToAbsent || reuploadRequestedBy != null) { map['reupload_requested_by'] = Variable($MediaFilesTable .$converterreuploadRequestedByn @@ -1769,7 +1766,7 @@ class MediaFile extends DataClass implements Insertable { : Value(downloadState), requiresAuthentication: Value(requiresAuthentication), reopenByContact: Value(reopenByContact), - storedByContact: Value(storedByContact), + stored: Value(stored), reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent ? const Value.absent() : Value(reuploadRequestedBy), @@ -1807,7 +1804,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication: serializer.fromJson(json['requiresAuthentication']), reopenByContact: serializer.fromJson(json['reopenByContact']), - storedByContact: serializer.fromJson(json['storedByContact']), + stored: serializer.fromJson(json['stored']), reuploadRequestedBy: serializer.fromJson?>(json['reuploadRequestedBy']), displayLimitInMilliseconds: @@ -1832,7 +1829,7 @@ class MediaFile extends DataClass implements Insertable { $MediaFilesTable.$converterdownloadStaten.toJson(downloadState)), 'requiresAuthentication': serializer.toJson(requiresAuthentication), 'reopenByContact': serializer.toJson(reopenByContact), - 'storedByContact': serializer.toJson(storedByContact), + 'stored': serializer.toJson(stored), 'reuploadRequestedBy': serializer.toJson?>(reuploadRequestedBy), 'displayLimitInMilliseconds': serializer.toJson(displayLimitInMilliseconds), @@ -1851,7 +1848,7 @@ class MediaFile extends DataClass implements Insertable { Value downloadState = const Value.absent(), bool? requiresAuthentication, bool? reopenByContact, - bool? storedByContact, + bool? stored, Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value downloadToken = const Value.absent(), @@ -1868,7 +1865,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication: requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, - storedByContact: storedByContact ?? this.storedByContact, + stored: stored ?? this.stored, reuploadRequestedBy: reuploadRequestedBy.present ? reuploadRequestedBy.value : this.reuploadRequestedBy, @@ -1901,9 +1898,7 @@ class MediaFile extends DataClass implements Insertable { reopenByContact: data.reopenByContact.present ? data.reopenByContact.value : this.reopenByContact, - storedByContact: data.storedByContact.present - ? data.storedByContact.value - : this.storedByContact, + stored: data.stored.present ? data.stored.value : this.stored, reuploadRequestedBy: data.reuploadRequestedBy.present ? data.reuploadRequestedBy.value : this.reuploadRequestedBy, @@ -1935,7 +1930,7 @@ class MediaFile extends DataClass implements Insertable { ..write('downloadState: $downloadState, ') ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') - ..write('storedByContact: $storedByContact, ') + ..write('stored: $stored, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('downloadToken: $downloadToken, ') @@ -1955,7 +1950,7 @@ class MediaFile extends DataClass implements Insertable { downloadState, requiresAuthentication, reopenByContact, - storedByContact, + stored, reuploadRequestedBy, displayLimitInMilliseconds, $driftBlobEquality.hash(downloadToken), @@ -1973,7 +1968,7 @@ class MediaFile extends DataClass implements Insertable { other.downloadState == this.downloadState && other.requiresAuthentication == this.requiresAuthentication && other.reopenByContact == this.reopenByContact && - other.storedByContact == this.storedByContact && + other.stored == this.stored && other.reuploadRequestedBy == this.reuploadRequestedBy && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && @@ -1991,7 +1986,7 @@ class MediaFilesCompanion extends UpdateCompanion { final Value downloadState; final Value requiresAuthentication; final Value reopenByContact; - final Value storedByContact; + final Value stored; final Value?> reuploadRequestedBy; final Value displayLimitInMilliseconds; final Value downloadToken; @@ -2007,7 +2002,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.downloadState = const Value.absent(), this.requiresAuthentication = const Value.absent(), this.reopenByContact = const Value.absent(), - this.storedByContact = const Value.absent(), + this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.downloadToken = const Value.absent(), @@ -2024,7 +2019,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.downloadState = const Value.absent(), required bool requiresAuthentication, this.reopenByContact = const Value.absent(), - this.storedByContact = const Value.absent(), + this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.downloadToken = const Value.absent(), @@ -2042,7 +2037,7 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? downloadState, Expression? requiresAuthentication, Expression? reopenByContact, - Expression? storedByContact, + Expression? stored, Expression? reuploadRequestedBy, Expression? displayLimitInMilliseconds, Expression? downloadToken, @@ -2060,7 +2055,7 @@ class MediaFilesCompanion extends UpdateCompanion { if (requiresAuthentication != null) 'requires_authentication': requiresAuthentication, if (reopenByContact != null) 'reopen_by_contact': reopenByContact, - if (storedByContact != null) 'stored_by_contact': storedByContact, + if (stored != null) 'stored': stored, if (reuploadRequestedBy != null) 'reupload_requested_by': reuploadRequestedBy, if (displayLimitInMilliseconds != null) @@ -2081,7 +2076,7 @@ class MediaFilesCompanion extends UpdateCompanion { Value? downloadState, Value? requiresAuthentication, Value? reopenByContact, - Value? storedByContact, + Value? stored, Value?>? reuploadRequestedBy, Value? displayLimitInMilliseconds, Value? downloadToken, @@ -2098,7 +2093,7 @@ class MediaFilesCompanion extends UpdateCompanion { requiresAuthentication: requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, - storedByContact: storedByContact ?? this.storedByContact, + stored: stored ?? this.stored, reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, @@ -2136,8 +2131,8 @@ class MediaFilesCompanion extends UpdateCompanion { if (reopenByContact.present) { map['reopen_by_contact'] = Variable(reopenByContact.value); } - if (storedByContact.present) { - map['stored_by_contact'] = Variable(storedByContact.value); + if (stored.present) { + map['stored'] = Variable(stored.value); } if (reuploadRequestedBy.present) { map['reupload_requested_by'] = Variable($MediaFilesTable @@ -2178,7 +2173,7 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('downloadState: $downloadState, ') ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') - ..write('storedByContact: $storedByContact, ') + ..write('stored: $stored, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('downloadToken: $downloadToken, ') @@ -7107,7 +7102,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ Value downloadState, required bool requiresAuthentication, Value reopenByContact, - Value storedByContact, + Value stored, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value downloadToken, @@ -7124,7 +7119,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ Value downloadState, Value requiresAuthentication, Value reopenByContact, - Value storedByContact, + Value stored, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value downloadToken, @@ -7190,9 +7185,8 @@ class $$MediaFilesTableFilterComposer column: $table.reopenByContact, builder: (column) => ColumnFilters(column)); - ColumnFilters get storedByContact => $composableBuilder( - column: $table.storedByContact, - builder: (column) => ColumnFilters(column)); + ColumnFilters get stored => $composableBuilder( + column: $table.stored, builder: (column) => ColumnFilters(column)); ColumnWithTypeConverterFilters?, List, String> get reuploadRequestedBy => $composableBuilder( @@ -7271,9 +7265,8 @@ class $$MediaFilesTableOrderingComposer column: $table.reopenByContact, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get storedByContact => $composableBuilder( - column: $table.storedByContact, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get stored => $composableBuilder( + column: $table.stored, builder: (column) => ColumnOrderings(column)); ColumnOrderings get reuploadRequestedBy => $composableBuilder( column: $table.reuploadRequestedBy, @@ -7332,8 +7325,8 @@ class $$MediaFilesTableAnnotationComposer GeneratedColumn get reopenByContact => $composableBuilder( column: $table.reopenByContact, builder: (column) => column); - GeneratedColumn get storedByContact => $composableBuilder( - column: $table.storedByContact, builder: (column) => column); + GeneratedColumn get stored => + $composableBuilder(column: $table.stored, builder: (column) => column); GeneratedColumnWithTypeConverter?, String> get reuploadRequestedBy => $composableBuilder( @@ -7408,7 +7401,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value downloadState = const Value.absent(), Value requiresAuthentication = const Value.absent(), Value reopenByContact = const Value.absent(), - Value storedByContact = const Value.absent(), + Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value downloadToken = const Value.absent(), @@ -7425,7 +7418,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< downloadState: downloadState, requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, - storedByContact: storedByContact, + stored: stored, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, downloadToken: downloadToken, @@ -7442,7 +7435,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value downloadState = const Value.absent(), required bool requiresAuthentication, Value reopenByContact = const Value.absent(), - Value storedByContact = const Value.absent(), + Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value downloadToken = const Value.absent(), @@ -7459,7 +7452,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< downloadState: downloadState, requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, - storedByContact: storedByContact, + stored: stored, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, downloadToken: downloadToken, diff --git a/lib/src/services/api/media_download.dart b/lib/src/services/api/media_download.dart index 61887ed..67bc7f5 100644 --- a/lib/src/services/api/media_download.dart +++ b/lib/src/services/api/media_download.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; - import 'package:background_downloader/background_downloader.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; @@ -10,24 +8,23 @@ import 'package:drift/drift.dart'; import 'package:http/http.dart' as http; import 'package:mutex/mutex.dart'; import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/message_old.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/services/api/media_upload.dart'; -import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; Future tryDownloadAllMediaFiles({bool force = false}) async { // This is called when WebSocket is newly connected, so allow all downloads to be restarted. - final messages = - await twonlyDB.messagesDao.getAllMessagesPendingDownloading(); + final mediaFiles = + await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload(); - for (final message in messages) { - await startDownloadMedia(message, force); + for (final mediaFile in mediaFiles) { + await startDownloadMedia(mediaFile, force); } } @@ -44,7 +41,7 @@ Map> defaultAutoDownloadOptions = { ], }; -Future isAllowedToDownload(bool isVideo) async { +Future isAllowedToDownload({required bool isVideo}) async { final connectivityResult = await Connectivity().checkConnectivity(); final user = await getUser(); @@ -76,7 +73,7 @@ Future isAllowedToDownload(bool isVideo) async { } Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { - final messageId = int.parse(update.task.taskId.replaceAll('download_', '')); + final mediaId = update.task.taskId.replaceAll('download_', ''); var failed = false; if (update.status == TaskStatus.failed || @@ -93,83 +90,45 @@ Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { ); } } else { - Log.info('Got ${update.status} for $messageId'); + Log.info('Got ${update.status} for $mediaId'); return; } - await handleDownloadStatusUpdateInternal(messageId, failed); -} -Future handleDownloadStatusUpdateInternal( - int messageId, - bool failed, -) async { if (failed) { - Log.error('Download failed for $messageId'); - final message = await twonlyDB.messagesDao - .getMessageByMessageId(messageId) - .getSingleOrNull(); - if (message != null && message.downloadState != DownloadState.downloaded) { - await handleMediaError(message); - } + await requestMediaReupload(mediaId); } else { - Log.info('Download was successfully for $messageId'); - await handleEncryptedFile(messageId); + await handleEncryptedFile(mediaId); } } Mutex protectDownload = Mutex(); Future startDownloadMedia(MediaFile media, bool force) async { - Log.info( - 'Download blocked for ${message.messageId} because of network state.', - ); - if (message.contentJson == null) { - Log.error('Content of ${message.messageId} not found.'); - await handleMediaError(message); + final mediaService = await MediaFileService.fromMedia(media); + + if (mediaService.encryptedPath.existsSync()) { + await handleEncryptedFile(media.mediaId); return; } - final content = MessageContent.fromJson( - message.kind, - jsonDecode(message.contentJson!) as Map, - ); - - if (content is! MediaMessageContent) { - Log.error('Content of ${message.messageId} is not media file.'); - await handleMediaError(message); - return; - } - - if (content.downloadToken == null) { - Log.error('Download token not defined for ${message.messageId}.'); - await handleMediaError(message); - return; - } - - if (!force && !await isAllowedToDownload(content.isVideo)) { + if (!force && + !await isAllowedToDownload(isVideo: media.type == MediaType.video)) { Log.warn( - 'Download blocked for ${message.messageId} because of network state.', + 'Download blocked for ${media.mediaId} because of network state.', ); return; } final isBlocked = await protectDownload.protect(() async { - final msg = await twonlyDB.messagesDao - .getMessageByMessageId(message.messageId) - .getSingleOrNull(); + final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId); - if (msg == null) return true; - - if (msg.downloadState != DownloadState.pending) { - Log.error( - '${message.messageId} is already downloaded or is downloading.', - ); + if (msg == null || msg.downloadState != DownloadState.pending) { return true; } - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( + await twonlyDB.mediaFilesDao.updateMedia( + msg.mediaId, + const MediaFilesCompanion( downloadState: Value(DownloadState.downloading), ), ); @@ -178,11 +137,16 @@ Future startDownloadMedia(MediaFile media, bool force) async { }); if (isBlocked) { - Log.info('Download for ${message.messageId} already started.'); + Log.info('Download for ${media.mediaId} already started.'); return; } - final downloadToken = uint8ListToHex(content.downloadToken!); + if (media.downloadToken == null) { + Log.info('Download token for ${media.mediaId} not found.'); + return; + } + + final downloadToken = uint8ListToHex(media.downloadToken!); final apiUrl = 'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken'; @@ -190,20 +154,20 @@ Future startDownloadMedia(MediaFile media, bool force) async { try { final task = DownloadTask( url: apiUrl, - taskId: 'download_${message.messageId}', - directory: 'media/received/', - baseDirectory: BaseDirectory.applicationSupport, - filename: '${message.messageId}.encrypted', + taskId: 'download_${media.mediaId}', + directory: mediaService.encryptedPath.parent.path, + baseDirectory: BaseDirectory.root, + filename: basename(mediaService.encryptedPath.path), priority: 0, retries: 10, ); Log.info( - 'Got media file. Starting download: ${downloadToken.substring(0, 10)}', + 'Downloading ${media.mediaId} to ${mediaService.encryptedPath}', ); try { - await downloadFileFast(message.messageId, apiUrl); + await downloadFileFast(media, apiUrl, mediaService.encryptedPath); } catch (e) { Log.error('Fast download failed: $e'); await FileDownloader().enqueue(task); @@ -214,269 +178,114 @@ Future startDownloadMedia(MediaFile media, bool force) async { } Future downloadFileFast( - int messageId, + MediaFile media, String apiUrl, + File filePath, ) async { - final directoryPath = - '${(await getApplicationSupportDirectory()).path}/media/received/'; - final filename = '$messageId.encrypted'; - - final directory = Directory(directoryPath); - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - - final filePath = '${directory.path}/$filename'; - final response = await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10)); if (response.statusCode == 200) { - await File(filePath).writeAsBytes(response.bodyBytes); + await filePath.writeAsBytes(response.bodyBytes); Log.info('Fast Download successful: $filePath'); - await handleDownloadStatusUpdateInternal(messageId, false); + await handleEncryptedFile(media.mediaId); return; } else { - if (response.statusCode == 404 || response.statusCode == 403) { - await handleDownloadStatusUpdateInternal(messageId, true); + if (response.statusCode == 404 || + response.statusCode == 403 || + response.statusCode == 400) { + // Message was deleted from the server. Requesting it again from the sender to upload it again... + await requestMediaReupload(media.mediaId); return; } - // can be tried again + // Will be tried again using the slow method... throw Exception('Fast download failed with status: ${response.statusCode}'); } } -Future handleEncryptedFile(int messageId) async { - final msg = await twonlyDB.messagesDao - .getMessageByMessageId(messageId) - .getSingleOrNull(); - if (msg == null) { - Log.error('Not message for downloaded file found: $messageId'); +Future requestMediaReupload(String mediaId) async { + final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); + if (messages.length != 1 || messages.first.senderId == null) { + Log.error( + 'Media file has none or more than one sender. That is not possible'); return; } - final encryptedBytes = await readMediaFile(msg.messageId, 'encrypted'); + await sendCipherText( + messages.first.senderId!, + EncryptedContent( + mediaUpdate: EncryptedContent_MediaUpdate( + type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, + targetMessageId: mediaId, + ), + ), + ); - if (encryptedBytes == null) { - Log.error('encrypted bytes are not found for ${msg.messageId}'); + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.reuploadRequested), + ), + ); +} + +Future handleEncryptedFile(String mediaId) async { + final mediaService = await MediaFileService.fromMediaId(mediaId); + if (mediaService == null) { + Log.error('Media file $mediaId not found in database.'); return; } - final content = - MediaMessageContent.fromJson(jsonDecode(msg.contentJson!) as Map); + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.downloaded), + ), + ); + + late Uint8List encryptedBytes; + try { + encryptedBytes = await mediaService.encryptedPath.readAsBytes(); + } catch (e) { + Log.error('Could not read encrypted media file: $mediaId. $e'); + await requestMediaReupload(mediaId); + return; + } try { final chacha20 = FlutterChacha20.poly1305Aead(); - final secretKeyData = SecretKeyData(content.encryptionKey!); + final secretKeyData = SecretKeyData(mediaService.mediaFile.encryptionKey!); final secretBox = SecretBox( encryptedBytes, - nonce: content.encryptionNonce!, - mac: Mac(content.encryptionMac!), + nonce: mediaService.mediaFile.encryptionNonce!, + mac: Mac(mediaService.mediaFile.encryptionMac!), ); - // try { final plaintextBytes = await chacha20.decrypt(secretBox, secretKey: secretKeyData); - var imageBytes = Uint8List.fromList(plaintextBytes); - if (content.isVideo) { - final extractedBytes = extractUint8Lists(imageBytes); - imageBytes = extractedBytes[0]; - await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]); - } + final rawMediaBytes = Uint8List.fromList(plaintextBytes); - await writeMediaFile(msg.messageId, 'png', imageBytes); - // } catch (e) { - // Log.error( - // "could not decrypt the media file in the second try. reporting error to user: $e"); - // handleMediaError(msg); - // return; - // } + await mediaService.tempPath.writeAsBytes(rawMediaBytes); } catch (e) { - Log.error('$e'); - - /// legacy support - final chacha20 = Xchacha20.poly1305Aead(); - final secretKeyData = SecretKeyData(content.encryptionKey!); - - final secretBox = SecretBox( - encryptedBytes, - nonce: content.encryptionNonce!, - mac: Mac(content.encryptionMac!), + Log.error( + 'Could not decrypt the media file. Requesting a new upload.', ); - - try { - final plaintextBytes = - await chacha20.decrypt(secretBox, secretKey: secretKeyData); - var imageBytes = Uint8List.fromList(plaintextBytes); - - if (content.isVideo) { - final extractedBytes = extractUint8Lists(imageBytes); - imageBytes = extractedBytes[0]; - await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]); - } - - await writeMediaFile(msg.messageId, 'png', imageBytes); - } catch (e) { - Log.error( - 'could not decrypt the media file in the second try. reporting error to user: $e', - ); - await handleMediaError(msg); - return; - } + await requestMediaReupload(mediaId); + return; } - await twonlyDB.messagesDao.updateMessageByMessageId( - msg.messageId, - const MessagesCompanion(downloadState: Value(DownloadState.downloaded)), + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.ready), + ), ); - Log.info('Download and decryption of ${msg.messageId} was successful'); + Log.info('Decryption of $mediaId was successful'); - await deleteMediaFile(msg.messageId, 'encrypted'); + mediaService.encryptedPath.deleteSync(); - unawaited(apiService.downloadDone(content.downloadToken!)); + unawaited(apiService.downloadDone(mediaService.mediaFile.downloadToken!)); } - -Future getImageBytes(int mediaId) async { - return readMediaFile(mediaId, 'png'); -} - -Future getVideoPath(int mediaId) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - return File('$basePath.mp4'); -} - -/// --- helper functions --- - -Future readMediaFile(int mediaId, String type) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - Log.info('Reading: $file'); - if (!file.existsSync()) { - return null; - } - return file.readAsBytes(); -} - -Future existsMediaFile(int mediaId, String type) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - return file.existsSync(); -} - -Future writeMediaFile(int mediaId, String type, Uint8List data) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - await file.writeAsBytes(data); -} - -Future deleteMediaFile(int mediaId, String type) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - try { - if (file.existsSync()) { - await file.delete(); - } - } catch (e) { - Log.error('Error deleting: $e'); - } -} - -Future purgeReceivedMediaFiles() async { - final basedir = await getApplicationSupportDirectory(); - final directory = Directory(join(basedir.path, 'media', 'received')); - await purgeMediaFiles(directory); -} - -Future purgeMediaFiles(Directory directory) async { - // Check if the directory exists - if (directory.existsSync()) { - // List all files in the directory - final files = directory.listSync(); - - // Iterate over each file - for (final file in files) { - // Get the filename - final filename = file.uri.pathSegments.last; - - // Use a regular expression to extract the integer part - final match = RegExp(r'(\d+)').firstMatch(filename); - if (match != null) { - // Parse the integer and add it to the list - final fileId = int.parse(match.group(0)!); - - try { - if (directory.path.endsWith('send')) { - final messages = - await twonlyDB.messagesDao.getMessagesByMediaUploadId(fileId); - var canBeDeleted = true; - - for (final message in messages) { - try { - final content = MediaMessageContent.fromJson( - jsonDecode(message.contentJson!) as Map, - ); - - final oneDayAgo = - DateTime.now().subtract(const Duration(days: 1)); - final twoDaysAgo = - DateTime.now().subtract(const Duration(days: 1)); - - if ((message.openedAt == null || - oneDayAgo.isBefore(message.openedAt!)) && - !message.errorWhileSending) { - canBeDeleted = false; - } else if (message.mediaStored) { - if (!file.path.contains('.original.') && - !file.path.contains('.encrypted')) { - canBeDeleted = false; - } - } - - /// In case the image is not yet opened but successfully uploaded - /// to the server preserve the image for two days in case of an receiving error will happen - /// and then delete them as well. - if (message.acknowledgeByServer && - twoDaysAgo.isAfter(message.sendAt)) { - // Preserve images which can be stored by the other person... - if (content.maxShowTime != gMediaShowInfinite) { - canBeDeleted = true; - } - // Encrypted or upload data can be removed when acknowledgeByServer - if (file.path.contains('.upload') || - file.path.contains('.encrypted')) { - canBeDeleted = true; - } - } - } catch (e) { - Log.error(e); - } - } - if (canBeDeleted) { - Log.info('purged media file ${file.path} '); - file.deleteSync(); - } - } else { - final message = await twonlyDB.messagesDao - .getMessageByMessageId(fileId) - .getSingleOrNull(); - if ((message == null) || - (message.openedAt != null && - !message.mediaStored && - message.acknowledgeByServer) || - message.errorWhileSending) { - file.deleteSync(); - } - } - } catch (e) { - Log.error('$e'); - } - } - } - } -} - -// /data/user/0/eu.twonly.testing/files/media/received/27.encrypted -// /data/user/0/eu.twonly.testing/app_flutter/data/user/0/eu.twonly.testing/files/media/received/27.encrypted diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index 741c774..bef08fa 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @@ -8,7 +7,6 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/mediafile.service.dart'; -import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/utils/log.dart'; Future handleMedia( @@ -33,7 +31,10 @@ Future handleMedia( } // in case there was already a downloaded file delete it... - await removeMediaFile(message.mediaId!); + final mediaService = await MediaFileService.fromMediaId(message.mediaId!); + if (mediaService != null) { + mediaService.tempPath.deleteSync(); + } await twonlyDB.mediaFilesDao.updateMedia( message.mediaId!, @@ -81,6 +82,7 @@ Future handleMedia( ); if (mediaFile == null) { + Log.error('Could not insert media file into database'); return; } @@ -121,7 +123,11 @@ Future handleMediaUpdate( if (message == null || message.mediaId == null) return; final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); - if (mediaFile == null) return; + if (mediaFile == null) { + Log.info( + 'Got media file update, but media file was not found ${message.mediaId}'); + return; + } switch (mediaUpdate.type) { case EncryptedContent_MediaUpdate_Type.REOPENED: @@ -137,11 +143,12 @@ Future handleMediaUpdate( await twonlyDB.mediaFilesDao.updateMedia( mediaFile.mediaId, const MediaFilesCompanion( - storedByContact: Value(true), + stored: Value(true), ), ); - unawaited(createThumbnailForMediaFile(mediaFile)); + final mediaService = await MediaFileService.fromMedia(mediaFile); + unawaited(mediaService.createThumbnail()); case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: Log.info('Got media file decryption error ${mediaFile.mediaId}'); diff --git a/lib/src/services/mediafile.service.dart b/lib/src/services/mediafile.service.dart index eea8842..cc2b3f1 100644 --- a/lib/src/services/mediafile.service.dart +++ b/lib/src/services/mediafile.service.dart @@ -1,5 +1,124 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/utils/log.dart'; -Future removeMediaFile(String mediaId) async { - Log.error('TODO removeMediaFile: $mediaId'); +class MediaFileService { + MediaFileService(this.mediaFile, {required this.applicationSupportDirectory}); + MediaFile mediaFile; + + final Directory applicationSupportDirectory; + + static Future fromMedia(MediaFile media) async { + return MediaFileService( + media, + applicationSupportDirectory: await getApplicationSupportDirectory(), + ); + } + + static Future fromMediaId(String mediaId) async { + final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); + if (mediaFile == null) return null; + return MediaFileService( + mediaFile, + applicationSupportDirectory: await getApplicationSupportDirectory(), + ); + } + + Future updateFromDB() async { + final updated = + await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId); + if (updated != null) { + mediaFile = updated; + } + } + + Future createThumbnail() async { + if (!storedPath.existsSync()) { + Log.error('Could not create Thumbnail as stored media does not exists.'); + return; + } + switch (mediaFile.type) { + case MediaType.image: + await createThumbnailsForImage(storedPath, thumbnailPath); + case MediaType.video: + await createThumbnailsForVideo(storedPath, thumbnailPath); + case MediaType.gif: + Log.error('Thumbnail for .gif is not implemented yet'); + } + } + + void fullMediaRemoval() { + if (tempPath.existsSync()) { + tempPath.deleteSync(); + } + if (encryptedPath.existsSync()) { + encryptedPath.deleteSync(); + } + if (storedPath.existsSync()) { + storedPath.deleteSync(); + } + if (thumbnailPath.existsSync()) { + thumbnailPath.deleteSync(); + } + } + + Future storeMediaFile() async { + Log.info('Storing media file ${mediaFile.mediaId}'); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + stored: Value(true), + ), + ); + await tempPath.copy(storedPath.path); + await updateFromDB(); + } + + File _buildFilePath( + String directory, { + String namePrefix = '', + String extensionParam = '', + }) { + final mediaBaseDir = Directory(join( + applicationSupportDirectory.path, + 'mediafiles', + directory, + )); + if (!mediaBaseDir.existsSync()) { + mediaBaseDir.createSync(recursive: true); + } + var extension = extensionParam; + if (extension == '') { + switch (mediaFile.type) { + case MediaType.image: + extension = 'webp'; + case MediaType.video: + extension = 'mp4'; + case MediaType.gif: + extension = 'gif'; + } + } + return File( + join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'), + ); + } + + File get tempPath => _buildFilePath('tmp'); + File get storedPath => _buildFilePath('stored'); + File get thumbnailPath => _buildFilePath( + 'stored', + namePrefix: '.thumbnail', + extensionParam: 'webp', + ); + File get encryptedPath => _buildFilePath( + 'tmp', + namePrefix: '.encrypted', + ); } diff --git a/lib/src/services/thumbnail.service.dart b/lib/src/services/thumbnail.service.dart index a40d07c..ffaa999 100644 --- a/lib/src/services/thumbnail.service.dart +++ b/lib/src/services/thumbnail.service.dart @@ -1,26 +1,13 @@ import 'dart:io'; - import 'package:flutter_image_compress/flutter_image_compress.dart'; -import 'package:path/path.dart'; -import 'package:twonly/src/database/tables/mediafiles.table.dart'; -import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; - -Future createThumbnailForMediaFile(MediaFile media) async { - - switch (media.type) { - case MediaType.image: - TODO - break; - default: - } - -} - -Future createThumbnailsForImage(File file) async { - final fileExtension = file.path.split('.').last.toLowerCase(); +Future createThumbnailsForImage( + File sourceFile, + File destinationFile, +) async { + final fileExtension = sourceFile.path.split('.').last.toLowerCase(); if (fileExtension != 'png') { Log.error('Could not create thumbnail for image. $fileExtension != .png'); return; @@ -30,7 +17,7 @@ Future createThumbnailsForImage(File file) async { final imageBytesCompressed = await FlutterImageCompress.compressWithFile( minHeight: 800, minWidth: 450, - file.path, + sourceFile.path, format: CompressFormat.webp, quality: 50, ); @@ -39,16 +26,17 @@ Future createThumbnailsForImage(File file) async { Log.error('Could not compress the image'); return; } - - final thumbnailFile = getThumbnailPath(file); - await thumbnailFile.writeAsBytes(imageBytesCompressed); + await destinationFile.writeAsBytes(imageBytesCompressed); } catch (e) { Log.error('Could not compress the image got :$e'); } } -Future createThumbnailsForVideo(File file) async { - final fileExtension = file.path.split('.').last.toLowerCase(); +Future createThumbnailsForVideo( + File sourceFile, + File destinationFile, +) async { + final fileExtension = sourceFile.path.split('.').last.toLowerCase(); if (fileExtension != 'mp4') { Log.error('Could not create thumbnail for video. $fileExtension != .mp4'); return; @@ -56,8 +44,8 @@ Future createThumbnailsForVideo(File file) async { try { await VideoThumbnail.thumbnailFile( - video: file.path, - thumbnailPath: getThumbnailPath(file).path, + video: sourceFile.path, + thumbnailPath: destinationFile.path, maxWidth: 450, quality: 75, ); @@ -65,15 +53,3 @@ Future createThumbnailsForVideo(File file) async { Log.error('Could not create the video thumbnail: $e'); } } - -File getThumbnailPath(File file) { - final originalFileName = file.uri.pathSegments.last; - final fileNameWithoutExtension = originalFileName.split('.').first; - var fileExtension = originalFileName.split('.').last; - if (fileExtension == 'mp4') { - fileExtension = 'png'; - } - final newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension'; - Directory(file.parent.path).createSync(); - return File(join(file.parent.path, newFileName)); -} diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 5a93816..b31cc35 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -13,7 +13,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/model/protobuf/backup/backup.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; @@ -48,20 +48,19 @@ Future performTwonlySafeBackup({bool force = false}) async { final backupDir = Directory(join(baseDir, 'backup_twonly_safe/')); await backupDir.create(recursive: true); - final backupDatabaseFile = - File(join(backupDir.path, 'twonly_database.backup.sqlite')); + final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite')); final backupDatabaseFileCleaned = - File(join(backupDir.path, 'twonly_database.backup.cleaned.sqlite')); + File(join(backupDir.path, 'twonly.backup.cleaned.sqlite')); // copy database - final originalDatabase = File(join(baseDir, 'twonly_database.sqlite')); + final originalDatabase = File(join(baseDir, 'twonly.sqlite')); await originalDatabase.copy(backupDatabaseFile.path); driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; - final backupDB = TwonlyDatabase( + final backupDB = TwonlyDB( driftDatabase( - name: 'twonly_database.backup', + name: 'twonly.backup', native: DriftNativeOptions( databaseDirectory: () async { return backupDir; diff --git a/lib/src/services/twonly_safe/restore.twonly_safe.dart b/lib/src/services/twonly_safe/restore.twonly_safe.dart index 28a14c0..7669090 100644 --- a/lib/src/services/twonly_safe/restore.twonly_safe.dart +++ b/lib/src/services/twonly_safe/restore.twonly_safe.dart @@ -10,10 +10,8 @@ import 'package:http/http.dart' as http; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/model/protobuf/backup/backup.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -90,44 +88,9 @@ Future handleBackupData( ); final baseDir = (await getApplicationSupportDirectory()).path; - final originalDatabase = File(join(baseDir, 'twonly_database.sqlite')); + final originalDatabase = File(join(baseDir, 'twonly.sqlite')); await originalDatabase.writeAsBytes(backupContent.twonlyDatabase); - /// When restoring the last message ID must be increased otherwise - /// receivers would mark them as duplicates as they where already - /// send. - final database = TwonlyDatabase(); - var lastMessageSend = 0; - int? randomUserId; - - final contacts = await database.contactsDao.getAllNotBlockedContacts(); - for (final contact in contacts) { - randomUserId = contact.userId; - final days = DateTime.now().difference(contact.lastMessageExchange).inDays; - if (days < lastMessageSend) { - lastMessageSend = days; - } - } - - if (randomUserId != null) { - // for each day add 400 message ids - final dummyMessagesCounter = (lastMessageSend + 1) * 400; - Log.info( - 'Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.', - ); - for (var i = 0; i < dummyMessagesCounter; i++) { - await database.messagesDao.insertMessage( - MessagesCompanion( - contactId: Value(randomUserId), - kind: const Value(MessageKind.ack), - acknowledgeByServer: const Value(true), - errorWhileSending: const Value(true), - ), - ); - } - await database.messagesDao.deleteAllMessagesByContactId(randomUserId); - } - const storage = FlutterSecureStorage(); final secureStorage = jsonDecode(backupContent.secureStorageJson); diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index acd8eaa..3653b9c 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -340,6 +340,10 @@ class _ShareImageEditorView extends State { Future getMergedImage() async { Uint8List? image; + + TODO: When changed then create a new mediaID!!!!!! + As storedMediaId would overwrite it.... + if (layers.length > 1 || widget.videoFilePath != null) { for (final x in layers) { x.showCustomButtons = false;