guildResourceState and preparing for soft img src state too

This commit is contained in:
Michael Peters 2022-02-04 00:11:15 -06:00
parent 5b708f2a94
commit 0c1c900724
4 changed files with 84 additions and 34 deletions

View File

@ -9,9 +9,8 @@ import Globals from '../../globals';
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 CombinedGuild from '../../guild-combined';
import { guildMetaState, isLoaded } from '../require/atoms-2';
import { guildMetaState, guildResourceState, isFailed, isLoaded } from '../require/atoms-2';
import { useRecoilValue } from 'recoil';
interface GuildOverviewDisplayProps {
@ -22,8 +21,7 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
const guildMeta = useRecoilValue(guildMetaState(guild.id));
// TODO: Use the one from guild.tsx (for both of these?)
const [ iconResourceResult, iconResourceError ] = useResourceSubscription(guild, isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null, guild);
const iconResource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId: isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null }));
const [ savedName, setSavedName ] = useState<string | null>(null);
const [ savedIconBuff, setSavedIconBuff ] = useState<Buffer | null>(null);
@ -47,22 +45,22 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
}, [ guildMeta ]);
useEffect(() => {
if (iconResourceResult && iconResourceResult.value) {
if (iconBuff === savedIconBuff) setIconBuff(iconResourceResult.value.data);
setSavedIconBuff(iconResourceResult.value.data);
if (isLoaded(iconResource)) {
if (iconBuff === savedIconBuff) setIconBuff(iconResource.value.data);
setSavedIconBuff(iconResource.value.data);
}
}, [ iconResourceResult ]);
}, [ iconResource ]);
const changes = useMemo(() => {
return name !== savedName || iconBuff?.toString('hex') !== savedIconBuff?.toString('hex')
}, [ name, savedName, iconBuff, savedIconBuff ]);
const errorMessage = useMemo(() => {
if (iconResourceError) return 'Unable to load icon';
if (isFailed(iconResource)) return 'Unable to load icon';
if (!iconInputValid && iconInputMessage) return iconInputMessage;
if (!nameInputValid && nameInputMessage) return nameInputMessage;
return null;
}, [ iconResourceError, iconInputValid, iconInputMessage, nameInputValid, nameInputMessage ]);
}, [ iconResource, iconInputValid, iconInputMessage, nameInputValid, nameInputMessage ]);
const infoMessage = useMemo(() => {
if (iconInputValid && iconInputMessage) return iconInputMessage;

View File

@ -11,11 +11,10 @@ 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 { useSetRecoilState } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { overlayState } from '../require/atoms-2';
import { guildResourceState, isFailed, isLoaded, overlayState } from '../require/atoms-2';
interface PersonalizeOverlayProps {
guild: CombinedGuild;
@ -25,11 +24,10 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
const { guild, selfMember } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const avatarResource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId: selfMember.avatarResourceId }));
const rootRef = useRef<HTMLDivElement>(null);
const [ avatarResourceResult, avatarResourceError ] = useResourceSubscription(guild, selfMember.avatarResourceId, guild);
const displayNameInputRef = createRef<HTMLInputElement>();
const [ savedDisplayName, setSavedDisplayName ] = useState<string>(selfMember.displayName);
@ -45,22 +43,22 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
const [ avatarInputMessage, setAvatarInputMessage ] = useState<string | null>(null);
useEffect(() => {
if (avatarResourceResult && avatarResourceResult.value) {
if (avatarBuff === savedAvatarBuff) setAvatarBuff(avatarResourceResult.value.data);
setSavedAvatarBuff(avatarResourceResult.value.data);
if (isLoaded(avatarResource)) {
if (avatarBuff === savedAvatarBuff) setAvatarBuff(avatarResource.value.data);
setSavedAvatarBuff(avatarResource.value.data);
}
}, [ avatarResourceResult ]);
}, [ avatarResource ]);
useEffect(() => {
displayNameInputRef.current?.focus();
}, []);
const validationErrorMessage = useMemo(() => {
if (avatarResourceError) return 'Unable to load avatar';
if (isFailed(avatarResource)) return 'Unable to load avatar';
if ( !avatarInputValid && avatarInputMessage) return avatarInputMessage;
if (!displayNameInputValid && displayNameInputMessage) return displayNameInputMessage;
return null;
}, [ avatarResourceError, displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage ]);
}, [ avatarResource, displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage ]);
const infoMessage = useMemo(() => {
if (avatarInputValid && avatarInputMessage) return avatarInputMessage;

View File

@ -4,13 +4,14 @@ import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import { ReactNode, useEffect } from "react";
import { atom, atomFamily, GetCallback, GetRecoilValue, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useSetRecoilState } from "recoil";
import { Changes, Channel, GuildMetadata, Member, Resource } from "../../data-types";
import { atom, atomFamily, GetRecoilValue, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useSetRecoilState } from "recoil";
import { Changes, Channel, GuildMetadata, Member, Resource, ShouldNeverHappenError } from "../../data-types";
import CombinedGuild from "../../guild-combined";
import GuildsManager from "../../guilds-manager";
import { AutoVerifierChangesType } from '../../auto-verifier';
import { Conflictable, Connectable } from '../../guild-types';
import { IDQuery } from '../../auto-verifier-with-args';
import ElementsUtil from './elements-util';
// General typescript type that infers the arguments of a function
type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
@ -117,6 +118,7 @@ function createFetchValueFunc<T>(
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
): () => Promise<void> {
const fetchValueFunc = async () => {
// TODO: Look into using getCallback in case guild is null https://recoiljs.org/docs/api-reference/core/selector#returning-objects-with-callbacks
const guild = await getPromise(guildState(guildId));
if (guild === null) return; // Can't send a request without an associated guild
@ -336,9 +338,12 @@ function singleGuildSubscriptionEffect<
>(
guildId: number,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
eventMapping: SingleEventMappingParams<T, UE, CE>
eventMapping: SingleEventMappingParams<T, UE, CE>,
skipFunc?: () => boolean
) {
return (params: RecoilLoadableAtomEffectParams<T>) => {
if (skipFunc && skipFunc()) return; // Don't run if this atom should be skipped for some reason (e.g. null resourceId)
const { node, trigger, setSelf, getPromise } = params;
const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc);
@ -405,25 +410,44 @@ export const guildMetaState = atomFamily<LoadableValue<GuildMetadata>, number>({
dangerouslyAllowMutability: true
});
export const guildResourceState = atomFamily<LoadableValue<Resource>, { guildId: number, resourceId: string }>({
key: 'guildResourceState',
// Note: this is also useful for resources that may have a null/unloaded resourceId
// Note: this function has somewhat dirty types
export const guildResourceState = atomFamily<LoadableValue<Resource>, { guildId: number, resourceId: string | null }>({
key: 'guildPotentialResourceState',
default: DEF_UNLOADED_VALUE,
effects_UNSTABLE: (param: { guildId: number, resourceId: string }) => [
effects_UNSTABLE: (param: { guildId: number, resourceId: string | null }) => [
singleGuildSubscriptionEffect(
param.guildId,
async (guild: CombinedGuild) => await guild.fetchResource(param.resourceId),
async (guild: CombinedGuild) => {
if (param.resourceId === null) { // Should never happen because of skipFunc (last argument)
throw new ShouldNeverHappenError('null resourceId');
} else {
return await guild.fetchResource(param.resourceId);
}
},
{
updatedEventName: 'update-resource',
updatedEventArgsMap: (updatedResource: Resource) => updatedResource,
updatedEventCondition: (resource: Resource) => resource.id === param.resourceId,
conflictEventName: 'conflict-resource',
conflictEventArgsMap: (_query: IDQuery, _changeType: AutoVerifierChangesType, _oldResource: Resource, newResource: Resource) => newResource,
conflictEventCondition: (resource: Resource) => resource.id === param.resourceId
}
conflictEventCondition: (resource: Resource) => resource.id === param.resourceId,
},
() => param.resourceId === null // Never load/bind if resourceId === null
)
],
dangerouslyAllowMutability: true
]
});
// NOTE: This is a Loadable Recoil selectorFamily so do useRecoilValueLoadable
export const guildResourceSoftImgSrcState = selectorFamily<string, { guildId: number, resourceId: string | null }>({
key: 'guildResourceSoftImgSrcState',
get: (param: { guildId: number, resourceId: string | null }) => async ({ get }) => {
const resource = get(guildResourceState(param));
if (isFailed(resource)) return './img/error.png'; // TODO: Use BaseElements
if (!isLoaded(resource)) return './img/loading.svg'; // TODO: Use BaseElements
const imgSrc = await ElementsUtil.getImageSrcFromBufferFailSoftly(resource.value.data);
return imgSrc;
}
})
const guildMembersState = atomFamily<LoadableValue<Member[]>, number>({
key: 'guildMembersState',
@ -543,8 +567,18 @@ function createCurrentGuildStateGetter<T>(subSelectorFamily: (guildId: number) =
return value;
}
}
function createCurrentGuildHardValueWithParamStateGetter<T, P>(subSelectorFamily: (param: P) => RecoilValueReadOnly<T> | RecoilState<T>, guildIdToParam: (guildId: number) => P, unloadedValue: T) {
return ({ get }: { get: GetRecoilValue }) => {
// Use the unloaded value if the current guild hasn't been selected yet or doesn't exist
const currGuildId = get(currGuildIdState);
if (currGuildId === null) return unloadedValue;
const value = get(subSelectorFamily(guildIdToParam(currGuildId)));
if (value === null) return unloadedValue;
return value;
}
}
function createCurrentGuildLoadableStateGetter<T>(subSelectorFamily: (guildId: number) => RecoilValueReadOnly<LoadableValue<T>> | RecoilState<LoadableValue<T>>) {
return ({ get }: { get: GetRecoilValue; getCallback: GetCallback }) => {
return ({ get }: { get: GetRecoilValue }) => {
// Use the unloaded value if the current guild hasn't been selected yet or doesn't exist
const currGuildId = get(currGuildIdState);
if (currGuildId === null) return DEF_UNLOADED_VALUE;
@ -553,6 +587,16 @@ function createCurrentGuildLoadableStateGetter<T>(subSelectorFamily: (guildId: n
return value;
}
}
function createCurrentGuildLoadableWithParamStateGetter<T, P>(subSelectorFamily: (param: P) => RecoilValueReadOnly<LoadableValue<T>> | RecoilState<LoadableValue<T>>, guildIdToParam: (guildId: number) => P) {
return ({ get }: { get: GetRecoilValue }) => {
// Use the unloaded value if the current guild hasn't been selected yet or doesn't exist
const currGuildId = get(currGuildIdState);
if (currGuildId === null) return DEF_UNLOADED_VALUE;
const value = get(subSelectorFamily(guildIdToParam(currGuildId)));
if (value === null) return DEF_UNLOADED_VALUE;
return value;
}
}
// Note: These will all update in parallel when the guild changes. They will always reference the same guild.
// What's great about these is that they all change at once when the guild is changed so there are not extraneous renders!
@ -566,6 +610,16 @@ export const currGuildMetaState = selector<LoadableValue<GuildMetadata>>({
get: createCurrentGuildLoadableStateGetter(guildMetaState),
dangerouslyAllowMutability: true
});
export const currGuildResourceState = selectorFamily<LoadableValue<Resource>, string | null>({
key: 'currGuildResourceState',
get: (resourceId: string | null) => createCurrentGuildLoadableWithParamStateGetter(guildResourceState, (guildId: number) => ({ guildId, resourceId })),
dangerouslyAllowMutability: true
});
export const currGuildResourceSoftImgSrcState = selectorFamily<string, string | null>({
key: 'currGuildResourceSoftImgSrcState',
get: (resourceId: string | null) => createCurrentGuildHardValueWithParamStateGetter(guildResourceSoftImgSrcState, (guildId: number) => ({ guildId, resourceId }), './img/loading.svg'),
dangerouslyAllowMutability: true
})
export const currGuildMembersState = selector<LoadableValue<Member[]>>({
key: 'currGuildMembersState',
get: createCurrentGuildLoadableStateGetter(guildMembersState),

View File

@ -732,7 +732,7 @@ function useGuildMetadataSubscription(guild: CombinedGuild) {
* fetchError: Any error from fetching
* ]
*/
export function useResourceSubscription(guild: CombinedGuild, resourceId: string | null, resourceIdGuild: CombinedGuild | null) {
function useResourceSubscription(guild: CombinedGuild, resourceId: string | null, resourceIdGuild: CombinedGuild | null) {
const fetchResourceFunc = useCallback(async () => {
//LOG.silly('fetching resource for subscription (resourceId: ' + resourceId + ')');
// Note: Returning null skips the load. This will prevent a null resourceResult