From fbedaa88258dc1dbd1ecd87c21f2ef12b8038eb7 Mon Sep 17 00:00:00 2001 From: zerosoul Date: Tue, 26 Apr 2022 11:29:03 +0800 Subject: [PATCH] feat: select multiple messages and forward --- src/app/services/message.js | 8 + src/app/slices/ui.js | 30 ++++ src/assets/icons/forward.svg | 3 + src/common/component/Avatar.js | 6 +- src/common/component/Contact.js | 7 +- src/common/component/ForwardModal/index.js | 10 +- src/common/component/ForwardModal/styled.js | 12 +- src/common/component/Message/Commands.js | 30 +++- .../component/Message/PinMessageModal.js | 60 ++++++++ src/common/component/Message/Reply.js | 1 + src/common/component/Message/index.js | 13 +- src/common/component/Message/renderContent.js | 94 +++++++++--- src/common/component/Message/styled.js | 5 + src/common/component/Send/index.js | 41 +++-- src/common/component/styled/Menu.js | 2 +- src/common/hook/useNormalizeMessage.js | 15 +- src/routes/chat/ChannelChat/index.js | 85 +++++------ src/routes/chat/ChannelChat/styled.js | 101 ++++++------- src/routes/chat/DMChat/index.js | 76 +++++----- src/routes/chat/DMChat/styled.js | 19 +-- src/routes/chat/Layout/Operations.js | 68 +++++++++ .../chat/{Layout.js => Layout/index.js} | 141 ++++-------------- src/routes/chat/Layout/styled.js | 119 +++++++++++++++ src/routes/chat/utils.js | 85 ++++++++++- 24 files changed, 702 insertions(+), 329 deletions(-) create mode 100644 src/assets/icons/forward.svg create mode 100644 src/common/component/Message/PinMessageModal.js create mode 100644 src/routes/chat/Layout/Operations.js rename src/routes/chat/{Layout.js => Layout/index.js} (50%) create mode 100644 src/routes/chat/Layout/styled.js diff --git a/src/app/services/message.js b/src/app/services/message.js index f4dfccbe..928ad702 100644 --- a/src/app/services/message.js +++ b/src/app/services/message.js @@ -76,6 +76,13 @@ export const messageApi = createApi({ url: `/resource/archive?file_path=${encodeURIComponent(file_path)}`, }), }), + pinMessage: builder.mutation({ + query: ({ gid, mid }) => ({ + url: `/group/${gid}/pin`, + method: "POST", + body: { mid }, + }), + }), replyMessage: builder.mutation({ query: ({ reply_mid, content, type = "text" }) => ({ headers: { @@ -114,6 +121,7 @@ export const messageApi = createApi({ }); export const { + usePinMessageMutation, useLazyGetArchiveMessageQuery, useGetArchiveMessageQuery, useLazyGetOGInfoQuery, diff --git a/src/app/slices/ui.js b/src/app/slices/ui.js index 9545d491..8a6854b6 100644 --- a/src/app/slices/ui.js +++ b/src/app/slices/ui.js @@ -11,6 +11,7 @@ const initialState = { menuExpand: false, fileListView: Views.grid, uploadFiles: {}, + selectMessages: {}, }; const uiSlice = createSlice({ name: "ui", @@ -40,6 +41,7 @@ const uiSlice = createSlice({ state.userGuide[key] = obj[key]; }); }, + updateUploadFiles(state, action) { const { context = "channel", @@ -105,6 +107,33 @@ const uiSlice = createSlice({ break; } }, + updateSelectMessages(state, action) { + const { + context = "channel", + id = null, + operation = "add", + data = null, + } = action.payload; + let currData = state.selectMessages[`${context}_${id}`]; + switch (operation) { + case "add": { + currData = currData ? [...currData, data] : [data]; + break; + } + case "remove": { + currData = currData.filter((mid) => mid != data); + break; + } + case "reset": { + currData = null; + break; + } + + default: + break; + } + state.selectMessages[`${context}_${id}`] = currData; + }, }, }); export const { @@ -115,6 +144,7 @@ export const { toggleMenuExpand, updateFileListView, updateUploadFiles, + updateSelectMessages, updateUserGuide, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/src/assets/icons/forward.svg b/src/assets/icons/forward.svg new file mode 100644 index 00000000..70dcf2da --- /dev/null +++ b/src/assets/icons/forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/component/Avatar.js b/src/common/component/Avatar.js index 806b1808..73c01aaa 100644 --- a/src/common/component/Avatar.js +++ b/src/common/component/Avatar.js @@ -14,7 +14,8 @@ export default function Avatar({ } const tmp = getInitialsAvatar({ initials: getInitials(name), - background: type == "channel" ? "#4ea758" : undefined, + background: type == "channel" ? "#EAECF0" : undefined, + foreground: type == "channel" ? "#475467" : undefined, }); setSrc(tmp); }; @@ -22,7 +23,8 @@ export default function Avatar({ if (!url) { const tmp = getInitialsAvatar({ initials: getInitials(name), - background: type == "channel" ? "#4ea758" : undefined, + background: type == "channel" ? "#EAECF0" : undefined, + foreground: type == "channel" ? "#475467" : undefined, }); setSrc(tmp); } else { diff --git a/src/common/component/Contact.js b/src/common/component/Contact.js index 47a90e67..ec7e0381 100644 --- a/src/common/component/Contact.js +++ b/src/common/component/Contact.js @@ -35,12 +35,13 @@ const StyledWrapper = styled.div` } .status { position: absolute; - bottom: 0; - right: -2px; + bottom: -2px; + right: -4px; width: 10px; height: 10px; + box-sizing: content-box; border-radius: 50%; - outline: 2px solid #fff; + border: 2px solid #fff; &.online { background-color: #22c55e; } diff --git a/src/common/component/ForwardModal/index.js b/src/common/component/ForwardModal/index.js index ea28a1ef..fead6fbf 100644 --- a/src/common/component/ForwardModal/index.js +++ b/src/common/component/ForwardModal/index.js @@ -16,7 +16,7 @@ import CloseIcon from "../../../assets/icons/close.circle.svg"; import StyledCheckbox from "../../component/styled/Checkbox"; import toast from "react-hot-toast"; -export default function ForwardModal({ mid, closeModal }) { +export default function ForwardModal({ mids, closeModal }) { const [appendText, setAppendText] = useState(""); const { sendMessages } = useSendMessage(); const { forwardMessage, forwarding } = useForwardMessage(); @@ -45,7 +45,7 @@ export default function ForwardModal({ mid, closeModal }) { }; const handleForward = async () => { await forwardMessage({ - mids: [mid], + mids: mids, users: selectedMembers, channels: selectedChannels, }); @@ -170,8 +170,10 @@ export default function ForwardModal({ mid, closeModal }) { ); })} -
- +
+ {mids.map((mid) => ( + + ))}
.reply { + background: none; + } } .input { margin-bottom: 32px; diff --git a/src/common/component/Message/Commands.js b/src/common/component/Message/Commands.js index c7c66af0..873bcfcd 100644 --- a/src/common/component/Message/Commands.js +++ b/src/common/component/Message/Commands.js @@ -4,6 +4,7 @@ import styled from "styled-components"; import Tippy from "@tippyjs/react"; import { hideAll } from "tippy.js"; // import toast from "react-hot-toast"; +import { updateSelectMessages } from "../../../app/slices/ui"; import { addReplyingMessage } from "../../../app/slices/message"; import StyledMenu from "../styled/Menu"; import Tooltip from "../../component/Tooltip"; @@ -15,6 +16,7 @@ import editIcon from "../../../assets/icons/edit.svg?url"; // import bookmarkIcon from "../../../assets/icons/bookmark.svg?url"; import moreIcon from "../../../assets/icons/more.svg?url"; import ForwardModal from "../ForwardModal"; +import PinMessageModal from "./PinMessageModal"; const StyledCmds = styled.ul` z-index: 9999; position: absolute; @@ -50,12 +52,14 @@ const StyledCmds = styled.ul` } `; export default function Commands({ + context = "user", contextId = 0, mid = 0, from_uid = 0, toggleEditMessage, }) { const dispatch = useDispatch(); + const [pinModalVisible, setPinModalVisible] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [forwardModalVisible, setForwardModalVisible] = useState(false); const [tippyVisible, setTippyVisible] = useState(false); @@ -77,9 +81,17 @@ export default function Commands({ hideAll(); setDeleteModalVisible((prev) => !prev); }; + const togglePinModal = () => { + hideAll(); + setPinModalVisible((prev) => !prev); + }; const handleTippyVisible = (visible = true) => { setTippyVisible(visible); }; + const handleSelect = (mid) => { + dispatch(updateSelectMessages({ context, id: contextId, data: mid })); + hideAll(); + }; return ( {/*
  • Edit Message
  • */} - {/*
  • Pin Message
  • */} + {context == "channel" && ( +
  • + Pin Message +
  • + )}
  • Forward
  • Reply
  • +
  • + Select +
  • {currUid == from_uid && (
  • Delete Message @@ -153,7 +172,14 @@ export default function Commands({ )} {forwardModalVisible && ( - + + )} + {pinModalVisible && ( + )} ); diff --git a/src/common/component/Message/PinMessageModal.js b/src/common/component/Message/PinMessageModal.js new file mode 100644 index 00000000..6d54ef92 --- /dev/null +++ b/src/common/component/Message/PinMessageModal.js @@ -0,0 +1,60 @@ +// import React from "react"; +import { useEffect } from "react"; +import styled from "styled-components"; +const StyledPinModal = styled(StyledModal)` + min-width: 406px; + .title, + .desc { + text-align: left; + } + .preview { + border: 1px solid #f2f4f7; + max-height: 256px; + overflow: auto; + background: none; + overflow-x: hidden; + } +`; +// import { useDispatch } from "react-redux"; +// import BASE_URL from "../../app/config"; +import { usePinMessageMutation } from "../../../app/services/message"; +import StyledModal from "../styled/Modal"; +import Button from "../styled/Button"; +import Modal from "../Modal"; +import PreviewMessage from "./PreviewMessage"; + +export default function PinMessageModal({ closeModal, mid = 0, gid = 0 }) { + // const dispatch = useDispatch(); + const [pinMessage, { isLoading, isSuccess }] = usePinMessageMutation(); + const handlePin = () => { + pinMessage({ mid, gid }); + }; + useEffect(() => { + if (isSuccess) { + closeModal(); + } + }, [isSuccess]); + + if (!mid) return null; + return ( + + + + + + } + title="Pin It" + description="You sure you want to pin this message to #gerenal?" + > + + + + ); +} diff --git a/src/common/component/Message/Reply.js b/src/common/component/Message/Reply.js index 9e404e5e..15b2ec66 100644 --- a/src/common/component/Message/Reply.js +++ b/src/common/component/Message/Reply.js @@ -38,6 +38,7 @@ const Styled = styled.div` } } .content { + overflow: hidden; font-weight: 500; font-size: 14px; line-height: 20px; diff --git a/src/common/component/Message/index.js b/src/common/component/Message/index.js index 23eb0623..6cf87ac2 100644 --- a/src/common/component/Message/index.js +++ b/src/common/component/Message/index.js @@ -17,6 +17,7 @@ import Tooltip from "../Tooltip"; dayjs.extend(isToday); dayjs.extend(isYesterday); function Message({ + readOnly = false, contextId = 0, mid = "", context = "user", @@ -77,8 +78,13 @@ function Message({ console.log("render message"); // return null; return ( - + {currUser.name} @@ -130,8 +136,9 @@ function Message({ {reactions && }
  • - {!edit && ( + {!edit && !readOnly && ( .tip { + display: flex; + align-items: center; + gap: 4px; + .icon { + width: 16px; + height: 16px; + path { + fill: #98a2b3; + } + } + font-weight: 400; + font-size: 12px; + line-height: 18px; + color: #98a2b3; + } +`; const ForwardedMessage = ({ context, to, from_uid, id }) => { const { normalizeMessage, messages } = useNormalizeMessage(); const [forwards, setForwards] = useState(null); @@ -123,25 +150,54 @@ const ForwardedMessage = ({ context, to, from_uid, id }) => { useEffect(() => { if (messages) { setForwards( - messages.map((msg) => { - const { - download, - content, - content_type, - properties, - thumbnail, - } = msg; - return renderContent({ - download, - context, - to, - from_uid, - content, - content_type, - properties, - thumbnail, - }); - }) + +

    + + Forwarded +

    +
    + {messages.map((msg, idx) => { + const { + user = {}, + created_at, + download, + content, + content_type, + properties, + thumbnail, + } = msg; + return ( + + {user && ( +
    + +
    + )} +
    +
    + {user?.name} + + {dayjs(created_at).format("YYYY-MM-DD h:mm:ss A")} + +
    +
    + {renderContent({ + download, + context, + to, + from_uid, + content, + content_type, + properties, + thumbnail, + })} +
    +
    +
    + ); + })} +
    +
    ); } }, [messages, context, to, from_uid]); diff --git a/src/common/component/Message/styled.js b/src/common/component/Message/styled.js index 6db0a4a2..b186e888 100644 --- a/src/common/component/Message/styled.js +++ b/src/common/component/Message/styled.js @@ -17,11 +17,16 @@ const StyledMsg = styled.div` } &:hover, &.preview { + content-visibility: inherit; + contain-intrinsic-size: inherit; background: #f5f6f7; .cmds { visibility: visible; } } + &.readonly:hover { + background: none; + } .avatar { flex-shrink: 0; cursor: pointer; diff --git a/src/common/component/Send/index.js b/src/common/component/Send/index.js index 99387bf2..dc2132bb 100644 --- a/src/common/component/Send/index.js +++ b/src/common/component/Send/index.js @@ -26,10 +26,8 @@ const Modes = { markdown: "markdown", }; function Send({ - name, - context = "channel", - members = [], // 发给谁,或者是channel,或者是user + context = "channel", id = "", }) { const editor = useMixedEditor(`${context}_${id}`); @@ -37,16 +35,25 @@ function Send({ const dispatch = useDispatch(); const addLocalFileMesage = useAddLocalFileMessage({ context, to: id }); // 谁发的 - const { from_uid, replying_mid = null, mode, uploadFiles } = useSelector( - (store) => { - return { - mode: store.ui.inputMode, - from_uid: store.authData.uid, - replying_mid: store.message.replying[id], - uploadFiles: store.ui.uploadFiles[`${context}_${id}`], - }; - } - ); + const { + from_uid, + replying_mid = null, + mode, + uploadFiles, + channelsData, + contactsData, + uids, + } = useSelector((store) => { + return { + channelsData: store.channels.byId, + uids: store.contacts.ids, + contactsData: store.contacts.byId, + mode: store.ui.inputMode, + from_uid: store.authData.uid, + replying_mid: store.message.replying[id], + uploadFiles: store.ui.uploadFiles[`${context}_${id}`], + }; + }); const { sendMessage } = useSendMessage({ context, from: from_uid, to: id }); useEffect(() => { @@ -121,7 +128,15 @@ function Send({ const toggleMode = () => { dispatch(updateInputMode(mode == Modes.text ? Modes.markdown : Modes.text)); }; + const name = + context == "channel" ? channelsData[id]?.name : contactsData[id]?.name; const placeholder = `Send to ${Types[context]}${name} `; + const members = + context == "channel" + ? channelsData[id]?.is_public + ? uids + : channelsData[id]?.members + : []; return ( {replying_mid && } diff --git a/src/common/component/styled/Menu.js b/src/common/component/styled/Menu.js index a0f18d7a..f8f75ea1 100644 --- a/src/common/component/styled/Menu.js +++ b/src/common/component/styled/Menu.js @@ -7,7 +7,7 @@ const StyledMenu = styled.ul` background-color: #fff; box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1), 0px 10px 10px rgba(31, 41, 55, 0.04); - border-radius: var(--br); + border-radius: 12px; .item { white-space: nowrap; cursor: pointer; diff --git a/src/common/hook/useNormalizeMessage.js b/src/common/hook/useNormalizeMessage.js index ad736921..e8ea8bf1 100644 --- a/src/common/hook/useNormalizeMessage.js +++ b/src/common/hook/useNormalizeMessage.js @@ -11,7 +11,14 @@ export default function useNormalizeMessage() { useEffect(() => { if (data && isSuccess) { const msgs = data.messages.map( - ({ content, file_id, thumbnail_id, content_type, properties }) => { + ({ + content, + file_id, + thumbnail_id, + content_type, + properties, + from_user, + }) => { const transformedContent = content_type == ContentTypes.file ? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}` @@ -24,7 +31,13 @@ export default function useNormalizeMessage() { content_type == ContentTypes.file ? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}&download=true` : ""; + let user = { ...(data.users[from_user] || {}) }; + user.avatar = + typeof user.avatar !== "undefined" + ? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${user.avatar}` + : ""; return { + user, content: transformedContent, content_type, properties, diff --git a/src/routes/chat/ChannelChat/index.js b/src/routes/chat/ChannelChat/index.js index ced58a9a..d56f0d51 100644 --- a/src/routes/chat/ChannelChat/index.js +++ b/src/routes/chat/ChannelChat/index.js @@ -5,7 +5,6 @@ import { useReadMessageMutation } from "../../../app/services/message"; import useChatScroll from "../../../common/hook/useChatScroll"; import ChannelIcon from "../../../common/component/ChannelIcon"; import Tooltip from "../../../common/component/Tooltip"; -import Send from "../../../common/component/Send"; import Contact from "../../../common/component/Contact"; import Layout from "../Layout"; import { renderMessageFragment } from "../utils"; @@ -13,7 +12,7 @@ import EditIcon from "../../../assets/icons/edit.svg"; import alertIcon from "../../../assets/icons/alert.svg?url"; import peopleIcon from "../../../assets/icons/people.svg?url"; import pinIcon from "../../../assets/icons/pin.svg?url"; -import searchIcon from "../../../assets/icons/search.svg?url"; +// import searchIcon from "../../../assets/icons/search.svg?url"; import headphoneIcon from "../../../assets/icons/headphone.svg?url"; import boardosIcon from "../../../assets/icons/app.boardos.svg?url"; import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url"; @@ -35,6 +34,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) { const [addMemberModalVisible, setAddMemberModalVisible] = useState(false); // const dispatch = useDispatch(); const { + selects, msgIds, userIds, data, @@ -44,6 +44,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) { footprint, } = useSelector((store) => { return { + selects: store.ui.selectMessages[`channel_${cid}`], footprint: store.footprint, loginUser: store.contacts.byId[store.authData.uid], loginUid: store.authData.uid, @@ -150,52 +151,40 @@ 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({ - 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/styled.js b/src/routes/chat/ChannelChat/styled.js index 86345845..953bd69e 100644 --- a/src/routes/chat/ChannelChat/styled.js +++ b/src/routes/chat/ChannelChat/styled.js @@ -82,68 +82,53 @@ export const StyledContacts = styled.div` } `; export const StyledChannelChat = styled.article` - position: relative; + padding: 18px 16px; width: 100%; - /* margin-bottom: 120px; */ - > .wrapper { - background-color: #fff; + height: 100%; + height: -webkit-fill-available; + overflow: auto; + > .info { + padding-top: 62px; display: flex; flex-direction: column; - padding: 0; - padding-bottom: 16px; - height: calc(100vh - 56px - 22px); - .chat { - padding: 18px 16px; - height: 100%; - height: -webkit-fill-available; - overflow: auto; - > .info { - padding-top: 62px; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; - .title { - font-weight: bold; - font-size: 36px; - line-height: 44px; - } - .desc { - color: #78787c; - font-weight: 500; - font-size: 16px; - line-height: 24px; - } - .edit { - display: flex; - align-items: center; - gap: 4px; - .icon { - width: 16px; - height: 16px; - path { - fill: #3c8ce7; - } - } - padding: 0; - font-style: normal; - font-weight: 500; - font-size: 16px; - line-height: 24px; - background: linear-gradient(135deg, #3c8ce7 0%, #00eaff 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - text-fill-color: transparent; - } - } - > .feed { - /* display: flex; - flex-direction: column-reverse; */ - } + align-items: flex-start; + gap: 8px; + .title { + font-weight: bold; + font-size: 36px; + line-height: 44px; } - > .send { - margin: 0 16px; + .desc { + color: #78787c; + font-weight: 500; + font-size: 16px; + line-height: 24px; + } + .edit { + display: flex; + align-items: center; + gap: 4px; + .icon { + width: 16px; + height: 16px; + path { + fill: #3c8ce7; + } + } + padding: 0; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + background: linear-gradient(135deg, #3c8ce7 0%, #00eaff 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; } } + > .feed { + /* display: flex; + flex-direction: column-reverse; */ + } `; diff --git a/src/routes/chat/DMChat/index.js b/src/routes/chat/DMChat/index.js index 1bfadd33..6009a093 100644 --- a/src/routes/chat/DMChat/index.js +++ b/src/routes/chat/DMChat/index.js @@ -4,11 +4,10 @@ import { useDebounce } from "rooks"; import Tooltip from "../../../common/component/Tooltip"; import alertIcon from "../../../assets/icons/alert.svg?url"; import pinIcon from "../../../assets/icons/pin.svg?url"; -import searchIcon from "../../../assets/icons/search.svg?url"; +// import searchIcon from "../../../assets/icons/search.svg?url"; import boardosIcon from "../../../assets/icons/app.boardos.svg?url"; import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url"; import useChatScroll from "../../../common/hook/useChatScroll"; -import Send from "../../../common/component/Send"; import { useReadMessageMutation } from "../../../app/services/message"; import Contact from "../../../common/component/Contact"; import Layout from "../Layout"; @@ -19,17 +18,23 @@ export default function DMChat({ uid = "", dropFiles = [] }) { const updateReadDebounced = useDebounce(updateReadIndex, 300); console.log("dm files", dropFiles); // const [mids, setMids] = useState([]); - const { msgIds, currUser, messageData, footprint, loginUid } = useSelector( - (store) => { - return { - loginUid: store.authData.uid, - footprint: store.footprint, - currUser: store.contacts.byId[uid], - msgIds: store.userMessage.byId[uid] || [], - messageData: store.message, - }; - } - ); + const { + msgIds, + currUser, + messageData, + footprint, + loginUid, + selects, + } = useSelector((store) => { + return { + selects: store.ui.selectMessages[`user_${uid}`], + loginUid: store.authData.uid, + footprint: store.footprint, + currUser: store.contacts.byId[uid], + msgIds: store.userMessage.byId[uid] || [], + messageData: store.message, + }; + }); const ref = useChatScroll(msgIds); if (!currUser) return null; @@ -74,32 +79,25 @@ export default function DMChat({ uid = "", dropFiles = [] }) { } > - -
    - {[...msgIds] - .sort((a, b) => { - return Number(a) - Number(b); - }) - .map((mid, idx) => { - const curr = messageData[mid]; - const prev = idx == 0 ? null : messageData[msgIds[idx - 1]]; - const read = curr?.from_uid == loginUid || mid <= readIndex; - return renderMessageFragment({ - updateReadIndex: updateReadDebounced, - read, - prev, - curr, - contextId: uid, - context: "user", - }); - })} -
    - + + {[...msgIds] + .sort((a, b) => { + return Number(a) - Number(b); + }) + .map((mid, idx) => { + const curr = messageData[mid]; + const prev = idx == 0 ? null : messageData[msgIds[idx - 1]]; + const read = curr?.from_uid == loginUid || mid <= readIndex; + return renderMessageFragment({ + selectMode: !!selects, + updateReadIndex: updateReadDebounced, + read, + prev, + curr, + contextId: uid, + context: "user", + }); + })} ); diff --git a/src/routes/chat/DMChat/styled.js b/src/routes/chat/DMChat/styled.js index 6131a63b..09a31415 100644 --- a/src/routes/chat/DMChat/styled.js +++ b/src/routes/chat/DMChat/styled.js @@ -28,20 +28,9 @@ export const StyledHeader = styled.header` } `; export const StyledDMChat = styled.article` - position: relative; - display: flex; - flex-direction: column; width: 100%; - padding: 0; - padding-bottom: 16px; - height: calc(100vh - 56px - 22px); - > .chat { - padding: 18px 16px; - height: 100%; - height: -webkit-fill-available; - overflow: auto; - } - > .send { - margin: 0 16px; - } + padding: 18px 16px; + height: 100%; + height: -webkit-fill-available; + overflow: auto; `; diff --git a/src/routes/chat/Layout/Operations.js b/src/routes/chat/Layout/Operations.js new file mode 100644 index 00000000..bac43b8f --- /dev/null +++ b/src/routes/chat/Layout/Operations.js @@ -0,0 +1,68 @@ +import { useState } from "react"; +import styled from "styled-components"; +import { useDispatch, useSelector } from "react-redux"; +import { useKey } from "rooks"; +import { updateSelectMessages } from "../../../app/slices/ui"; +import IconForward from "../../../assets/icons/forward.svg"; +import IconBookmark from "../../../assets/icons/bookmark.svg"; +import IconDelete from "../../../assets/icons/delete.svg"; +import IconClose from "../../../assets/icons/close.circle.svg"; +import ForwardModal from "../../../common/component/ForwardModal"; +const Styled = styled.div` + position: relative; + padding: 16px; + /* padding-bottom: 0; */ + display: flex; + gap: 32px; + align-items: center; + justify-content: center; + box-shadow: 0px -1px 0px rgba(0, 0, 0, 0.05); + .opt { + padding: 8px; + background: #f2f4f7; + border-radius: var(--br); + } + .close { + cursor: pointer; + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + } +`; +export default function Operations({ context, id }) { + const mids = useSelector( + (store) => store.ui.selectMessages[`${context}_${id}`] + ); + const [forwardModalVisible, setForwardModalVisible] = useState(false); + const dispatch = useDispatch(); + const handleClose = () => { + dispatch(updateSelectMessages({ context, id, operation: "reset" })); + }; + const toggleForwardModal = () => { + setForwardModalVisible((prev) => !prev); + }; + useKey("Escape", (evt) => { + console.log("Escape keypress", evt); + dispatch(updateSelectMessages({ context, id, operation: "reset" })); + }); + return ( + <> + + + + + + + {forwardModalVisible && ( + + )} + + ); +} diff --git a/src/routes/chat/Layout.js b/src/routes/chat/Layout/index.js similarity index 50% rename from src/routes/chat/Layout.js rename to src/routes/chat/Layout/index.js index 4196b16f..a439797b 100644 --- a/src/routes/chat/Layout.js +++ b/src/routes/chat/Layout/index.js @@ -1,117 +1,20 @@ import { useState, useRef, useEffect } from "react"; import { useDrop } from "react-dnd"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { NativeTypes } from "react-dnd-html5-backend"; -import styled from "styled-components"; -import { updateUploadFiles } from "../../app/slices/ui"; -import ImagePreviewModal from "../../common/component/ImagePreviewModal"; -const StyledWrapper = styled.article` - position: relative; - width: 100%; - background: #fff; - border-top-right-radius: 16px; - border-bottom-right-radius: 16px; - > .head { - box-sizing: content-box; - height: 56px; - padding: 0 20px; - /* box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1); */ - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - } - > .main { - height: calc(100vh - 56px - 22px); - width: 100%; - display: flex; - align-items: flex-start; - justify-content: space-between; - position: relative; - .members { - box-shadow: inset 0px 10px 2px -10px rgba(0, 0, 0, 0.1); - /* margin-top: 1px; */ - /* border-top: 1px solid transparent; */ - } - > .aside { - padding: 12px; - position: absolute; - right: 0; - top: -56px; - transform: translateX(100%); - display: flex; - flex-direction: column; - .divider { - border: none; - background-color: #d4d4d4; - width: 16px; - height: 1px; - margin: 18px auto; - } - .tools, - .apps { - display: flex; - flex-direction: column; - } - .tools { - gap: 24px; - .tool { - cursor: pointer; - } - } - .apps { - gap: 15px; - } - } - } - .drag_tip { - display: flex; - justify-content: center; - align-items: center; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - visibility: hidden; - /* pointer-events: none; */ - &.visible { - visibility: visible; - } - .box { - padding: 16px; - filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25)); - border-radius: 8px; - background: #52edff; - .inner { - padding: 16px; - padding-top: 64px; - border: 2px dashed #a5f3fc; - border-radius: 6px; - display: flex; - flex-direction: column; - align-items: center; - color: #fff; - .head { - font-weight: 600; - font-size: 20px; - line-height: 30px; - } - .intro { - font-weight: normal; - font-size: 14px; - line-height: 20px; - } - } - } - } -`; +import { updateUploadFiles } from "../../../app/slices/ui"; +import ImagePreviewModal from "../../../common/component/ImagePreviewModal"; +import Send from "../../../common/component/Send"; +import Styled from "./styled"; +import Operations from "./Operations"; export default function Layout({ children, header, aside = null, contacts = null, - // dropFiles = [], + dropFiles = [], context = "channel", to = null, }) { @@ -119,6 +22,9 @@ export default function Layout({ const messagesContainer = useRef(null); const [previewImage, setPreviewImage] = useState(null); + const selects = useSelector( + (store) => store.ui.selectMessages[`${context}_${to}`] + ); const [{ isActive }, drop] = useDrop( () => ({ accept: [NativeTypes.FILE], @@ -139,11 +45,16 @@ export default function Layout({ }), [context, to] ); - // useEffect(() => { - // if (dropFiles?.length) { - // setFiles((prevs) => [...prevs, ...dropFiles]); - // } - // }, [dropFiles]); + useEffect(() => { + if (dropFiles?.length) { + const filesData = dropFiles.map((file) => { + const { size, type, name } = file; + const url = URL.createObjectURL(file); + return { size, type, name, url }; + }); + dispatch(updateUploadFiles({ context, id: to, data: filesData })); + } + }, [dropFiles]); const closePreviewModal = () => { setPreviewImage(null); @@ -177,10 +88,16 @@ export default function Layout({ {previewImage && ( )} - + {header}
    - {children} +
    + {children} +
    + + {selects && } +
    +
    {contacts &&
    {contacts}
    } {aside &&
    {aside}
    }
    @@ -202,7 +119,7 @@ export default function Layout({
    - + ); } diff --git a/src/routes/chat/Layout/styled.js b/src/routes/chat/Layout/styled.js new file mode 100644 index 00000000..65ad82e8 --- /dev/null +++ b/src/routes/chat/Layout/styled.js @@ -0,0 +1,119 @@ +import styled from "styled-components"; +const Styled = styled.article` + position: relative; + width: 100%; + background: #fff; + border-top-right-radius: 16px; + border-bottom-right-radius: 16px; + > .head { + box-sizing: content-box; + height: 56px; + padding: 0 20px; + /* box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1); */ + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + } + > .main { + height: calc(100vh - 56px - 22px); + width: 100%; + display: flex; + align-items: flex-start; + justify-content: space-between; + position: relative; + > .chat { + width: 100%; + background-color: #fff; + display: flex; + flex-direction: column; + padding: 0; + /* padding-bottom: 16px; */ + height: calc(100vh - 56px - 22px); + > .send { + padding: 0 16px 16px 16px; + &.selecting { + padding: 0; + > .send { + display: none; + } + } + } + } + .members { + box-shadow: inset 0px 10px 2px -10px rgba(0, 0, 0, 0.1); + /* margin-top: 1px; */ + /* border-top: 1px solid transparent; */ + } + > .aside { + padding: 12px; + position: absolute; + right: 0; + top: -56px; + transform: translateX(100%); + display: flex; + flex-direction: column; + .divider { + border: none; + background-color: #d4d4d4; + width: 16px; + height: 1px; + margin: 18px auto; + } + .tools, + .apps { + display: flex; + flex-direction: column; + } + .tools { + gap: 24px; + .tool { + cursor: pointer; + } + } + .apps { + gap: 15px; + } + } + } + .drag_tip { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + visibility: hidden; + /* pointer-events: none; */ + &.visible { + visibility: visible; + } + .box { + padding: 16px; + filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25)); + border-radius: 8px; + background: #52edff; + .inner { + padding: 16px; + padding-top: 64px; + border: 2px dashed #a5f3fc; + border-radius: 6px; + display: flex; + flex-direction: column; + align-items: center; + color: #fff; + .head { + font-weight: 600; + font-size: 20px; + line-height: 30px; + } + .intro { + font-weight: normal; + font-size: 14px; + line-height: 20px; + } + } + } + } +`; +export default Styled; diff --git a/src/routes/chat/utils.js b/src/routes/chat/utils.js index 4df3badb..a26cf0e2 100644 --- a/src/routes/chat/utils.js +++ b/src/routes/chat/utils.js @@ -1,9 +1,13 @@ import React from "react"; import dayjs from "dayjs"; +import styled from "styled-components"; +import { useDispatch, useSelector } from "react-redux"; import { isImage } from "../../common/utils"; import { ContentTypes } from "../../app/config"; +import Checkbox from "../../common/component/styled/Checkbox"; import Divider from "../../common/component/Divider"; import Message from "../../common/component/Message"; +import { updateSelectMessages } from "../../app/slices/ui"; // function debounce(callback, wait = 2000, immediate = false) { // let timeout = null; // return function () { @@ -86,7 +90,66 @@ export const renderPreviewMessage = (message = null) => { } return res; }; +const StyledWrapper = styled.div` + position: relative; + width: 100%; + display: flex; + align-items: flex-start; + gap: 8px; + > .overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + cursor: pointer; + } + > .check { + display: none; + margin-top: 18px; + margin-left: 8px; + } + > .message { + flex: 1; + } + &.select { + /* cursor: pointer; */ + &:hover { + border-radius: var(--br); + background: #f5f6f7; + } + > .check { + display: block; + } + } +`; +const MessageWrapper = ({ selectMode = false, context, id, mid, children }) => { + const dispatch = useDispatch(); + + const selects = useSelector( + (store) => store.ui.selectMessages[`${context}_${id}`] + ); + const selected = !!(selects && selects.find((s) => s == mid)); + const toggleSelect = () => { + const operation = selected ? "remove" : "add"; + dispatch(updateSelectMessages({ context, id, operation, data: mid })); + }; + return ( + + + {/* {React.cloneElement(children, { readOnly: selected })} */} + {children} + {selectMode && ( +
    + )} +
    + ); +}; export const renderMessageFragment = ({ + selectMode = false, isFirst = false, read = true, updateReadIndex, @@ -111,15 +174,23 @@ export const renderMessageFragment = ({ return ( {divider && } - + selectMode={selectMode} + > + + ); };