Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions packages/gui/src/@types/CacheService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type CacheInfo from './CacheInfo';

type CacheRequestOptions = {
maxSize?: number;
timeout?: number;
Expand All @@ -22,6 +24,8 @@ type CacheService = {
getChecksum: (url: string, options?: CacheRequestOptions) => Promise<string>;
getURI: (url: string, options?: CacheRequestOptions) => Promise<string>;
invalidate: (url: string) => Promise<void>;
// Read-only lookup of the persisted cache state of each url — never downloads
getCacheInfos: (urls: string[]) => Promise<CacheInfo[]>;

// Event subscriptions
subscribeToDirectoryChange: (callback: (newDirectory: string) => void) => () => void;
Expand Down
8 changes: 8 additions & 0 deletions packages/gui/src/@types/NFTPreviewAvailability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
enum NFTPreviewAvailability {
AVAILABLE = 'available',
UNAVAILABLE = 'unavailable',
ALL = 'all',
NONE = 'none',
}

export default NFTPreviewAvailability;
9 changes: 9 additions & 0 deletions packages/gui/src/@types/NFTPreviewStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Whether an NFT's gallery tile can show its media. UNAVAILABLE covers every
// placeholder a tile renders instead of content: no file to verify against, a
// file that failed to download, and a file that does not match its hash.
enum NFTPreviewStatus {
AVAILABLE = 'available',
UNAVAILABLE = 'unavailable',
}

export default NFTPreviewStatus;
5 changes: 4 additions & 1 deletion packages/gui/src/@types/NFTsDataStatistics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type FileType from '../constants/FileType';

type NFTsDataStatistics = Record<FileType | 'visible' | 'hidden' | 'total' | 'sensitive', number>;
type NFTsDataStatistics = Record<
FileType | 'visible' | 'hidden' | 'total' | 'sensitive' | 'previewAvailable' | 'previewUnavailable',
number
>;

export default NFTsDataStatistics;
19 changes: 18 additions & 1 deletion packages/gui/src/components/nfts/NFTFilterProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import React, { createContext, useMemo, useState, type ReactNode } from 'react';

import NFTPreviewAvailability from '../../@types/NFTPreviewAvailability';
import NFTVisibility from '../../@types/NFTVisibility';
import FileType from '../../constants/FileType';

export interface NFTFilterContextData {
walletIds: number[];
types: FileType[];
visibility: NFTVisibility;
previewAvailability: NFTPreviewAvailability;
search: string | undefined;

setWalletIds: (value: number[]) => void;
setTypes: (value: FileType[]) => void;
setVisibility: (value: NFTVisibility) => void;
setPreviewAvailability: (value: NFTPreviewAvailability) => void;
setSearch: (value: string | undefined) => void;
}

Expand All @@ -34,21 +37,35 @@ export default function NFTFilterProvider(props: NFTFilterProviderProps) {
FileType.UNKNOWN,
]);
const [visibility, setVisibility] = useState<NFTVisibility>(NFTVisibility.ALL);
const [previewAvailability, setPreviewAvailability] = useState<NFTPreviewAvailability>(NFTPreviewAvailability.ALL);
const [search, setSearch] = useState('');

const value = useMemo(
() => ({
walletIds,
types,
visibility,
previewAvailability,
search,

setWalletIds,
setTypes,
setVisibility,
setPreviewAvailability,
setSearch,
}),
[walletIds, types, visibility, search, setWalletIds, setTypes, setVisibility, setSearch],
[
walletIds,
types,
visibility,
previewAvailability,
search,
setWalletIds,
setTypes,
setVisibility,
setPreviewAvailability,
setSearch,
],
);

return <NFTFilterContext.Provider value={value}>{children}</NFTFilterContext.Provider>;
Expand Down
43 changes: 43 additions & 0 deletions packages/gui/src/components/nfts/NFTPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { alpha, Box, IconButton, Tooltip } from '@mui/material';
import React, { useMemo, useRef, Fragment, useCallback, useEffect, type ReactNode } from 'react';
import styled from 'styled-components';

import NFTPreviewStatus from '../../@types/NFTPreviewStatus';
import AudioSmallIcon from '../../assets/img/audio-small.svg';
import DocumentBlobIcon from '../../assets/img/document-blob.svg';
import DocumentSmallIcon from '../../assets/img/document-small.svg';
Expand All @@ -30,6 +31,7 @@ import useHideObjectionableContent from '../../hooks/useHideObjectionableContent
import useNFT from '../../hooks/useNFT';
import useNFTImageFittingMode from '../../hooks/useNFTImageFittingMode';
import useNFTMetadata from '../../hooks/useNFTMetadata';
import useNFTProvider from '../../hooks/useNFTProvider';
import useNFTVerifyHash from '../../hooks/useNFTVerifyHash';
import { useNFTVideoLoopGlobal, useNFTVideoLoopForNFT } from '../../hooks/useNFTVideoLoop';
import useStateAbort from '../../hooks/useStateAbort';
Expand Down Expand Up @@ -139,6 +141,7 @@ export default function NFTPreview(props: NFTPreviewProps) {
} = props;

const { getURI } = useCache();
const { setPreviewStatus } = useNFTProvider();
const nftId = useMemo(() => getNFTId(id), [id]);
const iframeRef = useRef<any>(null);
const { isDarkMode } = useDarkMode();
Expand Down Expand Up @@ -513,6 +516,46 @@ export default function NFTPreview(props: NFTPreviewProps) {
!icon &&
![FileType.MODEL, FileType.DOCUMENT].includes(previewFileType);

// The verdict behind the gallery's preview filter. It follows the
// verification state rather than the render path: a document or model
// tile draws its type icon even when the file could not be fetched, and a
// compact tile never opens the iframe, yet the file is just as unavailable
// (the hash badge says so) — so every preview-mode tile of an NFT reaches
// the same verdict, whichever of them happens to mount. Undefined while
// anything that could still change the verdict is in flight.
const previewStatus = useMemo(() => {
if (isLoading || isLoadingVerifyHash) {
return undefined;
}

if (!preview?.isVerified) {
// no file, a settled mismatch, or a file that failed to download — but
// a thumbnail in metadata that has not arrived yet may still verify
return isLoadingMetadata ? undefined : NFTPreviewStatus.UNAVAILABLE;
}

if (prepareError) {
// the verified file could not be served from the cache
return NFTPreviewStatus.UNAVAILABLE;
}

if (!previewContent) {
// preparePreview has not settled on this uri yet
return undefined;
}

return NFTPreviewStatus.AVAILABLE;
}, [isLoading, isLoadingVerifyHash, isLoadingMetadata, preview, prepareError, previewContent]);

useEffect(() => {
// Only preview-mode tiles report: the detail view verifies the full data
// file rather than the thumbnail and can legitimately disagree with the
// gallery tile for the same NFT.
if (isPreview && previewStatus) {
setPreviewStatus(nftId, previewStatus);
}
}, [isPreview, previewStatus, nftId, setPreviewStatus]);
Comment thread
cursor[bot] marked this conversation as resolved.

return (
<StyledCardPreview width={width} height={height} sx={{ aspectRatio: ratio.toString() }}>
{isLoading ? (
Expand Down
114 changes: 114 additions & 0 deletions packages/gui/src/components/nfts/gallery/NFTGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { xor, intersection /* , sortBy */ } from 'lodash';
import React, { useMemo, useCallback, useRef, useEffect } from 'react';
import { VirtuosoGrid } from 'react-virtuoso';

import NFTPreviewAvailability from '../../../@types/NFTPreviewAvailability';
import NFTVisibility from '../../../@types/NFTVisibility';
import FileType from '../../../constants/FileType';
import useFilteredNFTs from '../../../hooks/useFilteredNFTs';
Expand Down Expand Up @@ -111,6 +112,9 @@ export default function NFTGallery() {
visibility,
setVisibility,

previewAvailability,
setPreviewAvailability,

statistics,
} = useFilteredNFTs();

Expand Down Expand Up @@ -245,6 +249,40 @@ export default function NFTGallery() {
}
}

function togglePreviewAvailable() {
switch (previewAvailability) {
case NFTPreviewAvailability.ALL:
setPreviewAvailability(NFTPreviewAvailability.UNAVAILABLE);
return;
case NFTPreviewAvailability.AVAILABLE:
setPreviewAvailability(NFTPreviewAvailability.NONE);
return;
case NFTPreviewAvailability.NONE:
setPreviewAvailability(NFTPreviewAvailability.AVAILABLE);
return;
case NFTPreviewAvailability.UNAVAILABLE:
default:
setPreviewAvailability(NFTPreviewAvailability.ALL);
}
}

function togglePreviewUnavailable() {
switch (previewAvailability) {
case NFTPreviewAvailability.ALL:
setPreviewAvailability(NFTPreviewAvailability.AVAILABLE);
return;
case NFTPreviewAvailability.AVAILABLE:
setPreviewAvailability(NFTPreviewAvailability.ALL);
return;
case NFTPreviewAvailability.NONE:
setPreviewAvailability(NFTPreviewAvailability.UNAVAILABLE);
return;
case NFTPreviewAvailability.UNAVAILABLE:
default:
setPreviewAvailability(NFTPreviewAvailability.NONE);
}
}

function renderNFTCard(index: number, nft: NFTInfo) {
return (
<NFTCard
Expand Down Expand Up @@ -450,6 +488,82 @@ export default function NFTGallery() {
</FilterPill>
</Box>
</Fade>
<Fade in={showFilters} unmountOnExit>
<Box>
<FilterPill
title={
previewAvailability === NFTPreviewAvailability.ALL ? (
<Trans>
Any preview &nbsp;
<Chip label={<FormatLargeNumber value={statistics.total} />} size="extraSmall" />
</Trans>
) : previewAvailability === NFTPreviewAvailability.AVAILABLE ? (
<Trans>
Preview available &nbsp;
<Chip label={<FormatLargeNumber value={statistics.previewAvailable} />} size="extraSmall" />
</Trans>
) : previewAvailability === NFTPreviewAvailability.UNAVAILABLE ? (
<Trans>
Preview not available &nbsp;
<Chip
label={<FormatLargeNumber value={statistics.previewUnavailable} />}
size="extraSmall"
/>
</Trans>
) : (
<Trans>None (0)</Trans>
)
}
>
<FormControl>
<Flex flexDirection="column">
<FormControlLabel
control={
<Checkbox
checked={[NFTPreviewAvailability.AVAILABLE, NFTPreviewAvailability.ALL].includes(
previewAvailability,
)}
onChange={togglePreviewAvailable}
/>
}
label={
<Flex width="100%" gap={1} justifyContent="space-between" alignItems="center">
<Box>
<Trans>Preview available</Trans>
</Box>
<Chip
label={<FormatLargeNumber value={statistics.previewAvailable} />}
size="extraSmall"
/>
</Flex>
}
/>
<FormControlLabel
control={
<Checkbox
checked={[NFTPreviewAvailability.UNAVAILABLE, NFTPreviewAvailability.ALL].includes(
previewAvailability,
)}
onChange={togglePreviewUnavailable}
/>
}
label={
<Flex width="100%" gap={1} justifyContent="space-between" alignItems="center">
<Box>
<Trans>Preview not available</Trans>
</Box>
<Chip
label={<FormatLargeNumber value={statistics.previewUnavailable} />}
size="extraSmall"
/>
</Flex>
}
/>
</Flex>
</FormControl>
</FilterPill>
</Box>
</Fade>
</Flex>
</Box>
</Flex>
Expand Down
Loading