Add message context menu (#206)
This commit is contained in:
parent
fc2d65f202
commit
f2aa41309a
|
@ -6,11 +6,16 @@ import { textSmallStyles } from "../Text";
|
|||
type DropdownMenuProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
style?: { top: number; left: number };
|
||||
};
|
||||
|
||||
export function DropdownMenu({ children, className }: DropdownMenuProps) {
|
||||
export function DropdownMenu({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
}: DropdownMenuProps) {
|
||||
return (
|
||||
<MenuBlock className={className}>
|
||||
<MenuBlock className={className} style={style}>
|
||||
<MenuList>{children}</MenuList>
|
||||
</MenuBlock>
|
||||
);
|
||||
|
@ -24,8 +29,6 @@ const MenuBlock = styled.div`
|
|||
border-radius: 8px;
|
||||
padding: 8px 0;
|
||||
position: absolute;
|
||||
top: calc(100% - 8px);
|
||||
right: 8px;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
|
@ -47,6 +50,10 @@ export const MenuItem = styled.li`
|
|||
background: ${({ theme }) => theme.border};
|
||||
}
|
||||
|
||||
&.picker:hover {
|
||||
background: ${({ theme }) => theme.bodyBackgroundColor};
|
||||
}
|
||||
|
||||
& > svg.red {
|
||||
fill: ${({ theme }) => theme.redColor};
|
||||
}
|
||||
|
@ -74,4 +81,10 @@ export const MenuSection = styled.div`
|
|||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.message {
|
||||
padding: 4px 0 0;
|
||||
margin: 4px 0 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import { utils } from "@waku/status-communities/dist/cjs";
|
||||
import { BaseEmoji } from "emoji-mart";
|
||||
import React, { useRef } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useIdentity } from "../../contexts/identityProvider";
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||
import { useClickPosition } from "../../hooks/useClickPosition";
|
||||
import { useContextMenu } from "../../hooks/useContextMenu";
|
||||
import { Reply } from "../../hooks/useReply";
|
||||
import { ChatMessage } from "../../models/ChatMessage";
|
||||
import { DeleteIcon } from "../Icons/DeleteIcon";
|
||||
import { EditIcon } from "../Icons/EditIcon";
|
||||
import { PinIcon } from "../Icons/PinIcon";
|
||||
import { ReplySvg } from "../Icons/ReplyIcon";
|
||||
import { ReactionPicker } from "../Reactions/ReactionPicker";
|
||||
|
||||
import { DropdownMenu, MenuItem, MenuSection, MenuText } from "./DropdownMenu";
|
||||
|
||||
interface MessageMenuProps {
|
||||
message: ChatMessage;
|
||||
messageReactions: BaseEmoji[];
|
||||
setMessageReactions: React.Dispatch<React.SetStateAction<BaseEmoji[]>>;
|
||||
setReply: (val: Reply | undefined) => void;
|
||||
messageRef: React.MutableRefObject<null>;
|
||||
}
|
||||
|
||||
export const MessageMenu = ({
|
||||
message,
|
||||
messageReactions,
|
||||
setMessageReactions,
|
||||
setReply,
|
||||
messageRef,
|
||||
}: MessageMenuProps) => {
|
||||
const identity = useIdentity();
|
||||
const { activeChannel } = useMessengerContext();
|
||||
const { showMenu, setShowMenu } = useContextMenu(message.id);
|
||||
const { topPosition, leftPosition } = useClickPosition(messageRef);
|
||||
|
||||
const menuStyle = {
|
||||
top: topPosition,
|
||||
left: leftPosition,
|
||||
};
|
||||
|
||||
const ref = useRef(null);
|
||||
useClickOutside(ref, () => setShowMenu(false));
|
||||
|
||||
const userMessage =
|
||||
identity && message.sender === utils.bufToHex(identity.publicKey);
|
||||
|
||||
return identity && showMenu ? (
|
||||
<div ref={ref} id={"messageDropdown"}>
|
||||
<MessageDropdown style={menuStyle}>
|
||||
<MenuItem className="picker">
|
||||
<ReactionPicker
|
||||
messageReactions={messageReactions}
|
||||
setMessageReactions={setMessageReactions}
|
||||
className="menu"
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuSection className={`${!userMessage && "message"}`}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setReply({
|
||||
sender: message.sender,
|
||||
content: message.content,
|
||||
image: message.image,
|
||||
id: message.id,
|
||||
});
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<ReplySvg width={16} height={16} className="menu" />
|
||||
<MenuText>Reply</MenuText>
|
||||
</MenuItem>
|
||||
|
||||
{userMessage && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<EditIcon width={16} height={16} />
|
||||
<MenuText>Edit</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{activeChannel?.type !== "channel" && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<PinIcon width={16} height={16} className="menu" />
|
||||
<MenuText>Pin</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuSection>
|
||||
{userMessage && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon width={16} height={16} className="red" />
|
||||
<MenuText className="red">Delete message</MenuText>
|
||||
</MenuItem>
|
||||
)}
|
||||
</MessageDropdown>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
const MessageDropdown = styled(DropdownMenu)`
|
||||
width: 176px;
|
||||
`;
|
|
@ -31,4 +31,8 @@ const Icon = styled.svg`
|
|||
&.red {
|
||||
fill: ${({ theme }) => theme.redColor};
|
||||
}
|
||||
|
||||
&.grey {
|
||||
fill: ${({ theme }) => theme.secondary};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -27,4 +27,8 @@ export function EditIcon({ width, height, className }: EditIconProps) {
|
|||
|
||||
const Icon = styled.svg`
|
||||
fill: ${({ theme }) => theme.tertiary};
|
||||
|
||||
&.grey {
|
||||
fill: ${({ theme }) => theme.secondary};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type PinIconProps = {
|
||||
width: number;
|
||||
height: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function PinIcon({ width, height, className }: PinIconProps) {
|
||||
return (
|
||||
<Icon
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path d="M14.8956 7.28455C14.8956 6.45612 14.2474 5.60892 13.4478 5.39227C12.6482 5.17563 12 5.67157 12 6.5C12 7.32843 12.6482 8.17563 13.4478 8.39227C14.2474 8.60892 14.8956 8.11297 14.8956 7.28455Z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C8.68629 2 6 4.68629 6 8C6 10.9077 8.06835 13.3323 10.814 13.8828C11.0613 13.9324 11.25 14.1429 11.25 14.3951L11.25 21C11.25 21.4142 11.5858 21.75 12 21.75C12.4142 21.75 12.75 21.4142 12.75 21L12.75 14.3951C12.75 14.1429 12.9387 13.9324 13.186 13.8828C15.9317 13.3323 18 10.9077 18 8C18 4.68629 15.3137 2 12 2ZM7.5 8C7.5 10.4853 9.51472 12.5 12 12.5C14.4853 12.5 16.5 10.4853 16.5 8C16.5 5.51472 14.4853 3.5 12 3.5C9.51472 3.5 7.5 5.51472 7.5 8Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = styled.svg`
|
||||
fill: ${({ theme }) => theme.secondary};
|
||||
|
||||
&.menu {
|
||||
fill: ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
|
||||
&.small {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
`;
|
|
@ -27,4 +27,8 @@ const Icon = styled.svg`
|
|||
&.input {
|
||||
fill: ${({ theme }) => theme.primary};
|
||||
}
|
||||
|
||||
&.menu {
|
||||
fill: ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ChatMessage } from "../../models/ChatMessage";
|
|||
import { equalDate } from "../../utils";
|
||||
import { ChatMessageContent } from "../Chat/ChatMessageContent";
|
||||
import { ContactMenu } from "../Form/ContactMenu";
|
||||
import { MessageMenu } from "../Form/MessageMenu";
|
||||
import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
|
||||
import { UserLogo } from "../Members/UserLogo";
|
||||
import { Reactions } from "../Reactions/Reactions";
|
||||
|
@ -104,6 +105,8 @@ export function UiMessage({
|
|||
const ref = useRef(null);
|
||||
useClickOutside(ref, () => setShowMenu(false));
|
||||
|
||||
const messageRef = useRef(null);
|
||||
|
||||
return (
|
||||
<MessageOuterWrapper>
|
||||
{(idx === 0 || !equalDate(prevMessage.date, message.date)) && (
|
||||
|
@ -115,7 +118,7 @@ export function UiMessage({
|
|||
)}
|
||||
<MessageWrapper className={`${mentioned && "mention"}`} id={message.id}>
|
||||
<MessageQuote quote={quote} />
|
||||
<UserMessageWrapper>
|
||||
<UserMessageWrapper ref={messageRef}>
|
||||
<IconBtn
|
||||
onClick={() => {
|
||||
if (identity) setShowMenu((e) => !e);
|
||||
|
@ -173,6 +176,13 @@ export function UiMessage({
|
|||
/>
|
||||
)}
|
||||
</ContentWrapper>
|
||||
<MessageMenu
|
||||
message={message}
|
||||
setReply={setReply}
|
||||
messageReactions={messageReactions}
|
||||
setMessageReactions={setMessageReactions}
|
||||
messageRef={messageRef}
|
||||
/>
|
||||
</UserMessageWrapper>
|
||||
{identity && (
|
||||
<Reactions
|
||||
|
@ -190,4 +200,5 @@ export function UiMessage({
|
|||
const UserMessageWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
`;
|
||||
|
|
|
@ -56,15 +56,24 @@ export const ReactionBtn = styled.button`
|
|||
align-items: center;
|
||||
border-radius: 8px;
|
||||
align-self: center;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.buttonBgHover};
|
||||
}
|
||||
|
||||
&.red:hover {
|
||||
background: ${({ theme }) => theme.buttonNoBgHover};
|
||||
}
|
||||
|
||||
&:hover > svg {
|
||||
fill: ${({ theme }) => theme.tertiary};
|
||||
}
|
||||
|
||||
&.red:hover > svg {
|
||||
fill: ${({ theme }) => theme.redColor};
|
||||
}
|
||||
|
||||
&:hover > div {
|
||||
visibility: visible;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ const emojiLaughing = getEmojiDataFromNative("😆", "twitter", data);
|
|||
const emojiDisappointed = getEmojiDataFromNative("😥", "twitter", data);
|
||||
const emojiRage = getEmojiDataFromNative("😡", "twitter", data);
|
||||
|
||||
const emojiArr = [
|
||||
export const emojiArr = [
|
||||
emojiHeart,
|
||||
emojiLike,
|
||||
emojiDislike,
|
||||
|
@ -46,13 +46,14 @@ export function ReactionPicker({
|
|||
key={emoji.id}
|
||||
onClick={() => handleReaction(emoji)}
|
||||
className={`${messageReactions.includes(emoji) && "chosen"}`}
|
||||
menuMode={className === "menu"}
|
||||
>
|
||||
{" "}
|
||||
<Emoji
|
||||
emoji={emoji}
|
||||
set={"twitter"}
|
||||
skin={emoji.skin || 1}
|
||||
size={32}
|
||||
size={className === "menu" ? 20 : 32}
|
||||
/>
|
||||
</EmojiBtn>
|
||||
))}
|
||||
|
@ -78,11 +79,19 @@ const Wrapper = styled.div`
|
|||
transform: none;
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
}
|
||||
|
||||
&.menu {
|
||||
width: 100%;
|
||||
position: static;
|
||||
box-shadow: unset;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const EmojiBtn = styled.button`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
export const EmojiBtn = styled.button<{ menuMode: boolean }>`
|
||||
width: ${({ menuMode }) => (menuMode ? "24px" : "40px")};
|
||||
height: ${({ menuMode }) => (menuMode ? "24px" : "40px")};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import { utils } from "@waku/status-communities/dist/cjs";
|
||||
import { BaseEmoji } from "emoji-mart";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useIdentity } from "../../contexts/identityProvider";
|
||||
import { useMessengerContext } from "../../contexts/messengerProvider";
|
||||
import { Reply } from "../../hooks/useReply";
|
||||
import { ChatMessage } from "../../models/ChatMessage";
|
||||
import { Tooltip } from "../Form/Tooltip";
|
||||
import { DeleteIcon } from "../Icons/DeleteIcon";
|
||||
import { EditIcon } from "../Icons/EditIcon";
|
||||
import { PinIcon } from "../Icons/PinIcon";
|
||||
import { ReplySvg } from "../Icons/ReplyIcon";
|
||||
|
||||
import { ReactionBtn, ReactionButton } from "./ReactionButton";
|
||||
|
@ -22,6 +28,12 @@ export function Reactions({
|
|||
messageReactions,
|
||||
setMessageReactions,
|
||||
}: ReactionsProps) {
|
||||
const identity = useIdentity();
|
||||
const { activeChannel } = useMessengerContext();
|
||||
|
||||
const userMessage =
|
||||
identity && message.sender === utils.bufToHex(identity.publicKey);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<ReactionButton
|
||||
|
@ -41,6 +53,24 @@ export function Reactions({
|
|||
<ReplySvg width={22} height={22} />
|
||||
<Tooltip tip="Reply" />
|
||||
</ReactionBtn>
|
||||
{userMessage && (
|
||||
<ReactionBtn>
|
||||
<EditIcon width={22} height={22} className="grey" />
|
||||
<Tooltip tip="Edit" />
|
||||
</ReactionBtn>
|
||||
)}
|
||||
{activeChannel?.type !== "channel" && (
|
||||
<ReactionBtn>
|
||||
<PinIcon width={22} height={22} />
|
||||
<Tooltip tip="Pin" />
|
||||
</ReactionBtn>
|
||||
)}
|
||||
{userMessage && (
|
||||
<ReactionBtn className="red">
|
||||
<DeleteIcon width={22} height={22} className="grey" />
|
||||
<Tooltip tip="Delete" />
|
||||
</ReactionBtn>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { RefObject, useCallback, useEffect, useState } from "react";
|
||||
|
||||
export const useClickPosition = (ref: RefObject<HTMLDivElement>) => {
|
||||
const [topPosition, setTopPosition] = useState(0);
|
||||
const [leftPosition, setLeftPosition] = useState(0);
|
||||
|
||||
const getPosition = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (ref.current) {
|
||||
const target = e.target as HTMLImageElement;
|
||||
const imgTarget = target.tagName === "IMG";
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const x = ref.current.clientWidth - e.clientX < 180 ? 180 : 0;
|
||||
setLeftPosition(imgTarget ? -200 : e.clientX - rect.left - x);
|
||||
setTopPosition(imgTarget ? 0 : e.clientY - rect.top);
|
||||
}
|
||||
},
|
||||
[setTopPosition, setLeftPosition]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("contextmenu", getPosition);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("contextmenu", getPosition);
|
||||
};
|
||||
});
|
||||
|
||||
return { topPosition, leftPosition };
|
||||
};
|
Loading…
Reference in New Issue