2023-08-10 15:56:36 -07:00
|
|
|
import React from "react";
|
|
|
|
import { useToast } from "@chakra-ui/react";
|
2023-08-10 17:15:07 -07:00
|
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
2023-08-10 15:56:36 -07:00
|
|
|
import { useDebounce } from "../util";
|
|
|
|
import useCurrentUser from "../components/useCurrentUser";
|
|
|
|
import gql from "graphql-tag";
|
|
|
|
import { useMutation } from "@apollo/client";
|
|
|
|
import { outfitStatesAreEqual } from "./useOutfitState";
|
|
|
|
|
|
|
|
function useOutfitSaving(outfitState, dispatchToOutfit) {
|
|
|
|
const { isLoggedIn, id: currentUserId } = useCurrentUser();
|
2023-08-10 17:15:07 -07:00
|
|
|
const { pathname } = useLocation();
|
|
|
|
const navigate = useNavigate();
|
2023-08-10 15:56:36 -07:00
|
|
|
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,
|
2023-10-24 16:45:49 -07:00
|
|
|
outfitState.savedOutfitState,
|
2023-08-10 15:56:36 -07:00
|
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
|
|
console.debug(
|
|
|
|
"[useOutfitSaving] Latest version is saved? %s\nCurrent: %o\nSaved: %o",
|
|
|
|
latestVersionIsSaved,
|
|
|
|
outfitState.outfitStateWithoutExtras,
|
2023-10-24 16:45:49 -07:00
|
|
|
outfitState.savedOutfitState,
|
2023-08-10 15:56:36 -07:00
|
|
|
);
|
|
|
|
}, [
|
|
|
|
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 [sendSaveOutfitMutation, { loading: isSaving }] = useMutation(
|
|
|
|
gql`
|
|
|
|
mutation UseOutfitSaving_SaveOutfit(
|
|
|
|
$id: ID # Optional, is null when saving new outfits.
|
|
|
|
$name: String # Optional, server may fill in a placeholder.
|
|
|
|
$speciesId: ID!
|
|
|
|
$colorId: ID!
|
|
|
|
$pose: Pose!
|
|
|
|
$wornItemIds: [ID!]!
|
|
|
|
$closetedItemIds: [ID!]!
|
|
|
|
) {
|
|
|
|
outfit: saveOutfit(
|
|
|
|
id: $id
|
|
|
|
name: $name
|
|
|
|
speciesId: $speciesId
|
|
|
|
colorId: $colorId
|
|
|
|
pose: $pose
|
|
|
|
wornItemIds: $wornItemIds
|
|
|
|
closetedItemIds: $closetedItemIds
|
|
|
|
) {
|
|
|
|
id
|
|
|
|
name
|
|
|
|
petAppearance {
|
|
|
|
id
|
|
|
|
species {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
color {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
pose
|
|
|
|
}
|
|
|
|
wornItems {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
closetedItems {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
creator {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
{
|
|
|
|
context: { sendAuth: true },
|
|
|
|
update: (cache, { data: { outfit } }) => {
|
|
|
|
// After save, add this outfit to the current user's outfit list. This
|
|
|
|
// will help when navigating back to Your Outfits, to force a refresh.
|
|
|
|
// https://www.apollographql.com/docs/react/caching/cache-interaction/#example-updating-the-cache-after-a-mutation
|
|
|
|
cache.modify({
|
|
|
|
id: cache.identify(outfit.creator),
|
|
|
|
fields: {
|
|
|
|
outfits: (existingOutfitRefs = [], { readField }) => {
|
|
|
|
const isAlreadyInList = existingOutfitRefs.some(
|
2023-10-24 16:45:49 -07:00
|
|
|
(ref) => readField("id", ref) === outfit.id,
|
2023-08-10 15:56:36 -07:00
|
|
|
);
|
|
|
|
if (isAlreadyInList) {
|
|
|
|
return existingOutfitRefs;
|
|
|
|
}
|
|
|
|
|
|
|
|
const newOutfitRef = cache.writeFragment({
|
|
|
|
data: outfit,
|
|
|
|
fragment: gql`
|
|
|
|
fragment NewOutfit on Outfit {
|
|
|
|
id
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
});
|
|
|
|
|
|
|
|
return [...existingOutfitRefs, newOutfitRef];
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
// Also, send a `rename` action, if this is still the current outfit,
|
|
|
|
// and the server renamed it (e.g. "Untitled outfit (1)"). (It's
|
|
|
|
// tempting to do a full reset, in case the server knows something we
|
|
|
|
// don't, but we don't want to clobber changes the user made since
|
|
|
|
// starting the save!)
|
|
|
|
if (outfit.id === outfitState.id && outfit.name !== outfitState.name) {
|
|
|
|
dispatchToOutfit({
|
|
|
|
type: "rename",
|
|
|
|
outfitName: outfit.name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
2023-10-24 16:45:49 -07:00
|
|
|
},
|
2023-08-10 15:56:36 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
const saveOutfitFromProvidedState = React.useCallback(
|
|
|
|
(outfitState) => {
|
|
|
|
sendSaveOutfitMutation({
|
|
|
|
variables: {
|
|
|
|
id: outfitState.id,
|
|
|
|
name: outfitState.name,
|
|
|
|
speciesId: outfitState.speciesId,
|
|
|
|
colorId: outfitState.colorId,
|
|
|
|
pose: outfitState.pose,
|
|
|
|
wornItemIds: [...outfitState.wornItemIds],
|
|
|
|
closetedItemIds: [...outfitState.closetedItemIds],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.then(({ data: { 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]`) {
|
2023-08-10 17:15:07 -07:00
|
|
|
navigate(`/outfits/${outfit.id}`);
|
2023-08-10 15:56:36 -07:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.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!
|
2023-10-24 16:45:49 -07:00
|
|
|
[sendSaveOutfitMutation, pathname, navigate, toast],
|
2023-08-10 15:56:36 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
const saveOutfit = React.useCallback(
|
|
|
|
() => saveOutfitFromProvidedState(outfitState.outfitStateWithoutExtras),
|
2023-10-24 16:45:49 -07:00
|
|
|
[saveOutfitFromProvidedState, outfitState.outfitStateWithoutExtras],
|
2023-08-10 15:56:36 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
// 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,
|
2023-10-24 16:45:49 -07:00
|
|
|
},
|
2023-08-10 15:56:36 -07:00
|
|
|
);
|
|
|
|
// 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,
|
2023-10-24 16:45:49 -07:00
|
|
|
debouncedOutfitState,
|
2023-08-10 15:56:36 -07:00
|
|
|
);
|
|
|
|
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;
|