Files
zap.stream/src/element/stream-editor.tsx
2023-09-06 14:48:39 +00:00

266 lines
8.3 KiB
TypeScript

import "./stream-editor.css";
import { useEffect, useState, useCallback } from "react";
import { NostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared";
import { TagsInput } from "react-tag-input-component";
import { FormattedMessage, useIntl } from "react-intl";
import AsyncButton from "./async-button";
import { StreamState } from "../index";
import { findTag } from "../utils";
import { useLogin } from "hooks/login";
import { NewGoalDialog } from "element/new-goal";
import { useGoals } from "hooks/goals";
export interface StreamEditorProps {
ev?: NostrEvent;
onFinish?: (ev: NostrEvent) => void;
options?: {
canSetTitle?: boolean;
canSetSummary?: boolean;
canSetImage?: boolean;
canSetStatus?: boolean;
canSetStream?: boolean;
canSetTags?: boolean;
canSetContentWarning?: boolean;
};
}
interface GoalSelectorProps {
goal?: string;
pubkey: string;
onGoalSelect: (g: string) => void;
}
function GoalSelector({ goal, pubkey, onGoalSelect }: GoalSelectorProps) {
const goals = useGoals(pubkey, true);
const { formatMessage } = useIntl();
return (
<select defaultValue={goal} onChange={ev => onGoalSelect(ev.target.value)}>
<option value="">{formatMessage({ defaultMessage: "Select a goal..." })}</option>
{goals?.map(goal => (
<option key={goal.id} value={goal.id}>
{goal.content}
</option>
))}
</select>
);
}
export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
const [title, setTitle] = useState("");
const [summary, setSummary] = useState("");
const [image, setImage] = useState("");
const [stream, setStream] = useState("");
const [status, setStatus] = useState("");
const [start, setStart] = useState<string>();
const [tags, setTags] = useState<string[]>([]);
const [contentWarning, setContentWarning] = useState(false);
const [isValid, setIsValid] = useState(false);
const [goal, setGoal] = useState<string | undefined>();
const login = useLogin();
const { formatMessage } = useIntl();
useEffect(() => {
setTitle(findTag(ev, "title") ?? "");
setSummary(findTag(ev, "summary") ?? "");
setImage(findTag(ev, "image") ?? "");
setStream(findTag(ev, "streaming") ?? "");
setStatus(findTag(ev, "status") ?? StreamState.Live);
setStart(findTag(ev, "starts"));
setTags(ev?.tags.filter(a => a[0] === "t").map(a => a[1]) ?? []);
setContentWarning(findTag(ev, "content-warning") !== undefined);
setGoal(findTag(ev, "goal"));
}, [ev?.id]);
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]);
useEffect(() => {
setIsValid(ev !== undefined || validate());
}, [validate, title, summary, image, stream]);
async function publishStream() {
const pub = login?.publisher();
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(30311)
.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]);
}
for (const tx of tags) {
eb.tag(["t", tx.trim()]);
}
if (contentWarning) {
eb.tag(["content-warning", "nsfw"]);
}
if (goal && goal.length > 0) {
eb.tag(["goal", goal]);
}
return eb;
});
console.debug(evNew);
onFinish && onFinish(evNew);
}
}
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 (
<>
<h3>{ev ? "Edit Stream" : "New Stream"}</h3>
{(options?.canSetTitle ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Title" />
</p>
<div className="paper">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "What are we steaming today?" })}
value={title}
onChange={e => setTitle(e.target.value)}
/>
</div>
</div>
)}
{(options?.canSetSummary ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Summary" />
</p>
<div className="paper">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "A short description of the content" })}
value={summary}
onChange={e => setSummary(e.target.value)}
/>
</div>
</div>
)}
{(options?.canSetImage ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Cover Image" />
</p>
<div className="paper">
<input type="text" placeholder="https://" value={image} onChange={e => setImage(e.target.value)} />
</div>
</div>
)}
{(options?.canSetStream ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Stream URL" />
</p>
<div className="paper">
<input type="text" placeholder="https://" value={stream} onChange={e => setStream(e.target.value)} />
</div>
<small>
<FormattedMessage defaultMessage="Stream type should be HLS" />
</small>
</div>
)}
{(options?.canSetStatus ?? true) && (
<>
<div>
<p>
<FormattedMessage defaultMessage="Status" />
</p>
<div className="flex g12">
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(v => (
<span className={`pill${status === v ? " active" : ""}`} onClick={() => setStatus(v)} key={v}>
{v}
</span>
))}
</div>
</div>
{status === StreamState.Planned && (
<div>
<p>
<FormattedMessage defaultMessage="Start Time" />
</p>
<div className="paper">
<input
type="datetime-local"
value={toDateTimeString(Number(start ?? "0"))}
onChange={e => setStart(fromDateTimeString(e.target.value).toString())}
/>
</div>
</div>
)}
</>
)}
{(options?.canSetTags ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Tags" />
</p>
<div className="paper">
<TagsInput value={tags} onChange={setTags} placeHolder="Music,DJ,English" separators={["Enter", ","]} />
</div>
</div>
)}
{login?.pubkey && (
<>
<div>
<p>
<FormattedMessage defaultMessage="Goal" />
</p>
<div className="paper">
<GoalSelector goal={goal} pubkey={login?.pubkey} onGoalSelect={setGoal} />
</div>
</div>
<NewGoalDialog />
</>
)}
{(options?.canSetContentWarning ?? true) && (
<div className="flex g12 content-warning">
<div>
<input type="checkbox" checked={contentWarning} onChange={e => setContentWarning(e.target.checked)} />
</div>
<div>
<div className="warning">
<FormattedMessage defaultMessage="NSFW Content" />
</div>
<FormattedMessage defaultMessage="Check here if this stream contains nudity or pornographic content." />
</div>
</div>
)}
<div>
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishStream}>
{ev ? <FormattedMessage defaultMessage="Save" /> : <FormattedMessage defaultMessage="Start Stream" />}
</AsyncButton>
</div>
</>
);
}