mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-18 14:42:54 +00:00
add option to reopen images with context menu
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
19a53a879b
commit
587740f306
5 changed files with 124 additions and 85 deletions
|
|
@ -1,5 +1,9 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
- Fix: Several minor issues with the user interface
|
||||||
|
|
||||||
## 0.1.3
|
## 0.1.3
|
||||||
|
|
||||||
- New: Video stabilization
|
- New: Video stabilization
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 662b8ddafcbf1c789f54c93da51ebb0514ba1f81
|
Subproject commit f633c60dfe0edf36a8ed91804dba7a2879b5bc52
|
||||||
|
|
@ -44,8 +44,9 @@ class MediaFileService {
|
||||||
delete = false;
|
delete = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final messages =
|
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
||||||
await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
|
mediaId,
|
||||||
|
);
|
||||||
|
|
||||||
// in case messages in empty the file will be deleted, as delete is true by default
|
// 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
|
// 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
|
// delete = true; // do not overwrite a previous delete = false
|
||||||
// this is just to make it easier to understand :)
|
// this is just to make it easier to understand :)
|
||||||
} else if (message.openedAt!
|
} else if (message.openedAt!.isAfter(
|
||||||
.isAfter(clock.now().subtract(const Duration(days: 2)))) {
|
clock.now().subtract(const Duration(days: 2)),
|
||||||
|
)) {
|
||||||
// In case the image was opened, but send with unlimited time or no authentication.
|
// In case the image was opened, but send with unlimited time or no authentication.
|
||||||
if (message.senderId == null) {
|
if (message.senderId == null) {
|
||||||
delete = false;
|
delete = false;
|
||||||
} else {
|
} 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.
|
// 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.
|
// This also allows to reopen this image for two days.
|
||||||
final group =
|
final group = await twonlyDB.groupsDao.getGroup(
|
||||||
await twonlyDB.groupsDao.getGroup(message.groupId);
|
message.groupId,
|
||||||
|
);
|
||||||
if (group != null && !group.isDirectChat) {
|
if (group != null && !group.isDirectChat) {
|
||||||
delete = false;
|
delete = false;
|
||||||
}
|
}
|
||||||
|
|
@ -93,8 +96,9 @@ class MediaFileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateFromDB() async {
|
Future<void> updateFromDB() async {
|
||||||
final updated =
|
final updated = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||||
await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId);
|
mediaFile.mediaId,
|
||||||
|
);
|
||||||
if (updated != null) {
|
if (updated != null) {
|
||||||
mediaFile = updated;
|
mediaFile = updated;
|
||||||
}
|
}
|
||||||
|
|
@ -151,8 +155,9 @@ class MediaFileService {
|
||||||
mediaFile.mediaId,
|
mediaFile.mediaId,
|
||||||
MediaFilesCompanion(
|
MediaFilesCompanion(
|
||||||
requiresAuthentication: Value(requiresAuthentication),
|
requiresAuthentication: Value(requiresAuthentication),
|
||||||
displayLimitInMilliseconds:
|
displayLimitInMilliseconds: requiresAuthentication
|
||||||
requiresAuthentication ? const Value(12000) : const Value.absent(),
|
? const Value(12000)
|
||||||
|
: const Value.absent(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await updateFromDB();
|
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 =>
|
bool get imagePreviewAvailable =>
|
||||||
thumbnailPath.existsSync() || storedPath.existsSync();
|
thumbnailPath.existsSync() || storedPath.existsSync();
|
||||||
|
|
||||||
|
|
@ -293,8 +305,10 @@ class MediaFileService {
|
||||||
extension = 'm4a';
|
extension = 'm4a';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final mediaBaseDir =
|
final mediaBaseDir = buildDirectoryPath(
|
||||||
buildDirectoryPath(directory, globalApplicationSupportDirectory);
|
directory,
|
||||||
|
globalApplicationSupportDirectory,
|
||||||
|
);
|
||||||
return File(
|
return File(
|
||||||
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
|
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
|
||||||
);
|
);
|
||||||
|
|
@ -303,29 +317,29 @@ class MediaFileService {
|
||||||
File get tempPath => _buildFilePath('tmp');
|
File get tempPath => _buildFilePath('tmp');
|
||||||
File get storedPath => _buildFilePath('stored');
|
File get storedPath => _buildFilePath('stored');
|
||||||
File get thumbnailPath => _buildFilePath(
|
File get thumbnailPath => _buildFilePath(
|
||||||
'stored',
|
'stored',
|
||||||
namePrefix: '.thumbnail',
|
namePrefix: '.thumbnail',
|
||||||
extensionParam: 'webp',
|
extensionParam: 'webp',
|
||||||
);
|
);
|
||||||
File get encryptedPath => _buildFilePath(
|
File get encryptedPath => _buildFilePath(
|
||||||
'tmp',
|
'tmp',
|
||||||
namePrefix: '.encrypted',
|
namePrefix: '.encrypted',
|
||||||
);
|
);
|
||||||
File get uploadRequestPath => _buildFilePath(
|
File get uploadRequestPath => _buildFilePath(
|
||||||
'tmp',
|
'tmp',
|
||||||
namePrefix: '.upload',
|
namePrefix: '.upload',
|
||||||
);
|
);
|
||||||
File get originalPath => _buildFilePath(
|
File get originalPath => _buildFilePath(
|
||||||
'tmp',
|
'tmp',
|
||||||
namePrefix: '.original',
|
namePrefix: '.original',
|
||||||
);
|
);
|
||||||
File get ffmpegOutputPath => _buildFilePath(
|
File get ffmpegOutputPath => _buildFilePath(
|
||||||
'tmp',
|
'tmp',
|
||||||
namePrefix: '.ffmpeg',
|
namePrefix: '.ffmpeg',
|
||||||
);
|
);
|
||||||
File get overlayImagePath => _buildFilePath(
|
File get overlayImagePath => _buildFilePath(
|
||||||
'tmp',
|
'tmp',
|
||||||
namePrefix: '.overlay',
|
namePrefix: '.overlay',
|
||||||
extensionParam: 'png',
|
extensionParam: 'png',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,12 +57,10 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
widget.mediaService.mediaFile.displayLimitInMilliseconds != null) {
|
widget.mediaService.mediaFile.displayLimitInMilliseconds != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (widget.mediaService.tempPath.existsSync()) {
|
if (widget.mediaService.tempPath.existsSync() && mounted) {
|
||||||
if (mounted) {
|
setState(() {
|
||||||
setState(() {
|
_canBeReopened = true;
|
||||||
_canBeReopened = true;
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +68,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
if (widget.message.openedAt == null || widget.message.mediaStored) {
|
if (widget.message.openedAt == null || widget.message.mediaStored) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (widget.mediaService.tempPath.existsSync() &&
|
if (widget.mediaService.canBeOpenedAgain &&
|
||||||
widget.message.senderId != null) {
|
widget.message.senderId != null) {
|
||||||
await sendCipherText(
|
await sendCipherText(
|
||||||
widget.message.senderId!,
|
widget.message.senderId!,
|
||||||
|
|
@ -123,8 +121,14 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
|
|
||||||
final addData = widget.message.additionalMessageData;
|
final addData = widget.message.additionalMessageData;
|
||||||
if (addData != null) {
|
if (addData != null) {
|
||||||
final info =
|
final info = getBubbleInfo(
|
||||||
getBubbleInfo(context, widget.message, null, null, null, 200);
|
context,
|
||||||
|
widget.message,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
200,
|
||||||
|
);
|
||||||
final data = AdditionalMessageData.fromBuffer(addData);
|
final data = AdditionalMessageData.fromBuffer(addData);
|
||||||
if (data.hasLink() && widget.message.mediaStored) {
|
if (data.hasLink() && widget.message.mediaStored) {
|
||||||
imageBorderRadius = const BorderRadius.only(
|
imageBorderRadius = const BorderRadius.only(
|
||||||
|
|
@ -138,8 +142,12 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
),
|
),
|
||||||
padding:
|
padding: const EdgeInsets.only(
|
||||||
const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
left: 10,
|
||||||
|
top: 6,
|
||||||
|
bottom: 6,
|
||||||
|
right: 10,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: info.color,
|
color: info.color,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
|
|
@ -170,7 +178,8 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
onTap: (widget.message.type == MessageType.media.name) ? onTap : null,
|
onTap: (widget.message.type == MessageType.media.name) ? onTap : null,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: (widget.minWidth > 150) ? widget.minWidth : 150,
|
width: (widget.minWidth > 150) ? widget.minWidth : 150,
|
||||||
height: (widget.message.mediaStored &&
|
height:
|
||||||
|
(widget.message.mediaStored &&
|
||||||
widget.mediaService.imagePreviewAvailable)
|
widget.mediaService.imagePreviewAvailable)
|
||||||
? 271
|
? 271
|
||||||
: null,
|
: null,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// ignore_for_file: inference_failure_on_function_invocation
|
// ignore_for_file: inference_failure_on_function_invocation
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
@ -39,57 +40,67 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
final VoidCallback onResponseTriggered;
|
final VoidCallback onResponseTriggered;
|
||||||
|
|
||||||
Future<void> reopenMediaFile(BuildContext context) async {
|
Future<void> reopenMediaFile(BuildContext context) async {
|
||||||
final isAuth = await authenticateUser(
|
if (message.senderId == null) {
|
||||||
context.lang.authRequestReopenImage,
|
final isAuth = await authenticateUser(
|
||||||
force: false,
|
context.lang.authRequestReopenImage,
|
||||||
);
|
force: false,
|
||||||
|
);
|
||||||
|
if (!isAuth) return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isAuth && context.mounted && mediaFileService != null) {
|
if (!context.mounted || mediaFileService == null) return;
|
||||||
final galleryItems = [
|
|
||||||
MemoryItem(mediaService: mediaFileService!, messages: []),
|
|
||||||
];
|
|
||||||
|
|
||||||
await Navigator.push(
|
if (message.senderId != null) {
|
||||||
context,
|
// notify the sender
|
||||||
PageRouteBuilder(
|
await sendCipherText(
|
||||||
opaque: false,
|
message.senderId!,
|
||||||
pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView(
|
pb.EncryptedContent(
|
||||||
galleryItems: galleryItems,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return ContextMenu(
|
||||||
items: [
|
items: [
|
||||||
if (!message.isDeletedFromSender)
|
if (!message.isDeletedFromSender)
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
title: context.lang.react,
|
title: context.lang.react,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final layer = await showModalBottomSheet(
|
final layer =
|
||||||
context: context,
|
await showModalBottomSheet(
|
||||||
backgroundColor: Colors.black,
|
context: context,
|
||||||
builder: (context) {
|
backgroundColor: Colors.black,
|
||||||
return const EmojiPickerBottom();
|
builder: (context) {
|
||||||
},
|
return const EmojiPickerBottom();
|
||||||
) as EmojiLayerData?;
|
},
|
||||||
|
)
|
||||||
|
as EmojiLayerData?;
|
||||||
if (layer == null) return;
|
if (layer == null) return;
|
||||||
|
|
||||||
await twonlyDB.reactionsDao.updateMyReaction(
|
await twonlyDB.reactionsDao.updateMyReaction(
|
||||||
|
|
@ -111,7 +122,7 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
},
|
},
|
||||||
icon: FontAwesomeIcons.faceLaugh,
|
icon: FontAwesomeIcons.faceLaugh,
|
||||||
),
|
),
|
||||||
if (canBeOpenedAgain)
|
if (mediaFileService?.canBeOpenedAgain ?? false)
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
title: context.lang.contextMenuViewAgain,
|
title: context.lang.contextMenuViewAgain,
|
||||||
onTap: () => reopenMediaFile(context),
|
onTap: () => reopenMediaFile(context),
|
||||||
|
|
@ -153,8 +164,8 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
null,
|
null,
|
||||||
customOk:
|
customOk:
|
||||||
(message.senderId == null && !message.isDeletedFromSender)
|
(message.senderId == null && !message.isDeletedFromSender)
|
||||||
? context.lang.deleteOkBtnForAll
|
? context.lang.deleteOkBtnForAll
|
||||||
: context.lang.deleteOkBtnForMe,
|
: context.lang.deleteOkBtnForMe,
|
||||||
);
|
);
|
||||||
if (delete) {
|
if (delete) {
|
||||||
if (message.senderId == null && !message.isDeletedFromSender) {
|
if (message.senderId == null && !message.isDeletedFromSender) {
|
||||||
|
|
@ -173,8 +184,9 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await twonlyDB.messagesDao
|
await twonlyDB.messagesDao.deleteMessagesById(
|
||||||
.deleteMessagesById(message.messageId);
|
message.messageId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue