feat: message context menu
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 4C6 2.89543 6.89543 2 8 2H14C15.1046 2 16 2.89543 16 4V14C16 15.1046 15.1046 16 14 16H8C6.89543 16 6 15.1046 6 14V4ZM4 6.00001C4 5.25973 4.4022 4.61339 5 4.26758V14.5C5 15.8807 6.11929 17 7.5 17H13.7324C13.3866 17.5978 12.7403 18 12 18H7.5C5.567 18 4 16.433 4 14.5V6.00001Z" fill="#475467"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2ZM13.3584 7.64645C13.1849 7.47288 12.9154 7.4536 12.7206 7.58859L12.6513 7.64645L9 11.298L7.35355 9.65131L7.28431 9.59346C7.08944 9.45846 6.82001 9.47775 6.64645 9.65131C6.47288 9.82488 6.4536 10.0943 6.58859 10.2892L6.64645 10.3584L8.64645 12.3584L8.71569 12.4163C8.8862 12.5344 9.1138 12.5344 9.28431 12.4163L9.35355 12.3584L13.3584 8.35355L13.4163 8.28431C13.5513 8.08944 13.532 7.82001 13.3584 7.64645Z" fill="#475467"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 648 B |
@@ -8,6 +8,7 @@ export default function ContextMenu({ items = [], hideMenu }) {
|
||||
if (!item) return null;
|
||||
const {
|
||||
title,
|
||||
icon = null,
|
||||
handler = (evt) => {
|
||||
evt.preventDefault();
|
||||
hideMenu();
|
||||
@@ -26,6 +27,7 @@ export default function ContextMenu({ items = [], hideMenu }) {
|
||||
hideMenu();
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{title}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,9 @@ export default function DeleteMessageConfirmModal({ closeModal, mids = [] }) {
|
||||
// className="animate__animated animate__fadeInDown animate__faster"
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal.bind(null, false)}>Cancel</Button>
|
||||
<Button className="cancel" onClick={closeModal.bind(null, false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isDeleting}
|
||||
onClick={handleDelete}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useState, useRef } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
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";
|
||||
import DeleteMessageConfirm from "../DeleteMessageConfirm";
|
||||
|
||||
import useFavMessage from "../../hook/useFavMessage";
|
||||
import ReactionPicker from "./ReactionPicker";
|
||||
import replyIcon from "../../../assets/icons/reply.svg?url";
|
||||
@@ -16,13 +15,9 @@ import reactIcon from "../../../assets/icons/reaction.svg?url";
|
||||
import editIcon from "../../../assets/icons/edit.svg?url";
|
||||
import IconBookmark from "../../../assets/icons/bookmark.add.svg";
|
||||
import moreIcon from "../../../assets/icons/more.svg?url";
|
||||
import ForwardModal from "../ForwardModal";
|
||||
import PinMessageModal from "./PinMessageModal";
|
||||
import usePinMessage from "../../hook/usePinMessage";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import toast from "react-hot-toast";
|
||||
import useMessageOperation from "./useMessageOperation";
|
||||
const StyledCmds = styled.ul`
|
||||
/* z-index: 9999; */
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
@@ -62,26 +57,30 @@ const StyledCmds = styled.ul`
|
||||
}
|
||||
`;
|
||||
export default function Commands({
|
||||
content_type = ContentTypes.text,
|
||||
context = "user",
|
||||
contextId = 0,
|
||||
mid = 0,
|
||||
from_uid = 0,
|
||||
toggleEditMessage,
|
||||
}) {
|
||||
const {
|
||||
canDelete,
|
||||
canReply,
|
||||
canEdit,
|
||||
canPin,
|
||||
unPin,
|
||||
pinned,
|
||||
toggleDeleteModal,
|
||||
toggleForwardModal,
|
||||
togglePinModal,
|
||||
PinModal,
|
||||
DeleteModal,
|
||||
ForwardModal,
|
||||
} = useMessageOperation({ mid, context, contextId });
|
||||
const { addFavorite, isFavorited } = useFavMessage({
|
||||
cid: context == "channel" ? contextId : null,
|
||||
});
|
||||
const [mids, setMids] = useState([]);
|
||||
const dispatch = useDispatch();
|
||||
const [pinModalVisible, setPinModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [forwardModalVisible, setForwardModalVisible] = useState(false);
|
||||
const [tippyVisible, setTippyVisible] = useState(false);
|
||||
const { canPin, pins, unpinMessage, isUnpinSuccess } = usePinMessage(
|
||||
context == "channel" ? contextId : undefined
|
||||
);
|
||||
const currUid = useSelector((store) => store.authData.uid);
|
||||
const cmdsRef = useRef(null);
|
||||
const handleReply = (fromMenu) => {
|
||||
if (contextId) {
|
||||
@@ -91,19 +90,7 @@ export default function Commands({
|
||||
hideAll();
|
||||
}
|
||||
};
|
||||
const toggleForwardModal = () => {
|
||||
hideAll();
|
||||
console.log("midss", mids);
|
||||
setForwardModalVisible((prev) => !prev);
|
||||
};
|
||||
const toggleDeleteModal = () => {
|
||||
hideAll();
|
||||
setDeleteModalVisible((prev) => !prev);
|
||||
};
|
||||
const togglePinModal = () => {
|
||||
hideAll();
|
||||
setPinModalVisible((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleTippyVisible = (visible = true) => {
|
||||
setTippyVisible(visible);
|
||||
};
|
||||
@@ -113,7 +100,7 @@ export default function Commands({
|
||||
};
|
||||
const handleUnpin = () => {
|
||||
hideAll();
|
||||
unpinMessage(mid);
|
||||
unPin(mid);
|
||||
};
|
||||
const handleAddFav = async () => {
|
||||
hideAll();
|
||||
@@ -129,34 +116,9 @@ export default function Commands({
|
||||
toast.error("Added Favorites Failed!");
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isUnpinSuccess) {
|
||||
toast.success("Unpin Message Successfully!");
|
||||
}
|
||||
}, [isUnpinSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (content_type == ContentTypes.archive) {
|
||||
// forward message
|
||||
const forwardEle = document.querySelector(
|
||||
`[data-msg-mid='${mid}'] .down [data-forwarded-mids]`
|
||||
);
|
||||
if (forwardEle) {
|
||||
const mids = forwardEle.dataset.forwardedMids.split(",");
|
||||
setMids(mids);
|
||||
}
|
||||
} else {
|
||||
setMids([mid]);
|
||||
}
|
||||
}, [mid, content_type]);
|
||||
|
||||
const enablePin = context == "channel" && canPin;
|
||||
const enableEdit =
|
||||
currUid == from_uid &&
|
||||
[ContentTypes.text, ContentTypes.markdown].includes(content_type);
|
||||
const enableReply = currUid != from_uid;
|
||||
const pinned = enablePin ? pins.findIndex((p) => p.mid == mid) > -1 : false;
|
||||
return (
|
||||
<>
|
||||
<StyledCmds
|
||||
ref={cmdsRef}
|
||||
className={`cmds ${tippyVisible ? "visible" : ""}`}
|
||||
@@ -177,14 +139,14 @@ export default function Commands({
|
||||
</Tooltip>
|
||||
</li>
|
||||
</Tippy>
|
||||
{enableEdit && (
|
||||
{canEdit && (
|
||||
<li className="cmd" onClick={toggleEditMessage}>
|
||||
<Tooltip placement="top" tip="Edit">
|
||||
<img src={editIcon} alt="icon edit" />
|
||||
</Tooltip>
|
||||
</li>
|
||||
)}
|
||||
{enableReply && (
|
||||
{canReply && (
|
||||
<li className="cmd" onClick={handleReply}>
|
||||
<Tooltip placement="top" tip="Reply">
|
||||
<img src={replyIcon} alt="icon reply" />
|
||||
@@ -205,8 +167,7 @@ export default function Commands({
|
||||
trigger="click"
|
||||
content={
|
||||
<StyledMenu className="menu">
|
||||
{/* <li className="item">Edit Message</li> */}
|
||||
{enablePin && (
|
||||
{canPin && (
|
||||
<li
|
||||
className="item"
|
||||
onClick={pinned ? handleUnpin : togglePinModal}
|
||||
@@ -223,7 +184,7 @@ export default function Commands({
|
||||
<li className="item" onClick={handleSelect.bind(null, mid)}>
|
||||
Select
|
||||
</li>
|
||||
{currUid == from_uid && (
|
||||
{canDelete && (
|
||||
<li className="item danger" onClick={toggleDeleteModal}>
|
||||
Delete Message
|
||||
</li>
|
||||
@@ -237,20 +198,10 @@ export default function Commands({
|
||||
</Tooltip>
|
||||
</li>
|
||||
</Tippy>
|
||||
|
||||
{deleteModalVisible && (
|
||||
<DeleteMessageConfirm closeModal={toggleDeleteModal} mids={mid} />
|
||||
)}
|
||||
{forwardModalVisible && (
|
||||
<ForwardModal mids={mids} closeModal={toggleForwardModal} />
|
||||
)}
|
||||
{pinModalVisible && (
|
||||
<PinMessageModal
|
||||
mid={mid}
|
||||
gid={contextId}
|
||||
closeModal={togglePinModal}
|
||||
/>
|
||||
)}
|
||||
</StyledCmds>
|
||||
{PinModal}
|
||||
{ForwardModal}
|
||||
{DeleteModal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// import { useState, useEffect } from "react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import ContextMenu from "../ContextMenu";
|
||||
import IconDelete from "../../../assets/icons/delete.svg";
|
||||
import IconEdit from "../../../assets/icons/edit.svg";
|
||||
import IconReply from "../../../assets/icons/reply.svg";
|
||||
import IconForward from "../../../assets/icons/forward.svg";
|
||||
import IconPin from "../../../assets/icons/pin.svg";
|
||||
import IconCopy from "../../../assets/icons/copy.svg";
|
||||
import IconSelect from "../../../assets/icons/select.svg";
|
||||
import { updateSelectMessages } from "../../../app/slices/ui";
|
||||
import { addReplyingMessage } from "../../../app/slices/message";
|
||||
|
||||
import useMessageOperation from "./useMessageOperation";
|
||||
|
||||
export default function MessageContextMenu({
|
||||
context,
|
||||
contextId,
|
||||
mid,
|
||||
offset = {},
|
||||
visible,
|
||||
hide,
|
||||
editMessage,
|
||||
children,
|
||||
}) {
|
||||
const {
|
||||
copyContent,
|
||||
isMarkdown,
|
||||
canEdit,
|
||||
canPin,
|
||||
canCopy,
|
||||
canReply,
|
||||
pinned,
|
||||
unPin,
|
||||
toggleDeleteModal,
|
||||
toggleForwardModal,
|
||||
togglePinModal,
|
||||
PinModal,
|
||||
ForwardModal,
|
||||
DeleteModal,
|
||||
} = useMessageOperation({ mid, contextId, context });
|
||||
const dispatch = useDispatch();
|
||||
const handleSelect = () => {
|
||||
dispatch(updateSelectMessages({ context, id: contextId, data: mid }));
|
||||
// hideAll();
|
||||
};
|
||||
const handleReply = () => {
|
||||
// console.log("dddd", contextId, mid);
|
||||
if (contextId) {
|
||||
dispatch(addReplyingMessage({ id: contextId, mid }));
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{ForwardModal}
|
||||
{PinModal}
|
||||
{DeleteModal}
|
||||
<Tippy
|
||||
duration={0}
|
||||
delay={[0, 0]}
|
||||
content={
|
||||
<ContextMenu
|
||||
hideMenu={hide}
|
||||
items={[
|
||||
canEdit && {
|
||||
title: "Edit Message",
|
||||
icon: <IconEdit className="icon" />,
|
||||
handler: editMessage,
|
||||
},
|
||||
canReply && {
|
||||
title: "Reply",
|
||||
icon: <IconReply className="icon" />,
|
||||
handler: handleReply,
|
||||
},
|
||||
canCopy && {
|
||||
title: isMarkdown ? "Copy Markdown" : "Copy Text",
|
||||
icon: <IconCopy className="icon" />,
|
||||
handler: copyContent,
|
||||
},
|
||||
canPin && {
|
||||
title: pinned ? "Unpin" : "Pin",
|
||||
icon: <IconPin className="icon" />,
|
||||
handler: pinned ? unPin.bind(null, mid) : togglePinModal,
|
||||
},
|
||||
{
|
||||
title: "Forward",
|
||||
icon: <IconForward className="icon" />,
|
||||
handler: toggleForwardModal,
|
||||
},
|
||||
{
|
||||
title: "Select",
|
||||
icon: <IconSelect className="icon" />,
|
||||
handler: handleSelect,
|
||||
},
|
||||
{
|
||||
title: "Delete",
|
||||
danger: true,
|
||||
icon: <IconDelete className="icon" />,
|
||||
handler: toggleDeleteModal,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
interactive
|
||||
placement="right-start"
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
offset={[offset.y, offset.x]}
|
||||
visible={visible}
|
||||
onClickOutside={hide}
|
||||
key={mid}
|
||||
>
|
||||
{children}
|
||||
</Tippy>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,9 @@ import Commands from "./Commands";
|
||||
import EditMessage from "./EditMessage";
|
||||
import renderContent from "./renderContent";
|
||||
import Tooltip from "../Tooltip";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
|
||||
import useContextMenu from "../../hook/useContextMenu";
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
function Message({
|
||||
@@ -24,6 +27,12 @@ function Message({
|
||||
updateReadIndex,
|
||||
read = true,
|
||||
}) {
|
||||
const {
|
||||
visible: contextMenuVisible,
|
||||
offset,
|
||||
handleContextMenuEvent,
|
||||
hideContextMenu,
|
||||
} = useContextMenu();
|
||||
const inviewRef = useInView();
|
||||
const [edit, setEdit] = useState(false);
|
||||
const avatarRef = useRef(null);
|
||||
@@ -78,7 +87,17 @@ function Message({
|
||||
console.log("render message");
|
||||
// return null;
|
||||
return (
|
||||
<ContextMenu
|
||||
editMessage={toggleEditMessage}
|
||||
context={context}
|
||||
contextId={contextId}
|
||||
mid={mid}
|
||||
visible={contextMenuVisible}
|
||||
hide={hideContextMenu}
|
||||
offset={offset}
|
||||
>
|
||||
<StyledWrapper
|
||||
onContextMenu={handleContextMenuEvent}
|
||||
data-msg-mid={mid}
|
||||
ref={inviewRef}
|
||||
className={`message ${readOnly ? "readonly" : ""}`}
|
||||
@@ -143,6 +162,7 @@ function Message({
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
export default React.memo(Message, (prevs, nexts) => {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import DeleteMessageConfirm from "../DeleteMessageConfirm";
|
||||
import ForwardModal from "../ForwardModal";
|
||||
import PinMessageModal from "./PinMessageModal";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import { useSelector } from "react-redux";
|
||||
import useCopy from "../../hook/useCopy";
|
||||
import usePinMessage from "../../hook/usePinMessage";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export default function useMessageOperation({ mid, context, contextId }) {
|
||||
const [copied, copy] = useCopy();
|
||||
const { content_type, currUid, from_uid, content } = useSelector((store) => {
|
||||
return {
|
||||
content: store.message[mid]?.content,
|
||||
from_uid: store.message[mid]?.from_uid,
|
||||
content_type: store.message[mid]?.content_type,
|
||||
currUid: store.authData.uid,
|
||||
};
|
||||
});
|
||||
const { canPin, pins, unpinMessage, isUnpinSuccess } = usePinMessage(
|
||||
context == "channel" ? contextId : undefined
|
||||
);
|
||||
const [mids, setMids] = useState([]);
|
||||
const [pinModalVisible, setPinModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [forwardModalVisible, setForwardModalVisible] = useState(false);
|
||||
|
||||
const toggleForwardModal = () => {
|
||||
// hideAll();
|
||||
setForwardModalVisible((prev) => !prev);
|
||||
};
|
||||
const toggleDeleteModal = () => {
|
||||
// hideAll();
|
||||
setDeleteModalVisible((prev) => !prev);
|
||||
};
|
||||
const togglePinModal = () => {
|
||||
// hideAll();
|
||||
setPinModalVisible((prev) => !prev);
|
||||
};
|
||||
const copyContent = () => {
|
||||
copy(content);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (content_type == ContentTypes.archive) {
|
||||
// forward message
|
||||
const forwardEle = document.querySelector(
|
||||
`[data-msg-mid='${mid}'] .down [data-forwarded-mids]`
|
||||
);
|
||||
if (forwardEle) {
|
||||
const mids = forwardEle.dataset.forwardedMids.split(",");
|
||||
setMids(mids);
|
||||
}
|
||||
} else {
|
||||
setMids([mid]);
|
||||
}
|
||||
}, [mid, content_type]);
|
||||
useEffect(() => {
|
||||
if (isUnpinSuccess) {
|
||||
toast.success("Unpin Message Successfully!");
|
||||
}
|
||||
}, [isUnpinSuccess]);
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
toast.success("Copied");
|
||||
}
|
||||
}, [copied]);
|
||||
const enablePin = context == "channel" && canPin;
|
||||
const enableReply = currUid != from_uid;
|
||||
|
||||
const enableEdit =
|
||||
currUid == from_uid &&
|
||||
[ContentTypes.text, ContentTypes.markdown].includes(content_type);
|
||||
const canDelete = currUid == from_uid;
|
||||
const canCopy = [ContentTypes.text, ContentTypes.markdown].includes(
|
||||
content_type
|
||||
);
|
||||
return {
|
||||
copyContent,
|
||||
canCopy,
|
||||
isMarkdown: content_type == ContentTypes.markdown,
|
||||
canDelete,
|
||||
canPin: context == "channel" && canPin,
|
||||
pinned: enablePin ? pins.findIndex((p) => p.mid == mid) > -1 : false,
|
||||
unPin: unpinMessage,
|
||||
canReply: enableReply,
|
||||
canEdit: enableEdit,
|
||||
toggleDeleteModal,
|
||||
toggleForwardModal,
|
||||
togglePinModal,
|
||||
DeleteModal: deleteModalVisible ? (
|
||||
<DeleteMessageConfirm closeModal={toggleDeleteModal} mids={mid} />
|
||||
) : null,
|
||||
ForwardModal: forwardModalVisible ? (
|
||||
<ForwardModal mids={mids} closeModal={toggleForwardModal} />
|
||||
) : null,
|
||||
PinModal: pinModalVisible ? (
|
||||
<PinMessageModal mid={mid} gid={contextId} closeModal={togglePinModal} />
|
||||
) : null,
|
||||
};
|
||||
}
|
||||
@@ -8,20 +8,35 @@ const StyledMenu = styled.ul`
|
||||
box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1),
|
||||
0px 10px 10px rgba(31, 41, 55, 0.04);
|
||||
border-radius: 12px;
|
||||
min-width: 200px;
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
padding: 6px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #616161;
|
||||
/* transition: color 0.2s ease; */
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
path {
|
||||
fill: #475467;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: #22ccee;
|
||||
color: #fff;
|
||||
.icon {
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.underline {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
Reference in New Issue
Block a user