diff --git a/pages/api/media-type.ts b/pages/api/media-type.ts
index f64301f094..7489fecdf1 100644
--- a/pages/api/media-type.ts
+++ b/pages/api/media-type.ts
@@ -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) {
diff --git a/ui/address/tokens/NFTItem.tsx b/ui/address/tokens/NFTItem.tsx
index 50a33ab73b..cc61b70dd1 100644
--- a/ui/address/tokens/NFTItem.tsx
+++ b/ui/address/tokens/NFTItem.tsx
@@ -26,7 +26,8 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P
diff --git a/ui/blocks/BlocksTable.tsx b/ui/blocks/BlocksTable.tsx
index cfd481321d..9a422d0de5 100644
--- a/ui/blocks/BlocksTable.tsx
+++ b/ui/blocks/BlocksTable.tsx
@@ -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);
diff --git a/ui/shared/Page/PageTitle.tsx b/ui/shared/Page/PageTitle.tsx
index d531921cc4..8c6e2f5d53 100644
--- a/ui/shared/Page/PageTitle.tsx
+++ b/ui/shared/Page/PageTitle.tsx
@@ -143,7 +143,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
{ withTextAd && }
{ secondRow && (
-
+
{ secondRow }
) }
diff --git a/ui/shared/nft/NftMedia.pw.tsx b/ui/shared/nft/NftMedia.pw.tsx
index 71d09de045..4b91def420 100644
--- a/ui/shared/nft/NftMedia.pw.tsx
+++ b/ui/shared/nft/NftMedia.pw.tsx
@@ -10,7 +10,54 @@ test.describe('no url', () => {
test('preview +@dark-mode', async({ mount }) => {
const component = await mount(
-
+
+ ,
+ );
+
+ 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(
+
+
+ ,
+ );
+
+ 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(
+
+
,
);
@@ -35,7 +82,7 @@ test.describe('image', () => {
test('preview +@dark-mode', async({ mount }) => {
const component = await mount(
-
+
,
);
@@ -55,7 +102,7 @@ test('image preview hover', async({ mount, page }) => {
const component = await mount(
-
+
,
);
@@ -75,7 +122,7 @@ test('image fullscreen +@dark-mode +@mobile', async({ mount, page }) => {
});
const component = await mount(
-
+
,
);
@@ -107,7 +154,7 @@ test.describe('page', () => {
test('preview +@dark-mode', async({ mount }) => {
const component = await mount(
-
+
,
);
diff --git a/ui/shared/nft/NftMedia.tsx b/ui/shared/nft/NftMedia.tsx
index e01569c314..ace164692a 100644
--- a/ui/shared/nft/NftMedia.tsx
+++ b/ui/shared/nft/NftMedia.tsx
@@ -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);
@@ -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 ;
}
+ const { type, url } = mediaInfo;
+
+ if (!url) {
+ return null;
+ }
+
const props = {
src: url,
onLoad: handleMediaLoaded,
@@ -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;
}
diff --git a/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-non-media-url-and-fallback-1.png b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-non-media-url-and-fallback-1.png
new file mode 100644
index 0000000000..4273139faf
Binary files /dev/null and b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-non-media-url-and-fallback-1.png differ
diff --git a/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-with-fallback-1.png b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-with-fallback-1.png
new file mode 100644
index 0000000000..4273139faf
Binary files /dev/null and b/ui/shared/nft/__screenshots__/NftMedia.pw.tsx_default_no-url-with-fallback-1.png differ
diff --git a/ui/shared/nft/useNftMediaInfo.tsx b/ui/shared/nft/useNftMediaInfo.tsx
new file mode 100644
index 0000000000..7c36f828e9
--- /dev/null
+++ b/ui/shared/nft/useNftMediaInfo.tsx
@@ -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, 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,
+ });
+}
diff --git a/ui/shared/nft/useNftMediaType.tsx b/ui/shared/nft/useNftMediaType.tsx
deleted file mode 100644
index 4444b122e3..0000000000
--- a/ui/shared/nft/useNftMediaType.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-
-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';
-
-export default function useNftMediaType(url: string | null, isEnabled: boolean) {
-
- const fetch = useFetch();
-
- const { data } = useQuery, MediaType>({
- queryKey: [ 'nft-media-type', url ],
- queryFn: async() => {
- if (!url) {
- return 'image';
- }
-
- // 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 preliminaryType;
- }
-
- 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 ?? 'image' : 'image';
- } catch (error) {
- return 'image';
- }
- },
- enabled: isEnabled && Boolean(url),
- staleTime: Infinity,
- });
-
- return data;
-}
diff --git a/ui/token/TokenInventoryItem.tsx b/ui/token/TokenInventoryItem.tsx
index 72f650d2b8..6add374550 100644
--- a/ui/token/TokenInventoryItem.tsx
+++ b/ui/token/TokenInventoryItem.tsx
@@ -20,7 +20,8 @@ const TokenInventoryItem = ({ item, token, isLoading }: Props) => {
const mediaElement = (
);
diff --git a/ui/tokenInstance/TokenInstanceDetails.tsx b/ui/tokenInstance/TokenInstanceDetails.tsx
index 6896f1cc4c..dfff7aacc4 100644
--- a/ui/tokenInstance/TokenInstanceDetails.tsx
+++ b/ui/tokenInstance/TokenInstanceDetails.tsx
@@ -74,7 +74,8 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {