improve guild subscriptions by linking guild with the fetched result

This commit is contained in:
Michael Peters 2022-01-22 19:09:15 -06:00
parent e35187b877
commit 9a5e5d822e
17 changed files with 349 additions and 401 deletions

View File

@ -5,39 +5,38 @@ import CombinedGuild from '../../guild-combined';
import Util from '../../util';
import { IAddGuildData } from '../overlays/overlay-add-guild';
import BaseElements from '../require/base-elements';
import { useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
import { SubscriptionResult, useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
import { useAsyncVoidCallback, useDownloadButton, useOneTimeAsyncAction } from '../require/react-helper';
import Button, { ButtonColorType } from './button';
export interface TokenRowProps {
url: string;
guild: CombinedGuild;
guildMeta: GuildMetadata;
guildMetaGuild: CombinedGuild;
guildMetaResult: SubscriptionResult<GuildMetadata>;
token: Token;
}
const TokenRow: FC<TokenRowProps> = (props: TokenRowProps) => {
const { guild, guildMeta, guildMetaGuild, token } = props;
const { guild, guildMetaResult, token } = props;
const [ guildSocketConfigs ] = useOneTimeAsyncAction(
async () => await guild.fetchSocketConfigs(),
null,
[ guild ]
);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, guildMeta.iconResourceId, guildMetaGuild);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, guildMetaResult.value.iconResourceId, guildMetaResult.guild);
const [ revoke ] = useAsyncVoidCallback(async () => {
await guild.requestDoRevokeToken(token.token);
}, [ guild, token ]);
const [ downloadFunc, downloadText, downloadShaking ] = useDownloadButton(
guildMeta.name + '.cordis',
guildMetaResult.value.name + '.cordis',
async () => {
if (guildSocketConfigs === null) return null;
const guildSocketConfig = Util.randomChoice(guildSocketConfigs);
const addGuildData: IAddGuildData = {
name: guildMeta.name,
name: guildMetaResult.value.name,
url: guildSocketConfig.url,
cert: guildSocketConfig.cert,
token: token.token,
@ -47,7 +46,7 @@ const TokenRow: FC<TokenRowProps> = (props: TokenRowProps) => {
const json = JSON.stringify(addGuildData);
return Buffer.from(json);
},
[ guildSocketConfigs, guildMeta, token, iconSrc ]
[ guildSocketConfigs, guildMetaResult, token, iconSrc ]
);
const userText = (token.member instanceof Member ? 'Used by ' + token.member.displayName : token.member?.id) ?? 'Unused Token';

View File

@ -2,25 +2,25 @@ import React, { Dispatch, FC, ReactNode, RefObject, SetStateAction, useCallback,
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import PersonalizeOverlay from '../overlays/overlay-personalize';
import { SubscriptionResult } from '../require/guild-subscriptions';
import ContextMenu from './components/context-menu';
export interface ConnectionInfoContextMenuProps {
guild: CombinedGuild;
selfMember: Member;
selfMemberGuild: CombinedGuild;
selfMemberResult: SubscriptionResult<Member>;
relativeToRef: RefObject<HTMLElement>;
close: () => void;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: ConnectionInfoContextMenuProps) => {
const { guild, selfMember, selfMemberGuild, relativeToRef, close, setOverlay } = props;
const { guild, selfMemberResult, relativeToRef, close, setOverlay } = props;
const setSelfStatus = useCallback(async (status: string) => {
if (selfMember.status !== status) {
if (selfMemberResult.value.status !== status) {
await guild.requestSetStatus(status);
}
}, [ guild, selfMember ]);
}, [ guild, selfMemberResult ]);
const statusElements = useMemo(() => {
return [ 'online', 'away', 'busy', 'invisible' ].map(status => {
@ -36,8 +36,8 @@ const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: Co
const openPersonalize = useCallback(() => {
close();
setOverlay(<PersonalizeOverlay guild={guild} selfMember={selfMember} selfMemberGuild={selfMemberGuild} close={() => setOverlay(null)} />);
}, [ guild, selfMember, selfMemberGuild, close ]);
setOverlay(<PersonalizeOverlay guild={guild} selfMemberResult={selfMemberResult} close={() => setOverlay(null)} />);
}, [ guild, selfMemberResult, close ]);
const alignment = useMemo(() => {
return { bottom: 'top', centerX: 'centerX' }

View File

@ -4,25 +4,25 @@ import CombinedGuild from '../../guild-combined';
import ChannelOverlay from '../overlays/overlay-channel';
import GuildSettingsOverlay from '../overlays/overlay-guild-settings';
import BaseElements from '../require/base-elements';
import { SubscriptionResult } from '../require/guild-subscriptions';
import ContextMenu from './components/context-menu';
export interface GuildTitleContextMenuProps {
close: () => void;
relativeToRef: RefObject<HTMLElement>;
guild: CombinedGuild;
guildMeta: GuildMetadata;
guildMetaGuild: CombinedGuild;
guildMetaResult: SubscriptionResult<GuildMetadata>;
selfMember: Member;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitleContextMenuProps) => {
const { close, relativeToRef, guild, guildMeta, guildMetaGuild, selfMember, setOverlay } = props;
const { close, relativeToRef, guild, guildMetaResult, selfMember, setOverlay } = props;
const openGuildSettings = useCallback(() => {
close();
setOverlay(<GuildSettingsOverlay guild={guild} guildMeta={guildMeta} guildMetaGuild={guildMetaGuild} close={() => setOverlay(null)} />);
}, [ guild, guildMeta, guildMetaGuild, close ]);
setOverlay(<GuildSettingsOverlay guild={guild} guildMetaResult={guildMetaResult} close={() => setOverlay(null)} />);
}, [ guild, guildMetaResult, close ]);
const openCreateChannel = useCallback(() => {
close();
@ -56,7 +56,7 @@ const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitle
}, []);
return (
<ContextMenu alignment={alignment} close={close} relativeToRef={relativeToRef} realignDeps={[ guild, guildMeta, selfMember ]}>
<ContextMenu alignment={alignment} close={close} relativeToRef={relativeToRef} realignDeps={[ guild, guildMetaResult, selfMember ]}>
<div className="guild-title-context-menu">
{guildSettingsElement}
{createChannelElement}
@ -66,3 +66,4 @@ const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitle
}
export default GuildTitleContextMenu;

View File

@ -13,26 +13,25 @@ import { Duration } from 'moment';
import moment from 'moment';
import DropdownInput from '../components/input-dropdown';
import Button from '../components/button';
import { useTokensSubscription, useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
import { useTokensSubscription, useSoftImageSrcResourceSubscription, SubscriptionResult } from '../require/guild-subscriptions';
import TokenRow from '../components/token-row';
export interface GuildInvitesDisplayProps {
guild: CombinedGuild;
guildMeta: GuildMetadata;
guildMetaGuild: CombinedGuild;
guildMetaResult: SubscriptionResult<GuildMetadata>;
}
const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDisplayProps) => {
const { guild, guildMeta, guildMetaGuild } = props;
const { guild, guildMetaResult } = props;
const url = 'https://localhost:3030'; // TODO: this will likely be a dropdown list at some point
const [ fetchRetryCallable, tokens, tokensGuild, tokensError ] = useTokensSubscription(guild);
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(guild, guildMeta?.iconResourceId ?? null, guildMetaGuild);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, guildMetaResult.value.iconResourceId ?? null, guildMetaResult.guild);
useEffect(() => {
if (expiresFromNowText === 'never') {
@ -68,11 +67,11 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
// TODO: Try Again
return <div className="tokens-failed">Unable to load tokens</div>;
}
if (!guildMeta) {
if (!guildMetaResult) {
return <div className="no-guild-meta">No Guild Metadata</div>;
}
return tokens?.map((token: Token) => <TokenRow key={guild.id + token.token} url={url} guild={guild} token={token} guildMeta={guildMeta} guildMetaGuild={guildMetaGuild} />);
}, [ url, guild, tokens, tokensError ]);
return tokensResult?.value?.map((token: Token) => <TokenRow key={guild.id + token.token} url={url} guild={guild} token={token} guildMetaResult={guildMetaResult} />);
}, [ url, guild, tokensResult, tokensError ]);
return (
<Display
@ -92,7 +91,7 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
<div><Button shaking={tokenButtonShaking} onClick={createTokenFunc}>{tokenButtonText}</Button></div>
</div>
<InvitePreview
name={guildMeta?.name ?? ''} iconSrc={iconSrc}
name={guildMetaResult.value.name ?? ''} iconSrc={iconSrc}
url={url} expiresFromNow={expiresFromNow}
/>
</div>

View File

@ -10,19 +10,18 @@ import CombinedGuild from '../../guild-combined';
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 { SubscriptionResult, useResourceSubscription } from '../require/guild-subscriptions';
import { GuildMetadata } from '../../data-types';
export interface GuildOverviewDisplayProps {
guild: CombinedGuild;
guildMeta: GuildMetadata;
guildMetaGuild: CombinedGuild;
guildMetaResult: SubscriptionResult<GuildMetadata>;
}
const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOverviewDisplayProps) => {
const { guild, guildMeta, guildMetaGuild } = props;
const { guild, guildMetaResult } = props;
// TODO: Use the one from guild.tsx (for both of these?)
const [ iconResource, iconResourceGuild, iconResourceError ] = useResourceSubscription(guild, guildMeta?.iconResourceId ?? null, guildMetaGuild);
const [ iconResourceResult, iconResourceError ] = useResourceSubscription(guild, guildMetaResult.value.iconResourceId, guildMetaResult.guild);
const [ savedName, setSavedName ] = useState<string | null>(null);
const [ savedIconBuff, setSavedIconBuff ] = useState<Buffer | null>(null);
@ -40,18 +39,18 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
const [ iconInputMessage, setIconInputMessage ] = useState<string | null>(null);
useEffect(() => {
if (guildMeta) {
if (name === savedName) setName(guildMeta.name);
setSavedName(guildMeta.name);
if (guildMetaResult) {
if (name === savedName) setName(guildMetaResult.value.name);
setSavedName(guildMetaResult.value.name);
}
}, [ guildMeta ]);
}, [ guildMetaResult ]);
useEffect(() => {
if (iconResource) {
if (iconBuff === savedIconBuff) setIconBuff(iconResource.data);
setSavedIconBuff(iconResource.data);
if (iconResourceResult && iconResourceResult.value) {
if (iconBuff === savedIconBuff) setIconBuff(iconResourceResult.value.data);
setSavedIconBuff(iconResourceResult.value.data);
}
}, [ iconResource ]);
}, [ iconResourceResult ]);
const changes = useMemo(() => {
return name !== savedName || iconBuff?.toString('hex') !== savedIconBuff?.toString('hex')
@ -143,3 +142,4 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
}
export default GuildOverviewDisplay;

View File

@ -22,29 +22,29 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
// TODO: state higher up
// TODO: handle metadata error
const [ guildMeta, guildMetaGuild, guildMetaError ] = useGuildMetadataSubscription(guild);
const [ selfMember ] = useSelfMemberSubscription(guild);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, guildMeta?.iconResourceId ?? null, guildMetaGuild);
const [ guildMetaResult, guildMetaError ] = useGuildMetadataSubscription(guild);
const [ selfMemberResult ] = useSelfMemberSubscription(guild);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, guildMetaResult?.value.iconResourceId ?? null, guildMetaResult?.guild ?? null);
const [ contextHover, mouseEnterCallable, mouseLeaveCallable ] = useContextHover(() => {
if (!guildMeta) return null;
if (!selfMember) return null;
const nameStyle = selfMember.roleColor ? { color: selfMember.roleColor } : {};
if (!guildMetaResult) 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={[ guildMeta, selfMember ]}>
<BasicHover relativeToRef={rootRef} side={BasicHoverSide.RIGHT} realignDeps={[ guildMetaResult, selfMemberResult ]}>
<div className="guild-hover">
<div className="tab">{BaseElements.TAB_LEFT}</div>
<div className="info">
<div className="guild-name">{guildMeta.name}</div>
<div className={'connection ' + selfMember.status}>
<div className="guild-name">{guildMetaResult.value.name}</div>
<div className={'connection ' + selfMemberResult.value.status}>
<div className="status-circle" />
<div className="display-name" style={nameStyle}>{selfMember.displayName}</div>
<div className="display-name" style={nameStyle}>{selfMemberResult.value.displayName}</div>
</div>
</div>
</div>
</BasicHover>
)
}, [ guildMeta, selfMember ]);
}, [ guildMetaResult, selfMemberResult ]);
const leaveGuildCallable = useCallback(async () => {
guild.disconnect();
@ -85,3 +85,4 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
}
export default GuildListElement;

View File

@ -1,9 +1,4 @@
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 React, { FC, useMemo, useEffect } from 'react';
import React, { FC, useMemo } from 'react';
import { Member } from '../../../data-types';
import CombinedGuild from '../../../guild-combined';
import { useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions';
@ -16,6 +11,7 @@ export interface DummyMember {
avatarResourceId: null;
}
// Note: Using non-SubscriptionResult since we accept DummyMembers
export interface MemberProps {
guild: CombinedGuild;
member: Member | DummyMember;

View File

@ -54,17 +54,17 @@ const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageEl
const { guild, previewWidth, previewHeight, resourcePreviewId, resourceId, resourceName, resourceIdGuild, setOverlay } = props;
// TODO: Handle resourceError
const [ previewImgSrc, previewResource, previewResourceGuild, previewResourceError ] = useSoftImageSrcResourceSubscription(guild, resourcePreviewId, resourceIdGuild);
const [ previewImgSrc, previewResourceResult, previewResourceError ] = useSoftImageSrcResourceSubscription(guild, resourcePreviewId, resourceIdGuild);
const [ contextMenu, onContextMenu ] = useContextClickContextMenu((alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => {
if (!previewResource) return null;
if (!previewResourceResult || !previewResourceResult.value) return null;
return (
<ImageContextMenu
relativeToPos={relativeToPos} close={close}
resourceName={resourceName} resourceBuff={previewResource.data} isPreview={true}
resourceName={resourceName} resourceBuff={previewResourceResult.value.data} isPreview={true}
/>
);
}, [ previewResource, resourceName ]);
}, [ previewResourceResult, resourceName ]);
const openImageOverlay = useCallback(() => {
setOverlay(<ImageOverlay guild={guild} resourceId={resourceId} resourceName={resourceName} resourceIdGuild={resourceIdGuild} close={() => setOverlay(null)} />);

View File

@ -1,28 +1,34 @@
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 React, { FC, useMemo } from 'react';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { isNonNullAndHasValue, SubscriptionResult } from '../require/guild-subscriptions';
import MemberElement from './components/member-element';
export interface MemberListProps {
guild: CombinedGuild;
members: Member[] | null;
membersGuild: CombinedGuild | null;
membersResult: SubscriptionResult<Member[] | null> | null;
membersFetchError: unknown | null;
}
const MemberList: FC<MemberListProps> = (props: MemberListProps) => {
const { guild, members, membersGuild, membersFetchError } = props;
const { guild, membersResult, membersFetchError } = props;
const memberElements = useMemo(() => {
if (membersFetchError) {
// TODO: Try Again
return <div className="members-failed">Unable to load members</div>
}
if (!members || !membersGuild) {
if (!isNonNullAndHasValue(membersResult)) {
return <div className="members-loading">Loading members...</div>
}
return members?.map((member: Member) => <MemberElement key={guild.id + member.id} guild={guild} member={member} memberGuild={membersGuild} />);
}, [ guild, members, membersGuild, membersFetchError ]);
LOG.debug(`drawing ${membersResult.value.length} members`);
return membersResult.value.map((member: Member) => <MemberElement key={guild.id + member.id} guild={guild} member={member} memberGuild={membersResult.guild} />);
}, [ guild, membersResult, membersFetchError ]);
return (
<div className="member-list">
@ -32,3 +38,4 @@ const MemberList: FC<MemberListProps> = (props: MemberListProps) => {
};
export default MemberList;

View File

@ -1,8 +1,3 @@
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 React, { Dispatch, FC, ReactNode, SetStateAction, useMemo } from 'react';
import { Channel, Message } from '../../data-types';
import CombinedGuild from '../../guild-combined';
@ -26,8 +21,7 @@ const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
fetchBelowCallable,
setScrollRatio,
fetchResult,
messages,
messagesGuild,
messagesResult,
messagesFetchError,
messagesFetchAboveError,
messagesFetchBelowError
@ -35,15 +29,15 @@ const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
const messageElements = useMemo(() => {
const result = [];
if (messages && messagesGuild) {
for (let i = 0; i < messages.length; ++i) {
const prevMessage = messages[i - 1] ?? null;
const message = messages[i] as Message;
result.push(<MessageElement key={guild.id + message.id} guild={guild} message={message} prevMessage={prevMessage} messageGuild={messagesGuild} setOverlay={setOverlay} />);
if (messagesResult && messagesResult.value) {
for (let i = 0; i < messagesResult.value.length; ++i) {
const prevMessage = messagesResult.value[i - 1] ?? null;
const message = messagesResult.value[i] as Message;
result.push(<MessageElement key={guild.id + message.id} guild={guild} message={message} prevMessage={prevMessage} messageGuild={messagesResult.guild} setOverlay={setOverlay} />);
}
}
return result;
}, [ messages, messagesGuild ]);
}, [ messagesResult ]);
return (
<div className="message-list">
@ -66,3 +60,4 @@ const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
}
export default MessageList;

View File

@ -1,8 +1,3 @@
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 React, { FC, useEffect, useRef, useState } from "react";
import CombinedGuild from "../../guild-combined";
import ChoicesControl from "../components/control-choices";
@ -10,15 +5,15 @@ import GuildInvitesDisplay from "../displays/display-guild-invites";
import GuildOverviewDisplay from "../displays/display-guild-overview";
import { GuildMetadata } from '../../data-types';
import Overlay from '../components/overlay';
import { SubscriptionResult } from '../require/guild-subscriptions';
export interface GuildSettingsOverlayProps {
guild: CombinedGuild;
guildMeta: GuildMetadata;
guildMetaGuild: CombinedGuild;
guildMetaResult: SubscriptionResult<GuildMetadata>;
close: () => void;
}
const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSettingsOverlayProps) => {
const { guild, guildMeta, guildMetaGuild, close } = props;
const { guild, guildMetaResult, close } = props;
const rootRef = useRef<HTMLDivElement>(null);
@ -26,15 +21,15 @@ const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSetting
const [ display, setDisplay ] = useState<JSX.Element>();
useEffect(() => {
if (selectedId === 'overview') setDisplay(<GuildOverviewDisplay guild={guild} guildMeta={guildMeta} guildMetaGuild={guildMetaGuild} />);
if (selectedId === 'overview') setDisplay(<GuildOverviewDisplay guild={guild} guildMetaResult={guildMetaResult} />);
//if (selectedId === 'roles' ) setDisplay(<GuildOverviewDisplay guild={guild} guildMeta={guildMeta} />);
if (selectedId === 'invites' ) setDisplay(<GuildInvitesDisplay guild={guild} guildMeta={guildMeta} guildMetaGuild={guildMetaGuild} />);
if (selectedId === 'invites' ) setDisplay(<GuildInvitesDisplay guild={guild} guildMetaResult={guildMetaResult} />);
}, [ selectedId ]);
return (
<Overlay childRootRef={rootRef} close={close}>
<div ref={rootRef} className="content display-swapper guild-settings">
<ChoicesControl title={guildMeta.name} selectedId={selectedId} setSelectedId={setSelectedId} choices={[
<ChoicesControl title={guildMetaResult.value.name} selectedId={selectedId} setSelectedId={setSelectedId} choices={[
{ id: 'overview', display: 'Overview' },
{ id: 'roles', display: 'Roles' },
{ id: 'invites', display: 'Invites' },
@ -46,3 +41,4 @@ const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSetting
}
export default GuildSettingsOverlay;

View File

@ -25,19 +25,22 @@ const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
const rootRef = useRef<HTMLDivElement>(null);
const [ imgSrc, resource, resourceGuild, resourceError ] = useSoftImageSrcResourceSubscription(guild, resourceId, resourceIdGuild);
const [ imgSrc, resourceResult, resourceError ] = useSoftImageSrcResourceSubscription(guild, resourceId, resourceIdGuild);
const [ contextMenu, onContextMenu ] = useContextClickContextMenu((alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => {
if (!resource) return null;
if (!resourceResult || !resourceResult.value) return null;
return (
<ImageContextMenu
relativeToPos={relativeToPos} close={close}
resourceName={resourceName} resourceBuff={resource.data} isPreview={false}
resourceName={resourceName} resourceBuff={resourceResult.value.data} isPreview={false}
/>
);
}, [ resource, resourceName ]);
}, [ resourceResult, resourceName ]);
const sizeText = useMemo(() => resource ? ElementsUtil.humanSize(resource.data.length) : 'Loading Size...', [ resource ]);
const sizeText = useMemo(
() => resourceResult?.value ? ElementsUtil.humanSize(resourceResult.value.data.length) : 'Loading Size...',
[ resourceResult ]
);
return (
<Overlay childRootRef={rootRef} close={close}>
@ -48,8 +51,9 @@ const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
<div className="name">{resourceName}</div>
<div className="size">{sizeText}</div>
</div>
{/* TODO: I think this may break if the download button gets clicked before the resource is loaded... */}
<DownloadButton
downloadBuff={resource?.data} downloadBuffErr={!!resourceError}
downloadBuff={resourceResult?.value?.data} downloadBuffErr={!!resourceError}
resourceName={resourceName}></DownloadButton>
</div>
{contextMenu}

View File

@ -13,27 +13,26 @@ import SubmitOverlayLower from '../components/submit-overlay-lower';
import { useAsyncSubmitButton } from '../require/react-helper';
import Button from '../components/button';
import Overlay from '../components/overlay';
import { useResourceSubscription } from '../require/guild-subscriptions';
import { SubscriptionResult, useResourceSubscription } from '../require/guild-subscriptions';
export interface PersonalizeOverlayProps {
guild: CombinedGuild;
selfMember: Member;
selfMemberGuild: CombinedGuild;
selfMemberResult: SubscriptionResult<Member>;
close: () => void;
}
const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverlayProps) => {
const { guild, selfMember, selfMemberGuild, close } = props;
const { guild, selfMemberResult, close } = props;
const rootRef = useRef<HTMLDivElement>(null);
const [ avatarResource, avatarResourceGuild, avatarResourceError ] = useResourceSubscription(guild, selfMember.avatarResourceId, selfMemberGuild);
const [ avatarResourceResult, avatarResourceError ] = useResourceSubscription(guild, selfMemberResult.value.avatarResourceId, selfMemberResult.guild);
const displayNameInputRef = createRef<HTMLInputElement>();
const [ savedDisplayName, setSavedDisplayName ] = useState<string>(selfMember.displayName);
const [ savedDisplayName, setSavedDisplayName ] = useState<string>(selfMemberResult.value.displayName);
const [ savedAvatarBuff, setSavedAvatarBuff ] = useState<Buffer | null>(null);
const [ displayName, setDisplayName ] = useState<string>(selfMember.displayName);
const [ displayName, setDisplayName ] = useState<string>(selfMemberResult.value.displayName);
const [ avatarBuff, setAvatarBuff ] = useState<Buffer | null>(null);
const [ displayNameInputValid, setDisplayNameInputValid ] = useState<boolean>(false);
@ -43,11 +42,11 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
const [ avatarInputMessage, setAvatarInputMessage ] = useState<string | null>(null);
useEffect(() => {
if (avatarResource) {
if (avatarBuff === savedAvatarBuff) setAvatarBuff(avatarResource.data);
setSavedAvatarBuff(avatarResource.data);
if (avatarResourceResult && avatarResourceResult.value) {
if (avatarBuff === savedAvatarBuff) setAvatarBuff(avatarResourceResult.value.data);
setSavedAvatarBuff(avatarResourceResult.value.data);
}
}, [ avatarResource ]);
}, [ avatarResourceResult ]);
useEffect(() => {
displayNameInputRef.current?.focus();

View File

@ -9,13 +9,22 @@ import CombinedGuild from "../../guild-combined";
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AutoVerifierChangesType } from "../../auto-verifier";
import { Conflictable, Connectable } from "../../guild-types";
import { EventEmitter } from 'tsee';
import { IDQuery, PartialMessageListQuery } from '../../auto-verifier-with-args';
import { Token, Channel } from '../../data-types';
import { useIsMountedRef, useOneTimeAsyncAction } from './react-helper';
import Globals from '../../globals';
import ElementsUtil from './elements-util';
// Abuse closures to get state in case it changed after an await call
function getStateAfterAwait<T>(setState: Dispatch<SetStateAction<T>>): T {
let x: unknown;
setState(state => {
x = state;
return state;
});
return x as T;
}
export type SingleSubscriptionEvents = {
'fetch': () => void;
'updated': () => void;
@ -34,7 +43,10 @@ export type MultipleSubscriptionEvents<T> = {
interface EffectParams<T> {
guild: CombinedGuild;
onFetch: (value: T | null, valueGuild: CombinedGuild) => void;
// TODO: I changed this from value: T | null to just value: T. I think
// this file (and potentially some others) has a bunch of spots that use .value?.xxx
// where it doesn't need to. Maybe there is an ESLint thing for this?
onFetch: (value: T, valueGuild: CombinedGuild) => void;
onFetchError: (e: unknown) => void;
bindEventsFunc: () => void;
unbindEventsFunc: () => void;
@ -42,6 +54,15 @@ interface EffectParams<T> {
type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
export interface SubscriptionResult<T> {
value: T;
guild: CombinedGuild;
}
export function isNonNullAndHasValue<T>(subscriptionResult: SubscriptionResult<T | null> | null): subscriptionResult is SubscriptionResult<T> {
return !!(subscriptionResult !== null && subscriptionResult.value !== null);
}
interface SingleEventMappingParams<T, UE extends keyof Connectable, CE extends keyof Conflictable> {
updatedEventName: UE;
updatedEventArgsMap: (...args: Arguments<Connectable[UE]>) => T;
@ -69,9 +90,16 @@ interface MultipleEventMappingParams<
sortFunc: (a: T, b: T) => number; // Friendly reminder that v8 uses timsort so this is O(n) for pre-sorted stuff
}
/**
* Core function to subscribe to a general fetchable guild function
* @param subscriptionParams Event callback functions
* @param fetchFunc Function that can be called to fetch the data for the subscription. This function will be called automatically if it is changed.
* Typically, this function will be set up in a useCallback with a dependency on at least the guild.
*/
function useGuildSubscriptionEffect<T>(
subscriptionParams: EffectParams<T>,
fetchFunc: (() => Promise<T>) | (() => Promise<T | null>)
fetchFunc: () => Promise<T>
): [ fetchRetryCallable: () => Promise<void> ] {
const { guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc } = subscriptionParams;
@ -85,7 +113,7 @@ function useGuildSubscriptionEffect<T>(
try {
const value = await fetchFunc();
if (!isMounted.current) return;
if (guildRef.current !== guild) return; // Don't call onFetch if we changed guilds. TODO: Test this
if (guildRef.current !== guild) return; // Don't even call onFetch if we changed guilds. TODO: Test this
onFetch(value, guild);
} catch (e: unknown) {
LOG.error('error fetching for subscription', e);
@ -115,66 +143,51 @@ function useGuildSubscriptionEffect<T>(
return [ fetchManagerFunc ];
}
/**
* Subscribe to a fetchable guild function that returns a single element (i.e. GuildMetadata)
* @param guild The guild to listen for changes on
* @param eventMappingParams The events to use to listen for changes (such as updates and conflicts)
* @param fetchFunc The function to call to fetch initial data
*/
function useSingleGuildSubscription<T, UE extends keyof Connectable, CE extends keyof Conflictable>(
guild: CombinedGuild,
eventMappingParams: SingleEventMappingParams<T, UE, CE>,
fetchFunc: (() => Promise<T>) | (() => Promise<T | null>)
): [value: T | null, valueGuild: CombinedGuild | null, fetchError: unknown | null, events: EventEmitter<SingleSubscriptionEvents>] {
fetchFunc: () => Promise<T>
): [lastResult: SubscriptionResult<T> | null, fetchError: unknown | null] {
const { updatedEventName, updatedEventArgsMap, conflictEventName, conflictEventArgsMap } = eventMappingParams;
const isMounted = useIsMountedRef();
const guildRef = useRef<CombinedGuild>(guild);
guildRef.current = guild;
const [ fetchError, setFetchError ] = useState<unknown | null>(null);
const [ value, setValue ] = useState<T | null>(null);
const [ valueGuild, setValueGuild ] = useState<CombinedGuild | null>(null);
const events = useMemo(() => new EventEmitter<SingleSubscriptionEvents>(), []);
const [ lastResult, setLastResult ] = useState<{ value: T, guild: CombinedGuild } | null>(null);
const onFetch = useCallback((fetchValue: T | null, fetchValueGuild: CombinedGuild) => {
setValue(fetchValue);
setValueGuild(fetchValueGuild);
setLastResult(fetchValue ? { value: fetchValue, guild: fetchValueGuild } : null);
setFetchError(null);
events.emit('fetch');
}, []);
const onFetchError = useCallback((e: unknown) => {
setFetchError(e);
setValue(null);
setValueGuild(null);
events.emit('fetch-error');
}, []);
const onUpdated = useCallback((updateValue: T, updateValueGuild: CombinedGuild) => {
setValue(updateValue);
if (updateValueGuild !== guildRef.current) {
LOG.warn(`update guild (${updateValueGuild.id}) != current guild (${guildRef.current})`);
}
events.emit('updated');
}, []);
const onConflict = useCallback((conflictValue: T, conflictValueGuild: CombinedGuild) => {
setValue(conflictValue);
if (conflictValueGuild !== guildRef.current) {
LOG.warn(`conflict guild (${conflictValueGuild.id}) != current guild (${guildRef.current})`);
}
events.emit('conflict');
setLastResult(null)
}, []);
// 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
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return;
const value = updatedEventArgsMap(...args);
onUpdated(value, guild);
setLastResult(lastResult => {
if (guild !== lastResult?.guild) return lastResult;
return { value: value, guild: guild };
});
}, [ guild ]) as (Connectable & Conflictable)[UE];
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return;
const value = conflictEventArgsMap(...args);
onConflict(value, guild);
setLastResult(lastResult => {
if (guild !== lastResult?.guild) return lastResult;
return { value: value, guild: guild };
});
}, [ guild ]) as (Connectable & Conflictable)[CE];
const bindEventsFunc = useCallback(() => {
@ -194,7 +207,7 @@ function useSingleGuildSubscription<T, UE extends keyof Connectable, CE extends
unbindEventsFunc
}, fetchFunc);
return [ value, valueGuild, fetchError, events ];
return [ lastResult, fetchError ];
}
function useMultipleGuildSubscription<
@ -206,13 +219,11 @@ function useMultipleGuildSubscription<
>(
guild: CombinedGuild,
eventMappingParams: MultipleEventMappingParams<T, NE, UE, RE, CE>,
fetchFunc: (() => Promise<T[]>) | (() => Promise<T[] | null>)
fetchFunc: () => Promise<T[]>
): [
fetchRetryCallable: () => Promise<void>,
value: T[] | null,
valueGuild: CombinedGuild | null,
lastResult: SubscriptionResult<T[]> | null,
fetchError: unknown | null,
events: EventEmitter<MultipleSubscriptionEvents<T>>
] {
const {
newEventName, newEventArgsMap,
@ -223,101 +234,73 @@ function useMultipleGuildSubscription<
} = eventMappingParams;
const isMounted = useIsMountedRef();
const guildRef = useRef<CombinedGuild>(guild);
guildRef.current = guild;
const [ fetchError, setFetchError ] = useState<unknown | null>(null);
const [ value, setValue ] = useState<T[] | null>(null);
const [ valueGuild, setValueGuild ] = useState<CombinedGuild | null>(null);
const [ lastResult, setLastResult ] = useState<{ value: T[], guild: CombinedGuild } | null>(null);
const events = useMemo(() => new EventEmitter<MultipleSubscriptionEvents<T>>(), []);
const onFetch = useCallback((fetchValue: T[] | null, fetchValueGuild: CombinedGuild) => {
const onFetch = useCallback((fetchValue: T[], fetchValueGuild: CombinedGuild) => {
if (fetchValue) fetchValue.sort(sortFunc);
/* LOG.debug('onFetch', { valueType: (fetchValue?.length && (fetchValue[0] as T).constructor.name) ?? 'null', guild: fetchValueGuild.id }); */
setValue(fetchValue);
setValueGuild(fetchValueGuild);
setLastResult({ value: fetchValue, guild: fetchValueGuild });
setFetchError(null);
events.emit('fetch');
}, [ sortFunc ]);
const onFetchError = useCallback((e: unknown) => {
setFetchError(e);
setValue(null);
events.emit('fetch-error');
setLastResult(null)
}, []);
const onNew = useCallback((newElements: T[], newElementsGuild: CombinedGuild) => {
setValue(currentValue => {
if (currentValue === null) return Array.from(newElements).sort(sortFunc);
return currentValue.concat(newElements).sort(sortFunc);
});
if (newElementsGuild !== guildRef.current) {
LOG.warn(`new elements guild (${newElementsGuild.id}) != current guild (${guildRef.current})`);
}
events.emit('new', newElements);
}, [ sortFunc ]);
const onUpdated = useCallback((updatedElements: T[], updatedElementsGuild: CombinedGuild) => {
setValue(currentValue => {
if (currentValue === null) return null;
return currentValue.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc);
});
if (updatedElementsGuild !== guildRef.current) {
LOG.warn(`updated elements guild (${updatedElementsGuild.id}) != current guild (${guildRef.current})`);
}
events.emit('updated', updatedElements);
}, [ sortFunc ]);
const onRemoved = useCallback((removedElements: T[], removedElementsGuild: CombinedGuild) => {
setValue(currentValue => {
if (currentValue === null) return null;
const deletedIds = new Set(removedElements.map(deletedElement => deletedElement.id));
return currentValue.filter(element => !deletedIds.has(element.id)).sort(sortFunc);
});
if (removedElementsGuild !== guildRef.current) {
LOG.warn(`removed elements guild (${removedElementsGuild.id}) != current guild (${guildRef.current})`);
}
events.emit('removed', removedElements);
}, [ sortFunc ]);
const onConflict = useCallback((changes: Changes<T>, changesGuild: CombinedGuild) => {
setValue(currentValue => {
if (currentValue === null) return null;
const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id));
return currentValue
.concat(changes.added)
.map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element)
.filter(element => !deletedIds.has(element.id))
.sort(sortFunc);
});
if (changesGuild !== guildRef.current) {
LOG.warn(`conflict changes guild (${changesGuild.id}) != current guild (${guildRef.current})`);
}
setValueGuild(changesGuild);
events.emit('conflict', changes);
}, [ sortFunc ]);
// 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
const boundNewFunc = useCallback((...args: Arguments<Connectable[NE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return; // prevent changes from a different guild
onNew(newEventArgsMap(...args), guild);
}, [ guild, onNew, newEventArgsMap ]) as (Connectable & Conflictable)[NE];
const newElements = newEventArgsMap(...args);
setLastResult((lastResult) => {
// TODO: There's a bug in this and other functions like it in the file where if you switch
// from one guild and then back again, there is potential for the new/add to be triggered twice
// if the add result happens slowly. This could be mitigated by adding some sort of "selection id"
// each time the guild changes. For our purposes so far, this "bug" should be OK to leave in.
// In the incredibly rare case where this does happen, you will see things duplicated
if (guild !== lastResult?.guild) return lastResult; // prevent changes from a different guild
if (!lastResult) { LOG.warn('got onNew with no lastResult'); return null; } // Sanity check
return { value: (lastResult.value ?? []).concat(newElements).sort(sortFunc), guild: guild };
});
}, [ guild, newEventArgsMap ]) as (Connectable & Conflictable)[NE];
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return;
onUpdated(updatedEventArgsMap(...args), guild);
}, [ guild, onUpdated, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE];
if (guild !== lastResult?.guild) return;
const updatedElements = updatedEventArgsMap(...args);
setLastResult((lastResult) => {
if (!lastResult) { LOG.warn('got onUpdated with no lastResult'); return null; } // Sanity check
return { value: (lastResult.value ?? []).map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc), guild: guild };
});
}, [ guild, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE];
const boundRemovedFunc = useCallback((...args: Arguments<Connectable[RE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return;
onRemoved(removedEventArgsMap(...args), guild);
}, [ guild, onRemoved, removedEventArgsMap ]) as (Connectable & Conflictable)[RE];
if (guild !== lastResult?.guild) return;
const removedElements = removedEventArgsMap(...args);
setLastResult((lastResult) => {
if (!lastResult) { LOG.warn('got onRemoved with no lastResult'); return null; } // Sanity check
const deletedIds = new Set(removedElements.map(deletedElement => deletedElement.id));
return { value: (lastResult.value ?? []).filter(element => !deletedIds.has(element.id)), guild: guild };
});
}, [ guild, removedEventArgsMap ]) as (Connectable & Conflictable)[RE];
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return;
onConflict(conflictEventArgsMap(...args), guild);
}, [ guild, onConflict, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE];
const changes = conflictEventArgsMap(...args);
setLastResult((lastResult) => {
if (!lastResult) { LOG.warn('got onConflict with no lastResult'); return null; } // Sanity check
const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id));
return {
value: (lastResult.value ?? [])
.concat(changes.added) // Added
.map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element) // Updated
.filter(element => !deletedIds.has(element.id)) // Deleted
.sort(sortFunc),
guild: guild
};
});
}, [ guild, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE];
const bindEventsFunc = useCallback(() => {
guild.on(newEventName, boundNewFunc);
@ -340,7 +323,7 @@ function useMultipleGuildSubscription<
unbindEventsFunc
}, fetchFunc);
return [ fetchRetryCallable, value, valueGuild, fetchError, events ];
return [ fetchRetryCallable, lastResult, fetchError ];
}
function useMultipleGuildSubscriptionScrolling<
@ -354,7 +337,7 @@ function useMultipleGuildSubscriptionScrolling<
eventMappingParams: MultipleEventMappingParams<T, NE, UE, RE, CE>,
maxElements: number,
maxFetchElements: number,
fetchFunc: (() => Promise<T[]>) | (() => Promise<T[] | null>),
fetchFunc: () => Promise<T[]>,
fetchAboveFunc: ((reference: T) => Promise<T[] | null>),
fetchBelowFunc: ((reference: T) => Promise<T[] | null>),
): [
@ -363,12 +346,10 @@ function useMultipleGuildSubscriptionScrolling<
fetchBelowCallable: () => Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }>,
setScrollRatio: Dispatch<SetStateAction<number>>,
fetchResult: { hasMoreAbove: boolean, hasMoreBelow: boolean } | null,
value: T[] | null,
valueGuild: CombinedGuild | null,
lastResult: SubscriptionResult<T[]> | null,
fetchError: unknown | null,
fetchAboveError: unknown | null,
fetchBelowError: unknown | null,
events: EventEmitter<MultipleSubscriptionEvents<T>>
] {
const {
newEventName, newEventArgsMap,
@ -379,11 +360,9 @@ function useMultipleGuildSubscriptionScrolling<
} = eventMappingParams;
const isMounted = useIsMountedRef();
const guildRef = useRef<CombinedGuild>(guild);
guildRef.current = guild;
const [ value, setValue ] = useState<T[] | null>(null);
const [ valueGuild, setValueGuild ] = useState<CombinedGuild | null>(null);
// TODO: lastResult.value should really be only T[] instead of | null since we set it to [] anyway in the onUpdate, etc functions
const [ lastResult, setLastResult ] = useState<{ value: T[], guild: CombinedGuild } | null>(null);
const [ fetchError, setFetchError ] = useState<unknown | null>(null);
const [ fetchAboveError, setFetchAboveError ] = useState<unknown | null>(null);
@ -413,28 +392,29 @@ function useMultipleGuildSubscriptionScrolling<
return elements.slice(fromTop, elements.length - fromBottom);
}
const events = useMemo(() => new EventEmitter<MultipleSubscriptionEvents<T>>(), []);
const fetchAboveCallable = useCallback(async (): Promise<{ hasMoreAbove: boolean, removedFromBottom: boolean }> => {
if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false };
if (guildRef.current !== guild) return { hasMoreAbove: false, removedFromBottom: false };
if (!value || value.length === 0) return { hasMoreAbove: false, removedFromBottom: false };
if (!lastResult || !lastResult.value || lastResult.value.length === 0) return { hasMoreAbove: false, removedFromBottom: false };
if (guild !== lastResult.guild) return { hasMoreAbove: false, removedFromBottom: false };
try {
const reference = value[0] as T;
const reference = lastResult.value[0] as T;
const aboveElements = await fetchAboveFunc(reference);
const lastResultAfterAwait = getStateAfterAwait(setLastResult);
if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false };
if (guildRef.current !== guild) return { hasMoreAbove: false, removedFromBottom: false };
if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return { hasMoreAbove: false, removedFromBottom: false };
setFetchAboveError(null);
if (aboveElements) {
const hasMoreAbove = aboveElements.length >= maxFetchElements;
let removedFromBottom = false;
setValue(currentValue => {
let newValue = aboveElements.concat(currentValue ?? []).sort(sortFunc);
setLastResult((lastResult) => {
if (!lastResult) return null;
let newValue = aboveElements.concat(lastResult.value ?? []).sort(sortFunc);
if (newValue.length > maxElements) {
newValue = newValue.slice(0, maxElements);
removedFromBottom = true;
}
return newValue;
return { value: newValue, guild: lastResult.guild };
});
return { hasMoreAbove, removedFromBottom };
} else {
@ -442,48 +422,52 @@ function useMultipleGuildSubscriptionScrolling<
}
} catch (e: unknown) {
LOG.error('error fetching above for subscription', e);
const lastResultAfterAwait = getStateAfterAwait(setLastResult);
if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false };
if (guildRef.current !== guild) return { hasMoreAbove: false, removedFromBottom: false };
if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return { hasMoreAbove: false, removedFromBottom: false };
setFetchAboveError(e);
return { hasMoreAbove: true, removedFromBottom: false };
}
}, [ guild, value, fetchAboveFunc, maxFetchElements ]);
}, [ guild, lastResult, fetchAboveFunc, maxFetchElements ]);
const fetchBelowCallable = useCallback(async (): Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }> => {
if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false };
if (guildRef.current !== guild) return { hasMoreBelow: false, removedFromTop: false };
if (!value || value.length === 0) return { hasMoreBelow: false, removedFromTop: false };
if (!lastResult || !lastResult.value || lastResult.value.length === 0) return { hasMoreBelow: false, removedFromTop: false };
if (guild !== lastResult.guild) return { hasMoreBelow: false, removedFromTop: false };
try {
const reference = value[value.length - 1] as T;
const reference = lastResult.value[lastResult.value.length - 1] as T;
const belowElements = await fetchBelowFunc(reference);
const lastResultAfterAwait = getStateAfterAwait(setLastResult);
if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false };
if (guildRef.current !== guild) return { hasMoreBelow: false, removedFromTop: false };
if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return { hasMoreBelow: false, removedFromTop: false };
setFetchBelowError(null);
if (belowElements) {
const hasMoreBelow = belowElements.length >= maxFetchElements;
let removedFromTop = false;
setValue(currentValue => {
let newValue = (currentValue ?? []).concat(belowElements).sort(sortFunc);
setLastResult((lastResult) => {
if (!lastResult) return null;
let newValue = (lastResult.value ?? []).concat(belowElements).sort(sortFunc);
if (newValue.length > maxElements) {
newValue = newValue.slice(Math.max(newValue.length - maxElements, 0));
removedFromTop = true;
}
return newValue;
});
return { value: newValue, guild: lastResult.guild };
})
return { hasMoreBelow, removedFromTop };
} else {
return { hasMoreBelow: false, removedFromTop: false };
}
} catch (e: unknown) {
LOG.error('error fetching below for subscription', e);
const lastResultAfterAwait = getStateAfterAwait(setLastResult);
if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false };
if (guildRef.current !== guild) return { hasMoreBelow: false, removedFromTop: false };
if (!lastResultAfterAwait || guild !== lastResultAfterAwait.guild) return { hasMoreBelow: false, removedFromTop: false };
setFetchBelowError(e);
return { hasMoreBelow: true, removedFromTop: false };
}
}, [ value, fetchBelowFunc, maxFetchElements ]);
}, [ lastResult, fetchBelowFunc, maxFetchElements ]);
const onFetch = useCallback((fetchValue: T[] | null, fetchValueGuild: CombinedGuild) => {
const onFetch = useCallback((fetchValue: T[], fetchValueGuild: CombinedGuild) => {
let hasMoreAbove = false;
if (fetchValue) {
if (fetchValue.length >= maxFetchElements) hasMoreAbove = true;
@ -491,60 +475,58 @@ function useMultipleGuildSubscriptionScrolling<
}
//LOG.debug('Got items: ', { fetchValueLength: fetchValue?.length ?? '<empty>' })
setFetchResult({ hasMoreAbove, hasMoreBelow: false });
setValue(fetchValue);
setValueGuild(fetchValueGuild);
setLastResult({ value: fetchValue, guild: fetchValueGuild });
setFetchError(null);
events.emit('fetch');
}, [ sortFunc, maxFetchElements, maxElements ]);
const onFetchError = useCallback((e: unknown) => {
setFetchError(e);
setValue(null);
setValueGuild(null);
events.emit('fetch-error');
setLastResult(null);
}, []);
const onNew = useCallback((newElements: T[], newElementsGuild: CombinedGuild) => {
setValue(currentValue => {
if (currentValue === null) return null;
let newValue = currentValue.concat(newElements).sort(sortFunc);
// 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
const boundNewFunc = useCallback((...args: Arguments<Connectable[NE]>): void => {
if (!isMounted.current) return;
if (guild !== lastResult?.guild) return; // Cancel calls when the guild changes
const newElements = newEventArgsMap(...args);
setLastResult((lastResult) => {
if (lastResult === null) return null;
let newValue = (lastResult.value ?? []).concat(newElements).sort(sortFunc);
if (newValue.length > maxElements) {
// Remove in a way that tries to keep the scrollbar position consistent
newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements));
}
return newValue;
return { value: newValue, guild: guild };
});
if (newElementsGuild !== guildRef.current) {
LOG.warn(`new elements guild (${newElementsGuild.id}) != current guild (${guildRef.current})`);
}
events.emit('new', newElements);
}, [ sortFunc, getRemoveCounts ]);
const onUpdated = useCallback((updatedElements: T[], updatedElementsGuild: CombinedGuild) => {
setValue(currentValue => {
if (currentValue === null) return null;
return currentValue.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc);
}, [ guild, newEventArgsMap ]) as (Connectable & Conflictable)[NE];
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
if (!isMounted.current) return;
if (guild !== lastResult?.guild) return; // Cancel calls when the guild changes
const updatedElements = updatedEventArgsMap(...args);
setLastResult((lastResult) => {
if (lastResult === null) return null;
return { value: (lastResult.value ?? []).map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc), guild: guild };
});
if (updatedElementsGuild !== guildRef.current) {
LOG.warn(`updated elements guild (${updatedElementsGuild.id}) != current guild (${guildRef.current})`);
}
events.emit('updated', updatedElements);
}, [ sortFunc ]);
const onRemoved = useCallback((removedElements: T[], removedElementsGuild: CombinedGuild) => {
setValue(currentValue => {
if (currentValue === null) return null;
const deletedIds = new Set(removedElements.map(deletedElement => deletedElement.id));
return currentValue.filter(element => !deletedIds.has(element.id)).sort(sortFunc);
}, [ guild, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE];
const boundRemovedFunc = useCallback((...args: Arguments<Connectable[RE]>): void => {
if (!isMounted.current) return;
if (guild !== lastResult?.guild) return; // Cancel calls when the guild changes
const removedElements = removedEventArgsMap(...args);
setLastResult((lastResult) => {
if (lastResult === null) return null;
const deletedIds = new Set(removedElements.map(removedElement => removedElement.id));
return { value: (lastResult.value ?? []).filter(element => !deletedIds.has(element.id)), guild: guild };
});
if (removedElementsGuild !== guildRef.current) {
LOG.warn(`updated elements guild (${removedElementsGuild.id}) != current guild (${guildRef.current})`);
}
events.emit('removed', removedElements);
}, [ sortFunc ]);
const onConflict = useCallback((changes: Changes<T>, changesGuild: CombinedGuild) => {
setValue(currentValue => {
if (currentValue === null) return null;
}, [ guild, removedEventArgsMap ]) as (Connectable & Conflictable)[RE];
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
if (!isMounted.current) return;
if (guild !== lastResult?.guild) return; // Cancel calls when the guild changes
const changes = conflictEventArgsMap(...args);
setLastResult((lastResult) => {
if (lastResult === null) return null;
const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id));
let newValue = currentValue
let newValue = (lastResult.value ?? [])
.concat(changes.added)
.map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element)
.filter(element => !deletedIds.has(element.id))
@ -552,36 +534,9 @@ function useMultipleGuildSubscriptionScrolling<
if (newValue.length > maxElements) {
newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements));
}
return newValue;
return { value: newValue, guild: guild };
});
if (changesGuild !== guildRef.current) {
LOG.warn(`conflict changes guild (${changesGuild.id}) != current guild (${guildRef.current})`);
}
events.emit('conflict', changes);
}, [ sortFunc, getRemoveCounts ]);
// 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
const boundNewFunc = useCallback((...args: Arguments<Connectable[NE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return; // Cancel calls when the guild changes
onNew(newEventArgsMap(...args), guild);
}, [ guild, onNew, newEventArgsMap ]) as (Connectable & Conflictable)[NE];
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return; // Cancel calls when the guild changes
onUpdated(updatedEventArgsMap(...args), guild);
}, [ onUpdated, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE];
const boundRemovedFunc = useCallback((...args: Arguments<Connectable[RE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return; // Cancel calls when the guild changes
onRemoved(removedEventArgsMap(...args), guild);
}, [ onRemoved, removedEventArgsMap ]) as (Connectable & Conflictable)[RE];
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
if (!isMounted.current) return;
if (guildRef.current !== guild) return; // Cancel calls when the guild changes
onConflict(conflictEventArgsMap(...args), guild);
}, [ onConflict, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE];
}, [ guild, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE];
const bindEventsFunc = useCallback(() => {
guild.on(newEventName, boundNewFunc);
@ -610,12 +565,10 @@ function useMultipleGuildSubscriptionScrolling<
fetchBelowCallable,
setScrollRatio,
fetchResult,
value,
valueGuild,
lastResult,
fetchError,
fetchAboveError,
fetchBelowError,
events
];
}
@ -628,47 +581,46 @@ export function useGuildMetadataSubscription(guild: CombinedGuild) {
updatedEventName: 'update-metadata',
updatedEventArgsMap: (guildMeta: GuildMetadata) => guildMeta,
conflictEventName: 'conflict-metadata',
conflictEventArgsMap: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, _oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta
}, fetchMetadataFunc);
}
export function useResourceSubscription(guild: CombinedGuild, resourceId: string | null, resourceIdGuild: CombinedGuild | null) {
const fetchResourceFunc = useCallback(async () => {
// TODO: This function isn't working for the members list
//LOG.silly('fetching resource for subscription (resourceId: ' + resourceId + ')');
if (resourceId === null) return null;
if (resourceIdGuild === null) return null;
if (resourceIdGuild !== guild) return null;
return await guild.fetchResource(resourceId);
}, [ guild, resourceIdGuild, resourceId ]);
return useSingleGuildSubscription<Resource, 'update-resource', 'conflict-resource'>(guild, {
const fetchResource = await guild.fetchResource(resourceId);
return fetchResource;
}, [ guild, resourceIdGuild, resourceId ]); // Explicitly do NOT want lastFetchResource since it would cause a re-fetch after fetching successfully
return useSingleGuildSubscription<Resource | null, 'update-resource', 'conflict-resource'>(guild, {
updatedEventName: 'update-resource',
updatedEventArgsMap: (resource: Resource) => resource,
conflictEventName: 'conflict-resource',
conflictEventArgsMap: (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => newResource
conflictEventArgsMap: (_query: IDQuery, _changesType: AutoVerifierChangesType, _oldResource: Resource, newResource: Resource) => newResource
}, fetchResourceFunc);
}
export function useSoftImageSrcResourceSubscription(guild: CombinedGuild, resourceId: string | null, resourceIdGuild: CombinedGuild | null): [
imgSrc: string,
resource: Resource | null,
resourceGuild: CombinedGuild | null,
resourceResult: { value: Resource | null, guild: CombinedGuild } | null,
fetchError: unknown | null
] {
const [ resource, resourceGuild, fetchError ] = useResourceSubscription(guild, resourceId, resourceIdGuild);
const [ resourceResult, fetchError ] = useResourceSubscription(guild, resourceId, resourceIdGuild);
const [ imgSrc ] = useOneTimeAsyncAction(
async () => {
//LOG.debug(`Fetching soft imgSrc for g#${guild.id} r#${resource?.id ?? '<null>'}`, { fetchError });
if (fetchError) return './img/error.png';
if (!resource) return './img/loading.svg';
return await ElementsUtil.getImageSrcFromBufferFailSoftly(resource.data);
if (!resourceResult || !resourceResult.value) return './img/loading.svg';
return await ElementsUtil.getImageSrcFromBufferFailSoftly(resourceResult.value.data);
},
'./img/loading.svg',
[ resource, fetchError ]
[ resourceResult, fetchError ]
);
return [ imgSrc, resource, resourceGuild, fetchError ];
return [ imgSrc, resourceResult, fetchError ];
}
export function useChannelsSubscription(guild: CombinedGuild) {
@ -683,7 +635,7 @@ export function useChannelsSubscription(guild: CombinedGuild) {
removedEventName: 'remove-channels',
removedEventArgsMap: (removedChannels: Channel[]) => removedChannels,
conflictEventName: 'conflict-channels',
conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes<Channel>) => changes,
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes<Channel>) => changes,
sortFunc: Channel.sortByIndex
}, fetchChannelsFunc);
}
@ -700,19 +652,22 @@ export function useMembersSubscription(guild: CombinedGuild) {
removedEventName: 'remove-members',
removedEventArgsMap: (removedMembers: Member[]) => removedMembers,
conflictEventName: 'conflict-members',
conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes<Member>) => changes,
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes<Member>) => changes,
sortFunc: Member.sortForList
}, fetchMembersFunc);
}
export function useSelfMemberSubscription(guild: CombinedGuild): [ selfMember: Member | null, selfMemberGuild: CombinedGuild | null ] {
const [ fetchRetryCallable, members, membersGuild, fetchError ] = useMembersSubscription(guild);
export function useSelfMemberSubscription(guild: CombinedGuild): [
selfMemberResult: { value: Member | null
guild: CombinedGuild } | null
] {
const [ _fetchRetryCallable, membersResult, _fetchError ] = useMembersSubscription(guild);
// TODO: Show an error if we can't fetch and allow retry
const selfMember = useMemo(() => {
if (members) {
const member = members.find(m => m.id === guild.memberId);
if (membersResult && membersResult.value) {
const member = membersResult.value.find(m => m.id === guild.memberId);
if (!member) {
LOG.warn('Unable to find self in members');
return null;
@ -720,9 +675,9 @@ export function useSelfMemberSubscription(guild: CombinedGuild): [ selfMember: M
return member;
}
return null;
}, [ guild.memberId, members ]);
}, [ guild.memberId, membersResult ]);
return [ selfMember, membersGuild ];
return [ membersResult ? { value: selfMember, guild: membersResult.guild } : null ];
}
export function useTokensSubscription(guild: CombinedGuild) {
@ -738,7 +693,7 @@ export function useTokensSubscription(guild: CombinedGuild) {
removedEventName: 'remove-tokens',
removedEventArgsMap: (removedTokens: Token[]) => removedTokens,
conflictEventName: 'conflict-tokens',
conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes<Token>) => changes,
conflictEventArgsMap: (_changesType: AutoVerifierChangesType, changes: Changes<Token>) => changes,
sortFunc: Token.sortRecentCreatedFirst
}, fetchTokensFunc);
}
@ -770,10 +725,11 @@ export function useMessagesScrollingSubscription(guild: CombinedGuild, channel:
removedEventName: 'remove-messages',
removedEventArgsMap: (removedMessages) => removedMessages,
conflictEventName: 'conflict-messages',
conflictEventArgsMap: (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<Message>) => changes,
conflictEventArgsMap: (_query: PartialMessageListQuery, _changesType: AutoVerifierChangesType, changes: Changes<Message>) => changes,
sortFunc: Message.sortOrder
},
maxElements, maxFetchElements,
fetchMessagesFunc, fetchAboveFunc, fetchBelowFunc
)
}

