This commit is contained in:
otsmr 2025-05-29 17:10:33 +02:00
parent d488e4db2c
commit 24cf6e552f
6 changed files with 271 additions and 307 deletions

View file

@ -1,100 +0,0 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry_components/chat_media_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry_components/chat_reaction_row.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry_components/chat_text_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry_components/chat_text_response_columns.dart';
import 'package:twonly/src/views/chats/chat_messages_components/sliding_response.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
class ChatListEntry extends StatefulWidget {
const ChatListEntry(
this.message,
this.contact,
this.lastMessageFromSameUser,
this.textReactions,
this.otherReactions, {
super.key,
required this.onResponseTriggered,
});
final Message message;
final Contact contact;
final bool lastMessageFromSameUser;
final List<Message> textReactions;
final List<Message> otherReactions;
final Function(Message) onResponseTriggered;
@override
State<ChatListEntry> createState() => _ChatListEntryState();
}
class _ChatListEntryState extends State<ChatListEntry> {
MessageContent? content;
String? textMessage;
@override
void initState() {
super.initState();
final msgContent = MessageContent.fromJson(
widget.message.kind, jsonDecode(widget.message.contentJson!));
if (msgContent is TextMessageContent) {
textMessage = msgContent.text;
}
content = msgContent;
}
@override
Widget build(BuildContext context) {
if (content == null) return Container();
bool right = widget.message.messageOtherId == null;
return Align(
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
child: Padding(
padding: widget.lastMessageFromSameUser
? EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10)
: EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
child: Column(
mainAxisAlignment:
right ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment:
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
SlidingResponse(
child: Stack(
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
children: [
(textMessage != null)
? ChatTextEntry(
message: widget.message, text: textMessage!)
: ChatMediaEntry(
message: widget.message,
contact: widget.contact,
content: content!,
),
Positioned(
bottom: 5,
left: 5,
right: 5,
child: ReactionRow(
otherReactions: widget.otherReactions,
message: widget.message,
),
),
],
),
onResponseTriggered: () {
widget.onResponseTriggered(widget.message);
},
),
ChatTextResponseColumns(
textReactions: widget.textReactions,
right: right,
)
],
),
),
);
}
}

View file

