Nostr streaming provider topup
This commit is contained in:
@ -8,61 +8,28 @@ import { useEffect, useState } from "react";
|
|||||||
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { eventLink } from "utils";
|
import { eventLink } from "utils";
|
||||||
|
import { NostrProviderDialog } from "./nostr-provider-dialog";
|
||||||
|
|
||||||
function NewStream({ ev, onFinish }: StreamEditorProps) {
|
function NewStream({ ev, onFinish }: StreamEditorProps) {
|
||||||
const providers = useStreamProvider();
|
const providers = useStreamProvider();
|
||||||
const [currentProvider, setCurrentProvider] = useState<StreamProvider>();
|
const [currentProvider, setCurrentProvider] = useState<StreamProvider>();
|
||||||
const [info, setInfo] = useState<StreamProviderInfo>();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
async function loadInfo(p: StreamProvider) {
|
|
||||||
const inf = await p.info();
|
|
||||||
setInfo(inf);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentProvider) {
|
if (!currentProvider) {
|
||||||
setCurrentProvider(providers.at(0));
|
setCurrentProvider(providers.at(0));
|
||||||
}
|
}
|
||||||
if (currentProvider) {
|
|
||||||
loadInfo(currentProvider).catch(console.error);
|
|
||||||
}
|
|
||||||
}, [providers, currentProvider]);
|
}, [providers, currentProvider]);
|
||||||
|
|
||||||
function nostrTypeDialog(p: StreamProviderInfo) {
|
|
||||||
return <>
|
|
||||||
<div>
|
|
||||||
<p>Stream Url</p>
|
|
||||||
<div className="paper">
|
|
||||||
<input type="text" value={p.ingressUrl} disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>Stream Key</p>
|
|
||||||
<div className="paper">
|
|
||||||
<input type="password" value={p.ingressKey} disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>Balance</p>
|
|
||||||
<div className="flex g12">
|
|
||||||
<div className="paper f-grow">
|
|
||||||
{p.balance?.toLocaleString()} sats
|
|
||||||
</div>
|
|
||||||
<button className="btn btn-primary">
|
|
||||||
Topup
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function providerDialog(p: StreamProviderInfo) {
|
function providerDialog() {
|
||||||
switch (p.type) {
|
if (!currentProvider) return;
|
||||||
|
|
||||||
|
switch (currentProvider.type) {
|
||||||
case StreamProviders.Manual: {
|
case StreamProviders.Manual: {
|
||||||
return <StreamEditor onFinish={ex => {
|
return <StreamEditor onFinish={ex => {
|
||||||
currentProvider?.updateStreamInfo(ex);
|
currentProvider.updateStreamInfo(ex);
|
||||||
if (!ev) {
|
if (!ev) {
|
||||||
navigate(eventLink(ex));
|
navigate(eventLink(ex));
|
||||||
} else {
|
} else {
|
||||||
@ -71,17 +38,7 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
|
|||||||
}} ev={ev} />
|
}} ev={ev} />
|
||||||
}
|
}
|
||||||
case StreamProviders.NostrType: {
|
case StreamProviders.NostrType: {
|
||||||
return <>
|
return <NostrProviderDialog provider={currentProvider} onFinish={onFinish} ev={ev} />
|
||||||
{nostrTypeDialog(p)}
|
|
||||||
<StreamEditor onFinish={(ex) => {
|
|
||||||
// patch to api
|
|
||||||
currentProvider?.updateStreamInfo(ex);
|
|
||||||
onFinish?.(ex);
|
|
||||||
}} ev={ev ?? p.publishedEvent} options={{
|
|
||||||
canSetStream: false,
|
|
||||||
canSetStatus: false
|
|
||||||
}} />
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
case StreamProviders.Owncast: {
|
case StreamProviders.Owncast: {
|
||||||
return
|
return
|
||||||
@ -94,7 +51,7 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
|
|||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
{providers.map(v => <span className={`pill${v === currentProvider ? " active" : ""}`} onClick={() => setCurrentProvider(v)}>{v.name}</span>)}
|
{providers.map(v => <span className={`pill${v === currentProvider ? " active" : ""}`} onClick={() => setCurrentProvider(v)}>{v.name}</span>)}
|
||||||
</div>
|
</div>
|
||||||
{info && providerDialog(info)}
|
{providerDialog()}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
76
src/element/nostr-provider-dialog.tsx
Normal file
76
src/element/nostr-provider-dialog.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { StreamProvider, StreamProviderInfo } from "providers";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { SendZaps } from "./send-zap";
|
||||||
|
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
||||||
|
import Spinner from "./spinner";
|
||||||
|
|
||||||
|
export function NostrProviderDialog({ provider, ...others }: { provider: StreamProvider } & StreamEditorProps) {
|
||||||
|
const [topup, setTopup] = useState(false);
|
||||||
|
const [info, setInfo] = useState<StreamProviderInfo>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (provider && !info) {
|
||||||
|
provider.info().then(v => setInfo(v));
|
||||||
|
}
|
||||||
|
}, [info, provider])
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
return <Spinner />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topup) {
|
||||||
|
return <SendZaps lnurl={{
|
||||||
|
name: provider.name,
|
||||||
|
canZap: false,
|
||||||
|
maxCommentLength: 0,
|
||||||
|
getInvoice: async (amount) => {
|
||||||
|
const pr = await provider.topup(amount);
|
||||||
|
return { pr };
|
||||||
|
}
|
||||||
|
}} onFinish={() => {
|
||||||
|
provider.info().then(v => {
|
||||||
|
setInfo(v);
|
||||||
|
setTopup(false);
|
||||||
|
});
|
||||||
|
}} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamEvent = others.ev ?? info.publishedEvent;
|
||||||
|
return <>
|
||||||
|
<div>
|
||||||
|
<p>Stream Url</p>
|
||||||
|
<div className="paper">
|
||||||
|
<input type="text" value={info.ingressUrl} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Stream Key</p>
|
||||||
|
<div className="flex g12">
|
||||||
|
<div className="paper f-grow">
|
||||||
|
<input type="password" value={info.ingressKey} disabled />
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={() => window.navigator.clipboard.writeText(info.ingressKey ?? "")}>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Balance</p>
|
||||||
|
<div className="flex g12">
|
||||||
|
<div className="paper f-grow">
|
||||||
|
{info.balance?.toLocaleString()} sats
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={() => setTopup(true)}>
|
||||||
|
Topup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{streamEvent && <StreamEditor onFinish={(ex) => {
|
||||||
|
provider.updateStreamInfo(ex);
|
||||||
|
others.onFinish?.(ex);
|
||||||
|
}} ev={streamEvent} options={{
|
||||||
|
canSetStream: false,
|
||||||
|
canSetStatus: false
|
||||||
|
}} />}
|
||||||
|
</>
|
||||||
|
}
|
@ -9,8 +9,15 @@ import AsyncButton from "./async-button";
|
|||||||
import { Relays } from "index";
|
import { Relays } from "index";
|
||||||
import QrCode from "./qr-code";
|
import QrCode from "./qr-code";
|
||||||
|
|
||||||
interface SendZapsProps {
|
export interface LNURLLike {
|
||||||
lnurl: string;
|
get name(): string;
|
||||||
|
get maxCommentLength(): number;
|
||||||
|
get canZap(): boolean;
|
||||||
|
getInvoice(amountInSats: number, comment?: string, zap?: NostrEvent): Promise<{ pr?: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendZapsProps {
|
||||||
|
lnurl: string | LNURLLike;
|
||||||
pubkey?: string;
|
pubkey?: string;
|
||||||
aTag?: string;
|
aTag?: string;
|
||||||
targetName?: string;
|
targetName?: string;
|
||||||
@ -18,7 +25,7 @@ interface SendZapsProps {
|
|||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SendZaps({
|
export function SendZaps({
|
||||||
lnurl,
|
lnurl,
|
||||||
pubkey,
|
pubkey,
|
||||||
aTag,
|
aTag,
|
||||||
@ -32,13 +39,13 @@ function SendZaps({
|
|||||||
];
|
];
|
||||||
const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200];
|
const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200];
|
||||||
const [isFiat, setIsFiat] = useState(false);
|
const [isFiat, setIsFiat] = useState(false);
|
||||||
const [svc, setSvc] = useState<LNURL>();
|
const [svc, setSvc] = useState<LNURLLike>();
|
||||||
const [amount, setAmount] = useState(satsAmounts[0]);
|
const [amount, setAmount] = useState(satsAmounts[0]);
|
||||||
const [comment, setComment] = useState("");
|
const [comment, setComment] = useState("");
|
||||||
const [invoice, setInvoice] = useState("");
|
const [invoice, setInvoice] = useState("");
|
||||||
|
|
||||||
const name = targetName ?? svc?.name;
|
const name = targetName ?? svc?.name;
|
||||||
async function loadService() {
|
async function loadService(lnurl: string) {
|
||||||
const s = new LNURL(lnurl);
|
const s = new LNURL(lnurl);
|
||||||
await s.load();
|
await s.load();
|
||||||
setSvc(s);
|
setSvc(s);
|
||||||
@ -46,7 +53,11 @@ function SendZaps({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!svc) {
|
if (!svc) {
|
||||||
loadService().catch(console.warn);
|
if (typeof lnurl === "string") {
|
||||||
|
loadService(lnurl).catch(console.warn);
|
||||||
|
} else {
|
||||||
|
setSvc(lnurl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [lnurl]);
|
}, [lnurl]);
|
||||||
|
|
||||||
@ -119,7 +130,7 @@ function SendZaps({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{svc && (svc.maxCommentLength > 0 || svc.canZap) && <div>
|
||||||
<small>Your comment for {name}</small>
|
<small>Your comment for {name}</small>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<textarea
|
<textarea
|
||||||
@ -128,7 +139,7 @@ function SendZaps({
|
|||||||
onChange={(e) => setComment(e.target.value)}
|
onChange={(e) => setComment(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>}
|
||||||
<div>
|
<div>
|
||||||
<AsyncButton onClick={send} className="btn btn-primary">
|
<AsyncButton onClick={send} className="btn btn-primary">
|
||||||
Zap!
|
Zap!
|
||||||
@ -142,7 +153,12 @@ function SendZaps({
|
|||||||
if (!invoice) return;
|
if (!invoice) return;
|
||||||
|
|
||||||
const link = `lightning:${invoice}`;
|
const link = `lightning:${invoice}`;
|
||||||
return <QrCode data={link} link={link} />;
|
return <>
|
||||||
|
<QrCode data={link} link={link} />
|
||||||
|
<button className="btn btn-primary wide" onClick={() => onFinish()}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -31,7 +31,7 @@ export function VideoTile({
|
|||||||
ev.pubkey
|
ev.pubkey
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Link to={`/live/${link}`} className="video-tile" ref={ref}>
|
<Link to={`/${link}`} className="video-tile" ref={ref}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${inView ? image : ""})`,
|
backgroundImage: `url(${inView ? image : ""})`,
|
||||||
|
@ -77,7 +77,7 @@ export function ProfilePage() {
|
|||||||
liveEvent.kind,
|
liveEvent.kind,
|
||||||
liveEvent.pubkey
|
liveEvent.pubkey
|
||||||
);
|
);
|
||||||
navigate(`/live/${naddr}`);
|
navigate(`/${naddr}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { OwncastProvider } from "./owncast";
|
|||||||
|
|
||||||
export interface StreamProvider {
|
export interface StreamProvider {
|
||||||
get name(): string
|
get name(): string
|
||||||
|
get type(): StreamProviders
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get general info about connected provider to test everything is working
|
* Get general info about connected provider to test everything is working
|
||||||
@ -22,7 +23,12 @@ export interface StreamProvider {
|
|||||||
/**
|
/**
|
||||||
* Update stream info event
|
* Update stream info event
|
||||||
*/
|
*/
|
||||||
updateStreamInfo(ev: NostrEvent): Promise<void>;
|
updateStreamInfo(ev: NostrEvent): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top-up balance with provider
|
||||||
|
*/
|
||||||
|
topup(amount: number): Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StreamProviders {
|
export enum StreamProviders {
|
||||||
@ -33,7 +39,6 @@ export enum StreamProviders {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StreamProviderInfo {
|
export interface StreamProviderInfo {
|
||||||
type: StreamProviders
|
|
||||||
name: string
|
name: string
|
||||||
summary?: string
|
summary?: string
|
||||||
version?: string
|
version?: string
|
||||||
|
@ -6,9 +6,13 @@ export class ManualProvider implements StreamProvider {
|
|||||||
get name(): string {
|
get name(): string {
|
||||||
return "Manual"
|
return "Manual"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return StreamProviders.Manual
|
||||||
|
}
|
||||||
|
|
||||||
info(): Promise<StreamProviderInfo> {
|
info(): Promise<StreamProviderInfo> {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
type: StreamProviders.Manual,
|
|
||||||
name: this.name
|
name: this.name
|
||||||
} as StreamProviderInfo)
|
} as StreamProviderInfo)
|
||||||
}
|
}
|
||||||
@ -23,4 +27,8 @@ export class ManualProvider implements StreamProvider {
|
|||||||
System.BroadcastEvent(ev);
|
System.BroadcastEvent(ev);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
topup(amount: number): Promise<string> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
}
|
}
|
@ -13,6 +13,10 @@ export class Nip103StreamProvider implements StreamProvider {
|
|||||||
return new URL(this.#url).host;
|
return new URL(this.#url).host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return StreamProviders.NostrType
|
||||||
|
}
|
||||||
|
|
||||||
async info() {
|
async info() {
|
||||||
const rsp = await this.#getJson<AccountResponse>("GET", "account");
|
const rsp = await this.#getJson<AccountResponse>("GET", "account");
|
||||||
const title = findTag(rsp.event, "title");
|
const title = findTag(rsp.event, "title");
|
||||||
@ -45,6 +49,11 @@ export class Nip103StreamProvider implements StreamProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async topup(amount: number): Promise<string> {
|
||||||
|
const rsp = await this.#getJson<TopUpResponse>("GET", `topup?amount=${amount}`);
|
||||||
|
return rsp.pr;
|
||||||
|
}
|
||||||
|
|
||||||
async #getJson<T>(method: "GET" | "POST" | "PATCH", path: string, body?: unknown): Promise<T> {
|
async #getJson<T>(method: "GET" | "POST" | "PATCH", path: string, body?: unknown): Promise<T> {
|
||||||
const pub = await EventPublisher.nip7();
|
const pub = await EventPublisher.nip7();
|
||||||
if (!pub) throw new Error("No event publisher");
|
if (!pub) throw new Error("No event publisher");
|
||||||
@ -82,3 +91,7 @@ interface AccountResponse {
|
|||||||
remaining: number
|
remaining: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TopUpResponse {
|
||||||
|
pr: string
|
||||||
|
}
|
@ -15,6 +15,10 @@ export class OwncastProvider implements StreamProvider {
|
|||||||
return new URL(this.#url).host
|
return new URL(this.#url).host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return StreamProviders.Owncast
|
||||||
|
}
|
||||||
|
|
||||||
createConfig(): any & { type: StreamProviders; } {
|
createConfig(): any & { type: StreamProviders; } {
|
||||||
return {
|
return {
|
||||||
type: StreamProviders.Owncast,
|
type: StreamProviders.Owncast,
|
||||||
@ -40,6 +44,10 @@ export class OwncastProvider implements StreamProvider {
|
|||||||
} as StreamProviderInfo
|
} as StreamProviderInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
topup(amount: number): Promise<string> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
async #getJson<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T> {
|
async #getJson<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T> {
|
||||||
const rsp = await fetch(`${this.#url}${path}`, {
|
const rsp = await fetch(`${this.#url}${path}`, {
|
||||||
method: method,
|
method: method,
|
||||||
|
Reference in New Issue
Block a user