diff --git a/src/app/slices/message.channel.js b/src/app/slices/message.channel.js index dd55e80b..2e444a44 100644 --- a/src/app/slices/message.channel.js +++ b/src/app/slices/message.channel.js @@ -33,6 +33,16 @@ const channelMsgSlice = createSlice({ } } }, + replaceChannelMsg(state, action) { + const { id, localMid, serverMid } = action.payload; + if (state[id]) { + const localIdx = state[id].findIndex((i) => i == localMid); + if (localIdx > -1 && serverMid) { + // 存在 则再删除 + state[id].splice(localIdx, 1, serverMid); + } + } + }, removeChannelSession(state, action) { const ids = Array.isArray(action.payload) ? action.payload @@ -49,5 +59,6 @@ export const { fullfillChannelMsg, addChannelMsg, removeChannelMsg, + replaceChannelMsg, } = channelMsgSlice.actions; export default channelMsgSlice.reducer; diff --git a/src/app/slices/message.user.js b/src/app/slices/message.user.js index 52478bc4..7f8a7966 100644 --- a/src/app/slices/message.user.js +++ b/src/app/slices/message.user.js @@ -42,6 +42,16 @@ const userMsgSlice = createSlice({ } } }, + replaceUserMsg(state, action) { + const { id, localMid, serverMid } = action.payload; + if (state.byId[id]) { + const localIdx = state.byId[id].findIndex((i) => i == localMid); + if (localIdx > -1 && serverMid) { + // 存在 则再删除 + state.byId[id].splice(localIdx, 1, serverMid); + } + } + }, removeUserSession(state, action) { const ids = Array.isArray(action.payload) ? action.payload @@ -59,5 +69,6 @@ export const { fullfillUserMsg, addUserMsg, removeUserMsg, + replaceUserMsg, } = userMsgSlice.actions; export default userMsgSlice.reducer; diff --git a/src/common/component/FileMessage/index.js b/src/common/component/FileMessage/index.js index 6170e638..bde5b793 100644 --- a/src/common/component/FileMessage/index.js +++ b/src/common/component/FileMessage/index.js @@ -1,148 +1,164 @@ -import { useEffect, useState } from 'react'; -import dayjs from 'dayjs'; -import relativeTime from 'dayjs/plugin/relativeTime'; -import { useSelector } from 'react-redux'; -import Styled from './styled'; -import Image from './Image'; -import useRemoveLocalMessage from '../../hook/useRemoveLocalMessage'; -import useUploadFile from '../../hook/useUploadFile'; -import useSendMessage from '../../hook/useSendMessage'; -import Progress from './Progress'; -import { getFileIcon, formatBytes, isImage, getImageSize } from '../../utils'; -import IconDownload from '../../../assets/icons/download.svg'; -import IconClose from '../../../assets/icons/close.circle.svg'; +import { useEffect, useState } from "react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { useSelector } from "react-redux"; +import Styled from "./styled"; +import Image from "./Image"; +import useRemoveLocalMessage from "../../hook/useRemoveLocalMessage"; +import useUploadFile from "../../hook/useUploadFile"; +import useSendMessage from "../../hook/useSendMessage"; +import Progress from "./Progress"; +import { getFileIcon, formatBytes, isImage, getImageSize } from "../../utils"; +import IconDownload from "../../../assets/icons/download.svg"; +import IconClose from "../../../assets/icons/close.circle.svg"; dayjs.extend(relativeTime); const isLocalFile = (content) => { - return content && typeof content == 'string' && content.startsWith('blob:'); + return content && typeof content == "string" && content.startsWith("blob:"); }; export default function FileMessage({ - context = '', - to = null, - created_at, - from_uid = null, - content = '', - download = '', - thumbnail = '', - properties = { local_id: 0, name: '', size: 0, content_type: '' } + context = "", + to = null, + created_at, + from_uid = null, + content = "", + download = "", + thumbnail = "", + properties = { local_id: 0, name: "", size: 0, content_type: "" }, }) { - const [imageSize, setImageSize] = useState(null); - const [uploadingFile, setUploadingFile] = useState(false); - const removeLocalMessage = useRemoveLocalMessage({ context, id: to }); - const { - sendMessage, - isSuccess: sendMessageSuccess, - isSending - } = useSendMessage({ - context, - from: from_uid, - to - }); - const { stopUploading, data, uploadFile, progress, isSuccess: uploadSuccess } = useUploadFile(); - const fromUser = useSelector((store) => store.contacts.byId[from_uid]); - const { size, name, content_type } = properties ?? {}; - useEffect(() => { - const handleUpSend = async ({ url, name, type }) => { - try { - setUploadingFile(true); - if (type.startsWith('image')) { - const size = await getImageSize(url); - setImageSize(size); + const [imageSize, setImageSize] = useState(null); + const [uploadingFile, setUploadingFile] = useState(false); + const removeLocalMessage = useRemoveLocalMessage({ context, id: to }); + const { + sendMessage, + isSuccess: sendMessageSuccess, + isSending, + } = useSendMessage({ + context, + from: from_uid, + to, + }); + const { + stopUploading, + data, + uploadFile, + progress, + isSuccess: uploadSuccess, + } = useUploadFile(); + const fromUser = useSelector((store) => store.contacts.byId[from_uid]); + const { size, name, content_type } = properties ?? {}; + useEffect(() => { + const handleUpSend = async ({ url, name, type }) => { + try { + setUploadingFile(true); + if (type.startsWith("image")) { + const size = await getImageSize(url); + setImageSize(size); + } + let file = await fetch(url) + .then((r) => r.blob()) + .then((blobFile) => new File([blobFile], name, { type })); + + await uploadFile(file); + setUploadingFile(false); + } catch (error) { + setUploadingFile(false); + console.log("fetch local file error", error); + } + }; + // local file + if (isLocalFile(content)) { + handleUpSend({ url: content, name, type: content_type }); } - let file = await fetch(url) - .then((r) => r.blob()) - .then((blobFile) => new File([blobFile], name, { type })); - - await uploadFile(file); - setUploadingFile(false); - } catch (error) { - setUploadingFile(false); - console.log('fetch local file error', error); - } + }, [content, name, content_type]); + useEffect(() => { + const props = properties ?? {}; + const propsV2 = imageSize ? { ...props, ...imageSize } : props; + // 本地文件 并且上传成功 + if (uploadSuccess && isLocalFile(content)) { + console.log( + "send local file message", + uploadSuccess, + propsV2, + data, + content + ); + // 把已经上传的东西当做消息发出去 + const { path } = data; + sendMessage({ + ignoreLocal: true, + type: "file", + content: { path }, + properties: propsV2, + }); + } + }, [uploadSuccess, data, properties, content]); + useEffect(() => { + if (sendMessageSuccess) { + // 回收本地资源 + URL.revokeObjectURL(content); + } + }, [sendMessageSuccess, content]); + const handleCancel = () => { + stopUploading(); + URL.revokeObjectURL(content); + removeLocalMessage(properties.local_id); }; - // local file - if (isLocalFile(content)) { - handleUpSend({ url: content, name, type: content_type }); - } - }, [content, name, content_type]); - useEffect(() => { - const props = properties ?? {}; - const propsV2 = imageSize ? { ...props, ...imageSize } : props; - // 本地文件 并且上传成功 - if (uploadSuccess && isLocalFile(content)) { - console.log('send local file message', uploadSuccess, propsV2, data, content); - // 把已经上传的东西当做消息发出去 - const { path } = data; - sendMessage({ - ignoreLocal: true, - type: 'file', - content: { path }, - properties: propsV2 - }); - } - }, [uploadSuccess, data, properties, content]); - useEffect(() => { - if (sendMessageSuccess) { - // 回收本地资源 - URL.revokeObjectURL(content); - } - }, [sendMessageSuccess, content]); - const handleCancel = () => { - stopUploading(); - URL.revokeObjectURL(content); - removeLocalMessage(properties.local_id); - }; - if (!properties) return null; - const icon = getFileIcon(content_type, name); + if (!properties) return null; + const icon = getFileIcon(content_type, name); - if (!content || !name) return null; + if (!content || !name) return null; - console.log('file content', content, name, content_type, size); - const sending = uploadingFile || isSending; + console.log("file content", content, name, content_type, size); + const sending = uploadingFile || isSending; - if (isImage(content_type, size)) + if (isImage(content_type, size)) + return ( + + ); return ( - - ); - return ( - -
- {icon} -
- {name} - - {/* */} - {sending ? ( - - ) : ( - <> - {formatBytes(size)} - {dayjs(created_at).fromNow()} - {fromUser && ( - - by {fromUser.name} - + +
+ {icon} +
+ {name} + + {/* */} + {sending ? ( + + ) : ( + <> + {formatBytes(size)} + {dayjs(created_at).fromNow()} + {fromUser && ( + + by {fromUser.name} + + )} + + )} + +
+ {/* */} + {sending ? ( + + ) : ( + + + )} - - )} - -
- {/* */} - {sending ? ( - - ) : ( - - - - )} -
- - ); +
+
+ ); } diff --git a/src/common/component/MixedInput/index.js b/src/common/component/MixedInput/index.js index aa9b73a6..e37e2b29 100644 --- a/src/common/component/MixedInput/index.js +++ b/src/common/component/MixedInput/index.js @@ -79,9 +79,9 @@ const Plugins = ({ window.addEventListener("paste", handlePasteEvent); return () => { window.removeEventListener("paste", handlePasteEvent); - if (plateEditor && updateDraft) { - updateDraft(plateEditor.children); - } + // if (plateEditor && updateDraft) { + // updateDraft(plateEditor.children); + // } }; // window.addEventListener("paste") }, [id, updateDraft]); @@ -277,3 +277,9 @@ export const useMixedEditor = (key) => { }; }; export default Plugins; +// export default memo(Plugins, (prev, next) => { +// return ( +// prev.id == next.id && +// JSON.stringify(prev.initialValue) == JSON.stringify(next.initialValue) +// ); +// }); diff --git a/src/common/hook/useMessageFeed.js b/src/common/hook/useMessageFeed.js index c03bc576..32770094 100644 --- a/src/common/hook/useMessageFeed.js +++ b/src/common/hook/useMessageFeed.js @@ -35,10 +35,14 @@ export default function useMessageFeed({ context = "channel", id = 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] || []; + const { mids, messageData } = useSelector((store) => { + return { + mids: + context == "channel" + ? store.channelMessage[id] || [] + : store.userMessage.byId[id] || [], + messageData: store.message, + }; }); useEffect(() => { listRef.current = []; @@ -47,7 +51,16 @@ export default function useMessageFeed({ context = "channel", id = null }) { setHasMore(true); setAppends([]); }, [context, id]); - + useEffect(() => { + if (appends.length) { + const feedsWrapperEle = document.querySelector( + `#RUSTCHAT_FEED_${context}_${id}` + ); + if (feedsWrapperEle) { + feedsWrapperEle.scrollTop = feedsWrapperEle.scrollHeight; + } + } + }, [appends, context, id]); useEffect(() => { if (listRef.current.length == 0) { // 初次 @@ -67,6 +80,7 @@ export default function useMessageFeed({ context = "channel", id = null }) { if (appends.length) { setAppends(appends); } + console.log("appends", appends); } }, [mids]); const pullUp = () => { @@ -97,7 +111,7 @@ export default function useMessageFeed({ context = "channel", id = null }) { setItems(listRef.current); console.log("pull up", currPageInfo, listRef.current); setHasMore(pageInfo.pageNumber !== 1); - }, 300); + }, 800); }; const pullDown = () => { // 向下加载 diff --git a/src/routes/chat/ChannelChat/LoadMore.js b/src/routes/chat/ChannelChat/LoadMore.js index 049a14ec..78d6edbe 100644 --- a/src/routes/chat/ChannelChat/LoadMore.js +++ b/src/routes/chat/ChannelChat/LoadMore.js @@ -7,7 +7,7 @@ const Styled = styled.div` justify-content: center; align-items: center; width: 100%; - padding: 8px; + padding: 30px 0; /* background-color: #eee; */ `; export default function LoadMore() { diff --git a/src/routes/chat/ChannelChat/index.js b/src/routes/chat/ChannelChat/index.js index 2f757cad..5e76ad86 100644 --- a/src/routes/chat/ChannelChat/index.js +++ b/src/routes/chat/ChannelChat/index.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,9 +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; return ( <> {addMemberModalVisible && ( @@ -209,10 +208,11 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) { {memberIds.map((uid) => { return ( @@ -223,66 +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} - > - {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", - }); - })} - - {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", - }); - })} - {/*
*/} + +
+

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.tmp.js b/src/routes/chat/ChannelChat/index.tmp.js new file mode 100644 index 00000000..43444b93 --- /dev/null +++ b/src/routes/chat/ChannelChat/index.tmp.js @@ -0,0 +1,287 @@ +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 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 { 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(); + const [updateReadIndex] = useReadMessageMutation(); + const updateReadDebounced = useDebounce(updateReadIndex, 300); + const [membersVisible, setMembersVisible] = useState(true); + const [addMemberModalVisible, setAddMemberModalVisible] = useState(false); + const { + selects, + 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, + userIds: store.contacts.ids, + data: store.channels.byId[cid] || {}, + messageData: store.message || {}, + }; + }); + // const ref = useChatScroll(mids); + // const handleClearUnreads = () => { + // dispatch(readMessage(msgIds)); + // }; + // remember the route while switching out + 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; + const readIndex = footprint.readChannels[cid]; + const pinCount = data?.pinned_messages?.length || 0; + // const finalHasMore = selects ? false : hasMore; + const msgs = [...msgIdList, ...appends]; + 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 + } + > + + {!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", + }); + })} + + {/*
*/} +
+ {/* {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/utils.js b/src/routes/chat/utils.js index 9e79f08b..2768435e 100644 --- a/src/routes/chat/utils.js +++ b/src/routes/chat/utils.js @@ -123,7 +123,14 @@ const StyledWrapper = styled.div` } } `; -const MessageWrapper = ({ selectMode = false, context, id, mid, children }) => { +const MessageWrapper = ({ + selectMode = false, + context, + id, + mid, + children, + ...rest +}) => { const dispatch = useDispatch(); const selects = useSelector( @@ -135,7 +142,7 @@ const MessageWrapper = ({ selectMode = false, context, id, mid, children }) => { dispatch(updateSelectMessages({ context, id, operation, data: mid })); }; return ( - + {/* {React.cloneElement(children, { readOnly: selected })} */} {children} @@ -177,6 +184,8 @@ export const renderMessageFragment = ({ {divider && }