This commit is contained in:
parent
9bfed646d1
commit
345c1d58bf
@ -1,20 +1,21 @@
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import { NostrEvent } from "@snort/system";
|
import { EventKind, NostrEvent } from "@snort/system";
|
||||||
import { unixNow } from "@snort/shared";
|
import { unixNow } from "@snort/shared";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { SnortContext } from "@snort/system-react";
|
||||||
|
|
||||||
import { extractStreamInfo, findTag } from "@/utils";
|
import { extractStreamInfo, findTag } from "@/utils";
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { StreamState } from "@/const";
|
import { GOAL, StreamState, defaultRelays } from "@/const";
|
||||||
import { DefaultButton } from "@/element/buttons";
|
import { DefaultButton } from "@/element/buttons";
|
||||||
import Pill from "@/element/pill";
|
import Pill from "@/element/pill";
|
||||||
|
|
||||||
import { NewGoalDialog } from "./new-goal";
|
|
||||||
import { StreamInput } from "./input";
|
import { StreamInput } from "./input";
|
||||||
import { GoalSelector } from "./goal-selector";
|
import { GoalSelector } from "./goal-selector";
|
||||||
import GameDatabase, { GameInfo } from "@/service/game-database";
|
import GameDatabase, { GameInfo } from "@/service/game-database";
|
||||||
import CategoryInput from "./category-input";
|
import CategoryInput from "./category-input";
|
||||||
|
import { FileUploader } from "@/element/file-uploader";
|
||||||
|
import AmountInput from "@/element/amount-input";
|
||||||
|
|
||||||
export interface StreamEditorProps {
|
export interface StreamEditorProps {
|
||||||
ev?: NostrEvent;
|
ev?: NostrEvent;
|
||||||
@ -42,10 +43,13 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
const [contentWarning, setContentWarning] = useState(false);
|
const [contentWarning, setContentWarning] = useState(false);
|
||||||
const [isValid, setIsValid] = useState(false);
|
const [isValid, setIsValid] = useState(false);
|
||||||
const [goal, setGoal] = useState<string>();
|
const [goal, setGoal] = useState<string>();
|
||||||
|
const [goalName, setGoalName] = useState("");
|
||||||
|
const [goalAmount, setGoalMount] = useState(0);
|
||||||
const [game, setGame] = useState<GameInfo>();
|
const [game, setGame] = useState<GameInfo>();
|
||||||
const [gameId, setGameId] = useState<string>();
|
const [gameId, setGameId] = useState<string>();
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const system = useContext(SnortContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { gameInfo, gameId, title, summary, image, stream, status, starts, tags, contentWarning, goal, recording } =
|
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() {
|
async function publishStream() {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
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 evNew = await pub.generic(eb => {
|
||||||
const now = unixNow();
|
const now = unixNow();
|
||||||
const dTag = findTag(ev, "d") ?? now.toString();
|
const dTag = findTag(ev, "d") ?? now.toString();
|
||||||
const starts = start ?? now.toString();
|
const starts = start ?? now.toString();
|
||||||
const ends = findTag(ev, "ends") ?? now.toString();
|
const ends = findTag(ev, "ends") ?? now.toString();
|
||||||
eb.kind(30311)
|
eb.kind(EventKind.LiveEvent)
|
||||||
.tag(["d", dTag])
|
.tag(["d", dTag])
|
||||||
.tag(["title", title])
|
.tag(["title", title])
|
||||||
.tag(["summary", summary])
|
.tag(["summary", summary])
|
||||||
@ -115,8 +131,8 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
if (contentWarning) {
|
if (contentWarning) {
|
||||||
eb.tag(["content-warning", "nsfw"]);
|
eb.tag(["content-warning", "nsfw"]);
|
||||||
}
|
}
|
||||||
if (goal && goal.length > 0) {
|
if (thisGoal && thisGoal.length > 0) {
|
||||||
eb.tag(["goal", goal]);
|
eb.tag(["goal", thisGoal]);
|
||||||
}
|
}
|
||||||
if (gameId) {
|
if (gameId) {
|
||||||
eb.tag(["t", gameId]);
|
eb.tag(["t", gameId]);
|
||||||
@ -161,11 +177,10 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
)}
|
)}
|
||||||
{(options?.canSetImage ?? true) && (
|
{(options?.canSetImage ?? true) && (
|
||||||
<StreamInput label={<FormattedMessage defaultMessage="Cover Image" />}>
|
<StreamInput label={<FormattedMessage defaultMessage="Cover Image" />}>
|
||||||
|
{image && <img src={image} className="mb-2 aspect-video object-cover rounded-xl" />}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input type="text" placeholder="https://" value={image} onChange={e => setImage(e.target.value)} />
|
<input type="text" placeholder="https://" value={image} onChange={e => setImage(e.target.value)} />
|
||||||
<DefaultButton>
|
<FileUploader onResult={v => setImage(v ?? "")} />
|
||||||
<FormattedMessage defaultMessage="Upload" />
|
|
||||||
</DefaultButton>
|
|
||||||
</div>
|
</div>
|
||||||
</StreamInput>
|
</StreamInput>
|
||||||
)}
|
)}
|
||||||
@ -180,9 +195,9 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
{(options?.canSetStatus ?? true) && (
|
{(options?.canSetStatus ?? true) && (
|
||||||
<>
|
<>
|
||||||
<StreamInput label={<FormattedMessage defaultMessage="Status" />}>
|
<StreamInput label={<FormattedMessage defaultMessage="Status" />}>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 uppercase">
|
||||||
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(v => (
|
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(v => (
|
||||||
<Pill className={status === v ? " active" : ""} onClick={() => setStatus(v)} key={v}>
|
<Pill selected={status === v} onClick={() => setStatus(v)} key={v}>
|
||||||
{v}
|
{v}
|
||||||
</Pill>
|
</Pill>
|
||||||
))}
|
))}
|
||||||
@ -218,7 +233,19 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
<StreamInput label={<FormattedMessage defaultMessage="Goal" />}>
|
<StreamInput label={<FormattedMessage defaultMessage="Goal" />}>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<GoalSelector goal={goal} onGoalSelect={setGoal} />
|
<GoalSelector goal={goal} onGoalSelect={setGoal} />
|
||||||
<NewGoalDialog />
|
{!goal && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={formatMessage({
|
||||||
|
defaultMessage: "Goal Name",
|
||||||
|
})}
|
||||||
|
value={goalName}
|
||||||
|
onChange={e => setGoalName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<AmountInput onChange={setGoalMount} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</StreamInput>
|
</StreamInput>
|
||||||
)}
|
)}
|
||||||
|
@ -25,7 +25,7 @@ interface StatSlot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function StreamSummary({ link, preload }: { link: NostrLink; preload?: NostrEvent }) {
|
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 thisLink = ev ? NostrLink.fromEvent(ev) : undefined;
|
||||||
const data = useReactions(
|
const data = useReactions(
|
||||||
`live:${link?.id}:${link?.author}:reactions`,
|
`live:${link?.id}:${link?.author}:reactions`,
|
||||||
@ -148,7 +148,7 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h1>{title}</h1>
|
<h2>{title}</h2>
|
||||||
{summary && <p>{summary}</p>}
|
{summary && <p>{summary}</p>}
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<StatePill state={status as StreamState} />
|
<StatePill state={status as StreamState} />
|
||||||
|
@ -6,7 +6,7 @@ import { useRequestBuilder } from "@snort/system-react";
|
|||||||
import { useUserEmojiPacks } from "@/hooks/emoji";
|
import { useUserEmojiPacks } from "@/hooks/emoji";
|
||||||
import { MUTED, USER_CARDS, USER_EMOJIS } from "@/const";
|
import { MUTED, USER_CARDS, USER_EMOJIS } from "@/const";
|
||||||
import type { Tags } from "@/types";
|
import type { Tags } from "@/types";
|
||||||
import { getPublisher, Login } from "@/login";
|
import { getPublisher, getSigner, Login } from "@/login";
|
||||||
|
|
||||||
export function useLogin() {
|
export function useLogin() {
|
||||||
const session = useSyncExternalStore(
|
const session = useSyncExternalStore(
|
||||||
@ -19,6 +19,9 @@ export function useLogin() {
|
|||||||
publisher: () => {
|
publisher: () => {
|
||||||
return getPublisher(session);
|
return getPublisher(session);
|
||||||
},
|
},
|
||||||
|
signer: () => {
|
||||||
|
return getSigner(session);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
src/login.ts
11
src/login.ts
@ -132,12 +132,19 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPublisher(session: LoginSession) {
|
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) {
|
switch (session?.type) {
|
||||||
case LoginType.Nip7: {
|
case LoginType.Nip7: {
|
||||||
return new EventPublisher(new Nip7Signer(), session.pubkey);
|
return new Nip7Signer();
|
||||||
}
|
}
|
||||||
case LoginType.PrivateKey: {
|
case LoginType.PrivateKey: {
|
||||||
return new EventPublisher(new PrivateKeySigner(unwrap(session.privateKey)), session.pubkey);
|
return new PrivateKeySigner(unwrap(session.privateKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export function DashboardZapColumn({
|
|||||||
const zapSum = sortedZaps.reduce((acc, v) => acc + v.amount, 0);
|
const zapSum = sortedZaps.reduce((acc, v) => acc + v.amount, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-2">
|
||||||
<DashboardCard className="flex flex-col gap-2">
|
<DashboardCard className="flex flex-col gap-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
|
@ -2,9 +2,9 @@ import { LiveChat } from "@/element/live-chat";
|
|||||||
import LiveVideoPlayer from "@/element/live-video-player";
|
import LiveVideoPlayer from "@/element/live-video-player";
|
||||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||||
import { extractStreamInfo } from "@/utils";
|
import { extractStreamInfo } from "@/utils";
|
||||||
import { NostrLink } from "@snort/system";
|
import { EventExt, NostrEvent, NostrLink } from "@snort/system";
|
||||||
import { useReactions } from "@snort/system-react";
|
import { SnortContext, useReactions } from "@snort/system-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { Suspense, lazy, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
import { StreamTimer } from "@/element/stream-time";
|
import { StreamTimer } from "@/element/stream-time";
|
||||||
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
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 { DefaultProvider, NostrStreamProvider, StreamProviderInfo } from "@/providers";
|
||||||
import { ExternalLink } from "@/element/external-link";
|
import { ExternalLink } from "@/element/external-link";
|
||||||
import BalanceTimeEstimate from "@/element/balance-time-estimate";
|
import BalanceTimeEstimate from "@/element/balance-time-estimate";
|
||||||
import { DefaultButton } from "@/element/buttons";
|
import { WarningButton } from "@/element/buttons";
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import AccountTopup from "@/element/provider/nostr/topup";
|
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 }) {
|
export function DashboardForLink({ link }: { link: NostrLink }) {
|
||||||
const streamEvent = useCurrentStreamFeed(link, true);
|
const streamEvent = useCurrentStreamFeed(link, true);
|
||||||
@ -32,6 +36,8 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined;
|
const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined;
|
||||||
const { stream, status, image, participants, service } = extractStreamInfo(streamEvent);
|
const { stream, status, image, participants, service } = extractStreamInfo(streamEvent);
|
||||||
const [info, setInfo] = useState<StreamProviderInfo>();
|
const [info, setInfo] = useState<StreamProviderInfo>();
|
||||||
|
const isMyManual = streamEvent?.pubkey === login?.pubkey;
|
||||||
|
const system = useContext(SnortContext);
|
||||||
|
|
||||||
const provider = useMemo(() => (service ? new NostrStreamProvider("", service) : DefaultProvider), [service]);
|
const provider = useMemo(() => (service ? new NostrStreamProvider("", service) : DefaultProvider), [service]);
|
||||||
const defaultEndpoint = useMemo(() => {
|
const defaultEndpoint = useMemo(() => {
|
||||||
@ -39,6 +45,7 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
}, [info]);
|
}, [info]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isMyManual) {
|
||||||
provider.info().then(setInfo);
|
provider.info().then(setInfo);
|
||||||
const t = setInterval(() => {
|
const t = setInterval(() => {
|
||||||
provider.info().then(setInfo);
|
provider.info().then(setInfo);
|
||||||
@ -46,7 +53,8 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
return () => {
|
return () => {
|
||||||
clearInterval(t);
|
clearInterval(t);
|
||||||
};
|
};
|
||||||
}, [provider]);
|
}
|
||||||
|
}, [isMyManual, provider]);
|
||||||
|
|
||||||
const [maxParticipants, setMaxParticipants] = useState(0);
|
const [maxParticipants, setMaxParticipants] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -69,7 +77,11 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
if (!streamLink && !location.search.includes("setupComplete=true")) return <DashboardIntro />;
|
if (!streamLink && !location.search.includes("setupComplete=true")) return <DashboardIntro />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-3 gap-2 h-[calc(100%-48px-1rem)]">
|
<div
|
||||||
|
className={classNames("grid gap-2 h-[calc(100%-48px-1rem)]", {
|
||||||
|
"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">
|
<div className="min-h-0 h-full grid grid-rows-[min-content_auto] gap-2">
|
||||||
<DashboardCard className="flex flex-col gap-4">
|
<DashboardCard className="flex flex-col gap-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@ -88,7 +100,7 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{streamLink && (
|
{streamLink && status === StreamState.Live && !isMyManual && (
|
||||||
<>
|
<>
|
||||||
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
|
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
@ -129,14 +141,42 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
|
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
|
||||||
<DashboardSettingsButton ev={streamEvent} />
|
<DashboardSettingsButton ev={streamEvent} />
|
||||||
</div>
|
</div>
|
||||||
{streamEvent?.pubkey === login?.pubkey && (
|
|
||||||
<DefaultButton>
|
|
||||||
<FormattedMessage defaultMessage="End Stream" />
|
|
||||||
</DefaultButton>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!streamLink && (
|
{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">
|
<div className="bg-layer-1 rounded-xl aspect-video flex items-center justify-center uppercase text-warning font-semibold">
|
||||||
<FormattedMessage defaultMessage="Offline" />
|
<FormattedMessage defaultMessage="Offline" />
|
||||||
@ -155,11 +195,12 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
{defaultEndpoint && <StreamKey ep={defaultEndpoint} />}
|
{defaultEndpoint && <StreamKey ep={defaultEndpoint} />}
|
||||||
|
<ManualStream />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
{streamLink && (
|
{streamLink && status === StreamState.Live && (
|
||||||
<DashboardCard className="flex flex-col gap-4">
|
<DashboardCard className="flex flex-col gap-4">
|
||||||
<h3>
|
<h3>
|
||||||
<FormattedMessage defaultMessage="Chat Users" />
|
<FormattedMessage defaultMessage="Chat Users" />
|
||||||
@ -170,16 +211,22 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{streamLink && (
|
{streamLink && status === StreamState.Live && (
|
||||||
<>
|
<>
|
||||||
<DashboardZapColumn ev={streamEvent!} link={streamLink} feed={feed} />
|
<DashboardZapColumn ev={streamEvent!} link={streamLink} feed={feed} />
|
||||||
<LiveChat link={streamLink} ev={streamEvent} className="min-h-0" />
|
<LiveChat link={streamLink} ev={streamEvent} className="min-h-0 border border-layer-2 rounded-xl px-4 py-3" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!streamLink && (
|
{streamLink && status === StreamState.Ended && (
|
||||||
<>
|
<>
|
||||||
<DashboardCard></DashboardCard>
|
<DashboardCard className="overflow-y-auto">
|
||||||
<DashboardCard></DashboardCard>
|
<h1>
|
||||||
|
<FormattedMessage defaultMessage="Last Stream Summary" />
|
||||||
|
</h1>
|
||||||
|
<Suspense>
|
||||||
|
<StreamSummary link={streamLink} />
|
||||||
|
</Suspense>
|
||||||
|
</DashboardCard>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,7 +58,7 @@ export default function DashboardIntroStep4() {
|
|||||||
<DefaultButton
|
<DefaultButton
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (!goal && pub) {
|
if (!goal && pub && goalName && goalAmount) {
|
||||||
const goalEvent = await pub.generic(eb => {
|
const goalEvent = await pub.generic(eb => {
|
||||||
return eb
|
return eb
|
||||||
.kind(GOAL)
|
.kind(GOAL)
|
||||||
|
39
src/pages/dashboard/manual-stream.tsx
Normal file
39
src/pages/dashboard/manual-stream.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="text-primary cursor-pointer select-none" onClick={() => setOpen(true)}>
|
||||||
|
<FormattedMessage defaultMessage="I have my own stream host" />
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<Modal id="new-stream" onClose={() => setOpen(false)}>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<StreamEditor
|
||||||
|
onFinish={ex => {
|
||||||
|
provider.updateStreamInfo(system, ex);
|
||||||
|
if (!ex) {
|
||||||
|
navigate(`/${NostrLink.fromEvent(ex).encode()}`, {
|
||||||
|
state: ex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user