271 lines
12 KiB
TypeScript
271 lines
12 KiB
TypeScript
import { LiveChat } from "@/element/chat/live-chat";
|
|
import LiveVideoPlayer from "@/element/stream/live-video-player";
|
|
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
|
import { extractStreamInfo } from "@/utils";
|
|
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/stream-time";
|
|
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
|
import { DashboardRaidButton } from "./button-raid";
|
|
import { DashboardZapColumn } from "./column-zaps";
|
|
import { DashboardChatList } from "./chat-list";
|
|
import { DashboardStatsCard } from "./stats-card";
|
|
import { DashboardCard } from "./card";
|
|
import { NewStreamDialog } from "@/element/new-stream";
|
|
import { DashboardSettingsButton } from "./button-settings";
|
|
import DashboardIntro from "./intro";
|
|
import { useLocation } from "react-router-dom";
|
|
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 { Layer1Button, 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";
|
|
import { Icon } from "@/element/icon";
|
|
import ForwardingModal from "./forwarding";
|
|
import BalanceHistoryModal from "./balance-history";
|
|
const StreamSummary = lazy(() => import("@/element/summary-chart"));
|
|
|
|
export function DashboardForLink({ link }: { link: NostrLink }) {
|
|
const streamEvent = useCurrentStreamFeed(link, true);
|
|
const location = useLocation();
|
|
const login = useLogin();
|
|
const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined;
|
|
const { stream, status, image, participants, service } = extractStreamInfo(streamEvent);
|
|
const [info, setInfo] = useState<StreamProviderInfo>();
|
|
const isMyManual = streamEvent?.pubkey === login?.pubkey;
|
|
const system = useContext(SnortContext);
|
|
const [recording, setRecording] = useState(Boolean(localStorage.getItem("default-recording") ?? "true"));
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem("default-recording", String(recording));
|
|
}, [recording]);
|
|
|
|
const provider = useMemo(() => (service ? new NostrStreamProvider("", service) : DefaultProvider), [service]);
|
|
const defaultEndpoint = useMemo(() => {
|
|
return info?.endpoints.find(a => a.name == (recording ? "Best" : "Good")) ?? info?.endpoints[0];
|
|
}, [info, recording]);
|
|
|
|
useEffect(() => {
|
|
if (!isMyManual) {
|
|
provider.info().then(setInfo);
|
|
const t = setInterval(() => {
|
|
provider.info().then(setInfo);
|
|
}, 1000 * 60);
|
|
return () => {
|
|
clearInterval(t);
|
|
};
|
|
}
|
|
}, [isMyManual, provider]);
|
|
|
|
const [maxParticipants, setMaxParticipants] = useState(0);
|
|
useEffect(() => {
|
|
if (participants) {
|
|
setMaxParticipants(v => (v < Number(participants) ? Number(participants) : v));
|
|
}
|
|
}, [participants]);
|
|
|
|
const feed = useReactions(
|
|
`live:${link?.id}:${streamLink?.author}:reactions`,
|
|
streamLink ? [streamLink] : [],
|
|
rb => {
|
|
if (streamLink) {
|
|
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([streamLink]);
|
|
}
|
|
},
|
|
true,
|
|
);
|
|
|
|
if (!streamLink && !location.search.includes("setupComplete=true")) return <DashboardIntro />;
|
|
|
|
return (
|
|
<div
|
|
className={classNames("grid gap-2 h-[calc(100dvh-52px)] w-full", {
|
|
"grid-cols-3": status === StreamState.Live,
|
|
"grid-cols-[20%_80%]": status === StreamState.Ended,
|
|
})}>
|
|
<div className="min-h-0 h-full grid grid-rows-[min-content_auto] gap-2">
|
|
<DashboardCard className="flex flex-col gap-4">
|
|
<div className="flex justify-between items-center">
|
|
<h3>
|
|
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
|
|
</h3>
|
|
<div className="uppercase font-semibold flex items-center gap-2">
|
|
<div
|
|
className={`w-3 h-3 rounded-full ${
|
|
status === StreamState.Live ? "animate-pulse bg-green-500" : "bg-red-500"
|
|
}`}></div>
|
|
{status === StreamState.Live ? (
|
|
<FormattedMessage defaultMessage="Started" />
|
|
) : (
|
|
<FormattedMessage defaultMessage="Stopped" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
{streamLink && status === StreamState.Live && !isMyManual && (
|
|
<>
|
|
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
|
|
<div className="flex gap-4">
|
|
<DashboardStatsCard
|
|
name={<FormattedMessage defaultMessage="Stream Time" />}
|
|
value={<StreamTimer ev={streamEvent} />}
|
|
/>
|
|
<DashboardStatsCard name={<FormattedMessage defaultMessage="Viewers" />} value={participants} />
|
|
<DashboardStatsCard name={<FormattedMessage defaultMessage="Top Viewers" />} value={maxParticipants} />
|
|
</div>
|
|
{defaultEndpoint && (
|
|
<div className="bg-layer-1 rounded-xl px-4 py-3 flex justify-between items-center text-layer-5">
|
|
<div>
|
|
<FormattedMessage
|
|
defaultMessage="{estimate} remaining ({balance} sats @ {rate} sats / {unit})"
|
|
values={{
|
|
estimate: (
|
|
<span className="text-white">
|
|
<BalanceTimeEstimate balance={info?.balance ?? 0} endpoint={defaultEndpoint} />
|
|
</span>
|
|
),
|
|
balance: <FormattedNumber value={info?.balance ?? 0} />,
|
|
rate: defaultEndpoint.rate ?? 0,
|
|
unit: defaultEndpoint.unit ?? "min",
|
|
}}
|
|
/>
|
|
</div>
|
|
<AccountTopup
|
|
provider={provider}
|
|
onFinish={() => {
|
|
provider.info().then(setInfo);
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="grid gap-2 grid-cols-3">
|
|
<DashboardRaidButton link={streamLink} />
|
|
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
|
|
<DashboardSettingsButton ev={streamEvent} />
|
|
</div>
|
|
</>
|
|
)}
|
|
{streamLink && isMyManual && status === StreamState.Live && (
|
|
<>
|
|
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
|
|
<div className="grid gap-2 grid-cols-3">
|
|
<DashboardRaidButton link={streamLink} />
|
|
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
|
|
<WarningButton
|
|
onClick={async () => {
|
|
//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);
|
|
}
|
|
}
|
|
}}>
|
|
<FormattedMessage defaultMessage="End Stream" />
|
|
</WarningButton>
|
|
</div>
|
|
</>
|
|
)}
|
|
{(!streamLink || status === StreamState.Ended) && (
|
|
<>
|
|
<div className="bg-layer-1 rounded-xl aspect-video flex items-center justify-center uppercase text-warning font-semibold">
|
|
<FormattedMessage defaultMessage="Offline" />
|
|
</div>
|
|
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
|
|
<div className="flex flex-col gap-4">
|
|
<h3>
|
|
<FormattedMessage defaultMessage="Stream Setup" />
|
|
</h3>
|
|
<p className="text-layer-5">
|
|
<FormattedMessage
|
|
defaultMessage="To go live, copy and paste your Server URL and Stream Key below into your streaming software settings and press 'Start Streaming'. We recommend <a>OBS</a>."
|
|
values={{
|
|
a: c => <ExternalLink href="https://obsproject.com/">{c}</ExternalLink>,
|
|
}}
|
|
/>
|
|
</p>
|
|
<div className="flex gap-4 items-center">
|
|
<input type="checkbox" checked={recording} onChange={e => setRecording(e.target.checked)} />
|
|
<FormattedMessage defaultMessage="Enable Recording" />
|
|
</div>
|
|
{defaultEndpoint && <StreamKey ep={defaultEndpoint} />}
|
|
<ManualStream />
|
|
</div>
|
|
</>
|
|
)}
|
|
</DashboardCard>
|
|
{streamLink && status === StreamState.Live && (
|
|
<DashboardCard className="flex flex-col gap-4">
|
|
<h3>
|
|
<FormattedMessage defaultMessage="Chat Users" />
|
|
</h3>
|
|
<div className="h-[calc(100%-4rem)] overflow-y-auto">
|
|
<DashboardChatList feed={feed} />
|
|
</div>
|
|
</DashboardCard>
|
|
)}
|
|
{(!streamLink || status === StreamState.Ended) && (
|
|
<DashboardCard className="flex flex-col gap-4">
|
|
<h3>
|
|
<FormattedMessage defaultMessage="Account Setup" />
|
|
</h3>
|
|
<div className="flex gap-2">
|
|
<BalanceHistoryModal provider={provider} />
|
|
<ForwardingModal provider={provider} />
|
|
</div>
|
|
</DashboardCard>
|
|
)}
|
|
</div>
|
|
{streamLink && status === StreamState.Live && (
|
|
<>
|
|
<DashboardZapColumn ev={streamEvent!} link={streamLink} feed={feed} />
|
|
<div className="border border-layer-2 rounded-xl px-4 py-3 flex flex-col gap-2 min-h-0">
|
|
<Layer1Button
|
|
onClick={() => {
|
|
window.open(
|
|
`${window.location.protocol}//${window.location.host}/chat/${link.encode()}?chat=true`,
|
|
"",
|
|
"popup=true,width=400,height=800",
|
|
);
|
|
}}>
|
|
<Icon name="link" size={24} />
|
|
<FormattedMessage defaultMessage="Chat Popout" />
|
|
</Layer1Button>
|
|
<LiveChat link={streamLink} ev={streamEvent} className="grow min-h-0" />
|
|
</div>
|
|
</>
|
|
)}
|
|
{streamLink && status === StreamState.Ended && (
|
|
<>
|
|
<DashboardCard className="overflow-y-auto">
|
|
<h1>
|
|
<FormattedMessage defaultMessage="Last Stream Summary" />
|
|
</h1>
|
|
<Suspense>
|
|
<StreamSummary link={streamLink} />
|
|
</Suspense>
|
|
</DashboardCard>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|