Add mute submenu (#204)

* Add mute submenu

* Change icon color

* Change icons color

* Add menu overlapping

* Add clickoutside hook to every dropdown
This commit is contained in:
Maria Rushkova 2022-01-27 14:23:06 +01:00 committed by GitHub
parent bcb41c6110
commit 9cfba1f50c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 251 additions and 120 deletions

View File

@ -1,8 +1,9 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { useClickOutside } from "../../hooks/useClickOutside";
import { Activity } from "../../models/Activity";
import { equalDate } from "../../utils/equalDate";
import { DownloadButton } from "../Buttons/DownloadButton";
@ -88,6 +89,9 @@ export function ActivityMessage({
}
}, [activity.message?.content]);
const ref = useRef(null);
useClickOutside(ref, () => setShowMenu(false));
return (
<MessageOuterWrapper>
<ActivityDate>
@ -214,6 +218,7 @@ export function ActivityMessage({
onClick={() => {
setShowMenu((e) => !e);
}}
ref={ref}
>
{showMenu && (
<ContactMenu id={activity.user} setShowMenu={setShowMenu} />

View File

@ -98,7 +98,7 @@ export function Channel({
<ChannelMenu
channel={channel}
setEditGroup={setEditGroup}
className={narrow ? "narrow" : "side"}
className={narrow ? "sideNarrow" : "side"}
/>
)}
</ChannelWrapper>

View File

@ -1,11 +1,12 @@
import { utils } from "@waku/status-communities/dist/cjs";
import { decode } from "html-entities";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components";
import { useFetchMetadata } from "../../contexts/fetchMetadataProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useClickOutside } from "../../hooks/useClickOutside";
import { ChatMessage } from "../../models/ChatMessage";
import { Metadata } from "../../models/Metadata";
import { ContactMenu } from "../Form/ContactMenu";
@ -32,8 +33,15 @@ export function Mention({ id, setMentioned, className }: MentionProps) {
}
}, [contact.id, identity]);
const ref = useRef(null);
useClickOutside(ref, () => setShowMenu(false));
return (
<MentionBLock onClick={() => setShowMenu(!showMenu)} className={className}>
<MentionBLock
onClick={() => setShowMenu(!showMenu)}
className={className}
ref={ref}
>
{`@${contact?.customName ?? contact.trueName}`}
{showMenu && <ContactMenu id={id.slice(1)} setShowMenu={setShowMenu} />}
</MentionBLock>

View File

@ -1,8 +1,9 @@
import React, { useState } from "react";
import React, { useRef, useState } from "react";
import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useNarrow } from "../../contexts/narrowProvider";
import { useClickOutside } from "../../hooks/useClickOutside";
import {
ActivityButton,
ActivityWrapper,
@ -68,6 +69,9 @@ export function ChatTopbar({
const narrow = useNarrow();
const [showChannelMenu, setShowChannelMenu] = useState(false);
const ref = useRef(null);
useClickOutside(ref, () => setShowChannelMenu(false));
if (!activeChannel) {
return <ChatTopbarLoading />;
}
@ -105,21 +109,24 @@ export function ChatTopbar({
<MembersIcon />
</TopBtn>
)}
<TopBtn onClick={() => setShowChannelMenu(!showChannelMenu)}>
<MoreIcon />
</TopBtn>
<div ref={ref}>
<TopBtn onClick={() => setShowChannelMenu(!showChannelMenu)}>
<MoreIcon />
{showChannelMenu && (
<ChannelMenu
channel={activeChannel}
showNarrowMembers={showState === ChatBodyState.Members}
switchMemberList={() => switchShowState(ChatBodyState.Members)}
setShowChannelMenu={setShowChannelMenu}
setEditGroup={setEditGroup}
className={`${narrow && "narrow"}`}
/>
)}
</TopBtn>
</div>
{!narrow && <ActivityButton />}
</MenuWrapper>
{loadingMessenger && <Loading />}
{showChannelMenu && (
<ChannelMenu
channel={activeChannel}
showNarrowMembers={showState === ChatBodyState.Members}
switchMemberList={() => switchShowState(ChatBodyState.Members)}
setShowChannelMenu={setShowChannelMenu}
setEditGroup={setEditGroup}
/>
)}
</Topbar>
);
}
@ -186,6 +193,7 @@ export const TopBtn = styled.button`
border-radius: 8px;
padding: 0;
background: ${({ theme }) => theme.bodyBackgroundColor};
position: relative;
&:hover {
background: ${({ theme }) => theme.inputColor};

View File

@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React, { useEffect, useMemo, useState } from "react";
import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider";
@ -14,12 +14,14 @@ import { EditIcon } from "../Icons/EditIcon";
import { LeftIcon } from "../Icons/LeftIcon";
import { MembersSmallIcon } from "../Icons/MembersSmallIcon";
import { MuteIcon } from "../Icons/MuteIcon";
import { NextIcon } from "../Icons/NextIcon";
import { ProfileIcon } from "../Icons/ProfileIcon";
import { EditModalName } from "../Modals/EditModal";
import { LeavingModalName } from "../Modals/LeavingModal";
import { ProfileModalName } from "../Modals/ProfileModal";
import { DropdownMenu, MenuItem, MenuText } from "./DropdownMenu";
import { DropdownMenu, MenuItem, MenuSection, MenuText } from "./DropdownMenu";
import { MuteMenu } from "./MuteMenu";
interface ChannelMenuProps {
channel: ChannelData;
@ -43,6 +45,15 @@ export const ChannelMenu = ({
const { setModal } = useModal(EditModalName);
const { setModal: setLeavingModal } = useModal(LeavingModalName);
const { setModal: setProfileModal } = useModal(ProfileModalName);
const [showSubmenu, setShowSubmenu] = useState(false);
const muting = channel.isMuted;
const [isMuted, setIsMuted] = useState(muting);
useEffect(() => {
if (isMuted) channel.isMuted = true;
if (!isMuted) channel.isMuted = false;
}, [isMuted]);
const { showMenu, setShowMenu: setShowSideMenu } = useContextMenu(
channel.id + "contextMenu"
@ -55,7 +66,7 @@ export const ChannelMenu = ({
if (showMenu || setShowChannelMenu) {
return (
<DropdownMenu closeMenu={setShowMenu} className={className}>
<ChannelDropdown className={className}>
{narrow && !className && (
<MenuItem
onClick={() => {
@ -102,15 +113,27 @@ export const ChannelMenu = ({
<MenuSection className={`${channel.type === "channel" && "channel"}`}>
<MenuItem
onClick={() => {
channel.isMuted = !channel.isMuted;
setShowMenu(false);
if (isMuted) {
setIsMuted(false);
setShowMenu(false);
}
}}
onMouseEnter={() => {
if (!isMuted) setShowSubmenu(true);
}}
onMouseLeave={() => {
if (!isMuted) setShowSubmenu(false);
}}
>
<MuteIcon width={16} height={16} />
{!isMuted && <NextIcon />}
<MenuText>
{(channel.isMuted ? "Unmute" : "Mute") +
{(isMuted ? "Unmute" : "Mute") +
(channel.type === "group" ? " Group" : " Chat")}
</MenuText>
{!isMuted && showSubmenu && (
<MuteMenu setIsMuted={setIsMuted} className={className} />
)}
</MenuItem>
<MenuItem onClick={() => clearNotifications(channel.id)}>
<CheckIcon width={16} height={16} />
@ -139,22 +162,25 @@ export const ChannelMenu = ({
</MenuText>
</MenuItem>
)}
</DropdownMenu>
</ChannelDropdown>
);
} else {
return null;
}
};
const MenuSection = styled.div`
padding: 4px 0;
margin: 4px 0;
border-top: 1px solid ${({ theme }) => theme.inputColor};
border-bottom: 1px solid ${({ theme }) => theme.inputColor};
const ChannelDropdown = styled(DropdownMenu)`
top: calc(100% + 4px);
right: 0px;
&.channel {
padding: 0;
margin: 0;
border: none;
&.side {
top: 20px;
left: calc(100% - 35px);
right: unset;
}
&.sideNarrow {
top: 20px;
right: 8px;
}
`;

View File

@ -5,7 +5,7 @@ import styled from "styled-components";
import { useIdentity } from "../../contexts/identityProvider";
import { useModal } from "../../contexts/modalProvider";
import { useManageContact } from "../../hooks/useManageContact";
import { AddContactSvg } from "../Icons/AddContactIcon";
import { AddContactIcon } from "../Icons/AddContactIcon";
import { BlockSvg } from "../Icons/BlockIcon";
import { ChatSvg } from "../Icons/ChatIcon";
import { EditIcon } from "../Icons/EditIcon";
@ -39,7 +39,7 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
if (!contact) return null;
return (
<ContactDropdown closeMenu={setShowMenu}>
<ContactDropdown>
<ContactInfo>
<UserIcon />
<UserNameWrapper>
@ -68,7 +68,7 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) {
setModal({ id, requestState: true });
}}
>
<AddContactSvg width={16} height={16} />
<AddContactIcon width={16} height={16} />
<MenuText>Send Contact Request</MenuText>
</MenuItem>
)}

View File

@ -1,30 +1,38 @@
import React, { ReactNode, useRef } from "react";
import React, { ReactNode } from "react";
import styled from "styled-components";
import { useClickOutside } from "../../hooks/useClickOutside";
import { textSmallStyles } from "../Text";
type DropdownMenuProps = {
children: ReactNode;
className?: string;
closeMenu: (val: boolean) => void;
};
export function DropdownMenu({
children,
className,
closeMenu,
}: DropdownMenuProps) {
const ref = useRef(null);
useClickOutside(ref, () => closeMenu(false));
export function DropdownMenu({ children, className }: DropdownMenuProps) {
return (
<MenuBlock className={className} ref={ref}>
<MenuBlock className={className}>
<MenuList>{children}</MenuList>
</MenuBlock>
);
}
const MenuBlock = styled.div`
width: 207px;
background: ${({ theme }) => theme.bodyBackgroundColor};
box-shadow: 0px 2px 4px rgba(0, 34, 51, 0.16),
0px 4px 12px rgba(0, 34, 51, 0.08);
border-radius: 8px;
padding: 8px 0;
position: absolute;
top: calc(100% - 8px);
right: 8px;
z-index: 2;
`;
const MenuList = styled.ul`
list-style: none;
`;
export const MenuItem = styled.li`
width: 100%;
display: flex;
@ -32,16 +40,13 @@ export const MenuItem = styled.li`
padding: 8px 8px 8px 14px;
cursor: pointer;
color: ${({ theme }) => theme.primary};
position: relative;
&:hover,
&:hover > span {
background: ${({ theme }) => theme.border};
}
& > svg {
fill: ${({ theme }) => theme.tertiary};
}
& > svg.red {
fill: ${({ theme }) => theme.redColor};
}
@ -58,29 +63,15 @@ export const MenuText = styled.span`
${textSmallStyles}
`;
const MenuBlock = styled.div`
width: 207px;
background: ${({ theme }) => theme.bodyBackgroundColor};
box-shadow: 0px 2px 4px rgba(0, 34, 51, 0.16),
0px 4px 12px rgba(0, 34, 51, 0.08);
border-radius: 8px;
padding: 8px 0;
position: absolute;
right: 8px;
top: calc(100% - 8px);
z-index: 2;
export const MenuSection = styled.div`
padding: 4px 0;
margin: 4px 0;
border-top: 1px solid ${({ theme }) => theme.inputColor};
border-bottom: 1px solid ${({ theme }) => theme.inputColor};
&.side {
top: 20px;
right: -90px;
}
&.narrow {
top: 20px;
right: 0;
&.channel {
padding: 0;
margin: 0;
border: none;
}
`;
const MenuList = styled.ul`
list-style: none;
`;

View File

@ -1,10 +1,11 @@
import React from "react";
import React, { useRef } from "react";
import styled from "styled-components";
import { useClickOutside } from "../../hooks/useClickOutside";
import { useContextMenu } from "../../hooks/useContextMenu";
import { copyImg } from "../../utils/copyImg";
import { downloadImg } from "../../utils/downloadImg";
import { CopySvg } from "../Icons/CopyIcon";
import { CopyIcon } from "../Icons/CopyIcon";
import { DownloadIcon } from "../Icons/DownloadIcon";
import { DropdownMenu, MenuItem, MenuText } from "./DropdownMenu";
@ -16,16 +17,21 @@ interface ImageMenuProps {
export const ImageMenu = ({ imageId }: ImageMenuProps) => {
const { showMenu, setShowMenu } = useContextMenu(imageId);
const ref = useRef(null);
useClickOutside(ref, () => setShowMenu(false));
return showMenu ? (
<ImageDropdown closeMenu={setShowMenu}>
<MenuItem onClick={() => copyImg(imageId)}>
<CopySvg height={16} width={16} /> <MenuText>Copy image</MenuText>
</MenuItem>
<MenuItem onClick={() => downloadImg(imageId)}>
<DownloadIcon height={16} width={16} />
<MenuText> Download image</MenuText>
</MenuItem>
</ImageDropdown>
<div ref={ref}>
<ImageDropdown>
<MenuItem onClick={() => copyImg(imageId)}>
<CopyIcon height={16} width={16} /> <MenuText>Copy image</MenuText>
</MenuItem>
<MenuItem onClick={() => downloadImg(imageId)}>
<DownloadIcon height={16} width={16} />
<MenuText> Download image</MenuText>
</MenuItem>
</ImageDropdown>
</div>
) : (
<></>
);

View File

@ -0,0 +1,64 @@
import React, { useCallback } from "react";
import styled from "styled-components";
import { DropdownMenu, MenuItem, MenuText } from "./DropdownMenu";
interface SubMenuProps {
setIsMuted: (val: boolean) => void;
className?: string;
}
export const MuteMenu = ({ setIsMuted, className }: SubMenuProps) => {
const muteChannel = useCallback(
(timeout: number) => {
setIsMuted(true);
const timer = setTimeout(() => setIsMuted(false), timeout * 6000000);
return () => {
clearTimeout(timer);
};
},
[setIsMuted]
);
return (
<MuteDropdown className={className}>
<MenuItem onClick={() => muteChannel(0.25)}>
<MenuText>For 15 min</MenuText>
</MenuItem>
<MenuItem onClick={() => muteChannel(1)}>
<MenuText>For 1 hour</MenuText>
</MenuItem>
<MenuItem onClick={() => muteChannel(8)}>
<MenuText>For 8 hours</MenuText>
</MenuItem>
<MenuItem onClick={() => muteChannel(24)}>
<MenuText>For 24 hours</MenuText>
</MenuItem>
<MenuItem onClick={() => setIsMuted(true)}>
<MenuText>Until I turn it back on</MenuText>
</MenuItem>
</MuteDropdown>
);
};
const MuteDropdown = styled(DropdownMenu)`
width: 176px;
top: 100%;
right: -60px;
z-index: 3;
&.side {
width: 176px;
top: -8px;
left: 100%;
right: unset;
}
&.narrow,
&.sideNarrow {
width: 176px;
top: 100%;
right: -16px;
z-index: 3;
}
`;

View File

@ -1,19 +1,19 @@
import React from "react";
import styled from "styled-components";
type AddContactSvgProps = {
type AddContactIconProps = {
width: number;
height: number;
className?: string;
};
export function AddContactSvg({
export function AddContactIcon({
width,
height,
className,
}: AddContactSvgProps) {
}: AddContactIconProps) {
return (
<svg
<Icon
width={width}
height={height}
viewBox="0 0 16 16"
@ -28,20 +28,10 @@ export function AddContactSvg({
/>
<path d="M8.07357 11.6671C8.35289 11.6707 8.60016 11.4677 8.62336 11.1893C8.64601 10.9175 8.44624 10.6758 8.17357 10.6689C8.11597 10.6674 8.05818 10.6667 8.00022 10.6667C5.97321 10.6667 4.15749 11.5713 2.93477 12.9989C2.77487 13.1856 2.79579 13.4622 2.96961 13.636C3.18548 13.8519 3.54282 13.8208 3.74449 13.5916C4.78296 12.4114 6.30462 11.6667 8.00022 11.6667C8.02471 11.6667 8.04916 11.6668 8.07357 11.6671Z" />
<path d="M10.167 11.1667C10.167 10.8905 10.3908 10.6667 10.667 10.6667H11.5003C11.6844 10.6667 11.8337 10.5174 11.8337 10.3333V9.5C11.8337 9.22386 12.0575 9 12.3337 9C12.6098 9 12.8337 9.22386 12.8337 9.5V10.3333C12.8337 10.5174 12.9829 10.6667 13.167 10.6667H14.0003C14.2765 10.6667 14.5003 10.8905 14.5003 11.1667C14.5003 11.4428 14.2765 11.6667 14.0003 11.6667H13.167C12.9829 11.6667 12.8337 11.8159 12.8337 12V12.8333C12.8337 13.1095 12.6098 13.3333 12.3337 13.3333C12.0575 13.3333 11.8337 13.1095 11.8337 12.8333V12C11.8337 11.8159 11.6844 11.6667 11.5003 11.6667H10.667C10.3908 11.6667 10.167 11.4428 10.167 11.1667Z" />
</svg>
</Icon>
);
}
export const AddContactIcon = () => {
return <Icon width={16} height={16} />;
};
const Icon = styled(AddContactSvg)`
& > path {
fill: ${({ theme }) => theme.tertiary};
}
&:hover > path {
fill: ${({ theme }) => theme.bodyBackgroundColor};
}
const Icon = styled.svg`
fill: ${({ theme }) => theme.tertiary};
`;

View File

@ -1,15 +1,15 @@
import React from "react";
import styled from "styled-components";
type CopySvgProps = {
type CopyIconProps = {
width: number;
height: number;
className?: string;
};
export function CopySvg({ width, height, className }: CopySvgProps) {
export function CopyIcon({ width, height, className }: CopyIconProps) {
return (
<svg
<Icon
width={width}
height={height}
viewBox="0 0 16 16"
@ -22,16 +22,10 @@ export function CopySvg({ width, height, className }: CopySvgProps) {
clipRule="evenodd"
d="M6.00016 4.00065C6.00016 2.52789 7.19407 1.33398 8.66683 1.33398H12.0002C13.4729 1.33398 14.6668 2.52789 14.6668 4.00065V7.33398C14.6668 8.80674 13.4729 10.0007 12.0002 10.0007H8.66683C7.19407 10.0007 6.00016 8.80674 6.00016 7.33398V4.00065ZM8.66683 2.33398H12.0002C12.9206 2.33398 13.6668 3.08018 13.6668 4.00065V7.33398C13.6668 8.25446 12.9206 9.00065 12.0002 9.00065H8.66683C7.74636 9.00065 7.00016 8.25446 7.00016 7.33398V4.00065C7.00016 3.08018 7.74636 2.33398 8.66683 2.33398Z"
/>
</svg>
</Icon>
);
}
export const CopyIcon = () => {
return <Icon width={16} height={16} />;
};
const Icon = styled(CopySvg)`
& > path {
fill: ${({ theme }) => theme.tertiary};
}
const Icon = styled.svg`
fill: ${({ theme }) => theme.tertiary};
`;

View File

@ -32,5 +32,5 @@ export function MembersSmallIcon({
}
const Icon = styled.svg`
fill: ${({ theme }) => theme.primary};
fill: ${({ theme }) => theme.tertiary};
`;

View File

@ -0,0 +1,28 @@
import React from "react";
import styled from "styled-components";
export const NextIcon = () => {
return (
<Icon
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.46967 5.46967C9.76256 5.17678 10.2374 5.17678 10.5303 5.46967L16.1768 11.1161C16.6649 11.6043 16.6649 12.3957 16.1768 12.8839L10.5303 18.5303C10.2374 18.8232 9.76256 18.8232 9.46967 18.5303C9.17678 18.2374 9.17678 17.7626 9.46967 17.4697L14.5858 12.3536C14.781 12.1583 14.781 11.8417 14.5858 11.6464L9.46967 6.53033C9.17678 6.23744 9.17678 5.76256 9.46967 5.46967Z"
/>
</Icon>
);
};
const Icon = styled.svg`
fill: ${({ theme }) => theme.primary};
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
`;

View File

@ -1,7 +1,8 @@
import React, { useState } from "react";
import React, { useRef, useState } from "react";
import styled from "styled-components";
import { useIdentity } from "../../contexts/identityProvider";
import { useClickOutside } from "../../hooks/useClickOutside";
import { Contact } from "../../models/Contact";
import { ContactMenu } from "../Form/ContactMenu";
import { IconBtn, UserAddress } from "../Messages/Styles";
@ -20,6 +21,9 @@ export function Member({ contact, isOnline, isYou, onClick }: MemberProps) {
const [showMenu, setShowMenu] = useState(false);
const ref = useRef(null);
useClickOutside(ref, () => setShowMenu(false));
return (
<MemberData onClick={onClick} className={`${isYou && "you"}`}>
<MemberIcon
@ -32,6 +36,7 @@ export function Member({ contact, isOnline, isYou, onClick }: MemberProps) {
onClick={() => {
if (identity) setShowMenu((e) => !e);
}}
ref={ref}
>
{showMenu && <ContactMenu id={contact.id} setShowMenu={setShowMenu} />}
<UserLogo

View File

@ -1,11 +1,12 @@
import { utils } from "@waku/status-communities/dist/cjs";
import { BaseEmoji } from "emoji-mart";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components";
import { useActivities } from "../../contexts/activityProvider";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useClickOutside } from "../../hooks/useClickOutside";
import { Reply } from "../../hooks/useReply";
import { ChannelData } from "../../models/ChannelData";
import { ChatMessage } from "../../models/ChatMessage";
@ -100,6 +101,9 @@ export function UiMessage({
]);
}, [mentioned, message, quote]);
const ref = useRef(null);
useClickOutside(ref, () => setShowMenu(false));
return (
<MessageOuterWrapper>
{(idx === 0 || !equalDate(prevMessage.date, message.date)) && (
@ -117,6 +121,7 @@ export function UiMessage({
if (identity) setShowMenu((e) => !e);
}}
disabled={!identity}
ref={ref}
>
{showMenu && (
<ContactMenu id={message.sender} setShowMenu={setShowMenu} />
@ -139,6 +144,7 @@ export function UiMessage({
if (identity) setShowMenu((e) => !e);
}}
disabled={!identity}
ref={ref}
>
<UserName>
{" "}

View File

@ -16,7 +16,7 @@ import {
NameInputWrapper,
} from "../Form/inputStyles";
import { ClearSvgFull } from "../Icons/ClearIconFull";
import { CopySvg } from "../Icons/CopyIcon";
import { CopyIcon } from "../Icons/CopyIcon";
import { EditIcon } from "../Icons/EditIcon";
import { LeftIcon } from "../Icons/LeftIcon";
import { UntrustworthIcon } from "../Icons/UntrustworthIcon";
@ -151,7 +151,7 @@ export const ProfileModal = () => {
</UserAddress>
<CopyButton onClick={() => copy(id)}>
<CopySvg width={24} height={24} />
<CopyIcon width={24} height={24} />
</CopyButton>
</>
)}