feat: select multiple messages and forward
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user