Browse Source

bug-fixes

imwald
Silberengel 2 weeks ago
parent
commit
5a7535a8a1
  1. 45
      src/components/Image/index.tsx
  2. 5
      src/hooks/useNip57QuickZap.ts
  3. 14
      src/lib/revealed-media-session.ts

45
src/components/Image/index.tsx

@ -1,9 +1,11 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { markMediaUrlRevealed, wasMediaUrlRevealed } from '@/lib/revealed-media-session'
import { import {
isRenderableMediaUrl, isRenderableMediaUrl,
isSafeMediaUrl, isSafeMediaUrl,
primalR2aMirrorForBlossomPrimalUrl, primalR2aMirrorForBlossomPrimalUrl,
primalR2aUploads2UrlFromSha256,
resolvePrimalBlossomPlayableUrl resolvePrimalBlossomPlayableUrl
} from '@/lib/url' } from '@/lib/url'
import { TImetaInfo } from '@/types' import { TImetaInfo } from '@/types'
@ -53,8 +55,17 @@ function formatFileSize(bytes: number): string {
return `${bytes} B` 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({ 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, alt,
className = '', className = '',
classNames = {}, classNames = {},
@ -112,6 +123,8 @@ export default function Image({
const loadWatchRef = useRef<number | null>(null) const loadWatchRef = useRef<number | null>(null)
/** After r2a + imeta fallbacks fail, try `url` on blossom.primal.net once (see handleError). */ /** After r2a + imeta fallbacks fail, try `url` on blossom.primal.net once (see handleError). */
const triedPrimaryBlossomDirectRef = useRef(false) 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. // Kept in sync in the reset effect; load-timeout runs only while tap-to-load is actually active.
const wasInitiallyHeldRef = useRef(effectiveHoldUntilClick) const wasInitiallyHeldRef = useRef(effectiveHoldUntilClick)
const imgRef = useRef<HTMLImageElement | null>(null) const imgRef = useRef<HTMLImageElement | null>(null)
@ -160,11 +173,14 @@ export default function Image({
loadSettledRef.current = false loadSettledRef.current = false
wasInitiallyHeldRef.current = effectiveHoldUntilClick wasInitiallyHeldRef.current = effectiveHoldUntilClick
const shouldHold = effectiveHoldUntilClick const shouldHold = effectiveHoldUntilClick
setRevealed(!shouldHold) const sessionRevealed = Boolean(url?.trim() && wasMediaUrlRevealed(url))
const showImmediately = !shouldHold || userRevealedRef.current || sessionRevealed
setRevealed(showImmediately)
setHasError(false) setHasError(false)
setDisplaySkeleton(true) setDisplaySkeleton(true)
setFallbackIndex(0) setFallbackIndex(0)
triedPrimaryBlossomDirectRef.current = false triedPrimaryBlossomDirectRef.current = false
triedR2aFromHashRef.current = false
clearLoadWatch() clearLoadWatch()
if (!url?.trim()) { if (!url?.trim()) {
setIsLoading(false) setIsLoading(false)
@ -172,7 +188,7 @@ export default function Image({
setDisplaySkeleton(false) setDisplaySkeleton(false)
return return
} }
setIsLoading(!shouldHold) setIsLoading(showImmediately)
}, [url, effectiveHoldUntilClick]) }, [url, effectiveHoldUntilClick])
const notifyLoaded = useCallback(() => { const notifyLoaded = useCallback(() => {
@ -195,7 +211,7 @@ export default function Image({
notifyLoaded() notifyLoaded()
return return
} }
if (!effectiveHoldUntilClick && typeof el.decode === 'function') { if (typeof el.decode === 'function') {
let cancelled = false let cancelled = false
el.decode().then(() => { el.decode().then(() => {
if (!cancelled && el.naturalWidth > 0) notifyLoaded() if (!cancelled && el.naturalWidth > 0) notifyLoaded()
@ -204,7 +220,7 @@ export default function Image({
cancelled = true cancelled = true
} }
} }
}, [revealed, badSrc, imageUrl, effectiveHoldUntilClick, notifyLoaded]) }, [revealed, badSrc, imageUrl, notifyLoaded])
useEffect(() => { useEffect(() => {
clearLoadWatch() clearLoadWatch()
@ -213,6 +229,11 @@ export default function Image({
if (!wasInitiallyHeldRef.current) return if (!wasInitiallyHeldRef.current) return
loadWatchRef.current = window.setTimeout(() => { loadWatchRef.current = window.setTimeout(() => {
loadWatchRef.current = null loadWatchRef.current = null
const el = imgRef.current
if (el?.complete && el.naturalWidth > 0) {
notifyLoaded()
return
}
setIsLoading(false) setIsLoading(false)
setDisplaySkeleton(false) setDisplaySkeleton(false)
setHasError(true) setHasError(true)
@ -244,6 +265,16 @@ export default function Image({
setImageUrl(primary) setImageUrl(primary)
return 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) setIsLoading(false)
setDisplaySkeleton(false) setDisplaySkeleton(false)
setHasError(true) setHasError(true)
@ -265,6 +296,8 @@ export default function Image({
const handleReveal = () => { const handleReveal = () => {
if (revealed) return if (revealed) return
userRevealedRef.current = true
if (url?.trim()) markMediaUrlRevealed(url)
setRevealed(true) setRevealed(true)
setIsLoading(true) setIsLoading(true)
} }
@ -323,7 +356,7 @@ export default function Image({
ref={imgRef} ref={imgRef}
src={imageUrl} src={imageUrl}
alt={finalAlt} alt={finalAlt}
referrerPolicy="no-referrer" referrerPolicy="no-referrer-when-downgrade"
decoding={effectiveHoldUntilClick ? 'async' : 'sync'} decoding={effectiveHoldUntilClick ? 'async' : 'sync'}
// `lazy` often never starts the request inside nested feed scrollers; always-load should fetch eagerly. // `lazy` often never starts the request inside nested feed scrollers; always-load should fetch eagerly.
loading="eager" loading="eager"

5
src/hooks/useNip57QuickZap.ts

@ -20,7 +20,8 @@ export function useNip57QuickZap(opts: {
onZapDialogClose?: () => void onZapDialogClose?: () => void
}) { }) {
const { t } = useTranslation() 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 { isWalletConnected, defaultZapSats, defaultZapComment, includePublicZapReceipt } = useZap()
const [zapping, setZapping] = useState(false) const [zapping, setZapping] = useState(false)
const enabled = opts.enabled ?? false const enabled = opts.enabled ?? false
@ -66,11 +67,11 @@ export function useNip57QuickZap(opts: {
const canQuickNip57Zap = const canQuickNip57Zap =
enabled && enabled &&
isLoggedIn &&
isWalletConnected && isWalletConnected &&
defaultZapSats >= 1 && defaultZapSats >= 1 &&
nip57Addresses !== null && nip57Addresses !== null &&
nip57Addresses.length > 0 && nip57Addresses.length > 0 &&
!!pubkey &&
pubkey !== opts.recipientPubkey pubkey !== opts.recipientPubkey
const recipientNpubLabel = useMemo(() => { const recipientNpubLabel = useMemo(() => {

14
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<string>()
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
}
Loading…
Cancel
Save