refactor: pull up message feed

This commit is contained in:
Tristan Yang
2023-01-13 21:08:16 +08:00
parent 6ecffe557b
commit d6847c9968
9 changed files with 136 additions and 96 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vocechat-web",
"version": "0.3.33",
"version": "0.3.34",
"private": true,
"homepage": "https://voce.chat",
"dependencies": {
+1 -1
View File
@@ -70,7 +70,7 @@ export const PAYMENT_URL_PREFIX =
process.env.NODE_ENV === "production"
? `https://vera.nicegoodthings.com`
: `http://localhost:4000`;
export const CACHE_VERSION = `0.3.32`;
export const CACHE_VERSION = `0.3.33`;
export const GuestRoutes = ["/", "/chat", "/chat/channel/:channel_id"];
export const ContentTypes = {
text: "text/plain",
+1 -1
View File
@@ -51,7 +51,7 @@ const StyledButton = styled.button`
}
&.border_less {
box-shadow: none;
border: none;
border: none !important;
}
&.cancel {
border: 1px solid #e5e7eb;
+77 -40
View File
@@ -2,6 +2,7 @@ import { useEffect, useState, useRef } from "react";
import { useLazyGetHistoryMessagesQuery } from "../../app/services/channel";
import { useLazyGetHistoryMessagesQuery as useLazyGetDMHistoryMsg } from "../../app/services/user";
import { useAppSelector } from "../../app/store";
import { isElementVisible } from "../utils";
export interface PageInfo {
isFirst: boolean;
isLast: boolean;
@@ -14,8 +15,9 @@ interface Config extends Partial<PageInfo> {
mids: number[];
}
const getFeedWithPagination = (config: Config): PageInfo => {
const { pageNumber = 1, pageSize = 40, mids = [], isLast = false } = config || {};
const { pageNumber = 1, pageSize = 50, mids = [], isLast = false } = config || {};
const shadowMids = mids.slice(0);
console.log("pagination", config, shadowMids);
if (shadowMids.length == 0)
return {
@@ -58,6 +60,7 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
const listRef = useRef<number[]>([]);
const pageRef = useRef<PageInfo | null>(null);
const containerRef = useRef<HTMLElement | null>(null);
const [pulling, setPulling] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [appends, setAppends] = useState<number[]>([]);
const [items, setItems] = useState<number[]>([]);
@@ -78,34 +81,21 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
setItems([]);
setHasMore(true);
setAppends([]);
setPulling(false);
}, [context, id]);
useEffect(() => {
if (items.length) {
containerRef.current = document.querySelector(`#VOCECHAT_FEED_${context}_${id}`);
if (containerRef.current) {
const newScroll = containerRef.current.scrollHeight - containerRef.current.clientHeight;
containerRef.current.scrollTop = curScrollPos + (newScroll - oldScroll);
// 处理自动滚动
if (items.length > 0) {
let wrapper = containerRef.current = document.querySelector(`#VOCECHAT_FEED_${context}_${id}`);
if (wrapper) {
const newScroll = wrapper.scrollHeight - wrapper.clientHeight;
wrapper.scrollTop = curScrollPos + (newScroll - oldScroll);
}
}
}, [items, context, id]);
useEffect(() => {
//过滤掉本地消息
const serverMids = mids.filter((id: number) => {
const ts = +new Date();
return Math.abs(ts - id) > 10 * 1000;
});
if (listRef.current.length == 0 && serverMids.length > 0) {
// 初次
const pageInfo = getFeedWithPagination({
mids: serverMids,
isLast: true
});
pageRef.current = pageInfo;
listRef.current = pageInfo.ids;
setItems(listRef.current);
// console.log("message pageInfo", serverMids, pageInfo);
} else {
// 追加
//处理追加:来自于其它人的实时消息以及自己发的
const [lastMid] = listRef.current.slice(-1);
const sorteds = mids.slice(0).sort((a: number, b: number) => {
return Number(a) - Number(b);
@@ -114,7 +104,7 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
if (appends.length) {
setAppends(appends);
const [newestMsgId] = appends.slice(-1);
// 自己发的消息
// 自己发的消息:自动往上滚动
const container = containerRef.current;
if (container) {
const msgFromSelf = loginUid == messageData[newestMsgId]?.from_uid;
@@ -130,25 +120,49 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
}
}
}
}
}, [mids, messageData, loginUid]);
const pullUp = async () => {
const currPageInfo = pageRef.current;
// 第一页
if (currPageInfo && currPageInfo.isFirst) {
const [firstMid] = currPageInfo.ids;
const { data: newList } = await loadMoreMsgsFromServer({
mid: firstMid,
id
useEffect(() => {
const currentItems = listRef.current;
// const [lastMid=Infinity]=currentItems.slice(-1)
//过滤掉本地(以及后来追加的消息?)
const serverMids = mids.filter((id: number) => {
// 如果是本地消息,id是时间戳
const ts = +new Date();
return Math.abs(ts - id) > 10 * 1000;
});
if (newList?.length == 0) {
setHasMore(false);
return;
if (serverMids.length > 0) {
if (currentItems.length == 0) {
//初次
const pageInfo = getFeedWithPagination({
mids: serverMids,
isLast: true
});
pageRef.current = pageInfo;
listRef.current = pageInfo.ids;
setItems(pageInfo.ids);
// console.log("message pageInfo", serverMids, pageInfo);
} else {
const container = containerRef.current;
if (container) {
const loadMoreEle = container.querySelector("[data-load-more]");
if (isElementVisible(loadMoreEle)) {
// 有更新:来自于拉取历史消息,拉取下一页数据
console.log("effected by pull server data");
loadMore();
}
}
}
}
}, [mids]);
const loadMore = () => {
console.log("load more start", mids, listRef.current, pageRef.current);
const currPageInfo = pageRef.current;
let pageInfo: PageInfo;
if (!currPageInfo) {
// 初始化
console.log("first pagination");
pageInfo = getFeedWithPagination({
mids,
isLast: true
@@ -157,27 +171,50 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
const prevPageNumber = currPageInfo.pageNumber - 1;
pageInfo = getFeedWithPagination({
mids,
pageNumber: prevPageNumber
pageNumber: prevPageNumber == 0 ? 1 : prevPageNumber
});
console.log("continue to next page", currPageInfo, prevPageNumber, pageInfo);
}
pageRef.current = pageInfo;
listRef.current = [...pageInfo.ids, ...listRef.current];
listRef.current = [...new Set([...pageInfo.ids, ...listRef.current])].sort((a, b) => a > b ? 1 : -1);
setTimeout(
() => {
console.log("load more timeout", currPageInfo, mids, listRef.current);
setItems(listRef.current);
console.log("pull up timeout", currPageInfo, listRef.current);
setHasMore(pageInfo.pageNumber !== 1);
const container = containerRef.current;
if (container) {
curScrollPos = container.scrollTop;
oldScroll = container.scrollHeight - container.clientHeight;
}
setPulling(false);
},
currPageInfo?.isLast ? 10 : 500
);
setPulling(false);
};
const pullUp = async () => {
setPulling(true);
const currPageInfo = pageRef.current;
console.log("pull up start", currPageInfo);
// 本地数据的第一页
if (currPageInfo && currPageInfo.isFirst || (!currPageInfo && mids.length == 0)) {
const [firstMid] = currPageInfo ? currPageInfo.ids : [0];
const { data: newList } = await loadMoreMsgsFromServer({
mid: firstMid,
id
});
if (newList?.length == 0) {
// 只有在这里,才可以把load more去掉
setHasMore(false);
}
return;
}
loadMore();
};
return {
pulling,
mids,
appends,
hasMore,
+19
View File
@@ -27,7 +27,26 @@ export const isTreatAsImage = (file: File) => {
}
return false;
};
export const isElementVisible = (el: Element | null) => {
if (!el) return false;
const rect = el.getBoundingClientRect(),
vWidth = window.innerWidth || document.documentElement.clientWidth,
vHeight = window.innerHeight || document.documentElement.clientHeight,
efp = function (x: number, y: number) { return document.elementFromPoint(x, y); };
// Return false if it's not in the viewport
if (rect.right < 0 || rect.bottom < 0
|| rect.left > vWidth || rect.top > vHeight)
return false;
// Return true if any of its four corners are visible
return (
el.contains(efp(rect.left, rect.top))
|| el.contains(efp(rect.right, rect.top))
|| el.contains(efp(rect.right, rect.bottom))
|| el.contains(efp(rect.left, rect.bottom))
);
};
export function getDefaultSize(size?: { width: number; height: number }, min = 480) {
if (!size) return { width: 0, height: 0 };
const { width: oWidth, height: oHeight } = size;
+2 -1
View File
@@ -35,6 +35,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
const { t } = useTranslation("chat");
const { values: agoraConfig } = useConfig("agora");
const {
pulling,
list: msgIds,
appends,
hasMore,
@@ -175,7 +176,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
>
<StyledChannelChat id={`VOCECHAT_FEED_channel_${cid}`}>
{hasMore ? (
<LoadMore pullUp={pullUp} />
<LoadMore pullUp={pullUp} pulling={pulling} />
) : (
<div className="info">
<h2 className="title">{t("welcome_channel", { name })}</h2>
+2 -1
View File
@@ -18,6 +18,7 @@ type Props = {
};
const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
const {
pulling,
list: msgIds,
appends,
hasMore,
@@ -70,7 +71,7 @@ const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
}
>
<StyledDMChat id={`VOCECHAT_FEED_user_${uid}`}>
{hasMore ? <LoadMore pullUp={pullUp} /> : null}
{hasMore ? <LoadMore pullUp={pullUp} pulling={pulling} /> : null}
{[...feeds].map((mid, idx) => {
const curr = messageData[mid];
const prev = idx == 0 ? null : messageData[feeds[idx - 1]];
+2 -2
View File
@@ -26,9 +26,9 @@ const DMList: FC<Props> = ({ uids, setDropFiles }) => {
const sessions = uids.map((uid) => {
const mids = userMessage[uid] || [];
if (mids.length == 0) {
return { lastMid: 0, unreads: 0, uid };
return { lastMid: null, unreads: 0, uid };
}
const lastMid = [...mids].pop() || 0;
const lastMid = [...mids].pop();
const readIndex = readUsers[uid];
const { unreads = 0 } = getUnreadCount({
mids,
+11 -29
View File
@@ -1,42 +1,24 @@
import { useRef, useEffect, FC } from "react";
import { memo, useEffect, FC } from "react";
import { Waveform } from "@uiball/loaders";
import { useInViewRef } from "rooks";
type Props = {
pulling?: boolean;
pullUp: () => Promise<void> | null;
};
const LoadMore: FC<Props> = ({ pullUp = null }) => {
const ref = useRef<HTMLDivElement | null>(null);
const LoadMore: FC<Props> = ({ pullUp = null, pulling }) => {
const [myRef, inView] = useInViewRef();
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const intersecting = entry.isIntersecting;
// const currEle = entry.target;
if (intersecting && pullUp) {
// load more
if (inView && pullUp && !pulling) {
pullUp();
}
});
},
{ threshold: 0 }
);
let currEle: HTMLDivElement | null = null;
if (ref) {
currEle = ref.current;
if (currEle) {
observer.observe(currEle);
}
}
return () => {
if (currEle) {
observer.unobserve(currEle);
}
};
}, [ref, pullUp]);
}, [inView, pullUp, pulling]);
return (
<div className="mt-2 flex justify-center items-center w-full py-2" ref={ref}>
<div data-load-more className="mt-2 flex justify-center items-center w-full py-2" ref={myRef}>
<Waveform size={18} lineWeight={4} speed={1} color="#ccc" />
</div>
);
};
export default LoadMore;
export default memo(LoadMore, (prev, next) => {
return prev.pulling === next.pulling;
});