diff --git a/src/app/services/handlers.js b/src/app/services/handlers.js index a7d37a83..3f80c5d8 100644 --- a/src/app/services/handlers.js +++ b/src/app/services/handlers.js @@ -59,10 +59,12 @@ export const onMessageSendStarted = async ( console.log("message server mid", server_mid); batch(() => { dispatch(removeContextMessage({ id, mid: ts })); - dispatch(removeMessage(ts)); dispatch(addMessage({ mid: server_mid, ...tmpMsg })); dispatch(addContextMessage({ id, mid: server_mid })); }); + setTimeout(() => { + dispatch(removeMessage(ts)); + }, 300); // dispatch(removePendingMessage({ id, mid:ts, type: from })); } catch { console.log("message send failed"); diff --git a/src/common/component/Message/ContextMenu.js b/src/common/component/Message/ContextMenu.js index 0380fd92..e4d14753 100644 --- a/src/common/component/Message/ContextMenu.js +++ b/src/common/component/Message/ContextMenu.js @@ -27,6 +27,7 @@ export default function MessageContextMenu({ // isMarkdown, canEdit, canPin, + canDelete, canCopy, canReply, pinned, @@ -97,7 +98,7 @@ export default function MessageContextMenu({ icon: , handler: handleSelect, }, - { + canDelete && { title: "Delete", danger: true, icon: , diff --git a/src/common/component/Message/index.js b/src/common/component/Message/index.js index f2347875..73fc4c75 100644 --- a/src/common/component/Message/index.js +++ b/src/common/component/Message/index.js @@ -103,7 +103,7 @@ function Message({ popperOptions={{ strategy: "fixed" }} disabled={readOnly} interactive - placement="left" + placement="right" trigger="click" content={} > diff --git a/src/common/hook/useMessageFeed.js b/src/common/hook/useMessageFeed.js index a0dd0602..78c593e1 100644 --- a/src/common/hook/useMessageFeed.js +++ b/src/common/hook/useMessageFeed.js @@ -1,8 +1,10 @@ import { useEffect, useState, useRef } from "react"; import { useSelector } from "react-redux"; import { useLazyGetHistoryMessagesQuery } from "../../app/services/channel"; + const getFeedWithPagination = (config) => { - const { pageNumber = 1, pageSize = 20, mids = [] } = config || {}; + const { pageNumber = 1, pageSize = 30, mids = [], isLast = false } = + config || {}; const shadowMids = mids.slice(0); if (shadowMids.length == 0) @@ -15,119 +17,112 @@ const getFeedWithPagination = (config) => { ids: [], }; shadowMids.sort((a, b) => { - return Number(b) - Number(a); + 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( - (pageNumber - 1) * pageSize, - pageNumber * pageSize + (computedPageNumber - 1) * pageSize, + computedPageNumber * pageSize ); - const info = { - isFirst: pageNumber == 1, - isLast: pageNumber == pageCount, + return { + isFirst: computedPageNumber == 1, + isLast: computedPageNumber == pageCount, pageCount, pageSize, - pageNumber, + pageNumber: computedPageNumber, ids, }; - console.log("get page Info", info); - return info; }; +let curScrollPos = 0; +let oldScroll = 0; export default function useMessageFeed({ context = "channel", id = null }) { const [loadMoreFromServer] = useLazyGetHistoryMessagesQuery(); const listRef = useRef([]); const pageRef = useRef(null); + const containerRef = useRef(null); const [hasMore, setHasMore] = useState(true); - const [prepends, setPrepends] = useState([]); - // const [appends, setAppends] = useState([]); + const [appends, setAppends] = useState([]); const [items, setItems] = useState([]); - const { mids } = useSelector((store) => { + const { mids, messageData, loginUid } = useSelector((store) => { return { + loginUid: store.authData.uid, 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(() => { - 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; + if (items.length) { + containerRef.current = document.querySelector( + `#RUSTCHAT_FEED_${context}_${id}` + ); + if (containerRef.current) { + const newScroll = + containerRef.current.scrollHeight - containerRef.current.clientHeight; + containerRef.current.scrollTop = curScrollPos + (newScroll - oldScroll); } - container.scrollTop += n; - }; - if (container) { - container.addEventListener("wheel", handler); } - return () => { - if (container) { - container.removeEventListener("wheel", handler); - } - }; - }, [context, id]); - + }, [items, context, id]); useEffect(() => { - 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); + if (listRef.current.length == 0 && mids.length) { + // 初次 + 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) { + 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; + }, 100); + } } - console.log("prepends", prepends, items); + setAppends(appends); } - }; - fetchMore(); - }, [mids]); - - const pullDown = async () => { - // 向下加载 + console.log("appends", appends, listRef.current); + } + }, [mids, messageData, loginUid]); + const pullUp = async () => { const currPageInfo = pageRef.current; - console.log("pull down", currPageInfo); - // 最后一页 - if (currPageInfo && currPageInfo.isLast) { - const [mid] = currPageInfo.ids.slice(-1); - const { data: newList } = await loadMoreFromServer({ mid, gid: id }); + console.log("pull up", currPageInfo); + // 第一页 + if (currPageInfo && currPageInfo.isFirst) { + const [firstMid] = currPageInfo.ids; + const { data: newList } = await loadMoreFromServer({ + mid: firstMid, + gid: id, + }); if (newList.length == 0) { setHasMore(false); return; @@ -138,29 +133,37 @@ export default function useMessageFeed({ context = "channel", id = null }) { // 初始化 pageInfo = getFeedWithPagination({ mids, + isLast: true, }); } else { - const nextPageNumber = currPageInfo.pageNumber + 1; + const prevPageNumber = currPageInfo.pageNumber - 1; pageInfo = getFeedWithPagination({ mids, - pageNumber: nextPageNumber, + pageNumber: prevPageNumber, }); } pageRef.current = pageInfo; - listRef.current = [...listRef.current, ...pageInfo.ids]; + listRef.current = [...pageInfo.ids, ...listRef.current]; setTimeout(() => { - setHasMore(!pageInfo.isLast); + const container = containerRef.current; + if (container) { + curScrollPos = container.scrollTop; + oldScroll = container.scrollHeight - container.clientHeight; + } setItems(listRef.current); - console.log("pull down update", currPageInfo, listRef.current); - }, 1000); + console.log("pull up", currPageInfo, listRef.current); + setHasMore(pageInfo.pageNumber !== 1); + }, 800); + }; + const pullDown = () => { + // 向下加载 }; return { mids, - prepends, - // appends, + appends, hasMore, - // pullUp, + pullUp, pullDown, list: items, }; diff --git a/src/routes/chat/ChannelChat/LoadMore.js b/src/routes/chat/ChannelChat/LoadMore.js index 87ef7fdd..a1ae9ba6 100644 --- a/src/routes/chat/ChannelChat/LoadMore.js +++ b/src/routes/chat/ChannelChat/LoadMore.js @@ -11,7 +11,7 @@ const Styled = styled.div` padding: 30px 0; /* background-color: #eee; */ `; -export default function LoadMore({ pullDown = null }) { +export default function LoadMore({ pullUp = null }) { const ref = useRef(undefined); useEffect(() => { const observer = new IntersectionObserver( @@ -19,10 +19,10 @@ export default function LoadMore({ pullDown = null }) { entries.forEach((entry) => { const intersecting = entry.isIntersecting; // const currEle = entry.target; - if (intersecting && pullDown) { + if (intersecting && pullUp) { // load more console.log("inview"); - pullDown(); + pullUp(); } }); }, @@ -37,7 +37,7 @@ export default function LoadMore({ pullDown = null }) { observer.unobserve(currEle); } }; - }, [ref, pullDown]); + }, [ref, pullUp]); return ( {addMemberModalVisible && ( @@ -233,27 +231,9 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) { } > - {/*
*/} - {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} !

