refactor: Make ChatListItem cache lasteventbody for better performance

This commit is contained in:
Christian Kußowski 2025-09-30 10:18:40 +02:00
commit 42438052b1
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652

View file

@ -15,7 +15,7 @@ import '../../widgets/avatar.dart';
enum ArchivedRoomAction { delete, rejoin } enum ArchivedRoomAction { delete, rejoin }
class ChatListItem extends StatelessWidget { class ChatListItem extends StatefulWidget {
final Room room; final Room room;
final Room? space; final Room? space;
final bool activeChat; final bool activeChat;
@ -35,37 +35,87 @@ class ChatListItem extends StatelessWidget {
super.key, super.key,
}); });
@override
State<ChatListItem> createState() => _ChatListItemState();
}
class _ChatListItemState extends State<ChatListItem> {
String? lastEventKey;
String? lastEventBody;
late final bool isDirectChat;
late final String? directChatMatrixId;
String _calcLastEventKey() =>
'${widget.room.lastEvent?.eventId}_${widget.room.lastEvent?.type}_${widget.room.lastEvent?.redacted}';
@override
void initState() {
isDirectChat = widget.room.isDirectChat;
directChatMatrixId = widget.room.directChatMatrixID;
super.initState();
lastEventKey = _calcLastEventKey();
lastEventBody = widget.room.lastEvent?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: (!isDirectChat ||
directChatMatrixId != widget.room.lastEvent?.senderId),
);
if (!widget.room.participantListComplete) {
widget.room.loadHeroUsers().then((_) {
setState(() {});
});
}
}
void _maybeUpdateLastEventBody() async {
final newLastEventKey = _calcLastEventKey();
if (newLastEventKey == lastEventKey) return;
final newLastEventBody = await widget.room.lastEvent?.calcLocalizedBody(
MatrixLocals(L10n.of(context)),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: (!isDirectChat ||
directChatMatrixId != widget.room.lastEvent?.senderId),
);
if (lastEventBody != newLastEventBody) {
setState(() {
lastEventBody = newLastEventBody;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_maybeUpdateLastEventBody();
final theme = Theme.of(context); final theme = Theme.of(context);
final isMuted = room.pushRuleState != PushRuleState.notify; final isMuted = widget.room.pushRuleState != PushRuleState.notify;
final typingText = room.getLocalizedTypingText(context); final typingText = widget.room.getLocalizedTypingText(context);
final lastEvent = room.lastEvent; final lastEvent = widget.room.lastEvent;
final ownMessage = lastEvent?.senderId == room.client.userID; final ownMessage = lastEvent?.senderId == widget.room.client.userID;
final unread = room.isUnread; final unread = widget.room.isUnread;
final directChatMatrixId = room.directChatMatrixID; final unreadBubbleSize = unread || widget.room.hasNewMessages
final isDirectChat = directChatMatrixId != null; ? widget.room.notificationCount > 0
final unreadBubbleSize = unread || room.hasNewMessages
? room.notificationCount > 0
? 20.0 ? 20.0
: 14.0 : 14.0
: 0.0; : 0.0;
final hasNotifications = room.notificationCount > 0; final hasNotifications = widget.room.notificationCount > 0;
final backgroundColor = final backgroundColor =
activeChat ? theme.colorScheme.secondaryContainer : null; widget.activeChat ? theme.colorScheme.secondaryContainer : null;
final displayname = room.getLocalizedDisplayname( final displayname = widget.room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)), MatrixLocals(L10n.of(context)),
); );
final filter = this.filter; final filter = widget.filter;
if (filter != null && !displayname.toLowerCase().contains(filter)) { if (filter != null && !displayname.toLowerCase().contains(filter)) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final space = widget.space;
final needLastEventSender = lastEvent == null
? false
: room.getState(EventTypes.RoomMember, lastEvent.senderId) == null;
final space = this.space;
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -76,314 +126,288 @@ class ChatListItem extends StatelessWidget {
borderRadius: BorderRadius.circular(AppConfig.borderRadius), borderRadius: BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
color: backgroundColor, color: backgroundColor,
child: FutureBuilder( child: HoverBuilder(
future: room.loadHeroUsers(), builder: (context, listTileHovered) => ListTile(
builder: (context, snapshot) => HoverBuilder( visualDensity: const VisualDensity(vertical: -0.5),
builder: (context, listTileHovered) => ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8),
visualDensity: const VisualDensity(vertical: -0.5), onLongPress: () => widget.onLongPress?.call(context),
contentPadding: const EdgeInsets.symmetric(horizontal: 8), leading: HoverBuilder(
onLongPress: () => onLongPress?.call(context), builder: (context, hovered) => AnimatedScale(
leading: HoverBuilder( duration: FluffyThemes.animationDuration,
builder: (context, hovered) => AnimatedScale( curve: FluffyThemes.animationCurve,
duration: FluffyThemes.animationDuration, scale: hovered ? 1.1 : 1.0,
curve: FluffyThemes.animationCurve, child: SizedBox(
scale: hovered ? 1.1 : 1.0, width: Avatar.defaultSize,
child: SizedBox( height: Avatar.defaultSize,
width: Avatar.defaultSize, child: Stack(
height: Avatar.defaultSize, children: [
child: Stack( if (space != null)
children: [
if (space != null)
Positioned(
top: 0,
left: 0,
child: Avatar(
border: BorderSide(
width: 2,
color: backgroundColor ??
theme.colorScheme.surface,
),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
mxContent: space.avatar,
size: Avatar.defaultSize * 0.75,
name: space.getLocalizedDisplayname(),
onTap: () => onLongPress?.call(context),
),
),
Positioned(
bottom: 0,
right: 0,
child: Avatar(
border: space == null
? room.isSpace
? BorderSide(
width: 1,
color: theme.dividerColor,
)
: null
: BorderSide(
width: 2,
color: backgroundColor ??
theme.colorScheme.surface,
),
borderRadius: room.isSpace
? BorderRadius.circular(
AppConfig.borderRadius / 4,
)
: null,
mxContent: room.avatar,
size: space != null
? Avatar.defaultSize * 0.75
: Avatar.defaultSize,
name: displayname,
presenceUserId: directChatMatrixId,
presenceBackgroundColor: backgroundColor,
onTap: () => onLongPress?.call(context),
),
),
Positioned( Positioned(
top: 0, top: 0,
right: 0, left: 0,
child: GestureDetector( child: Avatar(
onTap: () => onLongPress?.call(context), border: BorderSide(
child: AnimatedScale( width: 2,
duration: FluffyThemes.animationDuration, color:
curve: FluffyThemes.animationCurve, backgroundColor ?? theme.colorScheme.surface,
scale: listTileHovered ? 1.0 : 0.0, ),
child: Material( borderRadius: BorderRadius.circular(
color: backgroundColor, AppConfig.borderRadius / 4,
borderRadius: BorderRadius.circular(16), ),
child: const Icon( mxContent: space.avatar,
Icons.arrow_drop_down_circle_outlined, size: Avatar.defaultSize * 0.75,
size: 18, name: space.getLocalizedDisplayname(),
onTap: () => widget.onLongPress?.call(context),
),
),
Positioned(
bottom: 0,
right: 0,
child: Avatar(
border: space == null
? widget.room.isSpace
? BorderSide(
width: 1,
color: theme.dividerColor,
)
: null
: BorderSide(
width: 2,
color: backgroundColor ??
theme.colorScheme.surface,
), ),
borderRadius: widget.room.isSpace
? BorderRadius.circular(
AppConfig.borderRadius / 4,
)
: null,
mxContent: widget.room.avatar,
size: space != null
? Avatar.defaultSize * 0.75
: Avatar.defaultSize,
name: displayname,
presenceUserId: directChatMatrixId,
presenceBackgroundColor: backgroundColor,
onTap: () => widget.onLongPress?.call(context),
),
),
Positioned(
top: 0,
right: 0,
child: GestureDetector(
onTap: () => widget.onLongPress?.call(context),
child: AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: listTileHovered ? 1.0 : 0.0,
child: Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(16),
child: const Icon(
Icons.arrow_drop_down_circle_outlined,
size: 18,
), ),
), ),
), ),
), ),
], ),
), ],
), ),
), ),
), ),
title: Row( ),
children: <Widget>[ title: Row(
Expanded( children: <Widget>[
child: Text( Expanded(
displayname, child: Text(
maxLines: 1, displayname,
overflow: TextOverflow.ellipsis, maxLines: 1,
softWrap: false, overflow: TextOverflow.ellipsis,
style: TextStyle( softWrap: false,
fontWeight: unread || room.hasNewMessages style: TextStyle(
? FontWeight.w500 fontWeight: unread || widget.room.hasNewMessages
: null, ? FontWeight.w500
), : null,
), ),
), ),
if (isMuted) ),
const Padding( if (isMuted)
padding: EdgeInsets.only(left: 4.0), const Padding(
child: Icon( padding: EdgeInsets.only(left: 4.0),
Icons.notifications_off_outlined,
size: 16,
),
),
if (room.isFavourite)
Padding(
padding: EdgeInsets.only(
right: hasNotifications ? 4.0 : 0.0,
),
child: Icon(
Icons.push_pin,
size: 16,
color: theme.colorScheme.primary,
),
),
if (!room.isSpace && room.membership != Membership.invite)
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
room.latestEventReceivedTime
.localizedTimeShort(context),
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline,
),
),
),
],
),
subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (typingText.isEmpty &&
ownMessage &&
room.lastEvent!.status.isSending) ...[
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
),
const SizedBox(width: 4),
],
AnimatedContainer(
width: typingText.isEmpty ? 0 : 18,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
padding: const EdgeInsets.only(right: 4),
child: Icon( child: Icon(
Icons.edit_outlined, Icons.notifications_off_outlined,
color: theme.colorScheme.secondary, size: 16,
size: 14,
), ),
), ),
Expanded( if (widget.room.isFavourite)
child: room.isSpace && room.membership == Membership.join Padding(
? Text( padding: EdgeInsets.only(
L10n.of(context).countChatsAndCountParticipants( right: hasNotifications ? 4.0 : 0.0,
room.spaceChildren.length, ),
(room.summary.mJoinedMemberCount ?? 1), child: Icon(
), Icons.push_pin,
style: TextStyle(color: theme.colorScheme.outline), size: 16,
) color: theme.colorScheme.primary,
: typingText.isNotEmpty ),
? Text( ),
typingText, if (!widget.room.isSpace &&
style: TextStyle( widget.room.membership != Membership.invite)
color: theme.colorScheme.primary, Padding(
), padding: const EdgeInsets.only(left: 4.0),
maxLines: 1, child: Text(
softWrap: false, widget.room.latestEventReceivedTime
) .localizedTimeShort(context),
: FutureBuilder( style: TextStyle(
key: ValueKey( fontSize: 12,
'${lastEvent?.eventId}_${lastEvent?.type}_${lastEvent?.redacted}', color: theme.colorScheme.outline,
), ),
future: needLastEventSender ),
? lastEvent.calcLocalizedBody( ),
MatrixLocals(L10n.of(context)), ],
hideReply: true, ),
hideEdit: true, subtitle: Row(
plaintextBody: true, crossAxisAlignment: CrossAxisAlignment.start,
removeMarkdown: true, mainAxisAlignment: MainAxisAlignment.center,
withSenderNamePrefix: (!isDirectChat || children: <Widget>[
directChatMatrixId != if (typingText.isEmpty &&
room.lastEvent?.senderId), ownMessage &&
) widget.room.lastEvent!.status.isSending) ...[
: null, const SizedBox(
initialData: width: 16,
lastEvent?.calcLocalizedBodyFallback( height: 16,
MatrixLocals(L10n.of(context)), child: CircularProgressIndicator.adaptive(strokeWidth: 2),
hideReply: true, ),
hideEdit: true, const SizedBox(width: 4),
plaintextBody: true, ],
removeMarkdown: true, AnimatedContainer(
withSenderNamePrefix: (!isDirectChat || width: typingText.isEmpty ? 0 : 18,
directChatMatrixId != clipBehavior: Clip.hardEdge,
room.lastEvent?.senderId), decoration: const BoxDecoration(),
), duration: FluffyThemes.animationDuration,
builder: (context, snapshot) => Text( curve: FluffyThemes.animationCurve,
room.membership == Membership.invite padding: const EdgeInsets.only(right: 4),
? room child: Icon(
.getState( Icons.edit_outlined,
EventTypes.RoomMember, color: theme.colorScheme.secondary,
room.client.userID!, size: 14,
) ),
?.content ),
.tryGet<String>('reason') ?? Expanded(
(isDirectChat child: widget.room.isSpace &&
? L10n.of(context).newChatRequest widget.room.membership == Membership.join
: L10n.of(context) ? Text(
.inviteGroupChat) L10n.of(context).countChatsAndCountParticipants(
: snapshot.data ?? widget.room.spaceChildren.length,
L10n.of(context).noMessagesYet, (widget.room.summary.mJoinedMemberCount ?? 1),
softWrap: false, ),
maxLines: room.notificationCount >= 1 ? 2 : 1, style: TextStyle(color: theme.colorScheme.outline),
overflow: TextOverflow.ellipsis, )
style: TextStyle( : typingText.isNotEmpty
color: unread || room.hasNewMessages ? Text(
? theme.colorScheme.onSurface typingText,
: theme.colorScheme.outline, style: TextStyle(
decoration: room.lastEvent?.redacted == true color: theme.colorScheme.primary,
),
maxLines: 1,
softWrap: false,
)
: Text(
widget.room.membership == Membership.invite
? widget.room
.getState(
EventTypes.RoomMember,
widget.room.client.userID!,
)
?.content
.tryGet<String>('reason') ??
(isDirectChat
? L10n.of(context).newChatRequest
: L10n.of(context).inviteGroupChat)
: lastEventBody ??
L10n.of(context).noMessagesYet,
softWrap: false,
maxLines:
widget.room.notificationCount >= 1 ? 2 : 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: unread || widget.room.hasNewMessages
? theme.colorScheme.onSurface
: theme.colorScheme.outline,
decoration:
widget.room.lastEvent?.redacted == true
? TextDecoration.lineThrough ? TextDecoration.lineThrough
: null, : null,
),
),
), ),
),
const SizedBox(width: 8),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 7),
height: unreadBubbleSize,
width: !hasNotifications && !unread && !room.hasNewMessages
? 0
: (unreadBubbleSize - 9) *
room.notificationCount.toString().length +
9,
decoration: BoxDecoration(
color: room.highlightCount > 0
? theme.colorScheme.error
: hasNotifications || room.markedUnread
? theme.colorScheme.primary
: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(7),
),
child: hasNotifications
? Text(
room.notificationCount.toString(),
style: TextStyle(
color: room.highlightCount > 0
? theme.colorScheme.onError
: hasNotifications
? theme.colorScheme.onPrimary
: theme.colorScheme.onPrimaryContainer,
fontSize: 13,
fontWeight: FontWeight.w500,
), ),
textAlign: TextAlign.center, ),
) const SizedBox(width: 8),
: const SizedBox.shrink(), AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 7),
height: unreadBubbleSize,
width: !hasNotifications &&
!unread &&
!widget.room.hasNewMessages
? 0
: (unreadBubbleSize - 9) *
widget.room.notificationCount.toString().length +
9,
decoration: BoxDecoration(
color: widget.room.highlightCount > 0
? theme.colorScheme.error
: hasNotifications || widget.room.markedUnread
? theme.colorScheme.primary
: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(7),
), ),
], child: hasNotifications
), ? Text(
onTap: onTap, widget.room.notificationCount.toString(),
trailing: onForget == null style: TextStyle(
? room.membership == Membership.invite color: widget.room.highlightCount > 0
? IconButton( ? theme.colorScheme.onError
tooltip: L10n.of(context).declineInvitation, : hasNotifications
icon: const Icon(Icons.delete_forever_outlined), ? theme.colorScheme.onPrimary
color: theme.colorScheme.error, : theme.colorScheme.onPrimaryContainer,
onPressed: () async { fontSize: 13,
final consent = await showOkCancelAlertDialog( fontWeight: FontWeight.w500,
context: context, ),
title: L10n.of(context).declineInvitation, textAlign: TextAlign.center,
message: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
isDestructive: true,
);
if (consent != OkCancelResult.ok) return;
if (!context.mounted) return;
await showFutureLoadingDialog(
context: context,
future: room.leave,
);
},
) )
: null : const SizedBox.shrink(),
: IconButton( ),
icon: const Icon(Icons.delete_outlined), ],
onPressed: onForget,
),
), ),
onTap: widget.onTap,
trailing: widget.onForget == null
? widget.room.membership == Membership.invite
? IconButton(
tooltip: L10n.of(context).declineInvitation,
icon: const Icon(Icons.delete_forever_outlined),
color: theme.colorScheme.error,
onPressed: () async {
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).declineInvitation,
message: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
isDestructive: true,
);
if (consent != OkCancelResult.ok) return;
if (!context.mounted) return;
await showFutureLoadingDialog(
context: context,
future: widget.room.leave,
);
},
)
: null
: IconButton(
icon: const Icon(Icons.delete_outlined),
onPressed: widget.onForget,
),
), ),
), ),
), ),