import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID, StreamState } from "@/const" import { useCurrentStreamFeed } from "@/hooks/current-stream-feed" import { formatSats } from "@/number" import { extractStreamInfo } from "@/utils" import { unixNow } from "@snort/shared" import { NostrLink, type NostrEvent, type ParsedZap, EventKind, type TaggedNostrEvent } from "@snort/system" import { useEventReactions, useReactions } from "@snort/system-react" import { useMemo } from "react" import { FormattedMessage, FormattedNumber, FormattedDate } from "react-intl" import { ResponsiveContainer, BarChart, XAxis, YAxis, Bar, Tooltip } from "recharts" import { Profile } from "./profile" import { StatePill } from "./state-pill" import { Link } from "react-router" import { Icon } from "./icon" import EventReactions from "./event-reactions" interface StatSlot { time: number zaps: number messages: number reactions: number clips: number raids: number shares: number } export default function StreamSummary({ link, preload }: { link: NostrLink; preload?: NostrEvent }) { const ev = useCurrentStreamFeed(link, true, preload ? ({ ...preload, relays: [] } as TaggedNostrEvent) : undefined) const thisLink = ev ? NostrLink.fromEvent(ev) : undefined const data = useReactions( `live:${link?.id}:${link?.author}:reactions`, thisLink ? [thisLink] : [], rb => { if (thisLink) { rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([thisLink]) } }, true, ) const reactions = useEventReactions(thisLink ?? link, data) const chatSummary = useMemo(() => { return Object.entries( data .filter(a => a.kind === LIVE_STREAM_CHAT) .reduce( (acc, v) => { acc[v.pubkey] ??= [] acc[v.pubkey].push(v) return acc }, {} as Record>, ), ) .map(([k, v]) => ({ pubkey: k, messages: v, })) .sort((a, b) => (a.messages.length > b.messages.length ? -1 : 1)) }, [data]) const zapsSummary = useMemo(() => { return Object.entries( reactions.zaps.reduce( (acc, v) => { if (!v.sender) return acc acc[v.sender] ??= [] acc[v.sender].push(v) return acc }, {} as Record>, ), ) .map(([k, v]) => ({ pubkey: k, zaps: v, total: v.reduce((acc, vv) => acc + vv.amount, 0), })) .sort((a, b) => (a.total > b.total ? -1 : 1)) }, [reactions.zaps]) const totalZaps = useMemo(() => { return reactions.zaps.reduce((acc, v) => { return acc + v.amount }, 0) }, [reactions.zaps]) const { title, summary, status, starts } = extractStreamInfo(ev) const Day = 60 * 60 * 24 const startTime = starts ? Number(starts) : (ev?.created_at ?? unixNow()) const endTime = status === StreamState.Live ? unixNow() : (ev?.created_at ?? unixNow()) const streamLength = endTime - startTime const windowSize = streamLength > Day ? Day : 60 * 10 const stats = useMemo(() => { let min = unixNow() let max = 0 const ret = data .sort((a, b) => (a.created_at > b.created_at ? -1 : 1)) .filter(a => a.created_at >= startTime && a.created_at < endTime) .reduce( (acc, v) => { const time = Math.floor(v.created_at - (v.created_at % windowSize)) if (time < min) { min = time } if (time > max) { max = time } const key = time.toString() acc[key] ??= { time, zaps: 0, messages: 0, reactions: 0, clips: 0, raids: 0, shares: 0, } if (v.kind === LIVE_STREAM_CHAT) { acc[key].messages++ } else if (v.kind === EventKind.ZapReceipt) { acc[key].zaps++ } else if (v.kind === EventKind.Reaction) { acc[key].reactions++ } else if (v.kind === EventKind.TextNote) { acc[key].shares++ } else if (v.kind === LIVE_STREAM_CLIP) { acc[key].clips++ } else if (v.kind === LIVE_STREAM_RAID) { acc[key].raids++ } else { console.debug("Uncounted stat", v) } return acc }, {} as Record, ) // fill empty time slots for (let x = min; x < max; x += windowSize) { ret[x.toString()] ??= { time: x, zaps: 0, messages: 0, reactions: 0, clips: 0, raids: 0, shares: 0, } } return ret }, [data]) return (

{title}

{summary &&

{summary}

}
{streamLength > 0 && ( , }} /> )}

{ if (active && payload && payload.length) { const data = payload[0].payload as StatSlot return (
{data.messages}
{data.reactions}
{data.zaps}
{data.clips}
{data.raids}
{data.shares}
) } return null }} />

{chatSummary.slice(0, 5).map(a => (
, }} />
))}

), }} />
{zapsSummary.slice(0, 5).map(a => (
b.anonZap) ? "anon" : a.pubkey} avatarSize={30} />
))}

{data .filter(a => a.kind === LIVE_STREAM_RAID) .map(a => { const mins = a.created_at - startTime return (
) })}

{data .filter(a => a.kind === EventKind.TextNote) .map(a => ( ))}

{data .filter(a => a.kind === LIVE_STREAM_CLIP) .map(a => { const link = NostrLink.fromEvent(a)! return (
) })}
) } function SharedNote({ ev }: { ev: TaggedNostrEvent }) { return (
{ev.content}
) }