Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fallback to 'image_url' if 'animation_url' is invalid for NFT instance #1670

Merged
merged 3 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions pages/api/media-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ export default async function mediaTypeHandler(req: NextApiRequest, res: NextApi
return 'video';
}

if (contentType?.startsWith('image')) {
return 'image';
}

if (contentType?.startsWith('text/html')) {
return 'html';
}

return 'image';
})();
res.status(200).json({ type: mediaType });
} catch (error) {
Expand Down
3 changes: 2 additions & 1 deletion ui/address/tokens/NFTItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P
<Link href={ isLoading ? undefined : tokenInstanceLink }>
<NftMedia
mb="18px"
url={ tokenInstance?.animation_url || tokenInstance?.image_url || null }
animationUrl={ tokenInstance?.animation_url ?? null }
imageUrl={ tokenInstance?.image_url ?? null }
isLoading={ isLoading }
/>
</Link>
Expand Down
2 changes: 1 addition & 1 deletion ui/blocks/BlocksTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const isRollup = config.features.rollup.isEnabled;
const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum, socketInfoAlert }: Props) => {

const widthBase =
VALIDATOR_COL_WEIGHT +
(!config.UI.views.block.hiddenFields?.miner ? VALIDATOR_COL_WEIGHT : 0) +
GAS_COL_WEIGHT +
(!isRollup && !config.UI.views.block.hiddenFields?.total_reward ? REWARD_COL_WEIGHT : 0) +
(!isRollup && !config.UI.views.block.hiddenFields?.burnt_fees ? FEES_COL_WEIGHT : 0);
Expand Down
2 changes: 1 addition & 1 deletion ui/shared/Page/PageTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
{ withTextAd && <TextAd order={{ base: -1, lg: 100 }} mb={{ base: 6, lg: 0 }} ml="auto" w={{ base: '100%', lg: 'auto' }}/> }
</Flex>
{ secondRow && (
<Flex alignItems="center" minH={ 10 } overflow="hidden">
<Flex alignItems="center" minH={ 10 } overflow="hidden" _empty={{ display: 'none' }}>
{ secondRow }
</Flex>
) }
Expand Down
57 changes: 52 additions & 5 deletions ui/shared/nft/NftMedia.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,54 @@ test.describe('no url', () => {
test('preview +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<NftMedia url={ null }/>
<NftMedia animationUrl={ null } imageUrl={ null }/>
</TestApp>,
);

await expect(component).toHaveScreenshot();
});

test('with fallback', async({ mount, page }) => {
const IMAGE_URL = 'https://localhost:3000/my-image.jpg';

await page.route(IMAGE_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_long.jpg',
});
});

const component = await mount(
<TestApp>
<NftMedia animationUrl={ null } imageUrl={ IMAGE_URL }/>
</TestApp>,
);

await expect(component).toHaveScreenshot();
});

test('non-media url and fallback', async({ mount, page }) => {
const ANIMATION_URL = 'https://localhost:3000/my-animation.m3u8';
const ANIMATION_MEDIA_TYPE_API_URL = `/node-api/media-type?url=${ encodeURIComponent(ANIMATION_URL) }`;
const IMAGE_URL = 'https://localhost:3000/my-image.jpg';

await page.route(ANIMATION_MEDIA_TYPE_API_URL, (route) => {
return route.fulfill({
status: 200,
body: JSON.stringify({ type: undefined }),
});
});

await page.route(IMAGE_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_long.jpg',
});
});

const component = await mount(
<TestApp>
<NftMedia animationUrl={ ANIMATION_URL } imageUrl={ IMAGE_URL }/>
</TestApp>,
);

