mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-17 03:28:40 +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.mediaStored.equals(true) |
|
||||||
t.openedAt.isBiggerThanValue(
|
t.openedAt.isBiggerThanValue(
|
||||||
DateTime.now().subtract(const Duration(days: 1)))))
|
DateTime.now().subtract(const Duration(days: 1)))))
|
||||||
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
|
..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class MemoryItem {
|
||||||
final isSend = message.messageOtherId == null;
|
final isSend = message.messageOtherId == null;
|
||||||
final id = message.mediaUploadId ?? message.messageId;
|
final id = message.mediaUploadId ?? message.messageId;
|
||||||
final basePath = await send.getMediaFilePath(
|
final basePath = await send.getMediaFilePath(
|
||||||
isSend ? message.mediaUploadId! : message.messageId,
|
id,
|
||||||
isSend ? 'send' : 'received',
|
isSend ? 'send' : 'received',
|
||||||
);
|
);
|
||||||
File? imagePath;
|
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/services/notifications/background.notifications.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.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/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/animate_icon.dart';
|
||||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||||
import 'package:twonly/src/views/components/user_context_menu.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);
|
: 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.
|
/// Displays detailed information about a SampleItem.
|
||||||
class ChatMessagesView extends StatefulWidget {
|
class ChatMessagesView extends StatefulWidget {
|
||||||
const ChatMessagesView(this.contact, {super.key});
|
const ChatMessagesView(this.contact, {super.key});
|
||||||
|
|
@ -48,9 +75,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
String currentInputText = '';
|
String currentInputText = '';
|
||||||
late StreamSubscription<Contact?> userSub;
|
late StreamSubscription<Contact?> userSub;
|
||||||
late StreamSubscription<List<Message>> messageSub;
|
late StreamSubscription<List<Message>> messageSub;
|
||||||
List<Message> messages = [];
|
List<ChatItem> messages = [];
|
||||||
List<MemoryItem> galleryItems = [];
|
List<MemoryItem> galleryItems = [];
|
||||||
Map<int, List<Message>> textReactionsToMessageId = {};
|
|
||||||
Map<int, List<Message>> emojiReactionsToMessageId = {};
|
Map<int, List<Message>> emojiReactionsToMessageId = {};
|
||||||
Message? responseToMessage;
|
Message? responseToMessage;
|
||||||
GlobalKey verifyShieldKey = GlobalKey();
|
GlobalKey verifyShieldKey = GlobalKey();
|
||||||
|
|
@ -92,61 +118,85 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
|
|
||||||
final msgStream =
|
final msgStream =
|
||||||
twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId);
|
twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId);
|
||||||
messageSub = msgStream.listen((msgs) async {
|
messageSub = msgStream.listen((newMessages) async {
|
||||||
// if (!context.mounted) return;
|
// if (!context.mounted) return;
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
await flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
|
await flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
|
||||||
} else {
|
} else {
|
||||||
await flutterLocalNotificationsPlugin.cancelAll();
|
await flutterLocalNotificationsPlugin.cancelAll();
|
||||||
}
|
}
|
||||||
final displayedMessages = <Message>[];
|
final chatItems = <ChatItem>[];
|
||||||
// should be cleared
|
final storedMediaFiles = <Message>[];
|
||||||
final tmpTextReactionsToMessageId = <int, List<Message>>{};
|
DateTime? lastDate;
|
||||||
final tmpEmojiReactionsToMessageId = <int, List<Message>>{};
|
final tmpEmojiReactionsToMessageId = <int, List<Message>>{};
|
||||||
|
|
||||||
final openedMessageOtherIds = <int>[];
|
final openedMessageOtherIds = <int>[];
|
||||||
|
|
||||||
final messageOtherMessageIdToMyMessageId = <int, int>{};
|
final messageOtherMessageIdToMyMessageId = <int, int>{};
|
||||||
|
final messageIdToMessage = <int, Message>{};
|
||||||
|
|
||||||
/// there is probably a better way...
|
/// there is probably a better way...
|
||||||
for (final msg in msgs) {
|
for (final msg in newMessages) {
|
||||||
if (msg.messageOtherId != null) {
|
if (msg.messageOtherId != null) {
|
||||||
messageOtherMessageIdToMyMessageId[msg.messageOtherId!] =
|
messageOtherMessageIdToMyMessageId[msg.messageOtherId!] =
|
||||||
msg.messageId;
|
msg.messageId;
|
||||||
}
|
}
|
||||||
|
messageIdToMessage[msg.messageId] = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final msg in msgs) {
|
for (final msg in newMessages) {
|
||||||
if (msg.kind == MessageKind.textMessage &&
|
if (msg.kind == MessageKind.textMessage &&
|
||||||
msg.messageOtherId != null &&
|
msg.messageOtherId != null &&
|
||||||
msg.openedAt == null) {
|
msg.openedAt == null) {
|
||||||
openedMessageOtherIds.add(msg.messageOtherId!);
|
openedMessageOtherIds.add(msg.messageOtherId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message? responseTo;
|
||||||
|
|
||||||
|
if (msg.kind == MessageKind.media && msg.mediaStored) {
|
||||||
|
storedMediaFiles.add(msg);
|
||||||
|
}
|
||||||
|
|
||||||
final responseId = msg.responseToMessageId ??
|
final responseId = msg.responseToMessageId ??
|
||||||
messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId];
|
messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId];
|
||||||
|
|
||||||
|
var isReaction = false;
|
||||||
if (responseId != null) {
|
if (responseId != null) {
|
||||||
var added = false;
|
responseTo = messageIdToMessage[responseId];
|
||||||
final content = MessageContent.fromJson(
|
final content = MessageContent.fromJson(
|
||||||
msg.kind,
|
msg.kind,
|
||||||
jsonDecode(msg.contentJson!) as Map,
|
jsonDecode(msg.contentJson!) as Map,
|
||||||
);
|
);
|
||||||
if (content is TextMessageContent) {
|
if (content is TextMessageContent) {
|
||||||
if (content.text.isNotEmpty && !isEmoji(content.text)) {
|
if (isEmoji(content.text)) {
|
||||||
added = true;
|
isReaction = true;
|
||||||
tmpTextReactionsToMessageId
|
|
||||||
.putIfAbsent(responseId, () => [])
|
|
||||||
.add(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!added) {
|
|
||||||
tmpEmojiReactionsToMessageId
|
tmpEmojiReactionsToMessageId
|
||||||
.putIfAbsent(responseId, () => [])
|
.putIfAbsent(responseId, () => [])
|
||||||
.add(msg);
|
.add(msg);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
displayedMessages.add(msg);
|
if (msg.kind == MessageKind.reopenedMedia) {
|
||||||
|
isReaction = true;
|
||||||
|
tmpEmojiReactionsToMessageId
|
||||||
|
.putIfAbsent(responseId, () => [])
|
||||||
|
.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);
|
.openedAllNonMediaMessages(widget.contact.userId);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
textReactionsToMessageId = tmpTextReactionsToMessageId;
|
|
||||||
emojiReactionsToMessageId = tmpEmojiReactionsToMessageId;
|
emojiReactionsToMessageId = tmpEmojiReactionsToMessageId;
|
||||||
messages = displayedMessages;
|
messages = chatItems.reversed.toList();
|
||||||
});
|
});
|
||||||
|
|
||||||
final filteredMediaFiles = displayedMessages
|
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
||||||
.where((x) => x.kind == MessageKind.media && x.mediaStored)
|
|
||||||
.toList()
|
|
||||||
.reversed
|
|
||||||
.toList();
|
|
||||||
final items = await MemoryItem.convertFromMessages(filteredMediaFiles);
|
|
||||||
galleryItems = items.values.toList();
|
galleryItems = items.values.toList();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
|
|
@ -204,56 +248,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
setState(() {});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|
@ -296,70 +290,37 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: messages.length + 1,
|
|
||||||
reverse: true,
|
reverse: true,
|
||||||
itemExtentBuilder: (index, dimensions) {
|
itemCount: messages.length + 1,
|
||||||
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;
|
|
||||||
},
|
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
if (i == 0) {
|
if (i == messages.length) {
|
||||||
return Container(); // just a padding
|
return const Padding(
|
||||||
|
padding: EdgeInsetsGeometry.only(top: 10),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
i -= 1;
|
if (messages[i].isDate || messages[i].isTime) {
|
||||||
|
return ChatDateChip(
|
||||||
|
item: messages[i],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final chatMessage = messages[i].message!;
|
||||||
return ChatListEntry(
|
return ChatListEntry(
|
||||||
key: Key(messages[i].messageId.toString()),
|
key: Key(chatMessage.message.messageId.toString()),
|
||||||
messages[i],
|
chatMessage,
|
||||||
user,
|
user,
|
||||||
galleryItems,
|
galleryItems,
|
||||||
isLastMessageFromSameUser(messages, i),
|
isLastMessageFromSameUser(messages, i),
|
||||||
textReactionsToMessageId[messages[i].messageId] ?? [],
|
emojiReactionsToMessageId[
|
||||||
emojiReactionsToMessageId[messages[i].messageId] ?? [],
|
chatMessage.message.messageId] ??
|
||||||
onResponseTriggered: (message) {
|
[],
|
||||||
|
onResponseTriggered: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
responseToMessage = message;
|
responseToMessage = chatMessage.message;
|
||||||
});
|
});
|
||||||
textFieldFocus.requestFocus();
|
textFieldFocus.requestFocus();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -372,7 +333,13 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: getResponsePreview(responseToMessage!)),
|
Expanded(
|
||||||
|
child: ResponsePreview(
|
||||||
|
message: responseToMessage!,
|
||||||
|
showBorder: true,
|
||||||
|
contact: user,
|
||||||
|
),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
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) {
|
if (index <= 0) {
|
||||||
return true; // If there is no previous message, return true
|
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 lastMessage = messages[index - 1];
|
||||||
final currentMessage = messages[index];
|
final currentMessage = messages[index];
|
||||||
|
|
||||||
|
if (lastMessage.isMessage && currentMessage.isMessage) {
|
||||||
// Check if both messages have the same messageOtherId (or both are null)
|
// Check if both messages have the same messageOtherId (or both are null)
|
||||||
return (lastMessage.messageOtherId == null &&
|
return (lastMessage.message!.message.messageOtherId == null &&
|
||||||
currentMessage.messageOtherId == null) ||
|
currentMessage.message!.message.messageOtherId == null) ||
|
||||||
(lastMessage.messageOtherId != null &&
|
(lastMessage.message!.message.messageOtherId != null &&
|
||||||
currentMessage.messageOtherId != null);
|
currentMessage.message!.message.messageOtherId != null);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
double calculateNumberOfLines(String text, double width, double fontSize) {
|
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,16 +59,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> onDoubleTap() async {
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final color = getMessageColorFromType(
|
|
||||||
widget.content,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
key: reopenMediaFile,
|
|
||||||
onDoubleTap: () async {
|
|
||||||
if (widget.message.openedAt == null &&
|
if (widget.message.openedAt == null &&
|
||||||
widget.message.messageOtherId != null ||
|
widget.message.messageOtherId != null ||
|
||||||
widget.message.mediaStored) {
|
widget.message.mediaStored) {
|
||||||
|
|
@ -95,9 +86,9 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
const MessagesCompanion(openedAt: Value(null)),
|
const MessagesCompanion(openedAt: Value(null)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onTap: () async {
|
|
||||||
if (widget.message.kind == MessageKind.media) {
|
Future<void> onTap() async {
|
||||||
if (widget.message.downloadState == DownloadState.downloaded &&
|
if (widget.message.downloadState == DownloadState.downloaded &&
|
||||||
widget.message.openedAt == null) {
|
widget.message.openedAt == null) {
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
|
|
@ -112,9 +103,21 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
await received.startDownloadMedia(widget.message, true);
|
await received.startDownloadMedia(widget.message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = getMessageColorFromType(
|
||||||
|
widget.content,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
key: reopenMediaFile,
|
||||||
|
onDoubleTap: onDoubleTap,
|
||||||
|
onTap: widget.message.kind == MessageKind.media ? onTap : null,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 150,
|
width: 150,
|
||||||
|
height: widget.message.mediaStored ? 271 : null,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: ClipRRect(
|
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:font_awesome_flutter/font_awesome_flutter.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/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||||
|
|
||||||
class ReactionRow extends StatefulWidget {
|
class ReactionRow extends StatefulWidget {
|
||||||
|
|
@ -26,7 +27,7 @@ class _ReactionRowState extends State<ReactionRow> {
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
var hasOneTextReaction = false;
|
var hasOneTextReaction = false;
|
||||||
var hasOneReopened = false;
|
var hasOneReopened = false;
|
||||||
for (final reaction in widget.otherReactions) {
|
for (final reaction in widget.otherReactions.reversed) {
|
||||||
final content = MessageContent.fromJson(
|
final content = MessageContent.fromJson(
|
||||||
reaction.kind, jsonDecode(reaction.contentJson!) as Map);
|
reaction.kind, jsonDecode(reaction.contentJson!) as Map);
|
||||||
|
|
||||||
|
|
@ -34,15 +35,15 @@ class _ReactionRowState extends State<ReactionRow> {
|
||||||
if (hasOneReopened) continue;
|
if (hasOneReopened) continue;
|
||||||
hasOneReopened = true;
|
hasOneReopened = true;
|
||||||
children.add(
|
children.add(
|
||||||
const Expanded(
|
Expanded(
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(right: 3),
|
padding: const EdgeInsets.only(right: 3),
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.repeat,
|
FontAwesomeIcons.repeat,
|
||||||
size: 12,
|
size: 12,
|
||||||
color: Colors.white,
|
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -78,7 +79,7 @@ class _ReactionRowState extends State<ReactionRow> {
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: widget.message.messageOtherId == null
|
mainAxisAlignment: widget.message.messageOtherId == null
|
||||||
? MainAxisAlignment.start
|
? MainAxisAlignment.end
|
||||||
: MainAxisAlignment.end,
|
: MainAxisAlignment.end,
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,16 @@ import 'package:twonly/src/views/components/animate_icon.dart';
|
||||||
import 'package:twonly/src/views/components/better_text.dart';
|
import 'package:twonly/src/views/components/better_text.dart';
|
||||||
|
|
||||||
class ChatTextEntry extends StatelessWidget {
|
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 String text;
|
||||||
final Message message;
|
final Message message;
|
||||||
|
final bool hasReaction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -28,9 +34,12 @@ class ChatTextEntry extends StatelessWidget {
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
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(
|
decoration: BoxDecoration(
|
||||||
color: getMessageColor(message),
|
color: message.responseToMessageId == null
|
||||||
|
? getMessageColor(message)
|
||||||
|
: null,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: BetterText(text: text),
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (galleryItemIndex == null) {
|
if (!widget.message.mediaStored) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
minHeight: 39,
|
minHeight: 39,
|
||||||
|
|
@ -138,10 +138,12 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: MemoriesItemThumbnail(
|
child: galleryItemIndex != null
|
||||||
|
? MemoriesItemThumbnail(
|
||||||
galleryItem: widget.galleryItems[galleryItemIndex!],
|
galleryItem: widget.galleryItems[galleryItemIndex!],
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
),
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/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 {
|
class MessageActions extends StatefulWidget {
|
||||||
const MessageActions({
|
const MessageActions({
|
||||||
|
|
@ -70,13 +59,9 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onHorizontalDragUpdate: _onHorizontalDragUpdate,
|
onHorizontalDragUpdate: _onHorizontalDragUpdate,
|
||||||
onHorizontalDragEnd: _onHorizontalDragEnd,
|
onHorizontalDragEnd: _onHorizontalDragEnd,
|
||||||
child: MessageContextMenu(
|
|
||||||
message: widget.message,
|
|
||||||
onResponseTriggered: widget.onResponseTriggered,
|
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (_offsetX >= 40)
|
if (_offsetX >= 40)
|
||||||
const Positioned(
|
const Positioned(
|
||||||
left: 20,
|
left: 20,
|
||||||
|
|
@ -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,
|
iconColor: Theme.of(context).colorScheme.surfaceBright,
|
||||||
),
|
),
|
||||||
tooltipPadding: const EdgeInsets.all(20),
|
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,
|
// spacing: 0,
|
||||||
tooltipTextStyle: const TextStyle(
|
tooltipTextStyle: const TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue