feat: stream keys

This commit is contained in:
2024-08-28 11:49:14 +01:00
parent 662b260e18
commit 3a4435cda6
12 changed files with 152 additions and 13 deletions

View File

@ -54,7 +54,7 @@ export default function Modal(props: ModalProps) {
{ {
"max-xl:-translate-y-[calc(100vh-100dvh)]": props.ready ?? true, "max-xl:-translate-y-[calc(100vh-100dvh)]": props.ready ?? true,
"max-xl:translate-y-[50vh]": !(props.ready ?? true), "max-xl:translate-y-[50vh]": !(props.ready ?? true),
"lg:w-[500px]": !(props.largeModal ?? false), "lg:w-[50vw]": !(props.largeModal ?? false),
"lg:w-[80vw]": props.largeModal ?? false, "lg:w-[80vw]": props.largeModal ?? false,
}, },
) )

View File

@ -57,6 +57,7 @@ export function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish">
showEditor={true} showEditor={true}
showForwards={false} showForwards={false}
showBalanceHistory={false} showBalanceHistory={false}
showStreamKeys={false}
/> />
); );
} }

View File

@ -14,6 +14,7 @@ import StreamKey from "./stream-key";
import AccountTopup from "./topup"; import AccountTopup from "./topup";
import AccountWithdrawl from "./withdraw"; import AccountWithdrawl from "./withdraw";
import BalanceHistory from "./history"; import BalanceHistory from "./history";
import StreamKeyList from "./stream-keys";
export default function NostrProviderDialog({ export default function NostrProviderDialog({
provider, provider,
@ -21,6 +22,7 @@ export default function NostrProviderDialog({
showEditor, showEditor,
showForwards, showForwards,
showBalanceHistory, showBalanceHistory,
showStreamKeys,
...others ...others
}: { }: {
provider: NostrStreamProvider; provider: NostrStreamProvider;
@ -28,6 +30,7 @@ export default function NostrProviderDialog({
showEditor: boolean; showEditor: boolean;
showForwards: boolean; showForwards: boolean;
showBalanceHistory: boolean; showBalanceHistory: boolean;
showStreamKeys: boolean;
} & StreamEditorProps) { } & StreamEditorProps) {
const system = useContext(SnortContext); const system = useContext(SnortContext);
const [topup, setTopup] = useState(false); const [topup, setTopup] = useState(false);
@ -263,12 +266,18 @@ export default function NostrProviderDialog({
); );
} }
function streamKeys() {
if (!info || !showStreamKeys) return;
return <StreamKeyList provider={provider} />;
}
return ( return (
<> <>
{showEndpoints && streamEndpoints()} {showEndpoints && streamEndpoints()}
{streamEditor()} {streamEditor()}
{forwardInputs()} {forwardInputs()}
{balanceHist()} {balanceHist()}
{streamKeys()}
</> </>
); );
} }

View File

@ -0,0 +1,76 @@
import { StreamState } from "@/const";
import { Layer2Button } from "@/element/buttons";
import Copy from "@/element/copy";
import { StatePill } from "@/element/state-pill";
import { NostrStreamProvider } from "@/providers";
import { StreamKeysResult } from "@/providers/zsz";
import { eventLink, extractStreamInfo } from "@/utils";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
export default function StreamKeyList({ provider }: { provider: NostrStreamProvider }) {
const [keys, setKeys] = useState<StreamKeysResult>();
async function loadKeys() {
const k = await provider.streamKeys();
setKeys(k);
}
useEffect(() => {
loadKeys();
}, []);
return (
<div className="flex flex-col gap-2">
<h3>
<FormattedMessage defaultMessage="Stream Keys" />
</h3>
<table>
<thead>
<tr>
<th>
<FormattedMessage defaultMessage="Created" />
</th>
<th>
<FormattedMessage defaultMessage="Expires" />
</th>
<th>
<FormattedMessage defaultMessage="Key" />
</th>
<th>
<FormattedMessage defaultMessage="Stream" />
</th>
</tr>
</thead>
<tbody>
{keys?.items.map(a => (
<tr>
<td>{new Date(a.created * 1000).toLocaleString()}</td>
<td>{a.expires && new Date(a.expires * 1000).toLocaleString()}</td>
<td>
<Copy text={a.key} hideText={true} />
</td>
<td>
{a.stream && (
<Link to={`/${eventLink(a.stream)}`}>
<StatePill state={extractStreamInfo(a.stream).status as StreamState} />
</Link>
)}
</td>
</tr>
))}
</tbody>
</table>
{keys?.items.length === 0 && <FormattedMessage defaultMessage="No keys" />}
<Layer2Button
onClick={async () => {
await provider.createStreamKey();
loadKeys();
}}>
<FormattedMessage defaultMessage="Add" />
</Layer2Button>
</div>
);
}

View File

@ -1,13 +1,13 @@
import { StreamState } from "@/const"; import { StreamState } from "@/const";
import { useLogin } from "@/hooks/login"; import { useLogin } from "@/hooks/login";
import { formatSats } from "@/number"; import { formatSats } from "@/number";
import { getHost, extractStreamInfo, findTag } from "@/utils"; import { getHost, extractStreamInfo, findTag, eventLink } from "@/utils";
import { TaggedNostrEvent } from "@snort/system"; import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { SnortContext, useUserProfile } from "@snort/system-react"; import { SnortContext, useUserProfile } from "@snort/system-react";
import { useContext } from "react"; import { useContext } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { WarningButton } from "../buttons"; import { Layer2Button, WarningButton } from "../buttons";
import { ClipButton } from "./clip-button"; import { ClipButton } from "./clip-button";
import { FollowButton } from "../follow-button"; import { FollowButton } from "../follow-button";
import GameInfoCard from "../game-info"; import GameInfoCard from "../game-info";
@ -33,7 +33,7 @@ export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedN
const streamContext = useStream(); const streamContext = useStream();
const { status, participants, title, summary, service, gameId, gameInfo } = extractStreamInfo(ev); const { status, participants, title, summary, service, gameId, gameInfo } = extractStreamInfo(ev);
const isMine = ev?.pubkey === login?.pubkey; const isMine = ev?.pubkey === login?.pubkey || host === login?.pubkey;
async function deleteStream() { async function deleteStream() {
const pub = login?.publisher(); const pub = login?.publisher();
@ -99,12 +99,19 @@ export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedN
{ev && <Tags ev={ev} />} {ev && <Tags ev={ev} />}
</div> </div>
{summary && <StreamSummary text={summary} />} {summary && <StreamSummary text={summary} />}
{isMine && ( {ev && isMine && (
<div className="flex gap-4"> <div className="flex gap-2">
{ev && <NewStreamDialog text={<FormattedMessage defaultMessage="Edit" />} ev={ev} />} <NewStreamDialog text={<FormattedMessage defaultMessage="Edit" />} ev={ev} />
<WarningButton onClick={deleteStream}> <Link to={`/dashboard/${NostrLink.fromEvent(ev).encode()}`}>
<FormattedMessage defaultMessage="Delete" /> <Layer2Button>
</WarningButton> <FormattedMessage defaultMessage="Dashboard" />
</Layer2Button>
</Link>
{ev?.pubkey === login?.pubkey && (
<WarningButton onClick={deleteStream}>
<FormattedMessage defaultMessage="Delete" />
</WarningButton>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -263,6 +263,9 @@
"ESyhzp": { "ESyhzp": {
"defaultMessage": "Your comment for {name}" "defaultMessage": "Your comment for {name}"
}, },
"EcglP9": {
"defaultMessage": "Key"
},
"FAUhZf": { "FAUhZf": {
"defaultMessage": "What are sats?" "defaultMessage": "What are sats?"
}, },
@ -432,6 +435,9 @@
"OKhRC6": { "OKhRC6": {
"defaultMessage": "Share" "defaultMessage": "Share"
}, },
"ORGv1Q": {
"defaultMessage": "Created"
},
"OadZli": { "OadZli": {
"defaultMessage": "Manage Servers" "defaultMessage": "Manage Servers"
}, },
@ -530,6 +536,9 @@
"TP/cMX": { "TP/cMX": {
"defaultMessage": "Ended" "defaultMessage": "Ended"
}, },
"TcDwEB": {
"defaultMessage": "Stream Keys"
},
"TwyMau": { "TwyMau": {
"defaultMessage": "Account" "defaultMessage": "Account"
}, },
@ -592,6 +601,9 @@
"XgWvGA": { "XgWvGA": {
"defaultMessage": "Reactions" "defaultMessage": "Reactions"
}, },
"XmQZr5": {
"defaultMessage": "No keys"
},
"Xq2sb0": { "Xq2sb0": {
"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>." "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>."
}, },
@ -963,6 +975,9 @@
"x82IOl": { "x82IOl": {
"defaultMessage": "Mute" "defaultMessage": "Mute"
}, },
"xhQMeQ": {
"defaultMessage": "Expires"
},
"xi3sgh": { "xi3sgh": {
"defaultMessage": "How do i get more sats?" "defaultMessage": "How do i get more sats?"
}, },

View File

@ -20,6 +20,7 @@ export default function BalanceHistoryModal({ provider }: { provider: NostrStrea
showEditor={false} showEditor={false}
showEndpoints={true} showEndpoints={true}
showForwards={false} showForwards={false}
showStreamKeys={false}
/> />
</Modal> </Modal>
)} )}

View File

@ -25,6 +25,7 @@ export function DashboardSettingsButton({ ev }: { ev?: TaggedNostrEvent }) {
showForwards={true} showForwards={true}
showEditor={false} showEditor={false}
showBalanceHistory={false} showBalanceHistory={false}
showStreamKeys={true}
/> />
</div> </div>
</Modal> </Modal>

View File

@ -86,7 +86,7 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
return ( return (
<div <div
className={classNames("grid gap-2 h-[calc(100dvh-52px)]", { className={classNames("grid gap-2 h-[calc(100dvh-52px)] w-full", {
"grid-cols-3": status === StreamState.Live, "grid-cols-3": status === StreamState.Live,
"grid-cols-[20%_80%]": status === StreamState.Ended, "grid-cols-[20%_80%]": status === StreamState.Ended,
})}> })}>

View File

@ -20,6 +20,7 @@ export default function ForwardingModal({ provider }: { provider: NostrStreamPro
showEditor={false} showEditor={false}
showEndpoints={false} showEndpoints={false}
showForwards={true} showForwards={true}
showStreamKeys={false}
/> />
</Modal> </Modal>
)} )}

View File

@ -146,6 +146,17 @@ export class NostrStreamProvider implements StreamProvider {
return await this.#getJson<BalanceHistoryResult>("GET", `history?page=${page}&pageSize=${pageSize}`); return await this.#getJson<BalanceHistoryResult>("GET", `history?page=${page}&pageSize=${pageSize}`);
} }
async streamKeys(page = 0, pageSize = 20) {
return await this.#getJson<StreamKeysResult>("GET", `keys?page=${page}&pageSize=${pageSize}`);
}
async createStreamKey(expires?: undefined) {
return await this.#getJson<{ key: string; event: NostrEvent }>("POST", "keys", {
event: { title: "New stream key, who dis" },
expires,
});
}
async #getJson<T>(method: "GET" | "POST" | "PATCH" | "DELETE", path: string, body?: unknown): Promise<T> { async #getJson<T>(method: "GET" | "POST" | "PATCH" | "DELETE", path: string, body?: unknown): Promise<T> {
const pub = (() => { const pub = (() => {
if (this.#publisher) { if (this.#publisher) {
@ -217,3 +228,15 @@ export interface BalanceHistoryResult {
page: number; page: number;
pageSize: number; pageSize: number;
} }
export interface StreamKeysResult {
items: Array<{
id: string;
created: number;
key: string;
expires?: number;
stream?: NostrEvent;
}>;
page: number;
pageSize: number;
}

View File

@ -87,6 +87,7 @@
"E7n6zr": "Current stream cost: {amount} sats/{unit} (about {usd}/day for a {x}hr stream)", "E7n6zr": "Current stream cost: {amount} sats/{unit} (about {usd}/day for a {x}hr stream)",
"E9APoR": "Could not create stream URL", "E9APoR": "Could not create stream URL",
"ESyhzp": "Your comment for {name}", "ESyhzp": "Your comment for {name}",
"EcglP9": "Key",
"FAUhZf": "What are sats?", "FAUhZf": "What are sats?",
"FIDK5Y": "All Time Top Zappers", "FIDK5Y": "All Time Top Zappers",
"FXepR9": "{m}mo {ago}", "FXepR9": "{m}mo {ago}",
@ -143,6 +144,7 @@
"O7AeYh": "Description..", "O7AeYh": "Description..",
"OEW7yJ": "Zaps", "OEW7yJ": "Zaps",
"OKhRC6": "Share", "OKhRC6": "Share",
"ORGv1Q": "Created",
"OadZli": "Manage Servers", "OadZli": "Manage Servers",
"ObZZEz": "No clips yet", "ObZZEz": "No clips yet",
"OkXMLE": "Max Audio Bitrate", "OkXMLE": "Max Audio Bitrate",
@ -175,6 +177,7 @@
"SC2nJT": "Audio Codec", "SC2nJT": "Audio Codec",
"TDUfVk": "Started", "TDUfVk": "Started",
"TP/cMX": "Ended", "TP/cMX": "Ended",
"TcDwEB": "Stream Keys",
"TwyMau": "Account", "TwyMau": "Account",
"UCDS65": "ago", "UCDS65": "ago",
"UGFYV8": "Welcome to zap.stream!", "UGFYV8": "Welcome to zap.stream!",
@ -195,6 +198,7 @@
"XIvYvF": "Failed to get invoice", "XIvYvF": "Failed to get invoice",
"XMGfiA": "Recent Clips", "XMGfiA": "Recent Clips",
"XgWvGA": "Reactions", "XgWvGA": "Reactions",
"XmQZr5": "No keys",
"Xq2sb0": "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>.", "Xq2sb0": "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>.",
"Y0DXJb": "Recording URL", "Y0DXJb": "Recording URL",
"YPh5Nq": "@ {rate}", "YPh5Nq": "@ {rate}",
@ -317,6 +321,7 @@
"wTwfnv": "Invalid nostr address", "wTwfnv": "Invalid nostr address",
"we4Lby": "Info", "we4Lby": "Info",
"x82IOl": "Mute", "x82IOl": "Mute",
"xhQMeQ": "Expires",
"xi3sgh": "How do i get more sats?", "xi3sgh": "How do i get more sats?",
"xmcVZ0": "Search", "xmcVZ0": "Search",
"y867Vs": "Volume", "y867Vs": "Volume",