From d6847c996842210003859926a2498f645128f3c5 Mon Sep 17 00:00:00 2001 From: Tristan Yang Date: Fri, 13 Jan 2023 21:08:16 +0800 Subject: [PATCH] refactor: pull up message feed --- package.json | 2 +- src/app/config.ts | 2 +- src/common/component/styled/Button.tsx | 2 +- src/common/hook/useMessageFeed.ts | 155 +++++++++++++++---------- src/common/utils.tsx | 19 +++ src/routes/chat/ChannelChat/index.tsx | 3 +- src/routes/chat/DMChat/index.tsx | 3 +- src/routes/chat/DMList/index.tsx | 4 +- src/routes/chat/LoadMore.tsx | 42 ++----- 9 files changed, 136 insertions(+), 96 deletions(-) diff --git a/package.json b/package.json index 4d3a584d..06a13bc6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vocechat-web", - "version": "0.3.33", + "version": "0.3.34", "private": true, "homepage": "https://voce.chat", "dependencies": { diff --git a/src/app/config.ts b/src/app/config.ts index 761b9959..7e3f2611 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -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", diff --git a/src/common/component/styled/Button.tsx b/src/common/component/styled/Button.tsx index 159f6145..a06787dd 100644 --- a/src/common/component/styled/Button.tsx +++ b/src/common/component/styled/Button.tsx @@ -51,7 +51,7 @@ const StyledButton = styled.button` } &.border_less { box-shadow: none; - border: none; + border: none !important; } &.cancel { border: 1px solid #e5e7eb; diff --git a/src/common/hook/useMessageFeed.ts b/src/common/hook/useMessageFeed.ts index c5de8ec8..cfd8f4bb 100644 --- a/src/common/hook/useMessageFeed.ts +++ b/src/common/hook/useMessageFeed.ts @@ -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 { 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([]); const pageRef = useRef(null); const containerRef = useRef(null); + const [pulling, setPulling] = useState(false); const [hasMore, setHasMore] = useState(true); const [appends, setAppends] = useState([]); const [items, setItems] = useState([]); @@ -78,77 +81,88 @@ 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; + //处理追加:来自于其它人的实时消息以及自己发的 + const [lastMid] = listRef.current.slice(-1); + const sorteds = mids.slice(0).sort((a: number, b: number) => { + return Number(a) - Number(b); }); - 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); - }); - const appends = sorteds.filter((s: number) => s > lastMid); - if (appends.length) { - setAppends(appends); - const [newestMsgId] = appends.slice(-1); - // 自己发的消息 - const container = containerRef.current; - if (container) { - const msgFromSelf = loginUid == messageData[newestMsgId]?.from_uid; - const scrollDistance = - container.scrollHeight - (container.offsetHeight + container.scrollTop); - // console.log("scrollDistance", msgFromSelf, scrollDistance); - if (msgFromSelf) { + const appends = sorteds.filter((s: number) => s > lastMid); + if (appends.length) { + setAppends(appends); + const [newestMsgId] = appends.slice(-1); + // 自己发的消息:自动往上滚动 + const container = containerRef.current; + if (container) { + const msgFromSelf = loginUid == messageData[newestMsgId]?.from_uid; + const scrollDistance = + container.scrollHeight - (container.offsetHeight + container.scrollTop); + // console.log("scrollDistance", msgFromSelf, scrollDistance); + if (msgFromSelf) { + container.scrollTop = container.scrollHeight; + } else if (scrollDistance <= 100) { + setTimeout(() => { container.scrollTop = container.scrollHeight; - } else if (scrollDistance <= 100) { - setTimeout(() => { - container.scrollTop = container.scrollHeight; - }, 100); - } + }, 100); } } } }, [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 - }); - if (newList?.length == 0) { - setHasMore(false); - return; + + 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 (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, diff --git a/src/common/utils.tsx b/src/common/utils.tsx index 0b876801..84b5bbf5 100644 --- a/src/common/utils.tsx +++ b/src/common/utils.tsx @@ -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; diff --git a/src/routes/chat/ChannelChat/index.tsx b/src/routes/chat/ChannelChat/index.tsx index aa8e8e3e..49873a05 100644 --- a/src/routes/chat/ChannelChat/index.tsx +++ b/src/routes/chat/ChannelChat/index.tsx @@ -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) { > {hasMore ? ( - + ) : (

{t("welcome_channel", { name })}

diff --git a/src/routes/chat/DMChat/index.tsx b/src/routes/chat/DMChat/index.tsx index 1572f4ed..18e5cc5c 100644 --- a/src/routes/chat/DMChat/index.tsx +++ b/src/routes/chat/DMChat/index.tsx @@ -18,6 +18,7 @@ type Props = { }; const DMChat: FC = ({ uid = 0, dropFiles }) => { const { + pulling, list: msgIds, appends, hasMore, @@ -70,7 +71,7 @@ const DMChat: FC = ({ uid = 0, dropFiles }) => { } > - {hasMore ? : null} + {hasMore ? : null} {[...feeds].map((mid, idx) => { const curr = messageData[mid]; const prev = idx == 0 ? null : messageData[feeds[idx - 1]]; diff --git a/src/routes/chat/DMList/index.tsx b/src/routes/chat/DMList/index.tsx index 76272b5e..132f8a3a 100644 --- a/src/routes/chat/DMList/index.tsx +++ b/src/routes/chat/DMList/index.tsx @@ -26,9 +26,9 @@ const DMList: FC = ({ 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, diff --git a/src/routes/chat/LoadMore.tsx b/src/routes/chat/LoadMore.tsx index 97a0aea0..06ef616c 100644 --- a/src/routes/chat/LoadMore.tsx +++ b/src/routes/chat/LoadMore.tsx @@ -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 | null; }; -const LoadMore: FC = ({ pullUp = null }) => { - const ref = useRef(null); +const LoadMore: FC = ({ 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 - pullUp(); - } - }); - }, - { threshold: 0 } - ); - let currEle: HTMLDivElement | null = null; - if (ref) { - currEle = ref.current; - if (currEle) { - observer.observe(currEle); - } + if (inView && pullUp && !pulling) { + pullUp(); } - return () => { - if (currEle) { - observer.unobserve(currEle); - } - }; - }, [ref, pullUp]); + }, [inView, pullUp, pulling]); return ( -
+
); }; -export default LoadMore; +export default memo(LoadMore, (prev, next) => { + return prev.pulling === next.pulling; +});