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:
Michael Peters 2022-01-27 00:40:29 -06:00
parent 4f2d272d00
commit e0814f1096
15 changed files with 230 additions and 106 deletions

View File

@ -5,7 +5,7 @@ const LOG = Logger.create(__filename, electronConsole);
import React, { FC, ReactNode, RefObject, useCallback, useEffect } from "react";
import { useActionWhenEscapeOrClickedOrContextOutsideEffect } from '../require/react-helper';
import { overlayState } from '../atoms';
import { overlayState } from '../require/atoms';
import { useSetRecoilState } from 'recoil';
interface OverlayProps {

View File

@ -2,7 +2,7 @@ import React, { FC, ReactNode, RefObject, useCallback, useMemo } from 'react';
import { useSetRecoilState } from 'recoil';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { overlayState } from '../atoms';
import { overlayState } from '../require/atoms';
import PersonalizeOverlay from '../overlays/overlay-personalize';
import { SubscriptionResult } from '../require/guild-subscriptions';
import ContextMenu from './components/context-menu';

View File

@ -2,7 +2,7 @@ import React, { FC, ReactNode, RefObject, useCallback, useMemo } from 'react';
import { useSetRecoilState } from 'recoil';
import { GuildMetadata, Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { overlayState } from '../atoms';
import { overlayState } from '../require/atoms';
import ChannelOverlay from '../overlays/overlay-channel';
import GuildSettingsOverlay from '../overlays/overlay-guild-settings';
import BaseElements from '../require/base-elements';

View File

@ -10,7 +10,7 @@ import ChannelOverlay from '../../overlays/overlay-channel';
import BaseElements from '../../require/base-elements';
import { useContextHover } from '../../require/react-helper';
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
import { overlayState } from '../../atoms';
import { overlayState } from '../../require/atoms';
import { useSetRecoilState } from 'recoil';
export interface ChannelElementProps {

View File

@ -1,41 +1,42 @@
import React, { FC, useCallback, useMemo, useRef } from 'react';
import CombinedGuild from '../../../guild-combined';
import GuildsManager from '../../../guilds-manager';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { GuildMetadata } from '../../../data-types';
import ContextMenu from '../../contexts/components/context-menu';
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
import { guildsManagerState, GuildWithErrorableValue, selectedGuildIdState, selectedGuildState } from '../../require/atoms';
import BaseElements from '../../require/base-elements';
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';
export interface GuildListElementProps {
guildsManager: GuildsManager;
guild: CombinedGuild;
activeGuild: CombinedGuild | null;
setSelfActiveGuild: () => void;
guildWithMeta: GuildWithErrorableValue<GuildMetadata>,
}
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);
// 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
const [ guildMetaResult, guildMetaError ] = useGuildMetadataSubscription(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(() => {
if (!guildMetaResult) return null;
if (!guildMeta) return null;
if (!selfMemberResult || !selfMemberResult.value) return null;
const nameStyle = selfMemberResult.value.roleColor ? { color: selfMemberResult.value.roleColor } : {};
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="tab">{BaseElements.TAB_LEFT}</div>
<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="status-circle" />
<div className="display-name" style={nameStyle}>{selfMemberResult.value.displayName}</div>
@ -44,9 +45,10 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
</div>
</BasicHover>
)
}, [ guildMetaResult, selfMemberResult ]);
}, [ guildMeta, selfMemberResult ]);
const leaveGuildCallable = useCallback(async () => {
if (!guildsManager) return;
guild.disconnect();
await guildsManager.removeGuild(guild);
}, [ guildsManager, guild ])
@ -63,9 +65,14 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
)
}, [ leaveGuildCallable ]);
const setSelfActiveGuild = useCallback(() => {
setSelectedGuildId(guild.id);
}, [ guild ]);
const className = useMemo(() => {
return activeGuild && guild.id === activeGuild.id ? 'guild active' : 'guild';
}, [ guild, activeGuild ]);
return selectedGuild && guild.id === selectedGuild.id ? 'guild active' : 'guild';
}, [ guild, selectedGuild ]);
return (
<div>

View File

@ -3,7 +3,7 @@ import React, { FC, ReactNode, useCallback, useMemo } from 'react';
import { useSetRecoilState } from 'recoil';
import { Member, Message } from '../../../data-types';
import CombinedGuild from '../../../guild-combined';
import { overlayState } from '../../atoms';
import { overlayState } from '../../require/atoms';
import ImageContextMenu from '../../contexts/context-menu-image';
import ImageOverlay from '../../overlays/overlay-image';
import ElementsUtil, { IAlignment } from '../../require/elements-util';

View File

@ -1,27 +1,20 @@
import React, { Dispatch, FC, SetStateAction, useMemo } from 'react';
import CombinedGuild from '../../guild-combined';
import GuildsManager from '../../guilds-manager';
import React, { FC, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { GuildMetadata } from '../../data-types';
import { guildsState, GuildWithErrorableValue } from '../require/atoms';
import GuildListElement from './components/guild-list-element';
export interface GuildListProps {
guildsManager: GuildsManager;
guilds: CombinedGuild[];
activeGuild: CombinedGuild | null;
setActiveGuild: Dispatch<SetStateAction<CombinedGuild | null>>;
}
const GuildList: FC<GuildListProps> = (props: GuildListProps) => {
const { guildsManager, guilds, activeGuild, setActiveGuild } = props;
const GuildList: FC = () => {
const guildsWithMeta = useRecoilValue(guildsState);
const guildElements = useMemo(() => {
return guilds.map((guild: CombinedGuild) => (
if (!guildsWithMeta) return null;
return guildsWithMeta.map((guildWithMeta: GuildWithErrorableValue<GuildMetadata>) => (
<GuildListElement
key={guild.id}
guildsManager={guildsManager} guild={guild}
activeGuild={activeGuild} setSelfActiveGuild={() => { setActiveGuild(guild); } }
guildWithMeta={guildWithMeta} key={guildWithMeta.guild.id}
/>
));
}, [ guildsManager, guilds, activeGuild ]);
}, [ guildsWithMeta ]);
return (
<div className="guild-list">

View File

@ -3,8 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { Dispatch, FC, ReactNode, SetStateAction, useEffect, useMemo, useRef, useState } from 'react';
import GuildsManager from '../../guilds-manager';
import React, { FC, ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import moment from 'moment';
import TextInput from '../components/input-text';
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 Button from '../components/button';
import Overlay from '../components/overlay';
import { useSetRecoilState } from 'recoil';
import { overlayState } from '../atoms';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { guildsManagerState, overlayState, selectedGuildIdState } from '../require/atoms';
export interface IAddGuildData {
name: string,
@ -52,13 +51,14 @@ function getExampleAvatarPath(): string {
}
export interface AddGuildOverlayProps {
guildsManager: GuildsManager;
addGuildData: IAddGuildData;
setActiveGuild: Dispatch<SetStateAction<CombinedGuild | null>>;
}
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 setOverlay = useSetRecoilState<ReactNode>(overlayState);
@ -97,6 +97,7 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
const [ submitFunc, submitButtonText, submitButtonShaking, _, submitFailMessage ] = useAsyncSubmitButton(
async () => {
if (!guildsManager) return { result: null, errorMessage: null };
if (validationErrorMessage || !avatarBuff) return { result: null, errorMessage: 'Validation failed' };
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' };
}
setActiveGuild(newGuild);
setSelectedGuildId(newGuild.id);
setOverlay(null);
return { result: newGuild, errorMessage: null };

View File

@ -14,7 +14,7 @@ import { useAsyncSubmitButton } from '../require/react-helper';
import Button from '../components/button';
import Overlay from '../components/overlay';
import { useSetRecoilState } from 'recoil';
import { overlayState } from '../atoms';
import { overlayState } from '../require/atoms';
export interface ChannelOverlayProps {
guild: CombinedGuild;

View File

@ -15,7 +15,7 @@ import Button from '../components/button';
import Overlay from '../components/overlay';
import { SubscriptionResult, useResourceSubscription } from '../require/guild-subscriptions';
import { useSetRecoilState } from 'recoil';
import { overlayState } from '../atoms';
import { overlayState } from '../require/atoms';
export interface PersonalizeOverlayProps {
guild: CombinedGuild;

View File

@ -1,7 +1,8 @@
import { ReactNode } from 'react';
import { atom, selector } from 'recoil';
import { Channel, GuildMetadata, Member, Message } from '../data-types';
import CombinedGuild from '../guild-combined';
import { atom, DefaultValue, selector } from 'recoil';
import { Channel, GuildMetadata, Member, Message } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import GuildsManager from '../../guilds-manager';
export interface GuildWithValue<T> {
guild: CombinedGuild;
@ -25,9 +26,18 @@ export const overlayState = atom<ReactNode>({
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>({
key: 'guildsState',
default: null
default: null,
dangerouslyAllowMutability: true
});
export const selectedGuildIdState = atom<number | null>({
@ -45,7 +55,8 @@ export const selectedGuildWithMetaState = selector<GuildWithErrorableValue<Guild
if (guildId === null) return null;
return guildsWithMeta.find(guildWithMeta => guildWithMeta.guild.id === guildId) ?? null;
}
},
dangerouslyAllowMutability: true
});
export const selectedGuildState = selector<CombinedGuild | null>({
@ -55,7 +66,8 @@ export const selectedGuildState = selector<CombinedGuild | null>({
if (guildWithMeta === null) return null;
return guildWithMeta.guild;
}
},
dangerouslyAllowMutability: true
});
export const selectedGuildMembersState = atom<GuildWithErrorableValue<Member[]> | null>({

View 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);
}
});
}

View File

@ -1,7 +1,8 @@
import React, { FC, ReactNode } from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
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 TitleBar from './sections/title-bar';
@ -12,12 +13,14 @@ export interface RootElementProps {
const RootElement: FC<RootElementProps> = (props: RootElementProps) => {
const { guildsManager } = props;
const [ overlay, setOverlay ] = useRecoilState<ReactNode>(overlayState);
const overlay = useRecoilValue<ReactNode>(overlayState);
useGuildsManagerWithRecoil(guildsManager);
return (
<div>
<TitleBar />
<GuildsManagerElement guildsManager={guildsManager} />
<GuildsManagerElement />
<div className="react-overlays">{overlay}</div>
</div>
);

View File

@ -3,9 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { Dispatch, FC, ReactNode, SetStateAction, useRef } from 'react';
import CombinedGuild from '../../guild-combined';
import GuildsManager from '../../guilds-manager';
import React, { FC, ReactNode, useRef } from 'react';
import GuildList from '../lists/guild-list';
import { useAsyncVoidCallback, useContextHover } from '../require/react-helper';
import fs from 'fs/promises';
@ -13,19 +11,10 @@ import AddGuildOverlay from '../overlays/overlay-add-guild';
import ErrorMessageOverlay from '../overlays/overlay-error-message';
import BasicHover, { BasicHoverSide } from '../contexts/context-hover-basic';
import BaseElements from '../require/base-elements';
import { overlayState } from '../atoms';
import { overlayState } from '../require/atoms';
import { useSetRecoilState } from 'recoil';
export interface GuildListContainerProps {
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 GuildListContainer: FC = () => {
const addGuildRef = useRef<HTMLDivElement>(null);
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
@ -71,16 +60,13 @@ const GuildListContainer: FC<GuildListContainerProps> = (props: GuildListContain
LOG.debug('bad guild data:', { addGuildData, fileText });
setOverlay(<ErrorMessageOverlay title="Unable to parse guild file" message="Bad guild data" />);
} else {
setOverlay(<AddGuildOverlay guildsManager={guildsManager} addGuildData={addGuildData} setActiveGuild={setActiveGuild} />);
setOverlay(<AddGuildOverlay addGuildData={addGuildData} />);
}
}, [ guildsManager ]);
}, []);
return (
<div className="guild-list-container">
<GuildList
guildsManager={guildsManager} guilds={guilds}
activeGuild={activeGuild} setActiveGuild={setActiveGuild}
/>
<GuildList />
<div
ref={addGuildRef}
className="add-guild" onClick={onAddGuildClickCallback}

View File

@ -1,36 +1,16 @@
import React, { Dispatch, FC, ReactNode, SetStateAction, useEffect, useState } from 'react';
import CombinedGuild from '../../guild-combined';
import GuildsManager from '../../guilds-manager';
import { useGuildListSubscription } from '../require/guilds-manager-subscriptions';
import React, { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { selectedGuildState } from '../require/atoms';
import GuildElement from './guild';
import GuildListContainer from './guild-list-container';
export interface GuildsManagerElementProps {
guildsManager: GuildsManager;
}
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 ]);
const GuildsManagerElement: FC = () => {
const selectedGuild = useRecoilValue(selectedGuildState);
return (
<div className="guilds-manager">
<GuildListContainer
guildsManager={guildsManager} guilds={guilds}
activeGuild={activeGuild} setActiveGuild={setActiveGuild}
/>
{activeGuild && <GuildElement guild={activeGuild} />}
<GuildListContainer />
{selectedGuild && <GuildElement guild={selectedGuild} />}
</div>
);
}