import { NostrPrefix, dedupe, removeUndefined, sanitizeRelayUrl } from "@snort/shared" import { EventKind, type NostrEvent, NostrLink, RequestBuilder, type TaggedNostrEvent } from "@snort/system" import { useEventReactions, useRequestBuilder } from "@snort/system-react" import { type ReactNode, createContext, useContext, useMemo, useState } from "react" import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID } from "@/const" import { useCurrentStreamFeed } from "@/hooks/current-stream-feed" import { useZapGoal } from "@/hooks/goals" import { type StreamInfo, extractStreamInfo } from "@/utils" interface StreamState { link?: NostrLink event?: NostrEvent goal?: NostrEvent relays: string[] showDetails: boolean info?: StreamInfo feed: Array reactions: ReturnType update: (fn: (c: StreamState) => StreamState) => void } const StreamContext = createContext(undefined) export function StreamContextProvider({ children, link, evPreload, }: { children?: ReactNode link: NostrLink evPreload?: TaggedNostrEvent }) { const event = useCurrentStreamFeed(link, true, evPreload) const [state, setState] = useState({ link, showDetails: false, relays: link.relays ?? [], feed: [], reactions: { deletions: [], reactions: { all: [], positive: [], negative: [], }, replies: [], reposts: [], zaps: [], others: {}, }, update: () => {}, }) // Extract goal tag from stream event and load the goal const info = extractStreamInfo(event) const goal = useZapGoal(info.goal) // Extract relays from stream event const relays = useMemo(() => { return dedupe(removeUndefined(event?.tags.filter(a => a[0] === "relays").map(a => sanitizeRelayUrl(a[1])) ?? [])) }, [event?.id]) const eventLink = useMemo( () => event ? NostrLink.fromEvent(event) : link.type === NostrPrefix.Address || link.type === NostrPrefix.Event ? link : undefined, [event?.id, link], ) const goalLink = useMemo(() => (goal ? NostrLink.fromEvent(goal) : null), [goal?.id]) const rb = useMemo(() => { const rb = new RequestBuilder(`stream:${link.tagKey}`) rb.withOptions({ leaveOpen: true, }) if (eventLink) { rb.withFilter() .kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]) .limit(200) .replyToLink([eventLink]) .relay(relays) rb.withFilter().kinds([EventKind.ZapReceipt]).replyToLink([eventLink]).relay(relays) } else if (link.type === NostrPrefix.PublicKey || link.type === NostrPrefix.Profile) { rb.withFilter() .kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]) .authors([link.id]) .limit(200) .relay(relays) rb.withFilter().kinds([EventKind.ZapReceipt]).authors([link.id]).relay(relays) } else if (link.type === NostrPrefix.Address || link.type === NostrPrefix.Event) { const authorTag = link.tags.find(t => t[0] === "p") if (authorTag) { rb.withFilter() .kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]) .authors([authorTag[1]]) .limit(200) .relay(relays) rb.withFilter().kinds([EventKind.ZapReceipt]).authors([authorTag[1]]).relay(relays) } else { rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).limit(200).relay(relays) rb.withFilter().kinds([EventKind.ZapReceipt]).limit(200).relay(relays) } } if (goalLink) { rb.withFilter().kinds([EventKind.ZapReceipt]).replyToLink([goalLink]).relay(relays) } return rb }, [link, eventLink, goalLink, relays]) const feed = useRequestBuilder(rb) const reactions = useEventReactions(eventLink ?? link, feed) const streamState = { ...state, link: eventLink, event, goal, relays, feed, info, reactions, update: (fn: (c: StreamState) => StreamState) => { setState(x => { return { ...x, ...fn(x), } }) }, } return {children} } export function useStream() { const ctx = useContext(StreamContext) if (!ctx) { throw new Error("Context doesnt exist!") } return ctx }