View File

@ -1,29 +1,24 @@
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 React, { Dispatch, FC, ReactNode, SetStateAction, useMemo, useRef } from 'react';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import MemberElement, { DummyMember } from '../lists/components/member-element';
import ConnectionInfoContextMenu from '../contexts/context-menu-connection-info';
import { useContextMenu } from '../require/react-helper';
import { isNonNullAndHasValue, SubscriptionResult } from '../require/guild-subscriptions';
export interface ConnectionInfoProps {
guild: CombinedGuild;
selfMember: Member | null;
selfMemberGuild: CombinedGuild | null;
selfMemberResult: SubscriptionResult<Member | null> | null;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const ConnectionInfo: FC<ConnectionInfoProps> = (props: ConnectionInfoProps) => {
const { guild, selfMember, selfMemberGuild, setOverlay } = props;
const { guild, selfMemberResult, setOverlay } = props;
const rootRef = useRef<HTMLDivElement>(null);
const displayMember = useMemo((): Member | DummyMember => {
if (!selfMember) {
if (!isNonNullAndHasValue(selfMemberResult)) {
return {
id: 'dummy',
displayName: 'Connecting...',
@ -32,25 +27,26 @@ const ConnectionInfo: FC<ConnectionInfoProps> = (props: ConnectionInfoProps) =>
avatarResourceId: null
};
}
return selfMember;
}, [ selfMember ]);
return selfMemberResult.value;
}, [ selfMemberResult ]);
const [ contextMenu, toggleContextMenu ] = useContextMenu((close: () => void) => {
if (!selfMember || !selfMemberGuild) return null;
if (!isNonNullAndHasValue(selfMemberResult)) return null;
return (
<ConnectionInfoContextMenu
guild={guild} selfMember={selfMember} selfMemberGuild={selfMemberGuild} relativeToRef={rootRef}
guild={guild} selfMemberResult={selfMemberResult} relativeToRef={rootRef}
close={close} setOverlay={setOverlay}
/>
);
}, [ guild, selfMember, rootRef ]);
}, [ guild, selfMemberResult, rootRef ]);
return (
<div ref={rootRef} className="connection-info">
<div onClick={toggleContextMenu}><MemberElement guild={guild} member={displayMember} memberGuild={selfMemberGuild} /></div>
<div onClick={toggleContextMenu}><MemberElement guild={guild} member={displayMember} memberGuild={selfMemberResult?.guild ?? null} /></div>
{contextMenu}
</div>
);
}
export default ConnectionInfo;

