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/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
class ChatListEntry extends StatefulWidget {
const ChatListEntry(
this.message,
this.contact,
this.galleryItems,
this.lastMessageFromSameUser,
this.textReactions,
this.otherReactions, {
@ -23,6 +25,7 @@ class ChatListEntry extends StatefulWidget {
final bool lastMessageFromSameUser;
final List<Message> textReactions;
final List<Message> otherReactions;
final List<GalleryItem> galleryItems;
final Function(Message) onResponseTriggered;
@override
@ -49,7 +52,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
if (content == null) return Container();
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,
child: Padding(
padding: widget.lastMessageFromSameUser
@ -63,7 +68,8 @@ class _ChatListEntryState extends State<ChatListEntry> {
children: [
SlidingResponse(
child: Stack(
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
alignment:
right ? Alignment.centerRight : Alignment.centerLeft,
children: [
(textMessage != null)
? ChatTextEntry(
@ -71,6 +77,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
: ChatMediaEntry(
message: widget.message,
contact: widget.contact,
galleryItems: widget.galleryItems,
content: content!,
),
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/services/notification_service.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 {
const ChatMediaEntry({
@ -16,11 +17,13 @@ class ChatMediaEntry extends StatelessWidget {
required this.message,
required this.contact,
required this.content,
required this.galleryItems,
});
final Message message;
final Contact contact;
final MessageContent content;
final List<GalleryItem> galleryItems;
@override
Widget build(BuildContext context) {
@ -70,15 +73,8 @@ class ChatMediaEntry extends StatelessWidget {
}
}
},
child: Container(
child: SizedBox(
width: 150,
decoration: BoxDecoration(
border: Border.all(
color: color,
width: 1.0,
),
borderRadius: BorderRadius.circular(12.0),
),
child: Align(
alignment: Alignment.centerRight,
child: ClipRRect(
@ -86,6 +82,8 @@ class ChatMediaEntry extends StatelessWidget {
child: InChatMediaViewer(
message: message,
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/model/json/message.dart';
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';
class ChatMediaViewerFullScreen extends StatefulWidget {
const ChatMediaViewerFullScreen(
{super.key, required this.message, required this.contact});
final Message message;
final Contact contact;
// class ChatMediaViewerFullScreen extends StatefulWidget {
// const ChatMediaViewerFullScreen({
// super.key,
// required this.message,
// required this.contact,
// required this.color,
// });
// final Message message;
// final Contact contact;
// final Color color;
@override
State<ChatMediaViewerFullScreen> createState() =>
_ChatMediaViewerFullScreenState();
}
// @override
// State<ChatMediaViewerFullScreen> createState() =>
// _ChatMediaViewerFullScreenState();
// }
class _ChatMediaViewerFullScreenState extends State<ChatMediaViewerFullScreen> {
bool hideMediaFile = false;
// class _ChatMediaViewerFullScreenState extends State<ChatMediaViewerFullScreen> {
// bool hideMediaFile = false;
Future deleteFiles(context) async {
bool confirmed = await showAlertDialog(
context, "Are you sure?", "The image will be irrevocably deleted.");
// Future deleteFiles(context) async {
// bool confirmed = await showAlertDialog(
// context, "Are you sure?", "The image will be irrevocably deleted.");
if (!confirmed) return;
// if (!confirmed) return;
await twonlyDatabase.messagesDao.updateMessageByMessageId(
widget.message.messageId,
MessagesCompanion(mediaStored: Value(false)),
);
await send.purgeSendMediaFiles();
await received.purgeReceivedMediaFiles();
if (context.mounted) {
Navigator.pop(context, true);
}
}
// await twonlyDatabase.messagesDao.updateMessageByMessageId(
// widget.message.messageId,
// MessagesCompanion(mediaStored: Value(false)),
// );
// await send.purgeSendMediaFiles();
// await received.purgeReceivedMediaFiles();
// if (context.mounted) {
// Navigator.pop(context, true);
// }
// }
@override
Widget build(BuildContext context) {
return Scaffold(
body: MediaViewSizing(
bottomNavigation: Positioned(
bottom: 10,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton.outlined(
onPressed: () {
deleteFiles(context);
},
icon: FaIcon(FontAwesomeIcons.trashCan),
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
),
),
),
IconButton.filled(
icon: FaIcon(FontAwesomeIcons.camera),
onPressed: () async {
setState(() {
hideMediaFile = true;
});
await Navigator.push(context, MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.contact);
},
));
setState(() {
hideMediaFile = false;
});
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
backgroundColor: WidgetStateProperty.all<Color>(
Theme.of(context).colorScheme.primary,
)),
),
],
),
),
child: (hideMediaFile)
? Container()
: InChatMediaViewer(
message: widget.message,
contact: widget.contact,
isInFullscreen: true,
),
),
);
}
}
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// body: MediaViewSizing(
// bottomNavigation: Positioned(
// bottom: 10,
// left: 0,
// right: 0,
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// children: [
// IconButton.outlined(
// onPressed: () {
// deleteFiles(context);
// },
// icon: FaIcon(FontAwesomeIcons.trashCan),
// style: ButtonStyle(
// padding: WidgetStateProperty.all<EdgeInsets>(
// EdgeInsets.symmetric(vertical: 10, horizontal: 20),
// ),
// ),
// ),
// IconButton.filled(
// icon: FaIcon(FontAwesomeIcons.camera),
// onPressed: () async {
// setState(() {
// hideMediaFile = true;
// });
// await Navigator.push(context, MaterialPageRoute(
// builder: (context) {
// return CameraSendToView(widget.contact);
// },
// ));
// setState(() {
// hideMediaFile = false;
// });
// },
// style: ButtonStyle(
// padding: WidgetStateProperty.all<EdgeInsets>(
// EdgeInsets.symmetric(vertical: 10, horizontal: 30),
// ),
// backgroundColor: WidgetStateProperty.all<Color>(
// Theme.of(context).colorScheme.primary,
// )),
// ),
// ],
// ),
// ),
// child: (hideMediaFile)
// ? Container()
// : Hero(
// tag: "chat_entry_${widget.message.messageId}",
// child: InChatMediaViewer(
// message: widget.message,
// contact: widget.contact,
// color: widget.color,
// isInFullscreen: true,
// ),
// )),
// );
// }
// }
class InChatMediaViewer extends StatefulWidget {
const InChatMediaViewer({
super.key,
required this.message,
required this.contact,
this.isInFullscreen = false,
required this.color,
required this.galleryItems,
});
final Message message;
final Contact contact;
final bool isInFullscreen;
final List<GalleryItem> galleryItems;
final Color color;
@override
State<InChatMediaViewer> createState() => _InChatMediaViewerState();
@ -184,13 +195,10 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
}
videoController = VideoPlayerController.file(
videoPath,
videoPlayerOptions:
VideoPlayerOptions(mixWithOthers: !widget.isInFullscreen),
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
videoController?.initialize().then((_) {
if (!widget.isInFullscreen) {
videoController!.setVolume(0);
}
videoController!.play();
videoController!.setLooping(true);
});
@ -209,40 +217,72 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
}
Future onTap() async {
bool? removed = await Navigator.push(
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return ChatMediaViewerFullScreen(
message: widget.message,
contact: widget.contact,
);
}),
MaterialPageRoute(
builder: (context) => GalleryPhotoViewWrapper(
galleryItems: widget.galleryItems,
// 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) {
image = null;
videoController?.dispose();
videoController = null;
if (isMounted) setState(() {});
}
// if (removed != null && removed) {
// image = null;
// videoController?.dispose();
// videoController = null;
// if (isMounted) setState(() {});
// }
}
@override
Widget build(BuildContext context) {
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),
child: MessageSendStateIcon(
[widget.message],
mainAxisAlignment: MainAxisAlignment.center,
),
),
);
}
return GestureDetector(
onTap:
((image == null && videoController == null) || widget.isInFullscreen)
? null
: onTap,
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.transparent,
width: 1.0,
),
color: Colors.transparent,
borderRadius: BorderRadius.circular(12.0),
),
child: GestureDetector(
onTap: ((image == null && videoController == null)) ? null : onTap,
child: Stack(
children: [
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/utils/misc.dart';
import 'package:twonly/src/views/contact/contact_view.dart';
import 'package:twonly/src/views/gallery/gallery_main_view.dart';
Color getMessageColor(Message message) {
return (message.messageOtherId == null)
@ -43,6 +44,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
late StreamSubscription<Contact> userSub;
late StreamSubscription<List<Message>> messageSub;
List<Message> messages = [];
List<GalleryItem> galleryItems = [];
Map<int, List<Message>> textReactionsToMessageId = {};
Map<int, List<Message>> emojiReactionsToMessageId = {};
Message? responseToMessage;
@ -76,7 +78,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Stream<List<Message>> msgStream =
twonlyDatabase.messagesDao.watchAllMessagesFrom(widget.contact.userId);
messageSub = msgStream.listen((msgs) {
messageSub = msgStream.listen((msgs) async {
// if (!context.mounted) return;
if (Platform.isAndroid) {
flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
@ -131,6 +133,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
displayedMessages.add(msg);
}
}
if (openedMessageOtherIds.isNotEmpty) {
notifyContactAboutOpeningMessage(
widget.contact.userId, openedMessageOtherIds);
@ -145,7 +148,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
emojiReactionsToMessageId = tmpEmojiReactionsToMessageId;
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()),
messages[i],
user,
galleryItems,
isLastMessageFromSameUser(messages, i),
textReactionsToMessageId[messages[i].messageId] ?? [],
emojiReactionsToMessageId[messages[i].messageId] ?? [],

View file

@ -26,6 +26,40 @@ class GalleryItem {
final DateTime date;
final File? imagePath;
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 {
@ -180,40 +214,12 @@ class GalleryMainViewState extends State<GalleryMainView> {
List<Message> storedMediaFiles =
await twonlyDatabase.messagesDao.getAllStoredMediaFiles();
Map<int, GalleryItem> items = {};
for (final message in 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);
}
Map<int, GalleryItem> items =
await GalleryItem.convertFromMessages(storedMediaFiles);
// Group items by month
orderedByMonth = {};
months = [];
String lastMonth = "";
galleryItems = await loadMemoriesDirectory();
galleryItems += items.values.toList();