@@ -267,6 +247,25 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
)} + {/*
*/} + {feeds.map((mid, idx) => { + const curr = messageData[mid]; + if (!curr) return null; + const isFirst = idx == 0; + const prev = isFirst ? 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", + }); + })} + {/*
*/} {/* {unreads != 0 && ( diff --git a/src/routes/chat/ChannelChat/index.reverse.pagination.js b/src/routes/chat/ChannelChat/index.reverse.pagination.js new file mode 100644 index 00000000..dfd99a71 --- /dev/null +++ b/src/routes/chat/ChannelChat/index.reverse.pagination.js @@ -0,0 +1,288 @@ +import { useState, useEffect } from "react"; +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 useMessageFeed from "../../../common/hook/useMessageFeed"; + +import ChannelIcon from "../../../common/component/ChannelIcon"; +import Tooltip from "../../../common/component/Tooltip"; +import Contact from "../../../common/component/Contact"; +import Layout from "../Layout"; +import { renderMessageFragment } from "../utils"; +import EditIcon from "../../../assets/icons/edit.svg"; +// import alertIcon from "../../../assets/icons/alert.svg?url"; +import IconFav from "../../../assets/icons/bookmark.svg"; +import IconPeople from "../../../assets/icons/people.svg"; +import IconPin from "../../../assets/icons/pin.svg"; +// import searchIcon from "../../../assets/icons/search.svg?url"; +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 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(); + const [updateReadIndex] = useReadMessageMutation(); + const updateReadDebounced = useDebounce(updateReadIndex, 300); + const [membersVisible, setMembersVisible] = useState(true); + const [addMemberModalVisible, setAddMemberModalVisible] = useState(false); + const { + selects, + // msgIds, + userIds, + data, + messageData, + loginUid, + loginUser, + footprint, + } = useSelector((store) => { + return { + selects: store.ui.selectMessages[`channel_${cid}`], + 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 handleClearUnreads = () => { + // dispatch(readMessage(msgIds)); + // }; + useEffect(() => { + dispatch(updateRemeberedNavs()); + return () => { + dispatch(updateRemeberedNavs({ path: pathname })); + }; + }, [pathname]); + + const toggleMembersVisible = () => { + setMembersVisible((prev) => !prev); + }; + const toggleAddVisible = () => { + setAddMemberModalVisible((prev) => !prev); + }; + if (!data) return null; + const { name, description, is_public, members = [], owner } = data; + const memberIds = is_public + ? 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 feeds = [...prepends, ...msgIds]; + return ( + <> + {addMemberModalVisible && ( + + )} + +
    +
  • + + + +
  • + + { + setToolVisible("pin"); + }} + onHide={() => { + setToolVisible(""); + }} + placement="left-start" + popperOptions={{ strategy: "fixed" }} + offset={[0, 80]} + interactive + trigger="click" + content={} + > +
  • 0 ? "badge" : ""} ${ + toolVisible == "pin" ? "active" : "" + } `} + data-count={pinCount} + > + +
  • +
    +
    + + { + setToolVisible("favorite"); + }} + onHide={() => { + setToolVisible(""); + }} + placement="left-start" + popperOptions={{ strategy: "fixed" }} + offset={[0, 180]} + interactive + trigger="click" + content={} + > +
  • + +
  • +
    +
    +
  • + + + +
  • +
+
+
    +
  • + + app icon + +
  • +
  • + + app icon + +
  • +
+ + } + header={ + +
+ + {name} + {description} +
+
+ } + contacts={ + membersVisible ? ( + <> + + {addVisible && ( +
+ +
Add members
+
+ )} + {memberIds.map((uid) => { + return ( + + ); + })} +
+ + ) : null + } + > + + {/*
*/} + {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 && ( + +
+ {unreads} new messages + {msgs.lastAccess + ? `since ${dayjs(msgs.lastAccess).format("YYYY-MM-DD h:mm:ss A")}` + : ""} +
+ +
+ )} */} +
+ + ); +} diff --git a/src/routes/chat/ChannelChat/styled.js b/src/routes/chat/ChannelChat/styled.js index 0c636961..c5c55970 100644 --- a/src/routes/chat/ChannelChat/styled.js +++ b/src/routes/chat/ChannelChat/styled.js @@ -88,14 +88,15 @@ export const StyledChannelChat = styled.article` height: -webkit-fill-available; overflow-x: hidden; overflow-y: auto; + /* overflow-anchor: none; */ /* pagination start */ - transform: rotate(180deg); + /* transform: rotate(180deg); direction: rtl; > div, > hr { direction: ltr; transform: rotate(180deg); - } + } */ /* pagination end */ > .info { padding-top: 62px; diff --git a/src/routes/chat/utils.js b/src/routes/chat/utils.js index ab2b9a83..52394d18 100644 --- a/src/routes/chat/utils.js +++ b/src/routes/chat/utils.js @@ -182,6 +182,7 @@ export const renderMessageFragment = ({ console.log("_key", _key, local_id, mid); return ( + {divider && } - {divider && } ); // React.memo(