forked from OpenNeo/impress
170 lines
6 KiB
JavaScript
170 lines
6 KiB
JavaScript
import React from "react";
|
|
import { useToast } from "@chakra-ui/react";
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
import { useDebounce } from "../util";
|
|
import useCurrentUser from "../components/useCurrentUser";
|
|
import { outfitStatesAreEqual } from "./useOutfitState";
|
|
import { useSaveOutfitMutation } from "../loaders/outfits";
|
|
|
|
function useOutfitSaving(outfitState, dispatchToOutfit) {
|
|
const { isLoggedIn, id: currentUserId } = useCurrentUser();
|
|
const { pathname } = useLocation();
|
|
const navigate = useNavigate();
|
|
const toast = useToast();
|
|
|
|
// There's not a way to reset an Apollo mutation state to clear out the error
|
|
// when the outfit changes… so we track the error state ourselves!
|
|
const [saveError, setSaveError] = React.useState(null);
|
|
|
|
// Whether this outfit is new, i.e. local-only, i.e. has _never_ been saved
|
|
// to the server.
|
|
const isNewOutfit = outfitState.id == null;
|
|
|
|
// Whether this outfit's latest local changes have been saved to the server.
|
|
// And log it to the console!
|
|
const latestVersionIsSaved =
|
|
outfitState.savedOutfitState &&
|
|
outfitStatesAreEqual(
|
|
outfitState.outfitStateWithoutExtras,
|
|
outfitState.savedOutfitState,
|
|
);
|
|
React.useEffect(() => {
|
|
console.debug(
|
|
"[useOutfitSaving] Latest version is saved? %s\nCurrent: %o\nSaved: %o",
|
|
latestVersionIsSaved,
|
|
outfitState.outfitStateWithoutExtras,
|
|
outfitState.savedOutfitState,
|
|
);
|
|
}, [
|
|
latestVersionIsSaved,
|
|
outfitState.outfitStateWithoutExtras,
|
|
outfitState.savedOutfitState,
|
|
]);
|
|
|
|
// Only logged-in users can save outfits - and they can only save new outfits,
|
|
// or outfits they created.
|
|
const canSaveOutfit =
|
|
isLoggedIn && (isNewOutfit || outfitState.creator?.id === currentUserId);
|
|
|
|
// Users can delete their own outfits too. The logic is slightly different
|
|
// than for saving, because you can save an outfit that hasn't been saved
|
|
// yet, but you can't delete it.
|
|
const canDeleteOutfit = !isNewOutfit && canSaveOutfit;
|
|
|
|
const saveOutfitMutation = useSaveOutfitMutation({
|
|
onSuccess: (outfit) => {
|
|
if (outfit.id === outfitState.id && outfit.name !== outfitState.name) {
|
|
dispatchToOutfit({
|
|
type: "rename",
|
|
outfitName: outfit.name,
|
|
});
|
|
}
|
|
},
|
|
});
|
|
const isSaving = saveOutfitMutation.isPending;
|
|
|
|
const saveOutfitFromProvidedState = React.useCallback(
|
|
(outfitState) => {
|
|
saveOutfitMutation
|
|
.mutateAsync({
|
|
id: outfitState.id,
|
|
name: outfitState.name,
|
|
speciesId: outfitState.speciesId,
|
|
colorId: outfitState.colorId,
|
|
pose: outfitState.pose,
|
|
wornItemIds: [...outfitState.wornItemIds],
|
|
closetedItemIds: [...outfitState.closetedItemIds],
|
|
})
|
|
.then((outfit) => {
|
|
// Navigate to the new saved outfit URL. Our Apollo cache should pick
|
|
// up the data from this mutation response, and combine it with the
|
|
// existing cached data, to make this smooth without any loading UI.
|
|
if (pathname !== `/outfits/[outfitId]`) {
|
|
navigate(`/outfits/${outfit.id}`);
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
console.error(e);
|
|
setSaveError(e);
|
|
toast({
|
|
status: "error",
|
|
title: "Sorry, there was an error saving this outfit!",
|
|
description: "Maybe check your connection and try again.",
|
|
});
|
|
});
|
|
},
|
|
// It's important that this callback _doesn't_ change when the outfit
|
|
// changes, so that the auto-save effect is only responding to the
|
|
// debounced state!
|
|
[saveOutfitMutation.mutateAsync, pathname, navigate, toast],
|
|
);
|
|
|
|
const saveOutfit = React.useCallback(
|
|
() => saveOutfitFromProvidedState(outfitState.outfitStateWithoutExtras),
|
|
[saveOutfitFromProvidedState, outfitState.outfitStateWithoutExtras],
|
|
);
|
|
|
|
// Auto-saving! First, debounce the outfit state. Use `outfitStateWithoutExtras`,
|
|
// which only contains the basic fields, and will keep a stable object
|
|
// identity until actual changes occur. Then, save the outfit after the user
|
|
// has left it alone for long enough, so long as it's actually different
|
|
// than the saved state.
|
|
const debouncedOutfitState = useDebounce(
|
|
outfitState.outfitStateWithoutExtras,
|
|
2000,
|
|
{
|
|
// When the outfit ID changes, update the debounced state immediately!
|
|
forceReset: (debouncedOutfitState, newOutfitState) =>
|
|
debouncedOutfitState.id !== newOutfitState.id,
|
|
},
|
|
);
|
|
// HACK: This prevents us from auto-saving the outfit state that's still
|
|
// loading. I worry that this might not catch other loading scenarios
|
|
// though, like if the species/color/pose is in the GQL cache, but the
|
|
// items are still loading in... not sure where this would happen tho!
|
|
const debouncedOutfitStateIsSaveable =
|
|
debouncedOutfitState.speciesId &&
|
|
debouncedOutfitState.colorId &&
|
|
debouncedOutfitState.pose;
|
|
React.useEffect(() => {
|
|
if (
|
|
!isNewOutfit &&
|
|
canSaveOutfit &&
|
|
debouncedOutfitStateIsSaveable &&
|
|
!outfitStatesAreEqual(debouncedOutfitState, outfitState.savedOutfitState)
|
|
) {
|
|
console.info(
|
|
"[useOutfitSaving] Auto-saving outfit\nSaved: %o\nCurrent (debounced): %o",
|
|
outfitState.savedOutfitState,
|
|
debouncedOutfitState,
|
|
);
|
|
saveOutfitFromProvidedState(debouncedOutfitState);
|
|
}
|
|
}, [
|
|
isNewOutfit,
|
|
canSaveOutfit,
|
|
debouncedOutfitState,
|
|
debouncedOutfitStateIsSaveable,
|
|
outfitState.savedOutfitState,
|
|
saveOutfitFromProvidedState,
|
|
]);
|
|
|
|
// When the outfit changes, clear out the error state from previous saves.
|
|
// We'll send the mutation again after the debounce, and we don't want to
|
|
// show the error UI in the meantime!
|
|
React.useEffect(() => {
|
|
setSaveError(null);
|
|
}, [outfitState.outfitStateWithoutExtras]);
|
|
|
|
return {
|
|
canSaveOutfit,
|
|
canDeleteOutfit,
|
|
isNewOutfit,
|
|
isSaving,
|
|
latestVersionIsSaved,
|
|
saveError,
|
|
saveOutfit,
|
|
};
|
|
}
|
|
|
|
export default useOutfitSaving;
|