feat: stream keys
This commit is contained in:
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
76
src/element/provider/nostr/stream-keys.tsx
Normal file
76
src/element/provider/nostr/stream-keys.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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?"
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
})}>
|
})}>
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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",
|
||||||
|
Reference in New Issue
Block a user