refactor: 优化消息加载 (#288)
* refactor: 尝试优化消息加载 - Updated webpack configuration to exclude additional sourcemaps. - Changed local development URL for easier access. - Implemented batch processing for handling chat messages to prevent main thread blocking. - Refined message component to selectively subscribe to user and reaction data. - Enhanced virtual message feed to manage visible message count and improve loading performance. * chore: bump version to v0.9.44
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||
import { batch } from "react-redux";
|
||||
|
||||
import { ChatContext } from "@/types/common";
|
||||
import { ChatMessage, ContentTypeKey, UploadFileResponse } from "@/types/message";
|
||||
@@ -224,9 +225,23 @@ export const messageApi = createApi({
|
||||
const { data: messages } = await queryFulfilled;
|
||||
const fromHistory = true;
|
||||
if (messages?.length) {
|
||||
messages.forEach((msg) => {
|
||||
handleChatMessage(msg, dispatch, getState() as RootState, fromHistory);
|
||||
});
|
||||
// 分批处理消息,避免阻塞主线程
|
||||
const batchSize = 20;
|
||||
const processBatch = (startIndex: number) => {
|
||||
const endIndex = Math.min(startIndex + batchSize, messages.length);
|
||||
batch(() => {
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
handleChatMessage(messages[i], dispatch, getState() as RootState, fromHistory);
|
||||
}
|
||||
});
|
||||
|
||||
if (endIndex < messages.length) {
|
||||
// 使用 setTimeout 让出主线程,继续处理下一批
|
||||
setTimeout(() => processBatch(endIndex), 0);
|
||||
}
|
||||
};
|
||||
|
||||
processBatch(0);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -49,8 +49,10 @@ const Message: FC<IProps> = ({
|
||||
shallowEqual
|
||||
);
|
||||
const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual);
|
||||
const usersData = useAppSelector((store) => store.users.byId, shallowEqual);
|
||||
const reactionMessageData = useAppSelector((store) => store.reactionMessage, shallowEqual);
|
||||
// 只订阅当前消息发送者的用户信息,而不是整个usersData
|
||||
const currUser = useAppSelector((store) => store.users.byId[message?.from_uid || 0], shallowEqual);
|
||||
// 只订阅当前消息的reaction,而不是整个reactionMessageData
|
||||
const reactions = useAppSelector((store) => store.reactionMessage[mid], shallowEqual);
|
||||
|
||||
const toggleEditMessage = () => {
|
||||
setEdit((prev) => !prev);
|
||||
@@ -84,14 +86,18 @@ const Message: FC<IProps> = ({
|
||||
failed = false,
|
||||
} = message;
|
||||
|
||||
const reactions = reactionMessageData[mid];
|
||||
const currUser = usersData[fromUid || 0];
|
||||
// 获取pinInfo中需要的用户信息
|
||||
const pinInfo = getPinInfo(mid);
|
||||
const pinCreatorName = useAppSelector((store) =>
|
||||
pinInfo?.created_by ? store.users.byId[pinInfo.created_by]?.name : undefined,
|
||||
shallowEqual
|
||||
);
|
||||
|
||||
// if (!message) return null;
|
||||
let timePrefix = null;
|
||||
const dayjsTime = dayjs(time);
|
||||
timePrefix = dayjsTime.isToday() ? "Today" : dayjsTime.isYesterday() ? "Yesterday" : null;
|
||||
|
||||
const pinInfo = getPinInfo(mid);
|
||||
// return null;
|
||||
const _key = properties?.local_id || mid;
|
||||
const showExpire = (expires_in ?? 0) > 0;
|
||||
@@ -143,9 +149,7 @@ const Message: FC<IProps> = ({
|
||||
pinInfo && "relative",
|
||||
isSelf && "items-end"
|
||||
)}
|
||||
data-pin-tip={`pinned by ${
|
||||
pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
|
||||
}`}
|
||||
data-pin-tip={`pinned by ${pinCreatorName || ""}`}
|
||||
>
|
||||
{pinInfo && (
|
||||
<span
|
||||
@@ -154,7 +158,7 @@ const Message: FC<IProps> = ({
|
||||
isSelf ? "right-0" : "left-0"
|
||||
)}
|
||||
>
|
||||
{`pinned by ${pinInfo.created_by ? usersData[pinInfo.created_by]?.name : ""}`}
|
||||
{`pinned by ${pinCreatorName || ""}`}
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// @ts-ignore
|
||||
const CustomList = ({ style, ref, ...props }) => {
|
||||
// @ts-ignore
|
||||
const CustomList = ({ style, ref, ...props }: any) => {
|
||||
return <div style={{ ...style, width: `calc(100% - 2rem)` }} {...props} ref={ref} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle } from "react";
|
||||
import { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle, useMemo } from "react";
|
||||
import { shallowEqual, useDispatch } from "react-redux";
|
||||
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
||||
import { useDebounce } from "rooks";
|
||||
@@ -28,6 +28,8 @@ 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);
|
||||
const [loadMoreMessage, { isLoading: loadingMore, isSuccess, data: historyData }] =
|
||||
useLazyLoadMoreMessagesQuery();
|
||||
const vList = useRef<VirtuosoHandle | null>(null);
|
||||
@@ -40,20 +42,33 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
: store.footprint.historyChannels[id] ?? "",
|
||||
shallowEqual
|
||||
);
|
||||
const mids = useAppSelector(
|
||||
const allMids = useAppSelector(
|
||||
(store) =>
|
||||
context == "dm" ? store.userMessage.byId[id] ?? [] : store.channelMessage[id] ?? [],
|
||||
shallowEqual
|
||||
);
|
||||
|
||||
const mids = useMemo(() => {
|
||||
if (allMids.length <= visibleCount) return allMids;
|
||||
return allMids.slice(-visibleCount);
|
||||
}, [allMids, visibleCount]);
|
||||
const selects = useAppSelector(
|
||||
(store) => store.ui.selectMessages[`${context}_${id}`],
|
||||
shallowEqual
|
||||
);
|
||||
const messageData = useAppSelector((store) => store.message || {}, shallowEqual);
|
||||
const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual);
|
||||
const messageData = useAppSelector((store) => store.message, shallowEqual);
|
||||
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);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess && historyData) {
|
||||
if (historyData.length == 0) {
|
||||
@@ -75,14 +90,17 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
// 加载更多
|
||||
const handleTopStateChange = (isTop: boolean) => {
|
||||
console.log("reach top ", isTop);
|
||||
if (isTop) {
|
||||
if (historyMid === "reached") return;
|
||||
let lastMid = mids.slice(0, 1)[0];
|
||||
if (historyMid) {
|
||||
lastMid = +historyMid;
|
||||
if (isTop && !isInitializing.current) {
|
||||
if (allMids.length > visibleCount) {
|
||||
setVisibleCount(prev => Math.min(prev + 100, allMids.length));
|
||||
} else {
|
||||
if (historyMid === "reached") return;
|
||||
let lastMid = allMids.slice(0, 1)[0];
|
||||
if (historyMid) {
|
||||
lastMid = +historyMid;
|
||||
}
|
||||
loadMoreMessage({ context, id, mid: lastMid });
|
||||
}
|
||||
// prevMids = mids;
|
||||
loadMoreMessage({ context, id, mid: lastMid });
|
||||
}
|
||||
};
|
||||
// 自动跟随
|
||||
@@ -101,9 +119,9 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
const handleScrollBottom = useCallback(() => {
|
||||
const vl = vList!.current;
|
||||
if (vl) {
|
||||
vl.scrollToIndex(mids.length - 1);
|
||||
vl.scrollToIndex(allMids.length - 1);
|
||||
}
|
||||
}, [mids]);
|
||||
}, [allMids]);
|
||||
const handleBottomStateChange = (bottom: boolean) => {
|
||||
setAtBottom(bottom);
|
||||
};
|
||||
@@ -113,16 +131,44 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
const index = mids.findIndex((m) => m === mid);
|
||||
if (index !== -1 && vList.current) {
|
||||
vList.current.scrollToIndex({ index, align: "center", behavior: "smooth" });
|
||||
} else if (allMids.includes(mid)) {
|
||||
setVisibleCount(allMids.length);
|
||||
setTimeout(() => {
|
||||
const idx = allMids.findIndex((m) => m === mid);
|
||||
if (idx !== -1 && vList.current) {
|
||||
vList.current.scrollToIndex({ index: idx, align: "center", behavior: "smooth" });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const readIndex = context == "channel" ? readChannels[id] : readUsers[id];
|
||||
|
||||
// 缓存itemContent函数,避免每次都创建新函数
|
||||
const itemContent = useCallback((idx: number, mid: number) => {
|
||||
const curr = messageData[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;
|
||||
return renderMessageFragment({
|
||||
selectMode: !!selects,
|
||||
updateReadIndex: updateReadDebounced,
|
||||
read,
|
||||
prev,
|
||||
curr,
|
||||
contextId: id,
|
||||
context
|
||||
});
|
||||
}, [messageData, mids, loginUid, readIndex, selects, updateReadDebounced, id, context]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Virtuoso
|
||||
// logLevel={LogLevel.DEBUG}
|
||||
overscan={50}
|
||||
increaseViewportBy={{ top: 0, bottom: 400 }}
|
||||
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"
|
||||
@@ -133,6 +179,7 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
}}
|
||||
// firstItemIndex={firstItemIndex}
|
||||
initialTopMostItemIndex={mids.length - 1}
|
||||
alignToBottom
|
||||
// startReached={handleLoadMore}
|
||||
data={mids}
|
||||
atTopThreshold={context == "channel" ? 160 : 0}
|
||||
@@ -140,24 +187,7 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
||||
atBottomStateChange={handleBottomStateChange}
|
||||
atBottomThreshold={400}
|
||||
followOutput={handleFollowOutput}
|
||||
itemContent={(idx, mid) => {
|
||||
// 计算出真正的 index
|
||||
// const idx = index - firstItemIndex;
|
||||
const curr = messageData[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;
|
||||
return renderMessageFragment({
|
||||
selectMode: !!selects,
|
||||
updateReadIndex: updateReadDebounced,
|
||||
read,
|
||||
prev,
|
||||
curr,
|
||||
contextId: id,
|
||||
context
|
||||
});
|
||||
}}
|
||||
itemContent={itemContent}
|
||||
/>
|
||||
{!atBottom && (
|
||||
<NewMessageBottomTip context={context} id={id} scrollToBottom={handleScrollBottom} />
|
||||
|
||||
Reference in New Issue
Block a user