feat: stream keys
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
kieran 2024-08-28 11:49:14 +01:00
parent 662b260e18
commit 3a4435cda6
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
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-[50vh]": !(props.ready ?? true),
"lg:w-[500px]": !(props.largeModal ?? false),
"lg:w-[50vw]": !(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}
showForwards={false}
showBalanceHistory={false}
showStreamKeys={false}
/>
);
}

View File

@ -14,6 +14,7 @@ import StreamKey from "./stream-key";
import AccountTopup from "./topup";
import AccountWithdrawl from "./withdraw";
import BalanceHistory from "./history";
import StreamKeyList from "./stream-keys";
export default function NostrProviderDialog({
provider,
@ -21,6 +22,7 @@ export default function NostrProviderDialog({
showEditor,
showForwards,
showBalanceHistory,
showStreamKeys,
...others
}: {
provider: NostrStreamProvider;
@ -28,6 +30,7 @@ export default function NostrProviderDialog({
showEditor: boolean;
showForwards: boolean;
showBalanceHistory: boolean;
showStreamKeys: boolean;
} & StreamEditorProps) {
const system = useContext(SnortContext);
const [topup, setTopup] = useState(false);
@ -263,12 +266,18 @@ export default function NostrProviderDialog({
);
}
function streamKeys() {
if (!info || !showStreamKeys) return;
return <StreamKeyList provider={provider} />;
}
return (
<>
{showEndpoints && streamEndpoints()}
{streamEditor()}
{forwardInputs()}
{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 { useLogin } from "@/hooks/login";
import { formatSats } from "@/number";
import { getHost, extractStreamInfo, findTag } from "@/utils";
import { TaggedNostrEvent } from "@snort/system";
import { getHost, extractStreamInfo, findTag, eventLink } from "@/utils";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { SnortContext, useUserProfile } from "@snort/system-react";
import { useContext } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { WarningButton } from "../buttons";
import { Link, useNavigate } from "react-router-dom";
import { Layer2Button, WarningButton } from "../buttons";
import { ClipButton } from "./clip-button";
import { FollowButton } from "../follow-button";
import GameInfoCard from "../game-info";
@ -33,7 +33,7 @@ export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedN
const streamContext = useStream();
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() {
const pub = login?.publisher();
@ -99,12 +99,19 @@ export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedN
{ev && <Tags ev={ev} />}
</div>
{summary && <StreamSummary text={summary} />}
{isMine && (
<div className="flex gap-4">
{ev && <NewStreamDialog text={<FormattedMessage defaultMessage="Edit" />} ev={ev} />}
<WarningButton onClick={deleteStream}>
<FormattedMessage defaultMessage="Delete" />
</WarningButton>
{ev && isMine && (
<div className="flex gap-2">
<NewStreamDialog text={<FormattedMessage defaultMessage="Edit" />} ev={ev} />
<Link to={`/dashboard/${NostrLink.fromEvent(ev).encode()}`}>
<Layer2Button>
<FormattedMessage defaultMessage="Dashboard" />
</Layer2Button>
</Link>
{ev?.pubkey === login?.pubkey && (
<WarningButton onClick={deleteStream}>
<FormattedMessage defaultMessage="Delete" />
</WarningButton>
)}
</div>
)}
</div>

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
return (
<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-[20%_80%]": status === StreamState.Ended,
})}>

View File

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

View File

@ -146,6 +146,17 @@ export class NostrStreamProvider implements StreamProvider {
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> {
const pub = (() => {
if (this.#publisher) {
@ -217,3 +228,15 @@ export interface BalanceHistoryResult {
page: 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)",
"E9APoR": "Could not create stream URL",
"ESyhzp": "Your comment for {name}",
"EcglP9": "Key",
"FAUhZf": "What are sats?",
"FIDK5Y": "All Time Top Zappers",
"FXepR9": "{m}mo {ago}",
@ -143,6 +144,7 @@
"O7AeYh": "Description..",
"OEW7yJ": "Zaps",
"OKhRC6": "Share",
"ORGv1Q": "Created",
"OadZli": "Manage Servers",
"ObZZEz": "No clips yet",
"OkXMLE": "Max Audio Bitrate",
@ -175,6 +177,7 @@
"SC2nJT": "Audio Codec",
"TDUfVk": "Started",
"TP/cMX": "Ended",
"TcDwEB": "Stream Keys",
"TwyMau": "Account",
"UCDS65": "ago",
"UGFYV8": "Welcome to zap.stream!",
@ -195,6 +198,7 @@
"XIvYvF": "Failed to get invoice",
"XMGfiA": "Recent Clips",
"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>.",
"Y0DXJb": "Recording URL",
"YPh5Nq": "@ {rate}",
@ -317,6 +321,7 @@
"wTwfnv": "Invalid nostr address",
"we4Lby": "Info",
"x82IOl": "Mute",
"xhQMeQ": "Expires",
"xi3sgh": "How do i get more sats?",
"xmcVZ0": "Search",
"y867Vs": "Volume",