@ -7,11 +7,13 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_message_ent
import 'package:twonly/src/views/chats/chat_messages_components/sliding_response.dart'; import 'package:twonly/src/views/chats/chat_messages_components/sliding_response.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
class ChatListEntry extends StatefulWidget { class ChatListEntry extends StatefulWidget {
const ChatListEntry( const ChatListEntry(
this.message, this.message,
this.contact, this.contact,
this.galleryItems,
this.lastMessageFromSameUser, this.lastMessageFromSameUser,
this.textReactions, this.textReactions,
this.otherReactions, { this.otherReactions, {
@ -23,6 +25,7 @@ class ChatListEntry extends StatefulWidget {
final bool lastMessageFromSameUser; final bool lastMessageFromSameUser;
final List<Message> textReactions; final List<Message> textReactions;
final List<Message> otherReactions; final List<Message> otherReactions;
final List<GalleryItem> galleryItems;
final Function(Message) onResponseTriggered; final Function(Message) onResponseTriggered;
@override @override
@ -49,7 +52,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
if (content == null) return Container(); if (content == null) return Container();
bool right = widget.message.messageOtherId == null; bool right = widget.message.messageOtherId == null;
return Align( return Hero(
tag: "${widget.message.mediaUploadId ?? widget.message.messageId}",
child: Align(
alignment: right ? Alignment.centerRight : Alignment.centerLeft, alignment: right ? Alignment.centerRight : Alignment.centerLeft,
child: Padding( child: Padding(
padding: widget.lastMessageFromSameUser padding: widget.lastMessageFromSameUser
@ -63,7 +68,8 @@ class _ChatListEntryState extends State<ChatListEntry> {
children: [ children: [
SlidingResponse( SlidingResponse(
child: Stack( child: Stack(
alignment: right ? Alignment.centerRight : Alignment.centerLeft, alignment:
right ? Alignment.centerRight : Alignment.centerLeft,
children: [ children: [
(textMessage != null) (textMessage != null)
? ChatTextEntry( ? ChatTextEntry(
@ -71,6 +77,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
: ChatMediaEntry( : ChatMediaEntry(
message: widget.message, message: widget.message,
contact: widget.contact, contact: widget.contact,
galleryItems: widget.galleryItems,
content: content!, content: content!,
), ),
Positioned( Positioned(
@ -95,6 +102,6 @@ class _ChatListEntryState extends State<ChatListEntry> {
], ],
), ),
), ),
); ));
} }
} }

View file

@ -9,6 +9,7 @@ import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api/media_received.dart' as received; import 'package:twonly/src/providers/api/media_received.dart' as received;
import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/services/notification_service.dart';
import 'package:twonly/src/views/chats/media_viewer_view.dart'; import 'package:twonly/src/views/chats/media_viewer_view.dart';
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
class ChatMediaEntry extends StatelessWidget { class ChatMediaEntry extends StatelessWidget {
const ChatMediaEntry({ const ChatMediaEntry({
@ -16,11 +17,13 @@ class ChatMediaEntry extends StatelessWidget {
required this.message, required this.message,
required this.contact, required this.contact,
required this.content, required this.content,
required this.galleryItems,
}); });
final Message message; final Message message;
final Contact contact; final Contact contact;
final MessageContent content; final MessageContent content;
final List<GalleryItem> galleryItems;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -70,15 +73,8 @@ class ChatMediaEntry extends StatelessWidget {
} }
} }
}, },
child: Container( child: SizedBox(
width: 150, width: 150,
decoration: BoxDecoration(
border: Border.all(
color: color,
width: 1.0,
),
borderRadius: BorderRadius.circular(12.0),
),
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: ClipRRect( child: ClipRRect(
@ -86,6 +82,8 @@ class ChatMediaEntry extends StatelessWidget {
child: InChatMediaViewer( child: InChatMediaViewer(
message: message, message: message,
contact: contact, contact: contact,
color: color,
galleryItems: galleryItems,
), ),
), ),
), ),

View file

@ -14,110 +14,121 @@ import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/providers/api/media_received.dart' as received; import 'package:twonly/src/providers/api/media_received.dart' as received;
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
class ChatMediaViewerFullScreen extends StatefulWidget { // class ChatMediaViewerFullScreen extends StatefulWidget {
const ChatMediaViewerFullScreen( // const ChatMediaViewerFullScreen({
{super.key, required this.message, required this.contact}); // super.key,
final Message message; // required this.message,
final Contact contact; // required this.contact,
// required this.color,
// });
// final Message message;
// final Contact contact;
// final Color color;
@override // @override
State<ChatMediaViewerFullScreen> createState() => // State<ChatMediaViewerFullScreen> createState() =>
_ChatMediaViewerFullScreenState(); // _ChatMediaViewerFullScreenState();
} // }
class _ChatMediaViewerFullScreenState extends State<ChatMediaViewerFullScreen> { // class _ChatMediaViewerFullScreenState extends State<ChatMediaViewerFullScreen> {
bool hideMediaFile = false; // bool hideMediaFile = false;
Future deleteFiles(context) async { // Future deleteFiles(context) async {
bool confirmed = await showAlertDialog( // bool confirmed = await showAlertDialog(
context, "Are you sure?", "The image will be irrevocably deleted."); // context, "Are you sure?", "The image will be irrevocably deleted.");
if (!confirmed) return; // if (!confirmed) return;
await twonlyDatabase.messagesDao.updateMessageByMessageId( // await twonlyDatabase.messagesDao.updateMessageByMessageId(
widget.message.messageId, // widget.message.messageId,
MessagesCompanion(mediaStored: Value(false)), // MessagesCompanion(mediaStored: Value(false)),
); // );
await send.purgeSendMediaFiles(); // await send.purgeSendMediaFiles();
await received.purgeReceivedMediaFiles(); // await received.purgeReceivedMediaFiles();
if (context.mounted) { // if (context.mounted) {
Navigator.pop(context, true); // Navigator.pop(context, true);
} // }
} // }
@override // @override
Widget build(BuildContext context) { // Widget build(BuildContext context) {
return Scaffold( // return Scaffold(
body: MediaViewSizing( // body: MediaViewSizing(
bottomNavigation: Positioned( // bottomNavigation: Positioned(
bottom: 10, // bottom: 10,
left: 0, // left: 0,
right: 0, // right: 0,
child: Row( // child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ // children: [
IconButton.outlined( // IconButton.outlined(
onPressed: () { // onPressed: () {
deleteFiles(context); // deleteFiles(context);
}, // },
icon: FaIcon(FontAwesomeIcons.trashCan), // icon: FaIcon(FontAwesomeIcons.trashCan),
style: ButtonStyle( // style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( // padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20), // EdgeInsets.symmetric(vertical: 10, horizontal: 20),
), // ),
), // ),
), // ),
IconButton.filled( // IconButton.filled(
icon: FaIcon(FontAwesomeIcons.camera), // icon: FaIcon(FontAwesomeIcons.camera),
onPressed: () async { // onPressed: () async {
setState(() { // setState(() {
hideMediaFile = true; // hideMediaFile = true;
}); // });
await Navigator.push(context, MaterialPageRoute( // await Navigator.push(context, MaterialPageRoute(
builder: (context) { // builder: (context) {
return CameraSendToView(widget.contact); // return CameraSendToView(widget.contact);
}, // },
)); // ));
setState(() { // setState(() {
hideMediaFile = false; // hideMediaFile = false;
}); // });
}, // },
style: ButtonStyle( // style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( // padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30), // EdgeInsets.symmetric(vertical: 10, horizontal: 30),
), // ),
backgroundColor: WidgetStateProperty.all<Color>( // backgroundColor: WidgetStateProperty.all<Color>(
Theme.of(context).colorScheme.primary, // Theme.of(context).colorScheme.primary,
)), // )),
), // ),
], // ],
), // ),
), // ),
child: (hideMediaFile) // child: (hideMediaFile)
? Container() // ? Container()
: InChatMediaViewer( // : Hero(
message: widget.message, // tag: "chat_entry_${widget.message.messageId}",
contact: widget.contact, // child: InChatMediaViewer(
isInFullscreen: true, // message: widget.message,
), // contact: widget.contact,
), // color: widget.color,
); // isInFullscreen: true,
} // ),
} // )),
// );
// }
// }
class InChatMediaViewer extends StatefulWidget { class InChatMediaViewer extends StatefulWidget {
const InChatMediaViewer({ const InChatMediaViewer({
super.key, super.key,
required this.message, required this.message,
required this.contact, required this.contact,
this.isInFullscreen = false, required this.color,
required this.galleryItems,
}); });
final Message message; final Message message;
final Contact contact; final Contact contact;
final bool isInFullscreen; final List<GalleryItem> galleryItems;
final Color color;
@override @override
State<InChatMediaViewer> createState() => _InChatMediaViewerState(); State<InChatMediaViewer> createState() => _InChatMediaViewerState();
@ -184,13 +195,10 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
} }
videoController = VideoPlayerController.file( videoController = VideoPlayerController.file(
videoPath, videoPath,
videoPlayerOptions: videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
VideoPlayerOptions(mixWithOthers: !widget.isInFullscreen),
); );
videoController?.initialize().then((_) { videoController?.initialize().then((_) {
if (!widget.isInFullscreen) {
videoController!.setVolume(0); videoController!.setVolume(0);
}
videoController!.play(); videoController!.play();
videoController!.setLooping(true); videoController!.setLooping(true);
}); });
@ -209,40 +217,72 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
} }
Future onTap() async { Future onTap() async {
bool? removed = await Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(
return ChatMediaViewerFullScreen( builder: (context) => GalleryPhotoViewWrapper(
message: widget.message, galleryItems: widget.galleryItems,
contact: widget.contact, // backgroundDecoration: const BoxDecoration(
); // color: Colors.black,
}), // ),
initialIndex: widget.galleryItems.indexWhere((x) =>
x.id ==
(widget.message.mediaUploadId ?? widget.message.messageId)
.toString()),
scrollDirection: Axis.horizontal,
),
),
); );
// bool? removed = await Navigator.push(
// context,
// MaterialPageRoute(builder: (context) {
// return ChatMediaViewerFullScreen(
// message: widget.message,
// contact: widget.contact,
// color: widget.color,
// );
// }),
// );
if (removed != null && removed) { // if (removed != null && removed) {
image = null; // image = null;
videoController?.dispose(); // videoController?.dispose();
videoController = null; // videoController = null;
if (isMounted) setState(() {}); // if (isMounted) setState(() {});
} // }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (image == null && video == null) { if (image == null && video == null) {
return Padding( return Container(
decoration: BoxDecoration(
border: Border.all(
color: widget.color,
width: 1.0,
),
borderRadius: BorderRadius.circular(12.0),
),
child: Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
child: MessageSendStateIcon( child: MessageSendStateIcon(
[widget.message], [widget.message],
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
), ),
),
); );
} }
return GestureDetector( return Container(
onTap: decoration: BoxDecoration(
((image == null && videoController == null) || widget.isInFullscreen) border: Border.all(
? null color: Colors.transparent,
: onTap, width: 1.0,
),
color: Colors.transparent,
borderRadius: BorderRadius.circular(12.0),
),
child: GestureDetector(
onTap: ((image == null && videoController == null)) ? null : onTap,
child: Stack( child: Stack(
children: [ children: [
if (image != null) Image.file(image!), if (image != null) Image.file(image!),
@ -255,6 +295,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
), ),
], ],
), ),
),
); );
} }
} }

