mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:38:41 +00:00
parent
63fa506fde
commit
0845701e16
15 changed files with 679 additions and 505 deletions
|
|
@ -52,7 +52,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
t.mediaStored.equals(true) |
|
||||
t.openedAt.isBiggerThanValue(
|
||||
DateTime.now().subtract(const Duration(days: 1)))))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
|
||||
..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class MemoryItem {
|
|||
final isSend = message.messageOtherId == null;
|
||||
final id = message.mediaUploadId ?? message.messageId;
|
||||
final basePath = await send.getMediaFilePath(
|
||||
isSend ? message.mediaUploadId! : message.messageId,
|
||||
id,
|
||||
isSend ? 'send' : 'received',
|
||||
);
|
||||
File? imagePath;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ import 'package:twonly/src/services/api/messages.dart';
|
|||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/user_context_menu.dart';
|
||||
|
|
@ -31,6 +33,31 @@ Color getMessageColor(Message message) {
|
|||
: const Color.fromARGB(233, 68, 137, 255);
|
||||
}
|
||||
|
||||
class ChatMessage {
|
||||
ChatMessage({required this.message, required this.responseTo});
|
||||
final Message message;
|
||||
final Message? responseTo;
|
||||
}
|
||||
|
||||
class ChatItem {
|
||||
const ChatItem._({this.message, this.date, this.time});
|
||||
factory ChatItem.date(DateTime date) {
|
||||
return ChatItem._(date: date);
|
||||
}
|
||||
factory ChatItem.time(DateTime time) {
|
||||
return ChatItem._(time: time);
|
||||
}
|
||||
factory ChatItem.message(ChatMessage message) {
|
||||
return ChatItem._(message: message);
|
||||
}
|
||||
final ChatMessage? message;
|
||||
final DateTime? date;
|
||||
final DateTime? time;
|
||||
bool get isMessage => message != null;
|
||||
bool get isDate => date != null;
|
||||
bool get isTime => time != null;
|
||||
}
|
||||
|
||||
/// Displays detailed information about a SampleItem.
|
||||
class ChatMessagesView extends StatefulWidget {
|
||||
const ChatMessagesView(this.contact, {super.key});
|
||||
|
|
@ -48,9 +75,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
String currentInputText = '';
|
||||
late StreamSubscription<Contact?> userSub;
|
||||
late StreamSubscription<List<Message>> messageSub;
|
||||
List<Message> messages = [];
|
||||
List<ChatItem> messages = [];
|
||||
List<MemoryItem> galleryItems = [];
|
||||
Map<int, List<Message>> textReactionsToMessageId = {};
|
||||
Map<int, List<Message>> emojiReactionsToMessageId = {};
|
||||
Message? responseToMessage;
|
||||
GlobalKey verifyShieldKey = GlobalKey();
|
||||
|
|
@ -92,61 +118,85 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
|
||||
final msgStream =
|
||||
twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId);
|
||||
messageSub = msgStream.listen((msgs) async {
|
||||
messageSub = msgStream.listen((newMessages) async {
|
||||
// if (!context.mounted) return;
|
||||
if (Platform.isAndroid) {
|
||||
await flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
|
||||
} else {
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
}
|
||||
final displayedMessages = <Message>[];
|
||||
// should be cleared
|
||||
final tmpTextReactionsToMessageId = <int, List<Message>>{};
|
||||
final chatItems = <ChatItem>[];
|
||||
final storedMediaFiles = <Message>[];
|
||||
DateTime? lastDate;
|
||||
final tmpEmojiReactionsToMessageId = <int, List<Message>>{};
|
||||
|
||||
final openedMessageOtherIds = <int>[];
|
||||
|
||||
final messageOtherMessageIdToMyMessageId = <int, int>{};
|
||||
final messageIdToMessage = <int, Message>{};
|
||||
|
||||
/// there is probably a better way...
|
||||
for (final msg in msgs) {
|
||||
for (final msg in newMessages) {
|
||||
if (msg.messageOtherId != null) {
|
||||
messageOtherMessageIdToMyMessageId[msg.messageOtherId!] =
|
||||
msg.messageId;
|
||||
}
|
||||
messageIdToMessage[msg.messageId] = msg;
|
||||
}
|
||||
|
||||
for (final msg in msgs) {
|
||||
for (final msg in newMessages) {
|
||||
if (msg.kind == MessageKind.textMessage &&
|
||||
msg.messageOtherId != null &&
|
||||
msg.openedAt == null) {
|
||||
openedMessageOtherIds.add(msg.messageOtherId!);
|
||||
}
|
||||
|
||||
Message? responseTo;
|
||||
|
||||
if (msg.kind == MessageKind.media && msg.mediaStored) {
|
||||
storedMediaFiles.add(msg);
|
||||
}
|
||||
|
||||
final responseId = msg.responseToMessageId ??
|
||||
messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId];
|
||||
|
||||
var isReaction = false;
|
||||
if (responseId != null) {
|
||||
var added = false;
|
||||
responseTo = messageIdToMessage[responseId];
|
||||
final content = MessageContent.fromJson(
|
||||
msg.kind,
|
||||
jsonDecode(msg.contentJson!) as Map,
|
||||
);
|
||||
if (content is TextMessageContent) {
|
||||
if (content.text.isNotEmpty && !isEmoji(content.text)) {
|
||||
added = true;
|
||||
tmpTextReactionsToMessageId
|
||||
if (isEmoji(content.text)) {
|
||||
isReaction = true;
|
||||
tmpEmojiReactionsToMessageId
|
||||
.putIfAbsent(responseId, () => [])
|
||||
.add(msg);
|
||||
}
|
||||
}
|
||||
if (!added) {
|
||||
if (msg.kind == MessageKind.reopenedMedia) {
|
||||
isReaction = true;
|
||||
tmpEmojiReactionsToMessageId
|
||||
.putIfAbsent(responseId, () => [])
|
||||
.add(msg);
|
||||
}
|
||||
} else {
|
||||
displayedMessages.add(msg);
|
||||
}
|
||||
if (!isReaction) {
|
||||
if (lastDate == null ||
|
||||
msg.sendAt.day != lastDate.day ||
|
||||
msg.sendAt.month != lastDate.month ||
|
||||
msg.sendAt.year != lastDate.year) {
|
||||
chatItems.add(ChatItem.date(msg.sendAt));
|
||||
lastDate = msg.sendAt;
|
||||
} else if (msg.sendAt.difference(lastDate).inMinutes >= 20) {
|
||||
chatItems.add(ChatItem.time(msg.sendAt));
|
||||
lastDate = msg.sendAt;
|
||||
}
|
||||
chatItems.add(ChatItem.message(ChatMessage(
|
||||
message: msg,
|
||||
responseTo: responseTo,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,17 +211,11 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
.openedAllNonMediaMessages(widget.contact.userId);
|
||||
|
||||
setState(() {
|
||||
textReactionsToMessageId = tmpTextReactionsToMessageId;
|
||||
emojiReactionsToMessageId = tmpEmojiReactionsToMessageId;
|
||||
messages = displayedMessages;
|
||||
messages = chatItems.reversed.toList();
|
||||
});
|
||||
|
||||
final filteredMediaFiles = displayedMessages
|
||||
.where((x) => x.kind == MessageKind.media && x.mediaStored)
|
||||
.toList()
|
||||
.reversed
|
||||
.toList();
|
||||
final items = await MemoryItem.convertFromMessages(filteredMediaFiles);
|
||||
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
||||
galleryItems = items.values.toList();
|
||||
setState(() {});
|
||||
});
|
||||
|
|
@ -204,56 +248,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
Widget getResponsePreview(Message message) {
|
||||
String? subtitle;
|
||||
|
||||
if (message.kind == MessageKind.textMessage) {
|
||||
if (message.contentJson != null) {
|
||||
final content = MessageContent.fromJson(
|
||||
MessageKind.textMessage, jsonDecode(message.contentJson!) as Map);
|
||||
if (content is TextMessageContent) {
|
||||
subtitle = truncateString(content.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.kind == MessageKind.media) {
|
||||
final content = MessageContent.fromJson(
|
||||
MessageKind.media, jsonDecode(message.contentJson!) as Map);
|
||||
if (content is MediaMessageContent) {
|
||||
subtitle = content.isVideo ? 'Video' : 'Image';
|
||||
}
|
||||
}
|
||||
|
||||
var username = 'You';
|
||||
if (message.messageOtherId != null) {
|
||||
username = getContactDisplayName(widget.contact);
|
||||
}
|
||||
|
||||
final color = getMessageColor(message);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: color,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
username,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (subtitle != null) Text(subtitle)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
|
|
@ -296,70 +290,37 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: messages.length + 1,
|
||||
reverse: true,
|
||||
itemExtentBuilder: (index, dimensions) {
|
||||
if (index == 0) return 10; // empty padding
|
||||
index -= 1;
|
||||
double size = 44;
|
||||
if (messages[index].kind == MessageKind.textMessage) {
|
||||
final content = TextMessageContent.fromJson(
|
||||
jsonDecode(messages[index].contentJson!) as Map);
|
||||
if (EmojiAnimation.supported(content.text)) {
|
||||
size = 99;
|
||||
} else {
|
||||
size = 11 +
|
||||
calculateNumberOfLines(
|
||||
content.text,
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
17) *
|
||||
27;
|
||||
}
|
||||
}
|
||||
if (messages[index].mediaStored) {
|
||||
size = 271;
|
||||
}
|
||||
final reactions =
|
||||
textReactionsToMessageId[messages[index].messageId];
|
||||
if (reactions != null && reactions.isNotEmpty) {
|
||||
for (final reaction in reactions) {
|
||||
if (reaction.kind == MessageKind.textMessage) {
|
||||
final content = TextMessageContent.fromJson(
|
||||
jsonDecode(reaction.contentJson!) as Map);
|
||||
size += calculateNumberOfLines(
|
||||
content.text,
|
||||
MediaQuery.of(context).size.width * 0.5,
|
||||
14) *
|
||||
27;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLastMessageFromSameUser(messages, index)) {
|
||||
size += 20;
|
||||
}
|
||||
return size;
|
||||
},
|
||||
itemCount: messages.length + 1,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
return Container(); // just a padding
|
||||
if (i == messages.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsetsGeometry.only(top: 10),
|
||||
);
|
||||
}
|
||||
if (messages[i].isDate || messages[i].isTime) {
|
||||
return ChatDateChip(
|
||||
item: messages[i],
|
||||
);
|
||||
} else {
|
||||
final chatMessage = messages[i].message!;
|
||||
return ChatListEntry(
|
||||
key: Key(chatMessage.message.messageId.toString()),
|
||||
chatMessage,
|
||||
user,
|
||||
galleryItems,
|
||||
isLastMessageFromSameUser(messages, i),
|
||||
emojiReactionsToMessageId[
|
||||
chatMessage.message.messageId] ??
|
||||
[],
|
||||
onResponseTriggered: () {
|
||||
setState(() {
|
||||
responseToMessage = chatMessage.message;
|
||||
});
|
||||
textFieldFocus.requestFocus();
|
||||
},
|
||||
);
|
||||
}
|
||||
i -= 1;
|
||||
return ChatListEntry(
|
||||
key: Key(messages[i].messageId.toString()),
|
||||
messages[i],
|
||||
user,
|
||||
galleryItems,
|
||||
isLastMessageFromSameUser(messages, i),
|
||||
textReactionsToMessageId[messages[i].messageId] ?? [],
|
||||
emojiReactionsToMessageId[messages[i].messageId] ?? [],
|
||||
onResponseTriggered: (message) {
|
||||
setState(() {
|
||||
responseToMessage = message;
|
||||
});
|
||||
textFieldFocus.requestFocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -372,7 +333,13 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: getResponsePreview(responseToMessage!)),
|
||||
Expanded(
|
||||
child: ResponsePreview(
|
||||
message: responseToMessage!,
|
||||
showBorder: true,
|
||||
contact: user,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
|
|
@ -449,7 +416,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
}
|
||||
}
|
||||
|
||||
bool isLastMessageFromSameUser(List<Message> messages, int index) {
|
||||
bool isLastMessageFromSameUser(List<ChatItem> messages, int index) {
|
||||
if (index <= 0) {
|
||||
return true; // If there is no previous message, return true
|
||||
}
|
||||
|
|
@ -457,11 +424,14 @@ bool isLastMessageFromSameUser(List<Message> messages, int index) {
|
|||
final lastMessage = messages[index - 1];
|
||||
final currentMessage = messages[index];
|
||||
|
||||
// Check if both messages have the same messageOtherId (or both are null)
|
||||
return (lastMessage.messageOtherId == null &&
|
||||
currentMessage.messageOtherId == null) ||
|
||||
(lastMessage.messageOtherId != null &&
|
||||
currentMessage.messageOtherId != null);
|
||||
if (lastMessage.isMessage && currentMessage.isMessage) {
|
||||
// Check if both messages have the same messageOtherId (or both are null)
|
||||
return (lastMessage.message!.message.messageOtherId == null &&
|
||||
currentMessage.message!.message.messageOtherId == null) ||
|
||||
(lastMessage.message!.message.messageOtherId != null &&
|
||||
currentMessage.message!.message.messageOtherId != null);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
double calculateNumberOfLines(String text, double width, double fontSize) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
|
||||
class ChatDateChip extends StatelessWidget {
|
||||
const ChatDateChip({required this.item, super.key});
|
||||
final ChatItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var formattedDate = item.isTime
|
||||
? DateFormat.Hm(Localizations.localeOf(context).toLanguageTag())
|
||||
.format(item.time!)
|
||||
: '${DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()).format(item.date!)} ${DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}';
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(40),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Text(
|
||||
formattedDate,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||
|
||||
class ChatListEntry extends StatefulWidget {
|
||||
const ChatListEntry(
|
||||
this.msg,
|
||||
this.contact,
|
||||
this.galleryItems,
|
||||
this.lastMessageFromSameUser,
|
||||
this.otherReactions, {
|
||||
required this.onResponseTriggered,
|
||||
super.key,
|
||||
});
|
||||
final ChatMessage msg;
|
||||
final Contact contact;
|
||||
final bool lastMessageFromSameUser;
|
||||
final List<Message> otherReactions;
|
||||
final List<MemoryItem> galleryItems;
|
||||
final void Function() 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.msg.message.kind,
|
||||
jsonDecode(widget.msg.message.contentJson!) as Map);
|
||||
if (msgContent is TextMessageContent) {
|
||||
textMessage = msgContent.text;
|
||||
}
|
||||
content = msgContent;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (content == null) return Container();
|
||||
final right = widget.msg.message.messageOtherId == null;
|
||||
|
||||
return Align(
|
||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: widget.lastMessageFromSameUser
|
||||
? const EdgeInsets.only(top: 5, right: 10, left: 10)
|
||||
: const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
|
||||
child: MessageContextMenu(
|
||||
message: widget.msg.message,
|
||||
onResponseTriggered: widget.onResponseTriggered,
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
right ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
MessageActions(
|
||||
message: widget.msg.message,
|
||||
onResponseTriggered: widget.onResponseTriggered,
|
||||
child: Stack(
|
||||
alignment:
|
||||
right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
children: [
|
||||
ResponseContainer(
|
||||
msg: widget.msg,
|
||||
contact: widget.contact,
|
||||
child: (textMessage != null)
|
||||
? ChatTextEntry(
|
||||
message: widget.msg.message,
|
||||
text: textMessage!,
|
||||
hasReaction: widget.otherReactions.isNotEmpty,
|
||||
)
|
||||
: ChatMediaEntry(
|
||||
message: widget.msg.message,
|
||||
contact: widget.contact,
|
||||
galleryItems: widget.galleryItems,
|
||||
content: content!,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
right: 5,
|
||||
child: ReactionRow(
|
||||
otherReactions: widget.otherReactions,
|
||||
message: widget.msg.message,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +59,51 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> onDoubleTap() async {
|
||||
if (widget.message.openedAt == null &&
|
||||
widget.message.messageOtherId != null ||
|
||||
widget.message.mediaStored) {
|
||||
return;
|
||||
}
|
||||
if (await received.existsMediaFile(widget.message.messageId, 'png')) {
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
widget.contact.userId,
|
||||
MessageJson(
|
||||
kind: MessageKind.reopenedMedia,
|
||||
messageSenderId: widget.message.messageId,
|
||||
content: ReopenedMediaFileContent(
|
||||
messageId: widget.message.messageOtherId!,
|
||||
),
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
pushNotification: PushNotification(
|
||||
kind: PushKind.reopenedMedia,
|
||||
),
|
||||
);
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
widget.message.messageId,
|
||||
const MessagesCompanion(openedAt: Value(null)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onTap() async {
|
||||
if (widget.message.downloadState == DownloadState.downloaded &&
|
||||
widget.message.openedAt == null) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return MediaViewerView(widget.contact,
|
||||
initialMessage: widget.message);
|
||||
}),
|
||||
);
|
||||
await checkIfTutorialCanBeShown();
|
||||
} else if (widget.message.downloadState == DownloadState.pending) {
|
||||
await received.startDownloadMedia(widget.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = getMessageColorFromType(
|
||||
|
|
@ -68,53 +113,11 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
|
||||
return GestureDetector(
|
||||
key: reopenMediaFile,
|
||||
onDoubleTap: () async {
|
||||
if (widget.message.openedAt == null &&
|
||||
widget.message.messageOtherId != null ||
|
||||
widget.message.mediaStored) {
|
||||
return;
|
||||
}
|
||||
if (await received.existsMediaFile(widget.message.messageId, 'png')) {
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
widget.contact.userId,
|
||||
MessageJson(
|
||||
kind: MessageKind.reopenedMedia,
|
||||
messageSenderId: widget.message.messageId,
|
||||
content: ReopenedMediaFileContent(
|
||||
messageId: widget.message.messageOtherId!,
|
||||
),
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
pushNotification: PushNotification(
|
||||
kind: PushKind.reopenedMedia,
|
||||
),
|
||||
);
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
widget.message.messageId,
|
||||
const MessagesCompanion(openedAt: Value(null)),
|
||||
);
|
||||
}
|
||||
},
|
||||
onTap: () async {
|
||||
if (widget.message.kind == MessageKind.media) {
|
||||
if (widget.message.downloadState == DownloadState.downloaded &&
|
||||
widget.message.openedAt == null) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return MediaViewerView(widget.contact,
|
||||
initialMessage: widget.message);
|
||||
}),
|
||||
);
|
||||
await checkIfTutorialCanBeShown();
|
||||
} else if (widget.message.downloadState == DownloadState.pending) {
|
||||
await received.startDownloadMedia(widget.message, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDoubleTap: onDoubleTap,
|
||||
onTap: widget.message.kind == MessageKind.media ? onTap : null,
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
height: widget.message.mediaStored ? 271 : null,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ClipRRect(
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_response_columns.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart';
|
||||
|
||||
class ChatListEntry extends StatefulWidget {
|
||||
const ChatListEntry(
|
||||
this.message,
|
||||
this.contact,
|
||||
this.galleryItems,
|
||||
this.lastMessageFromSameUser,
|
||||
this.textReactions,
|
||||
this.otherReactions, {
|
||||
required this.onResponseTriggered,
|
||||
super.key,
|
||||
});
|
||||
final Message message;
|
||||
final Contact contact;
|
||||
final bool lastMessageFromSameUser;
|
||||
final List<Message> textReactions;
|
||||
final List<Message> otherReactions;
|
||||
final List<MemoryItem> galleryItems;
|
||||
final void 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!) as Map);
|
||||
if (msgContent is TextMessageContent) {
|
||||
textMessage = msgContent.text;
|
||||
}
|
||||
content = msgContent;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (content == null) return Container();
|
||||
final right = widget.message.messageOtherId == null;
|
||||
|
||||
return Align(
|
||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: widget.lastMessageFromSameUser
|
||||
? const EdgeInsets.only(top: 5, right: 10, left: 10)
|
||||
: const 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: [
|
||||
MessageActions(
|
||||
message: widget.message,
|
||||
child: Stack(
|
||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
children: [
|
||||
if (textMessage != null)
|
||||
ChatTextEntry(
|
||||
message: widget.message,
|
||||
text: textMessage!,
|
||||
)
|
||||
else
|
||||
ChatMediaEntry(
|
||||
message: widget.message,
|
||||
contact: widget.contact,
|
||||
galleryItems: widget.galleryItems,
|
||||
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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
|
||||
class ReactionRow extends StatefulWidget {
|
||||
|
|
@ -26,7 +27,7 @@ class _ReactionRowState extends State<ReactionRow> {
|
|||
final children = <Widget>[];
|
||||
var hasOneTextReaction = false;
|
||||
var hasOneReopened = false;
|
||||
for (final reaction in widget.otherReactions) {
|
||||
for (final reaction in widget.otherReactions.reversed) {
|
||||
final content = MessageContent.fromJson(
|
||||
reaction.kind, jsonDecode(reaction.contentJson!) as Map);
|
||||
|
||||
|
|
@ -34,15 +35,15 @@ class _ReactionRowState extends State<ReactionRow> {
|
|||
if (hasOneReopened) continue;
|
||||
hasOneReopened = true;
|
||||
children.add(
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 3),
|
||||
padding: const EdgeInsets.only(right: 3),
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.repeat,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -78,7 +79,7 @@ class _ReactionRowState extends State<ReactionRow> {
|
|||
|
||||
return Row(
|
||||
mainAxisAlignment: widget.message.messageOtherId == null
|
||||
? MainAxisAlignment.start
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.end,
|
||||
children: children,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,10 +5,16 @@ import 'package:twonly/src/views/components/animate_icon.dart';
|
|||
import 'package:twonly/src/views/components/better_text.dart';
|
||||
|
||||
class ChatTextEntry extends StatelessWidget {
|
||||
const ChatTextEntry({required this.message, required this.text, super.key});
|
||||
const ChatTextEntry({
|
||||
required this.message,
|
||||
required this.text,
|
||||
required this.hasReaction,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final Message message;
|
||||
final bool hasReaction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -28,9 +34,12 @@ class ChatTextEntry extends StatelessWidget {
|
|||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 10),
|
||||
padding: EdgeInsets.only(
|
||||
left: 10, top: 4, bottom: 4, right: hasReaction ? 30 : 10),
|
||||
decoration: BoxDecoration(
|
||||
color: getMessageColor(message),
|
||||
color: message.responseToMessageId == null
|
||||
? getMessageColor(message)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: BetterText(text: text),
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
|
||||
class ChatTextResponseColumns extends StatelessWidget {
|
||||
const ChatTextResponseColumns({
|
||||
required this.textReactions,
|
||||
required this.right,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Message> textReactions;
|
||||
final bool right;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = <Widget>[];
|
||||
for (final reaction in textReactions) {
|
||||
final content = MessageContent.fromJson(
|
||||
reaction.kind, jsonDecode(reaction.contentJson!) as Map);
|
||||
|
||||
if (content is TextMessageContent) {
|
||||
var entries = [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 10,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.5,
|
||||
),
|
||||
child: Text(
|
||||
content.text,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
textAlign: right ? TextAlign.right : TextAlign.left,
|
||||
)),
|
||||
];
|
||||
if (right) {
|
||||
entries = entries.reversed.toList();
|
||||
}
|
||||
|
||||
final color = getMessageColor(reaction);
|
||||
|
||||
children.insert(
|
||||
0,
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 5, right: 10, left: 10),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: entries,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (children.isEmpty) return Container();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (galleryItemIndex == null) {
|
||||
if (!widget.message.mediaStored) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 39,
|
||||
|
|
@ -138,10 +138,12 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: MemoriesItemThumbnail(
|
||||
galleryItem: widget.galleryItems[galleryItemIndex!],
|
||||
onTap: onTap,
|
||||
),
|
||||
child: galleryItemIndex != null
|
||||
? MemoriesItemThumbnail(
|
||||
galleryItem: widget.galleryItems[galleryItemIndex!],
|
||||
onTap: onTap,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
|
||||
class MessageActions extends StatefulWidget {
|
||||
const MessageActions({
|
||||
|
|
@ -70,11 +59,7 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
|
|||
child: GestureDetector(
|
||||
onHorizontalDragUpdate: _onHorizontalDragUpdate,
|
||||
onHorizontalDragEnd: _onHorizontalDragEnd,
|
||||
child: MessageContextMenu(
|
||||
message: widget.message,
|
||||
onResponseTriggered: widget.onResponseTriggered,
|
||||
child: widget.child,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
if (_offsetX >= 40)
|
||||
|
|
@ -97,100 +82,3 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageContextMenu extends StatelessWidget {
|
||||
const MessageContextMenu({
|
||||
required this.message,
|
||||
required this.child,
|
||||
required this.onResponseTriggered,
|
||||
super.key,
|
||||
});
|
||||
final Widget child;
|
||||
final Message message;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
onPressed: () => (),
|
||||
onToggle: (menuOpen) {
|
||||
if (menuOpen) {
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
},
|
||||
actions: [
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.react),
|
||||
onSelect: () async {
|
||||
final layer = await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black,
|
||||
builder: (BuildContext context) {
|
||||
return const Emojis();
|
||||
},
|
||||
) as TextLayerData?;
|
||||
if (layer == null) return;
|
||||
Log.info(layer.text);
|
||||
|
||||
await sendTextMessage(
|
||||
message.contactId,
|
||||
TextMessageContent(
|
||||
text: layer.text,
|
||||
responseToMessageId: message.messageOtherId,
|
||||
responseToOtherMessageId: (message.messageOtherId == null)
|
||||
? message.messageId
|
||||
: null),
|
||||
(message.messageOtherId != null)
|
||||
? PushNotification(
|
||||
kind: (message.kind == MessageKind.textMessage)
|
||||
? PushKind.reactionToText
|
||||
: (getMediaContent(message)!.isVideo)
|
||||
? PushKind.reactionToVideo
|
||||
: PushKind.reactionToImage,
|
||||
reactionContent: layer.text,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.faceLaugh),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.reply),
|
||||
onSelect: onResponseTriggered,
|
||||
child: const FaIcon(FontAwesomeIcons.reply),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.copy),
|
||||
onSelect: () {
|
||||
final text = getMessageText(message);
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidCopy),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.delete),
|
||||
onSelect: () async {
|
||||
final delete = await showAlertDialog(
|
||||
context,
|
||||
context.lang.deleteTitle,
|
||||
null,
|
||||
customOk: context.lang.deleteOkBtn,
|
||||
);
|
||||
if (delete) {
|
||||
await twonlyDB.messagesDao
|
||||
.deleteMessagesByMessageId(message.messageId);
|
||||
}
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.trash),
|
||||
),
|
||||
// PieAction(
|
||||
// tooltip: Text(context.lang.info),
|
||||
// onSelect: () {},
|
||||
// child: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||
// ),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_function_invocation
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
|
||||
class MessageContextMenu extends StatelessWidget {
|
||||
const MessageContextMenu({
|
||||
required this.message,
|
||||
required this.child,
|
||||
required this.onResponseTriggered,
|
||||
super.key,
|
||||
});
|
||||
final Widget child;
|
||||
final Message message;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
onPressed: () => (),
|
||||
onToggle: (menuOpen) {
|
||||
if (menuOpen) {
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
},
|
||||
actions: [
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.react),
|
||||
onSelect: () async {
|
||||
final layer = await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black,
|
||||
builder: (BuildContext context) {
|
||||
return const Emojis();
|
||||
},
|
||||
) as EmojiLayerData?;
|
||||
if (layer == null) return;
|
||||
Log.info(layer.text);
|
||||
|
||||
await sendTextMessage(
|
||||
message.contactId,
|
||||
TextMessageContent(
|
||||
text: layer.text,
|
||||
responseToMessageId: message.messageOtherId,
|
||||
responseToOtherMessageId: (message.messageOtherId == null)
|
||||
? message.messageId
|
||||
: null),
|
||||
(message.messageOtherId != null)
|
||||
? PushNotification(
|
||||
kind: (message.kind == MessageKind.textMessage)
|
||||
? PushKind.reactionToText
|
||||
: (getMediaContent(message)!.isVideo)
|
||||
? PushKind.reactionToVideo
|
||||
: PushKind.reactionToImage,
|
||||
reactionContent: layer.text,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.faceLaugh),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.reply),
|
||||
onSelect: onResponseTriggered,
|
||||
child: const FaIcon(FontAwesomeIcons.reply),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.copy),
|
||||
onSelect: () {
|
||||
final text = getMessageText(message);
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidCopy),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.delete),
|
||||
onSelect: () async {
|
||||
final delete = await showAlertDialog(
|
||||
context,
|
||||
context.lang.deleteTitle,
|
||||
null,
|
||||
customOk: context.lang.deleteOkBtn,
|
||||
);
|
||||
if (delete) {
|
||||
await twonlyDB.messagesDao
|
||||
.deleteMessagesByMessageId(message.messageId);
|
||||
}
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.trash),
|
||||
),
|
||||
// PieAction(
|
||||
// tooltip: Text(context.lang.info),
|
||||
// onSelect: () {},
|
||||
// child: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||
// ),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
|
||||
class ResponseContainer extends StatefulWidget {
|
||||
const ResponseContainer({
|
||||
required this.msg,
|
||||
required this.contact,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ChatMessage msg;
|
||||
final Widget child;
|
||||
final Contact contact;
|
||||
|
||||
@override
|
||||
State<ResponseContainer> createState() => _ResponseContainerState();
|
||||
}
|
||||
|
||||
class _ResponseContainerState extends State<ResponseContainer> {
|
||||
double? minWidth;
|
||||
final GlobalKey _message = GlobalKey();
|
||||
final GlobalKey _preview = GlobalKey();
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final messageBox =
|
||||
_message.currentContext?.findRenderObject() as RenderBox?;
|
||||
final previewBox =
|
||||
_preview.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (messageBox == null || previewBox == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
if (messageBox.size.width > previewBox.size.width) {
|
||||
minWidth = messageBox.size.width;
|
||||
} else {
|
||||
minWidth = previewBox.size.width;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.msg.responseTo == null) {
|
||||
return widget.child;
|
||||
}
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getMessageColor(widget.msg.message),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, right: 4, left: 4),
|
||||
child: Container(
|
||||
key: _preview,
|
||||
width: minWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.surface.withAlpha(150),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(8),
|
||||
topLeft: Radius.circular(8),
|
||||
bottomLeft: Radius.circular(4),
|
||||
bottomRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: ResponsePreview(
|
||||
contact: widget.contact,
|
||||
message: widget.msg.responseTo!,
|
||||
showBorder: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
key: _message,
|
||||
width: minWidth,
|
||||
child: widget.child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ResponsePreview extends StatefulWidget {
|
||||
const ResponsePreview({
|
||||
required this.message,
|
||||
required this.contact,
|
||||
required this.showBorder,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final Contact contact;
|
||||
final bool showBorder;
|
||||
|
||||
@override
|
||||
State<ResponsePreview> createState() => _ResponsePreviewState();
|
||||
}
|
||||
|
||||
class _ResponsePreviewState extends State<ResponsePreview> {
|
||||
File? thumbnailPath;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final items = await MemoryItem.convertFromMessages([widget.message]);
|
||||
if (items.length == 1) {
|
||||
setState(() {
|
||||
thumbnailPath = items.values.first.thumbnailPath;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String? subtitle;
|
||||
|
||||
if (widget.message.kind == MessageKind.textMessage) {
|
||||
if (widget.message.contentJson != null) {
|
||||
final content = MessageContent.fromJson(MessageKind.textMessage,
|
||||
jsonDecode(widget.message.contentJson!) as Map);
|
||||
if (content is TextMessageContent) {
|
||||
subtitle = truncateString(content.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (widget.message.kind == MessageKind.media) {
|
||||
final content = MessageContent.fromJson(
|
||||
MessageKind.media, jsonDecode(widget.message.contentJson!) as Map);
|
||||
if (content is MediaMessageContent) {
|
||||
subtitle = content.isVideo ? 'Video' : 'Image';
|
||||
}
|
||||
}
|
||||
|
||||
var username = 'You';
|
||||
if (widget.message.messageOtherId != null) {
|
||||
username = getContactDisplayName(widget.contact);
|
||||
}
|
||||
|
||||
final color = getMessageColor(widget.message);
|
||||
|
||||
if (!widget.message.mediaStored) {
|
||||
return Container(
|
||||
padding: widget.showBorder
|
||||
? const EdgeInsets.only(left: 10, right: 10)
|
||||
: const EdgeInsets.symmetric(horizontal: 5),
|
||||
decoration: (widget.showBorder)
|
||||
? BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: color,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
username,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (subtitle != null) Text(subtitle)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: color,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
username,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (subtitle != null) Text(subtitle)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (thumbnailPath != null)
|
||||
SizedBox(
|
||||
height: widget.showBorder ? 100 : 210,
|
||||
child: Image.file(thumbnailPath!),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -179,7 +179,9 @@ PieTheme getPieCanvasTheme(BuildContext context) {
|
|||
iconColor: Theme.of(context).colorScheme.surfaceBright,
|
||||
),
|
||||
tooltipPadding: const EdgeInsets.all(20),
|
||||
overlayColor: const Color.fromARGB(69, 0, 0, 0),
|
||||
overlayColor: isDarkMode(context)
|
||||
? const Color.fromARGB(69, 0, 0, 0)
|
||||
: const Color.fromARGB(40, 0, 0, 0),
|
||||
// spacing: 0,
|
||||
tooltipTextStyle: const TextStyle(
|
||||
fontSize: 32,
|
||||
|
|
|
|||
Loading…
Reference in a new issue