From 587740f306a84bfc6f2f217c481ca5bd9aec695f Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 9 Apr 2026 22:20:15 +0200 Subject: [PATCH] add option to reopen images with context menu --- CHANGELOG.md | 4 + lib/src/localization/translations | 2 +- .../mediafiles/mediafile.service.dart | 78 +++++++++------- .../entries/chat_media_entry.dart | 33 ++++--- .../message_context_menu.dart | 92 +++++++++++-------- 5 files changed, 124 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81e7ef3..b08fdc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.1.4 + +- Fix: Several minor issues with the user interface + ## 0.1.3 - New: Video stabilization diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 662b8dd..f633c60 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 662b8ddafcbf1c789f54c93da51ebb0514ba1f81 +Subproject commit f633c60dfe0edf36a8ed91804dba7a2879b5bc52 diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 2528ea0..6fb1827 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -44,8 +44,9 @@ class MediaFileService { delete = false; } - final messages = - await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); + final messages = await twonlyDB.messagesDao.getMessagesByMediaId( + mediaId, + ); // in case messages in empty the file will be deleted, as delete is true by default @@ -63,16 +64,18 @@ class MediaFileService { // 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)))) { + } else if (message.openedAt!.isAfter( + clock.now().subtract(const Duration(days: 2)), + )) { // In case the image was opened, but send with unlimited time or no authentication. if (message.senderId == null) { delete = false; } else { // Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image. // This also allows to reopen this image for two days. - final group = - await twonlyDB.groupsDao.getGroup(message.groupId); + final group = await twonlyDB.groupsDao.getGroup( + message.groupId, + ); if (group != null && !group.isDirectChat) { delete = false; } @@ -93,8 +96,9 @@ class MediaFileService { } Future updateFromDB() async { - final updated = - await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId); + final updated = await twonlyDB.mediaFilesDao.getMediaFileById( + mediaFile.mediaId, + ); if (updated != null) { mediaFile = updated; } @@ -151,8 +155,9 @@ class MediaFileService { mediaFile.mediaId, MediaFilesCompanion( requiresAuthentication: Value(requiresAuthentication), - displayLimitInMilliseconds: - requiresAuthentication ? const Value(12000) : const Value.absent(), + displayLimitInMilliseconds: requiresAuthentication + ? const Value(12000) + : const Value.absent(), ), ); await updateFromDB(); @@ -208,6 +213,13 @@ class MediaFileService { } } + // Media was send with unlimited display limit time and without auth required + // and the temp media file still exists, then the media file can be reopened again... + bool get canBeOpenedAgain => + !mediaFile.requiresAuthentication && + mediaFile.displayLimitInMilliseconds == null && + tempPath.existsSync(); + bool get imagePreviewAvailable => thumbnailPath.existsSync() || storedPath.existsSync(); @@ -293,8 +305,10 @@ class MediaFileService { extension = 'm4a'; } } - final mediaBaseDir = - buildDirectoryPath(directory, globalApplicationSupportDirectory); + final mediaBaseDir = buildDirectoryPath( + directory, + globalApplicationSupportDirectory, + ); return File( join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'), ); @@ -303,29 +317,29 @@ class MediaFileService { File get tempPath => _buildFilePath('tmp'); File get storedPath => _buildFilePath('stored'); File get thumbnailPath => _buildFilePath( - 'stored', - namePrefix: '.thumbnail', - extensionParam: 'webp', - ); + 'stored', + namePrefix: '.thumbnail', + extensionParam: 'webp', + ); File get encryptedPath => _buildFilePath( - 'tmp', - namePrefix: '.encrypted', - ); + 'tmp', + namePrefix: '.encrypted', + ); File get uploadRequestPath => _buildFilePath( - 'tmp', - namePrefix: '.upload', - ); + 'tmp', + namePrefix: '.upload', + ); File get originalPath => _buildFilePath( - 'tmp', - namePrefix: '.original', - ); + 'tmp', + namePrefix: '.original', + ); File get ffmpegOutputPath => _buildFilePath( - 'tmp', - namePrefix: '.ffmpeg', - ); + 'tmp', + namePrefix: '.ffmpeg', + ); File get overlayImagePath => _buildFilePath( - 'tmp', - namePrefix: '.overlay', - extensionParam: 'png', - ); + 'tmp', + namePrefix: '.overlay', + extensionParam: 'png', + ); } diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart index 6668e78..2867938 100644 --- a/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart @@ -57,12 +57,10 @@ class _ChatMediaEntryState extends State { widget.mediaService.mediaFile.displayLimitInMilliseconds != null) { return; } - if (widget.mediaService.tempPath.existsSync()) { - if (mounted) { - setState(() { - _canBeReopened = true; - }); - } + if (widget.mediaService.tempPath.existsSync() && mounted) { + setState(() { + _canBeReopened = true; + }); } } @@ -70,7 +68,7 @@ class _ChatMediaEntryState extends State { if (widget.message.openedAt == null || widget.message.mediaStored) { return; } - if (widget.mediaService.tempPath.existsSync() && + if (widget.mediaService.canBeOpenedAgain && widget.message.senderId != null) { await sendCipherText( widget.message.senderId!, @@ -123,8 +121,14 @@ class _ChatMediaEntryState extends State { final addData = widget.message.additionalMessageData; if (addData != null) { - final info = - getBubbleInfo(context, widget.message, null, null, null, 200); + final info = getBubbleInfo( + context, + widget.message, + null, + null, + null, + 200, + ); final data = AdditionalMessageData.fromBuffer(addData); if (data.hasLink() && widget.message.mediaStored) { imageBorderRadius = const BorderRadius.only( @@ -138,8 +142,12 @@ class _ChatMediaEntryState extends State { constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, ), - padding: - const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), + padding: const EdgeInsets.only( + left: 10, + top: 6, + bottom: 6, + right: 10, + ), decoration: BoxDecoration( color: info.color, borderRadius: const BorderRadius.only( @@ -170,7 +178,8 @@ class _ChatMediaEntryState extends State { onTap: (widget.message.type == MessageType.media.name) ? onTap : null, child: SizedBox( width: (widget.minWidth > 150) ? widget.minWidth : 150, - height: (widget.message.mediaStored && + height: + (widget.message.mediaStored && widget.mediaService.imagePreviewAvailable) ? 271 : null, diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index e2b12b1..f3ef4a4 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -1,6 +1,7 @@ // ignore_for_file: inference_failure_on_function_invocation import 'package:clock/clock.dart'; +import 'package:drift/drift.dart' show Value; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -39,57 +40,67 @@ class MessageContextMenu extends StatelessWidget { final VoidCallback onResponseTriggered; Future reopenMediaFile(BuildContext context) async { - final isAuth = await authenticateUser( - context.lang.authRequestReopenImage, - force: false, - ); + if (message.senderId == null) { + final isAuth = await authenticateUser( + context.lang.authRequestReopenImage, + force: false, + ); + if (!isAuth) return; + } - if (isAuth && context.mounted && mediaFileService != null) { - final galleryItems = [ - MemoryItem(mediaService: mediaFileService!, messages: []), - ]; + if (!context.mounted || mediaFileService == null) return; - await Navigator.push( - context, - PageRouteBuilder( - opaque: false, - pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( - galleryItems: galleryItems, + if (message.senderId != null) { + // notify the sender + await sendCipherText( + message.senderId!, + pb.EncryptedContent( + mediaUpdate: pb.EncryptedContent_MediaUpdate( + type: pb.EncryptedContent_MediaUpdate_Type.REOPENED, + targetMessageId: message.messageId, ), ), ); + await twonlyDB.messagesDao.updateMessageId( + message.messageId, + const MessagesCompanion(openedAt: Value(null)), + ); + return; } + if (!context.mounted) return; + + final galleryItems = [ + MemoryItem(mediaService: mediaFileService!, messages: []), + ]; + + await Navigator.push( + context, + PageRouteBuilder( + opaque: false, + pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( + galleryItems: galleryItems, + ), + ), + ); } @override Widget build(BuildContext context) { - var canBeOpenedAgain = false; - // in case this is a media send from this user... - if (mediaFileService != null && message.senderId == null) { - // and the media was send with unlimited display limit time and without auth required... - if (!mediaFileService!.mediaFile.requiresAuthentication && - mediaFileService!.mediaFile.displayLimitInMilliseconds == null) { - // and the temp media file still exists - if (mediaFileService!.tempPath.existsSync()) { - // the media file can be opened again... - canBeOpenedAgain = true; - } - } - } - return ContextMenu( items: [ if (!message.isDeletedFromSender) ContextMenuItem( title: context.lang.react, onTap: () async { - final layer = await showModalBottomSheet( - context: context, - backgroundColor: Colors.black, - builder: (context) { - return const EmojiPickerBottom(); - }, - ) as EmojiLayerData?; + final layer = + await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (context) { + return const EmojiPickerBottom(); + }, + ) + as EmojiLayerData?; if (layer == null) return; await twonlyDB.reactionsDao.updateMyReaction( @@ -111,7 +122,7 @@ class MessageContextMenu extends StatelessWidget { }, icon: FontAwesomeIcons.faceLaugh, ), - if (canBeOpenedAgain) + if (mediaFileService?.canBeOpenedAgain ?? false) ContextMenuItem( title: context.lang.contextMenuViewAgain, onTap: () => reopenMediaFile(context), @@ -153,8 +164,8 @@ class MessageContextMenu extends StatelessWidget { null, customOk: (message.senderId == null && !message.isDeletedFromSender) - ? context.lang.deleteOkBtnForAll - : context.lang.deleteOkBtnForMe, + ? context.lang.deleteOkBtnForAll + : context.lang.deleteOkBtnForMe, ); if (delete) { if (message.senderId == null && !message.isDeletedFromSender) { @@ -173,8 +184,9 @@ class MessageContextMenu extends StatelessWidget { ), ); } else { - await twonlyDB.messagesDao - .deleteMessagesById(message.messageId); + await twonlyDB.messagesDao.deleteMessagesById( + message.messageId, + ); } } },