From 54536c1b0308cbd330f2d2ead23c5db055c0641d Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Sat, 25 Dec 2021 19:04:55 -0600 Subject: [PATCH] useContextMenu hook --- .../context-menu-guild-title.tsx | 9 ++++-- .../context-menus/context-menu-image.tsx | 8 ++--- .../lists/components/message-element.tsx | 2 +- .../elements/overlays/overlay-image.tsx | 25 +++++++-------- .../webapp/elements/require/react-helper.ts | 31 ++++++++++++++++++- .../elements/sections/connection-info.tsx | 19 ++++++------ .../webapp/elements/sections/guild-title.tsx | 19 +++--------- 7 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/client/webapp/elements/context-menus/context-menu-guild-title.tsx b/src/client/webapp/elements/context-menus/context-menu-guild-title.tsx index 6f7189f..11e67a4 100644 --- a/src/client/webapp/elements/context-menus/context-menu-guild-title.tsx +++ b/src/client/webapp/elements/context-menus/context-menu-guild-title.tsx @@ -4,11 +4,10 @@ import CombinedGuild from '../../guild-combined'; import ChannelOverlay from '../overlays/overlay-channel'; import GuildSettingsOverlay from '../overlays/overlay-guild-settings'; import BaseElements from '../require/base-elements'; -import ElementsUtil, { IAlignment } from '../require/elements-util'; +import ElementsUtil from '../require/elements-util'; import ContextMenu from './components/context-menu'; export interface GuildTitleContextMenuProps { - alignment: IAlignment; close: () => void; relativeToRef: RefObject guild: CombinedGuild; @@ -17,7 +16,7 @@ export interface GuildTitleContextMenuProps { } const GuildTitleContextMenu: FC = (props: GuildTitleContextMenuProps) => { - const { alignment, close, relativeToRef, guild, guildMeta, selfMember } = props; + const { close, relativeToRef, guild, guildMeta, selfMember } = props; const openGuildSettings = useCallback(() => { close(); @@ -51,6 +50,10 @@ const GuildTitleContextMenu: FC = (props: GuildTitle if (guildSettingsElement === null && createChannelElement === null) return null; + const alignment = useMemo(() => { + return { top: 'bottom', centerX: 'centerX' } + }, []); + return (
diff --git a/src/client/webapp/elements/context-menus/context-menu-image.tsx b/src/client/webapp/elements/context-menus/context-menu-image.tsx index 4bd70f1..087fb70 100644 --- a/src/client/webapp/elements/context-menus/context-menu-image.tsx +++ b/src/client/webapp/elements/context-menus/context-menu-image.tsx @@ -1,13 +1,11 @@ -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import * as electron from 'electron'; import ReactHelper from '../require/react-helper'; import ContextMenu from './components/context-menu'; import * as FileType from 'file-type'; import sharp from 'sharp'; -import { IAlignment } from '../require/elements-util'; export interface ImageContextMenuProps { - alignment: IAlignment; relativeToPos: { x: number, y: number }; close: () => void; resourceName: string; @@ -16,7 +14,7 @@ export interface ImageContextMenuProps { } const ImageContextMenu: FC = (props: ImageContextMenuProps) => { - const { alignment, relativeToPos, close, resourceName, resourceBuff, isPreview } = props; + const { relativeToPos, close, resourceName, resourceBuff, isPreview } = props; const previewText = isPreview ? ' Preview' : ''; @@ -56,6 +54,8 @@ const ImageContextMenu: FC = (props: ImageContextMenuProp } ); + const alignment = useMemo(() => ({ top: 'centerY', left: 'centerX' }), []); + return (
diff --git a/src/client/webapp/elements/lists/components/message-element.tsx b/src/client/webapp/elements/lists/components/message-element.tsx index 0ed9642..42dd3e0 100644 --- a/src/client/webapp/elements/lists/components/message-element.tsx +++ b/src/client/webapp/elements/lists/components/message-element.tsx @@ -53,7 +53,7 @@ const PreviewImageElement: FC = (props: PreviewImageEl const [ imgSrc ] = GuildSubscriptions.useSoftImageSrcResourceSubscription(guild, resourcePreviewId); const openImageOverlay = useCallback(() => { - // Note: document here isn't 100% guaranteed but we should be getting rid of this eventually anyway + // Note: document here isn't 100% guaranteed (I think) but we should be getting rid of this eventually anyway ElementsUtil.presentReactOverlay(document, ); }, [ guild, resourceId, resourceName ]); diff --git a/src/client/webapp/elements/overlays/overlay-image.tsx b/src/client/webapp/elements/overlays/overlay-image.tsx index 682474e..c4fc55a 100644 --- a/src/client/webapp/elements/overlays/overlay-image.tsx +++ b/src/client/webapp/elements/overlays/overlay-image.tsx @@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); -import React, { FC, useCallback, useMemo, useState, MouseEvent, useRef } from 'react'; +import React, { FC, useMemo, useState, useRef, MouseEvent, useCallback } from 'react'; import CombinedGuild from '../../guild-combined'; import ElementsUtil from '../require/elements-util'; import DownloadButton from '../components/button-download'; @@ -31,33 +31,30 @@ const ImageOverlay: FC = (props: ImageOverlayProps) => { './img/loading.svg', [ guild, resource ] ); - - const [ contextMenuOpen, setContextMenuOpen ] = useState(false); - const alignment = useMemo(() => ({ top: 'centerY', left: 'centerX' }), []); + const [ relativeToPos, setRelativeToPos ] = useState<{ x: number, y: number }>({ x: 0, y: 0 }); - const contextMenu = useMemo(() => { + const [ contextMenu, toggleContextMenu ] = ReactHelper.useContextMenu((close: () => void) => { if (!resource) return null; return ( { setContextMenuOpen(false); }} + relativeToPos={relativeToPos} close={close} resourceName={resourceName} resourceBuff={resource.data} isPreview={false} /> ); - }, [ resource, alignment, relativeToPos, resourceName ]); + }, [ resource, relativeToPos, resourceName ]); - const toggleContextMenu = useCallback((e: MouseEvent) => { - LOG.debug('toggle context menu'); - setRelativeToPos({ x: e.clientX, y: e.clientY }); - setContextMenuOpen(oldContextMenuOpen => !!contextMenu && !oldContextMenuOpen); - }, [ contextMenu ]); + const onContextMenu = useCallback((event: MouseEvent) => { + setRelativeToPos({ x: event.clientX, y: event.clientY }); + toggleContextMenu(); + }, [ toggleContextMenu ]); const sizeText = useMemo(() => resource ? ElementsUtil.humanSize(resource.data.length) : 'Loading Size...', [ resource ]); return (
- {resourceName} + {resourceName}
{resourceName}
@@ -67,7 +64,7 @@ const ImageOverlay: FC = (props: ImageOverlayProps) => { downloadBuff={resource?.data} downloadBuffErr={!!resourceError} resourceName={resourceName}>
- {contextMenuOpen ? contextMenu : null} + {contextMenu}
); diff --git a/src/client/webapp/elements/require/react-helper.ts b/src/client/webapp/elements/require/react-helper.ts index d609c69..2c629d0 100644 --- a/src/client/webapp/elements/require/react-helper.ts +++ b/src/client/webapp/elements/require/react-helper.ts @@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); -import { DependencyList, Dispatch, MutableRefObject, RefObject, SetStateAction, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { DependencyList, Dispatch, MutableRefObject, ReactNode, RefObject, SetStateAction, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactDOMServer from "react-dom/server"; import { ShouldNeverHappenError } from "../../data-types"; import Util from '../../util'; @@ -395,4 +395,33 @@ export default class ReactHelper { } }, [ handleMouseUp ]); } + + static useContextMenu( + createContextMenu: (close: () => void) => ReactNode, + createContextMenuDeps: DependencyList + ): [ + contextMenu: ReactNode, + toggle: () => void, + close: () => void, + open: () => void, + isOpen: boolean + ] { + const [ isOpen, setIsOpen ] = useState(false); + + const close = useCallback(() => { + setIsOpen(false); + }, []); + const contextMenu = useMemo(() => { + return createContextMenu(close); + }, createContextMenuDeps); + + const toggle = useCallback(() => { + setIsOpen(oldIsOpen => !!contextMenu && !oldIsOpen); + }, [ contextMenu ]); + const open = useCallback(() => { + setIsOpen(!!contextMenu); + }, [ contextMenu ]); + + return [ isOpen ? contextMenu : null, toggle, close, open, isOpen ]; + } } \ No newline at end of file diff --git a/src/client/webapp/elements/sections/connection-info.tsx b/src/client/webapp/elements/sections/connection-info.tsx index 51dbcc4..1416a0b 100644 --- a/src/client/webapp/elements/sections/connection-info.tsx +++ b/src/client/webapp/elements/sections/connection-info.tsx @@ -3,12 +3,13 @@ const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); -import React, { FC, useCallback, useMemo, useRef, useState } from 'react'; +import React, { FC, useMemo, useRef } from 'react'; import { Member } from '../../data-types'; import CombinedGuild from '../../guild-combined'; import MemberElement, { DummyMember } from '../lists/components/member-element'; import GuildSubscriptions from '../require/guild-subscriptions'; import ConnectionInfoContextMenu from '../context-menus/context-menu-connection-info'; +import ReactHelper from '../require/react-helper'; export interface ConnectionInfoProps { guild: CombinedGuild; @@ -19,8 +20,6 @@ const ConnectionInfo: FC = (props: ConnectionInfoProps) => const rootRef = useRef(null); - const [ contextMenuOpen, setContextMenuOpen ] = useState(false); - const [ selfMember ] = GuildSubscriptions.useSelfMemberSubscription(guild); const displayMember = useMemo((): Member | DummyMember => { @@ -36,19 +35,19 @@ const ConnectionInfo: FC = (props: ConnectionInfoProps) => return selfMember; }, [ selfMember ]); - const contextMenu = useMemo(() => { + const [ contextMenu, toggleContextMenu ] = ReactHelper.useContextMenu((close: () => void) => { if (!selfMember) return null; - return { setContextMenuOpen(false); }} /> + return ( + + ); }, [ guild, selfMember, rootRef ]); - const toggleContextMenu = useCallback(() => { - setContextMenuOpen(oldContextMenuOpen => !!contextMenu && !oldContextMenuOpen); - }, [ contextMenu ]); - return (
- {contextMenuOpen ? contextMenu : null} + {contextMenu}
); } diff --git a/src/client/webapp/elements/sections/guild-title.tsx b/src/client/webapp/elements/sections/guild-title.tsx index afea9e4..79d335b 100644 --- a/src/client/webapp/elements/sections/guild-title.tsx +++ b/src/client/webapp/elements/sections/guild-title.tsx @@ -1,7 +1,8 @@ -import React, { FC, useCallback, useMemo, useRef, useState } from 'react'; +import React, { FC, useRef } from 'react'; import CombinedGuild from '../../guild-combined'; import GuildTitleContextMenu from '../context-menus/context-menu-guild-title'; import GuildSubscriptions from '../require/guild-subscriptions'; +import ReactHelper from '../require/react-helper'; export interface GuildTitleProps { guild: CombinedGuild; @@ -12,37 +13,27 @@ const GuildTitle: FC = (props: GuildTitleProps) => { const rootRef = useRef(null); - const [ contextMenuOpen, setContextMenuOpen ] = useState(false); - // TODO: Handle fetch error const [ guildMeta, fetchError ] = GuildSubscriptions.useGuildMetadataSubscription(guild); const [ selfMember ] = GuildSubscriptions.useSelfMemberSubscription(guild); - const alignment = useMemo(() => { - return { top: 'bottom', centerX: 'centerX' } - }, []); - - const contextMenu = useMemo(() => { + const [ contextMenu, toggleContextMenu ] = ReactHelper.useContextMenu((close: () => void) => { if (!guildMeta) return null; if (!selfMember) return null; return ( { setContextMenuOpen(false); }} + relativeToRef={rootRef} close={close} guild={guild} guildMeta={guildMeta} selfMember={selfMember} /> ); }, [ guild, guildMeta, selfMember, rootRef ]); - const toggleContextMenu = useCallback(() => { - setContextMenuOpen(oldContextMenuOpen => !!contextMenu && !oldContextMenuOpen); - }, [ contextMenu ]); - return (
{guildMeta?.name ?? null}
- {contextMenuOpen ? contextMenu : null} + {contextMenu}
); }