Skip to content

fix: hide broken nfts#897

Open
bohdan-struk-avalabs wants to merge 4 commits intomainfrom
fix/hide-broken-nfts
Open

fix: hide broken nfts#897
bohdan-struk-avalabs wants to merge 4 commits intomainfrom
fix/hide-broken-nfts

Conversation

@bohdan-struk-avalabs
Copy link
Copy Markdown
Collaborator

Description

Jira ticket:
https://ava-labs.atlassian.net/browse/CP-13883

Screenshot 2026-04-17 at 9 29 29 PM Screenshot 2026-04-17 at 9 29 45 PM Screenshot 2026-04-17 at 9 30 02 PM

Changes

Testing

Screenshots:

Checklist for the author

Tick each of them when done or if not applicable.

  • I've covered new/modified business logic with Jest test cases.
  • I've tested the changes myself before sending it to code review and QA.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a user-controlled option to hide “broken” collectibles in the Portfolio → Collectibles view by tracking media load failures and filtering affected NFTs from the rendered grid.

Changes:

  • Add persisted “hide broken images” setting to the collectibles toolbar state and manage popup.
  • Track per-NFT media load failures and use that state to filter collectibles from the list.
  • Improve MediaRenderer error propagation so parents are notified even when media elements never mount (e.g., no source / error UI paths).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/next/src/pages/Portfolio/components/PortfolioHome/components/PortolioDetails/components/CollectiblesTab/hooks/useCollectiblesToolbar.ts Adds persisted hide-broken setting and filters collectibles based on missing media and tracked failures.
apps/next/src/pages/Portfolio/components/PortfolioHome/components/PortolioDetails/components/CollectiblesTab/components/MediaRenderer.tsx Ensures onError is fired for “error UI without media element” cases; prevents double notification.
apps/next/src/pages/Portfolio/components/PortfolioHome/components/PortolioDetails/components/CollectiblesTab/components/CollectiblesManagePopup.tsx Adds a switch to toggle hiding broken/missing-media NFTs.
apps/next/src/pages/Portfolio/components/PortfolioHome/components/PortolioDetails/components/CollectiblesTab/components/CollectibleCard.tsx Wires media load/error callbacks from MediaRenderer up to the tab state.
apps/next/src/pages/Portfolio/components/PortfolioHome/components/PortolioDetails/components/CollectiblesTab/CollectiblesTab.tsx Tracks failed media IDs and passes them into toolbar filtering; updates failed set on load/error events.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +249 to +263
// When the error state is active and not just a refresh animation, the <img> may never
// render (hasNoSource, showError, mimeTypeIsError), so browser onerror never fires.
// Notify the parent here instead.
// The hasNotifiedErrorRef guard prevents double-firing when handleError already notified.
const isRealError = !isRefreshingLocally && !isLoading && isError;
useEffect(() => {
if (
isRealError &&
!sourceIsBase64Image &&
!hasNotifiedErrorRef.current
) {
hasNotifiedErrorRef.current = true;
onErrorRef.current?.();
}
}, [isRealError, sourceIsBase64Image]);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new error-notification effect can fire when isError is true due to hasNoSource (e.g., when logoUri is missing). In this component, currentSource is derived from logoUri unless useFallback is already true, so NFTs that do have logoSmall/tokenUri can still end up with hasNoSource, triggering onError and causing the parent to mark them as failed/hidden. Consider selecting srcRaw as the initial source when logoUri is absent (or gating this effect so it only notifies after an actual media load attempt), to avoid hiding valid NFTs.

Suggested change
// When the error state is active and not just a refresh animation, the <img> may never
// render (hasNoSource, showError, mimeTypeIsError), so browser onerror never fires.
// Notify the parent here instead.
// The hasNotifiedErrorRef guard prevents double-firing when handleError already notified.
const isRealError = !isRefreshingLocally && !isLoading && isError;
useEffect(() => {
if (
isRealError &&
!sourceIsBase64Image &&
!hasNotifiedErrorRef.current
) {
hasNotifiedErrorRef.current = true;
onErrorRef.current?.();
}
}, [isRealError, sourceIsBase64Image]);
// If the current source is empty but a raw source still exists and fallback has not been
// attempted yet, avoid notifying the parent as a terminal failure. This prevents valid NFTs
// from being hidden before an actual media load attempt can happen.
const isAwaitingFallbackSource = hasNoSource && !!srcRaw && !useFallback;
// When the error state is active and not just a refresh animation, the <img> may never
// render (hasNoSource, showError, mimeTypeIsError), so browser onerror never fires.
// Notify the parent here instead, but only once we know there is no remaining fallback
// source to try.
// The hasNotifiedErrorRef guard prevents double-firing when handleError already notified.
const isRealError = !isRefreshingLocally && !isLoading && isError;
useEffect(() => {
if (
isRealError &&
!isAwaitingFallbackSource &&
!sourceIsBase64Image &&
!hasNotifiedErrorRef.current
) {
hasNotifiedErrorRef.current = true;
onErrorRef.current?.();
}
}, [isAwaitingFallbackSource, isRealError, sourceIsBase64Image]);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed

Comment on lines +101 to +107
<Typography variant="body2">
{t('Hide NFTs without media')}
</Typography>
<Switch
checked={hideBrokenImages}
onChange={() => setHideBrokenImages(!hideBrokenImages)}
size="small"
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toggle label says "Hide NFTs without media", but enabling it also hides NFTs whose media fails to load (via failedCollectibleIds). Please update the copy to reflect the actual behavior (e.g., missing or broken media) so users understand what will be hidden.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are ok with that

Comment on lines 71 to +95
@@ -83,7 +89,26 @@ export function CollectiblesTab() {
networkFilters,
toggleNetworkFilter,
clearNetworkFilter,
} = useCollectiblesToolbar({ collectibles: formattedCollectibles });
} = useCollectiblesToolbar({
collectibles: formattedCollectibles,
failedCollectibleIds,
});
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

failedCollectibleIds is never cleared when the underlying collectibles list changes (account/network switch, refetch, etc.). Once an ID is marked failed it can stay hidden for the rest of the session even if the media becomes reachable again. Consider resetting this set when collectibles (or formattedCollectibles) changes, and/or when the user toggles the "hide broken" setting.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are ok with that, this causes unnecessary re-renders

const handleMediaError = useCallback((id: string) => {
setFailedCollectibleIds((prev) => {
if (prev.has(id)) return prev;
return new Set([...prev, id]);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleMediaError builds a new Set via new Set([...prev, id]), which copies the entire set into an array on every new failure. Using const next = new Set(prev); next.add(id); return next; avoids the intermediate array and is cheaper for large NFT lists.

Suggested change
return new Set([...prev, id]);
const next = new Set(prev);
next.add(id);
return next;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants