diff --git a/packages/react-chat/src/components/ActivityCenter/ActivityMessage.tsx b/packages/react-chat/src/components/ActivityCenter/ActivityMessage.tsx index 617a7eaa..368cc26d 100644 --- a/packages/react-chat/src/components/ActivityCenter/ActivityMessage.tsx +++ b/packages/react-chat/src/components/ActivityCenter/ActivityMessage.tsx @@ -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 ( @@ -214,6 +218,7 @@ export function ActivityMessage({ onClick={() => { setShowMenu((e) => !e); }} + ref={ref} > {showMenu && ( diff --git a/packages/react-chat/src/components/Channels/Channel.tsx b/packages/react-chat/src/components/Channels/Channel.tsx index 8e71b19a..ebe1a611 100644 --- a/packages/react-chat/src/components/Channels/Channel.tsx +++ b/packages/react-chat/src/components/Channels/Channel.tsx @@ -98,7 +98,7 @@ export function Channel({ )} diff --git a/packages/react-chat/src/components/Chat/ChatMessageContent.tsx b/packages/react-chat/src/components/Chat/ChatMessageContent.tsx index cc4d49c1..9f44a9e8 100644 --- a/packages/react-chat/src/components/Chat/ChatMessageContent.tsx +++ b/packages/react-chat/src/components/Chat/ChatMessageContent.tsx @@ -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 ( - setShowMenu(!showMenu)} className={className}> + setShowMenu(!showMenu)} + className={className} + ref={ref} + > {`@${contact?.customName ?? contact.trueName}`} {showMenu && } diff --git a/packages/react-chat/src/components/Chat/ChatTopbar.tsx b/packages/react-chat/src/components/Chat/ChatTopbar.tsx index 23af853d..bc2090fc 100644 --- a/packages/react-chat/src/components/Chat/ChatTopbar.tsx +++ b/packages/react-chat/src/components/Chat/ChatTopbar.tsx @@ -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 ; } @@ -105,21 +109,24 @@ export function ChatTopbar({ )} - setShowChannelMenu(!showChannelMenu)}> - - +
+ setShowChannelMenu(!showChannelMenu)}> + + {showChannelMenu && ( + switchShowState(ChatBodyState.Members)} + setShowChannelMenu={setShowChannelMenu} + setEditGroup={setEditGroup} + className={`${narrow && "narrow"}`} + /> + )} + +
{!narrow && } {loadingMessenger && } - {showChannelMenu && ( - switchShowState(ChatBodyState.Members)} - setShowChannelMenu={setShowChannelMenu} - setEditGroup={setEditGroup} - /> - )} ); } @@ -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}; diff --git a/packages/react-chat/src/components/Form/ChannelMenu.tsx b/packages/react-chat/src/components/Form/ChannelMenu.tsx index 08180d35..12c76573 100644 --- a/packages/react-chat/src/components/Form/ChannelMenu.tsx +++ b/packages/react-chat/src/components/Form/ChannelMenu.tsx @@ -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 ( - + {narrow && !className && ( { @@ -102,15 +113,27 @@ export const ChannelMenu = ({ { - channel.isMuted = !channel.isMuted; - setShowMenu(false); + if (isMuted) { + setIsMuted(false); + setShowMenu(false); + } + }} + onMouseEnter={() => { + if (!isMuted) setShowSubmenu(true); + }} + onMouseLeave={() => { + if (!isMuted) setShowSubmenu(false); }} > + {!isMuted && } - {(channel.isMuted ? "Unmute" : "Mute") + + {(isMuted ? "Unmute" : "Mute") + (channel.type === "group" ? " Group" : " Chat")} + {!isMuted && showSubmenu && ( + + )} clearNotifications(channel.id)}> @@ -139,22 +162,25 @@ export const ChannelMenu = ({ )} - + ); } 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; } `; diff --git a/packages/react-chat/src/components/Form/ContactMenu.tsx b/packages/react-chat/src/components/Form/ContactMenu.tsx index c3126e62..39390c77 100644 --- a/packages/react-chat/src/components/Form/ContactMenu.tsx +++ b/packages/react-chat/src/components/Form/ContactMenu.tsx @@ -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 ( - + @@ -68,7 +68,7 @@ export function ContactMenu({ id, setShowMenu }: ContactMenuProps) { setModal({ id, requestState: true }); }} > - + Send Contact Request )} diff --git a/packages/react-chat/src/components/Form/DropdownMenu.tsx b/packages/react-chat/src/components/Form/DropdownMenu.tsx index 9c0537c1..10aa61ee 100644 --- a/packages/react-chat/src/components/Form/DropdownMenu.tsx +++ b/packages/react-chat/src/components/Form/DropdownMenu.tsx @@ -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 ( - + {children} ); } +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; -`; diff --git a/packages/react-chat/src/components/Form/ImageMenu.tsx b/packages/react-chat/src/components/Form/ImageMenu.tsx index 0931202b..3aba2e2c 100644 --- a/packages/react-chat/src/components/Form/ImageMenu.tsx +++ b/packages/react-chat/src/components/Form/ImageMenu.tsx @@ -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 ? ( - - copyImg(imageId)}> - Copy image - - downloadImg(imageId)}> - - Download image - - +
+ + copyImg(imageId)}> + Copy image + + downloadImg(imageId)}> + + Download image + + +
) : ( <> ); diff --git a/packages/react-chat/src/components/Form/MuteMenu.tsx b/packages/react-chat/src/components/Form/MuteMenu.tsx new file mode 100644 index 00000000..9efd1790 --- /dev/null +++ b/packages/react-chat/src/components/Form/MuteMenu.tsx @@ -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 ( + + muteChannel(0.25)}> + For 15 min + + muteChannel(1)}> + For 1 hour + + muteChannel(8)}> + For 8 hours + + muteChannel(24)}> + For 24 hours + + setIsMuted(true)}> + Until I turn it back on + + + ); +}; + +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; + } +`; diff --git a/packages/react-chat/src/components/Icons/AddContactIcon.tsx b/packages/react-chat/src/components/Icons/AddContactIcon.tsx index 425c80e3..dd75b0c2 100644 --- a/packages/react-chat/src/components/Icons/AddContactIcon.tsx +++ b/packages/react-chat/src/components/Icons/AddContactIcon.tsx @@ -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 ( - - + ); } -export const AddContactIcon = () => { - return ; -}; - -const Icon = styled(AddContactSvg)` - & > path { - fill: ${({ theme }) => theme.tertiary}; - } - - &:hover > path { - fill: ${({ theme }) => theme.bodyBackgroundColor}; - } +const Icon = styled.svg` + fill: ${({ theme }) => theme.tertiary}; `; diff --git a/packages/react-chat/src/components/Icons/CopyIcon.tsx b/packages/react-chat/src/components/Icons/CopyIcon.tsx index 1f4a901b..72bba1d6 100644 --- a/packages/react-chat/src/components/Icons/CopyIcon.tsx +++ b/packages/react-chat/src/components/Icons/CopyIcon.tsx @@ -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 ( - - + ); } -export const CopyIcon = () => { - return ; -}; - -const Icon = styled(CopySvg)` - & > path { - fill: ${({ theme }) => theme.tertiary}; - } +const Icon = styled.svg` + fill: ${({ theme }) => theme.tertiary}; `; diff --git a/packages/react-chat/src/components/Icons/MembersSmallIcon.tsx b/packages/react-chat/src/components/Icons/MembersSmallIcon.tsx index 0fb510eb..f5b574ac 100644 --- a/packages/react-chat/src/components/Icons/MembersSmallIcon.tsx +++ b/packages/react-chat/src/components/Icons/MembersSmallIcon.tsx @@ -32,5 +32,5 @@ export function MembersSmallIcon({ } const Icon = styled.svg` - fill: ${({ theme }) => theme.primary}; + fill: ${({ theme }) => theme.tertiary}; `; diff --git a/packages/react-chat/src/components/Icons/NextIcon.tsx b/packages/react-chat/src/components/Icons/NextIcon.tsx new file mode 100644 index 00000000..88be547c --- /dev/null +++ b/packages/react-chat/src/components/Icons/NextIcon.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import styled from "styled-components"; + +export const NextIcon = () => { + return ( + + + + ); +}; + +const Icon = styled.svg` + fill: ${({ theme }) => theme.primary}; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); +`; diff --git a/packages/react-chat/src/components/Members/Member.tsx b/packages/react-chat/src/components/Members/Member.tsx index 2b3a091e..cd816449 100644 --- a/packages/react-chat/src/components/Members/Member.tsx +++ b/packages/react-chat/src/components/Members/Member.tsx @@ -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 ( { if (identity) setShowMenu((e) => !e); }} + ref={ref} > {showMenu && } setShowMenu(false)); + return ( {(idx === 0 || !equalDate(prevMessage.date, message.date)) && ( @@ -117,6 +121,7 @@ export function UiMessage({ if (identity) setShowMenu((e) => !e); }} disabled={!identity} + ref={ref} > {showMenu && ( @@ -139,6 +144,7 @@ export function UiMessage({ if (identity) setShowMenu((e) => !e); }} disabled={!identity} + ref={ref} > {" "} diff --git a/packages/react-chat/src/components/Modals/ProfileModal.tsx b/packages/react-chat/src/components/Modals/ProfileModal.tsx index 1ea1ed5d..858df849 100644 --- a/packages/react-chat/src/components/Modals/ProfileModal.tsx +++ b/packages/react-chat/src/components/Modals/ProfileModal.tsx @@ -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 = () => { copy(id)}> - + )}