You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
95 lines
3.4 KiB
95 lines
3.4 KiB
import { ExtendedKind, isMusicTrackKind, isNip71StyleVideoKind } from '@/constants' |
|
import { getMusicTrackFromEvent } from '@/lib/music-track' |
|
import { isLongFormNip71VideoEventKind } from '@/lib/long-video-load-policy' |
|
import { getCachedThreadContextEvents } from '@/lib/navigation-related-events' |
|
import { toNote } from '@/lib/link' |
|
import client from '@/services/client.service' |
|
import { extractAllMediaFromEvent } from '@/services/media-extraction.service' |
|
import { useSmartNoteNavigationOptional } from '@/PageManager' |
|
import { Image as ImageIcon, Images, Music, Play } from 'lucide-react' |
|
import { type Event } from 'nostr-tools' |
|
import { useMemo } from 'react' |
|
|
|
export default function MediaGridItem({ event }: { event: Event }) { |
|
const { navigateToNote } = useSmartNoteNavigationOptional() |
|
|
|
const media = useMemo(() => extractAllMediaFromEvent(event), [event]) |
|
const first = media.all[0] |
|
|
|
/** Kind 20 is always treated as image unless imeta explicitly says video (rare mis-tag). */ |
|
const isPictureKind = event.kind === ExtendedKind.PICTURE |
|
const isLongFormVideo = isLongFormNip71VideoEventKind(event.kind) |
|
const isVideo = |
|
(!isPictureKind && first?.m?.startsWith('video/')) || |
|
(!isPictureKind && isNip71StyleVideoKind(event.kind)) |
|
const musicTrack = isMusicTrackKind(event.kind) ? getMusicTrackFromEvent(event) : null |
|
const isAudio = |
|
first?.m?.startsWith('audio/') || |
|
event.kind === ExtendedKind.VOICE || |
|
musicTrack != null |
|
const hasMultiple = media.all.length > 1 |
|
|
|
// For videos prefer the poster image; long-form feed tiles never prefetch the .mp4 (open note to play). |
|
const displayUrl = isVideo |
|
? isLongFormVideo |
|
? (first?.image ?? first?.thumb) |
|
: (first?.image ?? first?.url) |
|
: musicTrack?.imageUrl ?? first?.thumb ?? first?.url |
|
|
|
const handleClick = () => { |
|
client.addEventToCache(event) |
|
navigateToNote(toNote(event), event, getCachedThreadContextEvents(event)) |
|
} |
|
|
|
return ( |
|
<div |
|
className="relative aspect-square cursor-pointer overflow-hidden bg-muted" |
|
onClick={handleClick} |
|
> |
|
{displayUrl ? ( |
|
isVideo && !isLongFormVideo && !(first?.image ?? first?.thumb) && first?.url ? ( |
|
<video |
|
src={first.url} |
|
className="h-full w-full object-cover" |
|
muted |
|
preload="metadata" |
|
/> |
|
) : ( |
|
<img |
|
src={displayUrl} |
|
alt={first?.alt ?? ''} |
|
className="h-full w-full object-cover" |
|
loading="lazy" |
|
/> |
|
) |
|
) : ( |
|
<div className="flex h-full w-full items-center justify-center text-muted-foreground/40"> |
|
{isAudio ? ( |
|
<Music className="size-8" /> |
|
) : isVideo ? ( |
|
<Play className="size-8" /> |
|
) : ( |
|
<ImageIcon className="size-8" /> |
|
)} |
|
</div> |
|
)} |
|
|
|
{/* Top-right badge */} |
|
{isVideo && ( |
|
<div className="absolute right-2 top-2 rounded bg-black/60 p-1"> |
|
<Play className="size-6 fill-white text-white" /> |
|
</div> |
|
)} |
|
{isAudio && ( |
|
<div className="absolute right-2 top-2 rounded bg-black/60 p-1"> |
|
<Music className="size-6 text-white" /> |
|
</div> |
|
)} |
|
{hasMultiple && !isVideo && !isAudio && ( |
|
<div className="absolute right-2 top-2 rounded bg-black/60 p-1"> |
|
<Images className="size-6 text-white" /> |
|
</div> |
|
)} |
|
</div> |
|
) |
|
}
|
|
|