View File

@ -2,43 +2,41 @@ import React, { Dispatch, FC, ReactNode, SetStateAction, useMemo, useRef } from
import { GuildMetadata, Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import GuildTitleContextMenu from '../contexts/context-menu-guild-title';
import { isNonNullAndHasValue, SubscriptionResult } from '../require/guild-subscriptions';
import { useContextMenu } from '../require/react-helper';
export interface GuildTitleProps {
guild: CombinedGuild;
guildMeta: GuildMetadata | null;
guildMetaGuild: CombinedGuild | null;
selfMember: Member | null;
selfMemberGuild: CombinedGuild | null;
guildMetaResult: SubscriptionResult<GuildMetadata | null> | null;
selfMemberResult: SubscriptionResult<Member | null> | null;
setOverlay: Dispatch<SetStateAction<ReactNode>>;
}
const GuildTitle: FC<GuildTitleProps> = (props: GuildTitleProps) => {
const { guild, guildMeta, guildMetaGuild, selfMember, selfMemberGuild, setOverlay } = props;
const { guild, guildMetaResult, selfMemberResult, setOverlay } = props;
const rootRef = useRef<HTMLDivElement>(null);
const hasContextMenu = useMemo(() => {
if (!isNonNullAndHasValue(selfMemberResult)) return false;
return (
selfMember &&
(
selfMember.privileges.includes('modify_profile') ||
selfMember.privileges.includes('modify_channels')
)
selfMemberResult.value.privileges.includes('modify_profile') ||
selfMemberResult.value.privileges.includes('modify_channels')
);
}, [ selfMember ]);
}, [ selfMemberResult ]);
const [ contextMenu, toggleContextMenu ] = useContextMenu((close: () => void) => {
if (!guildMeta || !guildMetaGuild) return null;
if (!selfMember || !selfMemberGuild) return null;
if (!isNonNullAndHasValue(guildMetaResult)) return null;
if (!isNonNullAndHasValue(selfMemberResult)) return null;
return (
<GuildTitleContextMenu
relativeToRef={rootRef} close={close}
guild={guild} guildMeta={guildMeta} guildMetaGuild={guildMetaGuild} selfMember={selfMember}
guild={guild} guildMetaResult={guildMetaResult} selfMember={selfMemberResult.value}
setOverlay={setOverlay}
/>
);
}, [ guild, guildMeta, guildMetaGuild, selfMember, selfMemberGuild, rootRef ]);
}, [ guild, guildMetaResult, selfMemberResult, rootRef ]);
const nameStyle = useMemo(() => {
if (hasContextMenu) {
@ -51,7 +49,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">{guildMeta?.name ?? null}</span>
<span className="guild-name">{guildMetaResult?.value?.name ?? null}</span>
</div>
{contextMenu}
</div>

View File

@ -28,10 +28,11 @@ const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
// TODO: React set hasMessagesAbove and hasMessagesBelow when re-verified?
// TODO: React jump messages to bottom when the current user sent a message
const [ selfMember, selfMemberGuild ] = useSelfMemberSubscription(guild);
const [ guildMeta, guildMetaGuild, guildMetaFetchError ] = useGuildMetadataSubscription(guild);
const [ membersRetry, members, membersGuild, membersFetchError ] = useMembersSubscription(guild);
const [ channelsRetry, channels, channelsGuild, channelsFetchError ] = useChannelsSubscription(guild);
const [ selfMemberResult ] = useSelfMemberSubscription(guild);
const [ guildMetaResult, guildMetaFetchError ] = useGuildMetadataSubscription(guild);
const [ membersRetry, membersResult, membersFetchError ] = useMembersSubscription(guild);
const [ channelsRetry, channelsResult, channelsFetchError ] = useChannelsSubscription(guild);
const [ activeChannel, setActiveChannel ] = useState<Channel | null>(null);
const [ activeChannelGuild, setActiveChannelGuild ] = useState<CombinedGuild | null>(null);
@ -42,31 +43,31 @@ const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
useEffect(() => {
if (activeChannel === null) {
// initial active channel is the first one in the list
if (channels && channelsGuild && channels.length > 0) {
setActiveChannel(channels[0] as Channel);
setActiveChannelGuild(channelsGuild);
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 (channels && channelsGuild && activeChannel) {
} else if (channelsResult && activeChannel) {
// in the active channel was updated
const newActiveChannel = channels.find(channel => channel.id === activeChannel.id) ?? null
const newActiveChannel = channelsResult.value.find(channel => channel.id === activeChannel.id) ?? null
setActiveChannel(newActiveChannel);
setActiveChannelGuild(channelsGuild);
setActiveChannelGuild(channelsResult.guild);
//LOG.debug('Active channel was updated...', { channel: newActiveChannel?.id, channelsGuild: channelsGuild?.id });
}
}, [ channels, channelsGuild, activeChannel ]);
}, [ channelsResult, activeChannel ]);
return (
<div className="guild-react">
<div className="guild-sidebar">
<GuildTitle guild={guild} selfMember={selfMember} selfMemberGuild={selfMemberGuild} guildMeta={guildMeta} guildMetaGuild={guildMetaGuild} setOverlay={setOverlay} />
<GuildTitle guild={guild} selfMemberResult={selfMemberResult} guildMetaResult={guildMetaResult} setOverlay={setOverlay} />
<ChannelList
guild={guild} selfMember={selfMember}
channels={channels} channelsFetchError={channelsFetchError}
guild={guild} selfMember={selfMemberResult?.value ?? null}
channels={channelsResult?.value ?? null} channelsFetchError={channelsFetchError}
activeChannel={activeChannel} setActiveChannel={setActiveChannel}
setOverlay={setOverlay}
/>
<ConnectionInfo guild={guild} selfMember={selfMember} selfMemberGuild={selfMemberGuild} setOverlay={setOverlay} />
<ConnectionInfo guild={guild} selfMemberResult={selfMemberResult} setOverlay={setOverlay} />
</div>
<div className="guild-channel">
<ChannelTitle channel={activeChannel} />
@ -76,7 +77,7 @@ const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
{activeChannel && activeChannelGuild && <SendMessage guild={guild} channel={activeChannel} />}
</div>
<div className="member-list-wrapper">
<MemberList guild={guild} members={members} membersGuild={membersGuild} membersFetchError={membersFetchError} />
<MemberList guild={guild} membersResult={membersResult} membersFetchError={membersFetchError} />
</div>
</div>
</div>