refactor: pull up message feed
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vocechat-web",
|
||||
"version": "0.3.33",
|
||||
"version": "0.3.34",
|
||||
"private": true,
|
||||
"homepage": "https://voce.chat",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -51,7 +51,7 @@ const StyledButton = styled.button`
|
||||
}
|
||||
&.border_less {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border: none !important;
|
||||
}
|
||||
&.cancel {
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user