From 345c1d58bf8ffaf8ddce66da54a777539ffa3c89 Mon Sep 17 00:00:00 2001 From: Kieran Date: Tue, 12 Mar 2024 22:42:58 +0000 Subject: [PATCH] fix: manual streams --- src/element/stream-editor/index.tsx | 55 +++++++++++---- src/element/summary-chart.tsx | 4 +- src/hooks/login.ts | 5 +- src/login.ts | 11 ++- src/pages/dashboard/column-zaps.tsx | 2 +- src/pages/dashboard/dashboard.tsx | 97 ++++++++++++++++++++------- src/pages/dashboard/intro/step4.tsx | 2 +- src/pages/dashboard/manual-stream.tsx | 39 +++++++++++ 8 files changed, 169 insertions(+), 46 deletions(-) create mode 100644 src/pages/dashboard/manual-stream.tsx diff --git a/src/element/stream-editor/index.tsx b/src/element/stream-editor/index.tsx index 8cbac0b..87b3083 100644 --- a/src/element/stream-editor/index.tsx +++ b/src/element/stream-editor/index.tsx @@ -1,20 +1,21 @@ import "./index.css"; -import { useCallback, useEffect, useState } from "react"; -import { NostrEvent } from "@snort/system"; +import { useCallback, useContext, useEffect, useState } from "react"; +import { EventKind, NostrEvent } from "@snort/system"; import { unixNow } from "@snort/shared"; import { FormattedMessage, useIntl } from "react-intl"; +import { SnortContext } from "@snort/system-react"; import { extractStreamInfo, findTag } from "@/utils"; import { useLogin } from "@/hooks/login"; -import { StreamState } from "@/const"; +import { GOAL, StreamState, defaultRelays } from "@/const"; import { DefaultButton } from "@/element/buttons"; import Pill from "@/element/pill"; - -import { NewGoalDialog } from "./new-goal"; import { StreamInput } from "./input"; import { GoalSelector } from "./goal-selector"; import GameDatabase, { GameInfo } from "@/service/game-database"; import CategoryInput from "./category-input"; +import { FileUploader } from "@/element/file-uploader"; +import AmountInput from "@/element/amount-input"; export interface StreamEditorProps { ev?: NostrEvent; @@ -42,10 +43,13 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { const [contentWarning, setContentWarning] = useState(false); const [isValid, setIsValid] = useState(false); const [goal, setGoal] = useState(); + const [goalName, setGoalName] = useState(""); + const [goalAmount, setGoalMount] = useState(0); const [game, setGame] = useState(); const [gameId, setGameId] = useState(); const login = useLogin(); const { formatMessage } = useIntl(); + const system = useContext(SnortContext); useEffect(() => { const { gameInfo, gameId, title, summary, image, stream, status, starts, tags, contentWarning, goal, recording } = @@ -88,12 +92,24 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { async function publishStream() { const pub = login?.publisher(); if (pub) { + let thisGoal = goal; + if (!goal && goalName && goalAmount) { + const goalEvent = await pub.generic(eb => { + return eb + .kind(GOAL) + .tag(["amount", String(goalAmount * 1000)]) + .tag(["relays", ...Object.keys(defaultRelays)]) + .content(goalName); + }); + await system.BroadcastEvent(goalEvent); + thisGoal = goalEvent.id; + } const evNew = await pub.generic(eb => { const now = unixNow(); const dTag = findTag(ev, "d") ?? now.toString(); const starts = start ?? now.toString(); const ends = findTag(ev, "ends") ?? now.toString(); - eb.kind(30311) + eb.kind(EventKind.LiveEvent) .tag(["d", dTag]) .tag(["title", title]) .tag(["summary", summary]) @@ -115,8 +131,8 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { if (contentWarning) { eb.tag(["content-warning", "nsfw"]); } - if (goal && goal.length > 0) { - eb.tag(["goal", goal]); + if (thisGoal && thisGoal.length > 0) { + eb.tag(["goal", thisGoal]); } if (gameId) { eb.tag(["t", gameId]); @@ -161,11 +177,10 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { )} {(options?.canSetImage ?? true) && ( }> + {image && }
setImage(e.target.value)} /> - - - + setImage(v ?? "")} />
)} @@ -180,9 +195,9 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { {(options?.canSetStatus ?? true) && ( <> }> -
+
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(v => ( - setStatus(v)} key={v}> + setStatus(v)} key={v}> {v} ))} @@ -218,7 +233,19 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) { }>
- + {!goal && ( +
+ setGoalName(e.target.value)} + /> + +
+ )}
)} diff --git a/src/element/summary-chart.tsx b/src/element/summary-chart.tsx index 9fd18ea..3b89c9a 100644 --- a/src/element/summary-chart.tsx +++ b/src/element/summary-chart.tsx @@ -25,7 +25,7 @@ interface StatSlot { } export default function StreamSummary({ link, preload }: { link: NostrLink; preload?: NostrEvent }) { - const ev = useCurrentStreamFeed(link, true, preload); + 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`, @@ -148,7 +148,7 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel return (
-

{title}

+

{title}

{summary &&

{summary}

}
diff --git a/src/hooks/login.ts b/src/hooks/login.ts index e8f2cf9..1f2ec94 100644 --- a/src/hooks/login.ts +++ b/src/hooks/login.ts @@ -6,7 +6,7 @@ import { useRequestBuilder } from "@snort/system-react"; import { useUserEmojiPacks } from "@/hooks/emoji"; import { MUTED, USER_CARDS, USER_EMOJIS } from "@/const"; import type { Tags } from "@/types"; -import { getPublisher, Login } from "@/login"; +import { getPublisher, getSigner, Login } from "@/login"; export function useLogin() { const session = useSyncExternalStore( @@ -19,6 +19,9 @@ export function useLogin() { publisher: () => { return getPublisher(session); }, + signer: () => { + return getSigner(session); + }, }; } diff --git a/src/login.ts b/src/login.ts index 92ef5db..ca4707f 100644 --- a/src/login.ts +++ b/src/login.ts @@ -132,12 +132,19 @@ export class LoginStore extends ExternalStore { } export function getPublisher(session: LoginSession) { + const signer = getSigner(session); + if (signer) { + return new EventPublisher(signer, session.pubkey); + } +} + +export function getSigner(session: LoginSession) { switch (session?.type) { case LoginType.Nip7: { - return new EventPublisher(new Nip7Signer(), session.pubkey); + return new Nip7Signer(); } case LoginType.PrivateKey: { - return new EventPublisher(new PrivateKeySigner(unwrap(session.privateKey)), session.pubkey); + return new PrivateKeySigner(unwrap(session.privateKey)); } } } diff --git a/src/pages/dashboard/column-zaps.tsx b/src/pages/dashboard/column-zaps.tsx index 5e7b190..9b646ec 100644 --- a/src/pages/dashboard/column-zaps.tsx +++ b/src/pages/dashboard/column-zaps.tsx @@ -26,7 +26,7 @@ export function DashboardZapColumn({ const zapSum = sortedZaps.reduce((acc, v) => acc + v.amount, 0); return ( -
+
diff --git a/src/pages/dashboard/dashboard.tsx b/src/pages/dashboard/dashboard.tsx index 7997bd5..797f2fd 100644 --- a/src/pages/dashboard/dashboard.tsx +++ b/src/pages/dashboard/dashboard.tsx @@ -2,9 +2,9 @@ import { LiveChat } from "@/element/live-chat"; import LiveVideoPlayer from "@/element/live-video-player"; import { useCurrentStreamFeed } from "@/hooks/current-stream-feed"; import { extractStreamInfo } from "@/utils"; -import { NostrLink } from "@snort/system"; -import { useReactions } from "@snort/system-react"; -import { useEffect, useMemo, useState } from "react"; +import { EventExt, NostrEvent, NostrLink } from "@snort/system"; +import { SnortContext, useReactions } from "@snort/system-react"; +import { Suspense, lazy, useContext, useEffect, useMemo, useState } from "react"; import { FormattedMessage, FormattedNumber } from "react-intl"; import { StreamTimer } from "@/element/stream-time"; import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP, StreamState } from "@/const"; @@ -21,9 +21,13 @@ import StreamKey from "@/element/provider/nostr/stream-key"; import { DefaultProvider, NostrStreamProvider, StreamProviderInfo } from "@/providers"; import { ExternalLink } from "@/element/external-link"; import BalanceTimeEstimate from "@/element/balance-time-estimate"; -import { DefaultButton } from "@/element/buttons"; +import { WarningButton } from "@/element/buttons"; import { useLogin } from "@/hooks/login"; import AccountTopup from "@/element/provider/nostr/topup"; +import classNames from "classnames"; +import ManualStream from "./manual-stream"; +import { unixNow } from "@snort/shared"; +const StreamSummary = lazy(() => import("@/element/summary-chart")); export function DashboardForLink({ link }: { link: NostrLink }) { const streamEvent = useCurrentStreamFeed(link, true); @@ -32,6 +36,8 @@ export function DashboardForLink({ link }: { link: NostrLink }) { const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined; const { stream, status, image, participants, service } = extractStreamInfo(streamEvent); const [info, setInfo] = useState(); + const isMyManual = streamEvent?.pubkey === login?.pubkey; + const system = useContext(SnortContext); const provider = useMemo(() => (service ? new NostrStreamProvider("", service) : DefaultProvider), [service]); const defaultEndpoint = useMemo(() => { @@ -39,14 +45,16 @@ export function DashboardForLink({ link }: { link: NostrLink }) { }, [info]); useEffect(() => { - provider.info().then(setInfo); - const t = setInterval(() => { + if (!isMyManual) { provider.info().then(setInfo); - }, 1000 * 60); - return () => { - clearInterval(t); - }; - }, [provider]); + const t = setInterval(() => { + provider.info().then(setInfo); + }, 1000 * 60); + return () => { + clearInterval(t); + }; + } + }, [isMyManual, provider]); const [maxParticipants, setMaxParticipants] = useState(0); useEffect(() => { @@ -69,7 +77,11 @@ export function DashboardForLink({ link }: { link: NostrLink }) { if (!streamLink && !location.search.includes("setupComplete=true")) return ; return ( -
+
@@ -88,7 +100,7 @@ export function DashboardForLink({ link }: { link: NostrLink }) { )}
- {streamLink && ( + {streamLink && status === StreamState.Live && !isMyManual && ( <>
@@ -129,14 +141,42 @@ export function DashboardForLink({ link }: { link: NostrLink }) { } />
- {streamEvent?.pubkey === login?.pubkey && ( - - - - )} )} - {!streamLink && ( + {streamLink && isMyManual && status === StreamState.Live && ( + <> + +
+ + } /> + { + //todo: clean this up + const copy = streamEvent ? ({ ...streamEvent } as NostrEvent) : undefined; + const statusTag = copy?.tags.find(a => a[0] === "status"); + const endedTag = copy?.tags.find(a => a[0] === "ends"); + const pub = login?.signer(); + if (statusTag && copy && pub) { + statusTag[1] = StreamState.Ended; + if (endedTag) { + endedTag[1] = String(unixNow()); + } else { + copy.tags.push(["ends", String(unixNow())]); + } + copy.created_at = unixNow(); + copy.id = EventExt.createId(copy); + const evPub = await pub.sign(copy); + if (evPub) { + await system.BroadcastEvent(evPub); + } + } + }}> + + +
+ + )} + {(!streamLink || status === StreamState.Ended) && ( <>
@@ -155,11 +195,12 @@ export function DashboardForLink({ link }: { link: NostrLink }) { />

{defaultEndpoint && } +
)} - {streamLink && ( + {streamLink && status === StreamState.Live && (

@@ -170,16 +211,22 @@ export function DashboardForLink({ link }: { link: NostrLink }) { )}

- {streamLink && ( + {streamLink && status === StreamState.Live && ( <> - + )} - {!streamLink && ( + {streamLink && status === StreamState.Ended && ( <> - - + +

+ +

+ + + +
)}
diff --git a/src/pages/dashboard/intro/step4.tsx b/src/pages/dashboard/intro/step4.tsx index 2d2feaf..194c70a 100644 --- a/src/pages/dashboard/intro/step4.tsx +++ b/src/pages/dashboard/intro/step4.tsx @@ -58,7 +58,7 @@ export default function DashboardIntroStep4() { { const pub = login?.publisher(); - if (!goal && pub) { + if (!goal && pub && goalName && goalAmount) { const goalEvent = await pub.generic(eb => { return eb .kind(GOAL) diff --git a/src/pages/dashboard/manual-stream.tsx b/src/pages/dashboard/manual-stream.tsx new file mode 100644 index 0000000..a809bdf --- /dev/null +++ b/src/pages/dashboard/manual-stream.tsx @@ -0,0 +1,39 @@ +import Modal from "@/element/modal"; +import { StreamEditor } from "@/element/stream-editor"; +import { ManualProvider } from "@/providers/manual"; +import { NostrLink } from "@snort/system"; +import { SnortContext } from "@snort/system-react"; +import { useContext, useState } from "react"; +import { FormattedMessage } from "react-intl"; +import { useNavigate } from "react-router-dom"; + +export default function ManualStream() { + const [open, setOpen] = useState(false); + const provider = new ManualProvider(); + const system = useContext(SnortContext); + const navigate = useNavigate(); + + return ( + <> +
setOpen(true)}> + +
+ {open && ( + setOpen(false)}> +
+ { + provider.updateStreamInfo(system, ex); + if (!ex) { + navigate(`/${NostrLink.fromEvent(ex).encode()}`, { + state: ex, + }); + } + }} + /> +
+
+ )} + + ); +}