From 6da480c36edc8b687b850d507cff7986fd0dd32a Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Fri, 4 Feb 2022 00:40:28 -0600 Subject: [PATCH] recoilize resources --- .../webapp/elements/components/token-row.tsx | 5 ++- .../displays/display-guild-invites.tsx | 6 ++-- .../lists/components/guild-list-element.tsx | 7 ++-- .../lists/components/member-element.tsx | 5 ++- .../lists/components/message-element.tsx | 20 ++++++------ .../elements/overlays/overlay-image.tsx | 32 +++++++++++-------- src/client/webapp/elements/require/atoms-2.ts | 20 +++++++++++- .../elements/require/guild-subscriptions.ts | 2 +- 8 files changed, 57 insertions(+), 40 deletions(-) diff --git a/src/client/webapp/elements/components/token-row.tsx b/src/client/webapp/elements/components/token-row.tsx index bd4203c..79513c5 100644 --- a/src/client/webapp/elements/components/token-row.tsx +++ b/src/client/webapp/elements/components/token-row.tsx @@ -5,9 +5,8 @@ import { Member, Token } from '../../data-types'; import CombinedGuild from '../../guild-combined'; import Util from '../../util'; 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 { useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions'; import { useAsyncVoidCallback, useDownloadButton, useOneTimeAsyncAction } from '../require/react-helper'; import Button, { ButtonColorType } from './button'; @@ -29,7 +28,7 @@ const TokenRow: FC = (props: TokenRowProps) => { null, [ 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 () => { await guild.requestDoRevokeToken(token.token); diff --git a/src/client/webapp/elements/displays/display-guild-invites.tsx b/src/client/webapp/elements/displays/display-guild-invites.tsx index d317a6d..8ea1c76 100644 --- a/src/client/webapp/elements/displays/display-guild-invites.tsx +++ b/src/client/webapp/elements/displays/display-guild-invites.tsx @@ -12,11 +12,11 @@ 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 } from '../require/guild-subscriptions'; import TokenRow from '../components/token-row'; import CombinedGuild from '../../guild-combined'; import { useRecoilValue } from 'recoil'; -import { guildMetaState, isLoaded } from '../require/atoms-2'; +import { guildMetaState, guildResourceSoftImgSrcState, isLoaded, useRecoilValueSoftImgSrc } from '../require/atoms-2'; export interface GuildInvitesDisplayProps { guild: CombinedGuild; @@ -34,7 +34,7 @@ const GuildInvitesDisplay: FC = (props: GuildInvitesDi const [ expiresFromNow, setExpiresFromNow ] = useState(moment.duration(1, 'day')); const [ expiresFromNowText, setExpiresFromNowText ] = useState('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(() => { if (expiresFromNowText === 'never') { diff --git a/src/client/webapp/elements/lists/components/guild-list-element.tsx b/src/client/webapp/elements/lists/components/guild-list-element.tsx index ee5cda8..40720e7 100644 --- a/src/client/webapp/elements/lists/components/guild-list-element.tsx +++ b/src/client/webapp/elements/lists/components/guild-list-element.tsx @@ -3,10 +3,9 @@ import { useRecoilState, useRecoilValue } from 'recoil'; import CombinedGuild from '../../../guild-combined'; import ContextMenu from '../../contexts/components/context-menu'; import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic'; -import { 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 { IAlignment } from '../../require/elements-util'; -import { useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions'; import { useContextClickContextMenu, useContextHover } from '../../require/react-helper'; export interface GuildListElementProps { @@ -23,9 +22,7 @@ const GuildListElement: FC = (props: GuildListElementProp const guildMeta = useRecoilValue(guildMetaState(guild.id)); const selfMember = useRecoilValue(guildSelfMemberState(guild.id)); - // TODO: more state higher up - // TODO: handle metadata error - 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 [ contextHover, mouseEnterCallable, mouseLeaveCallable ] = useContextHover(() => { if (!isLoaded(guildMeta)) return null; // TODO: Loading message here? diff --git a/src/client/webapp/elements/lists/components/member-element.tsx b/src/client/webapp/elements/lists/components/member-element.tsx index f033a6b..457d074 100644 --- a/src/client/webapp/elements/lists/components/member-element.tsx +++ b/src/client/webapp/elements/lists/components/member-element.tsx @@ -1,7 +1,7 @@ import React, { FC, useMemo } from 'react'; import { Member } from '../../../data-types'; import CombinedGuild from '../../../guild-combined'; -import { useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions'; +import { guildResourceSoftImgSrcState, useRecoilValueSoftImgSrc } from '../../require/atoms-2'; export interface DummyMember { id: 'dummy'; @@ -20,8 +20,7 @@ export interface MemberProps { const MemberElement: FC = (props: MemberProps) => { 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 ] = useSoftImageSrcResourceSubscription(guild, member.avatarResourceId, guild); + const avatarSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: member.avatarResourceId })); const nameStyle = useMemo(() => member.roleColor ? { color: member.roleColor } : {}, [ member.roleColor ]); diff --git a/src/client/webapp/elements/lists/components/message-element.tsx b/src/client/webapp/elements/lists/components/message-element.tsx index 1cf1619..7b28579 100644 --- a/src/client/webapp/elements/lists/components/message-element.tsx +++ b/src/client/webapp/elements/lists/components/message-element.tsx @@ -1,13 +1,12 @@ import moment from 'moment'; import React, { FC, ReactNode, useCallback, useMemo } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Member, Message } from '../../../data-types'; import CombinedGuild from '../../../guild-combined'; import ImageContextMenu from '../../contexts/context-menu-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 { useSoftImageSrcResourceSubscription } from '../../require/guild-subscriptions'; import { useContextClickContextMenu, useDownloadButton, useOneTimeAsyncAction } from '../../require/react-helper'; interface ResourceElementProps { @@ -48,7 +47,7 @@ interface PreviewImageElementProps { resourcePreviewId: string; resourceId: string; resourceName: string; - resourceIdGuild: CombinedGuild; + resourceIdGuild: CombinedGuild; // TODO: Remove in favor of just one guild } const PreviewImageElement: FC = (props: PreviewImageElementProps) => { @@ -57,20 +56,21 @@ const PreviewImageElement: FC = (props: PreviewImageEl const setOverlay = useSetRecoilState(overlayState); // 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) => { - if (!previewResourceResult || !previewResourceResult.value) return null; + const [ contextMenu, onContextMenu ] = useContextClickContextMenu((_alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => { + if (!isLoaded(previewResource)) return null; return ( ); - }, [ previewResourceResult, resourceName ]); + }, [ previewResource, resourceName ]); const openImageOverlay = useCallback(() => { - setOverlay(); + setOverlay(); }, [ guild, resourceId, resourceName ]); return ( diff --git a/src/client/webapp/elements/overlays/overlay-image.tsx b/src/client/webapp/elements/overlays/overlay-image.tsx index 7fea2a5..2669ee5 100644 --- a/src/client/webapp/elements/overlays/overlay-image.tsx +++ b/src/client/webapp/elements/overlays/overlay-image.tsx @@ -10,36 +10,40 @@ import DownloadButton from '../components/button-download'; import { useContextClickContextMenu } from '../require/react-helper'; import ImageContextMenu from '../contexts/context-menu-image'; 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 { guild: CombinedGuild; resourceId: string; resourceName: string; - resourceIdGuild: CombinedGuild; } const ImageOverlay: FC = (props: ImageOverlayProps) => { - const { guild, resourceId, resourceName, resourceIdGuild } = props; + const { guild, resourceId, resourceName } = props; const rootRef = useRef(null); - const [ imgSrc, resourceResult, resourceError ] = useSoftImageSrcResourceSubscription(guild, resourceId, resourceIdGuild); - - const [ contextMenu, onContextMenu ] = useContextClickContextMenu((alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => { - if (!resourceResult || !resourceResult.value) return null; + // NOTE: These will update together. How convenient! + const resource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId })); + const imgSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId })) + + // NOTE: Alignment is based on the image element, not the default context-click alignment because of ImageContextMenu + const [ contextMenu, onContextMenu ] = useContextClickContextMenu((_alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => { + if (!isLoaded(resource)) return null; return ( ); - }, [ resourceResult, resourceName ]); + }, [ resource, resourceName ]); - const sizeText = useMemo( - () => resourceResult?.value ? ElementsUtil.humanSize(resourceResult.value.data.length) : 'Loading Size...', - [ resourceResult ] - ); + const sizeText = useMemo(() => { + if (isFailed(resource)) return 'Failed to load'; + if (!isLoaded(resource)) return 'Loading size...'; + return ElementsUtil.humanSize(resource.value.data.length); + }, [ resource ]); return ( @@ -52,7 +56,7 @@ const ImageOverlay: FC = (props: ImageOverlayProps) => { {/* TODO: I think this may break if the download button gets clicked before the resource is loaded... */} {contextMenu} diff --git a/src/client/webapp/elements/require/atoms-2.ts b/src/client/webapp/elements/require/atoms-2.ts index 3b9967b..0bc6cda 100644 --- a/src/client/webapp/elements/require/atoms-2.ts +++ b/src/client/webapp/elements/require/atoms-2.ts @@ -4,7 +4,7 @@ import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); 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 CombinedGuild from "../../guild-combined"; import GuildsManager from "../../guilds-manager"; @@ -641,6 +641,24 @@ export const currGuildActiveChannelState = selector>({ dangerouslyAllowMutability: true }); +// Helper functions for using softImgSrc states +function useLoadedOrElse(loadable: Loadable, ifLoading: T, ifError: T) { + if (loadable.state === 'hasValue') { + return loadable.contents; + } else if (loadable.state === 'loading') { + return ifLoading; + } else { + return ifError; + } +} +function useRecoilValueLoadableOrElse(recoilValue: RecoilValue, ifLoading: T, ifError: T): T { + const loadableValue = useRecoilValueLoadable(recoilValue); + return useLoadedOrElse(loadableValue, ifLoading, ifError); +} +export function useRecoilValueSoftImgSrc(recoilValue: RecoilValue): string { + return useRecoilValueLoadableOrElse(recoilValue, './img/loading.svg', './img/error.png'); +} + // Initialize with a guildsManager export function initRecoil(guildsManager: GuildsManager) { const setGuildsManager = useSetRecoilState(guildsManagerState); diff --git a/src/client/webapp/elements/require/guild-subscriptions.ts b/src/client/webapp/elements/require/guild-subscriptions.ts index 3ba5287..afe631c 100644 --- a/src/client/webapp/elements/require/guild-subscriptions.ts +++ b/src/client/webapp/elements/require/guild-subscriptions.ts @@ -760,7 +760,7 @@ function useResourceSubscription(guild: CombinedGuild, resourceId: string | null * 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, resourceResult: SubscriptionResult | null, fetchError: unknown | null