fix: run prettier

This commit is contained in:
verbiricha 2023-08-01 14:23:25 +02:00
parent e2399d1bec
commit ad2685b701
61 changed files with 1197 additions and 950 deletions

View File

@ -43,4 +43,3 @@ declare module "*.jpg" {
value: string | Uint8Array | number | undefined;
}
}

View File

@ -55,7 +55,7 @@ export function ChatMessage({
const login = useLogin();
const profile = useUserProfile(
System,
inView?.isIntersecting ? ev.pubkey : undefined,
inView?.isIntersecting ? ev.pubkey : undefined
);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const zaps = useMemo(() => {
@ -79,7 +79,7 @@ export function ChatMessage({
}, [zaps, ev]);
const hasZaps = totalZaps > 0;
const awardedBadges = badges.filter(
(b) => b.awardees.has(ev.pubkey) && b.accepted.has(ev.pubkey),
(b) => b.awardees.has(ev.pubkey) && b.accepted.has(ev.pubkey)
);
useOnClickOutside(ref, () => {

View File

@ -10,13 +10,26 @@ export interface CopyProps {
export default function Copy({ text, maxSize = 32, className }: CopyProps) {
const { copy, copied } = useCopy();
const sliceLength = maxSize / 2;
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
const trimmed =
text.length > maxSize
? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}`
: text;
return (
<div className={`copy${className ? ` ${className}` : ""}`} onClick={() => copy(text)}>
<div
className={`copy${className ? ` ${className}` : ""}`}
onClick={() => copy(text)}
>
<span className="body">{trimmed}</span>
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
{copied ? <Icon name="check" size={14} /> : <Icon name="copy" size={14} />}
<span
className="icon"
style={{ color: copied ? "var(--success)" : "var(--highlight)" }}
>
{copied ? (
<Icon name="check" size={14} />
) : (
<Icon name="copy" size={14} />
)}
</span>
</div>
);

View File

@ -14,7 +14,7 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
const login = useLogin();
const name = findTag(ev, "d");
const isUsed = login?.emojis.find(
(e) => e.author === ev.pubkey && e.name === name,
(e) => e.author === ev.pubkey && e.name === name
);
const emoji = ev.tags.filter((e) => e.at(0) === "emoji");
@ -23,7 +23,7 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
if (isUsed) {
newPacks =
login?.emojis.filter(
(e) => e.author !== ev.pubkey && e.name !== name,
(e) => e.author !== ev.pubkey && e.name !== name
) ?? [];
} else {
newPacks = [...(login?.emojis ?? []), toEmojiPack(ev)];

View File

@ -29,7 +29,7 @@
}
.goal .progress-indicator {
background-color: #FF8D2B;
background-color: #ff8d2b;
width: 100%;
height: 100%;
transition: transform 660ms cubic-bezier(0.65, 0, 0.35, 1);
@ -63,13 +63,13 @@
}
.goal .progress-container.finished .zap-circle {
background: #FF8D2B;
background: #ff8d2b;
}
.goal .goal-finished {
color: #FFFFFF;
color: #ffffff;
}
.goal .goal-unfinished {
color: #FFFFFF33;
color: #ffffff33;
}

View File

@ -115,7 +115,7 @@ export function LiveChat({
.filter((z) => z && z.valid);
const events = useMemo(() => {
return [...feed.messages, ...feed.zaps, ...awards].sort(
(a, b) => b.created_at - a.created_at,
(a, b) => b.created_at - a.created_at
);
}, [feed.messages, feed.zaps, awards]);
const streamer = getHost(ev);
@ -126,7 +126,7 @@ export function LiveChat({
findTag(ev, "d") ?? "",
undefined,
ev.kind,
ev.pubkey,
ev.pubkey
);
}
}, [ev]);
@ -146,7 +146,7 @@ export function LiveChat({
window.open(
`/chat/${naddr}?chat=true`,
"_blank",
"popup,width=400,height=800",
"popup,width=400,height=800"
)
}
/>
@ -182,7 +182,7 @@ export function LiveChat({
}
case EventKind.ZapReceipt: {
const zap = zaps.find(
(b) => b.id === a.id && b.receiver === streamer,
(b) => b.id === a.id && b.receiver === streamer
);
if (zap) {
return <ChatZap zap={zap} key={a.id} />;

View File

@ -8,22 +8,19 @@ export enum VideoStatus {
}
export interface VideoPlayerProps {
stream?: string, status?: string, poster?: string
stream?: string;
status?: string;
poster?: string;
}
export function LiveVideoPlayer(
props: VideoPlayerProps
) {
export function LiveVideoPlayer(props: VideoPlayerProps) {
const video = useRef<HTMLVideoElement>(null);
const streamCached = useMemo(() => props.stream, [props.stream]);
const [status, setStatus] = useState<VideoStatus>();
const [src, setSrc] = useState<string>();
useEffect(() => {
if (
streamCached &&
video.current
) {
if (streamCached && video.current) {
if (Hls.isSupported()) {
try {
const hls = new Hls();
@ -63,14 +60,25 @@ export function LiveVideoPlayer(
<div className={status}>
<div>{status}</div>
</div>
<video ref={video} autoPlay={true} poster={props.poster} src={src} playsInline={true} controls={status === VideoStatus.Online} />
<video
ref={video}
autoPlay={true}
poster={props.poster}
src={src}
playsInline={true}
controls={status === VideoStatus.Online}
/>
</div>
);
}
export function WebRTCPlayer(props: VideoPlayerProps) {
const video = useRef<HTMLVideoElement>(null);
const streamCached = useMemo(() => "https://customer-uu10flpvos4pfhgu.cloudflarestream.com/7634aee1af35a2de4ac13ca3d1718a8b/webRTC/play", [props.stream]);
const streamCached = useMemo(
() =>
"https://customer-uu10flpvos4pfhgu.cloudflarestream.com/7634aee1af35a2de4ac13ca3d1718a8b/webRTC/play",
[props.stream]
);
const [status] = useState<VideoStatus>();
//https://customer-uu10flpvos4pfhgu.cloudflarestream.com/7634aee1af35a2de4ac13ca3d1718a8b/webRTC/play
@ -78,14 +86,19 @@ export function WebRTCPlayer(props: VideoPlayerProps) {
if (video.current && streamCached) {
const client = new WISH();
client.addEventListener("log", console.debug);
client.WithEndpoint(streamCached, true)
client.WithEndpoint(streamCached, true);
client.Play().then(s => {
client
.Play()
.then((s) => {
if (video.current) {
video.current.srcObject = s;
}
}).catch(console.error);
return () => { client.Disconnect().catch(console.error); }
})
.catch(console.error);
return () => {
client.Disconnect().catch(console.error);
};
}
}, [video, streamCached]);
@ -94,7 +107,12 @@ export function WebRTCPlayer(props: VideoPlayerProps) {
<div className={status}>
<div>{status}</div>
</div>
<video ref={video} autoPlay={true} poster={props.poster} controls={status === VideoStatus.Online} />
<video
ref={video}
autoPlay={true}
poster={props.poster}
controls={status === VideoStatus.Online}
/>
</div>
);
}

View File

@ -15,7 +15,7 @@ import { LoginType } from "login";
enum Stage {
Login = 0,
Details = 1,
SaveKey = 2
SaveKey = 2,
}
export function LoginSignup({ close }: { close: () => void }) {
@ -56,14 +56,15 @@ export function LoginSignup({ close }: { close: () => void }) {
async function uploadAvatar() {
const file = await openFile();
if (file) {
const VoidCatHost = "https://void.cat"
const VoidCatHost = "https://void.cat";
const api = new VoidApi(VoidCatHost);
const uploader = api.getUploader(file);
const result = await uploader.upload({
"V-Strip-Metadata": "true"
})
"V-Strip-Metadata": "true",
});
if (result.ok) {
const resultUrl = result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
const resultUrl =
result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
setAvatar(resultUrl);
} else {
setError(result.errorMessage ?? "Upload failed");
@ -76,7 +77,7 @@ export function LoginSignup({ close }: { close: () => void }) {
const profile = {
name: username,
picture: avatar,
lud16: `${pub.pubKey}@zap.stream`
lud16: `${pub.pubKey}@zap.stream`,
} as UserMetadata;
const ev = await pub.metadata(profile);
@ -88,52 +89,87 @@ export function LoginSignup({ close }: { close: () => void }) {
switch (stage) {
case Stage.Login: {
return <>
return (
<>
<h2>Login</h2>
{"nostr" in window &&
<AsyncButton type="button" className="btn btn-primary" onClick={doLogin}>
{"nostr" in window && (
<AsyncButton
type="button"
className="btn btn-primary"
onClick={doLogin}
>
Nostr Extension
</AsyncButton>}
<button type="button" className="btn btn-primary" onClick={createAccount}>
</AsyncButton>
)}
<button
type="button"
className="btn btn-primary"
onClick={createAccount}
>
Create Account
</button>
{error && <b className="error">{error}</b>}
</>
);
}
case Stage.Details: {
return <>
return (
<>
<h2>Setup Profile</h2>
<div className="flex f-center">
<div className="avatar-input" onClick={uploadAvatar} style={{
"--img": `url(${avatar})`
} as CSSProperties}>
<div
className="avatar-input"
onClick={uploadAvatar}
style={
{
"--img": `url(${avatar})`,
} as CSSProperties
}
>
<Icon name="camera-plus" />
</div>
</div>
<div>
<div className="paper">
<input type="text" placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} />
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<small>You can change this later</small>
</div>
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}>
<AsyncButton
type="button"
className="btn btn-primary"
onClick={saveProfile}
>
Save
</AsyncButton>
</>
);
}
case Stage.SaveKey: {
return <>
return (
<>
<h2>Save Key</h2>
<p>
Nostr uses private keys, please save yours, if you lose this key you wont be able to login to your account anymore!
Nostr uses private keys, please save yours, if you lose this key you
wont be able to login to your account anymore!
</p>
<div className="paper">
<Copy text={hexToBech32("nsec", key)} />
</div>
<button type="button" className="btn btn-primary" onClick={loginWithKey}>
<button
type="button"
className="btn btn-primary"
onClick={loginWithKey}
>
Ok, it's safe
</button>
</>
);
}
}
}

View File

@ -2,7 +2,8 @@
color: var(--text-link);
}
.markdown > ul, .markdown > ol {
.markdown > ul,
.markdown > ol {
margin: 0;
padding: 0 12px;
font-size: 18px;

View File

@ -1,4 +1,3 @@
.new-stream {
display: flex;
flex-direction: column;

View File

@ -21,38 +21,56 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
}
}, [providers, currentProvider]);
function providerDialog() {
if (!currentProvider) return;
switch (currentProvider.type) {
case StreamProviders.Manual: {
return <StreamEditor onFinish={ex => {
return (
<StreamEditor
onFinish={(ex) => {
currentProvider.updateStreamInfo(ex);
if (!ev) {
navigate(eventLink(ex));
} else {
onFinish?.(ev);
}
}} ev={ev} />
}}
ev={ev}
/>
);
}
case StreamProviders.NostrType: {
return <NostrProviderDialog provider={currentProvider} onFinish={onFinish} ev={ev} />
return (
<NostrProviderDialog
provider={currentProvider}
onFinish={onFinish}
ev={ev}
/>
);
}
case StreamProviders.Owncast: {
return
return;
}
}
}
return <>
return (
<>
<p>Stream Providers</p>
<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>
{providerDialog()}
</>
);
}
interface NewStreamDialogProps {
@ -60,7 +78,9 @@ interface NewStreamDialogProps {
btnClassName?: string;
}
export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps) {
export function NewStreamDialog(
props: NewStreamDialogProps & StreamEditorProps
) {
const [open, setOpen] = useState(false);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>

View File

@ -1,48 +1,68 @@
import { NostrEvent } from "@snort/system";
import { StreamProvider, StreamProviderEndpoint, StreamProviderInfo } from "providers";
import {
StreamProvider,
StreamProviderEndpoint,
StreamProviderInfo,
} from "providers";
import { useEffect, useState } from "react";
import { SendZaps } from "./send-zap";
import { StreamEditor, StreamEditorProps } from "./stream-editor";
import Spinner from "./spinner";
import { LIVE_STREAM } from "const";
const DummyEvent = { content: "", id: "", pubkey: "", sig: "", kind: LIVE_STREAM, created_at: 0, tags: [] } as NostrEvent;
const DummyEvent = {
content: "",
id: "",
pubkey: "",
sig: "",
kind: LIVE_STREAM,
created_at: 0,
tags: [],
} as NostrEvent;
export function NostrProviderDialog({ provider, ...others }: { provider: StreamProvider } & StreamEditorProps) {
export function NostrProviderDialog({
provider,
...others
}: { provider: StreamProvider } & StreamEditorProps) {
const [topup, setTopup] = useState(false);
const [info, setInfo] = useState<StreamProviderInfo>();
const [ep, setEndpoint] = useState<StreamProviderEndpoint>();
function sortEndpoints(arr: Array<StreamProviderEndpoint>) {
return arr.sort((a, b) => (a.rate ?? 0) > (b.rate ?? 0) ? -1 : 1);
return arr.sort((a, b) => ((a.rate ?? 0) > (b.rate ?? 0) ? -1 : 1));
}
useEffect(() => {
provider.info().then(v => {
provider.info().then((v) => {
setInfo(v);
setEndpoint(sortEndpoints(v.endpoints)[0]);
});
}, [provider]);
if (!info) {
return <Spinner />
return <Spinner />;
}
if (topup) {
return <SendZaps lnurl={{
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 => {
},
}}
onFinish={() => {
provider.info().then((v) => {
setInfo(v);
setTopup(false);
});
}} />
}}
/>
);
}
function calcEstimate() {
@ -50,9 +70,9 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
const raw = Math.max(0, info.balance / ep.rate);
if (ep.unit === "min" && raw > 60) {
return `${(raw / 60).toFixed(0)} hour @ ${ep.rate} sats/${ep.unit}`
return `${(raw / 60).toFixed(0)} hour @ ${ep.rate} sats/${ep.unit}`;
}
return `${raw.toFixed(0)} ${ep.unit} @ ${ep.rate} sats/${ep.unit}`
return `${raw.toFixed(0)} ${ep.unit} @ ${ep.rate} sats/${ep.unit}`;
}
function parseCapability(cap: string) {
@ -68,16 +88,23 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
}
const streamEvent = others.ev ?? info.publishedEvent ?? DummyEvent;
return <>
{info.endpoints.length > 1 && <div>
return (
<>
{info.endpoints.length > 1 && (
<div>
<p>Endpoint</p>
<div className="flex g12">
{sortEndpoints(info.endpoints).map(a => <span className={`pill${ep?.name === a.name ? " active" : ""}`}
onClick={() => setEndpoint(a)}>
{sortEndpoints(info.endpoints).map((a) => (
<span
className={`pill${ep?.name === a.name ? " active" : ""}`}
onClick={() => setEndpoint(a)}
>
{a.name}
</span>)}
</span>
))}
</div>
</div>}
</div>
)}
<div>
<p>Stream Url</p>
<div className="paper">
@ -90,7 +117,10 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
<div className="paper f-grow">
<input type="password" value={ep?.key} disabled />
</div>
<button className="btn btn-primary" onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}>
<button
className="btn btn-primary"
onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}
>
Copy
</button>
</div>
@ -110,15 +140,24 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
<div>
<p>Resolutions</p>
<div className="flex g12">
{ep?.capabilities?.map(a => <span className="pill">{parseCapability(a)}</span>)}
{ep?.capabilities?.map((a) => (
<span className="pill">{parseCapability(a)}</span>
))}
</div>
</div>
{streamEvent && <StreamEditor onFinish={(ex) => {
{streamEvent && (
<StreamEditor
onFinish={(ex) => {
provider.updateStreamInfo(ex);
others.onFinish?.(ex);
}} ev={streamEvent} options={{
}}
ev={streamEvent}
options={{
canSetStream: false,
canSetStatus: false
}} />}
canSetStatus: false,
}}
/>
)}
</>
);
}

View File

@ -11,7 +11,7 @@
width: 40px;
height: 40px;
border-radius: 100%;
background: #A7A7A7;
background: #a7a7a7;
border: unset;
outline: unset;
object-fit: cover;

View File

@ -21,7 +21,7 @@ export interface LNURLLike {
getInvoice(
amountInSats: number,
comment?: string,
zap?: NostrEvent,
zap?: NostrEvent
): Promise<{ pr?: string }>;
}
@ -79,7 +79,7 @@ export function SendZaps({
let isAnon = false;
if (!pub) {
pub = EventPublisher.privateKey(
bytesToHex(secp256k1.utils.randomPrivateKey()),
bytesToHex(secp256k1.utils.randomPrivateKey())
);
isAnon = true;
}
@ -104,7 +104,7 @@ export function SendZaps({
eb.tag(["anon", ""]);
}
return eb;
},
}
);
}
const invoice = await svc.getInvoice(amountInSats, comment, zap);

View File

@ -7,7 +7,13 @@ export interface IconProps {
}
const Spinner = (props: IconProps) => (
<svg width="20" height="20" stroke="currentColor" viewBox="0 0 20 20" {...props}>
<svg
width="20"
height="20"
stroke="currentColor"
viewBox="0 0 20 20"
{...props}
>
<g className="spinner_V8m1">
<circle cx="10" cy="10" r="7.5" fill="none" strokeWidth="3"></circle>
</g>

View File

@ -2,5 +2,9 @@ import "./state-pill.css";
import { StreamState } from "index";
export function StatePill({ state }: { state: StreamState }) {
return <span className={`state pill${state === StreamState.Live ? " live" : ""}`}>{state}</span>
return (
<span className={`state pill${state === StreamState.Live ? " live" : ""}`}>
{state}
</span>
);
}

View File

@ -59,7 +59,7 @@ const CardPreview = forwardRef(
<Markdown content={content} />
</div>
);
},
}
);
interface CardProps {
@ -96,7 +96,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
};
},
}),
[canEdit, identifier],
[canEdit, identifier]
);
function findTagByIdentifier(d: string) {
@ -147,7 +147,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
}
},
}),
[canEdit, tags, identifier],
[canEdit, tags, identifier]
);
const card = (

View File

@ -16,5 +16,5 @@
.content-warning {
padding: 16px;
border-radius: 16px;
border: 1px solid #FF563F;
border: 1px solid #ff563f;
}

View File

@ -13,14 +13,14 @@ export interface StreamEditorProps {
ev?: NostrEvent;
onFinish?: (ev: NostrEvent) => void;
options?: {
canSetTitle?: boolean
canSetSummary?: boolean
canSetImage?: boolean
canSetStatus?: boolean
canSetStream?: boolean
canSetTags?: boolean
canSetContentWarning?: boolean
}
canSetTitle?: boolean;
canSetSummary?: boolean;
canSetImage?: boolean;
canSetStatus?: boolean;
canSetStream?: boolean;
canSetTags?: boolean;
canSetContentWarning?: boolean;
};
}
export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
@ -42,7 +42,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
setStream(findTag(ev, "streaming") ?? "");
setStatus(findTag(ev, "status") ?? StreamState.Live);
setStart(findTag(ev, "starts"));
setTags(ev?.tags.filter(a => a[0] === "t").map(a => a[1]) ?? []);
setTags(ev?.tags.filter((a) => a[0] === "t").map((a) => a[1]) ?? []);
setContentWarning(findTag(ev, "content-warning") !== undefined);
}, [ev?.id]);
@ -86,7 +86,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
eb.tag(["t", tx.trim()]);
}
if (contentWarning) {
eb.tag(["content-warning", "nsfw"])
eb.tag(["content-warning", "nsfw"]);
}
return eb;
});
@ -106,48 +106,62 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
return (
<>
<h3>{ev ? "Edit Stream" : "New Stream"}</h3>
{(options?.canSetTitle ?? true) && <div>
{(options?.canSetTitle ?? true) && (
<div>
<p>Title</p>
<div className="paper">
<input
type="text"
placeholder="What are we steaming today?"
value={title}
onChange={(e) => setTitle(e.target.value)} />
onChange={(e) => setTitle(e.target.value)}
/>
</div>
</div>}
{(options?.canSetSummary ?? true) && <div>
</div>
)}
{(options?.canSetSummary ?? true) && (
<div>
<p>Summary</p>
<div className="paper">
<input
type="text"
placeholder="A short description of the content"
value={summary}
onChange={(e) => setSummary(e.target.value)} />
onChange={(e) => setSummary(e.target.value)}
/>
</div>
</div>}
{(options?.canSetImage ?? true) && <div>
</div>
)}
{(options?.canSetImage ?? true) && (
<div>
<p>Cover image</p>
<div className="paper">
<input
type="text"
placeholder="https://"
value={image}
onChange={(e) => setImage(e.target.value)} />
onChange={(e) => setImage(e.target.value)}
/>
</div>
</div>}
{(options?.canSetStream ?? true) && <div>
</div>
)}
{(options?.canSetStream ?? true) && (
<div>
<p>Stream Url</p>
<div className="paper">
<input
type="text"
placeholder="https://"
value={stream}
onChange={(e) => setStream(e.target.value)} />
onChange={(e) => setStream(e.target.value)}
/>
</div>
<small>Stream type should be HLS</small>
</div>}
{(options?.canSetStatus ?? true) && <><div>
</div>
)}
{(options?.canSetStatus ?? true) && (
<>
<div>
<p>Status</p>
<div className="flex g12">
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(
@ -170,11 +184,17 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
<input
type="datetime-local"
value={toDateTimeString(Number(start ?? "0"))}
onChange={(e) => setStart(fromDateTimeString(e.target.value).toString())} />
onChange={(e) =>
setStart(fromDateTimeString(e.target.value).toString())
}
/>
</div>
</div>
)}</>}
{(options?.canSetTags ?? true) && <div>
)}
</>
)}
{(options?.canSetTags ?? true) && (
<div>
<p>Tags</p>
<div className="paper">
<TagsInput
@ -184,16 +204,23 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
separators={["Enter", ","]}
/>
</div>
</div>}
{(options?.canSetContentWarning ?? true) && <div className="flex g12 content-warning">
</div>
)}
{(options?.canSetContentWarning ?? true) && (
<div className="flex g12 content-warning">
<div>
<input type="checkbox" checked={contentWarning} onChange={e => setContentWarning(e.target.checked)} />
<input
type="checkbox"
checked={contentWarning}
onChange={(e) => setContentWarning(e.target.checked)}
/>
</div>
<div>
<div className="warning">NSFW Content</div>
Check here if this stream contains nudity or pornographic content.
</div>
</div>}
</div>
)}
<div>
<AsyncButton
type="button"

View File

@ -11,7 +11,9 @@ export function StreamTimer({ ev }: { ev?: NostrEvent }) {
const diff = unixNow() - starts;
const hours = Number(diff / 60.0 / 60.0);
const mins = Number((diff / 60) % 60);
setTime(`${hours.toFixed(0).padStart(2, "0")}:${mins.toFixed(0).padStart(2, "0")}`);
setTime(
`${hours.toFixed(0).padStart(2, "0")}:${mins.toFixed(0).padStart(2, "0")}`
);
}
useEffect(() => {
@ -22,5 +24,5 @@ export function StreamTimer({ ev }: { ev?: NostrEvent }) {
return () => clearInterval(t);
}, []);
return time
return time;
}

View File

@ -10,10 +10,11 @@
}
.rta__entity--selected .emoji-item {
text-decoration: none;
background: #F838D9;
background: #f838d9;
}
.emoji-item, .user-item {
.emoji-item,
.user-item {
color: white;
background: #171717;
display: flex;
@ -24,7 +25,8 @@
padding: 10px;
}
.emoji-item:hover, .user-item:hover {
.emoji-item:hover,
.user-item:hover {
color: #171717;
background: white;
}

View File

@ -22,6 +22,6 @@
.toggle:hover svg {
color: white;
}
.toggle[data-state='on'] svg {
.toggle[data-state="on"] svg {
color: var(--text-link);
}

View File

@ -35,7 +35,7 @@ export function VideoTile({
id,
undefined,
ev.kind,
ev.pubkey,
ev.pubkey
);
return (
<div className="video-tile-container">

View File

@ -41,7 +41,7 @@ export function WriteMessage({
const reply = await pub?.generic((eb) => {
const emoji = [...emojiNames].map((name) =>
emojis.find((e) => e.at(1) === name),
emojis.find((e) => e.at(1) === name)
);
eb.kind(LIVE_STREAM_CHAT as EventKind)
.content(chat)

View File

@ -16,7 +16,7 @@ import type { Badge } from "types";
export function useBadges(
pubkey: string,
leaveOpen = true,
leaveOpen = true
): { badges: Badge[]; awards: TaggedRawEvent[] } {
const since = useMemo(() => unixNow() - WEEK, [pubkey]);
const rb = useMemo(() => {
@ -33,7 +33,7 @@ export function useBadges(
const { data: badgeEvents } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
rb,
rb
);
const rawBadges = useMemo(() => {
@ -64,7 +64,7 @@ export function useBadges(
const acceptedStream = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
acceptedSub,
acceptedSub
);
const acceptedEvents = acceptedStream.data ?? [];
@ -73,18 +73,18 @@ export function useBadges(
const name = findTag(e, "d") ?? "";
const address = toAddress(e);
const awardEvents = badgeAwards.filter(
(b) => findTag(b, "a") === address,
(b) => findTag(b, "a") === address
);
const awardees = new Set(
awardEvents.map((e) => getTagValues(e.tags, "p")).flat(),
awardEvents.map((e) => getTagValues(e.tags, "p")).flat()
);
const accepted = new Set(
acceptedEvents
.filter((pb) => awardees.has(pb.pubkey))
.filter((pb) =>
pb.tags.find((t) => t.at(0) === "a" && t.at(1) === address),
pb.tags.find((t) => t.at(0) === "a" && t.at(1) === address)
)
.map((pb) => pb.pubkey),
.map((pb) => pb.pubkey)
);
const thumb = findTag(e, "thumb");
const image = findTag(e, "image");

View File

@ -15,13 +15,13 @@ import { System } from "index";
export function useUserCards(
pubkey: string,
userCards: Array<string[]>,
leaveOpen = false,
leaveOpen = false
): TaggedRawEvent[] {
const related = useMemo(() => {
// filtering to only show CARD kinds for now, but in the future we could link and render anything
if (userCards?.length > 0) {
return userCards.filter(
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${CARD}:`),
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${CARD}:`)
);
}
return [];
@ -52,7 +52,7 @@ export function useUserCards(
const { data } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
subRelated,
subRelated
);
const cards = useMemo(() => {
@ -64,7 +64,7 @@ export function useUserCards(
(e) =>
e.kind === kind &&
e.pubkey === pubkey &&
findTag(e, "d") === identifier,
findTag(e, "d") === identifier
);
})
.filter((e) => e)
@ -89,14 +89,14 @@ export function useCards(pubkey: string, leaveOpen = false): TaggedRawEvent[] {
const { data: userCards } = useRequestBuilder<ReplaceableNoteStore>(
System,
ReplaceableNoteStore,
sub,
sub
);
const related = useMemo(() => {
// filtering to only show CARD kinds for now, but in the future we could link and render anything
if (userCards) {
return userCards.tags.filter(
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${CARD}:`),
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${CARD}:`)
);
}
return [];
@ -127,7 +127,7 @@ export function useCards(pubkey: string, leaveOpen = false): TaggedRawEvent[] {
const { data } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
subRelated,
subRelated
);
const cardEvents = data ?? [];
@ -140,7 +140,7 @@ export function useCards(pubkey: string, leaveOpen = false): TaggedRawEvent[] {
(e) =>
e.kind === kind &&
e.pubkey === pubkey &&
findTag(e, "d") === identifier,
findTag(e, "d") === identifier
);
})
.filter((e) => e)

View File

@ -37,7 +37,7 @@ export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
const related = useMemo(() => {
if (userEmoji) {
return userEmoji?.filter(
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`),
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`)
);
}
return [];
@ -67,7 +67,7 @@ export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
const { data: relatedData } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
subRelated,
subRelated
);
const emojiPacks = useMemo(() => {
@ -95,7 +95,7 @@ export default function useEmoji(pubkey?: string) {
const { data: userEmoji } = useRequestBuilder<ReplaceableNoteStore>(
System,
ReplaceableNoteStore,
sub,
sub
);
const emojis = useUserEmojiPacks(pubkey, userEmoji?.tags ?? []);

View File

@ -20,7 +20,7 @@ export function useAddress(kind: number, pubkey: string, identifier: string) {
const { data } = useRequestBuilder<ReplaceableNoteStore>(
System,
ReplaceableNoteStore,
sub,
sub
);
return data;
@ -52,7 +52,7 @@ export function useEvent(link: NostrLink) {
const { data } = useRequestBuilder<ReplaceableNoteStore>(
System,
ReplaceableNoteStore,
sub,
sub
);
return data;

View File

@ -26,10 +26,14 @@ export function useZaps(goal: NostrEvent, leaveOpen = false) {
const { data } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
sub,
sub
);
return data?.map((ev) => parseZap(ev, System.ProfileLoader.Cache)).filter((z) => z && z.valid) ?? [];
return (
data
?.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
.filter((z) => z && z.valid) ?? []
);
}
export function useZapGoal(host: string, link: NostrLink, leaveOpen = false) {
@ -46,7 +50,7 @@ export function useZapGoal(host: string, link: NostrLink, leaveOpen = false) {
const { data } = useRequestBuilder<ReplaceableNoteStore>(
System,
ReplaceableNoteStore,
sub,
sub
);
return data;

View File

@ -55,7 +55,7 @@ export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>) {
const reactionsSub = useRequestBuilder<FlatNoteStore>(
System,
FlatNoteStore,
esub,
esub
);
const reactions = reactionsSub.data ?? [];

View File

@ -43,10 +43,10 @@ export function useStreamsFeed(tag?: string) {
}, [feed.data]);
const live = feedSorted.filter(
(a) => findTag(a, "status") === StreamState.Live,
(a) => findTag(a, "status") === StreamState.Live
);
const planned = feedSorted.filter(
(a) => findTag(a, "status") === StreamState.Planned,
(a) => findTag(a, "status") === StreamState.Planned
);
const ended = feedSorted.filter((a) => {
const hasEnded = findTag(a, "status") === StreamState.Ended;

View File

@ -12,7 +12,7 @@ import { getPublisher } from "login";
export function useLogin() {
const session = useSyncExternalStore(
(c) => Login.hook(c),
() => Login.snapshot(),
() => Login.snapshot()
);
if (!session) return;
return {
@ -27,7 +27,7 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
const [userEmojis, setUserEmojis] = useState<Tags>([]);
const session = useSyncExternalStore(
(c) => Login.hook(c),
() => Login.snapshot(),
() => Login.snapshot()
);
const sub = useMemo(() => {
@ -45,7 +45,7 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
const { data } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
sub,
sub
);
useEffect(() => {

View File

@ -27,8 +27,7 @@ export function useProfile(link: NostrLink, leaveOpen = false) {
return b;
}, [link, leaveOpen]);
const { data: streamsData } =
useRequestBuilder<NoteCollection>(
const { data: streamsData } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
sub

View File

@ -2,5 +2,8 @@ import { StreamProviderStore } from "providers";
import { useSyncExternalStore } from "react";
export function useStreamProvider() {
return useSyncExternalStore(c => StreamProviderStore.hook(c), () => StreamProviderStore.snapshot());
return useSyncExternalStore(
(c) => StreamProviderStore.hook(c),
() => StreamProviderStore.snapshot()
);
}

View File

@ -13,8 +13,8 @@ body {
--gap-s: 16px;
--header-height: 48px;
--text-muted: #797979;
--text-link: #F838D9;
--text-danger: #FF563F;
--text-link: #f838d9;
--text-danger: #ff563f;
--border: #333;
}

View File

@ -66,10 +66,10 @@ const router = createBrowserRouter([
},
]);
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLDivElement,
document.getElementById("root") as HTMLDivElement
);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
</React.StrictMode>
);

View File

@ -132,7 +132,7 @@ export function getPublisher(session: LoginSession) {
case LoginType.PrivateKey: {
return new EventPublisher(
new PrivateKeySigner(session.privateKey!),
session.pubkey,
session.pubkey
);
}
}

View File

@ -10,7 +10,15 @@ export function ChatPopout() {
const link = parseNostrLink(params.id!);
const ev = useCurrentStreamFeed(link, true);
const lnk = parseNostrLink(encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev?.kind, ev?.pubkey));
const lnk = parseNostrLink(
encodeTLV(
NostrPrefix.Address,
findTag(ev, "d") ?? "",
undefined,
ev?.kind,
ev?.pubkey
)
);
const chat = Boolean(new URL(window.location.href).searchParams.get("chat"));
return (
<div className={`popout-chat${chat ? "" : " embed"}`}>

View File

@ -3,7 +3,6 @@
justify-content: center;
}
@media (min-width: 768px) {
.profile-page .profile-container {
width: 620px;
@ -19,7 +18,6 @@
border-radius: 16px;
}
@media (min-width: 768px) {
.profile-page .banner {
height: 348.75px;
@ -31,7 +29,7 @@
width: 88px;
height: 88px;
border-radius: 88px;
border: 3px solid #FFF;
border: 3px solid #fff;
object-fit: cover;
margin-left: 16px;
margin-top: -40px;
@ -65,7 +63,7 @@
.profile-page .name {
margin: 0;
color: #FFF;
color: #fff;
font-size: 21px;
font-style: normal;
font-weight: 600;
@ -74,7 +72,7 @@
.profile-page .bio {
margin: 0;
color: #ADADAD;
color: #adadad;
font-size: 16px;
font-style: normal;
font-weight: 500;
@ -124,10 +122,10 @@
}
.tabs-tab {
background: #0A0A0A;
background: #0a0a0a;
background-clip: padding-box;
color: white;
border: 1px solid #0A0A0A;
border: 1px solid #0a0a0a;
border-bottom: 1px solid transparent;
position: relative;
cursor: pointer;
@ -158,9 +156,14 @@
width: 100%;
}
.tabs-tab[data-state='active'] .tab-border {
.tabs-tab[data-state="active"] .tab-border {
height: 1px;
background: linear-gradient(94.73deg, #2BD9FF 0%, #8C8DED 47.4%, #F838D9 100%);
background: linear-gradient(
94.73deg,
#2bd9ff 0%,
#8c8ded 47.4%,
#f838d9 100%
);
}
.tabs-content {
@ -220,7 +223,7 @@
}
.stream-item .timestamp {
color: #ADADAD;
color: #adadad;
font-size: 16px;
font-style: normal;
font-weight: 500;

View File

@ -63,7 +63,7 @@ export function ProfilePage() {
}, [streams]);
const futureStreams = useMemo(() => {
return streams.filter(
(ev) => findTag(ev, "status") === StreamState.Planned,
(ev) => findTag(ev, "status") === StreamState.Planned
);
}, [streams]);
const isLive = Boolean(liveEvent);
@ -76,7 +76,7 @@ export function ProfilePage() {
d,
undefined,
liveEvent.kind,
liveEvent.pubkey,
liveEvent.pubkey
);
navigate(`/${naddr}`);
}
@ -115,7 +115,7 @@ export function ProfilePage() {
liveEvent
? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(
liveEvent,
"d",
"d"
)}`
: undefined
}
@ -173,7 +173,7 @@ export function ProfilePage() {
<span className="timestamp">
Streamed on{" "}
{moment(Number(ev.created_at) * 1000).format(
"MMM DD, YYYY",
"MMM DD, YYYY"
)}
</span>
</div>
@ -188,7 +188,7 @@ export function ProfilePage() {
<span className="timestamp">
Scheduled for{" "}
{moment(Number(ev.created_at) * 1000).format(
"MMM DD, YYYY h:mm:ss a",
"MMM DD, YYYY h:mm:ss a"
)}
</span>
</div>

View File

@ -13,38 +13,54 @@ export function StreamProvidersPage() {
function mapName(p: StreamProviders) {
switch (p) {
case StreamProviders.Owncast: return "Owncast"
case StreamProviders.Cloudflare: return "Cloudflare"
case StreamProviders.NostrType: return "Nostr Native"
case StreamProviders.Owncast:
return "Owncast";
case StreamProviders.Cloudflare:
return "Cloudflare";
case StreamProviders.NostrType:
return "Nostr Native";
}
return "Unknown"
return "Unknown";
}
function mapLogo(p: StreamProviders) {
switch (p) {
case StreamProviders.Owncast: return <img src={Owncast} />
case StreamProviders.Cloudflare: return <img src={Cloudflare} />
case StreamProviders.Owncast:
return <img src={Owncast} />;
case StreamProviders.Cloudflare:
return <img src={Cloudflare} />;
}
}
function providerLink(p: StreamProviders) {
return <div className="paper">
return (
<div className="paper">
<h3>{mapName(p)}</h3>
{mapLogo(p)}
<button className="btn btn-border" onClick={() => navigate(p)}>
+ Configure
</button>
</div>
);
}
function index() {
return <div className="stream-providers-page">
return (
<div className="stream-providers-page">
<h1>Providers</h1>
<p>Stream providers streamline the process of streaming on Nostr, some event accept lightning payments!</p>
<p>
Stream providers streamline the process of streaming on Nostr, some
event accept lightning payments!
</p>
<div className="stream-providers-grid">
{[StreamProviders.NostrType, StreamProviders.Owncast, StreamProviders.Cloudflare].map(v => providerLink(v))}
{[
StreamProviders.NostrType,
StreamProviders.Owncast,
StreamProviders.Cloudflare,
].map((v) => providerLink(v))}
</div>
</div>
);
}
if (!id) {
@ -52,10 +68,10 @@ export function StreamProvidersPage() {
} else {
switch (id) {
case StreamProviders.Owncast: {
return <ConfigureOwncast />
return <ConfigureOwncast />;
}
case StreamProviders.NostrType: {
return <ConfigureNostrType />
return <ConfigureNostrType />;
}
}
}

View File

@ -25,60 +25,68 @@ export function ConfigureNostrType() {
function status() {
if (!info) return;
return <>
return (
<>
<h3>Status</h3>
<div>
<StatePill state={info?.state ?? StreamState.Ended} />
</div>
<div>
<p>Name</p>
<div className="paper">
{info?.name}
<div className="paper">{info?.name}</div>
</div>
</div>
{info?.summary && <div>
<p>Summary</p>
<div className="paper">
{info?.summary}
</div>
</div>}
{info?.viewers && <div>
<p>Viewers</p>
<div className="paper">
{info?.viewers}
</div>
</div>}
{info?.version && <div>
<p>Version</p>
<div className="paper">
{info?.version}
</div>
</div>}
{info?.summary && (
<div>
<button className="btn btn-border" onClick={() => {
<p>Summary</p>
<div className="paper">{info?.summary}</div>
</div>
)}
{info?.viewers && (
<div>
<p>Viewers</p>
<div className="paper">{info?.viewers}</div>
</div>
)}
{info?.version && (
<div>
<p>Version</p>
<div className="paper">{info?.version}</div>
</div>
)}
<div>
<button
className="btn btn-border"
onClick={() => {
StreamProviderStore.add(new Nip103StreamProvider(url));
navigate("/");
}}>
}}
>
Save
</button>
</div>
</>
);
}
return <div className="owncast-config">
return (
<div className="owncast-config">
<div className="flex f-col g24">
<div>
<p>Nostr streaming provider URL</p>
<div className="paper">
<input type="text" placeholder="https://" value={url} onChange={e => setUrl(e.target.value)} />
<input
type="text"
placeholder="https://"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div>
</div>
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
Connect
</AsyncButton>
</div>
<div>
{status()}
</div>
<div>{status()}</div>
</div>
);
}

View File

@ -17,8 +17,7 @@ export function ConfigureOwncast() {
const api = new OwncastProvider(url, token);
const i = await api.info();
setInfo(i);
}
catch (e) {
} catch (e) {
console.debug(e);
}
}
@ -26,66 +25,78 @@ export function ConfigureOwncast() {
function status() {
if (!info) return;
return <>
return (
<>
<h3>Status</h3>
<div>
<StatePill state={info?.state ?? StreamState.Ended} />
</div>
<div>
<p>Name</p>
<div className="paper">
{info?.name}
<div className="paper">{info?.name}</div>
</div>
</div>
{info?.summary && <div>
<p>Summary</p>
<div className="paper">
{info?.summary}
</div>
</div>}
{info?.viewers && <div>
<p>Viewers</p>
<div className="paper">
{info?.viewers}
</div>
</div>}
{info?.version && <div>
<p>Version</p>
<div className="paper">
{info?.version}
</div>
</div>}
{info?.summary && (
<div>
<button className="btn btn-border" onClick={() => {
<p>Summary</p>
<div className="paper">{info?.summary}</div>
</div>
)}
{info?.viewers && (
<div>
<p>Viewers</p>
<div className="paper">{info?.viewers}</div>
</div>
)}
{info?.version && (
<div>
<p>Version</p>
<div className="paper">{info?.version}</div>
</div>
)}
<div>
<button
className="btn btn-border"
onClick={() => {
StreamProviderStore.add(new OwncastProvider(url, token));
navigate("/");
}}>
}}
>
Save
</button>
</div>
</>
);
}
return <div className="owncast-config">
return (
<div className="owncast-config">
<div className="flex f-col g24">
<div>
<p>Owncast instance url</p>
<div className="paper">
<input type="text" placeholder="https://" value={url} onChange={e => setUrl(e.target.value)} />
<input
type="text"
placeholder="https://"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div>
</div>
<div>
<p>API token</p>
<div className="paper">
<input type="password" value={token} onChange={e => setToken(e.target.value)} />
<input
type="password"
value={token}
onChange={(e) => setToken(e.target.value)}
/>
</div>
</div>
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
Connect
</AsyncButton>
</div>
<div>
{status()}
</div>
<div>{status()}</div>
</div>
);
}

View File

@ -16,7 +16,7 @@ export function RootPage() {
(ev: NostrEvent) => {
return login?.follows.tags.find((t) => t.at(1) === getHost(ev));
},
[login?.follows],
[login?.follows]
);
const hashtags = getTagValues(login?.follows.tags ?? [], "t");
const following = live.filter(followsHost);

View File

@ -116,7 +116,10 @@ export function StreamPage() {
const summary = findTag(ev, "summary");
const image = findTag(ev, "image");
const status = findTag(ev, "status");
const stream = status === StreamState.Live ? findTag(ev, "streaming") : findTag(ev, "recording");
const stream =
status === StreamState.Live
? findTag(ev, "streaming")
: findTag(ev, "recording");
const contentWarning = findTag(ev, "content-warning");
const tags = ev?.tags.filter((a) => a[0] === "t").map((a) => a[1]) ?? [];

View File

@ -78,7 +78,7 @@ export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
}
case StreamProviders.Owncast: {
this.#providers.push(
new OwncastProvider(c.url as string, c.token as string),
new OwncastProvider(c.url as string, c.token as string)
);
break;
}
@ -95,7 +95,7 @@ export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
takeSnapshot() {
const defaultProvider = new Nip103StreamProvider(
"https://api.zap.stream/api/nostr/",
"https://api.zap.stream/api/nostr/"
);
return [defaultProvider, new ManualProvider(), ...this.#providers];
}

View File

@ -4,23 +4,23 @@ import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers";
export class ManualProvider implements StreamProvider {
get name(): string {
return "Manual"
return "Manual";
}
get type() {
return StreamProviders.Manual
return StreamProviders.Manual;
}
info(): Promise<StreamProviderInfo> {
return Promise.resolve({
name: this.name
} as StreamProviderInfo)
name: this.name,
} as StreamProviderInfo);
}
createConfig() {
return {
type: StreamProviders.Manual
}
type: StreamProviders.Manual,
};
}
updateStreamInfo(ev: NostrEvent): Promise<void> {

View File

@ -1,11 +1,16 @@
import { StreamProvider, StreamProviderEndpoint, StreamProviderInfo, StreamProviders } from ".";
import {
StreamProvider,
StreamProviderEndpoint,
StreamProviderInfo,
StreamProviders,
} from ".";
import { EventKind, NostrEvent } from "@snort/system";
import { Login } from "index";
import { getPublisher } from "login";
import { findTag } from "utils";
export class Nip103StreamProvider implements StreamProvider {
#url: string
#url: string;
constructor(url: string) {
this.#url = url;
@ -16,7 +21,7 @@ export class Nip103StreamProvider implements StreamProvider {
}
get type() {
return StreamProviders.NostrType
return StreamProviders.NostrType;
}
async info() {
@ -30,87 +35,99 @@ export class Nip103StreamProvider implements StreamProvider {
viewers: 0,
publishedEvent: rsp.event,
balance: rsp.balance,
endpoints: rsp.endpoints.map(a => {
endpoints: rsp.endpoints.map((a) => {
return {
name: a.name,
url: a.url,
key: a.key,
rate: a.cost.rate,
unit: a.cost.unit,
capabilities: a.capabilities
} as StreamProviderEndpoint
})
} as StreamProviderInfo
capabilities: a.capabilities,
} as StreamProviderEndpoint;
}),
} as StreamProviderInfo;
}
createConfig() {
return {
type: StreamProviders.NostrType,
url: this.#url
}
url: this.#url,
};
}
async updateStreamInfo(ev: NostrEvent): Promise<void> {
const title = findTag(ev, "title");
const summary = findTag(ev, "summary");
const image = findTag(ev, "image");
const tags = ev?.tags.filter(a => a[0] === "t").map(a => a[1]);
const tags = ev?.tags.filter((a) => a[0] === "t").map((a) => a[1]);
const contentWarning = findTag(ev, "content-warning");
await this.#getJson("PATCH", "event", {
title, summary, image, tags, content_warning: contentWarning
title,
summary,
image,
tags,
content_warning: contentWarning,
});
}
async topup(amount: number): Promise<string> {
const rsp = await this.#getJson<TopUpResponse>("GET", `topup?amount=${amount}`);
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 login = Login.snapshot();
const pub = login && getPublisher(login);
if (!pub) throw new Error("No signer");
const u = `${this.#url}${path}`;
const token = await pub.generic(eb => {
return eb.kind(EventKind.HttpAuthentication)
const token = await pub.generic((eb) => {
return eb
.kind(EventKind.HttpAuthentication)
.content("")
.tag(["u", u])
.tag(["method", method])
.tag(["method", method]);
});
const rsp = await fetch(u, {
method: method,
body: body ? JSON.stringify(body) : undefined,
headers: {
"content-type": "application/json",
"authorization": `Nostr ${btoa(JSON.stringify(token))}`
authorization: `Nostr ${btoa(JSON.stringify(token))}`,
},
});
const json = await rsp.text();
if (!rsp.ok) {
throw new Error(json);
}
return json.length > 0 ? JSON.parse(json) as T : {} as T;
return json.length > 0 ? (JSON.parse(json) as T) : ({} as T);
}
}
interface AccountResponse {
balance: number
event?: NostrEvent
endpoints: Array<IngestEndpoint>
balance: number;
event?: NostrEvent;
endpoints: Array<IngestEndpoint>;
}
interface IngestEndpoint {
name: string
url: string
key: string
name: string;
url: string;
key: string;
cost: {
unit: string
rate: number
}
capabilities: Array<string>
unit: string;
rate: number;
};
capabilities: Array<string>;
}
interface TopUpResponse {
pr: string
pr: string;
}

View File

@ -51,7 +51,7 @@ export class OwncastProvider implements StreamProvider {
async #getJson<T>(
method: "GET" | "POST",
path: string,
body?: unknown,
body?: unknown
): Promise<T> {
const rsp = await fetch(`${this.#url}${path}`, {
method: method,

View File

@ -10,14 +10,23 @@ clientsClaim();
const staticTypes = ["image", "video", "audio", "script", "style", "font"];
registerRoute(
({ request, url }) => url.origin === self.location.origin && staticTypes.includes(request.destination),
({ request, url }) =>
url.origin === self.location.origin &&
staticTypes.includes(request.destination),
new CacheFirst({
cacheName: "static-content",
})
);
// External media domains which have unique urls (never changing content) and can be cached forever
const externalMediaHosts = ["void.cat", "nostr.build", "imgur.com", "i.imgur.com", "pbs.twimg.com", "i.ibb.co"];
const externalMediaHosts = [
"void.cat",
"nostr.build",
"imgur.com",
"i.imgur.com",
"pbs.twimg.com",
"i.ibb.co",
];
registerRoute(
({ url }) => externalMediaHosts.includes(url.host),
new CacheFirst({
@ -25,7 +34,7 @@ registerRoute(
})
);
self.addEventListener("message", event => {
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}

View File

@ -77,7 +77,7 @@ export function eventLink(ev: NostrEvent) {
d,
undefined,
ev.kind,
ev.pubkey,
ev.pubkey
);
return `/${naddr}`;
}