diff --git a/src/element/new-stream.css b/src/element/new-stream.css index e3ad1a1..b46fd51 100644 --- a/src/element/new-stream.css +++ b/src/element/new-stream.css @@ -1,3 +1,4 @@ + .new-stream { display: flex; flex-direction: column; @@ -9,11 +10,6 @@ margin: 0; } -.new-stream div.paper { - background: #262626; - height: 32px; -} - .new-stream p { margin: 0 0 8px 0; } @@ -23,14 +19,15 @@ margin: 8px 0 0 0; } -.new-stream .btn { +.new-stream .btn.wide { padding: 12px 16px; border-radius: 16px; width: 100%; } -.new-stream .btn>span { - justify-content: center; +.new-stream div.paper { + background: #262626; + height: 32px; } .new-stream .btn:disabled { @@ -48,4 +45,4 @@ .new-stream .pill.active { color: inherit; background: #353535; -} +} \ No newline at end of file diff --git a/src/element/new-stream.tsx b/src/element/new-stream.tsx index 27da545..45fc7b8 100644 --- a/src/element/new-stream.tsx +++ b/src/element/new-stream.tsx @@ -1,196 +1,75 @@ import "./new-stream.css"; import * as Dialog from "@radix-ui/react-dialog"; -import { useEffect, useState, useCallback } from "react"; -import { EventPublisher, NostrEvent } from "@snort/system"; -import { unixNow } from "@snort/shared"; - -import AsyncButton from "./async-button"; -import { StreamState, System } from "index"; import { Icon } from "element/icon"; -import { findTag } from "utils"; +import { useStreamProvider } from "hooks/stream-provider"; +import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers"; +import { useEffect, useState } from "react"; +import { StreamEditor, StreamEditorProps } from "./stream-editor"; +import { useNavigate } from "react-router-dom"; +import { eventLink } from "utils"; +import { NostrProviderDialog } from "./nostr-provider-dialog"; -export function NewStream({ - ev, - onFinish, -}: { - ev?: NostrEvent; - onFinish?: (ev: NostrEvent) => void; -}) { - const [title, setTitle] = useState(findTag(ev, "title") ?? ""); - const [summary, setSummary] = useState(findTag(ev, "summary") ?? ""); - const [image, setImage] = useState(findTag(ev, "image") ?? ""); - const [stream, setStream] = useState(findTag(ev, "streaming") ?? ""); - const [status, setStatus] = useState( - findTag(ev, "status") ?? StreamState.Live - ); - const [start, setStart] = useState(findTag(ev, "starts")); - const [isValid, setIsValid] = useState(false); - - const validate = useCallback(() => { - if (title.length < 2) { - return false; - } - if (stream.length < 5 || !stream.match(/^https?:\/\/.*\.m3u8?$/i)) { - return false; - } - if (image.length > 0 && !image.match(/^https?:\/\//i)) { - return false; - } - return true; - }, [title, image, stream]); +function NewStream({ ev, onFinish }: StreamEditorProps) { + const providers = useStreamProvider(); + const [currentProvider, setCurrentProvider] = useState(); + const navigate = useNavigate(); useEffect(() => { - setIsValid(validate()); - }, [validate, title, summary, image, stream]); + if (!currentProvider) { + setCurrentProvider(providers.at(0)); + } + }, [providers, currentProvider]); - async function publishStream() { - const pub = await EventPublisher.nip7(); - if (pub) { - 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(30_311) - .tag(["d", dTag]) - .tag(["title", title]) - .tag(["summary", summary]) - .tag(["image", image]) - .tag(["streaming", stream]) - .tag(["status", status]) - .tag(["starts", starts]); - if (status === StreamState.Ended) { - eb.tag(["ends", ends]); - } - return eb; - }); - console.debug(evNew); - System.BroadcastEvent(evNew); - onFinish && onFinish(evNew); + + + function providerDialog() { + if (!currentProvider) return; + + switch (currentProvider.type) { + case StreamProviders.Manual: { + return { + currentProvider.updateStreamInfo(ex); + if (!ev) { + navigate(eventLink(ex)); + } else { + onFinish?.(ev); + } + }} ev={ev} /> + } + case StreamProviders.NostrType: { + return + } + case StreamProviders.Owncast: { + return + } } } - function toDateTimeString(n: number) { - return new Date(n * 1000).toISOString().substring(0, -1); - } - - function fromDateTimeString(s: string) { - return Math.floor(new Date(s).getTime() / 1000); - } - - return ( -
-

{ev ? "Edit Stream" : "New Stream"}

-
-

Title

-
- setTitle(e.target.value)} - /> -
-
-
-

