Fix crash on invalid blurhash strings from Nostr events
Validate blurhash hashes before passing them to react-blurhash's <Blurhash> component, which throws when the encoded length doesn't match the component count header. Malformed hashes from third-party events (e.g. length 92 instead of 94) now gracefully fall back to a skeleton placeholder instead of crashing the page.
This commit is contained in:
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import { ChevronLeft, ChevronRight, X, Download } from 'lucide-react';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isValidBlurhash } from '@/lib/blurhash';
|
||||
import { openUrl } from '@/lib/downloadFile';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useBlossomFallback } from '@/hooks/useBlossomFallback';
|
||||
@@ -230,7 +231,7 @@ function GridImage({
|
||||
>
|
||||
{/* Placeholder shown while the image is loading */}
|
||||
{!loaded && (
|
||||
blurhash ? (
|
||||
isValidBlurhash(blurhash) ? (
|
||||
// Blurhash canvas fills the container via CSS — pass small integer decode
|
||||
// resolution; the canvas is stretched to 100%×100% by the style prop.
|
||||
<Blurhash
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
||||
import { Images, Play, ShieldAlert } from 'lucide-react';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import { isValidBlurhash } from '@/lib/blurhash';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { getAvatarShape } from '@/lib/avatarShape';
|
||||
@@ -139,7 +140,7 @@ function MediaThumb({ item, onClick }: { item: MediaItem; onClick: () => void })
|
||||
onClick={showBlur ? (e) => { e.stopPropagation(); setCwRevealed(true); } : onClick}
|
||||
aria-label={showBlur ? 'Reveal sensitive content' : 'View media'}
|
||||
>
|
||||
{item.blurhash && (
|
||||
{isValidBlurhash(item.blurhash) && (
|
||||
<Blurhash
|
||||
hash={item.blurhash}
|
||||
width="100%"
|
||||
@@ -151,7 +152,7 @@ function MediaThumb({ item, onClick }: { item: MediaItem; onClick: () => void })
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
)}
|
||||
{!item.blurhash && !loaded && item.type !== 'audio' && (
|
||||
{!isValidBlurhash(item.blurhash) && !loaded && item.type !== 'audio' && (
|
||||
<Skeleton className="absolute inset-0 w-full h-full rounded-none" />
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Check, Copy, QrCode, ExternalLink, Bitcoin, ShieldAlert, Mail } from 'l
|
||||
import { LinkFooter } from '@/components/LinkFooter';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isValidBlurhash } from '@/lib/blurhash';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@@ -193,7 +194,7 @@ function MediaTile({ item }: { item: MediaItem }) {
|
||||
<div className="relative w-full h-full">
|
||||
{/* Blurhash or skeleton placeholder while media loads */}
|
||||
{!loaded && (
|
||||
item.blurhash ? (
|
||||
isValidBlurhash(item.blurhash) ? (
|
||||
<Blurhash
|
||||
hash={item.blurhash}
|
||||
width={32}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type Hls from 'hls.js';
|
||||
import { Play, Pause, Volume1, Volume2, VolumeX, Expand } from 'lucide-react';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isValidBlurhash } from '@/lib/blurhash';
|
||||
import { useBlossomFallback } from '@/hooks/useBlossomFallback';
|
||||
import { usePlayerControls } from '@/hooks/usePlayerControls';
|
||||
import { useVideoThumbnail } from '@/hooks/useVideoThumbnail';
|
||||
@@ -203,7 +204,7 @@ export function VideoPlayer({ src: originalSrc, poster, className, dim, blurhash
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Blurhash placeholder — shown until the video has a displayable frame */}
|
||||
{blurhash && !videoReady && !generatedPoster && (
|
||||
{isValidBlurhash(blurhash) && !videoReady && !generatedPoster && (
|
||||
<Blurhash
|
||||
hash={blurhash}
|
||||
width="100%"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { isBlurhashValid } from 'blurhash';
|
||||
|
||||
/** Returns `true` when `hash` is a structurally valid blurhash string. */
|
||||
export function isValidBlurhash(hash: string | undefined | null): hash is string {
|
||||
if (!hash) return false;
|
||||
return isBlurhashValid(hash).result;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { nip19 } from "nostr-tools";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Blurhash } from "react-blurhash";
|
||||
import { Link } from "react-router-dom";
|
||||
import { isValidBlurhash } from "@/lib/blurhash";
|
||||
import { ARC_OVERHANG_PX } from "@/components/ArcBackground";
|
||||
import { FeedEmptyState } from "@/components/FeedEmptyState";
|
||||
import { KindInfoButton } from "@/components/KindInfoButton";
|
||||
@@ -247,7 +248,7 @@ function VideoGridCard({ event }: { event: NostrEvent }) {
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
<div className="relative aspect-video overflow-hidden rounded-xl bg-muted">
|
||||
{blurhash && (
|
||||
{isValidBlurhash(blurhash) && (
|
||||
<Blurhash
|
||||
hash={blurhash}
|
||||
width="100%"
|
||||
@@ -644,7 +645,7 @@ function ShortThumb({
|
||||
aria-label={title}
|
||||
>
|
||||
<div className="relative w-full aspect-[9/16] overflow-hidden rounded-xl bg-muted">
|
||||
{blurhash && !thumbnail && (
|
||||
{isValidBlurhash(blurhash) && !thumbnail && (
|
||||
<Blurhash
|
||||
hash={blurhash}
|
||||
width="100%"
|
||||
|
||||
Reference in New Issue
Block a user