From a7298b74cf5228750746afeb448d7eb8bbec436b Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 20 May 2025 18:34:41 +0200 Subject: [PATCH] maybe improved scrolling --- .../views/chats/chat_item_details_view.dart | 132 ++++++++++++------ lib/src/views/chats/chat_list_view.dart | 72 +++++----- .../chats/components/chat_list_entry.dart | 12 +- lib/src/views/components/better_text.dart | 16 +-- 4 files changed, 130 insertions(+), 102 deletions(-) diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index a3884ae..9826d39 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -6,6 +6,7 @@ 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/components/chat_list_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'; @@ -20,7 +21,7 @@ import 'package:twonly/src/views/contact/contact_view.dart'; Color getMessageColor(Message message) { return (message.messageOtherId == null) - ? Color.fromARGB(107, 124, 77, 255) + ? Color.fromARGB(255, 58, 136, 102) : Color.fromARGB(83, 68, 137, 255); } @@ -42,8 +43,8 @@ class _ChatItemDetailsViewState extends State { late StreamSubscription userSub; late StreamSubscription> messageSub; List messages = []; - Map> reactionsToMyMessages = {}; - Map> reactionsToOtherMessages = {}; + Map> textReactionsToMessageId = {}; + Map> emojiReactionsToMessageId = {}; Message? responseToMessage; late FocusNode textFieldFocus; @@ -84,8 +85,8 @@ class _ChatItemDetailsViewState extends State { } List displayedMessages = []; // should be cleared - Map> tmpReactionsToMyMessages = {}; - Map> tmpReactionsToOtherMessages = {}; + Map> tmpTextReactionsToMessageId = {}; + Map> tmpEmojiReactionsToMessageId = {}; List openedMessageOtherIds = []; for (Message msg in msgs) { @@ -95,24 +96,26 @@ class _ChatItemDetailsViewState extends State { openedMessageOtherIds.add(msg.messageOtherId!); } - if (msg.responseToOtherMessageId != null) { - if (!tmpReactionsToOtherMessages - .containsKey(msg.responseToOtherMessageId!)) { - tmpReactionsToOtherMessages[msg.responseToOtherMessageId!] = [msg]; - } else { - tmpReactionsToOtherMessages[msg.responseToOtherMessageId!]! + int? responseId = + msg.responseToMessageId ?? 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); } - } - if (msg.responseToMessageId != null) { - if (!tmpReactionsToMyMessages.containsKey(msg.responseToMessageId!)) { - tmpReactionsToMyMessages[msg.responseToMessageId!] = [msg]; - } else { - tmpReactionsToMyMessages[msg.responseToMessageId!]!.add(msg); - } - } - if (msg.responseToMessageId == null && - msg.responseToOtherMessageId == null) { + } else { displayedMessages.add(msg); } } @@ -126,8 +129,8 @@ class _ChatItemDetailsViewState extends State { // if (!updated) { // // The stream should be get an update, so only update the UI when all are opened setState(() { - reactionsToMyMessages = tmpReactionsToMyMessages; - reactionsToOtherMessages = tmpReactionsToOtherMessages; + textReactionsToMessageId = tmpTextReactionsToMessageId; + emojiReactionsToMessageId = tmpEmojiReactionsToMessageId; messages = displayedMessages; }); // } @@ -242,31 +245,45 @@ class _ChatItemDetailsViewState extends State { child: ListView.builder( itemCount: messages.length, reverse: true, + itemExtentBuilder: (index, dimensions) { + double size = 44; + if (messages[index].kind == MessageKind.textMessage) { + MessageContent? content = MessageContent.fromJson( + messages[index].kind, + jsonDecode(messages[index].contentJson!)); + if (content is TextMessageContent) { + if (EmojiAnimation.supported(content.text)) { + size = 95; + } else { + size = 11 + + calculateNumberOfLines(content.text, + MediaQuery.of(context).size.width * 0.8) * + 27; + } + } + } + if (messages[index].mediaStored) { + size = 271; + } + // add reaction size + size += (textReactionsToMessageId[messages[index].messageId] + ?.length ?? + 0) * + 27; + + if (!isLastMessageFromSameUser(messages, index)) { + size += 20; + } + return size; + }, itemBuilder: (context, i) { - bool lastMessageFromSameUser = false; - if (i > 0) { - lastMessageFromSameUser = - (messages[i - 1].messageOtherId == null && - messages[i].messageOtherId == null) || - (messages[i - 1].messageOtherId != null && - messages[i].messageOtherId != null); - } - Message msg = messages[i]; - List reactions = []; - if (reactionsToMyMessages.containsKey(msg.messageId)) { - reactions = reactionsToMyMessages[msg.messageId]!; - } - if (msg.messageOtherId != null && - reactionsToOtherMessages - .containsKey(msg.messageOtherId!)) { - reactions = reactionsToOtherMessages[msg.messageOtherId!]!; - } return ChatListEntry( - key: Key(msg.messageId.toString()), - msg, + key: Key(messages[i].messageId.toString()), + messages[i], user, - lastMessageFromSameUser, - reactions, + isLastMessageFromSameUser(messages, i), + textReactionsToMessageId[messages[i].messageId] ?? [], + emojiReactionsToMessageId[messages[i].messageId] ?? [], onResponseTriggered: (message) { setState(() { responseToMessage = message; @@ -355,3 +372,28 @@ class _ChatItemDetailsViewState extends State { ); } } + +bool isLastMessageFromSameUser(List 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) { + final textPainter = TextPainter( + text: TextSpan(text: text, style: TextStyle(fontSize: 17)), + // maxLines: null, + textDirection: TextDirection.ltr, + ); + textPainter.layout(maxWidth: (width - 20)); + return textPainter.computeLineMetrics().length.toDouble(); +} diff --git a/lib/src/views/chats/chat_list_view.dart b/lib/src/views/chats/chat_list_view.dart index b3c3e94..8dd0be2 100644 --- a/lib/src/views/chats/chat_list_view.dart +++ b/lib/src/views/chats/chat_list_view.dart @@ -134,14 +134,7 @@ class _ChatListViewState extends State { ); } - int maxTotalMediaCounter = 0; - if (contacts.isNotEmpty) { - maxTotalMediaCounter = contacts - .map((x) => x.totalMediaCounter) - .reduce((a, b) => a > b ? a : b); - } - - final pinnedUsers = contacts.where((c) => c.pinned); + final pinnedUsers = contacts.where((c) => c.pinned).toList(); return RefreshIndicator( onRefresh: () async { @@ -149,37 +142,46 @@ class _ChatListViewState extends State { await apiProvider.connect(); await Future.delayed(Duration(seconds: 1)); }, - child: ListView( - children: [ - ...pinnedUsers.map((contact) { + child: ListView.builder( + itemCount: pinnedUsers.length + + (pinnedUsers.isNotEmpty ? 1 : 0) + + contacts.where((c) => !c.pinned).length, + itemExtentBuilder: (index, dimensions) { + int adjustedIndex = index - pinnedUsers.length; + if (pinnedUsers.isNotEmpty && adjustedIndex == 0) { + return 16; + } + return 72; + }, + itemBuilder: (context, index) { + // Check if the index is for the pinned users + if (index < pinnedUsers.length) { + final contact = pinnedUsers[index]; return UserListItem( key: ValueKey(contact.userId), user: contact, - maxTotalMediaCounter: maxTotalMediaCounter, ); - }), - if (pinnedUsers.isNotEmpty) Divider(), - ...contacts.where((c) => !c.pinned).map((contact) { - return UserListItem( - key: ValueKey(contact.userId), - user: contact, - maxTotalMediaCounter: maxTotalMediaCounter, - ); - }) - ], + } + + // If there are pinned users, account for the Divider + int adjustedIndex = index - pinnedUsers.length; + if (pinnedUsers.isNotEmpty && adjustedIndex == 0) { + return Divider(); + } + + // Adjust the index for the contacts list + adjustedIndex -= (pinnedUsers.isNotEmpty ? 1 : 0); + + // Get the contacts that are not pinned + final contact = contacts + .where((c) => !c.pinned) + .elementAt(adjustedIndex); + return UserListItem( + key: ValueKey(contact.userId), + user: contact, + ); + }, ), - // child: ListView.builder( - // restorationId: 'chat_list_view', - // itemCount: contacts.length, - // itemBuilder: (BuildContext context, int index) { - // final user = contacts[index]; - // return UserListItem( - // key: ValueKey(user.userId), - // user: user, - // maxTotalMediaCounter: maxTotalMediaCounter, - // ); - // }, - // ), ); }, ), @@ -207,12 +209,10 @@ class _ChatListViewState extends State { class UserListItem extends StatefulWidget { final Contact user; - final int maxTotalMediaCounter; const UserListItem({ super.key, required this.user, - required this.maxTotalMediaCounter, }); @override diff --git a/lib/src/views/chats/components/chat_list_entry.dart b/lib/src/views/chats/components/chat_list_entry.dart index 5d98342..0a1ca99 100644 --- a/lib/src/views/chats/components/chat_list_entry.dart +++ b/lib/src/views/chats/components/chat_list_entry.dart @@ -22,21 +22,23 @@ class ChatListEntry extends StatelessWidget { this.message, this.contact, this.lastMessageFromSameUser, - this.reactions, { + this.textReactions, + this.otherReactions, { super.key, required this.onResponseTriggered, }); final Message message; final Contact contact; final bool lastMessageFromSameUser; - final List reactions; + final List textReactions; + final List otherReactions; final Function(Message) onResponseTriggered; Widget getReactionRow() { List children = []; bool hasOneTextReaction = false; bool hasOneReopened = false; - for (final reaction in reactions) { + for (final reaction in otherReactions) { MessageContent? content = MessageContent.fromJson( reaction.kind, jsonDecode(reaction.contentJson!)); @@ -96,13 +98,11 @@ class ChatListEntry extends StatelessWidget { Widget getTextResponseColumns(BuildContext context, bool right) { List children = []; - for (final reaction in reactions) { + for (final reaction in textReactions) { MessageContent? content = MessageContent.fromJson( reaction.kind, jsonDecode(reaction.contentJson!)); if (content is TextMessageContent) { - if (content.text.length <= 1) continue; - if (isEmoji(content.text)) continue; var entries = [ FaIcon( FontAwesomeIcons.reply, diff --git a/lib/src/views/components/better_text.dart b/lib/src/views/components/better_text.dart index 847770a..672a073 100644 --- a/lib/src/views/components/better_text.dart +++ b/lib/src/views/components/better_text.dart @@ -17,19 +17,16 @@ class BetterText extends StatelessWidget { multiLine: false, ); - // Split the text into parts based on the URLs and domains final List spans = []; final Iterable matches = urlRegExp.allMatches(text); int lastMatchEnd = 0; for (final match in matches) { - // Add the text before the URL/domain if (match.start > lastMatchEnd) { spans.add(TextSpan(text: text.substring(lastMatchEnd, match.start))); } - // Add the URL/domain as a clickable TextSpan final String? url = match.group(0); spans.add(TextSpan( text: url, @@ -49,7 +46,6 @@ class BetterText extends StatelessWidget { lastMatchEnd = match.end; } - // Add any remaining text after the last URL/domain if (lastMatchEnd < text.length) { spans.add(TextSpan(text: text.substring(lastMatchEnd))); } @@ -59,19 +55,9 @@ class BetterText extends StatelessWidget { children: spans, ), style: TextStyle( - color: Colors.white, // Set text color for contrast + color: Colors.white, fontSize: 17, ), ); - -// child: SelectableText( -// content.text, -// style: TextStyle( -// color: Colors.white, // Set text color for contrast -// fontSize: 17, -// ), -// textAlign: TextAlign.left, // Center the text -// ), -// RichText } }