import React from "react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import { ClassNames } from "@emotion/react";
import {
Box,
Button,
Flex,
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
Portal,
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
VisuallyHidden,
useBreakpointValue,
useColorModeValue,
useTheme,
useToast,
useToken,
} from "@chakra-ui/react";
import { ChevronDownIcon, WarningTwoIcon } from "@chakra-ui/icons";
import { loadable } from "../util";
import { petAppearanceFragment } from "../components/useOutfitAppearance";
import getVisibleLayers from "../components/getVisibleLayers";
import { OutfitLayers } from "../components/OutfitPreview";
import SupportOnly from "./support/SupportOnly";
import { useAltStylesForSpecies } from "../loaders/alt-styles";
import { useLocalStorage } from "../util";
// From https://twemoji.twitter.com/, thank you!
import twemojiSmile from "../images/twemoji/smile.svg";
import twemojiCry from "../images/twemoji/cry.svg";
import twemojiSick from "../images/twemoji/sick.svg";
import twemojiSunglasses from "../images/twemoji/sunglasses.svg";
import twemojiQuestion from "../images/twemoji/question.svg";
import twemojiMasc from "../images/twemoji/masc.svg";
import twemojiFem from "../images/twemoji/fem.svg";
import twemojiHourglass from "../images/twemoji/hourglass.svg";
const PosePickerSupport = loadable(() => import("./support/PosePickerSupport"));
const PosePickerSupportSwitch = loadable(() =>
import("./support/PosePickerSupport").then((m) => m.PosePickerSupportSwitch),
);
/**
* PosePicker shows the pet poses available on the current species/color, and
* lets the user choose which want they want!
*
* NOTE: This component is memoized with React.memo. It's relatively expensive
* to re-render on every outfit change - the contents update even if the
* popover is closed! This makes wearing/unwearing items noticeably
* slower on lower-power devices.
*
* So, instead of using `outfitState` like most components, we specify
* exactly which props we need, so that `React.memo` can see the changes
* that matter, and skip updates that don't.
*/
function PosePicker({
speciesId,
colorId,
pose,
altStyleId,
appearanceId,
dispatchToOutfit,
onLockFocus,
onUnlockFocus,
...props
}) {
const initialFocusRef = React.useRef();
const posesQuery = usePoses(speciesId, colorId, pose);
const altStylesQuery = useAltStylesForSpecies(speciesId);
const [isInSupportMode, setIsInSupportMode] = useLocalStorage(
"DTIPosePickerIsInSupportMode",
false,
);
const toast = useToast();
const loading = posesQuery.loading || altStylesQuery.isLoading;
const error = posesQuery.error ?? altStylesQuery.error;
const poseInfos = posesQuery.poseInfos;
const altStyles = altStylesQuery.data ?? [];
const [isOpen, setIsOpen] = React.useState(false);
const [tabIndex, setTabIndex] = React.useState(0);
const altStyle = altStyles.find((s) => s.id === altStyleId);
const placement = useBreakpointValue({ base: "bottom-end", md: "top-end" });
React.useEffect(() => {
// While the popover is open, don't change which tab is open.
if (isOpen) {
return;
}
// Otherwise, set the tab to Styles if we're wearing an Alt Style, or
// Expressions if we're not.
setTabIndex(altStyle != null ? 1 : 0);
}, [altStyle, isOpen]);
// Resize the Popover when we toggle support mode, because it probably will
// affect the content size.
React.useLayoutEffect(() => {
// HACK: To trigger a Popover resize, we simulate a window resize event,
// because Popover listens for window resizes to reposition itself.
// I've also filed an issue requesting an official API!
// https://github.com/chakra-ui/chakra-ui/issues/1853
window.dispatchEvent(new Event("resize"));
}, [isInSupportMode]);
// Generally, the app tries to never put us in an invalid pose state. But it
// can happen with direct URL navigation, or pet loading when modeling isn't
// updated! Let's do some recovery.
const selectedPoseIsAvailable = Object.values(poseInfos).some(
(pi) => pi.isSelected && pi.isAvailable,
);
const firstAvailablePose = Object.values(poseInfos).find(
(pi) => pi.isAvailable,
)?.pose;
React.useEffect(() => {
if (loading) {
return;
}
if (!selectedPoseIsAvailable) {
if (!firstAvailablePose) {
// TODO: I suppose this error would fit better in SpeciesColorPicker!
toast({
status: "error",
title: "Oops, we don't have data for this pet color!",
description:
"If it's new, this might be a modeling issue—try modeling it on " +
"Classic DTI first. Sorry!",
duration: null,
isClosable: true,
});
return;
}
console.warn(
`Pose ${pose} not found for speciesId=${speciesId}, ` +
`colorId=${colorId}. Redirecting to pose ${firstAvailablePose}.`,
);
dispatchToOutfit({ type: "setPose", pose: firstAvailablePose });
}
}, [
loading,
selectedPoseIsAvailable,
firstAvailablePose,
speciesId,
colorId,
pose,
toast,
dispatchToOutfit,
]);
// This is a low-stakes enough control, where enough pairs don't have data
// anyway, that I think I want to just not draw attention to failures.
if (error) {
return null;
}
const numStandardPoses = Object.values(poseInfos).filter(
(p) => p.isAvailable && STANDARD_POSES.includes(p.pose),
).length;
const onChangePose = (e) => {
dispatchToOutfit({ type: "setPose", pose: e.target.value });
};
const onChangeStyle = (altStyleId) => {
dispatchToOutfit({ type: "setStyle", altStyleId });
};
return (
{
setIsOpen(true);
onLockFocus();
}}
onClose={() => {
setIsOpen(false);
onUnlockFocus();
}}
initialFocusRef={initialFocusRef}
isLazy
lazyBehavior="keepMounted"
>
{() => (
<>
ExpressionsStyles
{isInSupportMode ? (
) : (
<>
{numStandardPoses == 0 && (
)}
>
)}
setIsInSupportMode(e.target.checked)}
/>
>
)}
);
}
const PosePickerButton = React.forwardRef(
({ pose, altStyle, isOpen, loading, ...props }, ref) => {
const theme = useTheme();
const icon = altStyle != null ? twemojiSunglasses : getIcon(pose);
const label = altStyle != null ? altStyle.seriesName : getLabel(pose);
return (
{({ css, cx }) => (
)}
);
},
);
function PosePickerTable({ poseInfos, onChange, initialFocusRef }) {
return (