maybe improved scrolling

This commit is contained in:
otsmr 2025-05-20 18:34:41 +02:00
parent 68c52a8215
commit a7298b74cf
4 changed files with 130 additions and 102 deletions

View file

@ -6,6 +6,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/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/views/chats/components/chat_list_entry.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/initialsavatar.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/database/daos/contacts_dao.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) { Color getMessageColor(Message message) {
return (message.messageOtherId == null) return (message.messageOtherId == null)
? Color.fromARGB(107, 124, 77, 255) ? Color.fromARGB(255, 58, 136, 102)
: Color.fromARGB(83, 68, 137, 255); : Color.fromARGB(83, 68, 137, 255);
} }
@ -42,8 +43,8 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
late StreamSubscription<Contact> userSub; late StreamSubscription<Contact> userSub;
late StreamSubscription<List<Message>> messageSub; late StreamSubscription<List<Message>> messageSub;
List<Message> messages = []; List<Message> messages = [];
Map<int, List<Message>> reactionsToMyMessages = {}; Map<int, List<Message>> textReactionsToMessageId = {};
Map<int, List<Message>> reactionsToOtherMessages = {}; Map<int, List<Message>> emojiReactionsToMessageId = {};
Message? responseToMessage; Message? responseToMessage;
late FocusNode textFieldFocus; late FocusNode textFieldFocus;
@ -84,8 +85,8 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
} }
List<Message> displayedMessages = []; List<Message> displayedMessages = [];
// should be cleared // should be cleared
Map<int, List<Message>> tmpReactionsToMyMessages = {}; Map<int, List<Message>> tmpTextReactionsToMessageId = {};
Map<int, List<Message>> tmpReactionsToOtherMessages = {}; Map<int, List<Message>> tmpEmojiReactionsToMessageId = {};
List<int> openedMessageOtherIds = []; List<int> openedMessageOtherIds = [];
for (Message msg in msgs) { for (Message msg in msgs) {
@ -95,24 +96,26 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
openedMessageOtherIds.add(msg.messageOtherId!); openedMessageOtherIds.add(msg.messageOtherId!);
} }
if (msg.responseToOtherMessageId != null) { int? responseId =
if (!tmpReactionsToOtherMessages msg.responseToMessageId ?? msg.responseToOtherMessageId;
.containsKey(msg.responseToOtherMessageId!)) { if (responseId != null) {
tmpReactionsToOtherMessages[msg.responseToOtherMessageId!] = [msg]; bool added = false;
} else { MessageContent? content =
tmpReactionsToOtherMessages[msg.responseToOtherMessageId!]! 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); .add(msg);
} }
} } else {
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) {
displayedMessages.add(msg); displayedMessages.add(msg);
} }
} }
@ -126,8 +129,8 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
// if (!updated) { // if (!updated) {
// // The stream should be get an update, so only update the UI when all are opened // // The stream should be get an update, so only update the UI when all are opened
setState(() { setState(() {
reactionsToMyMessages = tmpReactionsToMyMessages; textReactionsToMessageId = tmpTextReactionsToMessageId;
reactionsToOtherMessages = tmpReactionsToOtherMessages; emojiReactionsToMessageId = tmpEmojiReactionsToMessageId;
messages = displayedMessages; messages = displayedMessages;
}); });
// } // }
@ -242,31 +245,45 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
child: ListView.builder( child: ListView.builder(
itemCount: messages.length, itemCount: messages.length,
reverse: true, 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) { 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<Message> reactions = [];
if (reactionsToMyMessages.containsKey(msg.messageId)) {
reactions = reactionsToMyMessages[msg.messageId]!;
}
if (msg.messageOtherId != null &&
reactionsToOtherMessages
.containsKey(msg.messageOtherId!)) {
reactions = reactionsToOtherMessages[msg.messageOtherId!]!;
}
return ChatListEntry( return ChatListEntry(
key: Key(msg.messageId.toString()), key: Key(messages[i].messageId.toString()),
msg, messages[i],
user, user,
lastMessageFromSameUser, isLastMessageFromSameUser(messages, i),
reactions, textReactionsToMessageId[messages[i].messageId] ?? [],
emojiReactionsToMessageId[messages[i].messageId] ?? [],
onResponseTriggered: (message) { onResponseTriggered: (message) {
setState(() { setState(() {
responseToMessage = message; responseToMessage = message;
@ -355,3 +372,28 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
); );
} }
} }
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) {
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();
}

View file

@ -134,14 +134,7 @@ class _ChatListViewState extends State<ChatListView> {
); );
} }
int maxTotalMediaCounter = 0; final pinnedUsers = contacts.where((c) => c.pinned).toList();
if (contacts.isNotEmpty) {
maxTotalMediaCounter = contacts
.map((x) => x.totalMediaCounter)
.reduce((a, b) => a > b ? a : b);
}
final pinnedUsers = contacts.where((c) => c.pinned);
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
@ -149,37 +142,46 @@ class _ChatListViewState extends State<ChatListView> {
await apiProvider.connect(); await apiProvider.connect();
await Future.delayed(Duration(seconds: 1)); await Future.delayed(Duration(seconds: 1));
}, },
child: ListView( child: ListView.builder(
children: [ itemCount: pinnedUsers.length +
...pinnedUsers.map((contact) { (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( return UserListItem(
key: ValueKey(contact.userId), key: ValueKey(contact.userId),
user: contact, user: contact,
maxTotalMediaCounter: maxTotalMediaCounter,
); );
}), }
if (pinnedUsers.isNotEmpty) Divider(),
...contacts.where((c) => !c.pinned).map((contact) { // If there are pinned users, account for the Divider
return UserListItem( int adjustedIndex = index - pinnedUsers.length;
key: ValueKey(contact.userId), if (pinnedUsers.isNotEmpty && adjustedIndex == 0) {
user: contact, return Divider();
maxTotalMediaCounter: maxTotalMediaCounter, }
);
}) // 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<ChatListView> {
class UserListItem extends StatefulWidget { class UserListItem extends StatefulWidget {
final Contact user; final Contact user;
final int maxTotalMediaCounter;
const UserListItem({ const UserListItem({
super.key, super.key,
required this.user, required this.user,
required this.maxTotalMediaCounter,
}); });
@override @override

View file

@ -22,21 +22,23 @@ class ChatListEntry extends StatelessWidget {
this.message, this.message,
this.contact, this.contact,
this.lastMessageFromSameUser, this.lastMessageFromSameUser,
this.reactions, { this.textReactions,
this.otherReactions, {
super.key, super.key,
required this.onResponseTriggered, required this.onResponseTriggered,
}); });
final Message message; final Message message;
final Contact contact; final Contact contact;
final bool lastMessageFromSameUser; final bool lastMessageFromSameUser;
final List<Message> reactions; final List<Message> textReactions;
final List<Message> otherReactions;
final Function(Message) onResponseTriggered; final Function(Message) onResponseTriggered;
Widget getReactionRow() { Widget getReactionRow() {
List<Widget> children = []; List<Widget> children = [];
bool hasOneTextReaction = false; bool hasOneTextReaction = false;
bool hasOneReopened = false; bool hasOneReopened = false;
for (final reaction in reactions) { for (final reaction in otherReactions) {
MessageContent? content = MessageContent.fromJson( MessageContent? content = MessageContent.fromJson(
reaction.kind, jsonDecode(reaction.contentJson!)); reaction.kind, jsonDecode(reaction.contentJson!));
@ -96,13 +98,11 @@ class ChatListEntry extends StatelessWidget {
Widget getTextResponseColumns(BuildContext context, bool right) { Widget getTextResponseColumns(BuildContext context, bool right) {
List<Widget> children = []; List<Widget> children = [];
for (final reaction in reactions) { for (final reaction in textReactions) {
MessageContent? content = MessageContent.fromJson( MessageContent? content = MessageContent.fromJson(
reaction.kind, jsonDecode(reaction.contentJson!)); reaction.kind, jsonDecode(reaction.contentJson!));
if (content is TextMessageContent) { if (content is TextMessageContent) {
if (content.text.length <= 1) continue;
if (isEmoji(content.text)) continue;
var entries = [ var entries = [
FaIcon( FaIcon(
FontAwesomeIcons.reply, FontAwesomeIcons.reply,

View file

@ -17,19 +17,16 @@ class BetterText extends StatelessWidget {
multiLine: false, multiLine: false,
); );
// Split the text into parts based on the URLs and domains
final List<TextSpan> spans = []; final List<TextSpan> spans = [];
final Iterable<RegExpMatch> matches = urlRegExp.allMatches(text); final Iterable<RegExpMatch> matches = urlRegExp.allMatches(text);
int lastMatchEnd = 0; int lastMatchEnd = 0;
for (final match in matches) { for (final match in matches) {
// Add the text before the URL/domain
if (match.start > lastMatchEnd) { if (match.start > lastMatchEnd) {
spans.add(TextSpan(text: text.substring(lastMatchEnd, match.start))); spans.add(TextSpan(text: text.substring(lastMatchEnd, match.start)));
} }
// Add the URL/domain as a clickable TextSpan
final String? url = match.group(0); final String? url = match.group(0);
spans.add(TextSpan( spans.add(TextSpan(
text: url, text: url,
@ -49,7 +46,6 @@ class BetterText extends StatelessWidget {
lastMatchEnd = match.end; lastMatchEnd = match.end;
} }
// Add any remaining text after the last URL/domain
if (lastMatchEnd < text.length) { if (lastMatchEnd < text.length) {
spans.add(TextSpan(text: text.substring(lastMatchEnd))); spans.add(TextSpan(text: text.substring(lastMatchEnd)));
} }
@ -59,19 +55,9 @@ class BetterText extends StatelessWidget {
children: spans, children: spans,
), ),
style: TextStyle( style: TextStyle(
color: Colors.white, // Set text color for contrast color: Colors.white,
fontSize: 17, 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
} }
} }