mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 14:08:40 +00:00
436 lines
14 KiB
Dart
436 lines
14 KiB
Dart
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:twonly/globals.dart';
|
|
import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry.dart';
|
|
import 'package:twonly/src/views/components/animate_icon.dart';
|
|
import 'package:twonly/src/views/components/initialsavatar.dart';
|
|
import 'package:twonly/src/views/components/verified_shield.dart';
|
|
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
|
import 'package:twonly/src/database/twonly_database.dart';
|
|
import 'package:twonly/src/database/tables/messages_table.dart';
|
|
import 'package:twonly/src/model/json/message.dart';
|
|
import 'package:twonly/src/services/api/messages.dart';
|
|
import 'package:twonly/src/services/notification.service.dart';
|
|
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
|
import 'package:twonly/src/utils/misc.dart';
|
|
import 'package:twonly/src/views/contact/contact.view.dart';
|
|
import 'package:twonly/src/model/memory_item.model.dart';
|
|
|
|
Color getMessageColor(Message message) {
|
|
return (message.messageOtherId == null)
|
|
? Color.fromARGB(255, 58, 136, 102)
|
|
: Color.fromARGB(83, 68, 137, 255);
|
|
}
|
|
|
|
/// Displays detailed information about a SampleItem.
|
|
class ChatMessagesView extends StatefulWidget {
|
|
const ChatMessagesView(this.contact, {super.key});
|
|
|
|
final Contact contact;
|
|
|
|
@override
|
|
State<ChatMessagesView> createState() => _ChatMessagesViewState();
|
|
}
|
|
|
|
class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|
TextEditingController newMessageController = TextEditingController();
|
|
HashSet<int> alreadyReportedOpened = HashSet<int>();
|
|
late Contact user;
|
|
String currentInputText = "";
|
|
late StreamSubscription<Contact> userSub;
|
|
late StreamSubscription<List<Message>> messageSub;
|
|
List<Message> messages = [];
|
|
List<MemoryItem> galleryItems = [];
|
|
Map<int, List<Message>> textReactionsToMessageId = {};
|
|
Map<int, List<Message>> emojiReactionsToMessageId = {};
|
|
Message? responseToMessage;
|
|
late FocusNode textFieldFocus;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
user = widget.contact;
|
|
textFieldFocus = FocusNode();
|
|
initStreams();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
userSub.cancel();
|
|
messageSub.cancel();
|
|
textFieldFocus.dispose();
|
|
}
|
|
|
|
Future initStreams() async {
|
|
await twonlyDB.messagesDao.removeOldMessages();
|
|
Stream<Contact> contact =
|
|
twonlyDB.contactsDao.watchContact(widget.contact.userId);
|
|
userSub = contact.listen((contact) {
|
|
setState(() {
|
|
user = contact;
|
|
});
|
|
});
|
|
|
|
Stream<List<Message>> msgStream =
|
|
twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId);
|
|
messageSub = msgStream.listen((msgs) async {
|
|
// if (!context.mounted) return;
|
|
if (Platform.isAndroid) {
|
|
flutterLocalNotificationsPlugin.cancel(widget.contact.userId);
|
|
} else {
|
|
flutterLocalNotificationsPlugin.cancelAll();
|
|
}
|
|
List<Message> displayedMessages = [];
|
|
// should be cleared
|
|
Map<int, List<Message>> tmpTextReactionsToMessageId = {};
|
|
Map<int, List<Message>> tmpEmojiReactionsToMessageId = {};
|
|
|
|
List<int> openedMessageOtherIds = [];
|
|
|
|
Map<int, int> messageOtherMessageIdToMyMessageId = {};
|
|
|
|
/// there is probably a better way...
|
|
for (Message msg in msgs) {
|
|
if (msg.messageOtherId != null) {
|
|
messageOtherMessageIdToMyMessageId[msg.messageOtherId!] =
|
|
msg.messageId;
|
|
}
|
|
}
|
|
|
|
for (Message msg in msgs) {
|
|
if (msg.kind == MessageKind.textMessage &&
|
|
msg.messageOtherId != null &&
|
|
msg.openedAt == null) {
|
|
openedMessageOtherIds.add(msg.messageOtherId!);
|
|
}
|
|
|
|
int? responseId = msg.responseToMessageId ??
|
|
messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId];
|
|
|
|
if (responseId != null) {
|
|
bool added = false;
|
|
MessageContent? content =
|
|
MessageContent.fromJson(msg.kind, jsonDecode(msg.contentJson!));
|
|
if (content is TextMessageContent) {
|
|
if (content.text.isNotEmpty && !isEmoji(content.text)) {
|
|
added = true;
|
|
tmpTextReactionsToMessageId
|
|
.putIfAbsent(responseId, () => [])
|
|
.add(msg);
|
|
}
|
|
}
|
|
if (!added) {
|
|
tmpEmojiReactionsToMessageId
|
|
.putIfAbsent(responseId, () => [])
|
|
.add(msg);
|
|
}
|
|
} else {
|
|
displayedMessages.add(msg);
|
|
}
|
|
}
|
|
|
|
if (openedMessageOtherIds.isNotEmpty) {
|
|
notifyContactAboutOpeningMessage(
|
|
widget.contact.userId, openedMessageOtherIds);
|
|
}
|
|
|
|
twonlyDB.messagesDao.openedAllNonMediaMessages(widget.contact.userId);
|
|
|
|
setState(() {
|
|
textReactionsToMessageId = tmpTextReactionsToMessageId;
|
|
emojiReactionsToMessageId = tmpEmojiReactionsToMessageId;
|
|
messages = displayedMessages;
|
|
});
|
|
|
|
final filteredMediaFiles = displayedMessages
|
|
.where((x) => x.kind == MessageKind.media && x.mediaStored)
|
|
.toList()
|
|
.reversed
|
|
.toList();
|
|
final items = await MemoryItem.convertFromMessages(filteredMediaFiles);
|
|
setState(() {
|
|
galleryItems = items.values.toList();
|
|
});
|
|
});
|
|
}
|
|
|
|
Future _sendMessage() async {
|
|
if (newMessageController.text == "") return;
|
|
await sendTextMessage(
|
|
user.userId,
|
|
TextMessageContent(
|
|
text: newMessageController.text,
|
|
responseToMessageId: responseToMessage?.messageOtherId,
|
|
responseToOtherMessageId: responseToMessage?.messageId,
|
|
),
|
|
(responseToMessage == null) ? PushKind.text : PushKind.response,
|
|
);
|
|
newMessageController.clear();
|
|
currentInputText = "";
|
|
responseToMessage = null;
|
|
setState(() {});
|
|
}
|
|
|
|
Widget getResponsePreview(Message message) {
|
|
String? subtitle;
|
|
|
|
if (message.kind == MessageKind.textMessage) {
|
|
if (message.contentJson != null) {
|
|
MessageContent? content = MessageContent.fromJson(
|
|
MessageKind.textMessage, jsonDecode(message.contentJson!));
|
|
if (content is TextMessageContent) {
|
|
subtitle = truncateString(content.text);
|
|
}
|
|
}
|
|
}
|
|
if (message.kind == MessageKind.media) {
|
|
MessageContent? content = MessageContent.fromJson(
|
|
MessageKind.media, jsonDecode(message.contentJson!));
|
|
if (content is MediaMessageContent) {
|
|
subtitle = content.isVideo ? "Video" : "Image";
|
|
}
|
|
}
|
|
|
|
String username = "You";
|
|
if (message.messageOtherId != null) {
|
|
username = getContactDisplayName(widget.contact);
|
|
}
|
|
|
|
Color color = getMessageColor(message);
|
|
|
|
return Container(
|
|
padding: EdgeInsets.only(left: 10),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
left: BorderSide(
|
|
color: color,
|
|
width: 2.0,
|
|
),
|
|
),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
username,
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
if (subtitle != null) Text(subtitle)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: GestureDetector(
|
|
onTap: () {
|
|
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
|
return ContactView(widget.contact.userId);
|
|
}));
|
|
},
|
|
child: Row(
|
|
children: [
|
|
ContactAvatar(
|
|
contact: user,
|
|
fontSize: 19,
|
|
),
|
|
SizedBox(width: 10),
|
|
Expanded(
|
|
child: Container(
|
|
color: Colors.transparent,
|
|
child: Row(
|
|
children: [
|
|
Text(getContactDisplayName(user)),
|
|
SizedBox(width: 10),
|
|
VerifiedShield(user),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
body: SafeArea(
|
|
child: Column(
|
|
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) {
|
|
TextMessageContent? content = TextMessageContent.fromJson(
|
|
jsonDecode(messages[index].contentJson!));
|
|
if (EmojiAnimation.supported(content.text)) {
|
|
size = 95;
|
|
} 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) {
|
|
TextMessageContent? content =
|
|
TextMessageContent.fromJson(
|
|
jsonDecode(reaction.contentJson!));
|
|
size += calculateNumberOfLines(content.text,
|
|
MediaQuery.of(context).size.width * 0.5, 14) *
|
|
27;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isLastMessageFromSameUser(messages, index)) {
|
|
size += 20;
|
|
}
|
|
return size;
|
|
},
|
|
itemBuilder: (context, i) {
|
|
if (i == 0) {
|
|
return Container(); // just a padding
|
|
}
|
|
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();
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
if (responseToMessage != null)
|
|
Container(
|
|
padding: const EdgeInsets.only(
|
|
bottom: 00,
|
|
left: 20,
|
|
right: 20,
|
|
top: 10,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(child: getResponsePreview(responseToMessage!)),
|
|
IconButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
responseToMessage = null;
|
|
});
|
|
},
|
|
icon: FaIcon(
|
|
FontAwesomeIcons.xmark,
|
|
size: 16,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
bottom: 30,
|
|
left: 20,
|
|
right: 20,
|
|
top: 10,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: newMessageController,
|
|
focusNode: textFieldFocus,
|
|
onChanged: (value) {
|
|
currentInputText = value;
|
|
setState(() {});
|
|
},
|
|
onSubmitted: (_) {
|
|
_sendMessage();
|
|
},
|
|
decoration: inputTextMessageDeco(context),
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
(currentInputText != "")
|
|
? IconButton(
|
|
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
|
onPressed: () {
|
|
_sendMessage();
|
|
},
|
|
)
|
|
: IconButton(
|
|
icon: FaIcon(FontAwesomeIcons.camera),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) {
|
|
return CameraSendToView(widget.contact);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
)
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
bool isLastMessageFromSameUser(List<Message> messages, int index) {
|
|
if (index <= 0) {
|
|
return true; // If there is no previous message, return true
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
double calculateNumberOfLines(String text, double width, double fontSize) {
|
|
final textPainter = TextPainter(
|
|
text: TextSpan(
|
|
text: text,
|
|
style: TextStyle(fontSize: fontSize),
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
);
|
|
textPainter.layout(maxWidth: (width - 30));
|
|
return textPainter.computeLineMetrics().length.toDouble();
|
|
}
|