beginnings of react-recoil

Starting off with Recoilizing setOverlay

Also set up the atom structure
This commit is contained in:
Michael Peters 2022-01-26 19:57:29 -06:00
parent a0f25158d3
commit 4f2d272d00
25 changed files with 240 additions and 94 deletions

38
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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<T> {
guild: CombinedGuild;
value: T;
}
export interface GuildWithErrorableValue<T> {
guild: CombinedGuild;
value: T | null;
valueError: unknown | null;
}
export interface ChannelWithErrorableValue<T> {
channel: Channel;
value: T | null;
valueError: unknown | null;
}
export const overlayState = atom<ReactNode>({
key: 'overlayState',
default: null
});
export const guildsState = atom<GuildWithErrorableValue<GuildMetadata>[] | null>({
key: 'guildsState',
default: null
});
export const selectedGuildIdState = atom<number | null>({
key: 'selectedGuildIdState',
default: null
});
export const selectedGuildWithMetaState = selector<GuildWithErrorableValue<GuildMetadata> | 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<CombinedGuild | null>({
key: 'selectedGuildState',
get: ({ get }) => {
const guildWithMeta = get(selectedGuildWithMetaState);
if (guildWithMeta === null) return null;
return guildWithMeta.guild;
}
});
export const selectedGuildMembersState = atom<GuildWithErrorableValue<Member[]> | null>({
key: 'selectedGuildMembersState',
default: null
});
export const selectedGuildWithChannelsState = atom<GuildWithErrorableValue<Channel[]> | null>({
key: 'selectedGuildChannelsState',
default: null
});
export const selectedGuildWithActiveChannelIdState = atom<GuildWithValue<string | null> | null>({
key: 'selectedGuildWithActiveChannelIdState',
default: null
});
export const selectedGuildWithActiveChannelMessagesState = atom<GuildWithValue<ChannelWithErrorableValue<Message[]> | null> | null>({
key: 'selectedGuildWithActiveChannelMessagesState',
default: null
});
export const selectedGuildWithActiveChannelState = selector<GuildWithValue<Channel> | 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 };
}
});

View File

@ -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<HTMLElement>; // clicks outside this ref will close the overlay
close: () => void;
children: React.ReactNode;
}
const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
const { childRootRef, close, children } = props;
const { childRootRef, children } = props;
const setOverlay = useSetRecoilState<ReactNode>(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 <div className="overlay">{children}</div>
};

View File

