feat: pagination message feed
This commit is contained in:
@@ -53,6 +53,7 @@
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hot-toast": "^2.2.0",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-linkify": "^1.0.0-alpha",
|
||||
"react-pdf": "^5.7.2",
|
||||
"react-redux": "^8.0.2",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
const getFeedWithPagination = (config) => {
|
||||
const { pageNumber = 1, pageSize = 10, mids = [], isLast = false } =
|
||||
config || {};
|
||||
const shadowMids = mids.slice(0);
|
||||
|
||||
if (shadowMids.length == 0)
|
||||
return {
|
||||
pageCount: 0,
|
||||
pageSize,
|
||||
pageNumber: 1,
|
||||
ids: [],
|
||||
};
|
||||
shadowMids.sort((a, b) => {
|
||||
return Number(a) - Number(b);
|
||||
});
|
||||
console.log("message pagination", shadowMids);
|
||||
const pageCount = Math.ceil(shadowMids.length / pageSize);
|
||||
const computedPageNumber = isLast ? pageCount : pageNumber;
|
||||
const ids = shadowMids.slice(
|
||||
(computedPageNumber - 1) * pageSize,
|
||||
computedPageNumber * pageSize
|
||||
);
|
||||
return {
|
||||
pageCount,
|
||||
pageSize,
|
||||
pageNumber: computedPageNumber,
|
||||
ids,
|
||||
};
|
||||
};
|
||||
export default function useMessageFeed({ context = "channel", id = null }) {
|
||||
const listRef = useRef([]);
|
||||
const pageRef = useRef(null);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [appends, setAppends] = useState([]);
|
||||
const [items, setItems] = useState([]);
|
||||
const mids = useSelector((store) => {
|
||||
return context == "channel"
|
||||
? store.channelMessage[id] || []
|
||||
: store.userMessage.byId[id] || [];
|
||||
});
|
||||
useEffect(() => {
|
||||
listRef.current = [];
|
||||
pageRef.current = [];
|
||||
setItems([]);
|
||||
setHasMore(true);
|
||||
setAppends([]);
|
||||
}, [context, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (listRef.current.length == 0) {
|
||||
// 初次
|
||||
const pageInfo = getFeedWithPagination({ mids, isLast: true });
|
||||
console.log("pull up 2", pageInfo);
|
||||
pageRef.current = pageInfo;
|
||||
listRef.current = pageInfo.ids;
|
||||
setItems(listRef.current);
|
||||
console.log("message pageInfo", mids, pageInfo);
|
||||
} else {
|
||||
// 追加
|
||||
const lastMid = listRef.current.slice(-1);
|
||||
const sorteds = mids.slice(0).sort((a, b) => {
|
||||
return Number(a) - Number(b);
|
||||
});
|
||||
const appends = sorteds.filter((s) => s > lastMid);
|
||||
if (appends.length) {
|
||||
setAppends(appends);
|
||||
}
|
||||
}
|
||||
}, [mids]);
|
||||
const pullUp = () => {
|
||||
const currPageInfo = pageRef.current;
|
||||
console.log("pull up", currPageInfo);
|
||||
// 第一页
|
||||
if (currPageInfo && currPageInfo.pageNumber == 1) {
|
||||
setHasMore(false);
|
||||
return;
|
||||
}
|
||||
let pageInfo = null;
|
||||
if (!currPageInfo) {
|
||||
// 初始化
|
||||
pageInfo = getFeedWithPagination({
|
||||
mids,
|
||||
isLast: true,
|
||||
});
|
||||
} else {
|
||||
const prevPageNumber = currPageInfo.pageNumber - 1;
|
||||
pageInfo = getFeedWithPagination({
|
||||
mids,
|
||||
pageNumber: prevPageNumber,
|
||||
});
|
||||
}
|
||||
pageRef.current = pageInfo;
|
||||
listRef.current = [...pageInfo.ids, ...listRef.current];
|
||||
setTimeout(() => {
|
||||
setItems(listRef.current);
|
||||
console.log("pull up", currPageInfo, listRef.current);
|
||||
setHasMore(pageInfo.pageNumber !== 1);
|
||||
}, 300);
|
||||
};
|
||||
const pullDown = () => {
|
||||
// 向下加载
|
||||
};
|
||||
|
||||
return {
|
||||
mids,
|
||||
appends,
|
||||
hasMore,
|
||||
pullUp,
|
||||
pullDown,
|
||||
list: items,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// import { useRef, useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Waveform } from "@uiball/loaders";
|
||||
|
||||
const Styled = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
/* background-color: #eee; */
|
||||
`;
|
||||
export default function LoadMore() {
|
||||
// const ref = useRef(undefined);
|
||||
// useEffect(() => {
|
||||
// const observer = new IntersectionObserver(
|
||||
// (entries) => {
|
||||
// entries.forEach((entry) => {
|
||||
// const intersecting = entry.isIntersecting;
|
||||
// // const currEle = entry.target;
|
||||
// if (intersecting && loadMore) {
|
||||
// // load more
|
||||
// console.log("inview");
|
||||
// loadMore();
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
// { threshold: 0 }
|
||||
// );
|
||||
// const currEle = ref?.current;
|
||||
// if (currEle) {
|
||||
// observer.observe(ref.current);
|
||||
// }
|
||||
// return () => {
|
||||
// if (currEle) {
|
||||
// observer.unobserve(currEle);
|
||||
// }
|
||||
// };
|
||||
// }, [ref]);
|
||||
return (
|
||||
<Styled>
|
||||
<Waveform
|
||||
className="loading"
|
||||
size={24}
|
||||
lineWeight={5}
|
||||
speed={1}
|
||||
color="#ccc"
|
||||
/>
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
@@ -2,12 +2,15 @@ import { useState, useEffect } from "react";
|
||||
import { useDebounce } from "rooks";
|
||||
import { NavLink, useLocation } from "react-router-dom";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import InfiniteScroll from "react-infinite-scroller";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import PinList from "./PinList";
|
||||
import LoadMore from "./LoadMore";
|
||||
import FavList from "../FavList";
|
||||
import { useReadMessageMutation } from "../../../app/services/message";
|
||||
import { updateRemeberedNavs } from "../../../app/slices/ui";
|
||||
import useChatScroll from "../../../common/hook/useChatScroll";
|
||||
// import useChatScroll from "../../../common/hook/useChatScroll";
|
||||
import useMessageFeed from "../../../common/hook/useMessageFeed";
|
||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||
import Tooltip from "../../../common/component/Tooltip";
|
||||
import Contact from "../../../common/component/Contact";
|
||||
@@ -23,15 +26,14 @@ import IconHeadphone from "../../../assets/icons/headphone.svg";
|
||||
import boardosIcon from "../../../assets/icons/app.boardos.svg?url";
|
||||
import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url";
|
||||
import addIcon from "../../../assets/icons/add.svg?url";
|
||||
import {
|
||||
// StyledNotification,
|
||||
StyledContacts,
|
||||
StyledChannelChat,
|
||||
StyledHeader,
|
||||
} from "./styled";
|
||||
import { StyledContacts, StyledChannelChat, StyledHeader } from "./styled";
|
||||
import InviteModal from "../../../common/component/InviteModal";
|
||||
|
||||
export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
||||
const { list: msgIdList, appends, hasMore, pullUp } = useMessageFeed({
|
||||
context: "channel",
|
||||
id: cid,
|
||||
});
|
||||
const [toolVisible, setToolVisible] = useState("");
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
@@ -41,7 +43,6 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
||||
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
|
||||
const {
|
||||
selects,
|
||||
msgIds,
|
||||
userIds,
|
||||
data,
|
||||
messageData,
|
||||
@@ -54,16 +55,16 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
||||
footprint: store.footprint,
|
||||
loginUser: store.contacts.byId[store.authData.uid],
|
||||
loginUid: store.authData.uid,
|
||||
msgIds: store.channelMessage[cid] || [],
|
||||
userIds: store.contacts.ids,
|
||||
data: store.channels.byId[cid] || {},
|
||||
messageData: store.message || {},
|
||||
};
|
||||
});
|
||||
const ref = useChatScroll(msgIds);
|
||||
// const ref = useChatScroll(mids);
|
||||
// const handleClearUnreads = () => {
|
||||
// dispatch(readMessage(msgIds));
|
||||
// };
|
||||
// remember the route while switching out
|
||||
useEffect(() => {
|
||||
dispatch(updateRemeberedNavs());
|
||||
return () => {
|
||||
@@ -83,9 +84,9 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
||||
? userIds
|
||||
: members.slice(0).sort((n) => (n == owner ? -1 : 0));
|
||||
const addVisible = loginUser?.is_admin || owner == loginUid;
|
||||
console.log("channel message list", msgIds);
|
||||
const readIndex = footprint.readChannels[cid];
|
||||
const pinCount = data?.pinned_messages?.length || 0;
|
||||
const finalHasMore = selects ? false : hasMore;
|
||||
return (
|
||||
<>
|
||||
{addMemberModalVisible && (
|
||||
@@ -99,21 +100,11 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
||||
aside={
|
||||
<>
|
||||
<ul className="tools">
|
||||
{/* <li className="tool">
|
||||
<Tooltip tip="Search" placement="left">
|
||||
<img src={searchIcon} alt="opt icon" />
|
||||
</Tooltip>
|
||||
</li> */}
|
||||
<li className="tool">
|
||||
<Tooltip tip="Voice/Video Chat" placement="left">
|
||||
<IconHeadphone />
|
||||
</Tooltip>
|
||||
</li>
|
||||
{/* <li className="tool">
|
||||
<Tooltip tip="Notifications" placement="left">
|
||||
<img src={alertIcon} alt="opt icon" />
|
||||
</Tooltip>
|
||||
</li> */}
|
||||
<Tooltip
|
||||
tip="Pin"
|
||||
placement="left"
|
||||
@@ -231,41 +222,66 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<StyledChannelChat ref={ref}>
|
||||
<div className="info">
|
||||
<h2 className="title">Welcome to #{name} !</h2>
|
||||
<p className="desc">This is the start of the #{name} channel. </p>
|
||||
<NavLink
|
||||
to={`/setting/channel/${cid}?f=${pathname}`}
|
||||
className="edit"
|
||||
>
|
||||
<EditIcon className="icon" />
|
||||
Edit Channel
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="feed">
|
||||
{[...msgIds]
|
||||
.sort((a, b) => {
|
||||
return Number(a) - Number(b);
|
||||
})
|
||||
.map((mid, idx) => {
|
||||
const curr = messageData[mid];
|
||||
if (!curr) return null;
|
||||
const isFirst = idx == 0;
|
||||
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
|
||||
const read = curr?.from_uid == loginUid || mid <= readIndex;
|
||||
return renderMessageFragment({
|
||||
selectMode: !!selects,
|
||||
updateReadIndex: updateReadDebounced,
|
||||
read,
|
||||
isFirst,
|
||||
prev,
|
||||
curr,
|
||||
contextId: cid,
|
||||
context: "channel",
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
<StyledChannelChat>
|
||||
{!hasMore && (
|
||||
<div className="info">
|
||||
<h2 className="title">Welcome to #{name} !</h2>
|
||||
<p className="desc">This is the start of the #{name} channel. </p>
|
||||
<NavLink
|
||||
to={`/setting/channel/${cid}?f=${pathname}`}
|
||||
className="edit"
|
||||
>
|
||||
<EditIcon className="icon" />
|
||||
Edit Channel
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
{/* <div className="feed"> */}
|
||||
<InfiniteScroll
|
||||
threshold={50}
|
||||
hasMore={hasMore}
|
||||
useWindow={false}
|
||||
loader={<LoadMore />}
|
||||
isReverse={true}
|
||||
loadMore={pullUp}
|
||||
>
|
||||
{msgIdList.map((mid, idx) => {
|
||||
const curr = messageData[mid];
|
||||
if (!curr) return null;
|
||||
const isFirst = idx == 0;
|
||||
const prev = idx == 0 ? null : messageData[msgIdList[idx - 1]];
|
||||
const read = curr?.from_uid == loginUid || mid <= readIndex;
|
||||
return renderMessageFragment({
|
||||
selectMode: !!selects,
|
||||
updateReadIndex: updateReadDebounced,
|
||||
read,
|
||||
isFirst,
|
||||
prev,
|
||||
curr,
|
||||
contextId: cid,
|
||||
context: "channel",
|
||||
});
|
||||
})}
|
||||
</InfiniteScroll>
|
||||
{appends.map((mid, idx) => {
|
||||
const curr = messageData[mid];
|
||||
if (!curr) return null;
|
||||
const isFirst = idx == 0;
|
||||
const prev =
|
||||
idx == 0 ? msgIdList.slice(-1)[0] : messageData[appends[idx - 1]];
|
||||
const read = curr?.from_uid == loginUid || mid <= readIndex;
|
||||
return renderMessageFragment({
|
||||
selectMode: !!selects,
|
||||
updateReadIndex: updateReadDebounced,
|
||||
read,
|
||||
isFirst,
|
||||
prev,
|
||||
curr,
|
||||
contextId: cid,
|
||||
context: "channel",
|
||||
});
|
||||
})}
|
||||
{/* </div> */}
|
||||
</StyledChannelChat>
|
||||
{/* {unreads != 0 && (
|
||||
<StyledNotification>
|
||||
|
||||
@@ -86,7 +86,8 @@ export const StyledChannelChat = styled.article`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: -webkit-fill-available;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
> .info {
|
||||
padding-top: 62px;
|
||||
display: flex;
|
||||
@@ -127,8 +128,10 @@ export const StyledChannelChat = styled.article`
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
> .feed {
|
||||
/* display: flex;
|
||||
flex-direction: column-reverse; */
|
||||
}
|
||||
/* > .feed {
|
||||
overflow-anchor: none;
|
||||
> div {
|
||||
overflow-anchor: auto;
|
||||
}
|
||||
} */
|
||||
`;
|
||||
|
||||
@@ -7318,7 +7318,7 @@ prompts@^2.4.2:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://mirrors.tencent.com/npm/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@@ -7620,6 +7620,13 @@ react-icons@^4.3.1:
|
||||
resolved "http://mirrors.cloud.tencent.com/npm/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca"
|
||||
integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==
|
||||
|
||||
react-infinite-scroller@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "http://mirrors.cloud.tencent.com/npm/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz#8b80233226dc753a597a0eb52621247f49b15f18"
|
||||
integrity sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==
|
||||
dependencies:
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "http://mirrors.cloud.tencent.com/npm/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
||||
Reference in New Issue
Block a user