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:
@@ -261,7 +261,7 @@ module.exports = function (webpackEnv) {
|
|||||||
// Handle node_modules packages that contain sourcemaps
|
// Handle node_modules packages that contain sourcemaps
|
||||||
shouldUseSourceMap && {
|
shouldUseSourceMap && {
|
||||||
enforce: "pre",
|
enforce: "pre",
|
||||||
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
exclude: /@babel(?:\/|\\{1,2})runtime|@toast-ui\+editor/,
|
||||||
test: /\.(js|mjs|jsx|ts|tsx|css)$/,
|
test: /\.(js|mjs|jsx|ts|tsx|css)$/,
|
||||||
loader: require.resolve("source-map-loader")
|
loader: require.resolve("source-map-loader")
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vocechat-web",
|
"name": "vocechat-web",
|
||||||
"version": "0.9.43",
|
"version": "0.9.44",
|
||||||
"homepage": "https://voce.chat",
|
"homepage": "https://voce.chat",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@metamask/onboarding": "^1.0.1",
|
"@metamask/onboarding": "^1.0.1",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createApi } from "@reduxjs/toolkit/query/react";
|
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
|
import { batch } from "react-redux";
|
||||||
|
|
||||||
import { ChatContext } from "@/types/common";
|
import { ChatContext } from "@/types/common";
|
||||||
import { ChatMessage, ContentTypeKey, UploadFileResponse } from "@/types/message";
|
import { ChatMessage, ContentTypeKey, UploadFileResponse } from "@/types/message";
|
||||||
@@ -224,9 +225,23 @@ export const messageApi = createApi({
|
|||||||
const { data: messages } = await queryFulfilled;
|
const { data: messages } = await queryFulfilled;
|
||||||
const fromHistory = true;
|
const fromHistory = true;
|
||||||
if (messages?.length) {
|
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
|
shallowEqual
|
||||||
);
|
);
|
||||||
const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual);
|
const loginUid = useAppSelector((store) => store.authData.user?.uid, shallowEqual);
|
||||||
const usersData = useAppSelector((store) => store.users.byId, shallowEqual);
|
// 只订阅当前消息发送者的用户信息,而不是整个usersData
|
||||||
const reactionMessageData = useAppSelector((store) => store.reactionMessage, shallowEqual);
|
const currUser = useAppSelector((store) => store.users.byId[message?.from_uid || 0], shallowEqual);
|
||||||
|
// 只订阅当前消息的reaction,而不是整个reactionMessageData
|
||||||
|
const reactions = useAppSelector((store) => store.reactionMessage[mid], shallowEqual);
|
||||||
|
|
||||||
const toggleEditMessage = () => {
|
const toggleEditMessage = () => {
|
||||||
setEdit((prev) => !prev);
|
setEdit((prev) => !prev);
|
||||||
@@ -84,14 +86,18 @@ const Message: FC<IProps> = ({
|
|||||||
failed = false,
|
failed = false,
|
||||||
} = message;
|
} = message;
|
||||||
|
|
||||||
const reactions = reactionMessageData[mid];
|
// 获取pinInfo中需要的用户信息
|
||||||
const currUser = usersData[fromUid || 0];
|
const pinInfo = getPinInfo(mid);
|
||||||
|
const pinCreatorName = useAppSelector((store) =>
|
||||||
|
pinInfo?.created_by ? store.users.byId[pinInfo.created_by]?.name : undefined,
|
||||||
|
shallowEqual
|
||||||
|
);
|
||||||
|
|
||||||
// if (!message) return null;
|
// if (!message) return null;
|
||||||
let timePrefix = null;
|
let timePrefix = null;
|
||||||
const dayjsTime = dayjs(time);
|
const dayjsTime = dayjs(time);
|
||||||
timePrefix = dayjsTime.isToday() ? "Today" : dayjsTime.isYesterday() ? "Yesterday" : null;
|
timePrefix = dayjsTime.isToday() ? "Today" : dayjsTime.isYesterday() ? "Yesterday" : null;
|
||||||
|
|
||||||
const pinInfo = getPinInfo(mid);
|
|
||||||
// return null;
|
// return null;
|
||||||
const _key = properties?.local_id || mid;
|
const _key = properties?.local_id || mid;
|
||||||
const showExpire = (expires_in ?? 0) > 0;
|
const showExpire = (expires_in ?? 0) > 0;
|
||||||
@@ -143,9 +149,7 @@ const Message: FC<IProps> = ({
|
|||||||
pinInfo && "relative",
|
pinInfo && "relative",
|
||||||
isSelf && "items-end"
|
isSelf && "items-end"
|
||||||
)}
|
)}
|
||||||
data-pin-tip={`pinned by ${
|
data-pin-tip={`pinned by ${pinCreatorName || ""}`}
|
||||||
pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{pinInfo && (
|
{pinInfo && (
|
||||||
<span
|
<span
|
||||||
@@ -154,7 +158,7 @@ const Message: FC<IProps> = ({
|
|||||||
isSelf ? "right-0" : "left-0"
|
isSelf ? "right-0" : "left-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{`pinned by ${pinInfo.created_by ? usersData[pinInfo.created_by]?.name : ""}`}
|
{`pinned by ${pinCreatorName || ""}`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
// @ts-ignore
|
const CustomList = ({ style, ref, ...props }: any) => {
|
||||||
const CustomList = ({ style, ref, ...props }) => {
|
|
||||||
// @ts-ignore
|
|
||||||
return <div style={{ ...style, width: `calc(100% - 2rem)` }} {...props} ref={ref} />;
|
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 { shallowEqual, useDispatch } from "react-redux";
|
||||||
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
|
||||||
import { useDebounce } from "rooks";
|
import { useDebounce } from "rooks";
|
||||||
@@ -28,6 +28,8 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
|||||||
// const { t } = useTranslation("chat");
|
// const { t } = useTranslation("chat");
|
||||||
// const [firstItemIndex, setFirstItemIndex] = useState(firstMsgIndex);
|
// const [firstItemIndex, setFirstItemIndex] = useState(firstMsgIndex);
|
||||||
const [atBottom, setAtBottom] = useState(false);
|
const [atBottom, setAtBottom] = useState(false);
|
||||||
|
const [visibleCount, setVisibleCount] = useState(100);
|
||||||
|
const isInitializing = useRef(false);
|
||||||
const [loadMoreMessage, { isLoading: loadingMore, isSuccess, data: historyData }] =
|
const [loadMoreMessage, { isLoading: loadingMore, isSuccess, data: historyData }] =
|
||||||
useLazyLoadMoreMessagesQuery();
|
useLazyLoadMoreMessagesQuery();
|
||||||
const vList = useRef<VirtuosoHandle | null>(null);
|
const vList = useRef<VirtuosoHandle | null>(null);
|
||||||
@@ -40,20 +42,33 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
|||||||
: store.footprint.historyChannels[id] ?? "",
|
: store.footprint.historyChannels[id] ?? "",
|
||||||
shallowEqual
|
shallowEqual
|
||||||
);
|
);
|
||||||
const mids = useAppSelector(
|
const allMids = useAppSelector(
|
||||||
(store) =>
|
(store) =>
|
||||||
context == "dm" ? store.userMessage.byId[id] ?? [] : store.channelMessage[id] ?? [],
|
context == "dm" ? store.userMessage.byId[id] ?? [] : store.channelMessage[id] ?? [],
|
||||||
shallowEqual
|
shallowEqual
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mids = useMemo(() => {
|
||||||
|
if (allMids.length <= visibleCount) return allMids;
|
||||||
|
return allMids.slice(-visibleCount);
|
||||||
|
}, [allMids, visibleCount]);
|
||||||
const selects = useAppSelector(
|
const selects = useAppSelector(
|
||||||
(store) => store.ui.selectMessages[`${context}_${id}`],
|
(store) => store.ui.selectMessages[`${context}_${id}`],
|
||||||
shallowEqual
|
shallowEqual
|
||||||
);
|
);
|
||||||
const messageData = useAppSelector((store) => store.message || {}, shallowEqual);
|
|
||||||
const loginUid = useAppSelector((store) => store.authData.user?.uid, 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 readChannels = useAppSelector((store) => store.footprint.readChannels, shallowEqual);
|
||||||
const readUsers = useAppSelector((store) => store.footprint.readUsers, shallowEqual);
|
const readUsers = useAppSelector((store) => store.footprint.readUsers, shallowEqual);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isInitializing.current = true;
|
||||||
|
setVisibleCount(100);
|
||||||
|
setTimeout(() => {
|
||||||
|
isInitializing.current = false;
|
||||||
|
}, 500);
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSuccess && historyData) {
|
if (isSuccess && historyData) {
|
||||||
if (historyData.length == 0) {
|
if (historyData.length == 0) {
|
||||||
@@ -75,14 +90,17 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
|||||||
// 加载更多
|
// 加载更多
|
||||||
const handleTopStateChange = (isTop: boolean) => {
|
const handleTopStateChange = (isTop: boolean) => {
|
||||||
console.log("reach top ", isTop);
|
console.log("reach top ", isTop);
|
||||||
if (isTop) {
|
if (isTop && !isInitializing.current) {
|
||||||
if (historyMid === "reached") return;
|
if (allMids.length > visibleCount) {
|
||||||
let lastMid = mids.slice(0, 1)[0];
|
setVisibleCount(prev => Math.min(prev + 100, allMids.length));
|
||||||
if (historyMid) {
|
} else {
|
||||||
lastMid = +historyMid;
|
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 handleScrollBottom = useCallback(() => {
|
||||||
const vl = vList!.current;
|
const vl = vList!.current;
|
||||||
if (vl) {
|
if (vl) {
|
||||||
vl.scrollToIndex(mids.length - 1);
|
vl.scrollToIndex(allMids.length - 1);
|
||||||
}
|
}
|
||||||
}, [mids]);
|
}, [allMids]);
|
||||||
const handleBottomStateChange = (bottom: boolean) => {
|
const handleBottomStateChange = (bottom: boolean) => {
|
||||||
setAtBottom(bottom);
|
setAtBottom(bottom);
|
||||||
};
|
};
|
||||||
@@ -113,16 +131,44 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
|||||||
const index = mids.findIndex((m) => m === mid);
|
const index = mids.findIndex((m) => m === mid);
|
||||||
if (index !== -1 && vList.current) {
|
if (index !== -1 && vList.current) {
|
||||||
vList.current.scrollToIndex({ index, align: "center", behavior: "smooth" });
|
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];
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
// logLevel={LogLevel.DEBUG}
|
// logLevel={LogLevel.DEBUG}
|
||||||
overscan={50}
|
overscan={50}
|
||||||
|
increaseViewportBy={{ top: 0, bottom: 400 }}
|
||||||
context={{ loadingMore, id, isChannel: context == "channel" }}
|
context={{ loadingMore, id, isChannel: context == "channel" }}
|
||||||
id={`VOCECHAT_FEED_${context}_${id}`}
|
id={`VOCECHAT_FEED_${context}_${id}`}
|
||||||
className="px-1 md:px-4 py-4.5 overflow-x-hidden overflow-y-scroll"
|
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}
|
// firstItemIndex={firstItemIndex}
|
||||||
initialTopMostItemIndex={mids.length - 1}
|
initialTopMostItemIndex={mids.length - 1}
|
||||||
|
alignToBottom
|
||||||
// startReached={handleLoadMore}
|
// startReached={handleLoadMore}
|
||||||
data={mids}
|
data={mids}
|
||||||
atTopThreshold={context == "channel" ? 160 : 0}
|
atTopThreshold={context == "channel" ? 160 : 0}
|
||||||
@@ -140,24 +187,7 @@ const VirtualMessageFeed = forwardRef<VirtualMessageFeedHandle, Props>(({ contex
|
|||||||
atBottomStateChange={handleBottomStateChange}
|
atBottomStateChange={handleBottomStateChange}
|
||||||
atBottomThreshold={400}
|
atBottomThreshold={400}
|
||||||
followOutput={handleFollowOutput}
|
followOutput={handleFollowOutput}
|
||||||
itemContent={(idx, mid) => {
|
itemContent={itemContent}
|
||||||
// 计算出真正的 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
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{!atBottom && (
|
{!atBottom && (
|
||||||
<NewMessageBottomTip context={context} id={id} scrollToBottom={handleScrollBottom} />
|
<NewMessageBottomTip context={context} id={id} scrollToBottom={handleScrollBottom} />
|
||||||
|
|||||||
Reference in New Issue
Block a user