recoilize resources

This commit is contained in:
Michael Peters 2022-02-04 00:40:28 -06:00
parent 0c1c900724
commit 6da480c36e
8 changed files with 57 additions and 40 deletions

View File

@ -5,9 +5,8 @@ import { Member, Token } from '../../data-types';
import CombinedGuild from '../../guild-combined'; import CombinedGuild from '../../guild-combined';
import Util from '../../util'; import Util from '../../util';
import { IAddGuildData } from '../overlays/overlay-add-guild'; import { IAddGuildData } from '../overlays/overlay-add-guild';
import { guildMetaState, isLoaded } from '../require/atoms-2'; import { guildMetaState, guildResourceSoftImgSrcState, isLoaded, useRecoilValueSoftImgSrc } from '../require/atoms-2';
import BaseElements from '../require/base-elements'; import BaseElements from '../require/base-elements';
import { useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
import { useAsyncVoidCallback, useDownloadButton, useOneTimeAsyncAction } from '../require/react-helper'; import { useAsyncVoidCallback, useDownloadButton, useOneTimeAsyncAction } from '../require/react-helper';
import Button, { ButtonColorType } from './button'; import Button, { ButtonColorType } from './button';
@ -29,7 +28,7 @@ const TokenRow: FC<TokenRowProps> = (props: TokenRowProps) => {
null, null,
[ guild ] [ guild ]
); );
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null, guild); const iconSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null }));
const [ revoke ] = useAsyncVoidCallback(async () => { const [ revoke ] = useAsyncVoidCallback(async () => {
await guild.requestDoRevokeToken(token.token); await guild.requestDoRevokeToken(token.token);

View File

@ -12,11 +12,11 @@ import { Duration } from 'moment';
import moment from 'moment'; import moment from 'moment';
import DropdownInput from '../components/input-dropdown'; import DropdownInput from '../components/input-dropdown';
import Button from '../components/button'; import Button from '../components/button';
import { useTokensSubscription, useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions'; import { useTokensSubscription } from '../require/guild-subscriptions';
import TokenRow from '../components/token-row'; import TokenRow from '../components/token-row';
import CombinedGuild from '../../guild-combined'; import CombinedGuild from '../../guild-combined';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { guildMetaState, isLoaded } from '../require/atoms-2'; import { guildMetaState, guildResourceSoftImgSrcState, isLoaded, useRecoilValueSoftImgSrc } from '../require/atoms-2';
export interface GuildInvitesDisplayProps { export interface GuildInvitesDisplayProps {
guild: CombinedGuild; guild: CombinedGuild;
@ -34,7 +34,7 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
const [ expiresFromNow, setExpiresFromNow ] = useState<Duration | null>(moment.duration(1, 'day')); const [ expiresFromNow, setExpiresFromNow ] = useState<Duration | null>(moment.duration(1, 'day'));
const [ expiresFromNowText, setExpiresFromNowText ] = useState<string>('1 day'); const [ expiresFromNowText, setExpiresFromNowText ] = useState<string>('1 day');
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null, guild); const iconSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null }));
useEffect(() => { useEffect(() => {
if (expiresFromNowText === 'never') { if (expiresFromNowText === 'never') {

View File

@ -3,10 +3,9 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import CombinedGuild from '../../../guild-combined'; import CombinedGuild from '../../../guild-combined';
import ContextMenu from '../../contexts/components/context-menu'; import ContextMenu from '../../contexts/components/context-menu';
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic'; import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
import { currGuildIdState, guildMetaState, guildSelfMemberState, guildsManagerState, isLoaded } from '../../require/atoms-2'; import { currGuildIdState, guildMetaState, guildResourceSoftImgSrcState, guildSelfMemberState, guildsManagerState, isLoaded, useRecoilValueSoftImgSrc } from '../../require/atoms-2';
import BaseElements from '../../require/base-elements'; import BaseElements from '../../require/base-elements';
import { IAlignment } from '../../require/elements-util'; import { IAlignment } from '../../require/elements-util';
import { useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions';
import { useContextClickContextMenu, useContextHover } from '../../require/react-helper'; import { useContextClickContextMenu, useContextHover } from '../../require/react-helper';
export interface GuildListElementProps { export interface GuildListElementProps {
@ -23,9 +22,7 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
const guildMeta = useRecoilValue(guildMetaState(guild.id)); const guildMeta = useRecoilValue(guildMetaState(guild.id));
const selfMember = useRecoilValue(guildSelfMemberState(guild.id)); const selfMember = useRecoilValue(guildSelfMemberState(guild.id));
// TODO: more state higher up const iconSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null }));
// TODO: handle metadata error
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null, guild);
const [ contextHover, mouseEnterCallable, mouseLeaveCallable ] = useContextHover(() => { const [ contextHover, mouseEnterCallable, mouseLeaveCallable ] = useContextHover(() => {
if (!isLoaded(guildMeta)) return null; // TODO: Loading message here? if (!isLoaded(guildMeta)) return null; // TODO: Loading message here?

View File

@ -1,7 +1,7 @@
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { Member } from '../../../data-types'; import { Member } from '../../../data-types';
import CombinedGuild from '../../../guild-combined'; import CombinedGuild from '../../../guild-combined';
import { useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions'; import { guildResourceSoftImgSrcState, useRecoilValueSoftImgSrc } from '../../require/atoms-2';
export interface DummyMember { export interface DummyMember {
id: 'dummy'; id: 'dummy';
@ -20,8 +20,7 @@ export interface MemberProps {
const MemberElement: FC<MemberProps> = (props: MemberProps) => { const MemberElement: FC<MemberProps> = (props: MemberProps) => {
const { guild, member } = props; const { guild, member } = props;
// TODO: This is a terrible line of code. Make sure to fix it whe we do resources in recoil const avatarSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: member.avatarResourceId }));
const [ avatarSrc ] = useSoftImageSrcResourceSubscription(guild, member.avatarResourceId, guild);
const nameStyle = useMemo(() => member.roleColor ? { color: member.roleColor } : {}, [ member.roleColor ]); const nameStyle = useMemo(() => member.roleColor ? { color: member.roleColor } : {}, [ member.roleColor ]);

View File

@ -1,13 +1,12 @@
import moment from 'moment'; import moment from 'moment';
import React, { FC, ReactNode, useCallback, useMemo } from 'react'; import React, { FC, ReactNode, useCallback, useMemo } from 'react';
import { useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Member, Message } from '../../../data-types'; import { Member, Message } from '../../../data-types';
import CombinedGuild from '../../../guild-combined'; import CombinedGuild from '../../../guild-combined';
import ImageContextMenu from '../../contexts/context-menu-image'; import ImageContextMenu from '../../contexts/context-menu-image';
import ImageOverlay from '../../overlays/overlay-image'; import ImageOverlay from '../../overlays/overlay-image';
import { overlayState } from '../../require/atoms-2'; import { guildResourceSoftImgSrcState, guildResourceState, isLoaded, overlayState, useRecoilValueSoftImgSrc } from '../../require/atoms-2';
import ElementsUtil, { IAlignment } from '../../require/elements-util'; import ElementsUtil, { IAlignment } from '../../require/elements-util';
import { useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions';
import { useContextClickContextMenu, useDownloadButton, useOneTimeAsyncAction } from '../../require/react-helper'; import { useContextClickContextMenu, useDownloadButton, useOneTimeAsyncAction } from '../../require/react-helper';
interface ResourceElementProps { interface ResourceElementProps {
@ -48,7 +47,7 @@ interface PreviewImageElementProps {
resourcePreviewId: string; resourcePreviewId: string;
resourceId: string; resourceId: string;
resourceName: string; resourceName: string;
resourceIdGuild: CombinedGuild; resourceIdGuild: CombinedGuild; // TODO: Remove in favor of just one guild
} }
const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageElementProps) => { const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageElementProps) => {
@ -57,20 +56,21 @@ const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageEl
const setOverlay = useSetRecoilState<ReactNode>(overlayState); const setOverlay = useSetRecoilState<ReactNode>(overlayState);
// TODO: Handle resourceError // TODO: Handle resourceError
const [ previewImgSrc, previewResourceResult, previewResourceError ] = useSoftImageSrcResourceSubscription(guild, resourcePreviewId, resourceIdGuild); const previewResource = useRecoilValue(guildResourceState({ guildId: resourceIdGuild.id, resourceId: resourcePreviewId }));
const previewImgSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: resourceIdGuild.id, resourceId: resourcePreviewId }));
const [ contextMenu, onContextMenu ] = useContextClickContextMenu((alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => { const [ contextMenu, onContextMenu ] = useContextClickContextMenu((_alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => {
if (!previewResourceResult || !previewResourceResult.value) return null; if (!isLoaded(previewResource)) return null;
return ( return (
<ImageContextMenu <ImageContextMenu
relativeToPos={relativeToPos} close={close} relativeToPos={relativeToPos} close={close}
resourceName={resourceName} resourceBuff={previewResourceResult.value.data} isPreview={true} resourceName={resourceName} resourceBuff={previewResource.value.data} isPreview={true}
/> />
); );
}, [ previewResourceResult, resourceName ]); }, [ previewResource, resourceName ]);
const openImageOverlay = useCallback(() => { const openImageOverlay = useCallback(() => {
setOverlay(<ImageOverlay guild={guild} resourceId={resourceId} resourceName={resourceName} resourceIdGuild={resourceIdGuild} />); setOverlay(<ImageOverlay guild={resourceIdGuild} resourceId={resourceId} resourceName={resourceName} />);
}, [ guild, resourceId, resourceName ]); }, [ guild, resourceId, resourceName ]);
return ( return (

View File

@ -10,36 +10,40 @@ import DownloadButton from '../components/button-download';
import { useContextClickContextMenu } from '../require/react-helper'; import { useContextClickContextMenu } from '../require/react-helper';
import ImageContextMenu from '../contexts/context-menu-image'; import ImageContextMenu from '../contexts/context-menu-image';
import Overlay from '../components/overlay'; import Overlay from '../components/overlay';
import { useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions'; import { useRecoilValue } from 'recoil';
import { guildResourceSoftImgSrcState, guildResourceState, isFailed, isLoaded, useRecoilValueSoftImgSrc } from '../require/atoms-2';
export interface ImageOverlayProps { export interface ImageOverlayProps {
guild: CombinedGuild; guild: CombinedGuild;
resourceId: string; resourceId: string;
resourceName: string; resourceName: string;
resourceIdGuild: CombinedGuild;
} }
const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => { const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
const { guild, resourceId, resourceName, resourceIdGuild } = props; const { guild, resourceId, resourceName } = props;
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const [ imgSrc, resourceResult, resourceError ] = useSoftImageSrcResourceSubscription(guild, resourceId, resourceIdGuild); // NOTE: These will update together. How convenient!
const resource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId }));
const imgSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId }))
const [ contextMenu, onContextMenu ] = useContextClickContextMenu((alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => { // NOTE: Alignment is based on the image element, not the default context-click alignment because of ImageContextMenu
if (!resourceResult || !resourceResult.value) return null; const [ contextMenu, onContextMenu ] = useContextClickContextMenu((_alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => {
if (!isLoaded(resource)) return null;
return ( return (
<ImageContextMenu <ImageContextMenu
relativeToPos={relativeToPos} close={close} relativeToPos={relativeToPos} close={close}
resourceName={resourceName} resourceBuff={resourceResult.value.data} isPreview={false} resourceName={resourceName} resourceBuff={resource.value.data} isPreview={false}
/> />
); );
}, [ resourceResult, resourceName ]); }, [ resource, resourceName ]);
const sizeText = useMemo( const sizeText = useMemo(() => {
() => resourceResult?.value ? ElementsUtil.humanSize(resourceResult.value.data.length) : 'Loading Size...', if (isFailed(resource)) return 'Failed to load';
[ resourceResult ] if (!isLoaded(resource)) return 'Loading size...';
); return ElementsUtil.humanSize(resource.value.data.length);
}, [ resource ]);
return ( return (
<Overlay childRootRef={rootRef}> <Overlay childRootRef={rootRef}>
@ -52,7 +56,7 @@ const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
</div> </div>
{/* TODO: I think this may break if the download button gets clicked before the resource is loaded... */} {/* TODO: I think this may break if the download button gets clicked before the resource is loaded... */}
<DownloadButton <DownloadButton
downloadBuff={resourceResult?.value?.data} downloadBuffErr={!!resourceError} downloadBuff={isLoaded(resource) ? resource.value.data : undefined} downloadBuffErr={isFailed(resource)}
resourceName={resourceName}></DownloadButton> resourceName={resourceName}></DownloadButton>
</div> </div>
{contextMenu} {contextMenu}

View File

@ -4,7 +4,7 @@ import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole); const LOG = Logger.create(__filename, electronConsole);
import { ReactNode, useEffect } from "react"; import { ReactNode, useEffect } from "react";
import { atom, atomFamily, GetRecoilValue, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useSetRecoilState } from "recoil"; import { atom, atomFamily, GetRecoilValue, Loadable, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useRecoilValueLoadable, useSetRecoilState } from "recoil";
import { Changes, Channel, GuildMetadata, Member, Resource, ShouldNeverHappenError } from "../../data-types"; import { Changes, Channel, GuildMetadata, Member, Resource, ShouldNeverHappenError } from "../../data-types";
import CombinedGuild from "../../guild-combined"; import CombinedGuild from "../../guild-combined";
import GuildsManager from "../../guilds-manager"; import GuildsManager from "../../guilds-manager";
@ -641,6 +641,24 @@ export const currGuildActiveChannelState = selector<LoadableValue<Channel>>({
dangerouslyAllowMutability: true dangerouslyAllowMutability: true
}); });
// Helper functions for using softImgSrc states
function useLoadedOrElse<T>(loadable: Loadable<T>, ifLoading: T, ifError: T) {
if (loadable.state === 'hasValue') {
return loadable.contents;
} else if (loadable.state === 'loading') {
return ifLoading;
} else {
return ifError;
}
}
function useRecoilValueLoadableOrElse<T>(recoilValue: RecoilValue<T>, ifLoading: T, ifError: T): T {
const loadableValue = useRecoilValueLoadable(recoilValue);
return useLoadedOrElse(loadableValue, ifLoading, ifError);
}
export function useRecoilValueSoftImgSrc(recoilValue: RecoilValue<string>): string {
return useRecoilValueLoadableOrElse(recoilValue, './img/loading.svg', './img/error.png');
}
// Initialize with a guildsManager // Initialize with a guildsManager
export function initRecoil(guildsManager: GuildsManager) { export function initRecoil(guildsManager: GuildsManager) {
const setGuildsManager = useSetRecoilState(guildsManagerState); const setGuildsManager = useSetRecoilState(guildsManagerState);

View File

@ -760,7 +760,7 @@ function useResourceSubscription(guild: CombinedGuild, resourceId: string | null
* fetchError: Any error from fetching * fetchError: Any error from fetching
* ] * ]
*/ */
export function useSoftImageSrcResourceSubscription(guild: CombinedGuild, resourceId: string | null, resourceIdGuild: CombinedGuild | null): [ function useSoftImageSrcResourceSubscription(guild: CombinedGuild, resourceId: string | null, resourceIdGuild: CombinedGuild | null): [
imgSrc: string, imgSrc: string,
resourceResult: SubscriptionResult<Resource> | null, resourceResult: SubscriptionResult<Resource> | null,
fetchError: unknown | null fetchError: unknown | null