Summary

-
- setSummary(e.target.value)} - /> -
-
-
-

Cover image

-
- setImage(e.target.value)} - /> -
-
-
-

Stream Url

-
- setStream(e.target.value)} - /> -
- Stream type should be HLS -
-
-

Status

-
- {[StreamState.Live, StreamState.Planned, StreamState.Ended].map( - (v) => ( - setStatus(v)} - > - {v} - - ) - )} -
-
- {status === StreamState.Planned && ( -
-

Start Time

-
- - setStart(fromDateTimeString(e.target.value).toString()) - } - /> -
-
- )} -
- - {ev ? "Save" : "Start Stream"} - -
+ return <> +

Stream Providers

+
+ {providers.map(v => setCurrentProvider(v)}>{v.name})}
- ); + {providerDialog()} + } interface NewStreamDialogProps { text?: string; btnClassName?: string; - ev?: NostrEvent; - onFinish?: (e: NostrEvent) => void; } -export function NewStreamDialog({ - text, - ev, - onFinish, - btnClassName = "btn", -}: NewStreamDialogProps) { +export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps) { + const [open, setOpen] = useState(false); return ( - + - +
+ +
+

Balance

+
+
+ {info.balance?.toLocaleString()} sats +
+ +
+
+ {streamEvent && { + provider.updateStreamInfo(ex); + others.onFinish?.(ex); + }} ev={streamEvent} options={{ + canSetStream: false, + canSetStatus: false + }} />} + +} \ No newline at end of file diff --git a/src/element/send-zap.tsx b/src/element/send-zap.tsx index 27aa427..530b9d4 100644 --- a/src/element/send-zap.tsx +++ b/src/element/send-zap.tsx @@ -9,8 +9,15 @@ import AsyncButton from "./async-button"; import { Relays } from "index"; import QrCode from "./qr-code"; -interface SendZapsProps { - lnurl: string; +export interface LNURLLike { + get name(): string; + get maxCommentLength(): number; + get canZap(): boolean; + getInvoice(amountInSats: number, comment?: string, zap?: NostrEvent): Promise<{ pr?: string }> +} + +export interface SendZapsProps { + lnurl: string | LNURLLike; pubkey?: string; aTag?: string; eTag?: string; @@ -19,7 +26,7 @@ interface SendZapsProps { button?: ReactNode; } -function SendZaps({ +export function SendZaps({ lnurl, pubkey, aTag, @@ -34,13 +41,13 @@ function SendZaps({ ]; const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200]; const [isFiat, setIsFiat] = useState(false); - const [svc, setSvc] = useState(); + const [svc, setSvc] = useState(); const [amount, setAmount] = useState(satsAmounts[0]); const [comment, setComment] = useState(""); const [invoice, setInvoice] = useState(""); const name = targetName ?? svc?.name; - async function loadService() { + async function loadService(lnurl: string) { const s = new LNURL(lnurl); await s.load(); setSvc(s); @@ -48,7 +55,11 @@ function SendZaps({ useEffect(() => { if (!svc) { - loadService().catch(console.warn); + if (typeof lnurl === "string") { + loadService(lnurl).catch(console.warn); + } else { + setSvc(lnurl); + } } }, [lnurl]); @@ -127,7 +138,7 @@ function SendZaps({ ))} -
+ {svc && (svc.maxCommentLength > 0 || svc.canZap) &&
Your comment for {name}