View file

@ -18,6 +18,7 @@ import 'package:twonly/src/services/notification_service.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact_view.dart'; import 'package:twonly/src/views/contact/contact_view.dart';
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
Color getMessageColor(Message message) { Color getMessageColor(Message message) {
return (message.messageOtherId == null) return (message.messageOtherId == null)
@ -43,6 +44,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
late StreamSubscription<Contact> userSub; late StreamSubscription<Contact> userSub;
late StreamSubscription<List<Message>> messageSub; late StreamSubscription<List<Message>> messageSub;
List<Message> messages = []; List<Message> messages = [];
List<GalleryItem> galleryItems = [];
Map<int, List<Message>> textReactionsToMessageId = {}; Map<int, List<Message>> textReactionsToMessageId = {};
Map<int, List<Message>> emojiReactionsToMessageId = {}; Map<int, List<Message>> emojiReactionsToMessageId = {};
Message? responseToMessage; Message? responseToMessage;
@ -76,7 +78,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Stream<List<Message>> msgStream = Stream<List<Message>> msgStream =
twonlyDatabase.messagesDao.watchAllMessagesFrom(widget.contact.userId); twonlyDatabase.messagesDao.watchAllMessagesFrom(widget.contact.userId);
messageSub = msgStream.listen((msgs) { messageSub = msgStream.listen((msgs) async {
// if (!context.mounted) return; // if (!context.mounted) return;
if (Platform.isAndroid) { if (Platform.isAndroid) {
flutterLocalNotificationsPlugin.cancel(widget.contact.userId); flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
@ -131,6 +133,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
displayedMessages.add(msg); displayedMessages.add(msg);
} }
} }
if (openedMessageOtherIds.isNotEmpty) { if (openedMessageOtherIds.isNotEmpty) {
notifyContactAboutOpeningMessage( notifyContactAboutOpeningMessage(
widget.contact.userId, openedMessageOtherIds); widget.contact.userId, openedMessageOtherIds);
@ -145,7 +148,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
emojiReactionsToMessageId = tmpEmojiReactionsToMessageId; emojiReactionsToMessageId = tmpEmojiReactionsToMessageId;
messages = displayedMessages; messages = displayedMessages;
}); });
// } Map<int, GalleryItem> items = await GalleryItem.convertFromMessages(
displayedMessages
.where((x) => x.kind == MessageKind.media)
.toList()
.reversed
.toList());
setState(() {
galleryItems = items.values.toList();
});
}); });
} }
@ -299,6 +310,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
key: Key(messages[i].messageId.toString()), key: Key(messages[i].messageId.toString()),
messages[i], messages[i],
user, user,
galleryItems,
isLastMessageFromSameUser(messages, i), isLastMessageFromSameUser(messages, i),
textReactionsToMessageId[messages[i].messageId] ?? [], textReactionsToMessageId[messages[i].messageId] ?? [],
emojiReactionsToMessageId[messages[i].messageId] ?? [], emojiReactionsToMessageId[messages[i].messageId] ?? [],

