Nostr stream provider init
This commit is contained in:
@ -1,196 +1,118 @@
|
||||
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";
|
||||
|
||||
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);
|
||||
function NewStream({ ev, onFinish }: StreamEditorProps) {
|
||||
const providers = useStreamProvider();
|
||||
const [currentProvider, setCurrentProvider] = useState<StreamProvider>();
|
||||
const [info, setInfo] = useState<StreamProviderInfo>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
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]);
|
||||
async function loadInfo(p: StreamProvider) {
|
||||
const inf = await p.info();
|
||||
setInfo(inf);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsValid(validate());
|
||||
}, [validate, title, summary, image, stream]);
|
||||
|
||||
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);
|
||||
if (!currentProvider) {
|
||||
setCurrentProvider(providers.at(0));
|
||||
}
|
||||
}
|
||||
if (currentProvider) {
|
||||
loadInfo(currentProvider).catch(console.error);
|
||||
}
|
||||
}, [providers, currentProvider]);
|
||||
|
||||
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 (
|
||||
<div className="new-stream">
|
||||
<h3>{ev ? "Edit Stream" : "New Stream"}</h3>
|
||||
<div>
|
||||
<p>Title</p>
|
||||
<div className="paper">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="What are we steaming today?"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Summary</p>
|
||||
<div className="paper">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="A short description of the content"
|
||||
value={summary}
|
||||
onChange={(e) => setSummary(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Cover image</p>
|
||||
<div className="paper">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://"
|
||||
value={image}
|
||||
onChange={(e) => setImage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
function nostrTypeDialog(p: StreamProviderInfo) {
|
||||
return <>
|
||||
<div>
|
||||
<p>Stream Url</p>
|
||||
<div className="paper">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://"
|
||||
value={stream}
|
||||
onChange={(e) => setStream(e.target.value)}
|
||||
/>
|
||||
<input type="text" value={p.ingressUrl} disabled />
|
||||
</div>
|
||||
<small>Stream type should be HLS</small>
|
||||
</div>
|
||||
<div>
|
||||
<p>Status</p>
|
||||
<p>Stream Key</p>
|
||||
<div className="paper">
|
||||
<input type="password" value={p.ingressKey} disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Balance</p>
|
||||
<div className="flex g12">
|
||||
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(
|
||||
(v) => (
|
||||
<span
|
||||
className={`pill${status === v ? " active" : ""}`}
|
||||
onClick={() => setStatus(v)}
|
||||
>
|
||||
{v}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{status === StreamState.Planned && (
|
||||
<div>
|
||||
<p>Start Time</p>
|
||||
<div className="input">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={toDateTimeString(Number(start ?? "0"))}
|
||||
onChange={(e) =>
|
||||
setStart(fromDateTimeString(e.target.value).toString())
|
||||
}
|
||||
/>
|
||||
<div className="paper f-grow">
|
||||
{p.balance?.toLocaleString()} sats
|
||||
</div>
|
||||
<button className="btn btn-primary">
|
||||
Topup
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<AsyncButton
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={!isValid}
|
||||
onClick={publishStream}
|
||||
>
|
||||
{ev ? "Save" : "Start Stream"}
|
||||
</AsyncButton>
|
||||
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function providerDialog(p: StreamProviderInfo) {
|
||||
switch (p.type) {
|
||||
case StreamProviders.Manual: {
|
||||
return <StreamEditor onFinish={ex => {
|
||||
currentProvider?.updateStreamInfo(ex);
|
||||
if (!ev) {
|
||||
navigate(eventLink(ex));
|
||||
} else {
|
||||
onFinish?.(ev);
|
||||
}
|
||||
}} ev={ev} />
|
||||
}
|
||||
case StreamProviders.NostrType: {
|
||||
return <>
|
||||
{nostrTypeDialog(p)}
|
||||
<StreamEditor onFinish={(ex) => {
|
||||
// patch to api
|
||||
currentProvider?.updateStreamInfo(ex);
|
||||
onFinish?.(ex);
|
||||
}} ev={ev ?? p.publishedEvent} options={{
|
||||
canSetStream: false,
|
||||
canSetStatus: false
|
||||
}} />
|
||||
</>
|
||||
}
|
||||
case StreamProviders.Owncast: {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<p>Stream Providers</p>
|
||||
<div className="flex g12">
|
||||
{providers.map(v => <span className={`pill${v === currentProvider ? " active" : ""}`} onClick={() => setCurrentProvider(v)}>{v.name}</span>)}
|
||||
</div>
|
||||
);
|
||||
{info && providerDialog(info)}
|
||||
</>
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog.Root>
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger asChild>
|
||||
<button type="button" className={btnClassName}>
|
||||
{text && text}
|
||||
{!text && (
|
||||
<button type="button" className={props.btnClassName}>
|
||||
{props.text && props.text}
|
||||
{!props.text && (
|
||||
<>
|
||||
<span className="hide-on-mobile">New Stream</span>
|
||||
<span className="hide-on-mobile">Stream</span>
|
||||
<Icon name="signal" />
|
||||
</>
|
||||
)}
|
||||
@ -199,7 +121,9 @@ export function NewStreamDialog({
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-overlay" />
|
||||
<Dialog.Content className="dialog-content">
|
||||
<NewStream ev={ev} onFinish={onFinish} />
|
||||
<div className="new-stream">
|
||||
<NewStream {...props} onFinish={() => setOpen(false)} />
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
|
Reference in New Issue
Block a user