refactor: pull up message feed
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vocechat-web",
|
"name": "vocechat-web",
|
||||||
"version": "0.3.33",
|
"version": "0.3.34",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://voce.chat",
|
"homepage": "https://voce.chat",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ export const PAYMENT_URL_PREFIX =
|
|||||||
process.env.NODE_ENV === "production"
|
process.env.NODE_ENV === "production"
|
||||||
? `https://vera.nicegoodthings.com`
|
? `https://vera.nicegoodthings.com`
|
||||||
: `http://localhost:4000`;
|
: `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 GuestRoutes = ["/", "/chat", "/chat/channel/:channel_id"];
|
||||||
export const ContentTypes = {
|
export const ContentTypes = {
|
||||||
text: "text/plain",
|
text: "text/plain",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const StyledButton = styled.button`
|
|||||||
}
|
}
|
||||||
&.border_less {
|
&.border_less {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: none;
|
border: none !important;
|
||||||
}
|
}
|
||||||
&.cancel {
|
&.cancel {
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState, useRef } from "react";
|
|||||||
import { useLazyGetHistoryMessagesQuery } from "../../app/services/channel";
|
import { useLazyGetHistoryMessagesQuery } from "../../app/services/channel";
|
||||||
import { useLazyGetHistoryMessagesQuery as useLazyGetDMHistoryMsg } from "../../app/services/user";
|
import { useLazyGetHistoryMessagesQuery as useLazyGetDMHistoryMsg } from "../../app/services/user";
|
||||||
import { useAppSelector } from "../../app/store";
|
import { useAppSelector } from "../../app/store";
|
||||||
|
import { isElementVisible } from "../utils";
|
||||||
export interface PageInfo {
|
export interface PageInfo {
|
||||||
isFirst: boolean;
|
isFirst: boolean;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
@@ -14,8 +15,9 @@ interface Config extends Partial<PageInfo> {
|
|||||||
mids: number[];
|
mids: number[];
|
||||||
}
|
}
|
||||||
const getFeedWithPagination = (config: Config): PageInfo => {
|
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);
|
const shadowMids = mids.slice(0);
|
||||||
|
console.log("pagination", config, shadowMids);
|
||||||
|
|
||||||
if (shadowMids.length == 0)
|
if (shadowMids.length == 0)
|
||||||
return {
|
return {
|
||||||
@@ -58,6 +60,7 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
|
|||||||
const listRef = useRef<number[]>([]);
|
const listRef = useRef<number[]>([]);
|
||||||
const pageRef = useRef<PageInfo | null>(null);
|
const pageRef = useRef<PageInfo | null>(null);
|
||||||
const containerRef = useRef<HTMLElement | null>(null);
|
const containerRef = useRef<HTMLElement | null>(null);
|
||||||
|
const [pulling, setPulling] = useState(false);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [appends, setAppends] = useState<number[]>([]);
|
const [appends, setAppends] = useState<number[]>([]);
|
||||||
const [items, setItems] = useState<number[]>([]);
|
const [items, setItems] = useState<number[]>([]);
|
||||||
@@ -78,34 +81,21 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
|
|||||||
setItems([]);
|
setItems([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
setAppends([]);
|
setAppends([]);
|
||||||
|
setPulling(false);
|
||||||
}, [context, id]);
|
}, [context, id]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (items.length) {
|
// 处理自动滚动
|
||||||
containerRef.current = document.querySelector(`#VOCECHAT_FEED_${context}_${id}`);
|
if (items.length > 0) {
|
||||||
if (containerRef.current) {
|
let wrapper = containerRef.current = document.querySelector(`#VOCECHAT_FEED_${context}_${id}`);
|
||||||
const newScroll = containerRef.current.scrollHeight - containerRef.current.clientHeight;
|
if (wrapper) {
|
||||||
containerRef.current.scrollTop = curScrollPos + (newScroll - oldScroll);
|
const newScroll = wrapper.scrollHeight - wrapper.clientHeight;
|
||||||
|
wrapper.scrollTop = curScrollPos + (newScroll - oldScroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [items, context, id]);
|
}, [items, context, id]);
|
||||||
|
|
||||||
useEffect(() => {
|
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 [lastMid] = listRef.current.slice(-1);
|
||||||
const sorteds = mids.slice(0).sort((a: number, b: number) => {
|
const sorteds = mids.slice(0).sort((a: number, b: number) => {
|
||||||
return Number(a) - Number(b);
|
return Number(a) - Number(b);
|
||||||
@@ -114,7 +104,7 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
|
|||||||
if (appends.length) {
|
if (appends.length) {
|
||||||
setAppends(appends);
|
setAppends(appends);
|
||||||
const [newestMsgId] = appends.slice(-1);
|
const [newestMsgId] = appends.slice(-1);
|
||||||
// 自己发的消息
|
// 自己发的消息:自动往上滚动
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (container) {
|
if (container) {
|
||||||
const msgFromSelf = loginUid == messageData[newestMsgId]?.from_uid;
|
const msgFromSelf = loginUid == messageData[newestMsgId]?.from_uid;
|
||||||
@@ -130,25 +120,49 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [mids, messageData, loginUid]);
|
}, [mids, messageData, loginUid]);
|
||||||
const pullUp = async () => {
|
|
||||||
const currPageInfo = pageRef.current;
|
useEffect(() => {
|
||||||
// 第一页
|
const currentItems = listRef.current;
|
||||||
if (currPageInfo && currPageInfo.isFirst) {
|
// const [lastMid=Infinity]=currentItems.slice(-1)
|
||||||
const [firstMid] = currPageInfo.ids;
|
//过滤掉本地(以及后来追加的消息?)
|
||||||
const { data: newList } = await loadMoreMsgsFromServer({
|
const serverMids = mids.filter((id: number) => {
|
||||||
mid: firstMid,
|
// 如果是本地消息,id是时间戳
|
||||||
id
|
const ts = +new Date();
|
||||||
|
return Math.abs(ts - id) > 10 * 1000;
|
||||||
});
|
});
|
||||||
if (newList?.length == 0) {
|
if (serverMids.length > 0) {
|
||||||
setHasMore(false);
|
if (currentItems.length == 0) {
|
||||||
return;
|
//初次
|
||||||
|
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;
|
let pageInfo: PageInfo;
|
||||||
if (!currPageInfo) {
|
if (!currPageInfo) {
|
||||||
// 初始化
|
// 初始化
|
||||||
|
console.log("first pagination");
|
||||||
|
|
||||||
pageInfo = getFeedWithPagination({
|
pageInfo = getFeedWithPagination({
|
||||||
mids,
|
mids,
|
||||||
isLast: true
|
isLast: true
|
||||||
@@ -157,27 +171,50 @@ export default function useMessageFeed({ context = "channel", id }: Props) {
|
|||||||
const prevPageNumber = currPageInfo.pageNumber - 1;
|
const prevPageNumber = currPageInfo.pageNumber - 1;
|
||||||
pageInfo = getFeedWithPagination({
|
pageInfo = getFeedWithPagination({
|
||||||
mids,
|
mids,
|
||||||
pageNumber: prevPageNumber
|
pageNumber: prevPageNumber == 0 ? 1 : prevPageNumber
|
||||||
});
|
});
|
||||||
|
console.log("continue to next page", currPageInfo, prevPageNumber, pageInfo);
|
||||||
}
|
}
|
||||||
pageRef.current = 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(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
|
console.log("load more timeout", currPageInfo, mids, listRef.current);
|
||||||
setItems(listRef.current);
|
setItems(listRef.current);
|
||||||
console.log("pull up timeout", currPageInfo, listRef.current);
|
|
||||||
setHasMore(pageInfo.pageNumber !== 1);
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (container) {
|
if (container) {
|
||||||
curScrollPos = container.scrollTop;
|
curScrollPos = container.scrollTop;
|
||||||
oldScroll = container.scrollHeight - container.clientHeight;
|
oldScroll = container.scrollHeight - container.clientHeight;
|
||||||
}
|
}
|
||||||
|
setPulling(false);
|
||||||
},
|
},
|
||||||
currPageInfo?.isLast ? 10 : 500
|
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 {
|
return {
|
||||||
|
pulling,
|
||||||
mids,
|
mids,
|
||||||
appends,
|
appends,
|
||||||
hasMore,
|
hasMore,
|
||||||
|
|||||||
@@ -27,7 +27,26 @@ export const isTreatAsImage = (file: File) => {
|
|||||||
}
|
}
|
||||||
return false;
|
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) {
|
export function getDefaultSize(size?: { width: number; height: number }, min = 480) {
|
||||||
if (!size) return { width: 0, height: 0 };
|
if (!size) return { width: 0, height: 0 };
|
||||||
const { width: oWidth, height: oHeight } = size;
|
const { width: oWidth, height: oHeight } = size;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
|||||||
const { t } = useTranslation("chat");
|
const { t } = useTranslation("chat");
|
||||||
const { values: agoraConfig } = useConfig("agora");
|
const { values: agoraConfig } = useConfig("agora");
|
||||||
const {
|
const {
|
||||||
|
pulling,
|
||||||
list: msgIds,
|
list: msgIds,
|
||||||
appends,
|
appends,
|
||||||
hasMore,
|
hasMore,
|
||||||
@@ -175,7 +176,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
|||||||
>
|
>
|
||||||
<StyledChannelChat id={`VOCECHAT_FEED_channel_${cid}`}>
|
<StyledChannelChat id={`VOCECHAT_FEED_channel_${cid}`}>
|
||||||
{hasMore ? (
|
{hasMore ? (
|
||||||
<LoadMore pullUp={pullUp} />
|
<LoadMore pullUp={pullUp} pulling={pulling} />
|
||||||
) : (
|
) : (
|
||||||
<div className="info">
|
<div className="info">
|
||||||
<h2 className="title">{t("welcome_channel", { name })}</h2>
|
<h2 className="title">{t("welcome_channel", { name })}</h2>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
|
const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
|
||||||
const {
|
const {
|
||||||
|
pulling,
|
||||||
list: msgIds,
|
list: msgIds,
|
||||||
appends,
|
appends,
|
||||||
hasMore,
|
hasMore,
|
||||||
@@ -70,7 +71,7 @@ const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StyledDMChat id={`VOCECHAT_FEED_user_${uid}`}>
|
<StyledDMChat id={`VOCECHAT_FEED_user_${uid}`}>
|
||||||
{hasMore ? <LoadMore pullUp={pullUp} /> : null}
|
{hasMore ? <LoadMore pullUp={pullUp} pulling={pulling} /> : null}
|
||||||
{[...feeds].map((mid, idx) => {
|
{[...feeds].map((mid, idx) => {
|
||||||
const curr = messageData[mid];
|
const curr = messageData[mid];
|
||||||
const prev = idx == 0 ? null : messageData[feeds[idx - 1]];
|
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 sessions = uids.map((uid) => {
|
||||||
const mids = userMessage[uid] || [];
|
const mids = userMessage[uid] || [];
|
||||||
if (mids.length == 0) {
|
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 readIndex = readUsers[uid];
|
||||||
const { unreads = 0 } = getUnreadCount({
|
const { unreads = 0 } = getUnreadCount({
|
||||||
mids,
|
mids,
|
||||||
|
|||||||
@@ -1,42 +1,24 @@
|
|||||||
import { useRef, useEffect, FC } from "react";
|
import { memo, useEffect, FC } from "react";
|
||||||
import { Waveform } from "@uiball/loaders";
|
import { Waveform } from "@uiball/loaders";
|
||||||
|
import { useInViewRef } from "rooks";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
pulling?: boolean;
|
||||||
pullUp: () => Promise<void> | null;
|
pullUp: () => Promise<void> | null;
|
||||||
};
|
};
|
||||||
const LoadMore: FC<Props> = ({ pullUp = null }) => {
|
const LoadMore: FC<Props> = ({ pullUp = null, pulling }) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const [myRef, inView] = useInViewRef();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
if (inView && pullUp && !pulling) {
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
const intersecting = entry.isIntersecting;
|
|
||||||
// const currEle = entry.target;
|
|
||||||
if (intersecting && pullUp) {
|
|
||||||
// load more
|
|
||||||
pullUp();
|
pullUp();
|
||||||
}
|
}
|
||||||
});
|
}, [inView, pullUp, pulling]);
|
||||||
},
|
|
||||||
{ 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]);
|
|
||||||
return (
|
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" />
|
<Waveform size={18} lineWeight={4} speed={1} color="#ccc" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default LoadMore;
|
export default memo(LoadMore, (prev, next) => {
|
||||||
|
return prev.pulling === next.pulling;
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user