RECOIL IS SUPREME!

This commit is contained in:
Michael Peters 2022-02-02 23:05:38 -06:00
parent 7ceceecbb1
commit 222feb1de1
25 changed files with 813 additions and 266 deletions

View File

@ -47,7 +47,7 @@ export function isFailed<T>(withLoadableValue: WithLoadableValue<T>): withLoadab
return withLoadableValue.hasValueError;
}
// WARNING: This should NOT be used in a place that could prevent hooks from running
// WARNING: This must NOT be used in a place that could prevent hooks from running
// NOTE: This is useful to skip rendering elements that require an atom if its value is not available yet. Using this in the parent element will prevent the child eleemnt from
// needing to implement the complexity of loading without a value
export function isDevoidPendedOrFailed<T>(withNullableLoadableValue: WithLoadableValue<T> | null): withNullableLoadableValue is null | WithUnloadedValue | WithFailedValue {

View File

@ -5,8 +5,8 @@ const LOG = Logger.create(__filename, electronConsole);
import React, { FC, ReactNode, RefObject, useCallback, useEffect } from "react";
import { useActionWhenEscapeOrClickedOrContextOutsideEffect } from '../require/react-helper';
import { overlayState } from '../require/atoms';
import { useSetRecoilState } from 'recoil';
import { overlayState } from '../require/atoms-2';
interface OverlayProps {
childRootRef?: RefObject<HTMLElement>; // clicks outside this ref will close the overlay

View File

@ -1,9 +1,11 @@
import moment from 'moment';
import React, { FC } from 'react';
import { GuildMetadata, Member, Token } from '../../data-types';
import { useRecoilValue } from 'recoil';
import { Member, Token } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import Util from '../../util';
import { IAddGuildData } from '../overlays/overlay-add-guild';
import { GuildWithValue } from '../require/atoms';
import { guildMetaState, isLoaded } from '../require/atoms-2';
import BaseElements from '../require/base-elements';
import { useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
import { useAsyncVoidCallback, useDownloadButton, useOneTimeAsyncAction } from '../require/react-helper';
@ -12,33 +14,35 @@ import Button, { ButtonColorType } from './button';
export interface TokenRowProps {
url: string;
token: Token;
guildWithMeta: GuildWithValue<GuildMetadata>
guild: CombinedGuild;
}
const TokenRow: FC<TokenRowProps> = (props: TokenRowProps) => {
const { token, guildWithMeta } = props;
const { token, guild } = props;
const guildMeta = useRecoilValue(guildMetaState(guild.id));
const [ guildSocketConfigs ] = useOneTimeAsyncAction(
async () => {
return await guildWithMeta.guild.fetchSocketConfigs()
return await guild.fetchSocketConfigs()
},
null,
[ guildWithMeta ]
[ guild ]
);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guildWithMeta.guild, guildWithMeta.value.iconResourceId, guildWithMeta.guild);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null, guild);
const [ revoke ] = useAsyncVoidCallback(async () => {
await guildWithMeta.guild.requestDoRevokeToken(token.token);
}, [ guildWithMeta.guild, token ]);
await guild.requestDoRevokeToken(token.token);
}, [ guild, token ]);
const [ downloadFunc, downloadText, downloadShaking ] = useDownloadButton(
guildWithMeta.value.name + '.cordis',
isLoaded(guildMeta) ? guildMeta.value.name + '.cordis' : 'unknown-guild.cordis',
async () => {
if (guildSocketConfigs === null) return null;
if (guildWithMeta === null || guildWithMeta.value === null) return null;
if (!isLoaded(guildMeta)) return null; // TODO: Use placeholders / send error message?
const guildSocketConfig = Util.randomChoice(guildSocketConfigs);
const addGuildData: IAddGuildData = {
name: guildWithMeta.value.name,
name: guildMeta.value.name,
url: guildSocketConfig.url,
cert: guildSocketConfig.cert,
token: token.token,
@ -48,12 +52,12 @@ const TokenRow: FC<TokenRowProps> = (props: TokenRowProps) => {
const json = JSON.stringify(addGuildData);
return Buffer.from(json);
},
[ guildSocketConfigs, guildWithMeta, token, iconSrc ]
[ guildSocketConfigs, guildMeta, token, iconSrc ]
);
const userText = (token.member instanceof Member ? 'Used by ' + token.member.displayName : token.member?.id) ?? 'Unused Token';
return (
<div key={guildWithMeta.guild.id + token.token} className="token-row">
<div key={guild.id + token.token} className="token-row">
<div className="user-token">
<div className="user">{userText}</div>
<div className="token">{token.token}</div>

View File

@ -1,26 +1,28 @@
import React, { FC, ReactNode, RefObject, useCallback, useMemo } from 'react';
import { useSetRecoilState } from 'recoil';
import { GuildMetadata, Member } from '../../data-types';
import { GuildWithValue, overlayState } from '../require/atoms';
import { Member } from '../../data-types';
import PersonalizeOverlay from '../overlays/overlay-personalize';
import ContextMenu from './components/context-menu';
import CombinedGuild from '../../guild-combined';
import { overlayState } from '../require/atoms-2';
export interface ConnectionInfoContextMenuProps {
guildWithSelfMember: GuildWithValue<Member>;
guild: CombinedGuild;
selfMember: Member;
relativeToRef: RefObject<HTMLElement>;
close: () => void;
}
const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: ConnectionInfoContextMenuProps) => {
const { guildWithSelfMember, relativeToRef, close } = props;
const { guild, selfMember, relativeToRef, close } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState)
const setSelfStatus = useCallback(async (status: string) => {
if (guildWithSelfMember.value.status !== status) {
await guildWithSelfMember.guild.requestSetStatus(status);
if (selfMember.status !== status) {
await guild.requestSetStatus(status);
}
}, [ guildWithSelfMember ]);
}, [ guild, selfMember ]);
const statusElements = useMemo(() => {
return [ 'online', 'away', 'busy', 'invisible' ].map(status => {
@ -36,7 +38,7 @@ const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: Co
const openPersonalize = useCallback(() => {
close();
setOverlay(<PersonalizeOverlay guildWithSelfMember={guildWithSelfMember} />);
setOverlay(<PersonalizeOverlay guild={guild} selfMember={selfMember} />);
}, [ close ]);
const alignment = useMemo(() => {

View File

@ -5,33 +5,32 @@ const LOG = Logger.create(__filename, electronConsole);
import React, { FC, ReactNode, RefObject, useCallback, useMemo } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Member } from '../../data-types';
import { isDevoidPendedOrFailed, overlayState, selectedGuildWithMetaState } from '../require/atoms';
import ChannelOverlay from '../overlays/overlay-channel';
import GuildSettingsOverlay from '../overlays/overlay-guild-settings';
import BaseElements from '../require/base-elements';
import ContextMenu from './components/context-menu';
import { currGuildSelfMemberState, currGuildState, isLoaded, overlayState } from '../require/atoms-2';
export interface GuildTitleContextMenuProps {
close: () => void;
relativeToRef: RefObject<HTMLElement>;
selfMember: Member;
}
const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitleContextMenuProps) => {
const { close, relativeToRef, selfMember } = props;
const { close, relativeToRef } = props;
const guildWithMeta = useRecoilValue(selectedGuildWithMetaState);
const guild = useRecoilValue(currGuildState);
const selfMember = useRecoilValue(currGuildSelfMemberState);
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const openGuildSettings = useCallback(() => {
close();
if (isDevoidPendedOrFailed(guildWithMeta)) {
LOG.warn('not opening guild settings context menu due to devoid/pended/failed guildWithMeta');
if (guild === null) {
LOG.warn('not launching settings for a null guild');
return;
}
setOverlay(<GuildSettingsOverlay guildWithMeta={guildWithMeta} />);
setOverlay(<GuildSettingsOverlay guild={guild} />);
}, [ close ]);
const openCreateChannel = useCallback(() => {
@ -40,7 +39,8 @@ const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitle
}, [ close ]);
const guildSettingsElement = useMemo(() => {
if (!selfMember.privileges.includes('modify_profile')) return null;
if (!isLoaded(selfMember)) return null;
if (!selfMember.value.privileges.includes('modify_profile')) return null;
return (
<div className="item guild-settings" onClick={openGuildSettings}>
<div className="icon">{BaseElements.COG}</div>
@ -50,7 +50,8 @@ const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitle
}, [ selfMember, openGuildSettings ]);
const createChannelElement = useMemo(() => {
if (!selfMember.privileges.includes('modify_channels')) return null;
if (!isLoaded(selfMember)) return null;
if (!selfMember.value.privileges.includes('modify_channels')) return null;
return (
<div className="item create-channel" onClick={openCreateChannel}>
<div className="icon">{BaseElements.CREATE}</div>

View File

@ -6,7 +6,7 @@ const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useEffect, useMemo, useState } from 'react';
import Display from '../components/display';
import InvitePreview from '../components/invite-preview';
import { GuildMetadata, Token } from '../../data-types';
import { Token } from '../../data-types';
import { useAsyncSubmitButton } from '../require/react-helper';
import { Duration } from 'moment';
import moment from 'moment';
@ -14,22 +14,27 @@ import DropdownInput from '../components/input-dropdown';
import Button from '../components/button';
import { useTokensSubscription, useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
import TokenRow from '../components/token-row';
import { GuildWithValue } from '../require/atoms';
import CombinedGuild from '../../guild-combined';
import { useRecoilValue } from 'recoil';
import { guildMetaState, isLoaded } from '../require/atoms-2';
export interface GuildInvitesDisplayProps {
guildWithMeta: GuildWithValue<GuildMetadata>;
guild: CombinedGuild;
}
const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDisplayProps) => {
const { guildWithMeta } = props;
const { guild } = props;
const guildMeta = useRecoilValue(guildMetaState(guild.id));
const url = 'https://localhost:3030'; // TODO: this will likely be a dropdown list at some point
const [ _fetchRetryCallable, tokensResult, tokensError ] = useTokensSubscription(guildWithMeta.guild);
// TODO: Recoilize tokens :)
const [ _fetchRetryCallable, tokensResult, tokensError ] = useTokensSubscription(guild);
const [ expiresFromNow, setExpiresFromNow ] = useState<Duration | null>(moment.duration(1, 'day'));
const [ expiresFromNowText, setExpiresFromNowText ] = useState<string>('1 day');
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guildWithMeta.guild, guildWithMeta.value.iconResourceId ?? null, guildWithMeta.guild);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null, guild);
useEffect(() => {
if (expiresFromNowText === 'never') {
@ -44,14 +49,14 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
const [ createTokenFunc, tokenButtonText, tokenButtonShaking, _, createTokenFailMessage ] = useAsyncSubmitButton(
async () => {
try {
const createdToken = await guildWithMeta.guild.requestDoCreateToken(expiresFromNowText === 'never' ? null : expiresFromNowText)
const createdToken = await guild.requestDoCreateToken(expiresFromNowText === 'never' ? null : expiresFromNowText)
return { result: createdToken, errorMessage: null };
} catch (e: unknown) {
LOG.error('error creating token', e);
return { result: null, errorMessage: 'Error creating token' };
}
},
[ guildWithMeta, expiresFromNowText ],
[ guild, expiresFromNowText ],
{ start: 'Create Token', done: 'Create Another Token' }
);
@ -65,11 +70,9 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
// TODO: Try Again
return <div className="tokens-failed">Unable to load tokens</div>;
}
if (!guildWithMeta?.value) {
return <div className="no-guild-meta">No Guild Metadata</div>;
}
return tokensResult?.value?.map((token: Token) => <TokenRow key={guildWithMeta.guild.id + token.token} url={url} token={token} guildWithMeta={guildWithMeta} />);
}, [ url, guildWithMeta, tokensResult, tokensError ]);
// TODO: Try again?
return tokensResult?.value?.map((token: Token) => <TokenRow key={guild.id + token.token} url={url} token={token} guild={guild} />);
}, [ url, guild, tokensResult, tokensError ]);
return (
<Display
@ -89,7 +92,7 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
<div><Button shaking={tokenButtonShaking} onClick={createTokenFunc}>{tokenButtonText}</Button></div>
</div>
<InvitePreview
name={guildWithMeta.value.name} iconSrc={iconSrc}
name={isLoaded(guildMeta) ? guildMeta.value.name : 'Your Guild'} iconSrc={iconSrc}
url={url} expiresFromNow={expiresFromNow}
/>
</div>
@ -101,7 +104,8 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
</div>
</div>
</Display>
)
);
};
export default GuildInvitesDisplay;

View File

@ -10,17 +10,20 @@ import Display from '../components/display';
import TextInput from '../components/input-text';
import ImageEditInput from '../components/input-image-edit';
import { useResourceSubscription } from '../require/guild-subscriptions';
import { GuildWithValue } from '../require/atoms';
import { GuildMetadata } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { guildMetaState, isLoaded } from '../require/atoms-2';
import { useRecoilValue } from 'recoil';
interface GuildOverviewDisplayProps {
guildWithMeta: GuildWithValue<GuildMetadata>;
guild: CombinedGuild;
}
const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOverviewDisplayProps) => {
const { guildWithMeta } = props;
const { guild } = props;
const guildMeta = useRecoilValue(guildMetaState(guild.id));
// TODO: Use the one from guild.tsx (for both of these?)
const [ iconResourceResult, iconResourceError ] = useResourceSubscription(guildWithMeta.guild, guildWithMeta.value.iconResourceId, guildWithMeta.guild);
const [ iconResourceResult, iconResourceError ] = useResourceSubscription(guild, isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null, guild);
const [ savedName, setSavedName ] = useState<string | null>(null);
const [ savedIconBuff, setSavedIconBuff ] = useState<Buffer | null>(null);
@ -38,10 +41,10 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
const [ iconInputMessage, setIconInputMessage ] = useState<string | null>(null);
useEffect(() => {
if (guildWithMeta === null || guildWithMeta.value === null) return;
if (name === savedName) setName(guildWithMeta.value.name);
setSavedName(guildWithMeta.value.name);
}, [ guildWithMeta ]);
if (!isLoaded(guildMeta)) return;
if (name === savedName) setName(guildMeta.value.name);
setSavedName(guildMeta.value.name);
}, [ guildMeta ]);
useEffect(() => {
if (iconResourceResult && iconResourceResult.value) {
@ -82,7 +85,7 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
if (name !== savedName) {
// Save name
try {
await guildWithMeta.guild.requestSetGuildName(name);
await guild.requestSetGuildName(name);
setSavedName(name);
} catch (e: unknown) {
LOG.error('error setting guild name', e);
@ -96,7 +99,7 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
// Save icon
try {
LOG.debug('saving icon');
await guildWithMeta.guild.requestSetGuildIcon(iconBuff);
await guild.requestSetGuildIcon(iconBuff);
setSavedIconBuff(iconBuff);
} catch (e: unknown) {
LOG.error('error setting guild icon', e);
@ -107,7 +110,7 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
}
setSaving(false);
}, [ errorMessage, saving, name, savedName, iconBuff ]);
}, [ name, iconBuff, errorMessage, saving, guild, iconBuff, savedIconBuff ]);
return (
<Display

View File

@ -1,42 +1,29 @@
import React, { Dispatch, FC, ReactNode, SetStateAction, useMemo } from 'react'
import { Channel, Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import React, { FC, useMemo } from 'react'
import { useRecoilValue } from 'recoil';
import { Channel } from '../../data-types';
import { currGuildChannelsState, currGuildSelfMemberState, currGuildState, isFailed, isLoaded } from '../require/atoms-2';
import ChannelElement from './components/channel-element';
export interface ChannelListProps {
guild: CombinedGuild;
selfMember: Member | null;
channels: Channel[] | null;
channelsFetchError: unknown | null;
activeChannel: Channel | null;
setActiveChannel: Dispatch<SetStateAction<Channel | null>>;
}
const ChannelList: FC = () => {
const guild = useRecoilValue(currGuildState);
const selfMember = useRecoilValue(currGuildSelfMemberState);
const channels = useRecoilValue(currGuildChannelsState);
const ChannelList: FC<ChannelListProps> = (props: ChannelListProps) => {
const { guild, selfMember, channels, channelsFetchError, activeChannel, setActiveChannel } = props;
const hasModifyPrivilege = selfMember && selfMember.privileges.includes('modify_channels');
const hasModifyPrivilege = useMemo(() => {
if (!isLoaded(selfMember)) return false;
return selfMember.value.privileges.includes('modify_channels');
}, [ selfMember ]);
const baseClassName = hasModifyPrivilege ? 'channel-list modify_channels' : 'channel-list'
const channelElements = useMemo(() => {
if (!selfMember) return null;
if (channelsFetchError) {
// TODO: Retry button on error (pass in retryChannels)
return <div className="channels-failed">Unable to load channels</div>
}
if (!channels) {
return <div className="channels-loading">Loading channels...</div>
}
return channels.map((channel: Channel) => (
<ChannelElement
key={guild.id + channel.id} guild={guild}
selfMember={selfMember}
channel={channel} activeChannel={activeChannel}
setActiveChannel={() => { setActiveChannel(channel); }}
/>
// TODO: Retry button on error
if (isFailed(channels)) return <div className="channels-failed">Unable to load channels</div>;
if (guild === null || !isLoaded(channels)) return <div className="channels-loading">Loading channels...</div>; // Unloaded/Pending
return channels.value.map((channel: Channel) => (
<ChannelElement key={guild.id + channel.id} guild={guild} channel={channel} />
));
}, [ selfMember, channelsFetchError, channels, guild, selfMember, activeChannel ]);
}, [ channels, guild ]);
return (
<div className={baseClassName}>

View File

@ -4,31 +4,31 @@ import Logger from '../../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { Dispatch, FC, MouseEvent, ReactNode, SetStateAction, useCallback, useRef } from 'react'
import { Channel, Member } from '../../../data-types';
import { Channel } from '../../../data-types';
import CombinedGuild from '../../../guild-combined';
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 '../../require/atoms';
import { useSetRecoilState } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currGuildActiveChannelState, guildActiveChannelIdState, isLoaded, overlayState } from '../../require/atoms-2';
export interface ChannelElementProps {
guild: CombinedGuild;
channel: Channel;
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>>;
}
const ChannelElement: FC<ChannelElementProps> = (props: ChannelElementProps) => {
const { guild, channel, selfMember, activeChannel, setActiveChannel } = props;
const { guild, channel } = props;
const modifyRef = useRef<HTMLDivElement>(null);
const activeChannel = useRecoilValue(currGuildActiveChannelState);
const setActiveChannelId = useSetRecoilState(guildActiveChannelIdState(guild.id));
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const baseClassName = activeChannel?.id === channel.id ? 'channel text active' : 'channel text';
const modifyRef = useRef<HTMLDivElement>(null);
const baseClassName = (isLoaded(activeChannel) && activeChannel.value.id === channel.id) ? 'channel text active' : 'channel-text';
const [ modifyContextHover, modifyMouseEnterCallable, modifyMouseLeaveCallable ] = useContextHover(
() => {
@ -45,8 +45,8 @@ const ChannelElement: FC<ChannelElementProps> = (props: ChannelElementProps) =>
const setSelfActiveChannel = useCallback((event: MouseEvent<HTMLDivElement>) => {
if (modifyRef.current?.contains(event.target as Node)) return; // ignore "modify" button clicks
setActiveChannel(channel);
}, [ modifyRef, channel ]);
setActiveChannelId(channel.id);
}, [ modifyRef, channel, setActiveChannelId ]);
const launchModify = useCallback(() => {
setOverlay(<ChannelOverlay channel={channel} />);

View File

@ -1,51 +1,51 @@
import React, { FC, useCallback, useMemo, useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { GuildMetadata } from '../../../data-types';
import { useRecoilState, useRecoilValue } from 'recoil';
import CombinedGuild from '../../../guild-combined';
import ContextMenu from '../../contexts/components/context-menu';
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
import { guildsManagerState, GuildWithErrorableValue, selectedGuildIdState, selectedGuildState } from '../../require/atoms';
import { currGuildIdState, guildMetaState, guildSelfMemberState, guildsManagerState, isLoaded } from '../../require/atoms-2';
import BaseElements from '../../require/base-elements';
import { IAlignment } from '../../require/elements-util';
import { useSelfMemberSubscription, useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions';
import { useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions';
import { useContextClickContextMenu, useContextHover } from '../../require/react-helper';
export interface GuildListElementProps {
guildWithMeta: GuildWithErrorableValue<GuildMetadata>,
guild: CombinedGuild;
}
const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProps) => {
const { guildWithMeta: { guild, value: guildMeta, valueError: _guildMetaError } } = props;
const { guild } = props;
const rootRef = useRef<HTMLDivElement>(null);
const guildsManager = useRecoilValue(guildsManagerState);
const selectedGuild = useRecoilValue(selectedGuildState);
const setSelectedGuildId = useSetRecoilState(selectedGuildIdState);
const [ currGuildId, setCurrGuildId ] = useRecoilState(currGuildIdState);
const guildMeta = useRecoilValue(guildMetaState(guild.id));
const selfMember = useRecoilValue(guildSelfMemberState(guild.id));
// TODO: more state higher up
// TODO: handle metadata error
const [ selfMemberResult ] = useSelfMemberSubscription(guild);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, guildMeta?.iconResourceId ?? null, guild);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null, guild);
const [ contextHover, mouseEnterCallable, mouseLeaveCallable ] = useContextHover(() => {
if (!guildMeta) return null;
if (!selfMemberResult || !selfMemberResult.value) return null;
const nameStyle = selfMemberResult.value.roleColor ? { color: selfMemberResult.value.roleColor } : {};
if (!isLoaded(guildMeta)) return null; // TODO: Loading message here?
if (!isLoaded(selfMember)) return null; // TODO: Loading message here?
const nameStyle = selfMember.value.roleColor ? { color: selfMember.value.roleColor } : {};
return (
<BasicHover relativeToRef={rootRef} side={BasicHoverSide.RIGHT} realignDeps={[ guildMeta, selfMemberResult ]}>
<BasicHover relativeToRef={rootRef} side={BasicHoverSide.RIGHT} realignDeps={[ guildMeta, selfMember ]}>
<div className="guild-hover">
<div className="tab">{BaseElements.TAB_LEFT}</div>
<div className="info">
<div className="guild-name">{guildMeta.name}</div>
<div className={'connection ' + selfMemberResult.value.status}>
<div className="guild-name">{guildMeta.value.name}</div>
<div className={'connection ' + selfMember.value.status}>
<div className="status-circle" />
<div className="display-name" style={nameStyle}>{selfMemberResult.value.displayName}</div>
<div className="display-name" style={nameStyle}>{selfMember.value.displayName}</div>
</div>
</div>
</div>
</BasicHover>
)
}, [ guildMeta, selfMemberResult ]);
}, [ guildMeta, selfMember ]);
const leaveGuildCallable = useCallback(async () => {
if (!guildsManager) return;
@ -67,12 +67,12 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
const setSelfActiveGuild = useCallback(() => {
setSelectedGuildId(guild.id);
setCurrGuildId(guild.id);
}, [ guild ]);
const className = useMemo(() => {
return selectedGuild && guild.id === selectedGuild.id ? 'guild active' : 'guild';
}, [ guild, selectedGuild ]);
return currGuildId && guild.id === currGuildId ? 'guild active' : 'guild';
}, [ guild, currGuildId ]);
return (
<div>

View File

@ -3,9 +3,9 @@ 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 '../../require/atoms';
import ImageContextMenu from '../../contexts/context-menu-image';
import ImageOverlay from '../../overlays/overlay-image';
import { overlayState } from '../../require/atoms-2';
import ElementsUtil, { IAlignment } from '../../require/elements-util';
import { useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions';
import { useContextClickContextMenu, useDownloadButton, useOneTimeAsyncAction } from '../../require/react-helper';

View File

@ -1,20 +1,15 @@
import React, { FC, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { GuildMetadata } from '../../data-types';
import { guildsState, GuildWithErrorableValue } from '../require/atoms';
import { allGuildsState } from '../require/atoms-2';
import GuildListElement from './components/guild-list-element';
const GuildList: FC = () => {
const guildsWithMeta = useRecoilValue(guildsState);
const allGuilds = useRecoilValue(allGuildsState);
const guildElements = useMemo(() => {
if (!guildsWithMeta) return null;
return guildsWithMeta.map((guildWithMeta: GuildWithErrorableValue<GuildMetadata>) => (
<GuildListElement
guildWithMeta={guildWithMeta} key={guildWithMeta.guild.id}
/>
));
}, [ guildsWithMeta ]);
if (!allGuilds) return null;
return allGuilds.map(guild => <GuildListElement key={guild.id} guild={guild} />)
}, [ allGuilds ]);
return (
<div className="guild-list">

View File

@ -7,18 +7,19 @@ import React, { FC, useMemo } from 'react';
import { Member } from '../../data-types';
import MemberElement from './components/member-element';
import { useRecoilValue } from 'recoil';
import { isDevoid, isPended, isFailed, selectedGuildWithMembersState } from '../require/atoms';
import { currGuildMembersState, currGuildState, isFailed, isPended, isUnload } from '../require/atoms-2';
const MemberList: FC = () => {
const guildWithMembers = useRecoilValue(selectedGuildWithMembersState);
const guild = useRecoilValue(currGuildState);
const members = useRecoilValue(currGuildMembersState);
const memberElements = useMemo(() => {
if (isDevoid(guildWithMembers)) return null;
if (isPended(guildWithMembers)) return <div className="members-loading">Loading Members...</div>;
if (isFailed(guildWithMembers)) return <div className="members-failed">Unable to load members</div>;
if (!guild || isUnload(members)) return null
if (isPended(members)) return <div className="members-loading">Loading Members...</div>;
if (isFailed(members)) return <div className="members-failed">Unable to load members</div>;
//LOG.debug(`drawing ${membersResult.value.length} members`);
return guildWithMembers.value.map((member: Member) => <MemberElement key={guildWithMembers.guild.id + member.id} guild={guildWithMembers.guild} member={member} />);
}, [ guildWithMembers ]);
return members.value.map((member: Member) => <MemberElement key={guild.id + member.id} guild={guild} member={member} />);
}, [ guild, members ]);
return (
<div className="member-list">

View File

@ -17,7 +17,7 @@ import * as fs from 'fs/promises';
import Button from '../components/button';
import Overlay from '../components/overlay';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { guildsManagerState, overlayState, selectedGuildIdState } from '../require/atoms';
import { currGuildIdState, guildsManagerState, overlayState } from '../require/atoms-2';
export interface IAddGuildData {
name: string,
@ -58,7 +58,7 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
const { addGuildData } = props;
const guildsManager = useRecoilValue(guildsManagerState);
const setSelectedGuildId = useSetRecoilState(selectedGuildIdState);
const setCurrGuildId = useSetRecoilState(currGuildIdState);
const rootRef = useRef<HTMLDivElement>(null);
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
@ -109,7 +109,7 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
return { result: null, errorMessage: (e as Error).message ?? 'Error adding new guild' };
}
setSelectedGuildId(newGuild.id);
setCurrGuildId(newGuild.id);
setOverlay(null);
return { result: newGuild, errorMessage: null };

View File

@ -13,7 +13,7 @@ import { useAsyncSubmitButton } from '../require/react-helper';
import Button from '../components/button';
import Overlay from '../components/overlay';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { overlayState, selectedGuildState } from '../require/atoms';
import { currGuildState, overlayState } from '../require/atoms-2';
export interface ChannelOverlayProps {
channel?: Channel; // If this is undefined, this is an add-channel overlay
@ -21,8 +21,7 @@ export interface ChannelOverlayProps {
const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) => {
const { channel } = props;
const guild = useRecoilValue(selectedGuildState);
if (guild === null) return null;
const guild = useRecoilValue(currGuildState);
const rootRef = useRef<HTMLDivElement>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
@ -67,6 +66,7 @@ const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) =>
const [ submitFunc, submitButtonText, submitButtonShaking, submitFailMessage ] = useAsyncSubmitButton(
async () => {
if (guild === null) return { result: null, errorMessage: 'no guild' };
if (validationErrorMessage) return { result: null, errorMessage: 'Invalid input' };
if (!edited) {
@ -89,7 +89,7 @@ const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) =>
setOverlay(null);
return { result: null, errorMessage: null };
},
[ edited, validationErrorMessage, name, flavorText, setOverlay ],
[ guild, edited, validationErrorMessage, name, flavorText, setOverlay ],
{ start: channel ? 'Modify Channel' : 'Create Channel' }
);

View File

@ -3,14 +3,17 @@ import ChoicesControl from "../components/control-choices";
import GuildInvitesDisplay from "../displays/display-guild-invites";
import GuildOverviewDisplay from "../displays/display-guild-overview";
import Overlay from '../components/overlay';
import { GuildWithValue } from "../require/atoms";
import { GuildMetadata } from "../../data-types";
import CombinedGuild from "../../guild-combined";
import { guildMetaState, isLoaded } from "../require/atoms-2";
import { useRecoilValue } from "recoil";
interface GuildSettingsOverlayProps {
guildWithMeta: GuildWithValue<GuildMetadata>;
guild: CombinedGuild;
}
const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSettingsOverlayProps) => {
const { guildWithMeta } = props;
const { guild } = props;
const guildMeta = useRecoilValue(guildMetaState(guild.id));
const rootRef = useRef<HTMLDivElement>(null);
@ -18,15 +21,15 @@ const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSetting
const [ display, setDisplay ] = useState<JSX.Element>();
useEffect(() => {
if (selectedId === 'overview') setDisplay(<GuildOverviewDisplay guildWithMeta={guildWithMeta} />);
if (selectedId === 'overview') setDisplay(<GuildOverviewDisplay guild={guild} />);
//if (selectedId === 'roles' ) setDisplay(<GuildOverviewDisplay guild={guild} guildMeta={guildMeta} />);
if (selectedId === 'invites' ) setDisplay(<GuildInvitesDisplay guildWithMeta={guildWithMeta} />);
}, [ selectedId, guildWithMeta, setDisplay ]);
if (selectedId === 'invites' ) setDisplay(<GuildInvitesDisplay guild={guild} />);
}, [ guild, selectedId, setDisplay ]);
return (
<Overlay childRootRef={rootRef}>
<div ref={rootRef} className="content display-swapper guild-settings">
<ChoicesControl title={guildWithMeta.value.name ?? 'unknown guild'} selectedId={selectedId} setSelectedId={setSelectedId} choices={[
<ChoicesControl title={isLoaded(guildMeta) ? guildMeta.value.name : 'loading...'} selectedId={selectedId} setSelectedId={setSelectedId} choices={[
{ id: 'overview', display: 'Overview' },
{ id: 'roles', display: 'Roles' },
{ id: 'invites', display: 'Invites' },

View File

@ -13,19 +13,19 @@ import Button from '../components/button';
import Overlay from '../components/overlay';
import { useResourceSubscription } from '../require/guild-subscriptions';
import { useSetRecoilState } from 'recoil';
import { overlayState, GuildWithValue } from '../require/atoms';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { overlayState } from '../require/atoms-2';
interface PersonalizeOverlayProps {
guildWithSelfMember: GuildWithValue<Member>;
guild: CombinedGuild;
selfMember: Member;
}
const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverlayProps) => {
const { guildWithSelfMember } = props;
const { guild, selfMember } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const { guild, value: selfMember } = guildWithSelfMember;
const rootRef = useRef<HTMLDivElement>(null);
const [ avatarResourceResult, avatarResourceError ] = useResourceSubscription(guild, selfMember.avatarResourceId, guild);

View File

@ -0,0 +1,586 @@
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 { ReactNode, useEffect } from "react";
import { atom, atomFamily, GetCallback, GetRecoilValue, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useSetRecoilState } from "recoil";
import { Changes, Channel, GuildMetadata, Member } from "../../data-types";
import CombinedGuild from "../../guild-combined";
import GuildsManager from "../../guilds-manager";
import { AutoVerifierChangesType } from '../../auto-verifier';
import { Conflictable, Connectable } from '../../guild-types';
// General typescript type that infers the arguments of a function
type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
type Defined<T> = T extends undefined ? never : T | Awaited<T>;
export type UnloadedValue = {
value: undefined;
error: undefined;
retry: undefined;
hasError: false;
loading: false;
};
export type LoadingValue = {
value: undefined;
error: undefined;
retry: undefined;
hasError: false;
loading: true;
};
export type LoadedValue<T> = {
value: Defined<T>;
error: undefined;
retry: () => Promise<void>;
hasError: false;
loading: false;
};
export type FailedValue = {
value: undefined;
error: unknown;
retry: () => Promise<void>;
hasError: true;
loading: false;
};
export type LoadableValue<T> = UnloadedValue | LoadingValue | LoadedValue<T> | FailedValue;
export type QueriedValue<T> = LoadingValue | LoadedValue<T> | FailedValue;
const DEF_UNLOADED_VALUE: UnloadedValue = { value: undefined, error: undefined, retry: undefined, hasError: false, loading: false };
const DEF_PENDED_VALUE: LoadingValue = { value: undefined, error: undefined, retry: undefined, hasError: false, loading: true };
function createLoadedValue<T>(value: Defined<T>, retry: () => Promise<void>): LoadedValue<T> {
return {
value,
error: undefined,
retry,
hasError: false,
loading: false
};
}
function createFailedValue(error: unknown, retry: () => Promise<void>): FailedValue {
return {
value: undefined,
error,
retry,
hasError: true,
loading: false
};
}
export function isUnload<T>(loadableValue: LoadableValue<T>): loadableValue is UnloadedValue {
return loadableValue.value === undefined && loadableValue.hasError === false && loadableValue.loading === false;
}
export function isPended<T>(loadableValue: LoadableValue<T>): loadableValue is LoadingValue {
return loadableValue.loading === true;
}
export function isFailed<T>(loadableValue: LoadableValue<T>): loadableValue is FailedValue {
return loadableValue.hasError === true;
}
export function isLoaded<T>(loadableValue: LoadableValue<T>): loadableValue is LoadedValue<T> {
return loadableValue.value !== undefined;
}
export const overlayState = atom<ReactNode>({
key: 'overlayState',
default: null
});
export const guildsManagerState = atom<GuildsManager | null>({
key: 'guildsManager',
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
});
// This will be a sliced copy of guildsManager.guilds
export const allGuildsState = atom<CombinedGuild[] | null>({
key: 'allGuildsListState',
default: null,
dangerouslyAllowMutability: true
});
interface RecoilLoadableAtomEffectParams<T> {
trigger: 'get' | 'set';
setSelf: (loadableValue: LoadableValue<T>) => void;
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>;
}
function createFetchValueFunc<T>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number,
setSelf: (loadableValue: LoadableValue<T>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T>>,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
): () => Promise<void> {
const fetchValueFunc = async () => {
const guild = await getPromise(guildState(guildId));
if (guild === null) return; // Can't send a request without an associated guild
const selfState = await getPromise(stateAtomFamily(guildId));
if (isPended(selfState)) return; // Don't send another request if we're already loading
setSelf(DEF_PENDED_VALUE);
try {
const value = await fetchFunc(guild);
setSelf(createLoadedValue(value, fetchValueFunc));
} catch (e: unknown) {
LOG.error('unable to fetch initial guild metadata', e);
setSelf(createFailedValue(e, fetchValueFunc));
}
}
return fetchValueFunc;
}
// Creates an event handler that directly applies the result of the eventArgsMap as a loadedValue into self
function createDirectMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number,
setSelf: (loadableValue: LoadableValue<T>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T>>,
fetchValueFunc: () => Promise<void>,
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T>
): (Connectable & Conflictable)[XE] {
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => {
const selfState = await getPromise(stateAtomFamily(guildId));
if (isLoaded(selfState)) {
const value = eventArgsMap(...args);
setSelf(createLoadedValue(value, fetchValueFunc));
}
})();
}) as (Connectable & Conflictable)[XE];
}
function applyNew<T>(value: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] {
return value.concat(newElements).sort(sortFunc);
}
function applyUpdated<T extends { id: string }>(value: T[], updatedElements: T[], sortFunc: (a: T, b: T) => number): T[] {
return value.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc);
}
function applyRemoved<T extends { id: string }>(value: T[], removedElements: T[]): T[] {
const removedIds = new Set<string>(removedElements.map(removedElement => removedElement.id));
return value.filter(element => !removedIds.has(element.id));
}
// Useful for new-xxx, update-xxx, remove-xxx list events
function createListConnectableMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number,
setSelf: (loadableValue: LoadableValue<T[]>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T[]>>,
fetchValueFunc: () => Promise<void>,
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined<T[]>,
sortFunc: (a: T, b: T) => number,
applyFunc: (value: T[], eventArgsResult: T[], sortFunc: (a: T, b: T) => number) => T[],
): (Connectable & Conflictable)[XE] {
// I think the typed EventEmitter class isn't ready for this level of insane type safety
// otherwise, I may have done this wrong. Forcing it to work with these calls
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => {
const selfState = await getPromise(stateAtomFamily(guildId));
if (isLoaded(selfState)) {
const eventArgsResult = eventArgsMap(...args);
const value = applyFunc(selfState.value, eventArgsResult, sortFunc);
setSelf(createLoadedValue(value, fetchValueFunc));
}
})();
}) as (Connectable & Conflictable)[XE];
}
function applyChanges<T extends { id: string }>(value: T[], changes: Changes<T>, sortFunc: (a: T, b: T) => number): T[] {
const removedIds = new Set<string>(changes.deleted.map(deletedElement => deletedElement.id));
return value
.concat(changes.added)
.map(element => changes.updated.find(updatedPair => updatedPair.newDataPoint.id === element.id)?.newDataPoint ?? element)
.filter(element => !removedIds.has(element.id))
.sort(sortFunc);
}
// Useful for conflict-xxx list events
function createListConflictableMappedEventHandler<T, XE extends keyof (Connectable | Conflictable)>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number,
setSelf: (loadableValue: LoadableValue<T[]>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T[]>>,
fetchValueFunc: () => Promise<void>,
eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes<T>,
sortFunc: (a: T, b: T) => number,
applyFunc: (value: T[], eventArgsResult: Changes<T>, sortFunc: (a: T, b: T) => number) => T[],
): (Connectable & Conflictable)[XE] {
return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => {
(async () => {
const selfState = await getPromise(stateAtomFamily(guildId));
if (isLoaded(selfState)) {
const eventArgsResult = eventArgsMap(...args);
const value = applyFunc(selfState.value, eventArgsResult, sortFunc);
setSelf(createLoadedValue(value, fetchValueFunc));
}
})();
}) as (Connectable & Conflictable)[XE];
}
interface SingleEventMappingParams<T, UE extends keyof Connectable, CE extends keyof Conflictable> {
updatedEventName: UE;
updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T>;
conflictEventName: CE;
conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined<T>;
}
function listenToSingle<
T, // e.g. GuildMetadata
UE extends keyof Connectable, // Update Event
CE extends keyof Conflictable, // Conflict Event
>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number,
setSelf: (loadableValue: LoadableValue<T>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T>>,
fetchValueFunc: () => Promise<void>,
eventMapping: SingleEventMappingParams<T, UE, CE>
) {
// Listen for updates
let guild: CombinedGuild | null = null;
let onUpdateFunc: (Connectable & Conflictable)[UE] | null = null;
let onConflictFunc: (Connectable & Conflictable)[CE] | null = null;
let closed = false;
(async () => {
guild = await getPromise(guildState(guildId));
if (guild === null) return;
if (closed) return; // Make sure not to bind events if this closed while we were fetching the guild state
// I think the typed EventEmitter class isn't ready for this level of insane type safety
// otherwise, I may have done this wrong. Forcing it to work with these calls
onUpdateFunc = createDirectMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.updatedEventArgsMap);
onConflictFunc = createDirectMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.conflictEventArgsMap);
guild.on(eventMapping.updatedEventName, onUpdateFunc);
guild.on(eventMapping.conflictEventName, onConflictFunc);
})();
const cleanup = () => {
closed = true;
if (guild && onUpdateFunc) guild.off(eventMapping.updatedEventName, onUpdateFunc);
if (guild && onConflictFunc) guild.off(eventMapping.conflictEventName, onConflictFunc);
}
return cleanup;
}
interface MultipleEventMappingParams<
T,
NE extends keyof Connectable,
UE extends keyof Connectable,
RE extends keyof Connectable,
CE extends keyof Conflictable
> {
newEventName: NE;
newEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>;
updatedEventName: UE;
updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>;
removedEventName: RE;
removedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined<T[]>;
conflictEventName: CE;
conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes<T>;
}
function listenToMultiple<
T extends { id: string },
NE extends keyof Connectable, // New Event
UE extends keyof Connectable, // Update Event
RE extends keyof Connectable, // Remove Event
CE extends keyof Conflictable // Conflict Event
>(
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
guildId: number,
setSelf: (loadableValue: LoadableValue<T[]>) => void,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T[]>>,
fetchValueFunc: () => Promise<void>,
sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>
) {
// Listen for updates
let guild: CombinedGuild | null = null;
let onNewFunc: (Connectable & Conflictable)[NE] | null = null;
let onUpdateFunc: (Connectable & Conflictable)[UE] | null = null;
let onRemoveFunc: (Connectable & Conflictable)[RE] | null = null;
let onConflictFunc: (Connectable & Conflictable)[CE] | null = null;
let closed = false;
(async () => {
guild = await getPromise(guildState(guildId));
if (guild === null) return;
if (closed) return; // Make sure not to bind events if this closed while we were fetching the guild state
onNewFunc = createListConnectableMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.newEventArgsMap, sortFunc, applyNew);
onUpdateFunc = createListConnectableMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.updatedEventArgsMap, sortFunc, applyUpdated);
onRemoveFunc = createListConnectableMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.updatedEventArgsMap, sortFunc, applyRemoved);
onConflictFunc = createListConflictableMappedEventHandler(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping.conflictEventArgsMap, sortFunc, applyChanges);
guild.on(eventMapping.updatedEventName, onUpdateFunc);
guild.on(eventMapping.conflictEventName, onConflictFunc);
})();
const cleanup = () => {
closed = true;
if (guild && onNewFunc) guild.off(eventMapping.newEventName, onNewFunc);
if (guild && onUpdateFunc) guild.off(eventMapping.updatedEventName, onUpdateFunc);
if (guild && onRemoveFunc) guild.off(eventMapping.removedEventName, onRemoveFunc);
if (guild && onConflictFunc) guild.off(eventMapping.conflictEventName, onConflictFunc);
}
return cleanup;
}
function singleGuildSubscriptionEffect<
T, // e.g. GuildMetadata
UE extends keyof Connectable, // Update Event
CE extends keyof Conflictable, // Conflict Event
>(
guildId: number,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T>>,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
eventMapping: SingleEventMappingParams<T, UE, CE>
) {
return (params: RecoilLoadableAtomEffectParams<T>) => {
const { trigger, setSelf, getPromise } = params;
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, setSelf, stateAtomFamily, fetchFunc);
// Fetch initial value on first get
if (trigger === 'get') {
fetchValueFunc();
}
// Listen to changes
const cleanup = listenToSingle(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, eventMapping);
return () => {
cleanup();
}
}
}
function multipleGuildSubscriptionEffect<
T extends { id: string },
NE extends keyof Connectable,
UE extends keyof Connectable,
RE extends keyof Connectable,
CE extends keyof Conflictable
>(
guildId: number,
stateAtomFamily: (guildId: number) => RecoilState<LoadableValue<T[]>>,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>,
sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>,
) {
return (params: RecoilLoadableAtomEffectParams<T[]>) => {
const { trigger, setSelf, getPromise } = params;
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, setSelf, stateAtomFamily, fetchFunc);
// Fetch initial value on first get
if (trigger === 'get') {
fetchValueFunc();
}
// Listen to changes
const cleanup = listenToMultiple(getPromise, guildId, setSelf, stateAtomFamily, fetchValueFunc, sortFunc, eventMapping);
return () => {
cleanup();
}
}
}
// You probably want currGuildMetaState
export const guildMetaState: (guildId: number) => RecoilState<LoadableValue<GuildMetadata>> = atomFamily<LoadableValue<GuildMetadata>, number>({
key: 'guildMetaState',
default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (guildId: number) => [
singleGuildSubscriptionEffect(
guildId,
guildMetaState,
async (guild: CombinedGuild) => await guild.fetchMetadata(),
{
updatedEventName: 'update-metadata',
updatedEventArgsMap: (newMeta: GuildMetadata) => newMeta,
conflictEventName: 'conflict-metadata',
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, _oldMeta: GuildMetadata, newMeta: GuildMetadata) => newMeta
}
)
],
dangerouslyAllowMutability: true
});
const guildMembersState: (guildId: number) => RecoilState<LoadableValue<Member[]>> = atomFamily<LoadableValue<Member[]>, number>({
key: 'guildMembersState',
default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (guildId: number) => [
multipleGuildSubscriptionEffect(
guildId,
guildMembersState,
async (guild: CombinedGuild) => await guild.fetchMembers(),
Member.sortForList,
{
newEventName: 'new-members',
newEventArgsMap: (newMembers: Member[]) => newMembers,
updatedEventName: 'update-members',
updatedEventArgsMap: (updatedMembers: Member[]) => updatedMembers,
removedEventName: 'remove-members',
removedEventArgsMap: (removedMembers: Member[]) => removedMembers,
conflictEventName: 'conflict-members',
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes<Member>) => changes,
}
)
],
dangerouslyAllowMutability: true
});
// You probably want currGuildSelfMemberState
export const guildSelfMemberState = selectorFamily<LoadableValue<Member>, number>({
key: 'guildSelfMemberState',
get: (guildId: number) => ({ get }) => {
// Find the guild from the guilds manager list of guilds
const guildsManager = get(guildsManagerState);
if (guildsManager === null) return DEF_UNLOADED_VALUE;
// Make sure that the guild has a successfully loaded members list
const members = get(guildMembersState(guildId));
if (isUnload(members)) return DEF_UNLOADED_VALUE;
if (isPended(members)) return DEF_PENDED_VALUE;
if (isFailed(members)) return createFailedValue('failed to load members', members.retry);
// Find the guild's selfMember in the list
const guild = guildsManager.guilds.find(guild => guild.id === guildId);
if (!guild) return createFailedValue('unable to find self member guild', members.retry);
const selfMember = members.value.find(member => member.id === guild.memberId);
if (!selfMember) return createFailedValue('failed to find self in members', members.retry);
return createLoadedValue(selfMember, members.retry);
},
dangerouslyAllowMutability: true
});
const guildChannelsState: (guildId: number) => RecoilState<LoadableValue<Channel[]>> = atomFamily<LoadableValue<Channel[]>, number>({
key: 'guildChannelsState',
default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (guildId: number) => [
multipleGuildSubscriptionEffect(
guildId,
guildChannelsState,
async (guild: CombinedGuild) => await guild.fetchChannels(),
Channel.sortByIndex,
{
newEventName: 'new-channels',
newEventArgsMap: (newChannels: Channel[]) => newChannels,
updatedEventName: 'update-channels',
updatedEventArgsMap: (updatedChannels: Channel[]) => updatedChannels,
removedEventName: 'remove-channels',
removedEventArgsMap: (removedChannels: Channel[]) => removedChannels,
conflictEventName: 'conflict-channels',
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes<Channel>) => changes
}
)
],
dangerouslyAllowMutability: true
});
// You probably want currGuildActiveChannel
export const guildActiveChannelIdState: (guildId: number) => RecoilState<string | null> = atomFamily<string | null, number>({
key: 'guildActiveChannelIdState',
default: null,
});
const guildActiveChannelState = selectorFamily<LoadableValue<Channel>, number>({
key: 'guildActiveChannelState',
get: (guildId: number) => ({ get }) => {
const activeChannelId = get(guildActiveChannelIdState(guildId));
if (activeChannelId === null) return DEF_UNLOADED_VALUE;
const channels = get(guildChannelsState(guildId));
if (isUnload(channels)) return DEF_UNLOADED_VALUE;
if (isPended(channels)) return DEF_PENDED_VALUE;
if (isFailed(channels)) return createFailedValue('channels not loaded', channels.retry);
const channel = channels.value.find(channel => channel.id === activeChannelId) ?? null;
if (channel === null) {
LOG.warn('active channel was not found in channels');
return DEF_UNLOADED_VALUE;
}
return createLoadedValue(channel, channels.retry);
},
dangerouslyAllowMutability: true
});
const guildState = selectorFamily<CombinedGuild | null, number>({
key: 'guildState',
get: (guildId: number) => ({ get }) => {
const guildsManager = get(guildsManagerState);
if (guildsManager === null) return null;
const guild = guildsManager.guilds.find(guild => guild.id === guildId) ?? null;
return guild;
},
dangerouslyAllowMutability: true
});
export const currGuildIdState = atom<number | null>({
key: 'currGuildIdState',
default: null
});
function createCurrentGuildStateGetter<T>(subSelectorFamily: (guildId: number) => RecoilValueReadOnly<T>) {
return ({ get }: { get: GetRecoilValue }) => {
const currGuildId = get(currGuildIdState);
if (currGuildId === null) return null;
const value = get(subSelectorFamily(currGuildId));
return value;
}
}
function createCurrentGuildLoadableStateGetter<T>(subSelectorFamily: (guildId: number) => RecoilValueReadOnly<LoadableValue<T>> | RecoilState<LoadableValue<T>>) {
return ({ get }: { get: GetRecoilValue; getCallback: GetCallback }) => {
// Use the unloaded value if the current guild hasn't been selected yet or doesn't exist
const currGuildId = get(currGuildIdState);
if (currGuildId === null) return DEF_UNLOADED_VALUE;
const value = get(subSelectorFamily(currGuildId));
if (value === null) return DEF_UNLOADED_VALUE;
return value;
}
}
// Note: These will all update in parallel when the guild changes. They will always reference the same guild
// There should not need to be a worry about them cauing extra renders
export const currGuildState = selector<CombinedGuild | null>({
key: 'currGuildState',
get: createCurrentGuildStateGetter(guildState),
dangerouslyAllowMutability: true
});
export const currGuildMetaState = selector<LoadableValue<GuildMetadata>>({
key: 'currGuildMetaState',
get: createCurrentGuildLoadableStateGetter(guildMetaState),
dangerouslyAllowMutability: true
});
export const currGuildMembersState = selector<LoadableValue<Member[]>>({
key: 'currGuildMembersState',
get: createCurrentGuildLoadableStateGetter(guildMembersState),
dangerouslyAllowMutability: true
});
export const currGuildSelfMemberState = selector<LoadableValue<Member>>({
key: 'currGuildSelfMemberState',
get: createCurrentGuildLoadableStateGetter(guildSelfMemberState),
dangerouslyAllowMutability: true
});
export const currGuildChannelsState = selector<LoadableValue<Channel[]>>({
key: 'currGuildChannelsState',
get: createCurrentGuildLoadableStateGetter(guildChannelsState),
dangerouslyAllowMutability: true
});
export const currGuildActiveChannelState = selector<LoadableValue<Channel>>({
key: 'currGuildActiveChannelState',
get: createCurrentGuildLoadableStateGetter(guildActiveChannelState),
dangerouslyAllowMutability: true
});
// Initialize with a guildsManager
export function initRecoil(guildsManager: GuildsManager) {
const setGuildsManager = useSetRecoilState(guildsManagerState);
const setGuilds = useSetRecoilState(allGuildsState);
useEffect(() => {
setGuildsManager(guildsManager);
}, [ guildsManager, setGuildsManager ]);
useEffect(() => {
const updateGuilds = () => { setGuilds(guildsManager.guilds.slice()); }
updateGuilds();
guildsManager.on('update-guilds', updateGuilds);
return () => {
guildsManager.off('update-guilds', updateGuilds);
}
}, [ guildsManager ]);
}

