diff --git a/package-lock.json b/package-lock.json index 97b0d2d..4484e2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "moment": "^2.29.1", "pg": "^8.7.1", "react-contenteditable": "^3.3.6", + "recoil": "^0.5.2", "sass": "^1.43.4", "sharp": "^0.29.2", "socket.io": "^4.3.1", @@ -3770,6 +3771,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=" + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -6289,6 +6295,25 @@ "node": ">=8.10.0" } }, + "node_modules/recoil": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.5.2.tgz", + "integrity": "sha512-Edibzpu3dbUMLy6QRg73WL8dvMl9Xqhp+kU+f2sJtXxsaXvAlxU/GcnDE8HXPkprXrhHF2e6SZozptNvjNF5fw==", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -10679,6 +10704,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, + "hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=" + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -12613,6 +12643,14 @@ "picomatch": "^2.2.1" } }, + "recoil": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.5.2.tgz", + "integrity": "sha512-Edibzpu3dbUMLy6QRg73WL8dvMl9Xqhp+kU+f2sJtXxsaXvAlxU/GcnDE8HXPkprXrhHF2e6SZozptNvjNF5fw==", + "requires": { + "hamt_plus": "1.0.2" + } + }, "regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", diff --git a/package.json b/package.json index e5200a5..200bdde 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "moment": "^2.29.1", "pg": "^8.7.1", "react-contenteditable": "^3.3.6", + "recoil": "^0.5.2", "sass": "^1.43.4", "sharp": "^0.29.2", "socket.io": "^4.3.1", diff --git a/src/client/webapp/elements/atoms.ts b/src/client/webapp/elements/atoms.ts new file mode 100644 index 0000000..e5266e8 --- /dev/null +++ b/src/client/webapp/elements/atoms.ts @@ -0,0 +1,91 @@ +import { ReactNode } from 'react'; +import { atom, selector } from 'recoil'; +import { Channel, GuildMetadata, Member, Message } from '../data-types'; +import CombinedGuild from '../guild-combined'; + +export interface GuildWithValue { + guild: CombinedGuild; + value: T; +} + +export interface GuildWithErrorableValue { + guild: CombinedGuild; + value: T | null; + valueError: unknown | null; +} + +export interface ChannelWithErrorableValue { + channel: Channel; + value: T | null; + valueError: unknown | null; +} + +export const overlayState = atom({ + key: 'overlayState', + default: null +}); + +export const guildsState = atom[] | null>({ + key: 'guildsState', + default: null +}); + +export const selectedGuildIdState = atom({ + key: 'selectedGuildIdState', + default: null +}); + +export const selectedGuildWithMetaState = selector | null>({ + key: 'selectedGuildWithMetaState', + get: ({ get }) => { + const guildsWithMeta = get(guildsState); + if (guildsWithMeta === null) return null; + + const guildId = get(selectedGuildIdState); + if (guildId === null) return null; + + return guildsWithMeta.find(guildWithMeta => guildWithMeta.guild.id === guildId) ?? null; + } +}); + +export const selectedGuildState = selector({ + key: 'selectedGuildState', + get: ({ get }) => { + const guildWithMeta = get(selectedGuildWithMetaState); + if (guildWithMeta === null) return null; + + return guildWithMeta.guild; + } +}); + +export const selectedGuildMembersState = atom | null>({ + key: 'selectedGuildMembersState', + default: null +}); + +export const selectedGuildWithChannelsState = atom | null>({ + key: 'selectedGuildChannelsState', + default: null +}); + +export const selectedGuildWithActiveChannelIdState = atom | null>({ + key: 'selectedGuildWithActiveChannelIdState', + default: null +}); + + +export const selectedGuildWithActiveChannelMessagesState = atom | null> | null>({ + key: 'selectedGuildWithActiveChannelMessagesState', + default: null +}); + +export const selectedGuildWithActiveChannelState = selector | null>({ + key: 'selectedGuildActiveChannelState', + get: ({ get }) => { + const guildWithChannelMessages = get(selectedGuildWithActiveChannelMessagesState); + if (guildWithChannelMessages === null || guildWithChannelMessages.value === null) return null; + + return { guild: guildWithChannelMessages.guild, value: guildWithChannelMessages.value.channel }; + } +}); + diff --git a/src/client/webapp/elements/components/overlay.tsx b/src/client/webapp/elements/components/overlay.tsx index f909719..316114d 100644 --- a/src/client/webapp/elements/components/overlay.tsx +++ b/src/client/webapp/elements/components/overlay.tsx @@ -3,33 +3,36 @@ const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); -import React, { FC, RefObject, useCallback, useEffect } from "react"; -import { useCloseWhenEscapeOrClickedOrContextOutsideEffect } from '../require/react-helper'; +import React, { FC, ReactNode, RefObject, useCallback, useEffect } from "react"; +import { useActionWhenEscapeOrClickedOrContextOutsideEffect } from '../require/react-helper'; +import { overlayState } from '../atoms'; +import { useSetRecoilState } from 'recoil'; interface OverlayProps { childRootRef?: RefObject; // clicks outside this ref will close the overlay - close: () => void; children: React.ReactNode; } const Overlay: FC = (props: OverlayProps) => { - const { childRootRef, close, children } = props; + const { childRootRef, children } = props; + + const setOverlay = useSetRecoilState(overlayState); if (childRootRef) { - useCloseWhenEscapeOrClickedOrContextOutsideEffect(childRootRef, close); + useActionWhenEscapeOrClickedOrContextOutsideEffect(childRootRef, () => setOverlay(null)); } const keyDownHandler = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape') { - close() + setOverlay(null) } - }, [ close ]); + }, [ setOverlay ]); useEffect(() => { window.addEventListener('keydown', keyDownHandler); return () => { window.removeEventListener('keydown', keyDownHandler); } - }, []); + }, [ keyDownHandler ]); return
{children}
}; diff --git a/src/client/webapp/elements/contexts/components/context-menu.tsx b/src/client/webapp/elements/contexts/components/context-menu.tsx index 05638a3..0f2a8b6 100644 --- a/src/client/webapp/elements/contexts/components/context-menu.tsx +++ b/src/client/webapp/elements/contexts/components/context-menu.tsx @@ -1,6 +1,6 @@ import React, { DependencyList, FC, ReactNode, RefObject, useRef } from 'react' import { IAlignment } from '../../require/elements-util'; -import { useCloseWhenEscapeOrClickedOrContextOutsideEffect } from '../../require/react-helper'; +import { useActionWhenEscapeOrClickedOrContextOutsideEffect } from '../../require/react-helper'; import Context from './context'; export interface ContextMenuProps { @@ -18,7 +18,7 @@ const ContextMenu: FC = (props: ContextMenuProps) => { const rootRef = useRef(null); - useCloseWhenEscapeOrClickedOrContextOutsideEffect(rootRef, close); + useActionWhenEscapeOrClickedOrContextOutsideEffect(rootRef, close); return ( ; relativeToRef: RefObject; close: () => void; - setOverlay: Dispatch>; } const ConnectionInfoContextMenu: FC = (props: ConnectionInfoContextMenuProps) => { - const { guild, selfMemberResult, relativeToRef, close, setOverlay } = props; + const { guild, selfMemberResult, relativeToRef, close } = props; + + const setOverlay = useSetRecoilState(overlayState) const setSelfStatus = useCallback(async (status: string) => { if (selfMemberResult.value.status !== status) { @@ -36,7 +39,7 @@ const ConnectionInfoContextMenu: FC = (props: Co const openPersonalize = useCallback(() => { close(); - setOverlay( setOverlay(null)} />); + setOverlay(); }, [ guild, selfMemberResult, close ]); const alignment = useMemo(() => { diff --git a/src/client/webapp/elements/contexts/context-menu-guild-title.tsx b/src/client/webapp/elements/contexts/context-menu-guild-title.tsx index 32d45ea..87d22a5 100644 --- a/src/client/webapp/elements/contexts/context-menu-guild-title.tsx +++ b/src/client/webapp/elements/contexts/context-menu-guild-title.tsx @@ -1,6 +1,8 @@ -import React, { Dispatch, FC, ReactNode, RefObject, SetStateAction, useCallback, useMemo } from 'react'; +import React, { FC, ReactNode, RefObject, useCallback, useMemo } from 'react'; +import { useSetRecoilState } from 'recoil'; import { GuildMetadata, Member } from '../../data-types'; import CombinedGuild from '../../guild-combined'; +import { overlayState } from '../atoms'; import ChannelOverlay from '../overlays/overlay-channel'; import GuildSettingsOverlay from '../overlays/overlay-guild-settings'; import BaseElements from '../require/base-elements'; @@ -13,20 +15,21 @@ export interface GuildTitleContextMenuProps { guild: CombinedGuild; guildMetaResult: SubscriptionResult; selfMember: Member; - setOverlay: Dispatch>; } const GuildTitleContextMenu: FC = (props: GuildTitleContextMenuProps) => { - const { close, relativeToRef, guild, guildMetaResult, selfMember, setOverlay } = props; + const { close, relativeToRef, guild, guildMetaResult, selfMember } = props; + + const setOverlay = useSetRecoilState(overlayState); const openGuildSettings = useCallback(() => { close(); - setOverlay( setOverlay(null)} />); + setOverlay(); }, [ guild, guildMetaResult, close ]); const openCreateChannel = useCallback(() => { close(); - setOverlay( setOverlay(null)} />); + setOverlay(); }, [ guild, close ]); const guildSettingsElement = useMemo(() => { diff --git a/src/client/webapp/elements/lists/channel-list.tsx b/src/client/webapp/elements/lists/channel-list.tsx index d0a92a4..ab10e74 100644 --- a/src/client/webapp/elements/lists/channel-list.tsx +++ b/src/client/webapp/elements/lists/channel-list.tsx @@ -10,11 +10,10 @@ export interface ChannelListProps { channelsFetchError: unknown | null; activeChannel: Channel | null; setActiveChannel: Dispatch>; - setOverlay: Dispatch>; } const ChannelList: FC = (props: ChannelListProps) => { - const { guild, selfMember, channels, channelsFetchError, activeChannel, setActiveChannel, setOverlay } = props; + const { guild, selfMember, channels, channelsFetchError, activeChannel, setActiveChannel } = props; const hasModifyPrivilege = selfMember && selfMember.privileges.includes('modify_channels'); @@ -35,7 +34,6 @@ const ChannelList: FC = (props: ChannelListProps) => { selfMember={selfMember} channel={channel} activeChannel={activeChannel} setActiveChannel={() => { setActiveChannel(channel); }} - setOverlay={setOverlay} /> )); }, [ selfMember, channelsFetchError, channels, guild, selfMember, activeChannel ]); diff --git a/src/client/webapp/elements/lists/components/channel-element.tsx b/src/client/webapp/elements/lists/components/channel-element.tsx index 46497e9..ca681bf 100644 --- a/src/client/webapp/elements/lists/components/channel-element.tsx +++ b/src/client/webapp/elements/lists/components/channel-element.tsx @@ -10,6 +10,8 @@ import ChannelOverlay from '../../overlays/overlay-channel'; import BaseElements from '../../require/base-elements'; import { useContextHover } from '../../require/react-helper'; import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic'; +import { overlayState } from '../../atoms'; +import { useSetRecoilState } from 'recoil'; export interface ChannelElementProps { guild: CombinedGuild; @@ -17,14 +19,15 @@ export interface ChannelElementProps { selfMember: Member; // Note: Expected to use this later since it may not be best to have css-based hiding activeChannel: Channel | null; setActiveChannel: Dispatch>; - setOverlay: Dispatch>; } const ChannelElement: FC = (props: ChannelElementProps) => { - const { guild, channel, selfMember, activeChannel, setActiveChannel, setOverlay } = props; + const { guild, channel, selfMember, activeChannel, setActiveChannel } = props; const modifyRef = useRef(null); + const setOverlay = useSetRecoilState(overlayState); + const baseClassName = activeChannel?.id === channel.id ? 'channel text active' : 'channel text'; const [ modifyContextHover, modifyMouseEnterCallable, modifyMouseLeaveCallable ] = useContextHover( @@ -46,7 +49,7 @@ const ChannelElement: FC = (props: ChannelElementProps) => }, [ modifyRef, channel ]); const launchModify = useCallback(() => { - setOverlay( { setOverlay(null); }} />); + setOverlay(); }, [ guild, channel ]); return ( diff --git a/src/client/webapp/elements/lists/components/message-element.tsx b/src/client/webapp/elements/lists/components/message-element.tsx index 0f7be58..387a899 100644 --- a/src/client/webapp/elements/lists/components/message-element.tsx +++ b/src/client/webapp/elements/lists/components/message-element.tsx @@ -1,7 +1,9 @@ import moment from 'moment'; -import React, { Dispatch, FC, ReactNode, SetStateAction, useCallback, useMemo } from 'react'; +import React, { FC, ReactNode, useCallback, useMemo } from 'react'; +import { useSetRecoilState } from 'recoil'; import { Member, Message } from '../../../data-types'; import CombinedGuild from '../../../guild-combined'; +import { overlayState } from '../../atoms'; import ImageContextMenu from '../../contexts/context-menu-image'; import ImageOverlay from '../../overlays/overlay-image'; import ElementsUtil, { IAlignment } from '../../require/elements-util'; @@ -47,11 +49,12 @@ interface PreviewImageElementProps { resourceId: string; resourceName: string; resourceIdGuild: CombinedGuild; - setOverlay: Dispatch>; } const PreviewImageElement: FC = (props: PreviewImageElementProps) => { - const { guild, previewWidth, previewHeight, resourcePreviewId, resourceId, resourceName, resourceIdGuild, setOverlay } = props; + const { guild, previewWidth, previewHeight, resourcePreviewId, resourceId, resourceName, resourceIdGuild } = props; + + const setOverlay = useSetRecoilState(overlayState); // TODO: Handle resourceError const [ previewImgSrc, previewResourceResult, previewResourceError ] = useSoftImageSrcResourceSubscription(guild, resourcePreviewId, resourceIdGuild); @@ -67,7 +70,7 @@ const PreviewImageElement: FC = (props: PreviewImageEl }, [ previewResourceResult, resourceName ]); const openImageOverlay = useCallback(() => { - setOverlay( setOverlay(null)} />); + setOverlay(); }, [ guild, resourceId, resourceName ]); return ( @@ -86,11 +89,10 @@ export interface MessageElementProps { message: Message; prevMessage: Message | null; messageGuild: CombinedGuild; - setOverlay: Dispatch>; } const MessageElement: FC = (props: MessageElementProps) => { - const { guild, message, prevMessage, messageGuild, setOverlay } = props; + const { guild, message, prevMessage, messageGuild } = props; const className = useMemo(() => { return message.isContinued(prevMessage) ? 'message-react continued' : 'message-react'; @@ -146,7 +148,6 @@ const MessageElement: FC = (props: MessageElementProps) => resourceId={message.resourceId} resourceIdGuild={messageGuild} resourceName={message.resourceName ?? 'unknown.unk'} - setOverlay={setOverlay} /> ); } else { diff --git a/src/client/webapp/elements/lists/message-list.tsx b/src/client/webapp/elements/lists/message-list.tsx index 3aa789d..c8cb071 100644 --- a/src/client/webapp/elements/lists/message-list.tsx +++ b/src/client/webapp/elements/lists/message-list.tsx @@ -10,11 +10,10 @@ interface MessageListProps { channel: Channel; channelGuild: CombinedGuild; setFetchRetryCallable: Dispatch Promise) | null>>; - setOverlay: Dispatch>; } const MessageList: FC = (props: MessageListProps) => { - const { guild, channel, channelGuild, setFetchRetryCallable, setOverlay } = props; + const { guild, channel, channelGuild, setFetchRetryCallable } = props; const infiniteScrollElementRef = useRef(null); @@ -46,7 +45,7 @@ const MessageList: FC = (props: MessageListProps) => { for (let i = 0; i < messagesResult.value.elements.length; ++i) { const prevMessage = messagesResult.value.elements[i - 1] ?? null; const message = messagesResult.value.elements[i] as Message; - result.push(); + result.push(); } } return result; diff --git a/src/client/webapp/elements/overlays/overlay-add-guild.tsx b/src/client/webapp/elements/overlays/overlay-add-guild.tsx index 4795f9b..77db25d 100644 --- a/src/client/webapp/elements/overlays/overlay-add-guild.tsx +++ b/src/client/webapp/elements/overlays/overlay-add-guild.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, { Dispatch, FC, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'; +import React, { Dispatch, FC, ReactNode, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'; import GuildsManager from '../../guilds-manager'; import moment from 'moment'; import TextInput from '../components/input-text'; @@ -17,6 +17,8 @@ import { useAsyncSubmitButton, useOneTimeAsyncAction } from '../require/react-he import * as fs from 'fs/promises'; import Button from '../components/button'; import Overlay from '../components/overlay'; +import { useSetRecoilState } from 'recoil'; +import { overlayState } from '../atoms'; export interface IAddGuildData { name: string, @@ -53,13 +55,13 @@ export interface AddGuildOverlayProps { guildsManager: GuildsManager; addGuildData: IAddGuildData; setActiveGuild: Dispatch>; - close: () => void; } const AddGuildOverlay: FC = (props: AddGuildOverlayProps) => { - const { guildsManager, addGuildData, setActiveGuild, close } = props; + const { guildsManager, addGuildData, setActiveGuild } = props; const rootRef = useRef(null); + const setOverlay = useSetRecoilState(overlayState); const expired = addGuildData.expires && addGuildData.expires < new Date().getTime(); const exampleDisplayName = useMemo(() => getExampleDisplayName(), []); @@ -108,10 +110,10 @@ const AddGuildOverlay: FC = (props: AddGuildOverlayProps) setActiveGuild(newGuild); - close(); + setOverlay(null); return { result: newGuild, errorMessage: null }; }, - [ displayName, avatarBuff, displayNameInputValid, avatarInputValid, close ] + [ displayName, avatarBuff, displayNameInputValid, avatarInputValid, setOverlay ] ); const errorMessage = useMemo(() => { @@ -121,7 +123,7 @@ const AddGuildOverlay: FC = (props: AddGuildOverlayProps) }, [ validationErrorMessage, submitFailMessage ]); return ( - +
void; } const ChannelOverlay: FC = (props: ChannelOverlayProps) => { - const { guild, channel, close } = props; + const { guild, channel } = props; const rootRef = useRef(null); const nameInputRef = useRef(null); + const setOverlay = useSetRecoilState(overlayState); + const [ edited, setEdited ] = useState(false); const [ name, setName ] = useState(channel?.name ?? ''); @@ -66,7 +69,7 @@ const ChannelOverlay: FC = (props: ChannelOverlayProps) => if (validationErrorMessage) return { result: null, errorMessage: 'Invalid input' }; if (!edited) { - close(); + setOverlay(null); return { result: null, errorMessage: null }; } @@ -82,10 +85,10 @@ const ChannelOverlay: FC = (props: ChannelOverlayProps) => return { result: null, errorMessage: `Error ${channel ? 'updating' : 'creating'} channel`} } - close(); + setOverlay(null); return { result: null, errorMessage: null }; }, - [ edited, validationErrorMessage, name, flavorText, close ], + [ edited, validationErrorMessage, name, flavorText, setOverlay ], { start: channel ? 'Modify Channel' : 'Create Channel' } ); @@ -96,7 +99,7 @@ const ChannelOverlay: FC = (props: ChannelOverlayProps) => }, [ validationErrorMessage, submitFailMessage ]); return ( - +
{BaseElements.TEXT_CHANNEL_ICON}
diff --git a/src/client/webapp/elements/overlays/overlay-error-message.tsx b/src/client/webapp/elements/overlays/overlay-error-message.tsx index cca23b6..3b66108 100644 --- a/src/client/webapp/elements/overlays/overlay-error-message.tsx +++ b/src/client/webapp/elements/overlays/overlay-error-message.tsx @@ -4,15 +4,14 @@ import Overlay from '../components/overlay'; export interface ErrorMessageOverlayProps { title: string; message: string; - close: () => void; } const ErrorMessageOverlay: FC = (props: ErrorMessageOverlayProps) => { - const { title, message, close } = props; + const { title, message } = props; const rootRef = useRef(null); return ( - +
error diff --git a/src/client/webapp/elements/overlays/overlay-guild-settings.tsx b/src/client/webapp/elements/overlays/overlay-guild-settings.tsx index 6d98848..73d28bf 100644 --- a/src/client/webapp/elements/overlays/overlay-guild-settings.tsx +++ b/src/client/webapp/elements/overlays/overlay-guild-settings.tsx @@ -10,10 +10,9 @@ import { SubscriptionResult } from '../require/guild-subscriptions'; export interface GuildSettingsOverlayProps { guild: CombinedGuild; guildMetaResult: SubscriptionResult; - close: () => void; } const GuildSettingsOverlay: FC = (props: GuildSettingsOverlayProps) => { - const { guild, guildMetaResult, close } = props; + const { guild, guildMetaResult } = props; const rootRef = useRef(null); @@ -27,7 +26,7 @@ const GuildSettingsOverlay: FC = (props: GuildSetting }, [ selectedId ]); return ( - +
void; } const ImageOverlay: FC = (props: ImageOverlayProps) => { - const { guild, resourceId, resourceName, resourceIdGuild, close } = props; + const { guild, resourceId, resourceName, resourceIdGuild } = props; const rootRef = useRef(null); @@ -43,7 +42,7 @@ const ImageOverlay: FC = (props: ImageOverlayProps) => { ); return ( - +
{resourceName}
diff --git a/src/client/webapp/elements/overlays/overlay-personalize.tsx b/src/client/webapp/elements/overlays/overlay-personalize.tsx index 9ac6653..c6a44c4 100644 --- a/src/client/webapp/elements/overlays/overlay-personalize.tsx +++ b/src/client/webapp/elements/overlays/overlay-personalize.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, { createRef, FC, MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'; +import React, { createRef, FC, MutableRefObject, ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import { Member } from '../../data-types'; import Globals from '../../globals'; import CombinedGuild from '../../guild-combined'; @@ -14,14 +14,17 @@ import { useAsyncSubmitButton } from '../require/react-helper'; import Button from '../components/button'; import Overlay from '../components/overlay'; import { SubscriptionResult, useResourceSubscription } from '../require/guild-subscriptions'; +import { useSetRecoilState } from 'recoil'; +import { overlayState } from '../atoms'; export interface PersonalizeOverlayProps { guild: CombinedGuild; selfMemberResult: SubscriptionResult; - close: () => void; } const PersonalizeOverlay: FC = (props: PersonalizeOverlayProps) => { - const { guild, selfMemberResult, close } = props; + const { guild, selfMemberResult } = props; + + const setOverlay = useSetRecoilState(overlayState); const rootRef = useRef(null); @@ -93,10 +96,11 @@ const PersonalizeOverlay: FC = (props: PersonalizeOverl } } - close(); + // Close the overlay + setOverlay(null); return { result: null, errorMessage: null }; }, - [ validationErrorMessage, displayName, savedDisplayName, avatarBuff, savedAvatarBuff, close ] + [ validationErrorMessage, displayName, savedDisplayName, avatarBuff, savedAvatarBuff, setOverlay ] ); //if (saveFailed) return 'Unable to save personalization'; @@ -107,7 +111,7 @@ const PersonalizeOverlay: FC = (props: PersonalizeOverl }, [ validationErrorMessage, submitFailMessage ]); return ( - +
diff --git a/src/client/webapp/elements/require/react-helper.tsx b/src/client/webapp/elements/require/react-helper.tsx index c8361f3..8ad349a 100644 --- a/src/client/webapp/elements/require/react-helper.tsx +++ b/src/client/webapp/elements/require/react-helper.tsx @@ -323,7 +323,7 @@ export function useColumnReverseInfiniteScroll( // Makes sure to also allow you to fly-out a click starting inside of the ref'd element but was dragged outside /** Calls the close action when you hit escape or click outside of the ref element */ -export function useCloseWhenEscapeOrClickedOrContextOutsideEffect(ref: RefObject, close: () => void) { +export function useActionWhenEscapeOrClickedOrContextOutsideEffect(ref: RefObject, actionFunc: () => void) { // Have to use a ref here and not states since we can't re-assign state between mouseup and click const mouseRef = useRef<{ mouseDownTarget: Node | null, mouseUpTarget: Node | null}>({ mouseDownTarget: null, mouseUpTarget: null }); @@ -337,8 +337,8 @@ export function useCloseWhenEscapeOrClickedOrContextOutsideEffect(ref: RefObject if (mouseRef.current.mouseDownTarget !== null || mouseRef.current.mouseUpTarget !== null) return; - close(); - }, [ ref, mouseRef, close ]); + actionFunc(); + }, [ ref, mouseRef, actionFunc ]); const handleMouseDown = useCallback((event: MouseEvent) => { if (!ref.current) return; @@ -364,14 +364,14 @@ export function useCloseWhenEscapeOrClickedOrContextOutsideEffect(ref: RefObject if (!ref.current) return; if (ref.current.contains(event.target as Node)) return; // Context menu is fired on mouse-down so no need to do special checks. - close(); + actionFunc(); }, [ ref ]); const handleKeyDown = useCallback((event: KeyboardEvent) => { if (!ref.current) return; if (event.key !== 'Escape') return; - close(); + actionFunc(); }, [ ref ]); useEffect(() => { diff --git a/src/client/webapp/elements/root.tsx b/src/client/webapp/elements/root.tsx index 6e20cfa..9c43649 100644 --- a/src/client/webapp/elements/root.tsx +++ b/src/client/webapp/elements/root.tsx @@ -1,5 +1,7 @@ -import React, { FC, ReactNode, useState } from 'react'; +import React, { FC, ReactNode } from 'react'; +import { useRecoilState } from 'recoil'; import GuildsManager from '../guilds-manager'; +import { overlayState } from './atoms'; import GuildsManagerElement from './sections/guilds-manager'; import TitleBar from './sections/title-bar'; @@ -10,15 +12,16 @@ export interface RootElementProps { const RootElement: FC = (props: RootElementProps) => { const { guildsManager } = props; - const [ overlay, setOverlay ] = useState(null); + const [ overlay, setOverlay ] = useRecoilState(overlayState); return (
- +
{overlay}
); } export default RootElement; + diff --git a/src/client/webapp/elements/sections/connection-info.tsx b/src/client/webapp/elements/sections/connection-info.tsx index e914890..31a4c84 100644 --- a/src/client/webapp/elements/sections/connection-info.tsx +++ b/src/client/webapp/elements/sections/connection-info.tsx @@ -9,11 +9,10 @@ import { isNonNullAndHasValue, SubscriptionResult } from '../require/guild-subsc export interface ConnectionInfoProps { guild: CombinedGuild; selfMemberResult: SubscriptionResult | null; - setOverlay: Dispatch>; } const ConnectionInfo: FC = (props: ConnectionInfoProps) => { - const { guild, selfMemberResult, setOverlay } = props; + const { guild, selfMemberResult } = props; const rootRef = useRef(null); @@ -35,7 +34,7 @@ const ConnectionInfo: FC = (props: ConnectionInfoProps) => return ( ); }, [ guild, selfMemberResult, rootRef ]); diff --git a/src/client/webapp/elements/sections/guild-list-container.tsx b/src/client/webapp/elements/sections/guild-list-container.tsx index 81901b3..dd12f24 100644 --- a/src/client/webapp/elements/sections/guild-list-container.tsx +++ b/src/client/webapp/elements/sections/guild-list-container.tsx @@ -13,20 +13,23 @@ import AddGuildOverlay from '../overlays/overlay-add-guild'; import ErrorMessageOverlay from '../overlays/overlay-error-message'; import BasicHover, { BasicHoverSide } from '../contexts/context-hover-basic'; import BaseElements from '../require/base-elements'; +import { overlayState } from '../atoms'; +import { useSetRecoilState } from 'recoil'; export interface GuildListContainerProps { guildsManager: GuildsManager; guilds: CombinedGuild[]; activeGuild: CombinedGuild | null; setActiveGuild: Dispatch>; - setOverlay: Dispatch>; } const GuildListContainer: FC = (props: GuildListContainerProps) => { - const { guildsManager, guilds, activeGuild, setActiveGuild, setOverlay } = props; + const { guildsManager, guilds, activeGuild, setActiveGuild } = props; const addGuildRef = useRef(null); + const setOverlay = useSetRecoilState(overlayState); + const [ contextHover, onMouseEnter, onMouseLeave ] = useContextHover(() => { return ( @@ -66,9 +69,9 @@ const GuildListContainer: FC = (props: GuildListContain typeof addGuildData?.iconSrc !== 'string' ) { LOG.debug('bad guild data:', { addGuildData, fileText }); - setOverlay( setOverlay(null)} />); + setOverlay(); } else { - setOverlay( setOverlay(null)} />); + setOverlay(); } }, [ guildsManager ]); diff --git a/src/client/webapp/elements/sections/guild-title.tsx b/src/client/webapp/elements/sections/guild-title.tsx index c1aa4d7..c03c77e 100644 --- a/src/client/webapp/elements/sections/guild-title.tsx +++ b/src/client/webapp/elements/sections/guild-title.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, FC, ReactNode, SetStateAction, useMemo, useRef } from 'react'; +import React, { FC, useMemo, useRef } from 'react'; import { GuildMetadata, Member } from '../../data-types'; import CombinedGuild from '../../guild-combined'; import GuildTitleContextMenu from '../contexts/context-menu-guild-title'; @@ -9,11 +9,10 @@ export interface GuildTitleProps { guild: CombinedGuild; guildMetaResult: SubscriptionResult | null; selfMemberResult: SubscriptionResult | null; - setOverlay: Dispatch>; } const GuildTitle: FC = (props: GuildTitleProps) => { - const { guild, guildMetaResult, selfMemberResult, setOverlay } = props; + const { guild, guildMetaResult, selfMemberResult } = props; const rootRef = useRef(null); @@ -33,7 +32,6 @@ const GuildTitle: FC = (props: GuildTitleProps) => { ); }, [ guild, guildMetaResult, selfMemberResult, rootRef ]); diff --git a/src/client/webapp/elements/sections/guild.tsx b/src/client/webapp/elements/sections/guild.tsx index 523d5d4..99ed079 100644 --- a/src/client/webapp/elements/sections/guild.tsx +++ b/src/client/webapp/elements/sections/guild.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, { Dispatch, FC, ReactNode, SetStateAction, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useState } from 'react'; import { Channel, Message } from '../../data-types'; import CombinedGuild from '../../guild-combined'; import ChannelList from '../lists/channel-list'; @@ -17,11 +17,10 @@ import SendMessage from './send-message'; export interface GuildElementProps { guild: CombinedGuild; - setOverlay: Dispatch>; } const GuildElement: FC = (props: GuildElementProps) => { - const { guild, setOverlay } = props; + const { guild } = props; // TODO: Handle fetch errors by allowing for retry // TODO: Handle fetch errors in message list @@ -84,20 +83,19 @@ const GuildElement: FC = (props: GuildElementProps) => { return (
- + - +
- {activeChannel && activeChannelGuild && } + {activeChannel && activeChannelGuild && } {activeChannel && activeChannelGuild && }
diff --git a/src/client/webapp/elements/sections/guilds-manager.tsx b/src/client/webapp/elements/sections/guilds-manager.tsx index 17571df..a26907c 100644 --- a/src/client/webapp/elements/sections/guilds-manager.tsx +++ b/src/client/webapp/elements/sections/guilds-manager.tsx @@ -7,11 +7,10 @@ import GuildListContainer from './guild-list-container'; export interface GuildsManagerElementProps { guildsManager: GuildsManager; - setOverlay: Dispatch>; } const GuildsManagerElement: FC = (props: GuildsManagerElementProps) => { - const { guildsManager, setOverlay } = props; + const { guildsManager } = props; const [ guilds ] = useGuildListSubscription(guildsManager); const [ activeGuild, setActiveGuild ] = useState(null); @@ -30,9 +29,8 @@ const GuildsManagerElement: FC = (props: GuildsManage - {activeGuild && } + {activeGuild && }
); } diff --git a/src/client/webapp/preload.tsx b/src/client/webapp/preload.tsx index 7447a82..4ddd957 100644 --- a/src/client/webapp/preload.tsx +++ b/src/client/webapp/preload.tsx @@ -18,6 +18,7 @@ import ResourceRAMCache from './resource-ram-cache'; import ReactDOM from 'react-dom'; import React from 'react'; import RootElement from './elements/root'; +import { RecoilRoot } from 'recoil'; LOG.silly('modules loaded'); @@ -58,7 +59,7 @@ window.addEventListener('DOMContentLoaded', () => { LOG.silly('guildsManager initialized'); - ReactDOM.render(, document.getElementById('react-root')); + ReactDOM.render(, document.getElementById('react-root')); })(); });