Nostr streaming provider topup

This commit is contained in:
Kieran 2023-07-04 14:12:49 +01:00
parent 7cc613646c
commit 552a6744a8
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 148 additions and 65 deletions

View File

@ -8,61 +8,28 @@ import { useEffect, useState } from "react";
import { StreamEditor, StreamEditorProps } from "./stream-editor";
import { useNavigate } from "react-router-dom";
import { eventLink } from "utils";
import { NostrProviderDialog } from "./nostr-provider-dialog";
function NewStream({ ev, onFinish }: StreamEditorProps) {
const providers = useStreamProvider();
const [currentProvider, setCurrentProvider] = useState<StreamProvider>();
const [info, setInfo] = useState<StreamProviderInfo>();
const navigate = useNavigate();
async function loadInfo(p: StreamProvider) {
const inf = await p.info();
setInfo(inf);
}
useEffect(() => {
if (!currentProvider) {
setCurrentProvider(providers.at(0));
}
if (currentProvider) {
loadInfo(currentProvider).catch(console.error);
}
}, [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) {
switch (p.type) {
function providerDialog() {
if (!currentProvider) return;
switch (currentProvider.type) {
case StreamProviders.Manual: {
return <StreamEditor onFinish={ex => {
currentProvider?.updateStreamInfo(ex);
currentProvider.updateStreamInfo(ex);
if (!ev) {
navigate(eventLink(ex));
} else {
@ -71,17 +38,7 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
}} ev={ev} />
}
case StreamProviders.NostrType: {
return <>
{nostrTypeDialog(p)}
<StreamEditor onFinish={(ex) => {
// patch to api
currentProvider?.updateStreamInfo(ex);
onFinish?.(ex);
}} ev={ev ?? p.publishedEvent} options={{
canSetStream: false,
canSetStatus: false
}} />
</>
return <NostrProviderDialog provider={currentProvider} onFinish={onFinish} ev={ev} />
}
case StreamProviders.Owncast: {
return
@ -94,7 +51,7 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
<div className="flex g12">
{providers.map(v => <span className={`pill${v === currentProvider ? " active" : ""}`} onClick={() => setCurrentProvider(v)}>{v.name}</span>)}
</div>
{info && providerDialog(info)}
{providerDialog()}
</>
}

View 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
}} />}
</>
}

View File

@ -9,8 +9,15 @@ import AsyncButton from "./async-button";
import { Relays } from "index";
import QrCode from "./qr-code";
interface SendZapsProps {
lnurl: string;
export interface LNURLLike {
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;
aTag?: string;
targetName?: string;
@ -18,7 +25,7 @@ interface SendZapsProps {
button?: ReactNode;
}
function SendZaps({
export function SendZaps({
lnurl,
pubkey,
aTag,
@ -32,13 +39,13 @@ function SendZaps({
];
const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200];
const [isFiat, setIsFiat] = useState(false);
const [svc, setSvc] = useState<LNURL>();
const [svc, setSvc] = useState<LNURLLike>();
const [amount, setAmount] = useState(satsAmounts[0]);
const [comment, setComment] = useState("");
const [invoice, setInvoice] = useState("");
const name = targetName ?? svc?.name;
async function loadService() {
async function loadService(lnurl: string) {
const s = new LNURL(lnurl);
await s.load();
setSvc(s);
@ -46,7 +53,11 @@ function SendZaps({
useEffect(() => {
if (!svc) {
loadService().catch(console.warn);
if (typeof lnurl === "string") {
loadService(lnurl).catch(console.warn);
} else {
setSvc(lnurl);
}
}
}, [lnurl]);
@ -119,7 +130,7 @@ function SendZaps({
))}
</div>
</div>
<div>
{svc && (svc.maxCommentLength > 0 || svc.canZap) && <div>
<small>Your comment for {name}</small>
<div className="paper">
<textarea
@ -128,7 +139,7 @@ function SendZaps({
onChange={(e) => setComment(e.target.value)}
/>
</div>
</div>
</div>}
<div>
<AsyncButton onClick={send} className="btn btn-primary">
Zap!
@ -142,7 +153,12 @@ function SendZaps({
if (!invoice) return;
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 (

View File

@ -31,7 +31,7 @@ export function VideoTile({
ev.pubkey
);
return (
<Link to={`/live/${link}`} className="video-tile" ref={ref}>
<Link to={`/${link}`} className="video-tile" ref={ref}>
<div
style={{
backgroundImage: `url(${inView ? image : ""})`,

View File

@ -77,7 +77,7 @@ export function ProfilePage() {
liveEvent.kind,
liveEvent.pubkey
);
navigate(`/live/${naddr}`);
navigate(`/${naddr}`);
}
}

View File

@ -8,6 +8,7 @@ import { OwncastProvider } from "./owncast";
export interface StreamProvider {
get name(): string
get type(): StreamProviders
/**
* Get general info about connected provider to test everything is working
@ -22,7 +23,12 @@ export interface StreamProvider {
/**
* 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 {
@ -33,7 +39,6 @@ export enum StreamProviders {
}
export interface StreamProviderInfo {
type: StreamProviders
name: string
summary?: string
version?: string

View File

@ -6,9 +6,13 @@ export class ManualProvider implements StreamProvider {
get name(): string {
return "Manual"
}
get type() {
return StreamProviders.Manual
}
info(): Promise<StreamProviderInfo> {
return Promise.resolve({
type: StreamProviders.Manual,
name: this.name
} as StreamProviderInfo)
}
@ -23,4 +27,8 @@ export class ManualProvider implements StreamProvider {
System.BroadcastEvent(ev);
return Promise.resolve();
}
topup(amount: number): Promise<string> {
throw new Error("Method not implemented.");
}
}

View File

@ -13,6 +13,10 @@ export class Nip103StreamProvider implements StreamProvider {
return new URL(this.#url).host;
}
get type() {
return StreamProviders.NostrType
}
async info() {
const rsp = await this.#getJson<AccountResponse>("GET", "account");
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> {
const pub = await EventPublisher.nip7();
if (!pub) throw new Error("No event publisher");
@ -81,4 +90,8 @@ interface AccountResponse {
rate: number
remaining: number
}
}
interface TopUpResponse {
pr: string
}

View File

@ -15,6 +15,10 @@ export class OwncastProvider implements StreamProvider {
return new URL(this.#url).host
}
get type() {
return StreamProviders.Owncast
}
createConfig(): any & { type: StreamProviders; } {
return {
type: StreamProviders.Owncast,
@ -40,6 +44,10 @@ export class OwncastProvider implements StreamProvider {
} 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> {
const rsp = await fetch(`${this.#url}${path}`, {
method: method,