refactor: stash long list updates

This commit is contained in:
zerosoul
2022-06-01 10:59:41 +08:00
parent aa7c91416c
commit 5c6aed7165
12 changed files with 274 additions and 221 deletions
+13 -24
View File
@@ -8,6 +8,7 @@ import { removeMessage } from "../slices/message";
import { removeChannelSession } from "../slices/message.channel";
import { removeReactionMessage } from "../slices/message.reaction";
import { onMessageSendStarted } from "./handlers";
import handleChatMessage from "../../common/hook/useStreaming/chat.handler";
export const channelApi = createApi({
reducerPath: "channelApi",
baseQuery,
@@ -58,31 +59,19 @@ export const channelApi = createApi({
},
}),
getHistoryMessages: builder.query({
query: ({ gid, mid = 0, limit = 50 }) => ({
url: `/group/${gid}/history?before=${mid}&limit=${limit}`,
query: ({ gid, mid = null, limit = 100 }) => ({
url: mid
? `/group/${gid}/history?before=${mid}&limit=${limit}`
: `/group/${gid}/history?limit=${limit}`,
}),
// async onQueryStarted(id, { dispatch, getState, queryFulfilled }) {
// const {
// ui: { channelSetting },
// channelMessage,
// } = getState();
// try {
// await queryFulfilled;
// dispatch(removeChannel(id));
// if (id == channelSetting) {
// dispatch(toggleChannelSetting());
// }
// // 删掉该channel下的所有消息&reaction
// const mids = channelMessage[id];
// if (mids) {
// dispatch(removeChannelSession(id));
// dispatch(removeMessage(mids));
// dispatch(removeReactionMessage(mids));
// }
// } catch {
// console.log("remove channel error");
// }
// },
async onQueryStarted(id, { dispatch, getState, queryFulfilled }) {
const { data: messages } = await queryFulfilled;
if (messages?.length) {
messages.forEach((msg) => {
handleChatMessage(msg, dispatch, getState());
});
}
},
}),
createInviteLink: builder.query({
query: (gid) => ({
+5 -1
View File
@@ -1,7 +1,7 @@
// import { useRef } from "react";
// import styled from "styled-components";
import StyledMenu from "./styled/Menu";
export default function ContextMenu({ items = [], hideMenu }) {
export default function ContextMenu({ items = [], hideMenu = null }) {
return (
<StyledMenu>
{items.map((item) => {
@@ -11,7 +11,9 @@ export default function ContextMenu({ items = [], hideMenu }) {
icon = null,
handler = (evt) => {
evt.preventDefault();
if (hideMenu) {
hideMenu();
}
},
underline = false,
danger = false,
@@ -24,7 +26,9 @@ export default function ContextMenu({ items = [], hideMenu }) {
key={title}
onClick={(evt) => {
handler(evt);
if (hideMenu) {
hideMenu();
}
}}
>
{icon}
+4
View File
@@ -87,8 +87,10 @@ function Message({
const pinInfo = getPinInfo(mid);
// return null;
const _key = properties?.local_id || mid;
return (
<StyledWrapper
key={_key}
onContextMenu={handleContextMenuEvent}
data-msg-mid={mid}
ref={inviewRef}
@@ -97,6 +99,8 @@ function Message({
} ${contextMenuVisible ? "contextVisible" : ""} `}
>
<Tippy
key={_key}
popperOptions={{ strategy: "fixed" }}
disabled={readOnly}
interactive
placement="left"
+1
View File
@@ -1,6 +1,7 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
z-index: 9999;
display: flex;
flex-direction: column;
justify-content: center;
+83 -44
View File
@@ -1,127 +1,166 @@
import { useEffect, useState, useRef } from "react";
import { useSelector } from "react-redux";
import { useLazyGetHistoryMessagesQuery } from "../../app/services/channel";
const getFeedWithPagination = (config) => {
const { pageNumber = 1, pageSize = 10, mids = [], isLast = false } =
config || {};
const { pageNumber = 1, pageSize = 20, mids = [] } = config || {};
const shadowMids = mids.slice(0);
if (shadowMids.length == 0)
return {
isFirst: true,
isLast: true,
pageCount: 0,
pageSize,
pageNumber: 1,
ids: [],
};
shadowMids.sort((a, b) => {
return Number(a) - Number(b);
return Number(b) - Number(a);
});
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
(pageNumber - 1) * pageSize,
pageNumber * pageSize
);
return {
const info = {
isFirst: pageNumber == 1,
isLast: pageNumber == pageCount,
pageCount,
pageSize,
pageNumber: computedPageNumber,
pageNumber,
ids,
};
console.log("get page Info", info);
return info;
};
export default function useMessageFeed({ context = "channel", id = null }) {
const [loadMoreFromServer] = useLazyGetHistoryMessagesQuery();
const listRef = useRef([]);
const pageRef = useRef(null);
const [hasMore, setHasMore] = useState(true);
const [appends, setAppends] = useState([]);
const [prepends, setPrepends] = useState([]);
// const [appends, setAppends] = useState([]);
const [items, setItems] = useState([]);
const { mids, messageData } = useSelector((store) => {
const { mids } = useSelector((store) => {
return {
mids:
context == "channel"
? store.channelMessage[id] || []
: store.userMessage.byId[id] || [],
messageData: store.message,
// messageData: store.message,
};
});
useEffect(() => {
listRef.current = [];
pageRef.current = [];
setItems([]);
setPrepends([]);
setHasMore(true);
setAppends([]);
// setAppends([]);
}, [context, id]);
// useEffect(() => {
// if (prepends.length) {
// const feedsWrapperEle = document.querySelector(
// `#RUSTCHAT_FEED_${context}_${id}`
// );
// const [newestId]=prepends;
// if (feedsWrapperEle) {
// feedsWrapperEle.scrollTop = feedsWrapperEle.scrollHeight;
// }
// }
// }, [prepends, context, id,messageData]);
useEffect(() => {
if (appends.length) {
const feedsWrapperEle = document.querySelector(
`#RUSTCHAT_FEED_${context}_${id}`
);
if (feedsWrapperEle) {
feedsWrapperEle.scrollTop = feedsWrapperEle.scrollHeight;
const container = document.querySelector(`#RUSTCHAT_FEED_${context}_${id}`);
const handler = (e) => {
e.preventDefault();
var n = 0;
if ("deltaY" in e) n = 1 === e.deltaMode ? 20 * -e.deltaY : -e.deltaY;
else if ("wheelDeltaY" in e) n = (e.wheelDeltaY / 120) * 20;
else if ("wheelDelta" in e) n = (e.wheelDelta / 120) * 20;
else {
n = (-e.detail / 3) * 20;
}
container.scrollTop += n;
};
if (container) {
container.addEventListener("wheel", handler);
}
}, [appends, context, id]);
return () => {
if (container) {
container.removeEventListener("wheel", handler);
}
};
}, [context, id]);
useEffect(() => {
if (listRef.current.length == 0) {
const fetchMore = () => {
if (listRef.current.length == 0 && mids.length) {
// 初次
const pageInfo = getFeedWithPagination({ mids, isLast: true });
console.log("pull up 2", pageInfo);
const pageInfo = getFeedWithPagination({ mids });
console.log("pull down 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 lastMid = listRef.current[0];
const sorteds = mids.slice(0).sort((a, b) => {
return Number(a) - Number(b);
return Number(b) - Number(a);
});
const appends = sorteds.filter((s) => s > lastMid);
if (appends.length) {
setAppends(appends);
const prepends = sorteds.filter((s) => s > lastMid);
if (prepends.length) {
setPrepends(prepends);
}
console.log("appends", appends);
console.log("prepends", prepends, items);
}
};
fetchMore();
}, [mids]);
const pullUp = () => {
const pullDown = async () => {
// 向下加载
const currPageInfo = pageRef.current;
console.log("pull up", currPageInfo);
// 一页
if (currPageInfo && currPageInfo.pageNumber == 1) {
console.log("pull down", currPageInfo);
// 最后一页
if (currPageInfo && currPageInfo.isLast) {
const [mid] = currPageInfo.ids.slice(-1);
const { data: newList } = await loadMoreFromServer({ mid, gid: id });
if (newList.length == 0) {
setHasMore(false);
return;
}
}
let pageInfo = null;
if (!currPageInfo) {
// 初始化
pageInfo = getFeedWithPagination({
mids,
isLast: true,
});
} else {
const prevPageNumber = currPageInfo.pageNumber - 1;
const nextPageNumber = currPageInfo.pageNumber + 1;
pageInfo = getFeedWithPagination({
mids,
pageNumber: prevPageNumber,
pageNumber: nextPageNumber,
});
}
pageRef.current = pageInfo;
listRef.current = [...pageInfo.ids, ...listRef.current];
listRef.current = [...listRef.current, ...pageInfo.ids];
setTimeout(() => {
setHasMore(!pageInfo.isLast);
setItems(listRef.current);
console.log("pull up", currPageInfo, listRef.current);
setHasMore(pageInfo.pageNumber !== 1);
}, 800);
};
const pullDown = () => {
// 向下加载
console.log("pull down update", currPageInfo, listRef.current);
}, 1000);
};
return {
mids,
appends,
prepends,
// appends,
hasMore,
pullUp,
// pullUp,
pullDown,
list: items,
};
+30 -29
View File
@@ -1,8 +1,9 @@
// import { useRef, useEffect } from "react";
import { useRef, useEffect } from "react";
import styled from "styled-components";
import { Waveform } from "@uiball/loaders";
const Styled = styled.div`
margin-top: 80px;
display: flex;
justify-content: center;
align-items: center;
@@ -10,35 +11,35 @@ const Styled = styled.div`
padding: 30px 0;
/* 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]);
export default function LoadMore({ pullDown = null }) {
const ref = useRef(undefined);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const intersecting = entry.isIntersecting;
// const currEle = entry.target;
if (intersecting && pullDown) {
// load more
console.log("inview");
pullDown();
}
});
},
{ threshold: 0 }
);
const currEle = ref?.current;
if (currEle) {
observer.observe(ref.current);
}
return () => {
if (currEle) {
observer.unobserve(currEle);
}
};
}, [ref, pullDown]);
return (
<Styled>
<Styled ref={ref}>
<Waveform
className="loading"
size={24}
+33 -23
View File
@@ -3,11 +3,14 @@ import { useDebounce } from "rooks";
import { NavLink, useLocation } from "react-router-dom";
import Tippy from "@tippyjs/react";
import { useDispatch, useSelector } from "react-redux";
// import InfiniteScroll from "react-infinite-scroll-component";
import PinList from "./PinList";
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";
@@ -30,8 +33,13 @@ import {
StyledHeader,
} from "./styled";
import InviteModal from "../../../common/component/InviteModal";
import LoadMore from "./LoadMore";
export default function ChannelChat({ cid = "", dropFiles = [] }) {
const { list: msgIds, prepends, hasMore, pullDown } = useMessageFeed({
context: "channel",
id: cid,
});
const [toolVisible, setToolVisible] = useState("");
const { pathname } = useLocation();
const dispatch = useDispatch();
@@ -41,7 +49,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
const {
selects,
msgIds,
// msgIds,
userIds,
data,
messageData,
@@ -54,13 +62,13 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
footprint: store.footprint,
loginUser: store.contacts.byId[store.authData.uid],
loginUid: store.authData.uid,
msgIds: store.channelMessage[cid] || [],
// msgIds: store.channelMessage[cid] || [],
userIds: store.contacts.ids,
data: store.channels.byId[cid] || {},
messageData: store.message || {},
};
});
const ref = useChatScroll(msgIds);
// const ref = useChatScroll(msgIds);
// const handleClearUnreads = () => {
// dispatch(readMessage(msgIds));
// };
@@ -86,6 +94,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
console.log("channel message list", msgIds);
const readIndex = footprint.readChannels[cid];
const pinCount = data?.pinned_messages?.length || 0;
const feeds = [...prepends, ...msgIds];
return (
<>
{addMemberModalVisible && (
@@ -223,28 +232,14 @@ 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) => {
<StyledChannelChat id={`RUSTCHAT_FEED_channel_${cid}`}>
{/* <div className="feed"> */}
{feeds.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 prev =
idx == feeds.length - 1 ? null : messageData[feeds[idx + 1]];
const read = curr?.from_uid == loginUid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
@@ -257,7 +252,22 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
context: "channel",
});
})}
{hasMore ? (
<LoadMore pullDown={pullDown} />
) : (
<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> */}
</StyledChannelChat>
{/* {unreads != 0 && (
<StyledNotification>
@@ -2,15 +2,12 @@ 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 useMessageFeed from "../../../common/hook/useMessageFeed";
import useChatScroll from "../../../common/hook/useChatScroll";
import ChannelIcon from "../../../common/component/ChannelIcon";
import Tooltip from "../../../common/component/Tooltip";
import Contact from "../../../common/component/Contact";
@@ -26,14 +23,15 @@ 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 { StyledContacts, StyledChannelChat, StyledHeader } from "./styled";
import {
// StyledNotification,
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();
@@ -43,6 +41,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
const {
selects,
msgIds,
userIds,
data,
messageData,
@@ -55,16 +54,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(mids);
const ref = useChatScroll(msgIds);
// const handleClearUnreads = () => {
// dispatch(readMessage(msgIds));
// };
// remember the route while switching out
useEffect(() => {
dispatch(updateRemeberedNavs());
return () => {
@@ -84,10 +83,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;
const msgs = [...msgIdList, ...appends];
return (
<>
{addMemberModalVisible && (
@@ -211,10 +209,10 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
return (
<Contact
enableContextMenu={true}
cid={cid}
owner={owner == uid}
key={uid}
uid={uid}
cid={cid}
dm
popover
/>
@@ -225,8 +223,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
) : null
}
>
<StyledChannelChat id={`RUSTCHAT_FEED_channel_${cid}`}>
{!hasMore && (
<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>
@@ -238,21 +235,16 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
Edit Channel
</NavLink>
</div>
)}
{/* <div className="feed"> */}
<InfiniteScroll
threshold={100}
hasMore={hasMore}
useWindow={false}
loader={<LoadMore />}
isReverse={true}
loadMore={pullUp}
>
{msgs.map((mid, idx) => {
<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[msgs[idx - 1]];
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
const read = curr?.from_uid == loginUid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
@@ -265,8 +257,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
context: "channel",
});
})}
</InfiniteScroll>
{/* </div> */}
</div>
</StyledChannelChat>
{/* {unreads != 0 && (
<StyledNotification>
@@ -36,7 +36,7 @@ import InviteModal from "../../../common/component/InviteModal";
import LoadMore from "./LoadMore";
export default function ChannelChat({ cid = "", dropFiles = [] }) {
const { list: msgIds, appends, hasMore, pullDown } = useMessageFeed({
const { list: msgIds, prepends, hasMore, pullDown } = useMessageFeed({
context: "channel",
id: cid,
});
@@ -94,6 +94,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
console.log("channel message list", msgIds);
const readIndex = footprint.readChannels[cid];
const pinCount = data?.pinned_messages?.length || 0;
const feeds = [...prepends, ...msgIds];
return (
<>
{addMemberModalVisible && (
@@ -231,14 +232,14 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
) : null
}
>
<StyledChannelChat id="ScrollFeedWrapper">
<StyledChannelChat id={`RUSTCHAT_FEED_channel_${cid}`}>
{/* <div className="feed"> */}
{[...msgIds].map((mid, idx) => {
{feeds.map((mid, idx) => {
const curr = messageData[mid];
if (!curr) return null;
const isFirst = idx == 0;
const prev =
idx == msgIds.length - 1 ? null : messageData[msgIds[idx + 1]];
idx == feeds.length - 1 ? null : messageData[feeds[idx + 1]];
const read = curr?.from_uid == loginUid || mid <= readIndex;
return renderMessageFragment({
selectMode: !!selects,
+9 -4
View File
@@ -88,6 +88,15 @@ export const StyledChannelChat = styled.article`
height: -webkit-fill-available;
overflow-x: hidden;
overflow-y: auto;
/* pagination start */
transform: rotate(180deg);
direction: rtl;
> div,
> hr {
direction: ltr;
transform: rotate(180deg);
}
/* pagination end */
> .info {
padding-top: 62px;
display: flex;
@@ -129,9 +138,5 @@ export const StyledChannelChat = styled.article`
}
}
/* > .feed {
overflow-anchor: none;
> div {
overflow-anchor: auto;
}
} */
`;
+4 -4
View File
@@ -128,10 +128,10 @@ const NavItem = ({ id, setFiles, toggleRemoveConfirm }) => {
title: muted ? "Unmute" : "Mute",
handler: handleMute,
},
{
title: "Notification Settings",
underline: true,
},
// {
// title: "Notification Settings",
// underline: true,
// },
{
title: "Invite People",
handler: toggleInviteModalVisible,
+10 -2
View File
@@ -179,10 +179,9 @@ export const renderMessageFragment = ({
}
}
const _key = local_id || mid;
// console.log("_key", _key, local_id, mid);
console.log("_key", _key, local_id, mid);
return (
<React.Fragment key={_key}>
{divider && <Divider content={divider}></Divider>}
<MessageWrapper
key={_key}
data-key={_key}
@@ -202,8 +201,17 @@ export const renderMessageFragment = ({
contextId={contextId}
/>
</MessageWrapper>
{divider && <Divider content={divider}></Divider>}
</React.Fragment>
);
// React.memo(
// (prevs, nexts) => {
// // curr.properties?.local_id
// const prevObj = prevs.curr || undefined;
// const nextObj = nexts.curr || undefined;
// return prevObj?.properties?.local_id === nextObj?.properties?.local_id;
// }
// );
};
export default getUnreadCount;