guildResourceState and preparing for soft img src state too
This commit is contained in:
parent
5b708f2a94
commit
0c1c900724
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user