mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 14:48:41 +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: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);
|
.add(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (msg.responseToMessageId != null) {
|
if (!added) {
|
||||||
if (!tmpReactionsToMyMessages.containsKey(msg.responseToMessageId!)) {
|
tmpEmojiReactionsToMessageId
|
||||||
tmpReactionsToMyMessages[msg.responseToMessageId!] = [msg];
|
.putIfAbsent(responseId, () => [])
|
||||||
|
.add(msg);
|
||||||
|
}
|
||||||
} else {
|
} 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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
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(
|
return UserListItem(
|
||||||
key: ValueKey(contact.userId),
|
key: ValueKey(contact.userId),
|
||||||
user: contact,
|
user: contact,
|
||||||
maxTotalMediaCounter: maxTotalMediaCounter,
|
|
||||||
);
|
);
|
||||||
})
|
},
|
||||||
],
|
|
||||||
),
|
),
|
||||||
// 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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue