improve guild subscriptions by linking guild with the fetched result
This commit is contained in:
parent
e35187b877
commit
9a5e5d822e
@ -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';
|
||||
|
@ -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' }
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)} />);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user