diff --git a/src/app/services/channel.js b/src/app/services/channel.js index dcc1b7f8..f7b18d40 100644 --- a/src/app/services/channel.js +++ b/src/app/services/channel.js @@ -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) => ({ diff --git a/src/common/component/ContextMenu.js b/src/common/component/ContextMenu.js index 8b47eade..58f5a291 100644 --- a/src/common/component/ContextMenu.js +++ b/src/common/component/ContextMenu.js @@ -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 ( {items.map((item) => { @@ -11,7 +11,9 @@ export default function ContextMenu({ items = [], hideMenu }) { icon = null, handler = (evt) => { evt.preventDefault(); - hideMenu(); + if (hideMenu) { + hideMenu(); + } }, underline = false, danger = false, @@ -24,7 +26,9 @@ export default function ContextMenu({ items = [], hideMenu }) { key={title} onClick={(evt) => { handler(evt); - hideMenu(); + if (hideMenu) { + hideMenu(); + } }} > {icon} diff --git a/src/common/component/Message/index.js b/src/common/component/Message/index.js index ecfb5fcc..f2347875 100644 --- a/src/common/component/Message/index.js +++ b/src/common/component/Message/index.js @@ -87,8 +87,10 @@ function Message({ const pinInfo = getPinInfo(mid); // return null; + const _key = properties?.local_id || mid; return ( { - 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 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); + const fetchMore = () => { + if (listRef.current.length == 0 && mids.length) { + // 初次 + 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[0]; + const sorteds = mids.slice(0).sort((a, b) => { + return Number(b) - Number(a); + }); + const prepends = sorteds.filter((s) => s > lastMid); + if (prepends.length) { + setPrepends(prepends); + } + console.log("prepends", prepends, items); } - console.log("appends", appends); - } + }; + fetchMore(); }, [mids]); - const pullUp = () => { + + const pullDown = async () => { + // 向下加载 const currPageInfo = pageRef.current; - console.log("pull up", currPageInfo); - // 第一页 - if (currPageInfo && currPageInfo.pageNumber == 1) { - setHasMore(false); - return; + 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, }; diff --git a/src/routes/chat/ChannelChat/LoadMore.js b/src/routes/chat/ChannelChat/LoadMore.js index 78d6edbe..87ef7fdd 100644 --- a/src/routes/chat/ChannelChat/LoadMore.js +++ b/src/routes/chat/ChannelChat/LoadMore.js @@ -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 ( - + { // 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,41 +232,42 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) { ) : null } > - -
-

Welcome to #{name} !

-

This is the start of the #{name} channel.

- - - Edit Channel - -
-
- {[...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", - }); - })} -
+ + {/*
*/} + {feeds.map((mid, idx) => { + const curr = messageData[mid]; + if (!curr) return null; + const isFirst = idx == 0; + const prev = + idx == feeds.length - 1 ? null : messageData[feeds[idx + 1]]; + const read = curr?.from_uid == loginUid || mid <= readIndex; + return renderMessageFragment({ + selectMode: !!selects, + updateReadIndex: updateReadDebounced, + read, + isFirst, + prev, + curr, + contextId: cid, + context: "channel", + }); + })} + {hasMore ? ( + + ) : ( +
+

Welcome to #{name} !

+

This is the start of the #{name} channel.

+ + + Edit Channel + +
+ )} + {/*
*/}
{/* {unreads != 0 && ( diff --git a/src/routes/chat/ChannelChat/index.tmp.js b/src/routes/chat/ChannelChat/index.no.pagination.js similarity index 79% rename from src/routes/chat/ChannelChat/index.tmp.js rename to src/routes/chat/ChannelChat/index.no.pagination.js index 43444b93..5e76ad86 100644 --- a/src/routes/chat/ChannelChat/index.tmp.js +++ b/src/routes/chat/ChannelChat/index.no.pagination.js @@ -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 ( @@ -225,48 +223,41 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) { ) : null } > - - {!hasMore && ( -
-

Welcome to #{name} !

-

This is the start of the #{name} channel.

- - - Edit Channel - -
- )} - {/*
*/} - } - isReverse={true} - loadMore={pullUp} - > - {msgs.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 read = curr?.from_uid == loginUid || mid <= readIndex; - return renderMessageFragment({ - selectMode: !!selects, - updateReadIndex: updateReadDebounced, - read, - isFirst, - prev, - curr, - contextId: cid, - context: "channel", - }); - })} - - {/*
*/} + +
+

Welcome to #{name} !

+

This is the start of the #{name} channel.

+ + + Edit Channel + +
+
+ {[...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", + }); + })} +
{/* {unreads != 0 && ( diff --git a/src/routes/chat/ChannelChat/index.haodong.js b/src/routes/chat/ChannelChat/index.pagination.js similarity index 97% rename from src/routes/chat/ChannelChat/index.haodong.js rename to src/routes/chat/ChannelChat/index.pagination.js index 27c214c4..dfd99a71 100644 --- a/src/routes/chat/ChannelChat/index.haodong.js +++ b/src/routes/chat/ChannelChat/index.pagination.js @@ -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 } > - + {/*
*/} - {[...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, diff --git a/src/routes/chat/ChannelChat/styled.js b/src/routes/chat/ChannelChat/styled.js index 9690410b..0c636961 100644 --- a/src/routes/chat/ChannelChat/styled.js +++ b/src/routes/chat/ChannelChat/styled.js @@ -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; - } } */ `; diff --git a/src/routes/chat/ChannelList/NavItem.js b/src/routes/chat/ChannelList/NavItem.js index fd09c579..1ae3af82 100644 --- a/src/routes/chat/ChannelList/NavItem.js +++ b/src/routes/chat/ChannelList/NavItem.js @@ -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, diff --git a/src/routes/chat/utils.js b/src/routes/chat/utils.js index 2768435e..ab2b9a83 100644 --- a/src/routes/chat/utils.js +++ b/src/routes/chat/utils.js @@ -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 ( - {divider && } + {divider && } ); + // 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;