@ -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<ContextMenuProps> = (props: ContextMenuProps) => {
const rootRef = useRef<HTMLDivElement>(null);
useCloseWhenEscapeOrClickedOrContextOutsideEffect(rootRef, close);
useActionWhenEscapeOrClickedOrContextOutsideEffect(rootRef, close);
return (
<Context

View File

@ -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 { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { overlayState } from '../atoms';
import PersonalizeOverlay from '../overlays/overlay-personalize';
import { SubscriptionResult } from '../require/guild-subscriptions';
import ContextMenu from './components/context-menu';
@ -10,11 +12,12 @@ export interface ConnectionInfoContextMenuProps {
selfMemberResult: SubscriptionResult<Member>;
relativeToRef: RefObject<HTMLElement>;
close: () => void;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: ConnectionInfoContextMenuProps) => {
const { guild, selfMemberResult, relativeToRef, close, setOverlay } = props;
const { guild, selfMemberResult, relativeToRef, close } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState)
const setSelfStatus = useCallback(async (status: string) => {
if (selfMemberResult.value.status !== status) {
@ -36,7 +39,7 @@ const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: Co
const openPersonalize = useCallback(() => {
close();
setOverlay(<PersonalizeOverlay guild={guild} selfMemberResult={selfMemberResult} close={() => setOverlay(null)} />);
setOverlay(<PersonalizeOverlay guild={guild} selfMemberResult={selfMemberResult} />);
}, [ guild, selfMemberResult, close ]);
const alignment = useMemo(() => {

View File

@ -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<GuildMetadata>;
selfMember: Member;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitleContextMenuProps) => {
const { close, relativeToRef, guild, guildMetaResult, selfMember, setOverlay } = props;
const { close, relativeToRef, guild, guildMetaResult, selfMember } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const openGuildSettings = useCallback(() => {
close();
setOverlay(<GuildSettingsOverlay guild={guild} guildMetaResult={guildMetaResult} close={() => setOverlay(null)} />);
setOverlay(<GuildSettingsOverlay guild={guild} guildMetaResult={guildMetaResult} />);
}, [ guild, guildMetaResult, close ]);
const openCreateChannel = useCallback(() => {
close();
setOverlay(<ChannelOverlay guild={guild} close={() => setOverlay(null)} />);
setOverlay(<ChannelOverlay guild={guild} />);
}, [ guild, close ]);
const guildSettingsElement = useMemo(() => {

View File

@ -10,11 +10,10 @@ export interface ChannelListProps {
channelsFetchError: unknown | null;
activeChannel: Channel | null;
setActiveChannel: Dispatch<SetStateAction<Channel | null>>;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const ChannelList: FC<ChannelListProps> = (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<ChannelListProps> = (props: ChannelListProps) => {
selfMember={selfMember}
channel={channel} activeChannel={activeChannel}
setActiveChannel={() => { setActiveChannel(channel); }}
setOverlay={setOverlay}
/>
));
}, [ selfMember, channelsFetchError, channels, guild, selfMember, activeChannel ]);

View File

@ -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<SetStateAction<Channel | null>>;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const ChannelElement: FC<ChannelElementProps> = (props: ChannelElementProps) => {
const { guild, channel, selfMember, activeChannel, setActiveChannel, setOverlay } = props;
const { guild, channel, selfMember, activeChannel, setActiveChannel } = props;
const modifyRef = useRef<HTMLDivElement>(null);
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const baseClassName = activeChannel?.id === channel.id ? 'channel text active' : 'channel text';
const [ modifyContextHover, modifyMouseEnterCallable, modifyMouseLeaveCallable ] = useContextHover(
@ -46,7 +49,7 @@ const ChannelElement: FC<ChannelElementProps> = (props: ChannelElementProps) =>
}, [ modifyRef, channel ]);
const launchModify = useCallback(() => {
setOverlay(<ChannelOverlay guild={guild} channel={channel} close={() => { setOverlay(null); }} />);
setOverlay(<ChannelOverlay guild={guild} channel={channel} />);
}, [ guild, channel ]);
return (

View File

@ -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<SetStateAction<ReactNode>>;
}
const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageElementProps) => {
const { guild, previewWidth, previewHeight, resourcePreviewId, resourceId, resourceName, resourceIdGuild, setOverlay } = props;
const { guild, previewWidth, previewHeight, resourcePreviewId, resourceId, resourceName, resourceIdGuild } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
// TODO: Handle resourceError
const [ previewImgSrc, previewResourceResult, previewResourceError ] = useSoftImageSrcResourceSubscription(guild, resourcePreviewId, resourceIdGuild);
@ -67,7 +70,7 @@ const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageEl
}, [ previewResourceResult, resourceName ]);
const openImageOverlay = useCallback(() => {
setOverlay(<ImageOverlay guild={guild} resourceId={resourceId} resourceName={resourceName} resourceIdGuild={resourceIdGuild} close={() => setOverlay(null)} />);
setOverlay(<ImageOverlay guild={guild} resourceId={resourceId} resourceName={resourceName} resourceIdGuild={resourceIdGuild} />);
}, [ guild, resourceId, resourceName ]);
return (
@ -86,11 +89,10 @@ export interface MessageElementProps {
message: Message;
prevMessage: Message | null;
messageGuild: CombinedGuild;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const MessageElement: FC<MessageElementProps> = (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<MessageElementProps> = (props: MessageElementProps) =>
resourceId={message.resourceId}
resourceIdGuild={messageGuild}
resourceName={message.resourceName ?? 'unknown.unk'}
setOverlay={setOverlay}
/>
);
} else {

View File

@ -10,11 +10,10 @@ interface MessageListProps {
channel: Channel;
channelGuild: CombinedGuild;
setFetchRetryCallable: Dispatch<SetStateAction<(() => Promise<void>) | null>>;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
const { guild, channel, channelGuild, setFetchRetryCallable, setOverlay } = props;
const { guild, channel, channelGuild, setFetchRetryCallable } = props;
const infiniteScrollElementRef = useRef<HTMLDivElement>(null);
@ -46,7 +45,7 @@ const MessageList: FC<MessageListProps> = (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(<MessageElement key={guild.id + message.id} guild={guild} message={message} prevMessage={prevMessage} messageGuild={messagesResult.guild} setOverlay={setOverlay} />);
result.push(<MessageElement key={guild.id + message.id} guild={guild} message={message} prevMessage={prevMessage} messageGuild={messagesResult.guild} />);
}
}
return result;

View File

@ -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<SetStateAction<CombinedGuild | null>>;
close: () => void;
}
const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps) => {
const { guildsManager, addGuildData, setActiveGuild, close } = props;
const { guildsManager, addGuildData, setActiveGuild } = props;
const rootRef = useRef<HTMLDivElement>(null);
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const expired = addGuildData.expires && addGuildData.expires < new Date().getTime();
const exampleDisplayName = useMemo(() => getExampleDisplayName(), []);
@ -108,10 +110,10 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (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<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
}, [ validationErrorMessage, submitFailMessage ]);
return (
<Overlay childRootRef={rootRef} close={close} >
<Overlay childRootRef={rootRef}>
<div ref={rootRef} className="content add-guild">
<InvitePreview
name={addGuildData.name} iconSrc={addGuildData.iconSrc}

View File

@ -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, useEffect, useMemo, useRef, useState } from 'react';
import React, { FC, ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import CombinedGuild from '../../guild-combined';
import BaseElements from '../require/base-elements';
import TextInput from '../components/input-text';
@ -13,18 +13,21 @@ import { Channel } from '../../data-types';
import { useAsyncSubmitButton } from '../require/react-helper';
import Button from '../components/button';
import Overlay from '../components/overlay';
import { useSetRecoilState } from 'recoil';
import { overlayState } from '../atoms';
export interface ChannelOverlayProps {
guild: CombinedGuild;
channel?: Channel;
close: () => void;
}
const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) => {
const { guild, channel, close } = props;
const { guild, channel } = props;
const rootRef = useRef<HTMLDivElement>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const [ edited, setEdited ] = useState<boolean>(false);
const [ name, setName ] = useState<string>(channel?.name ?? '');
@ -66,7 +69,7 @@ const ChannelOverlay: FC<ChannelOverlayProps> = (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<ChannelOverlayProps> = (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<ChannelOverlayProps> = (props: ChannelOverlayProps) =>
}, [ validationErrorMessage, submitFailMessage ]);
return (
<Overlay childRootRef={rootRef} close={close} >
<Overlay childRootRef={rootRef}>
<div ref={rootRef} className="content submit-dialog modify-channel">
<div className="preview channel-title">
<div className="channel-icon">{BaseElements.TEXT_CHANNEL_ICON}</div>

View File

@ -4,15 +4,14 @@ import Overlay from '../components/overlay';
export interface ErrorMessageOverlayProps {
title: string;
message: string;
close: () => void;
}
const ErrorMessageOverlay: FC<ErrorMessageOverlayProps> = (props: ErrorMessageOverlayProps) => {
const { title, message, close } = props;
const { title, message } = props;
const rootRef = useRef<HTMLDivElement>(null);
return (
<Overlay childRootRef={rootRef} close={close}>
<Overlay childRootRef={rootRef}>
<div ref={rootRef} className="content error-message">
<div className="icon">
<img src="./img/error.png" alt="error" />

View File

@ -10,10 +10,9 @@ import { SubscriptionResult } from '../require/guild-subscriptions';
export interface GuildSettingsOverlayProps {
guild: CombinedGuild;
guildMetaResult: SubscriptionResult<GuildMetadata>;
close: () => void;
}
const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSettingsOverlayProps) => {
const { guild, guildMetaResult, close } = props;
const { guild, guildMetaResult } = props;
const rootRef = useRef<HTMLDivElement>(null);
@ -27,7 +26,7 @@ const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSetting
}, [ selectedId ]);
return (
<Overlay childRootRef={rootRef} close={close}>
<Overlay childRootRef={rootRef}>
<div ref={rootRef} className="content display-swapper guild-settings">
<ChoicesControl title={guildMetaResult.value.name} selectedId={selectedId} setSelectedId={setSelectedId} choices={[
{ id: 'overview', display: 'Overview' },

View File

@ -17,11 +17,10 @@ export interface ImageOverlayProps {
resourceId: string;
resourceName: string;
resourceIdGuild: CombinedGuild;
close: () => void;
}
const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
const { guild, resourceId, resourceName, resourceIdGuild, close } = props;
const { guild, resourceId, resourceName, resourceIdGuild } = props;
const rootRef = useRef<HTMLDivElement>(null);
@ -43,7 +42,7 @@ const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
);
return (
<Overlay childRootRef={rootRef} close={close}>
<Overlay childRootRef={rootRef}>
<div ref={rootRef} className="content popup-image">
<img src={imgSrc} alt={resourceName} title={resourceName} onContextMenu={onContextMenu}></img>
<div className="download">

View File

@ -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<Member>;
close: () => void;
}
const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverlayProps) => {
const { guild, selfMemberResult, close } = props;
const { guild, selfMemberResult } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const rootRef = useRef<HTMLDivElement>(null);
@ -93,10 +96,11 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (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<PersonalizeOverlayProps> = (props: PersonalizeOverl
}, [ validationErrorMessage, submitFailMessage ]);
return (
<Overlay childRootRef={rootRef} close={close}>
<Overlay childRootRef={rootRef}>
<div ref={rootRef} className="content personalize">
<div className="personalization">
<div className="avatar">

View File

@ -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<HTMLElement>, close: () => void) {
export function useActionWhenEscapeOrClickedOrContextOutsideEffect(ref: RefObject<HTMLElement>, 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(() => {

View File

@ -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<RootElementProps> = (props: RootElementProps) => {
const { guildsManager } = props;
const [ overlay, setOverlay ] = useState<ReactNode>(null);
const [ overlay, setOverlay ] = useRecoilState<ReactNode>(overlayState);
return (
<div>
<TitleBar />
<GuildsManagerElement guildsManager={guildsManager} setOverlay={setOverlay} />
<GuildsManagerElement guildsManager={guildsManager} />
<div className="react-overlays">{overlay}</div>
</div>
);
}
export default RootElement;

View File

@ -9,11 +9,10 @@ import { isNonNullAndHasValue, SubscriptionResult } from '../require/guild-subsc
export interface ConnectionInfoProps {
guild: CombinedGuild;
selfMemberResult: SubscriptionResult<Member | null> | null;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const ConnectionInfo: FC<ConnectionInfoProps> = (props: ConnectionInfoProps) => {
const { guild, selfMemberResult, setOverlay } = props;
const { guild, selfMemberResult } = props;
const rootRef = useRef<HTMLDivElement>(null);
@ -35,7 +34,7 @@ const ConnectionInfo: FC<ConnectionInfoProps> = (props: ConnectionInfoProps) =>
return (
<ConnectionInfoContextMenu
guild={guild} selfMemberResult={selfMemberResult} relativeToRef={rootRef}
close={close} setOverlay={setOverlay}
close={close}
/>
);
}, [ guild, selfMemberResult, rootRef ]);

View File

@ -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<SetStateAction<CombinedGuild | null>>;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const GuildListContainer: FC<GuildListContainerProps> = (props: GuildListContainerProps) => {
const { guildsManager, guilds, activeGuild, setActiveGuild, setOverlay } = props;
const { guildsManager, guilds, activeGuild, setActiveGuild } = props;
const addGuildRef = useRef<HTMLDivElement>(null);
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const [ contextHover, onMouseEnter, onMouseLeave ] = useContextHover(() => {
return (
<BasicHover relativeToRef={addGuildRef} realignDeps={[]} side={BasicHoverSide.RIGHT}>
@ -66,9 +69,9 @@ const GuildListContainer: FC<GuildListContainerProps> = (props: GuildListContain
typeof addGuildData?.iconSrc !== 'string'
) {
LOG.debug('bad guild data:', { addGuildData, fileText });
setOverlay(<ErrorMessageOverlay title="Unable to parse guild file" message="Bad guild data" close={() => setOverlay(null)} />);
setOverlay(<ErrorMessageOverlay title="Unable to parse guild file" message="Bad guild data" />);
} else {
setOverlay(<AddGuildOverlay guildsManager={guildsManager} addGuildData={addGuildData} setActiveGuild={setActiveGuild} close={() => setOverlay(null)} />);
setOverlay(<AddGuildOverlay guildsManager={guildsManager} addGuildData={addGuildData} setActiveGuild={setActiveGuild} />);
}
}, [ guildsManager ]);

View File

@ -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<GuildMetadata | null> | null;
selfMemberResult: SubscriptionResult<Member | null> | null;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const GuildTitle: FC<GuildTitleProps> = (props: GuildTitleProps) => {
const { guild, guildMetaResult, selfMemberResult, setOverlay } = props;
const { guild, guildMetaResult, selfMemberResult } = props;
const rootRef = useRef<HTMLDivElement>(null);
@ -33,7 +32,6 @@ const GuildTitle: FC<GuildTitleProps> = (props: GuildTitleProps) => {
<GuildTitleContextMenu
relativeToRef={rootRef} close={close}
guild={guild} guildMetaResult={guildMetaResult} selfMember={selfMemberResult.value}
setOverlay={setOverlay}
/>
);
}, [ guild, guildMetaResult, selfMemberResult, rootRef ]);

View File

@ -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<SetStateAction<ReactNode>>;
}
const GuildElement: FC<GuildElementProps> = (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<GuildElementProps> = (props: GuildElementProps) => {
return (
<div className="guild-react">
<div className="guild-sidebar">
<GuildTitle guild={guild} selfMemberResult={selfMemberResult} guildMetaResult={guildMetaResult} setOverlay={setOverlay} />
<GuildTitle guild={guild} selfMemberResult={selfMemberResult} guildMetaResult={guildMetaResult} />
<ChannelList
guild={guild} selfMember={selfMemberResult?.value ?? null}
channels={channelsResult?.value ?? null} channelsFetchError={channelsFetchError}
activeChannel={activeChannel} setActiveChannel={setActiveChannel}
setOverlay={setOverlay}
/>
<ConnectionInfo guild={guild} selfMemberResult={selfMemberResult} setOverlay={setOverlay} />
<ConnectionInfo guild={guild} selfMemberResult={selfMemberResult} />
</div>
<div className="guild-channel">
<ChannelTitle channel={activeChannel} />
<div className="guild-channel-content">
<div className="guild-channel-feed-wrapper">
{activeChannel && activeChannelGuild && <MessageList guild={guild} channel={activeChannel} channelGuild={activeChannelGuild} setFetchRetryCallable={setFetchMessagesRetryCallable} setOverlay={setOverlay} />}
{activeChannel && activeChannelGuild && <MessageList guild={guild} channel={activeChannel} channelGuild={activeChannelGuild} setFetchRetryCallable={setFetchMessagesRetryCallable} />}
{activeChannel && activeChannelGuild && <SendMessage guild={guild} channel={activeChannel} />}
</div>
<div className="member-list-wrapper">

View File

@ -7,11 +7,10 @@ import GuildListContainer from './guild-list-container';
export interface GuildsManagerElementProps {
guildsManager: GuildsManager;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const GuildsManagerElement: FC<GuildsManagerElementProps> = (props: GuildsManagerElementProps) => {
const { guildsManager, setOverlay } = props;
const { guildsManager } = props;
const [ guilds ] = useGuildListSubscription(guildsManager);
const [ activeGuild, setActiveGuild ] = useState<CombinedGuild | null>(null);
@ -30,9 +29,8 @@ const GuildsManagerElement: FC<GuildsManagerElementProps> = (props: GuildsManage
<GuildListContainer
guildsManager={guildsManager} guilds={guilds}
activeGuild={activeGuild} setActiveGuild={setActiveGuild}
setOverlay={setOverlay}
/>
{activeGuild && <GuildElement guild={activeGuild} setOverlay={setOverlay} />}
{activeGuild && <GuildElement guild={activeGuild} />}
</div>
);
}

View File

@ -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(<RootElement guildsManager={guildsManager} />, document.getElementById('react-root'));
ReactDOM.render(<RecoilRoot><RootElement guildsManager={guildsManager} /></RecoilRoot>, document.getElementById('react-root'));
})();
});