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: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<ChatItemDetailsView> {
late StreamSubscription<Contact> userSub;
late StreamSubscription<List<Message>> messageSub;
List<Message> messages = [];
Map<int, List<Message>> reactionsToMyMessages = {};
Map<int, List<Message>> reactionsToOtherMessages = {};
Map<int, List<Message>> textReactionsToMessageId = {};
Map<int, List<Message>> emojiReactionsToMessageId = {};
Message? responseToMessage;
late FocusNode textFieldFocus;
@ -84,8 +85,8 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
}
List<Message> displayedMessages = [];
// should be cleared
Map<int, List<Message>> tmpReactionsToMyMessages = {};
Map<int, List<Message>> tmpReactionsToOtherMessages = {};
Map<int, List<Message>> tmpTextReactionsToMessageId = {};
Map<int, List<Message>> tmpEmojiReactionsToMessageId = {};
List<int> openedMessageOtherIds = [];
for (Message msg in msgs) {
@ -95,24 +96,26 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
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<ChatItemDetailsView> {
// 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<ChatItemDetailsView> {
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<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(
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<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;
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<ChatListView> {
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<ChatListView> {
class UserListItem extends StatefulWidget {
final Contact user;
final int maxTotalMediaCounter;
const UserListItem({
super.key,
required this.user,
required this.maxTotalMediaCounter,
});
@override

View file

@ -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<Message> reactions;
final List<Message> textReactions;
final List<Message> otherReactions;
final Function(Message) onResponseTriggered;
Widget getReactionRow() {
List<Widget> 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<Widget> 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,

View file

@ -17,19 +17,16 @@ class BetterText extends StatelessWidget {
multiLine: false,
);
// Split the text into parts based on the URLs and domains
final List<TextSpan> spans = [];
final Iterable<RegExpMatch> 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
}
}