feat: select multiple messages and forward
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.2958 16.2945C13.9055 16.6853 13.906 17.3184 14.2968 17.7087C14.6876 18.0989 15.3207 18.0985 15.711 17.7077L20.7076 12.7043C21.0976 12.3137 21.0974 11.681 20.7072 11.2906L15.7106 6.29297C15.3201 5.9024 14.6869 5.90234 14.2964 6.29282C13.9058 6.6833 13.9057 7.31647 14.2962 7.70703L17.589 11H11C6.66509 11 3.13546 14.4478 3.00381 18.7508L3 19C3 19.5523 3.44772 20 4 20C4.55228 20 5 19.5523 5 19C5 15.7616 7.56557 13.1224 10.7751 13.0041L11 13H17.586L14.2958 16.2945Z" fill="#667085"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 599 B |
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }) {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="reply">
|
||||
<Reply mid={mid} interactive={false} />
|
||||
<div className="msgs">
|
||||
{mids.map((mid) => (
|
||||
<Reply key={mid} mid={mid} interactive={false} />
|
||||
))}
|
||||
</div>
|
||||
<Input
|
||||
className="input"
|
||||
|
||||
@@ -62,7 +62,7 @@ const StyledWrapper = styled.div`
|
||||
align-items: flex-start;
|
||||
/* height: 100%; */
|
||||
/* justify-content: space-between; */
|
||||
padding: 16px 32px 32px 32px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
@@ -87,9 +87,17 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
.reply {
|
||||
.msgs {
|
||||
border-radius: var(--br);
|
||||
padding: 8px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
background-color: #f4f4f5;
|
||||
width: 280px;
|
||||
margin-bottom: 4px;
|
||||
> .reply {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
.input {
|
||||
margin-bottom: 32px;
|
||||
|
||||
@@ -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 (
|
||||
<StyledCmds
|
||||
ref={cmdsRef}
|
||||
@@ -127,13 +139,20 @@ export default function Commands({
|
||||
content={
|
||||
<StyledMenu className="menu">
|
||||
{/* <li className="item">Edit Message</li> */}
|
||||
{/* <li className="item underline">Pin Message</li> */}
|
||||
{context == "channel" && (
|
||||
<li className="item underline" onClick={togglePinModal}>
|
||||
Pin Message
|
||||
</li>
|
||||
)}
|
||||
<li className="item" onClick={toggleForwardModal}>
|
||||
Forward
|
||||
</li>
|
||||
<li className="item" onClick={handleReply.bind(null, true)}>
|
||||
Reply
|
||||
</li>
|
||||
<li className="item" onClick={handleSelect.bind(null, mid)}>
|
||||
Select
|
||||
</li>
|
||||
{currUid == from_uid && (
|
||||
<li className="item danger" onClick={toggleDeleteModal}>
|
||||
Delete Message
|
||||
@@ -153,7 +172,14 @@ export default function Commands({
|
||||
<DeleteMessageConfirm closeModal={toggleDeleteModal} mid={mid} />
|
||||
)}
|
||||
{forwardModalVisible && (
|
||||
<ForwardModal mid={mid} closeModal={toggleForwardModal} />
|
||||
<ForwardModal mids={[mid]} closeModal={toggleForwardModal} />
|
||||
)}
|
||||
{pinModalVisible && (
|
||||
<PinMessageModal
|
||||
mid={mid}
|
||||
gid={contextId}
|
||||
closeModal={togglePinModal}
|
||||
/>
|
||||
)}
|
||||
</StyledCmds>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Modal>
|
||||
<StyledPinModal
|
||||
// className="animate__animated animate__fadeInDown animate__faster"
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal} className="cancel">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isLoading} onClick={handlePin} className="main">
|
||||
{isLoading ? "Pining" : `Pin It`}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
title="Pin It"
|
||||
description="You sure you want to pin this message to #gerenal?"
|
||||
>
|
||||
<PreviewMessage mid={mid} />
|
||||
</StyledPinModal>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -38,6 +38,7 @@ const Styled = styled.div`
|
||||
}
|
||||
}
|
||||
.content {
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
@@ -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 (
|
||||
<StyledWrapper data-msg-mid={mid} ref={inviewRef} className={`message`}>
|
||||
<StyledWrapper
|
||||
data-msg-mid={mid}
|
||||
ref={inviewRef}
|
||||
className={`message ${readOnly ? "readonly" : ""}`}
|
||||
>
|
||||
<Tippy
|
||||
disabled={readOnly}
|
||||
duration={0}
|
||||
interactive
|
||||
placement="left"
|
||||
@@ -94,7 +100,7 @@ function Message({
|
||||
<span className="name">{currUser.name}</span>
|
||||
<Tooltip
|
||||
delay={200}
|
||||
disabled={!timePrefix}
|
||||
disabled={!timePrefix || readOnly}
|
||||
placement="top"
|
||||
tip={dayjsTime.format("YYYY-MM-DD h:mm:ss A")}
|
||||
>
|
||||
@@ -130,8 +136,9 @@ function Message({
|
||||
{reactions && <Reaction mid={mid} reactions={reactions} />}
|
||||
</div>
|
||||
</div>
|
||||
{!edit && (
|
||||
{!edit && !readOnly && (
|
||||
<Commands
|
||||
context={context}
|
||||
contextId={contextId}
|
||||
mid={mid}
|
||||
from_uid={fromUid}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Linkit from "react-linkify";
|
||||
import styled from "styled-components";
|
||||
import dayjs from "dayjs";
|
||||
import StyledMsg from "./styled";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import Mention from "./Mention";
|
||||
import useNormalizeMessage from "../../hook/useNormalizeMessage";
|
||||
import MrakdownRender from "../MrakdownRender";
|
||||
import FileMessage from "../FileMessage";
|
||||
import URLPreview from "./URLPreview";
|
||||
import Avatar from "../Avatar";
|
||||
import IconForward from "../../../assets/icons/forward.svg";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
const renderContent = ({
|
||||
context,
|
||||
@@ -111,6 +115,29 @@ const renderContent = ({
|
||||
}
|
||||
return ctn;
|
||||
};
|
||||
const StyledForward = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--br);
|
||||
background-color: #f4f4f5;
|
||||
padding: 8px;
|
||||
> .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,
|
||||
});
|
||||
})
|
||||
<StyledForward>
|
||||
<h4 className="tip">
|
||||
<IconForward className="icon" />
|
||||
Forwarded
|
||||
</h4>
|
||||
<div className="list">
|
||||
{messages.map((msg, idx) => {
|
||||
const {
|
||||
user = {},
|
||||
created_at,
|
||||
download,
|
||||
content,
|
||||
content_type,
|
||||
properties,
|
||||
thumbnail,
|
||||
} = msg;
|
||||
return (
|
||||
<StyledMsg key={idx}>
|
||||
{user && (
|
||||
<div className="avatar">
|
||||
<Avatar url={user.avatar} name={user.name} />
|
||||
</div>
|
||||
)}
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{user?.name}</span>
|
||||
<i className="time">
|
||||
{dayjs(created_at).format("YYYY-MM-DD h:mm:ss A")}
|
||||
</i>
|
||||
</div>
|
||||
<div className="down">
|
||||
{renderContent({
|
||||
download,
|
||||
context,
|
||||
to,
|
||||
from_uid,
|
||||
content,
|
||||
content_type,
|
||||
properties,
|
||||
thumbnail,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</StyledMsg>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</StyledForward>
|
||||
);
|
||||
}
|
||||
}, [messages, context, to, from_uid]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<StyledSend className={`send ${replying_mid ? "reply" : ""} ${context}`}>
|
||||
{replying_mid && <Replying mid={replying_mid} id={id} />}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
>
|
||||
<StyledChannelChat>
|
||||
<div className="wrapper">
|
||||
<div className="chat" ref={ref}>
|
||||
<div className="info">
|
||||
<h2 className="title">Welcome to #{name} !</h2>
|
||||
<p className="desc">
|
||||
This is the start of the #{name} channel.{" "}
|
||||
</p>
|
||||
<NavLink
|
||||
to={`/setting/channel/${cid}?f=${pathname}`}
|
||||
className="edit"
|
||||
>
|
||||
<EditIcon className="icon" />
|
||||
Edit Channel
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="feed">
|
||||
{[...msgIds]
|
||||
.sort((a, b) => {
|
||||
return Number(a) - Number(b);
|
||||
})
|
||||
.map((mid, idx) => {
|
||||
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",
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Send
|
||||
key={cid}
|
||||
id={cid}
|
||||
context="channel"
|
||||
name={name}
|
||||
members={memberIds}
|
||||
/>
|
||||
<StyledChannelChat ref={ref}>
|
||||
<div className="info">
|
||||
<h2 className="title">Welcome to #{name} !</h2>
|
||||
<p className="desc">This is the start of the #{name} channel. </p>
|
||||
<NavLink
|
||||
to={`/setting/channel/${cid}?f=${pathname}`}
|
||||
className="edit"
|
||||
>
|
||||
<EditIcon className="icon" />
|
||||
Edit Channel
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="feed">
|
||||
{[...msgIds]
|
||||
.sort((a, b) => {
|
||||
return Number(a) - Number(b);
|
||||
})
|
||||
.map((mid, idx) => {
|
||||
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",
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
</StyledChannelChat>
|
||||
{/* {unreads != 0 && (
|
||||
|
||||
@@ -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; */
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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 = [] }) {
|
||||
</StyledHeader>
|
||||
}
|
||||
>
|
||||
<StyledDMChat>
|
||||
<div className="chat" ref={ref}>
|
||||
{[...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",
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
<Send
|
||||
key={currUser?.uid}
|
||||
context="user"
|
||||
name={currUser?.name}
|
||||
id={currUser?.uid}
|
||||
/>
|
||||
<StyledDMChat ref={ref}>
|
||||
{[...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",
|
||||
});
|
||||
})}
|
||||
</StyledDMChat>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Styled>
|
||||
<button className="opt" onClick={toggleForwardModal}>
|
||||
<IconForward />
|
||||
</button>
|
||||
<button className="opt">
|
||||
<IconBookmark />
|
||||
</button>
|
||||
<button className="opt">
|
||||
<IconDelete />
|
||||
</button>
|
||||
<IconClose className="close" onClick={handleClose} />
|
||||
</Styled>
|
||||
{forwardModalVisible && (
|
||||
<ForwardModal mids={mids} closeModal={toggleForwardModal} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 && (
|
||||
<ImagePreviewModal data={previewImage} closeModal={closePreviewModal} />
|
||||
)}
|
||||
<StyledWrapper ref={drop}>
|
||||
<Styled ref={drop}>
|
||||
{header}
|
||||
<main className="main" ref={messagesContainer}>
|
||||
{children}
|
||||
<div className="chat">
|
||||
{children}
|
||||
<div className={`send ${selects ? "selecting" : ""}`}>
|
||||
<Send key={to} id={to} context={context} />
|
||||
{selects && <Operations context={context} id={to} />}
|
||||
</div>
|
||||
</div>
|
||||
{contacts && <div className="members">{contacts}</div>}
|
||||
{aside && <div className="aside">{aside}</div>}
|
||||
</main>
|
||||
@@ -202,7 +119,7 @@ export default function Layout({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Styled>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<StyledWrapper className={selectMode ? "select" : ""}>
|
||||
<Checkbox className="check" checked={selected} />
|
||||
{/* {React.cloneElement(children, { readOnly: selected })} */}
|
||||
{children}
|
||||
{selectMode && (
|
||||
<div
|
||||
className="overlay"
|
||||
onClick={selectMode ? toggleSelect : null}
|
||||
></div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export const renderMessageFragment = ({
|
||||
selectMode = false,
|
||||
isFirst = false,
|
||||
read = true,
|
||||
updateReadIndex,
|
||||
@@ -111,15 +174,23 @@ export const renderMessageFragment = ({
|
||||
return (
|
||||
<React.Fragment key={mid}>
|
||||
{divider && <Divider content={divider}></Divider>}
|
||||
<Message
|
||||
isFirst={isFirst}
|
||||
updateReadIndex={updateReadIndex}
|
||||
read={read}
|
||||
<MessageWrapper
|
||||
context={context}
|
||||
id={contextId}
|
||||
mid={mid}
|
||||
key={mid}
|
||||
contextId={contextId}
|
||||
/>
|
||||
selectMode={selectMode}
|
||||
>
|
||||
<Message
|
||||
readOnly={selectMode}
|
||||
isFirst={isFirst}
|
||||
updateReadIndex={updateReadIndex}
|
||||
read={read}
|
||||
context={context}
|
||||
mid={mid}
|
||||
key={mid}
|
||||
contextId={contextId}
|
||||
/>
|
||||
</MessageWrapper>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user