View File

@ -1,8 +1,7 @@
import React, { FC, ReactNode } from 'react';
import { useRecoilValue } from 'recoil';
import GuildsManager from '../guilds-manager';
import { overlayState } from './require/atoms';
import { useGuildsManagerWithRecoil } from './require/setup-guild-recoil';
import { initRecoil, overlayState } from './require/atoms-2';
import GuildsManagerElement from './sections/guilds-manager';
import TitleBar from './sections/title-bar';
@ -13,9 +12,9 @@ export interface RootElementProps {
const RootElement: FC<RootElementProps> = (props: RootElementProps) => {
const { guildsManager } = props;
const overlay = useRecoilValue<ReactNode>(overlayState);
initRecoil(guildsManager);
useGuildsManagerWithRecoil(guildsManager);
const overlay = useRecoilValue<ReactNode>(overlayState);
return (
<div>

View File

@ -4,15 +4,16 @@ import MemberElement, { DummyMember } from '../lists/components/member-element';
import ConnectionInfoContextMenu from '../contexts/context-menu-connection-info';
import { useContextMenu } from '../require/react-helper';
import { useRecoilValue } from 'recoil';
import { isDevoid, isPended, isFailed, selectedGuildWithSelfMemberState, isDevoidPendedOrFailed } from '../require/atoms';
import { currGuildSelfMemberState, currGuildState, isFailed, isLoaded, isPended, isUnload } from '../require/atoms-2';
const ConnectionInfo: FC = () => {
const rootRef = useRef<HTMLDivElement>(null);
const guildWithSelfMember = useRecoilValue(selectedGuildWithSelfMemberState);
const guild = useRecoilValue(currGuildState);
const selfMember = useRecoilValue(currGuildSelfMemberState);
const displayMember = useMemo((): Member | DummyMember => {
if (isDevoid(guildWithSelfMember) || isPended(guildWithSelfMember)) {
if (isUnload(selfMember) || isPended(selfMember)) {
return {
id: 'dummy',
displayName: 'Connecting...',
@ -21,7 +22,7 @@ const ConnectionInfo: FC = () => {
avatarResourceId: null
};
}
if (isFailed(guildWithSelfMember)) {
if (isFailed(selfMember)) {
return {
id: 'dummy',
displayName: 'UNKNOWN',
@ -30,22 +31,27 @@ const ConnectionInfo: FC = () => {
avatarResourceId: null
};
}
return guildWithSelfMember.value;
}, [ guildWithSelfMember ]);
return selfMember.value;
}, [ selfMember ]);
const [ contextMenu, toggleContextMenu ] = useContextMenu((close: () => void) => {
if (isDevoidPendedOrFailed(guildWithSelfMember)) return null;
if (guild === null) return null;
if (!isLoaded(selfMember)) return null;
return (
<ConnectionInfoContextMenu guildWithSelfMember={guildWithSelfMember} relativeToRef={rootRef} close={close} />
<ConnectionInfoContextMenu guild={guild} selfMember={selfMember.value} relativeToRef={rootRef} close={close} />
);
}, [ guildWithSelfMember ]);
}, [ guild, selfMember ]);
return guildWithSelfMember?.guild ? (
<div ref={rootRef} className="connection-info">
<div onClick={toggleContextMenu}><MemberElement guild={guildWithSelfMember.guild} member={displayMember} /></div>
{contextMenu}
</div>
) : null;
if (guild) {
return (
<div ref={rootRef} className="connection-info">
<div onClick={toggleContextMenu}><MemberElement guild={guild} member={displayMember} /></div>
{contextMenu}
</div>
);
} else {
return null;
}
}
export default ConnectionInfo;

View File

@ -11,8 +11,8 @@ 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 '../require/atoms';
import { useSetRecoilState } from 'recoil';
import { overlayState } from '../require/atoms-2';
const GuildListContainer: FC = () => {
const addGuildRef = useRef<HTMLDivElement>(null);

View File

@ -1,40 +1,28 @@
import React, { FC, useMemo, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { Member } from '../../data-types';
import GuildTitleContextMenu from '../contexts/context-menu-guild-title';
import { selectedGuildWithMetaState } from '../require/atoms';
import { isNonNullAndHasValue, SubscriptionResult } from '../require/guild-subscriptions';
import { currGuildMetaState, currGuildSelfMemberState, isLoaded } from '../require/atoms-2';
import { useContextMenu } from '../require/react-helper';
export interface GuildTitleProps {
selfMemberResult: SubscriptionResult<Member | null> | null;
}
const GuildTitle: FC<GuildTitleProps> = (props: GuildTitleProps) => {
const { selfMemberResult } = props;
const guildWithMeta = useRecoilValue(selectedGuildWithMetaState)
const GuildTitle: FC = () => {
const guildMeta = useRecoilValue(currGuildMetaState);
const selfMember = useRecoilValue(currGuildSelfMemberState);
const rootRef = useRef<HTMLDivElement>(null);
const hasContextMenu = useMemo(() => {
if (!isNonNullAndHasValue(selfMemberResult)) return false;
if (!isLoaded(selfMember)) return false;
return (
selfMemberResult.value.privileges.includes('modify_profile') ||
selfMemberResult.value.privileges.includes('modify_channels')
selfMember.value.privileges.includes('modify_profile') ||
selfMember.value.privileges.includes('modify_channels')
);
}, [ selfMemberResult ]);
}, [ selfMember ]);
const [ contextMenu, toggleContextMenu ] = useContextMenu((close: () => void) => {
if (!isNonNullAndHasValue(selfMemberResult)) return null;
return (
<GuildTitleContextMenu
relativeToRef={rootRef} close={close}
selfMember={selfMemberResult.value}
/>
<GuildTitleContextMenu relativeToRef={rootRef} close={close} />
);
}, [ selfMemberResult, rootRef ]);
}, [ rootRef ]);
const nameStyle = useMemo(() => {
if (hasContextMenu) {
@ -47,7 +35,7 @@ const GuildTitle: FC<GuildTitleProps> = (props: GuildTitleProps) => {
return (
<div className="guild-title" ref={rootRef}>
<div className="guild-name-container" style={nameStyle} onClick={hasContextMenu ? toggleContextMenu : undefined}>
<span className="guild-name">{guildWithMeta?.value?.name ?? null}</span>
<span className="guild-name">{isLoaded(guildMeta) ? guildMeta.value.name : null}</span>
</div>
{contextMenu}
</div>

View File

@ -3,98 +3,70 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useCallback, useEffect, useState } from 'react';
import React, { FC, useEffect, useState } from 'react';
import { Channel, Message } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import ChannelList from '../lists/channel-list';
import MemberList from '../lists/member-list';
import MessageList from '../lists/message-list';
import { useSelfMemberSubscription, useChannelsSubscription } from '../require/guild-subscriptions';
import ChannelTitle from './channel-title';
import ConnectionInfo from './connection-info';
import GuildTitle from './guild-title';
import SendMessage from './send-message';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currGuildActiveChannelState, currGuildChannelsState, currGuildSelfMemberState, currGuildState, guildActiveChannelIdState, isLoaded, isUnload } from '../require/atoms-2';
export interface GuildElementProps {
guild: CombinedGuild;
}
const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
const { guild } = props;
const GuildElement: FC = () => {
// TODO: Handle fetch errors by allowing for retry
// TODO: Handle fetch errors in message list
// TODO: React set hasMessagesAbove and hasMessagesBelow when re-verified?
// TODO: React jump messages to bottom when the current user sent a message
const [ selfMemberResult ] = useSelfMemberSubscription(guild);
const [ channelsRetry, channelsResult, channelsFetchError ] = useChannelsSubscription(guild);
const [ activeChannel, setActiveChannel ] = useState<Channel | null>(null);
const [ activeChannelGuild, setActiveChannelGuild ] = useState<CombinedGuild | null>(null);
const guild = useRecoilValue(currGuildState);
const selfMember = useRecoilValue(currGuildSelfMemberState);
const channels = useRecoilValue(currGuildChannelsState);
const activeChannel = useRecoilValue(currGuildActiveChannelState);
const setActiveChannelId = useSetRecoilState(guildActiveChannelIdState(guild?.id ?? -1));
const [ fetchMessagesRetryCallable, setFetchMessagesRetryCallable ] = useState<(() => Promise<void>) | null>(null);
// If the active channel isn't set yet, set it to the first of the channels
useEffect(() => {
setActiveChannel(null);
}, [ guild ]);
useEffect(() => {
if (activeChannel === null) {
// initial active channel is the first one in the list
if (channelsResult && channelsResult.value.length > 0) {
setActiveChannel(channelsResult.value[0] as Channel);
setActiveChannelGuild(channelsResult.guild);
//LOG.debug('Active channel guild enabled', { channel: channels[0]?.id, channelsGuild: channelsGuild?.id });
}
} else if (channelsResult && activeChannel) {
// in the active channel was updated
const newActiveChannel = channelsResult.value.find(channel => channel.id === activeChannel.id) ?? null
setActiveChannel(newActiveChannel);
setActiveChannelGuild(channelsResult.guild);
//LOG.debug('Active channel was updated...', { channel: newActiveChannel?.id, channelsGuild: channelsGuild?.id });
if (isUnload(activeChannel) && isLoaded(channels) && channels.value.length > 0) {
const activeChannel = channels.value[0] as Channel;
setActiveChannelId(activeChannel.id);
}
}, [ channelsResult, activeChannel ]);
}, [ guild, channels, activeChannel, setActiveChannelId ])
// When the self member sends a message to the active channel and we get it as a new message,
// move the active channel's messages back to the bottom
const onNewMessage = useCallback((messages: Message[]) => {
if (messages.find(message =>
message.member.id === selfMemberResult?.value?.id &&
message.channel.id === activeChannel?.id)
) {
if (fetchMessagesRetryCallable) {
// TODO: Only do the fetch if we're not at the bottom. If we're at the bottom, just
// set the scroll height
fetchMessagesRetryCallable(); // Re-load the messages to move them to the bottom.
useEffect(() => {
if (guild === null) return;
const onNewMessage = (newMessages: Message[]) => {
if (!isLoaded(selfMember)) return;
if (!isLoaded(activeChannel)) return;
if (fetchMessagesRetryCallable === null) return;
if (newMessages.find(newMessage => newMessage.member.id === selfMember.value.id && newMessage.channel.id === activeChannel.value.id)) {
fetchMessagesRetryCallable
}
}
}, [ fetchMessagesRetryCallable/*, Note: Removing these to prevent re-sending fetch. This technique should probably be changed in the future with react/redux selfMemberResult, activeChannel */ ]);
useEffect(() => {
guild.on('new-messages', onNewMessage);
return () => {
guild.off('new-messages', onNewMessage);
}
});
}, [ guild, selfMember, activeChannel, fetchMessagesRetryCallable ]);
return (
<div className="guild-react">
<div className="guild-sidebar">
<GuildTitle selfMemberResult={selfMemberResult} />
<ChannelList
guild={guild} selfMember={selfMemberResult?.value ?? null}
channels={channelsResult?.value ?? null} channelsFetchError={channelsFetchError}
activeChannel={activeChannel} setActiveChannel={setActiveChannel}
/>
<GuildTitle />
<ChannelList />
<ConnectionInfo />
</div>
<div className="guild-channel">
<ChannelTitle channel={activeChannel} />
<ChannelTitle channel={isLoaded(activeChannel) ? activeChannel.value : null} />
<div className="guild-channel-content">
<div className="guild-channel-feed-wrapper">
{activeChannel && activeChannelGuild && <MessageList guild={guild} channel={activeChannel} channelGuild={activeChannelGuild} setFetchRetryCallable={setFetchMessagesRetryCallable} />}
{activeChannel && activeChannelGuild && <SendMessage guild={guild} channel={activeChannel} />}
{guild && isLoaded(activeChannel) && <MessageList guild={guild} channel={activeChannel.value} channelGuild={guild} setFetchRetryCallable={setFetchMessagesRetryCallable} />}
{guild && isLoaded(activeChannel) && <SendMessage guild={guild} channel={activeChannel.value} />}
</div>
<div className="member-list-wrapper">
<MemberList />

View File

@ -1,16 +1,12 @@
import React, { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { selectedGuildState } from '../require/atoms';
import GuildElement from './guild';
import GuildListContainer from './guild-list-container';
const GuildsManagerElement: FC = () => {
const selectedGuild = useRecoilValue(selectedGuildState);
return (
<div className="guilds-manager">
<GuildListContainer />
{selectedGuild && <GuildElement guild={selectedGuild} />}
<GuildElement />
</div>
);
}