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)}`,
|
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({
|
replyMessage: builder.mutation({
|
||||||
query: ({ reply_mid, content, type = "text" }) => ({
|
query: ({ reply_mid, content, type = "text" }) => ({
|
||||||
headers: {
|
headers: {
|
||||||
@@ -114,6 +121,7 @@ export const messageApi = createApi({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
usePinMessageMutation,
|
||||||
useLazyGetArchiveMessageQuery,
|
useLazyGetArchiveMessageQuery,
|
||||||
useGetArchiveMessageQuery,
|
useGetArchiveMessageQuery,
|
||||||
useLazyGetOGInfoQuery,
|
useLazyGetOGInfoQuery,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const initialState = {
|
|||||||
menuExpand: false,
|
menuExpand: false,
|
||||||
fileListView: Views.grid,
|
fileListView: Views.grid,
|
||||||
uploadFiles: {},
|
uploadFiles: {},
|
||||||
|
selectMessages: {},
|
||||||
};
|
};
|
||||||
const uiSlice = createSlice({
|
const uiSlice = createSlice({
|
||||||
name: "ui",
|
name: "ui",
|
||||||
@@ -40,6 +41,7 @@ const uiSlice = createSlice({
|
|||||||
state.userGuide[key] = obj[key];
|
state.userGuide[key] = obj[key];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateUploadFiles(state, action) {
|
updateUploadFiles(state, action) {
|
||||||
const {
|
const {
|
||||||
context = "channel",
|
context = "channel",
|
||||||
@@ -105,6 +107,33 @@ const uiSlice = createSlice({
|
|||||||
break;
|
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 {
|
export const {
|
||||||
@@ -115,6 +144,7 @@ export const {
|
|||||||
toggleMenuExpand,
|
toggleMenuExpand,
|
||||||
updateFileListView,
|
updateFileListView,
|
||||||
updateUploadFiles,
|
updateUploadFiles,
|
||||||
|
updateSelectMessages,
|
||||||
updateUserGuide,
|
updateUserGuide,
|
||||||
} = uiSlice.actions;
|
} = uiSlice.actions;
|
||||||
export default uiSlice.reducer;
|
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({
|
const tmp = getInitialsAvatar({
|
||||||
initials: getInitials(name),
|
initials: getInitials(name),
|
||||||
background: type == "channel" ? "#4ea758" : undefined,
|
background: type == "channel" ? "#EAECF0" : undefined,
|
||||||
|
foreground: type == "channel" ? "#475467" : undefined,
|
||||||
});
|
});
|
||||||
setSrc(tmp);
|
setSrc(tmp);
|
||||||
};
|
};
|
||||||
@@ -22,7 +23,8 @@ export default function Avatar({
|
|||||||
if (!url) {
|
if (!url) {
|
||||||
const tmp = getInitialsAvatar({
|
const tmp = getInitialsAvatar({
|
||||||
initials: getInitials(name),
|
initials: getInitials(name),
|
||||||
background: type == "channel" ? "#4ea758" : undefined,
|
background: type == "channel" ? "#EAECF0" : undefined,
|
||||||
|
foreground: type == "channel" ? "#475467" : undefined,
|
||||||
});
|
});
|
||||||
setSrc(tmp);
|
setSrc(tmp);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
.status {
|
.status {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: -2px;
|
||||||
right: -2px;
|
right: -4px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
box-sizing: content-box;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
outline: 2px solid #fff;
|
border: 2px solid #fff;
|
||||||
&.online {
|
&.online {
|
||||||
background-color: #22c55e;
|
background-color: #22c55e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import CloseIcon from "../../../assets/icons/close.circle.svg";
|
|||||||
import StyledCheckbox from "../../component/styled/Checkbox";
|
import StyledCheckbox from "../../component/styled/Checkbox";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export default function ForwardModal({ mid, closeModal }) {
|
export default function ForwardModal({ mids, closeModal }) {
|
||||||
const [appendText, setAppendText] = useState("");
|
const [appendText, setAppendText] = useState("");
|
||||||
const { sendMessages } = useSendMessage();
|
const { sendMessages } = useSendMessage();
|
||||||
const { forwardMessage, forwarding } = useForwardMessage();
|
const { forwardMessage, forwarding } = useForwardMessage();
|
||||||
@@ -45,7 +45,7 @@ export default function ForwardModal({ mid, closeModal }) {
|
|||||||
};
|
};
|
||||||
const handleForward = async () => {
|
const handleForward = async () => {
|
||||||
await forwardMessage({
|
await forwardMessage({
|
||||||
mids: [mid],
|
mids: mids,
|
||||||
users: selectedMembers,
|
users: selectedMembers,
|
||||||
channels: selectedChannels,
|
channels: selectedChannels,
|
||||||
});
|
});
|
||||||
@@ -170,8 +170,10 @@ export default function ForwardModal({ mid, closeModal }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="reply">
|
<div className="msgs">
|
||||||
<Reply mid={mid} interactive={false} />
|
{mids.map((mid) => (
|
||||||
|
<Reply key={mid} mid={mid} interactive={false} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
className="input"
|
className="input"
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const StyledWrapper = styled.div`
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
/* height: 100%; */
|
/* height: 100%; */
|
||||||
/* justify-content: space-between; */
|
/* justify-content: space-between; */
|
||||||
padding: 16px 32px 32px 32px;
|
padding: 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
.title {
|
.title {
|
||||||
font-weight: 600;
|
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;
|
width: 280px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
> .reply {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.input {
|
.input {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import styled from "styled-components";
|
|||||||
import Tippy from "@tippyjs/react";
|
import Tippy from "@tippyjs/react";
|
||||||
import { hideAll } from "tippy.js";
|
import { hideAll } from "tippy.js";
|
||||||
// import toast from "react-hot-toast";
|
// import toast from "react-hot-toast";
|
||||||
|
import { updateSelectMessages } from "../../../app/slices/ui";
|
||||||
import { addReplyingMessage } from "../../../app/slices/message";
|
import { addReplyingMessage } from "../../../app/slices/message";
|
||||||
import StyledMenu from "../styled/Menu";
|
import StyledMenu from "../styled/Menu";
|
||||||
import Tooltip from "../../component/Tooltip";
|
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 bookmarkIcon from "../../../assets/icons/bookmark.svg?url";
|
||||||
import moreIcon from "../../../assets/icons/more.svg?url";
|
import moreIcon from "../../../assets/icons/more.svg?url";
|
||||||
import ForwardModal from "../ForwardModal";
|
import ForwardModal from "../ForwardModal";
|
||||||
|
import PinMessageModal from "./PinMessageModal";
|
||||||
const StyledCmds = styled.ul`
|
const StyledCmds = styled.ul`
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -50,12 +52,14 @@ const StyledCmds = styled.ul`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export default function Commands({
|
export default function Commands({
|
||||||
|
context = "user",
|
||||||
contextId = 0,
|
contextId = 0,
|
||||||
mid = 0,
|
mid = 0,
|
||||||
from_uid = 0,
|
from_uid = 0,
|
||||||
toggleEditMessage,
|
toggleEditMessage,
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const [pinModalVisible, setPinModalVisible] = useState(false);
|
||||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||||
const [forwardModalVisible, setForwardModalVisible] = useState(false);
|
const [forwardModalVisible, setForwardModalVisible] = useState(false);
|
||||||
const [tippyVisible, setTippyVisible] = useState(false);
|
const [tippyVisible, setTippyVisible] = useState(false);
|
||||||
@@ -77,9 +81,17 @@ export default function Commands({
|
|||||||
hideAll();
|
hideAll();
|
||||||
setDeleteModalVisible((prev) => !prev);
|
setDeleteModalVisible((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
const togglePinModal = () => {
|
||||||
|
hideAll();
|
||||||
|
setPinModalVisible((prev) => !prev);
|
||||||
|
};
|
||||||
const handleTippyVisible = (visible = true) => {
|
const handleTippyVisible = (visible = true) => {
|
||||||
setTippyVisible(visible);
|
setTippyVisible(visible);
|
||||||
};
|
};
|
||||||
|
const handleSelect = (mid) => {
|
||||||
|
dispatch(updateSelectMessages({ context, id: contextId, data: mid }));
|
||||||
|
hideAll();
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<StyledCmds
|
<StyledCmds
|
||||||
ref={cmdsRef}
|
ref={cmdsRef}
|
||||||
@@ -127,13 +139,20 @@ export default function Commands({
|
|||||||
content={
|
content={
|
||||||
<StyledMenu className="menu">
|
<StyledMenu className="menu">
|
||||||
{/* <li className="item">Edit Message</li> */}
|
{/* <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}>
|
<li className="item" onClick={toggleForwardModal}>
|
||||||
Forward
|
Forward
|
||||||
</li>
|
</li>
|
||||||
<li className="item" onClick={handleReply.bind(null, true)}>
|
<li className="item" onClick={handleReply.bind(null, true)}>
|
||||||
Reply
|
Reply
|
||||||
</li>
|
</li>
|
||||||
|
<li className="item" onClick={handleSelect.bind(null, mid)}>
|
||||||
|
Select
|
||||||
|
</li>
|
||||||
{currUid == from_uid && (
|
{currUid == from_uid && (
|
||||||
<li className="item danger" onClick={toggleDeleteModal}>
|
<li className="item danger" onClick={toggleDeleteModal}>
|
||||||
Delete Message
|
Delete Message
|
||||||
@@ -153,7 +172,14 @@ export default function Commands({
|
|||||||
<DeleteMessageConfirm closeModal={toggleDeleteModal} mid={mid} />
|
<DeleteMessageConfirm closeModal={toggleDeleteModal} mid={mid} />
|
||||||
)}
|
)}
|
||||||
{forwardModalVisible && (
|
{forwardModalVisible && (
|
||||||
<ForwardModal mid={mid} closeModal={toggleForwardModal} />
|
<ForwardModal mids={[mid]} closeModal={toggleForwardModal} />
|
||||||
|
)}
|
||||||
|
{pinModalVisible && (
|
||||||
|
<PinMessageModal
|
||||||
|
mid={mid}
|
||||||
|
gid={contextId}
|
||||||
|
closeModal={togglePinModal}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledCmds>
|
</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 {
|
.content {
|
||||||
|
overflow: hidden;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import Tooltip from "../Tooltip";
|
|||||||
dayjs.extend(isToday);
|
dayjs.extend(isToday);
|
||||||
dayjs.extend(isYesterday);
|
dayjs.extend(isYesterday);
|
||||||
function Message({
|
function Message({
|
||||||
|
readOnly = false,
|
||||||
contextId = 0,
|
contextId = 0,
|
||||||
mid = "",
|
mid = "",
|
||||||
context = "user",
|
context = "user",
|
||||||
@@ -77,8 +78,13 @@ function Message({
|
|||||||
console.log("render message");
|
console.log("render message");
|
||||||
// return null;
|
// return null;
|
||||||
return (
|
return (
|
||||||
<StyledWrapper data-msg-mid={mid} ref={inviewRef} className={`message`}>
|
<StyledWrapper
|
||||||
|
data-msg-mid={mid}
|
||||||
|
ref={inviewRef}
|
||||||
|
className={`message ${readOnly ? "readonly" : ""}`}
|
||||||
|
>
|
||||||
<Tippy
|
<Tippy
|
||||||
|
disabled={readOnly}
|
||||||
duration={0}
|
duration={0}
|
||||||
interactive
|
interactive
|
||||||
placement="left"
|
placement="left"
|
||||||
@@ -94,7 +100,7 @@ function Message({
|
|||||||
<span className="name">{currUser.name}</span>
|
<span className="name">{currUser.name}</span>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
delay={200}
|
delay={200}
|
||||||
disabled={!timePrefix}
|
disabled={!timePrefix || readOnly}
|
||||||
placement="top"
|
placement="top"
|
||||||
tip={dayjsTime.format("YYYY-MM-DD h:mm:ss A")}
|
tip={dayjsTime.format("YYYY-MM-DD h:mm:ss A")}
|
||||||
>
|
>
|
||||||
@@ -130,8 +136,9 @@ function Message({
|
|||||||
{reactions && <Reaction mid={mid} reactions={reactions} />}
|
{reactions && <Reaction mid={mid} reactions={reactions} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!edit && (
|
{!edit && !readOnly && (
|
||||||
<Commands
|
<Commands
|
||||||
|
context={context}
|
||||||
contextId={contextId}
|
contextId={contextId}
|
||||||
mid={mid}
|
mid={mid}
|
||||||
from_uid={fromUid}
|
from_uid={fromUid}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import Linkit from "react-linkify";
|
import Linkit from "react-linkify";
|
||||||
|
import styled from "styled-components";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import StyledMsg from "./styled";
|
||||||
import { ContentTypes } from "../../../app/config";
|
import { ContentTypes } from "../../../app/config";
|
||||||
import Mention from "./Mention";
|
import Mention from "./Mention";
|
||||||
import useNormalizeMessage from "../../hook/useNormalizeMessage";
|
import useNormalizeMessage from "../../hook/useNormalizeMessage";
|
||||||
import MrakdownRender from "../MrakdownRender";
|
import MrakdownRender from "../MrakdownRender";
|
||||||
import FileMessage from "../FileMessage";
|
import FileMessage from "../FileMessage";
|
||||||
import URLPreview from "./URLPreview";
|
import URLPreview from "./URLPreview";
|
||||||
|
import Avatar from "../Avatar";
|
||||||
|
import IconForward from "../../../assets/icons/forward.svg";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
const renderContent = ({
|
const renderContent = ({
|
||||||
context,
|
context,
|
||||||
@@ -111,6 +115,29 @@ const renderContent = ({
|
|||||||
}
|
}
|
||||||
return ctn;
|
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 ForwardedMessage = ({ context, to, from_uid, id }) => {
|
||||||
const { normalizeMessage, messages } = useNormalizeMessage();
|
const { normalizeMessage, messages } = useNormalizeMessage();
|
||||||
const [forwards, setForwards] = useState(null);
|
const [forwards, setForwards] = useState(null);
|
||||||
@@ -123,25 +150,54 @@ const ForwardedMessage = ({ context, to, from_uid, id }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages) {
|
if (messages) {
|
||||||
setForwards(
|
setForwards(
|
||||||
messages.map((msg) => {
|
<StyledForward>
|
||||||
const {
|
<h4 className="tip">
|
||||||
download,
|
<IconForward className="icon" />
|
||||||
content,
|
Forwarded
|
||||||
content_type,
|
</h4>
|
||||||
properties,
|
<div className="list">
|
||||||
thumbnail,
|
{messages.map((msg, idx) => {
|
||||||
} = msg;
|
const {
|
||||||
return renderContent({
|
user = {},
|
||||||
download,
|
created_at,
|
||||||
context,
|
download,
|
||||||
to,
|
content,
|
||||||
from_uid,
|
content_type,
|
||||||
content,
|
properties,
|
||||||
content_type,
|
thumbnail,
|
||||||
properties,
|
} = msg;
|
||||||
thumbnail,
|
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]);
|
}, [messages, context, to, from_uid]);
|
||||||
|
|||||||
@@ -17,11 +17,16 @@ const StyledMsg = styled.div`
|
|||||||
}
|
}
|
||||||
&:hover,
|
&:hover,
|
||||||
&.preview {
|
&.preview {
|
||||||
|
content-visibility: inherit;
|
||||||
|
contain-intrinsic-size: inherit;
|
||||||
background: #f5f6f7;
|
background: #f5f6f7;
|
||||||
.cmds {
|
.cmds {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.readonly:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -26,10 +26,8 @@ const Modes = {
|
|||||||
markdown: "markdown",
|
markdown: "markdown",
|
||||||
};
|
};
|
||||||
function Send({
|
function Send({
|
||||||
name,
|
|
||||||
context = "channel",
|
|
||||||
members = [],
|
|
||||||
// 发给谁,或者是channel,或者是user
|
// 发给谁,或者是channel,或者是user
|
||||||
|
context = "channel",
|
||||||
id = "",
|
id = "",
|
||||||
}) {
|
}) {
|
||||||
const editor = useMixedEditor(`${context}_${id}`);
|
const editor = useMixedEditor(`${context}_${id}`);
|
||||||
@@ -37,16 +35,25 @@ function Send({
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const addLocalFileMesage = useAddLocalFileMessage({ context, to: id });
|
const addLocalFileMesage = useAddLocalFileMessage({ context, to: id });
|
||||||
// 谁发的
|
// 谁发的
|
||||||
const { from_uid, replying_mid = null, mode, uploadFiles } = useSelector(
|
const {
|
||||||
(store) => {
|
from_uid,
|
||||||
return {
|
replying_mid = null,
|
||||||
mode: store.ui.inputMode,
|
mode,
|
||||||
from_uid: store.authData.uid,
|
uploadFiles,
|
||||||
replying_mid: store.message.replying[id],
|
channelsData,
|
||||||
uploadFiles: store.ui.uploadFiles[`${context}_${id}`],
|
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 });
|
const { sendMessage } = useSendMessage({ context, from: from_uid, to: id });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -121,7 +128,15 @@ function Send({
|
|||||||
const toggleMode = () => {
|
const toggleMode = () => {
|
||||||
dispatch(updateInputMode(mode == Modes.text ? Modes.markdown : Modes.text));
|
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 placeholder = `Send to ${Types[context]}${name} `;
|
||||||
|
const members =
|
||||||
|
context == "channel"
|
||||||
|
? channelsData[id]?.is_public
|
||||||
|
? uids
|
||||||
|
: channelsData[id]?.members
|
||||||
|
: [];
|
||||||
return (
|
return (
|
||||||
<StyledSend className={`send ${replying_mid ? "reply" : ""} ${context}`}>
|
<StyledSend className={`send ${replying_mid ? "reply" : ""} ${context}`}>
|
||||||
{replying_mid && <Replying mid={replying_mid} id={id} />}
|
{replying_mid && <Replying mid={replying_mid} id={id} />}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const StyledMenu = styled.ul`
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1),
|
box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1),
|
||||||
0px 10px 10px rgba(31, 41, 55, 0.04);
|
0px 10px 10px rgba(31, 41, 55, 0.04);
|
||||||
border-radius: var(--br);
|
border-radius: 12px;
|
||||||
.item {
|
.item {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ export default function useNormalizeMessage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && isSuccess) {
|
if (data && isSuccess) {
|
||||||
const msgs = data.messages.map(
|
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 =
|
const transformedContent =
|
||||||
content_type == ContentTypes.file
|
content_type == ContentTypes.file
|
||||||
? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}`
|
? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}`
|
||||||
@@ -24,7 +31,13 @@ export default function useNormalizeMessage() {
|
|||||||
content_type == ContentTypes.file
|
content_type == ContentTypes.file
|
||||||
? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}&download=true`
|
? `${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 {
|
return {
|
||||||
|
user,
|
||||||
content: transformedContent,
|
content: transformedContent,
|
||||||
content_type,
|
content_type,
|
||||||
properties,
|
properties,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useReadMessageMutation } from "../../../app/services/message";
|
|||||||
import useChatScroll from "../../../common/hook/useChatScroll";
|
import useChatScroll from "../../../common/hook/useChatScroll";
|
||||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||||
import Tooltip from "../../../common/component/Tooltip";
|
import Tooltip from "../../../common/component/Tooltip";
|
||||||
import Send from "../../../common/component/Send";
|
|
||||||
import Contact from "../../../common/component/Contact";
|
import Contact from "../../../common/component/Contact";
|
||||||
import Layout from "../Layout";
|
import Layout from "../Layout";
|
||||||
import { renderMessageFragment } from "../utils";
|
import { renderMessageFragment } from "../utils";
|
||||||
@@ -13,7 +12,7 @@ import EditIcon from "../../../assets/icons/edit.svg";
|
|||||||
import alertIcon from "../../../assets/icons/alert.svg?url";
|
import alertIcon from "../../../assets/icons/alert.svg?url";
|
||||||
import peopleIcon from "../../../assets/icons/people.svg?url";
|
import peopleIcon from "../../../assets/icons/people.svg?url";
|
||||||
import pinIcon from "../../../assets/icons/pin.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 headphoneIcon from "../../../assets/icons/headphone.svg?url";
|
||||||
import boardosIcon from "../../../assets/icons/app.boardos.svg?url";
|
import boardosIcon from "../../../assets/icons/app.boardos.svg?url";
|
||||||
import webrowseIcon from "../../../assets/icons/app.webrowse.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 [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
|
||||||
// const dispatch = useDispatch();
|
// const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
|
selects,
|
||||||
msgIds,
|
msgIds,
|
||||||
userIds,
|
userIds,
|
||||||
data,
|
data,
|
||||||
@@ -44,6 +44,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
|||||||
footprint,
|
footprint,
|
||||||
} = useSelector((store) => {
|
} = useSelector((store) => {
|
||||||
return {
|
return {
|
||||||
|
selects: store.ui.selectMessages[`channel_${cid}`],
|
||||||
footprint: store.footprint,
|
footprint: store.footprint,
|
||||||
loginUser: store.contacts.byId[store.authData.uid],
|
loginUser: store.contacts.byId[store.authData.uid],
|
||||||
loginUid: store.authData.uid,
|
loginUid: store.authData.uid,
|
||||||
@@ -150,52 +151,40 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StyledChannelChat>
|
<StyledChannelChat ref={ref}>
|
||||||
<div className="wrapper">
|
<div className="info">
|
||||||
<div className="chat" ref={ref}>
|
<h2 className="title">Welcome to #{name} !</h2>
|
||||||
<div className="info">
|
<p className="desc">This is the start of the #{name} channel. </p>
|
||||||
<h2 className="title">Welcome to #{name} !</h2>
|
<NavLink
|
||||||
<p className="desc">
|
to={`/setting/channel/${cid}?f=${pathname}`}
|
||||||
This is the start of the #{name} channel.{" "}
|
className="edit"
|
||||||
</p>
|
>
|
||||||
<NavLink
|
<EditIcon className="icon" />
|
||||||
to={`/setting/channel/${cid}?f=${pathname}`}
|
Edit Channel
|
||||||
className="edit"
|
</NavLink>
|
||||||
>
|
</div>
|
||||||
<EditIcon className="icon" />
|
<div className="feed">
|
||||||
Edit Channel
|
{[...msgIds]
|
||||||
</NavLink>
|
.sort((a, b) => {
|
||||||
</div>
|
return Number(a) - Number(b);
|
||||||
<div className="feed">
|
})
|
||||||
{[...msgIds]
|
.map((mid, idx) => {
|
||||||
.sort((a, b) => {
|
const curr = messageData[mid];
|
||||||
return Number(a) - Number(b);
|
if (!curr) return null;
|
||||||
})
|
const isFirst = idx == 0;
|
||||||
.map((mid, idx) => {
|
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
|
||||||
const curr = messageData[mid];
|
const read = curr?.from_uid == loginUid || mid <= readIndex;
|
||||||
if (!curr) return null;
|
return renderMessageFragment({
|
||||||
const isFirst = idx == 0;
|
selectMode: !!selects,
|
||||||
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
|
updateReadIndex: updateReadDebounced,
|
||||||
const read = curr?.from_uid == loginUid || mid <= readIndex;
|
read,
|
||||||
return renderMessageFragment({
|
isFirst,
|
||||||
updateReadIndex: updateReadDebounced,
|
prev,
|
||||||
read,
|
curr,
|
||||||
isFirst,
|
contextId: cid,
|
||||||
prev,
|
context: "channel",
|
||||||
curr,
|
});
|
||||||
contextId: cid,
|
})}
|
||||||
context: "channel",
|
|
||||||
});
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Send
|
|
||||||
key={cid}
|
|
||||||
id={cid}
|
|
||||||
context="channel"
|
|
||||||
name={name}
|
|
||||||
members={memberIds}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</StyledChannelChat>
|
</StyledChannelChat>
|
||||||
{/* {unreads != 0 && (
|
{/* {unreads != 0 && (
|
||||||
|
|||||||
@@ -82,68 +82,53 @@ export const StyledContacts = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export const StyledChannelChat = styled.article`
|
export const StyledChannelChat = styled.article`
|
||||||
position: relative;
|
padding: 18px 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* margin-bottom: 120px; */
|
height: 100%;
|
||||||
> .wrapper {
|
height: -webkit-fill-available;
|
||||||
background-color: #fff;
|
overflow: auto;
|
||||||
|
> .info {
|
||||||
|
padding-top: 62px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0;
|
align-items: flex-start;
|
||||||
padding-bottom: 16px;
|
gap: 8px;
|
||||||
height: calc(100vh - 56px - 22px);
|
.title {
|
||||||
.chat {
|
font-weight: bold;
|
||||||
padding: 18px 16px;
|
font-size: 36px;
|
||||||
height: 100%;
|
line-height: 44px;
|
||||||
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; */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
> .send {
|
.desc {
|
||||||
margin: 0 16px;
|
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 Tooltip from "../../../common/component/Tooltip";
|
||||||
import alertIcon from "../../../assets/icons/alert.svg?url";
|
import alertIcon from "../../../assets/icons/alert.svg?url";
|
||||||
import pinIcon from "../../../assets/icons/pin.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 boardosIcon from "../../../assets/icons/app.boardos.svg?url";
|
||||||
import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url";
|
import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url";
|
||||||
import useChatScroll from "../../../common/hook/useChatScroll";
|
import useChatScroll from "../../../common/hook/useChatScroll";
|
||||||
import Send from "../../../common/component/Send";
|
|
||||||
import { useReadMessageMutation } from "../../../app/services/message";
|
import { useReadMessageMutation } from "../../../app/services/message";
|
||||||
import Contact from "../../../common/component/Contact";
|
import Contact from "../../../common/component/Contact";
|
||||||
import Layout from "../Layout";
|
import Layout from "../Layout";
|
||||||
@@ -19,17 +18,23 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
|
|||||||
const updateReadDebounced = useDebounce(updateReadIndex, 300);
|
const updateReadDebounced = useDebounce(updateReadIndex, 300);
|
||||||
console.log("dm files", dropFiles);
|
console.log("dm files", dropFiles);
|
||||||
// const [mids, setMids] = useState([]);
|
// const [mids, setMids] = useState([]);
|
||||||
const { msgIds, currUser, messageData, footprint, loginUid } = useSelector(
|
const {
|
||||||
(store) => {
|
msgIds,
|
||||||
return {
|
currUser,
|
||||||
loginUid: store.authData.uid,
|
messageData,
|
||||||
footprint: store.footprint,
|
footprint,
|
||||||
currUser: store.contacts.byId[uid],
|
loginUid,
|
||||||
msgIds: store.userMessage.byId[uid] || [],
|
selects,
|
||||||
messageData: store.message,
|
} = 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);
|
const ref = useChatScroll(msgIds);
|
||||||
|
|
||||||
if (!currUser) return null;
|
if (!currUser) return null;
|
||||||
@@ -74,32 +79,25 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
|
|||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StyledDMChat>
|
<StyledDMChat ref={ref}>
|
||||||
<div className="chat" ref={ref}>
|
{[...msgIds]
|
||||||
{[...msgIds]
|
.sort((a, b) => {
|
||||||
.sort((a, b) => {
|
return Number(a) - Number(b);
|
||||||
return Number(a) - Number(b);
|
})
|
||||||
})
|
.map((mid, idx) => {
|
||||||
.map((mid, idx) => {
|
const curr = messageData[mid];
|
||||||
const curr = messageData[mid];
|
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
|
||||||
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
|
const read = curr?.from_uid == loginUid || mid <= readIndex;
|
||||||
const read = curr?.from_uid == loginUid || mid <= readIndex;
|
return renderMessageFragment({
|
||||||
return renderMessageFragment({
|
selectMode: !!selects,
|
||||||
updateReadIndex: updateReadDebounced,
|
updateReadIndex: updateReadDebounced,
|
||||||
read,
|
read,
|
||||||
prev,
|
prev,
|
||||||
curr,
|
curr,
|
||||||
contextId: uid,
|
contextId: uid,
|
||||||
context: "user",
|
context: "user",
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
<Send
|
|
||||||
key={currUser?.uid}
|
|
||||||
context="user"
|
|
||||||
name={currUser?.name}
|
|
||||||
id={currUser?.uid}
|
|
||||||
/>
|
|
||||||
</StyledDMChat>
|
</StyledDMChat>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,20 +28,9 @@ export const StyledHeader = styled.header`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export const StyledDMChat = styled.article`
|
export const StyledDMChat = styled.article`
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0;
|
padding: 18px 16px;
|
||||||
padding-bottom: 16px;
|
height: 100%;
|
||||||
height: calc(100vh - 56px - 22px);
|
height: -webkit-fill-available;
|
||||||
> .chat {
|
overflow: auto;
|
||||||
padding: 18px 16px;
|
|
||||||
height: 100%;
|
|
||||||
height: -webkit-fill-available;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
> .send {
|
|
||||||
margin: 0 16px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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 { useState, useRef, useEffect } from "react";
|
||||||
import { useDrop } from "react-dnd";
|
import { useDrop } from "react-dnd";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import { NativeTypes } from "react-dnd-html5-backend";
|
import { NativeTypes } from "react-dnd-html5-backend";
|
||||||
import styled from "styled-components";
|
import { updateUploadFiles } from "../../../app/slices/ui";
|
||||||
import { updateUploadFiles } from "../../app/slices/ui";
|
import ImagePreviewModal from "../../../common/component/ImagePreviewModal";
|
||||||
import ImagePreviewModal from "../../common/component/ImagePreviewModal";
|
import Send from "../../../common/component/Send";
|
||||||
const StyledWrapper = styled.article`
|
import Styled from "./styled";
|
||||||
position: relative;
|
import Operations from "./Operations";
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function Layout({
|
export default function Layout({
|
||||||
children,
|
children,
|
||||||
header,
|
header,
|
||||||
aside = null,
|
aside = null,
|
||||||
contacts = null,
|
contacts = null,
|
||||||
// dropFiles = [],
|
dropFiles = [],
|
||||||
context = "channel",
|
context = "channel",
|
||||||
to = null,
|
to = null,
|
||||||
}) {
|
}) {
|
||||||
@@ -119,6 +22,9 @@ export default function Layout({
|
|||||||
|
|
||||||
const messagesContainer = useRef(null);
|
const messagesContainer = useRef(null);
|
||||||
const [previewImage, setPreviewImage] = useState(null);
|
const [previewImage, setPreviewImage] = useState(null);
|
||||||
|
const selects = useSelector(
|
||||||
|
(store) => store.ui.selectMessages[`${context}_${to}`]
|
||||||
|
);
|
||||||
const [{ isActive }, drop] = useDrop(
|
const [{ isActive }, drop] = useDrop(
|
||||||
() => ({
|
() => ({
|
||||||
accept: [NativeTypes.FILE],
|
accept: [NativeTypes.FILE],
|
||||||
@@ -139,11 +45,16 @@ export default function Layout({
|
|||||||
}),
|
}),
|
||||||
[context, to]
|
[context, to]
|
||||||
);
|
);
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// if (dropFiles?.length) {
|
if (dropFiles?.length) {
|
||||||
// setFiles((prevs) => [...prevs, ...dropFiles]);
|
const filesData = dropFiles.map((file) => {
|
||||||
// }
|
const { size, type, name } = file;
|
||||||
// }, [dropFiles]);
|
const url = URL.createObjectURL(file);
|
||||||
|
return { size, type, name, url };
|
||||||
|
});
|
||||||
|
dispatch(updateUploadFiles({ context, id: to, data: filesData }));
|
||||||
|
}
|
||||||
|
}, [dropFiles]);
|
||||||
|
|
||||||
const closePreviewModal = () => {
|
const closePreviewModal = () => {
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
@@ -177,10 +88,16 @@ export default function Layout({
|
|||||||
{previewImage && (
|
{previewImage && (
|
||||||
<ImagePreviewModal data={previewImage} closeModal={closePreviewModal} />
|
<ImagePreviewModal data={previewImage} closeModal={closePreviewModal} />
|
||||||
)}
|
)}
|
||||||
<StyledWrapper ref={drop}>
|
<Styled ref={drop}>
|
||||||
{header}
|
{header}
|
||||||
<main className="main" ref={messagesContainer}>
|
<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>}
|
{contacts && <div className="members">{contacts}</div>}
|
||||||
{aside && <div className="aside">{aside}</div>}
|
{aside && <div className="aside">{aside}</div>}
|
||||||
</main>
|
</main>
|
||||||
@@ -202,7 +119,7 @@ export default function Layout({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 React from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { isImage } from "../../common/utils";
|
import { isImage } from "../../common/utils";
|
||||||
import { ContentTypes } from "../../app/config";
|
import { ContentTypes } from "../../app/config";
|
||||||
|
import Checkbox from "../../common/component/styled/Checkbox";
|
||||||
import Divider from "../../common/component/Divider";
|
import Divider from "../../common/component/Divider";
|
||||||
import Message from "../../common/component/Message";
|
import Message from "../../common/component/Message";
|
||||||
|
import { updateSelectMessages } from "../../app/slices/ui";
|
||||||
// function debounce(callback, wait = 2000, immediate = false) {
|
// function debounce(callback, wait = 2000, immediate = false) {
|
||||||
// let timeout = null;
|
// let timeout = null;
|
||||||
// return function () {
|
// return function () {
|
||||||
@@ -86,7 +90,66 @@ export const renderPreviewMessage = (message = null) => {
|
|||||||
}
|
}
|
||||||
return res;
|
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 = ({
|
export const renderMessageFragment = ({
|
||||||
|
selectMode = false,
|
||||||
isFirst = false,
|
isFirst = false,
|
||||||
read = true,
|
read = true,
|
||||||
updateReadIndex,
|
updateReadIndex,
|
||||||
@@ -111,15 +174,23 @@ export const renderMessageFragment = ({
|
|||||||
return (
|
return (
|
||||||
<React.Fragment key={mid}>
|
<React.Fragment key={mid}>
|
||||||
{divider && <Divider content={divider}></Divider>}
|
{divider && <Divider content={divider}></Divider>}
|
||||||
<Message
|
<MessageWrapper
|
||||||
isFirst={isFirst}
|
|
||||||
updateReadIndex={updateReadIndex}
|
|
||||||
read={read}
|
|
||||||
context={context}
|
context={context}
|
||||||
|
id={contextId}
|
||||||
mid={mid}
|
mid={mid}
|
||||||
key={mid}
|
selectMode={selectMode}
|
||||||
contextId={contextId}
|
>
|
||||||
/>
|
<Message
|
||||||
|
readOnly={selectMode}
|
||||||
|
isFirst={isFirst}
|
||||||
|
updateReadIndex={updateReadIndex}
|
||||||
|
read={read}
|
||||||
|
context={context}
|
||||||
|
mid={mid}
|
||||||
|
key={mid}
|
||||||
|
contextId={contextId}
|
||||||
|
/>
|
||||||
|
</MessageWrapper>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user