From 5a7535a8a1c14bed85508243e39c9fc89e59a76d Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 1 Jun 2026 22:00:17 +0200 Subject: [PATCH] bug-fixes --- src/components/Image/index.tsx | 45 ++++++++++++++++++++++++++----- src/hooks/useNip57QuickZap.ts | 5 ++-- src/lib/revealed-media-session.ts | 14 ++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 src/lib/revealed-media-session.ts diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index 8ac1fc40..c3254f39 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -1,9 +1,11 @@ import { Skeleton } from '@/components/ui/skeleton' import { cn } from '@/lib/utils' +import { markMediaUrlRevealed, wasMediaUrlRevealed } from '@/lib/revealed-media-session' import { isRenderableMediaUrl, isSafeMediaUrl, primalR2aMirrorForBlossomPrimalUrl, + primalR2aUploads2UrlFromSha256, resolvePrimalBlossomPlayableUrl } from '@/lib/url' import { TImetaInfo } from '@/types' @@ -53,8 +55,17 @@ function formatFileSize(bytes: number): string { return `${bytes} B` } +function extensionWithDotFromUrl(url: string): string { + try { + const m = new URL(url).pathname.match(/(\.[a-z0-9]+)$/i) + return m?.[1]?.toLowerCase() ?? '' + } catch { + return '' + } +} + export default function Image({ - image: { url, blurHash, dim, alt: imetaAlt, fallback, size: fileSizeBytes }, + image: { url, blurHash, dim, alt: imetaAlt, fallback, size: fileSizeBytes, x: imetaHash }, alt, className = '', classNames = {}, @@ -112,6 +123,8 @@ export default function Image({ const loadWatchRef = useRef(null) /** After r2a + imeta fallbacks fail, try `url` on blossom.primal.net once (see handleError). */ const triedPrimaryBlossomDirectRef = useRef(false) + const triedR2aFromHashRef = useRef(false) + const userRevealedRef = useRef(false) // Kept in sync in the reset effect; load-timeout runs only while tap-to-load is actually active. const wasInitiallyHeldRef = useRef(effectiveHoldUntilClick) const imgRef = useRef(null) @@ -160,11 +173,14 @@ export default function Image({ loadSettledRef.current = false wasInitiallyHeldRef.current = effectiveHoldUntilClick const shouldHold = effectiveHoldUntilClick - setRevealed(!shouldHold) + const sessionRevealed = Boolean(url?.trim() && wasMediaUrlRevealed(url)) + const showImmediately = !shouldHold || userRevealedRef.current || sessionRevealed + setRevealed(showImmediately) setHasError(false) setDisplaySkeleton(true) setFallbackIndex(0) triedPrimaryBlossomDirectRef.current = false + triedR2aFromHashRef.current = false clearLoadWatch() if (!url?.trim()) { setIsLoading(false) @@ -172,7 +188,7 @@ export default function Image({ setDisplaySkeleton(false) return } - setIsLoading(!shouldHold) + setIsLoading(showImmediately) }, [url, effectiveHoldUntilClick]) const notifyLoaded = useCallback(() => { @@ -195,7 +211,7 @@ export default function Image({ notifyLoaded() return } - if (!effectiveHoldUntilClick && typeof el.decode === 'function') { + if (typeof el.decode === 'function') { let cancelled = false el.decode().then(() => { if (!cancelled && el.naturalWidth > 0) notifyLoaded() @@ -204,7 +220,7 @@ export default function Image({ cancelled = true } } - }, [revealed, badSrc, imageUrl, effectiveHoldUntilClick, notifyLoaded]) + }, [revealed, badSrc, imageUrl, notifyLoaded]) useEffect(() => { clearLoadWatch() @@ -213,6 +229,11 @@ export default function Image({ if (!wasInitiallyHeldRef.current) return loadWatchRef.current = window.setTimeout(() => { loadWatchRef.current = null + const el = imgRef.current + if (el?.complete && el.naturalWidth > 0) { + notifyLoaded() + return + } setIsLoading(false) setDisplaySkeleton(false) setHasError(true) @@ -244,6 +265,16 @@ export default function Image({ setImageUrl(primary) return } + const hash = imetaHash?.trim().toLowerCase() + if (hash && !triedR2aFromHashRef.current) { + const r2a = primalR2aUploads2UrlFromSha256(hash, extensionWithDotFromUrl(primary || imageUrl)) + if (r2a && imageUrl !== r2a) { + triedR2aFromHashRef.current = true + loadSettledRef.current = false + setImageUrl(r2a) + return + } + } setIsLoading(false) setDisplaySkeleton(false) setHasError(true) @@ -265,6 +296,8 @@ export default function Image({ const handleReveal = () => { if (revealed) return + userRevealedRef.current = true + if (url?.trim()) markMediaUrlRevealed(url) setRevealed(true) setIsLoading(true) } @@ -323,7 +356,7 @@ export default function Image({ ref={imgRef} src={imageUrl} alt={finalAlt} - referrerPolicy="no-referrer" + referrerPolicy="no-referrer-when-downgrade" decoding={effectiveHoldUntilClick ? 'async' : 'sync'} // `lazy` often never starts the request inside nested feed scrollers; always-load should fetch eagerly. loading="eager" diff --git a/src/hooks/useNip57QuickZap.ts b/src/hooks/useNip57QuickZap.ts index d56be3a0..e84cd4b9 100644 --- a/src/hooks/useNip57QuickZap.ts +++ b/src/hooks/useNip57QuickZap.ts @@ -20,7 +20,8 @@ export function useNip57QuickZap(opts: { onZapDialogClose?: () => void }) { const { t } = useTranslation() - const { pubkey, checkLogin } = useNostr() + const { pubkey, account, checkLogin } = useNostr() + const isLoggedIn = Boolean(pubkey && account && account.signerType !== 'npub') const { isWalletConnected, defaultZapSats, defaultZapComment, includePublicZapReceipt } = useZap() const [zapping, setZapping] = useState(false) const enabled = opts.enabled ?? false @@ -66,11 +67,11 @@ export function useNip57QuickZap(opts: { const canQuickNip57Zap = enabled && + isLoggedIn && isWalletConnected && defaultZapSats >= 1 && nip57Addresses !== null && nip57Addresses.length > 0 && - !!pubkey && pubkey !== opts.recipientPubkey const recipientNpubLabel = useMemo(() => { diff --git a/src/lib/revealed-media-session.ts b/src/lib/revealed-media-session.ts new file mode 100644 index 00000000..9a9169b5 --- /dev/null +++ b/src/lib/revealed-media-session.ts @@ -0,0 +1,14 @@ +import { cleanUrl } from '@/lib/url' + +/** URLs the user chose to load this session (tap-to-reveal); survives Image remounts when feeds re-parse. */ +const revealed = new Set() + +export function markMediaUrlRevealed(url: string): void { + const key = cleanUrl(url.trim()) + if (key) revealed.add(key) +} + +export function wasMediaUrlRevealed(url: string): boolean { + const key = cleanUrl(url.trim()) + return key ? revealed.has(key) : false +}