Expand All @@ -35,7 +82,7 @@ test.describe('image', () => {
test('preview +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<NftMedia url={ MEDIA_URL }/>
<NftMedia animationUrl={ MEDIA_URL } imageUrl={ null }/>
</TestApp>,
);

Expand All @@ -55,7 +102,7 @@ test('image preview hover', async({ mount, page }) => {

const component = await mount(
<TestApp>
<NftMedia url={ MEDIA_URL } w="250px"/>
<NftMedia animationUrl={ MEDIA_URL } imageUrl={ null } w="250px"/>
</TestApp>,
);

Expand All @@ -75,7 +122,7 @@ test('image fullscreen +@dark-mode +@mobile', async({ mount, page }) => {
});
const component = await mount(
<TestApp>
<NftMedia url={ MEDIA_URL } withFullscreen w="250px"/>
<NftMedia animationUrl={ MEDIA_URL } imageUrl={ null } withFullscreen w="250px"/>
</TestApp>,
);

Expand Down Expand Up @@ -107,7 +154,7 @@ test.describe('page', () => {
test('preview +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<NftMedia url={ MEDIA_URL }/>
<NftMedia animationUrl={ MEDIA_URL } imageUrl={ null }/>
</TestApp>,
);

Expand Down
32 changes: 23 additions & 9 deletions ui/shared/nft/NftMedia.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,31 @@ import NftImage from './NftImage';
import NftImageFullscreen from './NftImageFullscreen';
import NftVideo from './NftVideo';
import NftVideoFullscreen from './NftVideoFullscreen';
import useNftMediaType from './useNftMediaType';
import useNftMediaInfo from './useNftMediaInfo';
import { mediaStyleProps } from './utils';

interface Props {
url: string | null;
imageUrl: string | null;
animationUrl: string | null;
className?: string;
isLoading?: boolean;
withFullscreen?: boolean;
}

const NftMedia = ({ url, className, isLoading, withFullscreen }: Props) => {
const NftMedia = ({ imageUrl, animationUrl, className, isLoading, withFullscreen }: Props) => {
const [ isMediaLoading, setIsMediaLoading ] = React.useState(true);
const [ isLoadingError, setIsLoadingError ] = React.useState(false);

const { ref, inView } = useInView({ triggerOnce: true });

const type = useNftMediaType(url, !isLoading && inView);
const mediaInfo = useNftMediaInfo({ imageUrl, animationUrl, isEnabled: !isLoading && inView });

React.useEffect(() => {
if (!isLoading) {
setIsMediaLoading(Boolean(url));
if (!isLoading && !mediaInfo) {
setIsMediaLoading(false);
setIsLoadingError(true);
}
}, [ isLoading, url ]);
}, [ isLoading, mediaInfo ]);

const handleMediaLoaded = React.useCallback(() => {
setIsMediaLoading(false);
Expand All @@ -45,11 +47,17 @@ const NftMedia = ({ url, className, isLoading, withFullscreen }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();

const content = (() => {
if (!url || isLoadingError) {
if (!mediaInfo || isLoadingError) {
const styleProps = withFullscreen ? {} : mediaStyleProps;
return <NftFallback { ...styleProps }/>;
}

const { type, url } = mediaInfo;

if (!url) {
return null;
}

const props = {
src: url,
onLoad: handleMediaLoaded,
Expand All @@ -70,7 +78,13 @@ const NftMedia = ({ url, className, isLoading, withFullscreen }: Props) => {
})();

const modal = (() => {
if (!url || !withFullscreen) {
if (!mediaInfo || !withFullscreen) {
return null;
}

const { type, url } = mediaInfo;

if (!url) {
return null;
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 98 additions & 0 deletions ui/shared/nft/useNftMediaInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useQuery } from '@tanstack/react-query';
import React from 'react';

import type { StaticRoute } from 'nextjs-routes';
import { route } from 'nextjs-routes';

import type { ResourceError } from 'lib/api/resources';
import useFetch from 'lib/hooks/useFetch';

import type { MediaType } from './utils';
import { getPreliminaryMediaType } from './utils';

interface Params {
imageUrl: string | null;
animationUrl: string | null;
isEnabled: boolean;
}

interface ReturnType {
type: MediaType | undefined;
url: string | null;
}

export default function useNftMediaInfo({ imageUrl, animationUrl, isEnabled }: Params): ReturnType | null {

const primaryQuery = useNftMediaTypeQuery(animationUrl, isEnabled);
const secondaryQuery = useNftMediaTypeQuery(imageUrl, !primaryQuery.isPending && !primaryQuery.data);

return React.useMemo(() => {
if (primaryQuery.isPending) {
return {
type: undefined,
url: animationUrl,
};
}

if (primaryQuery.data) {
return primaryQuery.data;
}

if (secondaryQuery.isPending) {
return {
type: undefined,
url: imageUrl,
};
}

if (secondaryQuery.data) {
return secondaryQuery.data;
}

return null;
}, [ animationUrl, imageUrl, primaryQuery.data, primaryQuery.isPending, secondaryQuery.data, secondaryQuery.isPending ]);
}

function useNftMediaTypeQuery(url: string | null, enabled: boolean) {
const fetch = useFetch();

return useQuery<unknown, ResourceError<unknown>, ReturnType | null>({
queryKey: [ 'nft-media-type', url ],
queryFn: async() => {
if (!url) {
return null;
}

// media could be either image, gif, video or html-page
// so we pre-fetch the resources in order to get its content type
// have to do it via Node.js due to strict CSP for connect-src
// but in order not to abuse our server firstly we check file url extension
// and if it is valid we will trust it and display corresponding media component

const preliminaryType = getPreliminaryMediaType(url);

if (preliminaryType) {
return { type: preliminaryType, url };
}

const type = await (async() => {
try {
const mediaTypeResourceUrl = route({ pathname: '/node-api/media-type' as StaticRoute<'/api/media-type'>['pathname'], query: { url } });
const response = await fetch<{ type: MediaType | undefined }, ResourceError>(mediaTypeResourceUrl, undefined, { resource: 'media-type' });

return 'type' in response ? response.type : undefined;
} catch (error) {
return;
}
})();

if (!type) {
return null;
}

return { type, url };
},
enabled,
staleTime: Infinity,
});
}
49 changes: 0 additions & 49 deletions ui/shared/nft/useNftMediaType.tsx

This file was deleted.

3 changes: 2 additions & 1 deletion ui/token/TokenInventoryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const TokenInventoryItem = ({ item, token, isLoading }: Props) => {
const mediaElement = (
<NftMedia
mb="18px"
url={ item.animation_url || item.image_url }
animationUrl={ item.animation_url }
imageUrl={ item.image_url }
isLoading={ isLoading }
/>
);
Expand Down
3 changes: 2 additions & 1 deletion ui/tokenInstance/TokenInstanceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {
<TokenNftMarketplaces isLoading={ isLoading } hash={ token.address } id={ data.id }/>
</Grid>
<NftMedia
url={ data.animation_url || data.image_url }
animationUrl={ data.animation_url }
imageUrl={ data.image_url }
w="250px"
flexShrink={ 0 }
alignSelf={{ base: 'center', lg: 'flex-start' }}
Expand Down
Loading