Use standard image URLs on Your Outfits page

The old URLs were glitchy because we weren't escaping the `layerUrls` param… and this will let us take better advantage of the same shared caching as other stuff!
This commit is contained in:
Emi Matchu 2021-09-03 15:37:38 -07:00
parent ae6b012f88
commit 9a68bd1355
3 changed files with 24 additions and 96 deletions

View file

@ -5,12 +5,9 @@ import gql from "graphql-tag";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { ErrorMessage, Heading1, useCommonStyles } from "./util"; import { Heading1, MajorErrorMessage, useCommonStyles } from "./util";
import HangerSpinner from "./components/HangerSpinner"; import HangerSpinner from "./components/HangerSpinner";
import OutfitThumbnail, { import OutfitThumbnail from "./components/OutfitThumbnail";
outfitThumbnailFragment,
getOutfitThumbnailRenderSize,
} from "./components/OutfitThumbnail";
import useRequireLogin from "./components/useRequireLogin"; import useRequireLogin from "./components/useRequireLogin";
import WIPCallout from "./components/WIPCallout"; import WIPCallout from "./components/WIPCallout";
@ -31,14 +28,13 @@ function UserOutfitsPageContent() {
const { loading: queryLoading, error, data } = useQuery( const { loading: queryLoading, error, data } = useQuery(
gql` gql`
query UserOutfitsPageContent($size: LayerImageSize) { query UserOutfitsPageContent {
currentUser { currentUser {
id id
outfits { outfits {
id id
name name
updatedAt
...OutfitThumbnailFragment
# For alt text # For alt text
petAppearance { petAppearance {
@ -58,13 +54,8 @@ function UserOutfitsPageContent() {
} }
} }
} }
${outfitThumbnailFragment}
`, `,
{ {
variables: {
// NOTE: This parameter is used inside `OutfitThumbnailFragment`!
size: "SIZE_" + getOutfitThumbnailRenderSize(),
},
context: { sendAuth: true }, context: { sendAuth: true },
skip: userLoading, skip: userLoading,
} }
@ -79,7 +70,7 @@ function UserOutfitsPageContent() {
} }
if (error) { if (error) {
return <ErrorMessage>Error loading outfits: {error.message}</ErrorMessage>; return <MajorErrorMessage error={error} variant="network" />;
} }
const outfits = data.currentUser.outfits; const outfits = data.currentUser.outfits;
@ -106,8 +97,8 @@ function OutfitCard({ outfit }) {
<ClassNames> <ClassNames>
{({ css }) => ( {({ css }) => (
<OutfitThumbnail <OutfitThumbnail
petAppearance={outfit.petAppearance} outfitId={outfit.id}
itemAppearances={outfit.itemAppearances} updatedAt={outfit.updatedAt}
alt={buildOutfitAltText(outfit)} alt={buildOutfitAltText(outfit)}
// Firefox shows alt text as a fallback for images it can't show yet. // Firefox shows alt text as a fallback for images it can't show yet.
// Show our alt text clearly if the image failed to load... but hide // Show our alt text clearly if the image failed to load... but hide
@ -118,6 +109,7 @@ function OutfitCard({ outfit }) {
width={150} width={150}
height={150} height={150}
overflow="auto" overflow="auto"
loading="lazy"
className={css` className={css`
&:-moz-loading { &:-moz-loading {
visibility: hidden; visibility: hidden;

View file

@ -4,10 +4,7 @@ import gql from "graphql-tag";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import OutfitThumbnail, { import OutfitThumbnail from "../components/OutfitThumbnail";
outfitThumbnailFragment,
getOutfitThumbnailRenderSize,
} from "../components/OutfitThumbnail";
import { useOutfitPreview } from "../components/OutfitPreview"; import { useOutfitPreview } from "../components/OutfitPreview";
import { loadable, MajorErrorMessage, TestErrorSender } from "../util"; import { loadable, MajorErrorMessage, TestErrorSender } from "../util";
@ -65,21 +62,20 @@ function WardrobePreviewAndControls({
function OutfitThumbnailIfCached({ outfitId }) { function OutfitThumbnailIfCached({ outfitId }) {
const { data } = useQuery( const { data } = useQuery(
gql` gql`
query OutfitThumbnailIfCached($outfitId: ID!, $size: LayerImageSize!) { query OutfitThumbnailIfCached($outfitId: ID!) {
outfit(id: $outfitId) { outfit(id: $outfitId) {
id id
...OutfitThumbnailFragment updatedAt
} }
} }
${outfitThumbnailFragment}
`, `,
{ {
variables: { variables: {
outfitId, outfitId,
// NOTE: This parameter is used inside `OutfitThumbnailFragment`! skip: outfitId == null,
size: "SIZE_" + getOutfitThumbnailRenderSize(),
}, },
fetchPolicy: "cache-only", fetchPolicy: "cache-only",
onError: (e) => console.error(e),
} }
); );
@ -89,8 +85,8 @@ function OutfitThumbnailIfCached({ outfitId }) {
return ( return (
<OutfitThumbnail <OutfitThumbnail
petAppearance={data.outfit.petAppearance} outfitId={data.outfit.id}
itemAppearances={data.outfit.itemAppearances} updatedAt={data.outfit.updatedAt}
alt="" alt=""
objectFit="contain" objectFit="contain"
width="100%" width="100%"

View file

@ -1,81 +1,21 @@
import React from "react";
import { Box } from "@chakra-ui/react"; import { Box } from "@chakra-ui/react";
import gql from "graphql-tag";
import getVisibleLayers, { function OutfitThumbnail({ outfitId, updatedAt, ...props }) {
petAppearanceFragmentForGetVisibleLayers, const versionTimestamp = new Date(updatedAt).getTime();
itemAppearanceFragmentForGetVisibleLayers,
} from "../../shared/getVisibleLayers"; // NOTE: It'd be more reliable for testing to use a relative path, but
// generating these on dev is SO SLOW, that I'd rather just not.
const thumbnailUrl150 = `https://outfits.openneo-assets.net/outfits/${outfitId}/v/${versionTimestamp}/150.png`;
const thumbnailUrl300 = `https://outfits.openneo-assets.net/outfits/${outfitId}/v/${versionTimestamp}/300.png`;
function OutfitThumbnail({ petAppearance, itemAppearances, ...props }) {
return ( return (
<Box <Box
as="img" as="img"
src={buildOutfitThumbnailUrl(petAppearance, itemAppearances)} src={thumbnailUrl150}
srcSet={`${thumbnailUrl150} 1x, ${thumbnailUrl300} 2x`}
{...props} {...props}
/> />
); );
} }
function buildOutfitThumbnailUrl(petAppearance, itemAppearances) {
const size = getOutfitThumbnailRenderSize();
const visibleLayers = getVisibleLayers(petAppearance, itemAppearances);
const layerUrls = visibleLayers.map(
(layer) => layer.svgUrl || layer.imageUrl
);
return `/api/outfitImage?size=${size}&layerUrls=${layerUrls.join(",")}`;
}
/**
* getOutfitThumbnailRenderSize returns the right image size to render at
* 150x150, for the current device.
*
* On high-DPI devices, we'll download a 300x300 image to render at 150x150
* scale. On standard-DPI devices, we'll download a 150x150 image, to save
* bandwidth.
*/
export function getOutfitThumbnailRenderSize() {
if (window.devicePixelRatio > 1) {
return 300;
} else {
return 150;
}
}
// NOTE: The query must include a `$size: LayerImageSize` parameter, probably
// with the return value of `getOutfitThumbnailRenderSize`!
export const outfitThumbnailFragment = gql`
fragment OutfitThumbnailFragment on Outfit {
petAppearance {
id
layers {
id
svgUrl
imageUrl(size: $size)
}
species {
id
name
}
color {
id
name
}
...PetAppearanceForGetVisibleLayers
}
itemAppearances {
id
layers {
id
svgUrl
imageUrl(size: $size)
}
...ItemAppearanceForGetVisibleLayers
}
}
${petAppearanceFragmentForGetVisibleLayers}
${itemAppearanceFragmentForGetVisibleLayers}
`;
export default OutfitThumbnail; export default OutfitThumbnail;