refactor: 重构聊天显示部分以提升大量消息时web响应速度和在低性能设备上的表现 (#297)
* refactor: optimize message feed rendering and state management - Removed unnecessary initialization state in VirtualMessageFeed. - Updated message data subscription to only track visible messages, improving performance. - Simplified props in Layout component for VirtualMessageFeed and Send components. * refactor: 针对低性能设备进行更多优化,包括消息隔离等 - Added a script to suppress ResizeObserver errors in index.html. - Introduced memoized selectors for efficient message retrieval in message selectors. - Optimized message insertion logic in message slices to reduce unnecessary sorting. - Improved memoization in Message component for better rendering performance. - Enhanced MessageInput to reset editor state on chat changes. - Updated VirtualMessageFeed to manage visible messages more efficiently and reduce initial load for better performance on low-end devices. * chore: bump version to v0.9.50
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vocechat-web",
|
||||
"version": "0.9.49",
|
||||
"version": "0.9.50",
|
||||
"homepage": "https://voce.chat",
|
||||
"dependencies": {
|
||||
"@metamask/onboarding": "^1.0.1",
|
||||
|
||||
@@ -8,6 +8,55 @@
|
||||
/>
|
||||
<meta name="theme-color" content="#527ff1" />
|
||||
<meta name="description" content="Your private chat APP" />
|
||||
|
||||
<!-- Suppress ResizeObserver errors - must be first script -->
|
||||
<script>
|
||||
// Suppress benign ResizeObserver errors from virtualized lists
|
||||
// This is a known browser issue and doesn't affect functionality
|
||||
// Reference: https://github.com/WICG/resize-observer/issues/38
|
||||
|
||||
(function() {
|
||||
function isResizeObserverError(message) {
|
||||
return message && (
|
||||
message.includes('ResizeObserver loop') ||
|
||||
message.includes('ResizeObserver loop completed') ||
|
||||
message.includes('ResizeObserver loop limit exceeded')
|
||||
);
|
||||
}
|
||||
|
||||
// Intercept window.onerror
|
||||
const originalOnError = window.onerror;
|
||||
window.onerror = function(message, source, lineno, colno, error) {
|
||||
if (typeof message === 'string' && isResizeObserverError(message)) {
|
||||
return true; // Suppress ResizeObserver errors
|
||||
}
|
||||
if (originalOnError) {
|
||||
return originalOnError.apply(this, arguments);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Intercept error events (capture phase to catch early)
|
||||
window.addEventListener('error', function(e) {
|
||||
if (e.message && isResizeObserverError(e.message)) {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
return true;
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Intercept console.error
|
||||
const originalConsoleError = console.error;
|
||||
console.error = function() {
|
||||
const args = Array.from(arguments);
|
||||
if (args.length > 0 && typeof args[0] === 'string' && isResizeObserverError(args[0])) {
|
||||
return; // Suppress
|
||||
}
|
||||
originalConsoleError.apply(console, args);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
|
||||
<!-- splash -->
|
||||
<link
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '../store';
|
||||
import { ChatContext } from '@/types/common';
|
||||
|
||||
// Base selectors - these extract raw data from the store
|
||||
const selectMessageStore = (state: RootState) => state.message;
|
||||
const selectChannelMessages = (state: RootState) => state.channelMessage;
|
||||
const selectUserMessages = (state: RootState) => state.userMessage;
|
||||
|
||||
// Create a memoized selector for visible messages
|
||||
// This selector will only recompute when the specific messages change, not when any message in the store changes
|
||||
export const makeSelectVisibleMessages = () => {
|
||||
return createSelector(
|
||||
[
|
||||
selectMessageStore,
|
||||
(_: RootState, mids: number[]) => mids
|
||||
],
|
||||
(messageStore, mids) => {
|
||||
// Create a stable object that only changes when the actual message content changes
|
||||
const messages: Record<number, any> = {};
|
||||
mids.forEach(mid => {
|
||||
if (messageStore[mid]) {
|
||||
messages[mid] = messageStore[mid];
|
||||
}
|
||||
});
|
||||
return messages;
|
||||
},
|
||||
{
|
||||
// Use custom equality check to prevent unnecessary recomputation
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: (a, b) => {
|
||||
// Check if the message objects are the same
|
||||
const aKeys = Object.keys(a);
|
||||
const bKeys = Object.keys(b);
|
||||
if (aKeys.length !== bKeys.length) return false;
|
||||
|
||||
for (const key of aKeys) {
|
||||
if (a[key] !== b[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Create a memoized selector for message IDs
|
||||
export const makeSelectMessageIds = () => {
|
||||
return createSelector(
|
||||
[
|
||||
selectChannelMessages,
|
||||
selectUserMessages,
|
||||
(_: RootState, context: ChatContext, id: number) => ({ context, id })
|
||||
],
|
||||
(channelMessages, userMessages, { context, id }) => {
|
||||
return context === 'dm'
|
||||
? userMessages.byId[id] ?? []
|
||||
: channelMessages[id] ?? [];
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -26,9 +26,17 @@ const channelMsgSlice = createSlice({
|
||||
const midExisted = state[id]!.findIndex((id) => id == mid) > -1;
|
||||
const localMsgExisted = state[id]!.findIndex((id) => id == local_id) > -1;
|
||||
if (midExisted || localMsgExisted) return;
|
||||
// 每次入库,都排序
|
||||
const newArr = [...(state[id] as number[]), +mid].sort((a, b) => a - b);
|
||||
state[id] = newArr;
|
||||
|
||||
// Optimize: check if new message should be at the end (most common case)
|
||||
const lastMid = state[id]![state[id]!.length - 1];
|
||||
if (+mid > lastMid) {
|
||||
// New message is newer, just push to end - no need to sort
|
||||
state[id]!.push(+mid);
|
||||
} else {
|
||||
// Need to insert in sorted position
|
||||
const newArr = [...(state[id] as number[]), +mid].sort((a, b) => a - b);
|
||||
state[id] = newArr;
|
||||
}
|
||||
} else {
|
||||
state[id] = [+mid];
|
||||
}
|
||||
|
||||
@@ -28,9 +28,18 @@ const userMsgSlice = createSlice({
|
||||
const midExisted = state.byId[id].findIndex((id: number) => id == mid) > -1;
|
||||
const localMsgExisted = state.byId[id].findIndex((id: number) => id == local_id) > -1;
|
||||
if (midExisted || localMsgExisted) return;
|
||||
// 每次入库,都排序
|
||||
const newArr = [...state.byId[id], +mid].sort((a, b) => a - b);
|
||||
state.byId[id] = newArr;
|
||||
|
||||
// Optimize: check if new message should be at the end (most common case)
|
||||
const lastMid = state.byId[id][state.byId[id].length - 1];
|
||||
if (+mid > lastMid) {
|
||||
// New message is newer, just push to end - no need to sort
|
||||
state.byId[id].push(+mid);
|
||||
} else {
|
||||
// Need to insert in sorted position
|
||||
const newArr = [...state.byId[id], +mid].sort((a, b) => a - b);
|
||||
state.byId[id] = newArr;
|
||||
}
|
||||
|
||||
// 只要有新消息,就检查下
|
||||
if (state.ids.findIndex((uid) => uid == id) == -1) {
|
||||
state.ids.push(+id);
|
||||
|
||||
@@ -242,5 +242,12 @@ const Message: FC<IProps> = ({
|
||||
);
|
||||
};
|
||||
export default React.memo(Message, (prevs, nexts) => {
|
||||
return prevs.mid == nexts.mid;
|
||||
// More precise memo comparison for better performance
|
||||
return (
|
||||
prevs.mid === nexts.mid &&
|
||||
prevs.readOnly === nexts.readOnly &&
|
||||
prevs.read === nexts.read &&
|
||||
prevs.contextId === nexts.contextId &&
|
||||
prevs.context === nexts.context
|
||||
);
|
||||
});
|
||||
|
||||
@@ -48,6 +48,14 @@ export default function MessageInput({
|
||||
const handleSendMessage = () => {
|
||||
sendMessage();
|
||||
};
|
||||
|
||||
// Reset editor when chat changes without remounting
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.reset();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const text = getMessageFromPlateValues(input as ParagraphInput[]);
|
||||
updateMessage(text);
|
||||
@@ -89,7 +97,6 @@ export default function MessageInput({
|
||||
<Plate
|
||||
// @ts-ignore
|
||||
editorRef={editorRef}
|
||||
key={id}
|
||||
id={id}
|
||||
onChange={(values) => {
|
||||
setInput(values);
|
||||
@@ -100,7 +107,6 @@ export default function MessageInput({
|
||||
<Editor
|
||||
sendMessage={handleSendMessage}
|
||||
// className="px-2 py-3"
|
||||
autoFocus
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<MentionCombobox items={items} />
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
const CustomList = ({ style, ref, ...props }: any) => {
|
||||
return <div style={{ ...style, width: `calc(100% - 2rem)` }} {...props} ref={ref} />;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...style,
|
||||
width: `calc(100% - 2rem)`,
|
||||
// CSS performance hint for better scrolling performance
|
||||
// Note: avoid 'contain' as it interferes with ResizeObserver in virtuoso
|
||||
willChange: 'transform'
|
||||
}}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomList;
|
||||
|
||||
@@ -11,6 +11,8 @@ import { renderMessageFragment } from "../../utils";
|
||||
import NewMessageBottomTip from "../NewMessageBottomTip";
|
||||
import CustomHeader from "./CustomHeader";
|
||||
import CustomList from "./CustomList";
|
||||
import { makeSelectVisibleMessages } from "@/app/selectors/message";
|
||||
import { updateSelectMessages } from "@/app/slices/ui";
|
||||
|
||||
type Props = {
|
||||
context: ChatContext;
|
||||
@@ -28,13 +30,21 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
// const { t } = useTranslation("chat");
|
||||
// const [firstItemIndex, setFirstItemIndex] = useState(firstMsgIndex);
|
||||
const [atBottom, setAtBottom] = useState(false);
|
||||
const [visibleCount, setVisibleCount] = useState(100);
|
||||
const isInitializing = useRef(false);
|
||||
// Reduce initial visible count for better performance on low-end devices
|
||||
const [visibleCount, setVisibleCount] = useState(50);
|
||||
const [loadMoreMessage, { isLoading: loadingMore, isSuccess, data: historyData }] =
|
||||
useLazyLoadMoreMessagesQuery();
|
||||
const vList = useRef<VirtuosoHandle | null>(null);
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
const updateReadDebounced = useDebounce(updateReadIndex, 300);
|
||||
// Increase debounce time for better performance on low-end devices
|
||||
const updateReadDebounced = useDebounce(updateReadIndex, 500);
|
||||
// Store debounced function in ref to avoid recreating itemContent
|
||||
const updateReadDebouncedRef = useRef(updateReadDebounced);
|
||||
updateReadDebouncedRef.current = updateReadDebounced;
|
||||
|
||||
// Create memoized selector instance for this component
|
||||
const selectVisibleMessages = useMemo(() => makeSelectVisibleMessages(), []);
|
||||
|
||||
const historyMid = useAppSelector(
|
||||
(store) =>
|
||||
context == "dm"
|
||||
@@ -47,35 +57,61 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
context == "dm" ? store.userMessage.byId[id] ?? [] : store.channelMessage[id] ?? [],
|
||||
shallowEqual
|
||||
);
|
||||
|
||||
|
||||
// Stabilize mids array reference - only create new array if content actually changed
|
||||
const mids = useMemo(() => {
|
||||
if (allMids.length <= visibleCount) return allMids;
|
||||
return allMids.slice(-visibleCount);
|
||||
}, [allMids, visibleCount]);
|
||||
|
||||
// Use ref to track previous mids for comparison
|
||||
const prevMidsRef = useRef<number[]>([]);
|
||||
const stableMids = useMemo(() => {
|
||||
// Check if mids actually changed
|
||||
if (prevMidsRef.current.length === mids.length) {
|
||||
let same = true;
|
||||
for (let i = 0; i < mids.length; i++) {
|
||||
if (prevMidsRef.current[i] !== mids[i]) {
|
||||
same = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (same) return prevMidsRef.current;
|
||||
}
|
||||
prevMidsRef.current = mids;
|
||||
return mids;
|
||||
}, [mids]);
|
||||
const selects = useAppSelector(
|
||||
(store) => store.ui.selectMessages[`${context}_${id}`],
|
||||
shallowEqual
|
||||
);
|
||||
const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual);
|
||||
const messageData = useAppSelector((store) => store.message, shallowEqual);
|
||||
|
||||
// Create stable toggleSelect function
|
||||
const toggleSelect = useCallback((mid: number, selected: boolean) => {
|
||||
const operation = selected ? "remove" : "add";
|
||||
dispatch(updateSelectMessages({ context, id, operation, data: mid }));
|
||||
}, [context, id, dispatch]);
|
||||
|
||||
// Use memoized selector to get message data - this will only recompute when the specific messages change
|
||||
const messageData = useAppSelector((store) => selectVisibleMessages(store, stableMids));
|
||||
|
||||
const readChannels = useAppSelector((store) => store.footprint.readChannels, shallowEqual);
|
||||
const readUsers = useAppSelector((store) => store.footprint.readUsers, shallowEqual);
|
||||
|
||||
useEffect(() => {
|
||||
isInitializing.current = true;
|
||||
setVisibleCount(100);
|
||||
setTimeout(() => {
|
||||
isInitializing.current = false;
|
||||
}, 500);
|
||||
// Reset visible count when switching chats
|
||||
setVisibleCount(50);
|
||||
setAtBottom(false);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const feedId = `VOCECHAT_FEED_${context}_${id}`;
|
||||
const feedEle = document.getElementById(feedId);
|
||||
|
||||
|
||||
const handleScrollToMessage = (evt: CustomEvent) => {
|
||||
const { mid } = evt.detail;
|
||||
const index = mids.findIndex((m) => m === mid);
|
||||
const index = stableMids.findIndex((m) => m === mid);
|
||||
if (index !== -1 && vList.current) {
|
||||
vList.current.scrollToIndex({ index, align: "center", behavior: "smooth" });
|
||||
setTimeout(() => {
|
||||
@@ -114,12 +150,12 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
feedEle?.addEventListener('scrollToMessage', handleScrollToMessage as EventListener);
|
||||
return () => {
|
||||
feedEle?.removeEventListener('scrollToMessage', handleScrollToMessage as EventListener);
|
||||
};
|
||||
}, [context, id, mids, allMids]);
|
||||
}, [context, id, stableMids, allMids]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess && historyData) {
|
||||
@@ -132,7 +168,7 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
dispatch(updateHistoryMark({ type: context, id, mid: `${mid}` }));
|
||||
}
|
||||
}
|
||||
}, [isSuccess, historyData, mids, context, id]);
|
||||
}, [isSuccess, historyData, stableMids, context, id]);
|
||||
// useEffect(() => {
|
||||
// console.log("diff mids", prevMids, mids);
|
||||
// const newCount = mids.length - prevMids.length;
|
||||
@@ -142,9 +178,10 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
// 加载更多
|
||||
const handleTopStateChange = (isTop: boolean) => {
|
||||
console.log("reach top ", isTop);
|
||||
if (isTop && !isInitializing.current) {
|
||||
if (isTop) {
|
||||
if (allMids.length > visibleCount) {
|
||||
setVisibleCount(prev => Math.min(prev + 100, allMids.length));
|
||||
// Load 50 messages at a time for better performance
|
||||
setVisibleCount(prev => Math.min(prev + 50, allMids.length));
|
||||
} else {
|
||||
if (historyMid === "reached") return;
|
||||
let lastMid = allMids.slice(0, 1)[0];
|
||||
@@ -157,7 +194,7 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
};
|
||||
// 自动跟随
|
||||
const handleFollowOutput = (isAtBottom: boolean) => {
|
||||
const [lastMid] = mids ? mids.slice(-1) : [0];
|
||||
const [lastMid] = stableMids ? stableMids.slice(-1) : [0];
|
||||
const ts = new Date().getTime();
|
||||
// tricky
|
||||
const isSentByMyself = ts - lastMid < 1000;
|
||||
@@ -180,7 +217,7 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
scrollToMessage: (mid: number) => {
|
||||
const index = mids.findIndex((m) => m === mid);
|
||||
const index = stableMids.findIndex((m) => m === mid);
|
||||
if (index !== -1 && vList.current) {
|
||||
vList.current.scrollToIndex({ index, align: "center", behavior: "smooth" });
|
||||
} else if (allMids.includes(mid)) {
|
||||
@@ -196,31 +233,54 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
}));
|
||||
|
||||
const readIndex = context == "channel" ? readChannels[id] : readUsers[id];
|
||||
|
||||
// 缓存itemContent函数,避免每次都创建新函数
|
||||
|
||||
// Store frequently changing values in refs to avoid recreating itemContent
|
||||
const messageDataRef = useRef(messageData);
|
||||
const midsRef = useRef(stableMids);
|
||||
const readIndexRef = useRef(readIndex);
|
||||
const loginUidRef = useRef(loginUid);
|
||||
const selectsRef = useRef(selects);
|
||||
const toggleSelectRef = useRef(toggleSelect);
|
||||
|
||||
messageDataRef.current = messageData;
|
||||
midsRef.current = stableMids;
|
||||
readIndexRef.current = readIndex;
|
||||
loginUidRef.current = loginUid;
|
||||
selectsRef.current = selects;
|
||||
toggleSelectRef.current = toggleSelect;
|
||||
|
||||
// Stable itemContent function - only recreate when context or id changes
|
||||
const itemContent = useCallback((idx: number, mid: number) => {
|
||||
const curr = messageData[mid];
|
||||
const curr = messageDataRef.current[mid];
|
||||
if (!curr) return <div className="w-full h-[1px] invisible"></div>;
|
||||
const isFirst = idx == 0;
|
||||
const prev = isFirst ? null : messageData[mids[idx - 1]];
|
||||
const read = curr?.from_uid == loginUid || mid <= readIndex;
|
||||
const prev = isFirst ? null : messageDataRef.current[midsRef.current[idx - 1]];
|
||||
// Optimize read calculation: once a message is read, it stays read
|
||||
// This prevents unnecessary re-renders when readIndex updates
|
||||
const read = curr?.from_uid == loginUidRef.current || mid <= readIndexRef.current;
|
||||
const selected = !!(selectsRef.current && selectsRef.current.find((s: number) => s == mid));
|
||||
const handleToggleSelect = () => toggleSelectRef.current(mid, selected);
|
||||
return renderMessageFragment({
|
||||
selectMode: !!selects,
|
||||
updateReadIndex: updateReadDebounced,
|
||||
selectMode: !!selectsRef.current,
|
||||
updateReadIndex: updateReadDebouncedRef.current,
|
||||
read,
|
||||
prev,
|
||||
curr,
|
||||
contextId: id,
|
||||
context
|
||||
context,
|
||||
selected,
|
||||
toggleSelect: handleToggleSelect
|
||||
});
|
||||
}, [messageData, mids, loginUid, readIndex, selects, updateReadDebounced, id, context]);
|
||||
}, [id, context]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Virtuoso
|
||||
// logLevel={LogLevel.DEBUG}
|
||||
overscan={50}
|
||||
increaseViewportBy={{ top: 0, bottom: 400 }}
|
||||
// Reduce overscan for better performance on low-end devices
|
||||
overscan={20}
|
||||
// Reduce viewport extension for better performance
|
||||
increaseViewportBy={{ top: 0, bottom: 200 }}
|
||||
context={{ loadingMore, id, isChannel: context == "channel" }}
|
||||
id={`VOCECHAT_FEED_${context}_${id}`}
|
||||
className="px-1 md:px-4 py-4.5 overflow-x-hidden overflow-y-scroll"
|
||||
@@ -230,10 +290,10 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
Header: CustomHeader
|
||||
}}
|
||||
// firstItemIndex={firstItemIndex}
|
||||
initialTopMostItemIndex={mids.length - 1}
|
||||
initialTopMostItemIndex={stableMids.length - 1}
|
||||
alignToBottom
|
||||
// startReached={handleLoadMore}
|
||||
data={mids}
|
||||
data={stableMids}
|
||||
atTopThreshold={context == "channel" ? 160 : 0}
|
||||
atTopStateChange={handleTopStateChange}
|
||||
atBottomStateChange={handleBottomStateChange}
|
||||
|
||||
@@ -103,7 +103,7 @@ const Layout: FC<Props> = ({
|
||||
{context == "dm" && <DMVoice uid={to} />}
|
||||
{context == "dm" && <AddContactTip uid={to} />}
|
||||
{/* 消息流 */}
|
||||
<VirtualMessageFeed ref={feedRef} key={`${context}_${to}`} context={context} id={to} />
|
||||
<VirtualMessageFeed ref={feedRef} context={context} id={to} />
|
||||
{/* 发送框 */}
|
||||
<div className={`px-2 py-0 md:p-4 ${selects ? "selecting" : ""}`}>
|
||||
{readonly ? (
|
||||
@@ -112,7 +112,7 @@ const Layout: FC<Props> = ({
|
||||
<LicenseUpgradeTip />
|
||||
) : (
|
||||
<div className={clsx(`flex justify-center`, selects && "hidden")}>
|
||||
<Send key={to} id={to} context={context} />
|
||||
<Send id={to} context={context} />
|
||||
</div>
|
||||
)}
|
||||
{selects && <Operations context={context} id={to} />}
|
||||
|
||||
+24
-14
@@ -1,4 +1,5 @@
|
||||
// import React from "react";
|
||||
import React from "react";
|
||||
import { shallowEqual, useDispatch } from "react-redux";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@@ -94,18 +95,7 @@ export const renderPreviewMessage = (message = null) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
const MessageWrapper = ({ selectMode = false, context, id, mid, divider, children, ...rest }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const selects = useAppSelector(
|
||||
(store) => store.ui.selectMessages[`${context}_${id}`],
|
||||
shallowEqual
|
||||
);
|
||||
const selected = !!(selects && selects.find((s) => s == mid));
|
||||
const toggleSelect = () => {
|
||||
const operation = selected ? "remove" : "add";
|
||||
dispatch(updateSelectMessages({ context, id, operation, data: mid }));
|
||||
};
|
||||
const MessageWrapper = React.memo(({ selectMode = false, context, id, mid, divider, selected = false, toggleSelect, children, ...rest }) => {
|
||||
return (
|
||||
<div className={`group flex flex-col items-start gap-2 relative w-full `} {...rest}>
|
||||
{divider}
|
||||
@@ -125,7 +115,21 @@ const MessageWrapper = ({ selectMode = false, context, id, mid, divider, childre
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}, (prevProps, nextProps) => {
|
||||
// Optimize re-renders by comparing only necessary props
|
||||
// Compare divider by value, not reference
|
||||
const dividerSame = prevProps.divider === nextProps.divider ||
|
||||
(prevProps.divider?.props?.content === nextProps.divider?.props?.content);
|
||||
|
||||
return (
|
||||
prevProps.mid === nextProps.mid &&
|
||||
prevProps.selectMode === nextProps.selectMode &&
|
||||
prevProps.selected === nextProps.selected &&
|
||||
prevProps.context === nextProps.context &&
|
||||
prevProps.id === nextProps.id &&
|
||||
dividerSame
|
||||
);
|
||||
});
|
||||
type Params = {
|
||||
readonly?: boolean;
|
||||
selectMode: boolean;
|
||||
@@ -135,6 +139,8 @@ type Params = {
|
||||
curr: object | null;
|
||||
contextId: number;
|
||||
context: ChatContext;
|
||||
selected?: boolean;
|
||||
toggleSelect?: () => void;
|
||||
};
|
||||
export const renderMessageFragment = ({
|
||||
readonly = false,
|
||||
@@ -144,7 +150,9 @@ export const renderMessageFragment = ({
|
||||
prev,
|
||||
curr = null,
|
||||
contextId = 0,
|
||||
context = "dm"
|
||||
context = "dm",
|
||||
selected = false,
|
||||
toggleSelect
|
||||
}: Params) => {
|
||||
if (!curr) return <div className="w-full h-[1px] invisible"></div>;
|
||||
let { created_at, mid } = curr;
|
||||
@@ -169,6 +177,8 @@ export const renderMessageFragment = ({
|
||||
id={contextId}
|
||||
mid={mid}
|
||||
selectMode={selectMode}
|
||||
selected={selected}
|
||||
toggleSelect={toggleSelect}
|
||||
divider={divider ? <Divider className="w-full" content={divider}></Divider> : null}
|
||||
>
|
||||
<Message
|
||||
|
||||
Reference in New Issue
Block a user