mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:58:40 +00:00
maybe improved scrolling
This commit is contained in:
parent
68c52a8215
commit
a7298b74cf
4 changed files with 130 additions and 102 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue