⚡ Optimize performance for message item
This commit is contained in:
@@ -39,6 +39,7 @@ import "package:island/widgets/chat/chat_input.dart";
|
|||||||
import "package:island/widgets/chat/chat_link_attachments.dart";
|
import "package:island/widgets/chat/chat_link_attachments.dart";
|
||||||
import "package:island/widgets/chat/public_room_preview.dart";
|
import "package:island/widgets/chat/public_room_preview.dart";
|
||||||
import "package:island/screens/thought/think_sheet.dart";
|
import "package:island/screens/thought/think_sheet.dart";
|
||||||
|
import "package:island/screens/chat/widgets/message_item_wrapper.dart";
|
||||||
|
|
||||||
class ChatRoomScreen extends HookConsumerWidget {
|
class ChatRoomScreen extends HookConsumerWidget {
|
||||||
final String id;
|
final String id;
|
||||||
@@ -178,6 +179,38 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final isSelectionMode = useState<bool>(false);
|
final isSelectionMode = useState<bool>(false);
|
||||||
final selectedMessages = useState<Set<String>>({});
|
final selectedMessages = useState<Set<String>>({});
|
||||||
|
|
||||||
|
final roomOpenTime = useMemoized(() => DateTime.now());
|
||||||
|
|
||||||
|
final onMessageAction = useCallback(
|
||||||
|
(String action, LocalChatMessage message) {
|
||||||
|
switch (action) {
|
||||||
|
case MessageItemAction.delete:
|
||||||
|
messagesNotifier.deleteMessage(message.id);
|
||||||
|
case MessageItemAction.edit:
|
||||||
|
messageEditingTo.value = message.toRemoteMessage();
|
||||||
|
messageController.text = messageEditingTo.value?.content ?? '';
|
||||||
|
attachments.value =
|
||||||
|
messageEditingTo.value!.attachments
|
||||||
|
.map((e) => UniversalFile.fromAttachment(e))
|
||||||
|
.toList();
|
||||||
|
case MessageItemAction.forward:
|
||||||
|
messageForwardingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.reply:
|
||||||
|
messageReplyingTo.value = message.toRemoteMessage();
|
||||||
|
case MessageItemAction.resend:
|
||||||
|
messagesNotifier.retryMessage(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
messagesNotifier,
|
||||||
|
messageEditingTo,
|
||||||
|
messageController,
|
||||||
|
attachments,
|
||||||
|
messageForwardingTo,
|
||||||
|
messageReplyingTo,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
var isLoading = false;
|
var isLoading = false;
|
||||||
var isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
var isScrollingToMessage = false; // Flag to prevent scroll conflicts
|
||||||
|
|
||||||
@@ -627,7 +660,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 16,
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 8 + inputHeight.value,
|
bottom: MediaQuery.of(context).padding.bottom + 8 + inputHeight.value,
|
||||||
),
|
),
|
||||||
child: SuperListView.builder(
|
child: SuperListView.builder(
|
||||||
@@ -659,138 +691,30 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final key = Key('$messageKeyPrefix${message.nonce ?? message.id}');
|
final key = Key('$messageKeyPrefix${message.nonce ?? message.id}');
|
||||||
|
|
||||||
final messageWidget = chatIdentity.when(
|
return MessageItemWrapper(
|
||||||
skipError: true,
|
key: key,
|
||||||
data:
|
message: message,
|
||||||
(identity) => GestureDetector(
|
index: index,
|
||||||
onLongPress: () {
|
isLastInGroup: isLastInGroup,
|
||||||
if (!isSelectionMode.value) {
|
isSelectionMode: isSelectionMode.value,
|
||||||
toggleSelectionMode();
|
selectedMessages: selectedMessages.value,
|
||||||
toggleMessageSelection(message.id);
|
chatIdentity: chatIdentity,
|
||||||
}
|
toggleSelectionMode: toggleSelectionMode,
|
||||||
},
|
toggleMessageSelection: toggleMessageSelection,
|
||||||
onTap: () {
|
onMessageAction: onMessageAction,
|
||||||
if (isSelectionMode.value) {
|
onJump:
|
||||||
toggleMessageSelection(message.id);
|
(messageId) => scrollToMessage(
|
||||||
}
|
messageId: messageId,
|
||||||
},
|
messageList: messageList,
|
||||||
child: Container(
|
messagesNotifier: messagesNotifier,
|
||||||
color:
|
listController: listController,
|
||||||
selectedMessages.value.contains(message.id)
|
scrollController: scrollController,
|
||||||
? Theme.of(
|
ref: ref,
|
||||||
context,
|
|
||||||
).colorScheme.primaryContainer.withOpacity(0.3)
|
|
||||||
: null,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
MessageItem(
|
|
||||||
key: settings.disableAnimation ? key : null,
|
|
||||||
message: message,
|
|
||||||
isCurrentUser: identity?.id == message.senderId,
|
|
||||||
onAction:
|
|
||||||
isSelectionMode.value
|
|
||||||
? null
|
|
||||||
: (action) {
|
|
||||||
switch (action) {
|
|
||||||
case MessageItemAction.delete:
|
|
||||||
messagesNotifier.deleteMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
case MessageItemAction.edit:
|
|
||||||
messageEditingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
messageController.text =
|
|
||||||
messageEditingTo.value?.content ??
|
|
||||||
'';
|
|
||||||
attachments.value =
|
|
||||||
messageEditingTo.value!.attachments
|
|
||||||
.map(
|
|
||||||
(e) =>
|
|
||||||
UniversalFile.fromAttachment(
|
|
||||||
e,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
case MessageItemAction.forward:
|
|
||||||
messageForwardingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
case MessageItemAction.reply:
|
|
||||||
messageReplyingTo.value =
|
|
||||||
message.toRemoteMessage();
|
|
||||||
case MessageItemAction.resend:
|
|
||||||
messagesNotifier.retryMessage(
|
|
||||||
message.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onJump:
|
|
||||||
(messageId) => scrollToMessage(
|
|
||||||
messageId: messageId,
|
|
||||||
messageList: messageList,
|
|
||||||
messagesNotifier: messagesNotifier,
|
|
||||||
listController: listController,
|
|
||||||
scrollController: scrollController,
|
|
||||||
ref: ref,
|
|
||||||
),
|
|
||||||
progress: attachmentProgress.value[message.id],
|
|
||||||
showAvatar: isLastInGroup,
|
|
||||||
isSelectionMode: isSelectionMode.value,
|
|
||||||
isSelected: selectedMessages.value.contains(
|
|
||||||
message.id,
|
|
||||||
),
|
|
||||||
onToggleSelection: toggleMessageSelection,
|
|
||||||
onEnterSelectionMode: () {
|
|
||||||
if (!isSelectionMode.value) toggleSelectionMode();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (selectedMessages.value.contains(message.id))
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.check,
|
|
||||||
size: 12,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
loading:
|
attachmentProgress: attachmentProgress.value,
|
||||||
() => MessageItem(
|
disableAnimation: settings.disableAnimation,
|
||||||
message: message,
|
roomOpenTime: roomOpenTime,
|
||||||
isCurrentUser: false,
|
|
||||||
onAction: null,
|
|
||||||
progress: null,
|
|
||||||
showAvatar: false,
|
|
||||||
onJump: (_) {},
|
|
||||||
),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return settings.disableAnimation
|
|
||||||
? messageWidget
|
|
||||||
: TweenAnimationBuilder<double>(
|
|
||||||
key: key,
|
|
||||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
|
||||||
duration: Duration(milliseconds: 400 + (index % 5) * 50),
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
builder:
|
|
||||||
(context, animationValue, child) => Transform.translate(
|
|
||||||
offset: Offset(0, 20 * (1 - animationValue)),
|
|
||||||
child: Opacity(opacity: animationValue, child: child),
|
|
||||||
),
|
|
||||||
child: messageWidget,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
169
lib/screens/chat/widgets/message_item_wrapper.dart
Normal file
169
lib/screens/chat/widgets/message_item_wrapper.dart
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:island/database/message.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/widgets/chat/message_item.dart';
|
||||||
|
|
||||||
|
// Provider to track animated messages to prevent replay
|
||||||
|
final animatedMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||||
|
|
||||||
|
class MessageItemWrapper extends HookConsumerWidget {
|
||||||
|
final LocalChatMessage message;
|
||||||
|
final int index;
|
||||||
|
final bool isLastInGroup;
|
||||||
|
final bool isSelectionMode;
|
||||||
|
final Set<String> selectedMessages;
|
||||||
|
final AsyncValue<SnChatMember?> chatIdentity;
|
||||||
|
final VoidCallback toggleSelectionMode;
|
||||||
|
final Function(String) toggleMessageSelection;
|
||||||
|
final Function(String, LocalChatMessage) onMessageAction;
|
||||||
|
final Function(String) onJump;
|
||||||
|
final Map<String, Map<int, double?>> attachmentProgress;
|
||||||
|
final bool disableAnimation;
|
||||||
|
final DateTime roomOpenTime;
|
||||||
|
|
||||||
|
const MessageItemWrapper({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
required this.index,
|
||||||
|
required this.isLastInGroup,
|
||||||
|
required this.isSelectionMode,
|
||||||
|
required this.selectedMessages,
|
||||||
|
required this.chatIdentity,
|
||||||
|
required this.toggleSelectionMode,
|
||||||
|
required this.toggleMessageSelection,
|
||||||
|
required this.onMessageAction,
|
||||||
|
required this.onJump,
|
||||||
|
required this.attachmentProgress,
|
||||||
|
required this.disableAnimation,
|
||||||
|
required this.roomOpenTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Animation logic
|
||||||
|
final animatedMessages = ref.watch(animatedMessagesProvider);
|
||||||
|
final isNewMessage = message.createdAt.isAfter(roomOpenTime);
|
||||||
|
final hasAnimated = animatedMessages.contains(message.id);
|
||||||
|
|
||||||
|
// Only animate if:
|
||||||
|
// 1. Animation is enabled
|
||||||
|
// 2. Message is new (created after room open)
|
||||||
|
// 3. Has not animated yet
|
||||||
|
final shouldAnimate = !disableAnimation && isNewMessage && !hasAnimated;
|
||||||
|
|
||||||
|
final child = chatIdentity.when(
|
||||||
|
skipError: true,
|
||||||
|
data: (identity) => _buildContent(context, identity),
|
||||||
|
loading: () => _buildLoading(),
|
||||||
|
error: (_, __) => const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!shouldAnimate) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TweenAnimationBuilder<double>(
|
||||||
|
key: ValueKey('anim-${message.id}'), // Ensure unique key for animation
|
||||||
|
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||||
|
duration: Duration(milliseconds: 400 + (index % 5) * 50),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: Offset(0, 20 * (1 - value)),
|
||||||
|
child: Opacity(opacity: value, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onEnd: () {
|
||||||
|
// Mark as animated
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref
|
||||||
|
.read(animatedMessagesProvider.notifier)
|
||||||
|
.update((state) => {...state, message.id});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context, SnChatMember? identity) {
|
||||||
|
final isSelected = selectedMessages.contains(message.id);
|
||||||
|
final isCurrentUser = identity?.id == message.senderId;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onLongPress: () {
|
||||||
|
if (!isSelectionMode) {
|
||||||
|
toggleSelectionMode();
|
||||||
|
toggleMessageSelection(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
if (isSelectionMode) {
|
||||||
|
toggleMessageSelection(message.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color:
|
||||||
|
isSelected
|
||||||
|
? Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primaryContainer.withOpacity(0.3)
|
||||||
|
: null,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
MessageItem(
|
||||||
|
// If animation is disabled, we might want to pass a key to maintain state?
|
||||||
|
// But here we are inside the wrapper.
|
||||||
|
key: ValueKey('item-${message.id}'),
|
||||||
|
message: message,
|
||||||
|
isCurrentUser: isCurrentUser,
|
||||||
|
onAction:
|
||||||
|
isSelectionMode
|
||||||
|
? null
|
||||||
|
: (action) => onMessageAction(action, message),
|
||||||
|
onJump: onJump,
|
||||||
|
progress: attachmentProgress[message.id],
|
||||||
|
showAvatar: isLastInGroup,
|
||||||
|
isSelectionMode: isSelectionMode,
|
||||||
|
isSelected: isSelected,
|
||||||
|
onToggleSelection: toggleMessageSelection,
|
||||||
|
onEnterSelectionMode: () {
|
||||||
|
if (!isSelectionMode) toggleSelectionMode();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (isSelected)
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoading() {
|
||||||
|
return MessageItem(
|
||||||
|
message: message,
|
||||||
|
isCurrentUser: false,
|
||||||
|
onAction: null,
|
||||||
|
progress: null,
|
||||||
|
showAvatar: false,
|
||||||
|
onJump: (_) {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user