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.
 
 
 
 

136 lines
4.4 KiB

import AudioPlayer from '@/components/AudioPlayer'
import {
getMusicTrackFromEvent,
musicTrackCaptionContent,
musicTrackDisplayLine,
musicTrackMetaLine
} from '@/lib/music-track'
import { primalR2aMirrorForBlossomPrimalUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import MarkdownArticle from './MarkdownArticle/MarkdownArticle'
import MediaPlayer from '../MediaPlayer'
/** Tags already shown on the music card — omit from caption markdown so they are not rendered twice. */
const MUSIC_TRACK_CAPTION_OMIT_TAGS = new Set([
'd',
'title',
'artist',
'url',
'image',
'video',
'album',
'duration',
'format',
'language',
'track_number',
'released',
'explicit',
'alt',
'genre'
])
export default function MusicTrackNote({
event,
className,
loadMedia = false
}: {
event: Event
className?: string
loadMedia?: boolean
}) {
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey)
const mustLoad = loadMedia || autoLoadMedia
const { t } = useTranslation()
const track = useMemo(() => getMusicTrackFromEvent(event), [event])
const metaLine = useMemo(() => (track ? musicTrackMetaLine(track) : ''), [track])
const caption = useMemo(
() => (track ? musicTrackCaptionContent(event.content, track) : null),
[event.content, track]
)
const audioFallbackSrc = useMemo(
() => (track ? primalR2aMirrorForBlossomPrimalUrl(track.audioUrl) ?? undefined : undefined),
[track]
)
const captionEvent = useMemo(() => {
if (!caption) return null
const tags = event.tags.filter(([name]) => !MUSIC_TRACK_CAPTION_OMIT_TAGS.has(name))
return { ...event, content: caption, tags } as Event
}, [event, caption])
if (!track) {
return (
<p className={cn('text-sm text-muted-foreground', className)}>
{t('Invalid music track event', { defaultValue: 'Invalid music track event' })}
</p>
)
}
return (
<div className={cn('min-w-0', className)}>
<div className="not-prose w-full max-w-[400px] overflow-hidden rounded-lg border border-border bg-card shadow-sm">
<div className="flex gap-3 p-3">
{track.imageUrl ? (
<img
src={track.imageUrl}
alt=""
className="size-16 shrink-0 rounded-md object-cover shadow-sm"
loading="lazy"
referrerPolicy="no-referrer"
draggable={false}
/>
) : null}
<div className="min-w-0 flex-1">
<p className="line-clamp-2 text-sm font-semibold leading-snug">{track.title}</p>
{track.artist ? (
<p className="mt-0.5 line-clamp-1 text-xs text-muted-foreground">{track.artist}</p>
) : null}
{metaLine ? (
<p className="mt-0.5 line-clamp-2 text-[11px] text-muted-foreground">{metaLine}</p>
) : null}
</div>
</div>
<div className="border-t border-border px-2 pb-2.5 pt-1.5">
<AudioPlayer
src={track.audioUrl}
fallbackSrc={audioFallbackSrc}
className="w-full max-w-none"
/>
</div>
{track.videoUrl ? (
<div className="border-t border-border px-2 pb-2 pt-1">
<p className="mb-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{t('Music video', { defaultValue: 'Music video' })}
</p>
<MediaPlayer
src={track.videoUrl}
className="w-full max-w-none"
mustLoad={mustLoad}
authorPubkey={event.pubkey}
/>
</div>
) : null}
</div>
{captionEvent ? (
<div className="mt-2 min-w-0 text-sm text-muted-foreground">
<MarkdownArticle
event={captionEvent}
hideMetadata
lazyMedia={!mustLoad}
parentImageUrl={track.imageUrl}
className="prose-sm prose-headings:text-muted-foreground prose-p:text-muted-foreground"
/>
</div>
) : null}
</div>
)
}
export function musicTrackPreviewText(event: Event): string {
const track = getMusicTrackFromEvent(event)
return track ? musicTrackDisplayLine(track) : event.tags.find((t) => t[0] === 'title')?.[1] ?? ''
}