View file

@ -26,6 +26,40 @@ class GalleryItem {
final DateTime date; final DateTime date;
final File? imagePath; final File? imagePath;
final File? videoPath; final File? videoPath;
static Future<Map<int, GalleryItem>> convertFromMessages(
List<Message> messages) async {
Map<int, GalleryItem> items = {};
for (final message in messages) {
bool isSend = message.messageOtherId == null;
int id = message.mediaUploadId ?? message.messageId;
final basePath = await send.getMediaFilePath(
isSend ? message.mediaUploadId! : message.messageId,
isSend ? "send" : "received",
);
File? imagePath;
File? videoPath;
if (await File("$basePath.mp4").exists()) {
videoPath = File("$basePath.mp4");
} else if (await File("$basePath.png").exists()) {
imagePath = File("$basePath.png");
} else {
continue;
}
items
.putIfAbsent(
id,
() => GalleryItem(
id: id.toString(),
messages: [],
date: message.sendAt,
imagePath: imagePath,
videoPath: videoPath))
.messages
.add(message);
}
return items;
}
} }
class GalleryItemGrid { class GalleryItemGrid {
@ -180,40 +214,12 @@ class GalleryMainViewState extends State<GalleryMainView> {
List<Message> storedMediaFiles = List<Message> storedMediaFiles =
await twonlyDatabase.messagesDao.getAllStoredMediaFiles(); await twonlyDatabase.messagesDao.getAllStoredMediaFiles();
Map<int, GalleryItem> items = {}; Map<int, GalleryItem> items =
for (final message in storedMediaFiles) { await GalleryItem.convertFromMessages(storedMediaFiles);
bool isSend = message.messageOtherId == null;
int id = message.mediaUploadId ?? message.messageId;
final basePath = await send.getMediaFilePath(
isSend ? message.mediaUploadId! : message.messageId,
isSend ? "send" : "received",
);
File? imagePath;
File? videoPath;
if (await File("$basePath.mp4").exists()) {
videoPath = File("$basePath.mp4");
} else if (await File("$basePath.png").exists()) {
imagePath = File("$basePath.png");
} else {
continue;
}
items
.putIfAbsent(
id,
() => GalleryItem(
id: id.toString(),
messages: [],
date: message.sendAt,
imagePath: imagePath,
videoPath: videoPath))
.messages
.add(message);
}
// Group items by month // Group items by month
orderedByMonth = {}; orderedByMonth = {};
months = []; months = [];
String lastMonth = ""; String lastMonth = "";
galleryItems = await loadMemoriesDirectory(); galleryItems = await loadMemoriesDirectory();
galleryItems += items.values.toList(); galleryItems += items.values.toList();