add option to reopen images with context menu
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-04-09 22:20:15 +02:00
parent 19a53a879b
commit 587740f306
5 changed files with 124 additions and 85 deletions

View file

@ -1,5 +1,9 @@
# Changelog
## 0.1.4
- Fix: Several minor issues with the user interface
## 0.1.3
- New: Video stabilization

@ -1 +1 @@
Subproject commit 662b8ddafcbf1c789f54c93da51ebb0514ba1f81
Subproject commit f633c60dfe0edf36a8ed91804dba7a2879b5bc52

View file

@ -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<void> 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'),
);

View file

@ -57,20 +57,18 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
widget.mediaService.mediaFile.displayLimitInMilliseconds != null) {
return;
}
if (widget.mediaService.tempPath.existsSync()) {
if (mounted) {
if (widget.mediaService.tempPath.existsSync() && mounted) {
setState(() {
_canBeReopened = true;
});
}
}
}
Future<void> onDoubleTap() async {
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<ChatMediaEntry> {
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<ChatMediaEntry> {
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<ChatMediaEntry> {
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,

View file

@ -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,12 +40,35 @@ class MessageContextMenu extends StatelessWidget {
final VoidCallback onResponseTriggered;
Future<void> reopenMediaFile(BuildContext context) async {
if (message.senderId == null) {
final isAuth = await authenticateUser(
context.lang.authRequestReopenImage,
force: false,
);
if (!isAuth) return;
}
if (!context.mounted || mediaFileService == null) return;
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;
if (isAuth && context.mounted && mediaFileService != null) {
final galleryItems = [
MemoryItem(mediaService: mediaFileService!, messages: []),
];
@ -59,37 +83,24 @@ class MessageContextMenu extends StatelessWidget {
),
);
}
}
@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(
final layer =
await showModalBottomSheet(
context: context,
backgroundColor: Colors.black,
builder: (context) {
return const EmojiPickerBottom();
},
) as EmojiLayerData?;
)
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),
@ -173,8 +184,9 @@ class MessageContextMenu extends StatelessWidget {
),
);
} else {
await twonlyDB.messagesDao
.deleteMessagesById(message.messageId);
await twonlyDB.messagesDao.deleteMessagesById(
message.messageId,
);
}
}
},