Guild list in Recoil!
I'm liking it already. Quite a bit nicer than passing state all over the place.
This commit is contained in:
parent
4f2d272d00
commit
e0814f1096
@ -5,7 +5,7 @@ const LOG = Logger.create(__filename, electronConsole);
|
|||||||
|
|
||||||
import React, { FC, ReactNode, RefObject, useCallback, useEffect } from "react";
|
import React, { FC, ReactNode, RefObject, useCallback, useEffect } from "react";
|
||||||
import { useActionWhenEscapeOrClickedOrContextOutsideEffect } from '../require/react-helper';
|
import { useActionWhenEscapeOrClickedOrContextOutsideEffect } from '../require/react-helper';
|
||||||
import { overlayState } from '../atoms';
|
import { overlayState } from '../require/atoms';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
interface OverlayProps {
|
interface OverlayProps {
|
||||||
|
@ -2,7 +2,7 @@ import React, { FC, ReactNode, RefObject, useCallback, useMemo } from 'react';
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { Member } from '../../data-types';
|
import { Member } from '../../data-types';
|
||||||
import CombinedGuild from '../../guild-combined';
|
import CombinedGuild from '../../guild-combined';
|
||||||
import { overlayState } from '../atoms';
|
import { overlayState } from '../require/atoms';
|
||||||
import PersonalizeOverlay from '../overlays/overlay-personalize';
|
import PersonalizeOverlay from '../overlays/overlay-personalize';
|
||||||
import { SubscriptionResult } from '../require/guild-subscriptions';
|
import { SubscriptionResult } from '../require/guild-subscriptions';
|
||||||
import ContextMenu from './components/context-menu';
|
import ContextMenu from './components/context-menu';
|
||||||
|
@ -2,7 +2,7 @@ import React, { FC, ReactNode, RefObject, useCallback, useMemo } from 'react';
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { GuildMetadata, Member } from '../../data-types';
|
import { GuildMetadata, Member } from '../../data-types';
|
||||||
import CombinedGuild from '../../guild-combined';
|
import CombinedGuild from '../../guild-combined';
|
||||||
import { overlayState } from '../atoms';
|
import { overlayState } from '../require/atoms';
|
||||||
import ChannelOverlay from '../overlays/overlay-channel';
|
import ChannelOverlay from '../overlays/overlay-channel';
|
||||||
import GuildSettingsOverlay from '../overlays/overlay-guild-settings';
|
import GuildSettingsOverlay from '../overlays/overlay-guild-settings';
|
||||||
import BaseElements from '../require/base-elements';
|
import BaseElements from '../require/base-elements';
|
||||||
|
@ -10,7 +10,7 @@ import ChannelOverlay from '../../overlays/overlay-channel';
|
|||||||
import BaseElements from '../../require/base-elements';
|
import BaseElements from '../../require/base-elements';
|
||||||
import { useContextHover } from '../../require/react-helper';
|
import { useContextHover } from '../../require/react-helper';
|
||||||
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
|
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
|
||||||
import { overlayState } from '../../atoms';
|
import { overlayState } from '../../require/atoms';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
export interface ChannelElementProps {
|
export interface ChannelElementProps {
|
||||||
|
@ -1,41 +1,42 @@
|
|||||||
import React, { FC, useCallback, useMemo, useRef } from 'react';
|
import React, { FC, useCallback, useMemo, useRef } from 'react';
|
||||||
import CombinedGuild from '../../../guild-combined';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import GuildsManager from '../../../guilds-manager';
|
import { GuildMetadata } from '../../../data-types';
|
||||||
import ContextMenu from '../../contexts/components/context-menu';
|
import ContextMenu from '../../contexts/components/context-menu';
|
||||||
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
|
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
|
||||||
|
import { guildsManagerState, GuildWithErrorableValue, selectedGuildIdState, selectedGuildState } from '../../require/atoms';
|
||||||
import BaseElements from '../../require/base-elements';
|
import BaseElements from '../../require/base-elements';
|
||||||
import { IAlignment } from '../../require/elements-util';
|
import { IAlignment } from '../../require/elements-util';
|
||||||
import { useGuildMetadataSubscription, useSelfMemberSubscription, useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions';
|
import { useSelfMemberSubscription, useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions';
|
||||||
import { useContextClickContextMenu, useContextHover } from '../../require/react-helper';
|
import { useContextClickContextMenu, useContextHover } from '../../require/react-helper';
|
||||||
|
|
||||||
export interface GuildListElementProps {
|
export interface GuildListElementProps {
|
||||||
guildsManager: GuildsManager;
|
guildWithMeta: GuildWithErrorableValue<GuildMetadata>,
|
||||||
guild: CombinedGuild;
|
|
||||||
activeGuild: CombinedGuild | null;
|
|
||||||
setSelfActiveGuild: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProps) => {
|
const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProps) => {
|
||||||
const { guildsManager, guild, activeGuild, setSelfActiveGuild } = props;
|
const { guildWithMeta: { guild, value: guildMeta, valueError: _guildMetaError } } = props;
|
||||||
|
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// TODO: state higher up
|
const guildsManager = useRecoilValue(guildsManagerState);
|
||||||
|
const selectedGuild = useRecoilValue(selectedGuildState);
|
||||||
|
const setSelectedGuildId = useSetRecoilState(selectedGuildIdState);
|
||||||
|
|
||||||
|
// TODO: more state higher up
|
||||||
// TODO: handle metadata error
|
// TODO: handle metadata error
|
||||||
const [ guildMetaResult, guildMetaError ] = useGuildMetadataSubscription(guild);
|
|
||||||
const [ selfMemberResult ] = useSelfMemberSubscription(guild);
|
const [ selfMemberResult ] = useSelfMemberSubscription(guild);
|
||||||
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, guildMetaResult?.value.iconResourceId ?? null, guildMetaResult?.guild ?? null);
|
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, guildMeta?.iconResourceId ?? null, guild);
|
||||||
|
|
||||||
const [ contextHover, mouseEnterCallable, mouseLeaveCallable ] = useContextHover(() => {
|
const [ contextHover, mouseEnterCallable, mouseLeaveCallable ] = useContextHover(() => {
|
||||||
if (!guildMetaResult) return null;
|
if (!guildMeta) return null;
|
||||||
if (!selfMemberResult || !selfMemberResult.value) return null;
|
if (!selfMemberResult || !selfMemberResult.value) return null;
|
||||||
const nameStyle = selfMemberResult.value.roleColor ? { color: selfMemberResult.value.roleColor } : {};
|
const nameStyle = selfMemberResult.value.roleColor ? { color: selfMemberResult.value.roleColor } : {};
|
||||||
return (
|
return (
|
||||||
<BasicHover relativeToRef={rootRef} side={BasicHoverSide.RIGHT} realignDeps={[ guildMetaResult, selfMemberResult ]}>
|
<BasicHover relativeToRef={rootRef} side={BasicHoverSide.RIGHT} realignDeps={[ guildMeta, selfMemberResult ]}>
|
||||||
<div className="guild-hover">
|
<div className="guild-hover">
|
||||||
<div className="tab">{BaseElements.TAB_LEFT}</div>
|
<div className="tab">{BaseElements.TAB_LEFT}</div>
|
||||||
<div className="info">
|
<div className="info">
|
||||||
<div className="guild-name">{guildMetaResult.value.name}</div>
|
<div className="guild-name">{guildMeta.name}</div>
|
||||||
<div className={'connection ' + selfMemberResult.value.status}>
|
<div className={'connection ' + selfMemberResult.value.status}>
|
||||||
<div className="status-circle" />
|
<div className="status-circle" />
|
||||||
<div className="display-name" style={nameStyle}>{selfMemberResult.value.displayName}</div>
|
<div className="display-name" style={nameStyle}>{selfMemberResult.value.displayName}</div>
|
||||||
@ -44,9 +45,10 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
|
|||||||
</div>
|
</div>
|
||||||
</BasicHover>
|
</BasicHover>
|
||||||
)
|
)
|
||||||
}, [ guildMetaResult, selfMemberResult ]);
|
}, [ guildMeta, selfMemberResult ]);
|
||||||
|
|
||||||
const leaveGuildCallable = useCallback(async () => {
|
const leaveGuildCallable = useCallback(async () => {
|
||||||
|
if (!guildsManager) return;
|
||||||
guild.disconnect();
|
guild.disconnect();
|
||||||
await guildsManager.removeGuild(guild);
|
await guildsManager.removeGuild(guild);
|
||||||
}, [ guildsManager, guild ])
|
}, [ guildsManager, guild ])
|
||||||
@ -63,9 +65,14 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
|
|||||||
)
|
)
|
||||||
}, [ leaveGuildCallable ]);
|
}, [ leaveGuildCallable ]);
|
||||||
|
|
||||||
|
|
||||||
|
const setSelfActiveGuild = useCallback(() => {
|
||||||
|
setSelectedGuildId(guild.id);
|
||||||
|
}, [ guild ]);
|
||||||
|
|
||||||
const className = useMemo(() => {
|
const className = useMemo(() => {
|
||||||
return activeGuild && guild.id === activeGuild.id ? 'guild active' : 'guild';
|
return selectedGuild && guild.id === selectedGuild.id ? 'guild active' : 'guild';
|
||||||
}, [ guild, activeGuild ]);
|
}, [ guild, selectedGuild ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -3,7 +3,7 @@ import React, { FC, ReactNode, useCallback, useMemo } from 'react';
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { Member, Message } from '../../../data-types';
|
import { Member, Message } from '../../../data-types';
|
||||||
import CombinedGuild from '../../../guild-combined';
|
import CombinedGuild from '../../../guild-combined';
|
||||||
import { overlayState } from '../../atoms';
|
import { overlayState } from '../../require/atoms';
|
||||||
import ImageContextMenu from '../../contexts/context-menu-image';
|
import ImageContextMenu from '../../contexts/context-menu-image';
|
||||||
import ImageOverlay from '../../overlays/overlay-image';
|
import ImageOverlay from '../../overlays/overlay-image';
|
||||||
import ElementsUtil, { IAlignment } from '../../require/elements-util';
|
import ElementsUtil, { IAlignment } from '../../require/elements-util';
|
||||||
|
@ -1,27 +1,20 @@
|
|||||||
import React, { Dispatch, FC, SetStateAction, useMemo } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import CombinedGuild from '../../guild-combined';
|
import { useRecoilValue } from 'recoil';
|
||||||
import GuildsManager from '../../guilds-manager';
|
import { GuildMetadata } from '../../data-types';
|
||||||
|
import { guildsState, GuildWithErrorableValue } from '../require/atoms';
|
||||||
import GuildListElement from './components/guild-list-element';
|
import GuildListElement from './components/guild-list-element';
|
||||||
|
|
||||||
export interface GuildListProps {
|
const GuildList: FC = () => {
|
||||||
guildsManager: GuildsManager;
|
const guildsWithMeta = useRecoilValue(guildsState);
|
||||||
guilds: CombinedGuild[];
|
|
||||||
activeGuild: CombinedGuild | null;
|
|
||||||
setActiveGuild: Dispatch<SetStateAction<CombinedGuild | null>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GuildList: FC<GuildListProps> = (props: GuildListProps) => {
|
|
||||||
const { guildsManager, guilds, activeGuild, setActiveGuild } = props;
|
|
||||||
|
|
||||||
const guildElements = useMemo(() => {
|
const guildElements = useMemo(() => {
|
||||||
return guilds.map((guild: CombinedGuild) => (
|
if (!guildsWithMeta) return null;
|
||||||
|
return guildsWithMeta.map((guildWithMeta: GuildWithErrorableValue<GuildMetadata>) => (
|
||||||
<GuildListElement
|
<GuildListElement
|
||||||
key={guild.id}
|
guildWithMeta={guildWithMeta} key={guildWithMeta.guild.id}
|
||||||
guildsManager={guildsManager} guild={guild}
|
|
||||||
activeGuild={activeGuild} setSelfActiveGuild={() => { setActiveGuild(guild); } }
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}, [ guildsManager, guilds, activeGuild ]);
|
}, [ guildsWithMeta ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="guild-list">
|
<div className="guild-list">
|
||||||
|
@ -3,8 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
|||||||
import Logger from '../../../../logger/logger';
|
import Logger from '../../../../logger/logger';
|
||||||
const LOG = Logger.create(__filename, electronConsole);
|
const LOG = Logger.create(__filename, electronConsole);
|
||||||
|
|
||||||
import React, { Dispatch, FC, ReactNode, SetStateAction, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { FC, ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import GuildsManager from '../../guilds-manager';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import TextInput from '../components/input-text';
|
import TextInput from '../components/input-text';
|
||||||
import ImageEditInput from '../components/input-image-edit';
|
import ImageEditInput from '../components/input-image-edit';
|
||||||
@ -17,8 +16,8 @@ import { useAsyncSubmitButton, useOneTimeAsyncAction } from '../require/react-he
|
|||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import Button from '../components/button';
|
import Button from '../components/button';
|
||||||
import Overlay from '../components/overlay';
|
import Overlay from '../components/overlay';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { overlayState } from '../atoms';
|
import { guildsManagerState, overlayState, selectedGuildIdState } from '../require/atoms';
|
||||||
|
|
||||||
export interface IAddGuildData {
|
export interface IAddGuildData {
|
||||||
name: string,
|
name: string,
|
||||||
@ -52,13 +51,14 @@ function getExampleAvatarPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AddGuildOverlayProps {
|
export interface AddGuildOverlayProps {
|
||||||
guildsManager: GuildsManager;
|
|
||||||
addGuildData: IAddGuildData;
|
addGuildData: IAddGuildData;
|
||||||
setActiveGuild: Dispatch<SetStateAction<CombinedGuild | null>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps) => {
|
const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps) => {
|
||||||
const { guildsManager, addGuildData, setActiveGuild } = props;
|
const { addGuildData } = props;
|
||||||
|
|
||||||
|
const guildsManager = useRecoilValue(guildsManagerState);
|
||||||
|
const setSelectedGuildId = useSetRecoilState(selectedGuildIdState);
|
||||||
|
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
|
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
|
||||||
@ -97,6 +97,7 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
|||||||
|
|
||||||
const [ submitFunc, submitButtonText, submitButtonShaking, _, submitFailMessage ] = useAsyncSubmitButton(
|
const [ submitFunc, submitButtonText, submitButtonShaking, _, submitFailMessage ] = useAsyncSubmitButton(
|
||||||
async () => {
|
async () => {
|
||||||
|
if (!guildsManager) return { result: null, errorMessage: null };
|
||||||
if (validationErrorMessage || !avatarBuff) return { result: null, errorMessage: 'Validation failed' };
|
if (validationErrorMessage || !avatarBuff) return { result: null, errorMessage: 'Validation failed' };
|
||||||
if (expired) return { result: null, errorMessage: 'Token expired' };
|
if (expired) return { result: null, errorMessage: 'Token expired' };
|
||||||
|
|
||||||
@ -108,7 +109,7 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
|||||||
return { result: null, errorMessage: (e as Error).message ?? 'Error adding new guild' };
|
return { result: null, errorMessage: (e as Error).message ?? 'Error adding new guild' };
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveGuild(newGuild);
|
setSelectedGuildId(newGuild.id);
|
||||||
|
|
||||||
setOverlay(null);
|
setOverlay(null);
|
||||||
return { result: newGuild, errorMessage: null };
|
return { result: newGuild, errorMessage: null };
|
||||||
|
@ -14,7 +14,7 @@ import { useAsyncSubmitButton } from '../require/react-helper';
|
|||||||
import Button from '../components/button';
|
import Button from '../components/button';
|
||||||
import Overlay from '../components/overlay';
|
import Overlay from '../components/overlay';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { overlayState } from '../atoms';
|
import { overlayState } from '../require/atoms';
|
||||||
|
|
||||||
export interface ChannelOverlayProps {
|
export interface ChannelOverlayProps {
|
||||||
guild: CombinedGuild;
|
guild: CombinedGuild;
|
||||||
|
@ -15,7 +15,7 @@ import Button from '../components/button';
|
|||||||
import Overlay from '../components/overlay';
|
import Overlay from '../components/overlay';
|
||||||
import { SubscriptionResult, useResourceSubscription } from '../require/guild-subscriptions';
|
import { SubscriptionResult, useResourceSubscription } from '../require/guild-subscriptions';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { overlayState } from '../atoms';
|
import { overlayState } from '../require/atoms';
|
||||||
|
|
||||||
export interface PersonalizeOverlayProps {
|
export interface PersonalizeOverlayProps {
|
||||||
guild: CombinedGuild;
|
guild: CombinedGuild;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { atom, selector } from 'recoil';
|
import { atom, DefaultValue, selector } from 'recoil';
|
||||||
import { Channel, GuildMetadata, Member, Message } from '../data-types';
|
import { Channel, GuildMetadata, Member, Message } from '../../data-types';
|
||||||
import CombinedGuild from '../guild-combined';
|
import CombinedGuild from '../../guild-combined';
|
||||||
|
import GuildsManager from '../../guilds-manager';
|
||||||
|
|
||||||
export interface GuildWithValue<T> {
|
export interface GuildWithValue<T> {
|
||||||
guild: CombinedGuild;
|
guild: CombinedGuild;
|
||||||
@ -25,9 +26,18 @@ export const overlayState = atom<ReactNode>({
|
|||||||
default: null
|
default: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const guildsManagerState = atom<GuildsManager | null>({
|
||||||
|
key: 'guildsManagerState',
|
||||||
|
default: null,
|
||||||
|
// Allow mutability so that we can have changes to internal guild stuff
|
||||||
|
// We will still listen to new/update/delete/conflict events to maintain the UI's state
|
||||||
|
dangerouslyAllowMutability: true
|
||||||
|
})
|
||||||
|
|
||||||
export const guildsState = atom<GuildWithErrorableValue<GuildMetadata>[] | null>({
|
export const guildsState = atom<GuildWithErrorableValue<GuildMetadata>[] | null>({
|
||||||
key: 'guildsState',
|
key: 'guildsState',
|
||||||
default: null
|
default: null,
|
||||||
|
dangerouslyAllowMutability: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectedGuildIdState = atom<number | null>({
|
export const selectedGuildIdState = atom<number | null>({
|
||||||
@ -45,7 +55,8 @@ export const selectedGuildWithMetaState = selector<GuildWithErrorableValue<Guild
|
|||||||
if (guildId === null) return null;
|
if (guildId === null) return null;
|
||||||
|
|
||||||
return guildsWithMeta.find(guildWithMeta => guildWithMeta.guild.id === guildId) ?? null;
|
return guildsWithMeta.find(guildWithMeta => guildWithMeta.guild.id === guildId) ?? null;
|
||||||
}
|
},
|
||||||
|
dangerouslyAllowMutability: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectedGuildState = selector<CombinedGuild | null>({
|
export const selectedGuildState = selector<CombinedGuild | null>({
|
||||||
@ -55,7 +66,8 @@ export const selectedGuildState = selector<CombinedGuild | null>({
|
|||||||
if (guildWithMeta === null) return null;
|
if (guildWithMeta === null) return null;
|
||||||
|
|
||||||
return guildWithMeta.guild;
|
return guildWithMeta.guild;
|
||||||
}
|
},
|
||||||
|
dangerouslyAllowMutability: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectedGuildMembersState = atom<GuildWithErrorableValue<Member[]> | null>({
|
export const selectedGuildMembersState = atom<GuildWithErrorableValue<Member[]> | null>({
|
142
src/client/webapp/elements/require/setup-guild-recoil.ts
Normal file
142
src/client/webapp/elements/require/setup-guild-recoil.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import * as electronRemote from '@electron/remote';
|
||||||
|
const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||||
|
import Logger from '../../../../logger/logger';
|
||||||
|
const LOG = Logger.create(__filename, electronConsole);
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
|
||||||
|
import { GuildMetadata } from "../../data-types";
|
||||||
|
import GuildsManager from "../../guilds-manager";
|
||||||
|
import { guildsManagerState, guildsState, GuildWithErrorableValue, selectedGuildIdState, selectedGuildState } from "./atoms";
|
||||||
|
import { useIsMountedRef } from "./react-helper";
|
||||||
|
import CombinedGuild from '../../guild-combined';
|
||||||
|
import { AutoVerifierChangesType } from '../../auto-verifier';
|
||||||
|
|
||||||
|
|
||||||
|
/** Manages the guildsManager to ensure that guildsState is up to date with a list of guilds
|
||||||
|
* and that the guilds attempt to fetch their corresponding guildMetas
|
||||||
|
* This class it a bit more complicated since it has to fetch/listen to guild metadata changes on many guilds at once */
|
||||||
|
function useManagedGuildsManager(guildsManager: GuildsManager) {
|
||||||
|
const isMounted = useIsMountedRef();
|
||||||
|
const metadataInProgress = useRef<Set<number>>(new Set<number>());
|
||||||
|
const [ guildsWithMeta, setGuildsWithMeta ] = useRecoilState(guildsState);
|
||||||
|
|
||||||
|
// Update the guild list
|
||||||
|
// If new guilds are added, add them with null metadata
|
||||||
|
const onChange = useCallback(() => {
|
||||||
|
const guilds = guildsManager.guilds;
|
||||||
|
setGuildsWithMeta((prevGuildsWithMeta) => {
|
||||||
|
if (!prevGuildsWithMeta) {
|
||||||
|
return guilds.map(guild => ({ guild, value: null, valueError: null }));
|
||||||
|
} else {
|
||||||
|
const newGuilds = guilds.filter(guild => !prevGuildsWithMeta.find(guildWithMeta => guildWithMeta.guild.id === guild.id));
|
||||||
|
const newGuildsWithMeta = prevGuildsWithMeta
|
||||||
|
.filter(guildWithMeta => guilds.find(guild => guildWithMeta.guild.id === guild.id)) // Remove old guilds
|
||||||
|
.concat(newGuilds.map(guild => ({ guild, value: null, valueError: null }))); // Add new guilds
|
||||||
|
return newGuildsWithMeta;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [ guildsManager, setGuildsWithMeta ]);
|
||||||
|
|
||||||
|
// Listen for changes to the guild manager
|
||||||
|
useEffect(() => {
|
||||||
|
// Set initial guilds
|
||||||
|
onChange();
|
||||||
|
|
||||||
|
// Listen for updates
|
||||||
|
guildsManager.on('update-guilds', onChange);
|
||||||
|
return () => {
|
||||||
|
guildsManager.off('update-guilds', onChange);
|
||||||
|
}
|
||||||
|
}, [ guildsManager, onChange ]);
|
||||||
|
|
||||||
|
|
||||||
|
const setGuildWithMeta = useCallback((guildId: number, newGuildWithMeta: GuildWithErrorableValue<GuildMetadata>) => {
|
||||||
|
setGuildsWithMeta(prevGuildsWithMeta => {
|
||||||
|
if (!prevGuildsWithMeta) return null;
|
||||||
|
return prevGuildsWithMeta.map(guildWithMeta => guildWithMeta.guild.id === guildId ? newGuildWithMeta : guildWithMeta);
|
||||||
|
});
|
||||||
|
}, [ setGuildsWithMeta ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (guildsWithMeta === null) return;
|
||||||
|
|
||||||
|
// Load guild metadatas
|
||||||
|
for (const { guild, value, valueError } of guildsWithMeta) {
|
||||||
|
// Only fetch if the value has not been fetched yet and we are not currently fetching it.
|
||||||
|
if (!metadataInProgress.current.has(guild.id) && value === null && valueError === null) {
|
||||||
|
(async () => {
|
||||||
|
metadataInProgress.current.add(guild.id);
|
||||||
|
try {
|
||||||
|
const guildMeta = await guild.fetchMetadata();
|
||||||
|
if (!isMounted.current) return;
|
||||||
|
setGuildWithMeta(guild.id, { guild, value: guildMeta, valueError: null });
|
||||||
|
} catch (e: unknown) {
|
||||||
|
LOG.error(`error fetching metadata for g#${guild.id}`, e);
|
||||||
|
if (!isMounted.current) return;
|
||||||
|
setGuildWithMeta(guild.id, { guild, value: null, valueError: e });
|
||||||
|
}
|
||||||
|
metadataInProgress.current.delete(guild.id);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ guildsWithMeta, setGuildWithMeta ]);
|
||||||
|
|
||||||
|
// Listen for changes to the guild metadata
|
||||||
|
useEffect(() => {
|
||||||
|
if (guildsWithMeta === null) return;
|
||||||
|
|
||||||
|
// Listen for changes to metadata
|
||||||
|
function handleUpdateOrConflict(guild: CombinedGuild, guildMeta: GuildMetadata) {
|
||||||
|
setGuildWithMeta(guild.id, { guild, value: guildMeta, valueError: null });
|
||||||
|
}
|
||||||
|
const callbacks = new Map<CombinedGuild, {
|
||||||
|
updateCallback: (guildMeta: GuildMetadata) => void,
|
||||||
|
conflictCallback: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, guildMeta: GuildMetadata) => void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
for (const { guild } of guildsWithMeta) {
|
||||||
|
const updateCallback = (guildMeta: GuildMetadata) => handleUpdateOrConflict(guild, guildMeta);
|
||||||
|
const conflictCallback = (_changesType: AutoVerifierChangesType, _oldGuildMeta: GuildMetadata, guildMeta: GuildMetadata) => handleUpdateOrConflict(guild, guildMeta);
|
||||||
|
guild.on('update-metadata', updateCallback);
|
||||||
|
guild.on('conflict-metadata', conflictCallback);
|
||||||
|
callbacks.set(guild, { updateCallback, conflictCallback });
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const { guild } of guildsWithMeta) {
|
||||||
|
const { updateCallback, conflictCallback } = callbacks.get(guild) as {
|
||||||
|
updateCallback: (guildMeta: GuildMetadata) => void,
|
||||||
|
conflictCallback: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, guildMeta: GuildMetadata) => void
|
||||||
|
};
|
||||||
|
guild.off('update-metadata', updateCallback);
|
||||||
|
guild.off('conflict-metadata', conflictCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ guildsWithMeta ]);
|
||||||
|
|
||||||
|
//useEffect(() => {
|
||||||
|
// LOG.debug(`guildsWithMeta: `, { guildsWithMeta });
|
||||||
|
//}, [ guildsWithMeta ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entrypoint for recoil setup with guilds
|
||||||
|
export function useGuildsManagerWithRecoil(guildsManager: GuildsManager): void {
|
||||||
|
const setGuildsManager = useSetRecoilState(guildsManagerState);
|
||||||
|
const guilds = useRecoilValue(guildsState);
|
||||||
|
const [ selectedGuildId, setSelectedGuildId ] = useRecoilState(selectedGuildIdState);
|
||||||
|
|
||||||
|
// Set the guilds manager atom
|
||||||
|
useEffect(() => { setGuildsManager(guildsManager); }, [ guildsManager, setGuildsManager ]);
|
||||||
|
|
||||||
|
// Manage the guilds within the manager (adds them to the guilds atom and automatically loads and updates their metadata)
|
||||||
|
useManagedGuildsManager(guildsManager);
|
||||||
|
|
||||||
|
// Make sure that the first guild is set to the active guild once we get some guilds
|
||||||
|
useEffect(() => {
|
||||||
|
if (guilds && guilds.length > 0 && selectedGuildId === null) {
|
||||||
|
setSelectedGuildId((guilds[0] as GuildWithErrorableValue<GuildMetadata>).guild.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
|||||||
import React, { FC, ReactNode } from 'react';
|
import React, { FC, ReactNode } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import GuildsManager from '../guilds-manager';
|
import GuildsManager from '../guilds-manager';
|
||||||
import { overlayState } from './atoms';
|
import { overlayState } from './require/atoms';
|
||||||
|
import { useGuildsManagerWithRecoil } from './require/setup-guild-recoil';
|
||||||
import GuildsManagerElement from './sections/guilds-manager';
|
import GuildsManagerElement from './sections/guilds-manager';
|
||||||
import TitleBar from './sections/title-bar';
|
import TitleBar from './sections/title-bar';
|
||||||
|
|
||||||
@ -12,12 +13,14 @@ export interface RootElementProps {
|
|||||||
const RootElement: FC<RootElementProps> = (props: RootElementProps) => {
|
const RootElement: FC<RootElementProps> = (props: RootElementProps) => {
|
||||||
const { guildsManager } = props;
|
const { guildsManager } = props;
|
||||||
|
|
||||||
const [ overlay, setOverlay ] = useRecoilState<ReactNode>(overlayState);
|
const overlay = useRecoilValue<ReactNode>(overlayState);
|
||||||
|
|
||||||
|
useGuildsManagerWithRecoil(guildsManager);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TitleBar />
|
<TitleBar />
|
||||||
<GuildsManagerElement guildsManager={guildsManager} />
|
<GuildsManagerElement />
|
||||||
<div className="react-overlays">{overlay}</div>
|
<div className="react-overlays">{overlay}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,9 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
|||||||
import Logger from '../../../../logger/logger';
|
import Logger from '../../../../logger/logger';
|
||||||
const LOG = Logger.create(__filename, electronConsole);
|
const LOG = Logger.create(__filename, electronConsole);
|
||||||
|
|
||||||
import React, { Dispatch, FC, ReactNode, SetStateAction, useRef } from 'react';
|
import React, { FC, ReactNode, useRef } from 'react';
|
||||||
import CombinedGuild from '../../guild-combined';
|
|
||||||
import GuildsManager from '../../guilds-manager';
|
|
||||||
import GuildList from '../lists/guild-list';
|
import GuildList from '../lists/guild-list';
|
||||||
import { useAsyncVoidCallback, useContextHover } from '../require/react-helper';
|
import { useAsyncVoidCallback, useContextHover } from '../require/react-helper';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
@ -13,19 +11,10 @@ import AddGuildOverlay from '../overlays/overlay-add-guild';
|
|||||||
import ErrorMessageOverlay from '../overlays/overlay-error-message';
|
import ErrorMessageOverlay from '../overlays/overlay-error-message';
|
||||||
import BasicHover, { BasicHoverSide } from '../contexts/context-hover-basic';
|
import BasicHover, { BasicHoverSide } from '../contexts/context-hover-basic';
|
||||||
import BaseElements from '../require/base-elements';
|
import BaseElements from '../require/base-elements';
|
||||||
import { overlayState } from '../atoms';
|
import { overlayState } from '../require/atoms';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
export interface GuildListContainerProps {
|
const GuildListContainer: FC = () => {
|
||||||
guildsManager: GuildsManager;
|
|
||||||
guilds: CombinedGuild[];
|
|
||||||
activeGuild: CombinedGuild | null;
|
|
||||||
setActiveGuild: Dispatch<SetStateAction<CombinedGuild | null>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GuildListContainer: FC<GuildListContainerProps> = (props: GuildListContainerProps) => {
|
|
||||||
const { guildsManager, guilds, activeGuild, setActiveGuild } = props;
|
|
||||||
|
|
||||||
const addGuildRef = useRef<HTMLDivElement>(null);
|
const addGuildRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
|
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
|
||||||
@ -71,16 +60,13 @@ const GuildListContainer: FC<GuildListContainerProps> = (props: GuildListContain
|
|||||||
LOG.debug('bad guild data:', { addGuildData, fileText });
|
LOG.debug('bad guild data:', { addGuildData, fileText });
|
||||||
setOverlay(<ErrorMessageOverlay title="Unable to parse guild file" message="Bad guild data" />);
|
setOverlay(<ErrorMessageOverlay title="Unable to parse guild file" message="Bad guild data" />);
|
||||||
} else {
|
} else {
|
||||||
setOverlay(<AddGuildOverlay guildsManager={guildsManager} addGuildData={addGuildData} setActiveGuild={setActiveGuild} />);
|
setOverlay(<AddGuildOverlay addGuildData={addGuildData} />);
|
||||||
}
|
}
|
||||||
}, [ guildsManager ]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="guild-list-container">
|
<div className="guild-list-container">
|
||||||
<GuildList
|
<GuildList />
|
||||||
guildsManager={guildsManager} guilds={guilds}
|
|
||||||
activeGuild={activeGuild} setActiveGuild={setActiveGuild}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
ref={addGuildRef}
|
ref={addGuildRef}
|
||||||
className="add-guild" onClick={onAddGuildClickCallback}
|
className="add-guild" onClick={onAddGuildClickCallback}
|
||||||
|
@ -1,36 +1,16 @@
|
|||||||
import React, { Dispatch, FC, ReactNode, SetStateAction, useEffect, useState } from 'react';
|
import React, { FC } from 'react';
|
||||||
import CombinedGuild from '../../guild-combined';
|
import { useRecoilValue } from 'recoil';
|
||||||
import GuildsManager from '../../guilds-manager';
|
import { selectedGuildState } from '../require/atoms';
|
||||||
import { useGuildListSubscription } from '../require/guilds-manager-subscriptions';
|
|
||||||
import GuildElement from './guild';
|
import GuildElement from './guild';
|
||||||
import GuildListContainer from './guild-list-container';
|
import GuildListContainer from './guild-list-container';
|
||||||
|
|
||||||
export interface GuildsManagerElementProps {
|
const GuildsManagerElement: FC = () => {
|
||||||
guildsManager: GuildsManager;
|
const selectedGuild = useRecoilValue(selectedGuildState);
|
||||||
}
|
|
||||||
|
|
||||||
const GuildsManagerElement: FC<GuildsManagerElementProps> = (props: GuildsManagerElementProps) => {
|
|
||||||
const { guildsManager } = props;
|
|
||||||
|
|
||||||
const [ guilds ] = useGuildListSubscription(guildsManager);
|
|
||||||
const [ activeGuild, setActiveGuild ] = useState<CombinedGuild | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeGuild === null) {
|
|
||||||
// initial active channel is the first one in the list
|
|
||||||
if (guilds && guilds.length > 0) {
|
|
||||||
setActiveGuild(guilds[0] as CombinedGuild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [ guilds, activeGuild ]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="guilds-manager">
|
<div className="guilds-manager">
|
||||||
<GuildListContainer
|
<GuildListContainer />
|
||||||
guildsManager={guildsManager} guilds={guilds}
|
{selectedGuild && <GuildElement guild={selectedGuild} />}
|
||||||
activeGuild={activeGuild} setActiveGuild={setActiveGuild}
|
|
||||||
/>
|
|
||||||
{activeGuild && <GuildElement guild={activeGuild} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user