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

@ -1,46 +1,45 @@
/// <reference types="@webbtc/webln-types" />
declare module "*.jpg" {
const value: unknown;
export default value;
const value: unknown;
export default value;
}
declare module "*.svg" {
const value: unknown;
export default value;
}
declare module "*.webp" {
const value: string;
export default value;
}
declare module "*.png" {
const value: string;
export default value;
}
declare module "*.css" {
const stylesheet: CSSStyleSheet;
export default stylesheet;
}
declare module "translations/*.json" {
const value: Record<string, string>;
export default value;
}
declare module "light-bolt11-decoder" {
export function decode(pr?: string): ParsedInvoice;
export interface ParsedInvoice {
paymentRequest: string;
sections: Section[];
}
declare module "*.svg" {
const value: unknown;
export default value;
export interface Section {
name: string;
value: string | Uint8Array | number | undefined;
}
declare module "*.webp" {
const value: string;
export default value;
}
declare module "*.png" {
const value: string;
export default value;
}
declare module "*.css" {
const stylesheet: CSSStyleSheet;
export default stylesheet;
}
declare module "translations/*.json" {
const value: Record<string, string>;
export default value;
}
declare module "light-bolt11-decoder" {
export function decode(pr?: string): ParsedInvoice;
export interface ParsedInvoice {
paymentRequest: string;
sections: Section[];
}
export interface Section {
name: string;
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

@ -1,11 +1,11 @@
.copy {
display: flex;
cursor: pointer;
align-items: center;
gap: 8px;
display: flex;
cursor: pointer;
align-items: center;
gap: 8px;
}
.copy .body {
font-size: small;
color: white;
}
font-size: small;
color: white;
}

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 => {
if (video.current) {
video.current.srcObject = s;
}
}).catch(console.error);
return () => { client.Disconnect().catch(console.error); }
client
.Play()
.then((s) => {
if (video.current) {
video.current.srcObject = s;
}
})
.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

@ -1,13 +1,13 @@
.avatar-input {
width: 90px;
height: 90px;
background-color: #aaa;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background-image: var(--img);
background-position: center;
background-size: cover;
}
width: 90px;
height: 90px;
background-color: #aaa;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background-image: var(--img);
background-position: center;
background-size: cover;
}

View File

@ -13,127 +13,163 @@ import { VoidApi } from "@void-cat/api";
import { LoginType } from "login";
enum Stage {
Login = 0,
Details = 1,
SaveKey = 2
Login = 0,
Details = 1,
SaveKey = 2,
}
export function LoginSignup({ close }: { close: () => void }) {
const [error, setError] = useState("");
const [stage, setStage] = useState(Stage.Login);
const [username, setUsername] = useState("");
const [avatar, setAvatar] = useState("");
const [key, setNewKey] = useState("");
const [error, setError] = useState("");
const [stage, setStage] = useState(Stage.Login);
const [username, setUsername] = useState("");
const [avatar, setAvatar] = useState("");
const [key, setNewKey] = useState("");
async function doLogin() {
try {
const pub = await EventPublisher.nip7();
if (pub) {
Login.loginWithPubkey(pub.pubKey, LoginType.Nip7);
close();
}
} catch (e) {
console.error(e);
if (e instanceof Error) {
setError(e.message);
} else {
setError(e as string);
}
}
}
function createAccount() {
const newKey = bytesToHex(schnorr.utils.randomPrivateKey());
setNewKey(newKey);
setStage(Stage.Details);
}
function loginWithKey() {
Login.loginWithPrivateKey(key);
async function doLogin() {
try {
const pub = await EventPublisher.nip7();
if (pub) {
Login.loginWithPubkey(pub.pubKey, LoginType.Nip7);
close();
}
} catch (e) {
console.error(e);
if (e instanceof Error) {
setError(e.message);
} else {
setError(e as string);
}
}
}
async function uploadAvatar() {
const file = await openFile();
if (file) {
const VoidCatHost = "https://void.cat"
const api = new VoidApi(VoidCatHost);
const uploader = api.getUploader(file);
const result = await uploader.upload({
"V-Strip-Metadata": "true"
})
if (result.ok) {
const resultUrl = result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
setAvatar(resultUrl);
} else {
setError(result.errorMessage ?? "Upload failed");
}
}
function createAccount() {
const newKey = bytesToHex(schnorr.utils.randomPrivateKey());
setNewKey(newKey);
setStage(Stage.Details);
}
function loginWithKey() {
Login.loginWithPrivateKey(key);
close();
}
async function uploadAvatar() {
const file = await openFile();
if (file) {
const VoidCatHost = "https://void.cat";
const api = new VoidApi(VoidCatHost);
const uploader = api.getUploader(file);
const result = await uploader.upload({
"V-Strip-Metadata": "true",
});
if (result.ok) {
const resultUrl =
result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
setAvatar(resultUrl);
} else {
setError(result.errorMessage ?? "Upload failed");
}
}
}
async function saveProfile() {
const pub = EventPublisher.privateKey(key);
const profile = {
name: username,
picture: avatar,
lud16: `${pub.pubKey}@zap.stream`
} as UserMetadata;
async function saveProfile() {
const pub = EventPublisher.privateKey(key);
const profile = {
name: username,
picture: avatar,
lud16: `${pub.pubKey}@zap.stream`,
} as UserMetadata;
const ev = await pub.metadata(profile);
console.debug(ev);
System.BroadcastEvent(ev);
const ev = await pub.metadata(profile);
console.debug(ev);
System.BroadcastEvent(ev);
setStage(Stage.SaveKey);
setStage(Stage.SaveKey);
}
switch (stage) {
case Stage.Login: {
return (
<>
<h2>Login</h2>
{"nostr" in window && (
<AsyncButton
type="button"
className="btn btn-primary"
onClick={doLogin}
>
Nostr Extension
</AsyncButton>
)}
<button
type="button"
className="btn btn-primary"
onClick={createAccount}
>
Create Account
</button>
{error && <b className="error">{error}</b>}
</>
);
}
switch (stage) {
case Stage.Login: {
return <>
<h2>Login</h2>
{"nostr" in window &&
<AsyncButton type="button" className="btn btn-primary" onClick={doLogin}>
Nostr Extension
</AsyncButton>}
<button type="button" className="btn btn-primary" onClick={createAccount}>
Create Account
</button>
{error && <b className="error">{error}</b>}
</>
}
case Stage.Details: {
return <>
<h2>Setup Profile</h2>
<div className="flex f-center">
<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)} />
</div>
<small>You can change this later</small>
</div>
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}>
Save
</AsyncButton>
</>
}
case Stage.SaveKey: {
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!
</p>
<div className="paper">
<Copy text={hexToBech32("nsec", key)} />
</div>
<button type="button" className="btn btn-primary" onClick={loginWithKey}>
Ok, it's safe
</button>
</>
}
case Stage.Details: {
return (
<>
<h2>Setup Profile</h2>
<div className="flex f-center">
<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)}
/>
</div>
<small>You can change this later</small>
</div>
<AsyncButton
type="button"
className="btn btn-primary"
onClick={saveProfile}
>
Save
</AsyncButton>
</>
);
}
}
case Stage.SaveKey: {
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!
</p>
<div className="paper">
<Copy text={hexToBech32("nsec", key)} />
</div>
<button
type="button"
className="btn btn-primary"
onClick={loginWithKey}
>
Ok, it's safe
</button>
</>
);
}
}
}

View File

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

View File

@ -10,12 +10,12 @@
}
.new-goal .paper {
background: #262626;
height: 32px;
background: #262626;
height: 32px;
}
.new-goal .btn:disabled {
opacity: 0.3;
opacity: 0.3;
}
.new-goal .create-goal {

View File

@ -1,48 +1,47 @@
.new-stream {
display: flex;
flex-direction: column;
gap: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
.new-stream h3 {
font-size: 24px;
margin: 0;
font-size: 24px;
margin: 0;
}
.new-stream p {
margin: 0 0 8px 0;
margin: 0 0 8px 0;
}
.new-stream small {
display: block;
margin: 8px 0 0 0;
display: block;
margin: 8px 0 0 0;
}
.new-stream .btn.wide {
padding: 12px 16px;
border-radius: 16px;
width: 100%;
padding: 12px 16px;
border-radius: 16px;
width: 100%;
}
.new-stream div.paper {
background: #262626;
padding: 12px 16px;
background: #262626;
padding: 12px 16px;
}
.new-stream .btn:disabled {
opacity: 0.3;
opacity: 0.3;
}
.new-stream .pill {
border-radius: 16px;
background: #262626;
padding: 8px 12px;
text-align: center;
text-transform: uppercase;
border-radius: 16px;
background: #262626;
padding: 8px 12px;
text-align: center;
text-transform: uppercase;
}
.new-stream .pill.active {
color: inherit;
background: #353535;
}
color: inherit;
background: #353535;
}

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 => {
currentProvider.updateStreamInfo(ex);
if (!ev) {
navigate(eventLink(ex));
} else {
onFinish?.(ev);
}
}} ev={ev} />
return (
<StreamEditor
onFinish={(ex) => {
currentProvider.updateStreamInfo(ex);
if (!ev) {
navigate(eventLink(ex));
} else {
onFinish?.(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 <>
<p>Stream Providers</p>
<div className="flex g12">
{providers.map(v => <span className={`pill${v === currentProvider ? " active" : ""}`} onClick={() => setCurrentProvider(v)}>{v.name}</span>)}
</div>
{providerDialog()}
</>
return (
<>
<p>Stream Providers</p>
<div className="flex g12">
{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,124 +1,163 @@
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) {
const [topup, setTopup] = useState(false);
const [info, setInfo] = useState<StreamProviderInfo>();
const [ep, setEndpoint] = useState<StreamProviderEndpoint>();
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);
}
useEffect(() => {
provider.info().then(v => {
function sortEndpoints(arr: Array<StreamProviderEndpoint>) {
return arr.sort((a, b) => ((a.rate ?? 0) > (b.rate ?? 0) ? -1 : 1));
}
useEffect(() => {
provider.info().then((v) => {
setInfo(v);
setEndpoint(sortEndpoints(v.endpoints)[0]);
});
}, [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);
setEndpoint(sortEndpoints(v.endpoints)[0]);
});
}, [provider]);
setTopup(false);
});
}}
/>
);
}
if (!info) {
return <Spinner />
function calcEstimate() {
if (!ep?.rate || !ep?.unit || !info?.balance || !info.balance) return;
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.toFixed(0)} ${ep.unit} @ ${ep.rate} sats/${ep.unit}`;
}
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);
});
}} />
function parseCapability(cap: string) {
const [tag, ...others] = cap.split(":");
if (tag === "variant") {
const [height] = others;
return height === "source" ? height : `${height.slice(0, -1)}p`;
}
function calcEstimate() {
if (!ep?.rate || !ep?.unit || !info?.balance || !info.balance) return;
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.toFixed(0)} ${ep.unit} @ ${ep.rate} sats/${ep.unit}`
if (tag === "output") {
return others[0];
}
return cap;
}
function parseCapability(cap: string) {
const [tag, ...others] = cap.split(":");
if (tag === "variant") {
const [height] = others;
return height === "source" ? height : `${height.slice(0, -1)}p`;
}
if (tag === "output") {
return others[0];
}
return cap;
}
const streamEvent = others.ev ?? info.publishedEvent ?? DummyEvent;
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)}>
{a.name}
</span>)}
</div>
</div>}
const streamEvent = others.ev ?? info.publishedEvent ?? DummyEvent;
return (
<>
{info.endpoints.length > 1 && (
<div>
<p>Stream Url</p>
<div className="paper">
<input type="text" value={ep?.url} disabled />
</div>
<p>Endpoint</p>
<div className="flex g12">
{sortEndpoints(info.endpoints).map((a) => (
<span
className={`pill${ep?.name === a.name ? " active" : ""}`}
onClick={() => setEndpoint(a)}
>
{a.name}
</span>
))}
</div>
</div>
<div>
<p>Stream Key</p>
<div className="flex g12">
<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 ?? "")}>
Copy
</button>
</div>
)}
<div>
<p>Stream Url</p>
<div className="paper">
<input type="text" value={ep?.url} disabled />
</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>
<small>About {calcEstimate()}</small>
</div>
<div>
<p>Stream Key</p>
<div className="flex g12">
<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 ?? "")}
>
Copy
</button>
</div>
<div>
<p>Resolutions</p>
<div className="flex g12">
{ep?.capabilities?.map(a => <span className="pill">{parseCapability(a)}</span>)}
</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>
{streamEvent && <StreamEditor onFinish={(ex) => {
<small>About {calcEstimate()}</small>
</div>
<div>
<p>Resolutions</p>
<div className="flex g12">
{ep?.capabilities?.map((a) => (
<span className="pill">{parseCapability(a)}</span>
))}
</div>
</div>
{streamEvent && (
<StreamEditor
onFinish={(ex) => {
provider.updateStreamInfo(ex);
others.onFinish?.(ex);
}} ev={streamEvent} options={{
}}
ev={streamEvent}
options={{
canSetStream: false,
canSetStatus: false
}} />}
canSetStatus: false,
}}
/>
)}
</>
}
);
}

View File

@ -1,19 +1,19 @@
.profile {
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
font-size: 16px;
line-height: 20px;
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
font-size: 16px;
line-height: 20px;
}
.profile img {
width: 40px;
height: 40px;
border-radius: 100%;
background: #A7A7A7;
border: unset;
outline: unset;
object-fit: cover;
overflow: hidden;
width: 40px;
height: 40px;
border-radius: 100%;
background: #a7a7a7;
border: unset;
outline: unset;
object-fit: cover;
overflow: hidden;
}

View File

@ -1,43 +1,43 @@
.send-zap {
display: flex;
gap: 24px;
flex-direction: column;
display: flex;
gap: 24px;
flex-direction: column;
}
.send-zap .amounts {
display: grid;
grid-template-columns: repeat(4, 1fr);
justify-content: space-evenly;
gap: 8px;
display: grid;
grid-template-columns: repeat(4, 1fr);
justify-content: space-evenly;
gap: 8px;
}
.send-zap .pill {
border-radius: 16px;
background: #262626;
border-radius: 16px;
background: #262626;
padding: 8px 12px;
text-align: center;
padding: 8px 12px;
text-align: center;
}
.send-zap .pill.active {
color: inherit;
background: #353535;
color: inherit;
background: #353535;
}
.send-zap p {
margin: 0 0 8px 0;
font-weight: 500;
margin: 0 0 8px 0;
font-weight: 500;
}
.send-zap .btn {
width: 100%;
padding: 12px 16px;
width: 100%;
padding: 12px 16px;
}
.send-zap .btn>span {
justify-content: center;
.send-zap .btn > span {
justify-content: center;
}
.send-zap .qr {
align-self: center;
align-self: center;
}

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

@ -1,3 +1,3 @@
.pill.state {
text-transform: uppercase;
}
text-transform: uppercase;
}

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

@ -1,20 +1,20 @@
.rti--container {
background-color: unset !important;
border: 0 !important;
border-radius: 0 !important;
padding: 0 !important;
box-shadow: unset !important;
background-color: unset !important;
border: 0 !important;
border-radius: 0 !important;
padding: 0 !important;
box-shadow: unset !important;
}
.rti--tag {
color: black !important;
padding: 4px 10px !important;
border-radius: 12px !important;
display: unset !important;
color: black !important;
padding: 4px 10px !important;
border-radius: 12px !important;
display: unset !important;
}
.content-warning {
padding: 16px;
border-radius: 16px;
border: 1px solid #FF563F;
}
padding: 16px;
border-radius: 16px;
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,94 +106,121 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
return (
<>
<h3>{ev ? "Edit Stream" : "New Stream"}</h3>
{(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)} />
{(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)}
/>
</div>
</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)} />
)}
{(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)}
/>
</div>
</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)} />
)}
{(options?.canSetImage ?? true) && (
<div>
<p>Cover image</p>
<div className="paper">
<input
type="text"
placeholder="https://"
value={image}
onChange={(e) => setImage(e.target.value)}
/>
</div>
</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)} />
)}
{(options?.canSetStream ?? true) && (
<div>
<p>Stream Url</p>
<div className="paper">
<input
type="text"
placeholder="https://"
value={stream}
onChange={(e) => setStream(e.target.value)}
/>
</div>
<small>Stream type should be HLS</small>
</div>
<small>Stream type should be HLS</small>
</div>}
{(options?.canSetStatus ?? true) && <><div>
<p>Status</p>
<div className="flex g12">
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(
(v) => (
<span
className={`pill${status === v ? " active" : ""}`}
onClick={() => setStatus(v)}
key={v}
>
{v}
</span>
)
)}
</div>
</div>
{status === StreamState.Planned && (
)}
{(options?.canSetStatus ?? true) && (
<>
<div>
<p>Start Time</p>
<div className="paper">
<input
type="datetime-local"
value={toDateTimeString(Number(start ?? "0"))}
onChange={(e) => setStart(fromDateTimeString(e.target.value).toString())} />
<p>Status</p>
<div className="flex g12">
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(
(v) => (
<span
className={`pill${status === v ? " active" : ""}`}
onClick={() => setStatus(v)}
key={v}
>
{v}
</span>
)
)}
</div>
</div>
)}</>}
{(options?.canSetTags ?? true) && <div>
<p>Tags</p>
<div className="paper">
<TagsInput
value={tags}
onChange={setTags}
placeHolder="Music,DJ,English"
separators={["Enter", ","]}
/>
</div>
</div>}
{(options?.canSetContentWarning ?? true) && <div className="flex g12 content-warning">
{status === StreamState.Planned && (
<div>
<p>Start Time</p>
<div className="paper">
<input
type="datetime-local"
value={toDateTimeString(Number(start ?? "0"))}
onChange={(e) =>
setStart(fromDateTimeString(e.target.value).toString())
}
/>
</div>
</div>
)}
</>
)}
{(options?.canSetTags ?? true) && (
<div>
<input type="checkbox" checked={contentWarning} onChange={e => setContentWarning(e.target.checked)} />
<p>Tags</p>
<div className="paper">
<TagsInput
value={tags}
onChange={setTags}
placeHolder="Music,DJ,English"
separators={["Enter", ","]}
/>
</div>
</div>
<div>
<div className="warning">NSFW Content</div>
Check here if this stream contains nudity or pornographic content.
)}
{(options?.canSetContentWarning ?? true) && (
<div className="flex g12 content-warning">
<div>
<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

@ -4,23 +4,25 @@ import { unixNow } from "@snort/shared";
import { findTag } from "../utils";
export function StreamTimer({ ev }: { ev?: NostrEvent }) {
const [time, setTime] = useState("");
const [time, setTime] = useState("");
function updateTime() {
const starts = Number(findTag(ev, "starts") ?? unixNow());
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")}`);
}
function updateTime() {
const starts = Number(findTag(ev, "starts") ?? unixNow());
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")}`
);
}
useEffect(() => {
updateTime();
const t = setInterval(() => {
updateTime();
}, 1000);
return () => clearInterval(t);
}, []);
useEffect(() => {
updateTime();
const t = setInterval(() => {
updateTime();
}, 1000);
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,12 +27,11 @@ export function useProfile(link: NostrLink, leaveOpen = false) {
return b;
}, [link, leaveOpen]);
const { data: streamsData } =
useRequestBuilder<NoteCollection>(
System,
NoteCollection,
sub
);
const { data: streamsData } = useRequestBuilder<NoteCollection>(
System,
NoteCollection,
sub
);
const streams = streamsData ?? [];
const addresses = useMemo(() => {

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,12 +13,12 @@ body {
--gap-s: 16px;
--header-height: 48px;
--text-muted: #797979;
--text-link: #F838D9;
--text-danger: #FF563F;
--text-link: #f838d9;
--text-danger: #ff563f;
--border: #333;
}
@media(max-width: 1020px) {
@media (max-width: 1020px) {
:root {
--gap-l: 24px;
--gap-m: 16px;

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

@ -17,4 +17,4 @@ export function formatShort(fmt: Intl.NumberFormat, n: number) {
export function formatSats(n: number) {
return formatShort(intlSats, n);
}
}

View File

@ -13,4 +13,4 @@
.popout-chat.embed .live-chat .messages::-webkit-scrollbar {
display: none;
}
}

View File

@ -1,7 +1,7 @@
import "./chat-popout.css";
import { LiveChat } from "element/live-chat";
import { useParams } from "react-router-dom";
import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { findTag } from "utils";
@ -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,8 +18,7 @@
border-radius: 16px;
}
@media (min-width: 768px){
@media (min-width: 768px) {
.profile-page .banner {
height: 348.75px;
object-fit: cover;
@ -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;
@ -145,7 +143,7 @@
display: flex;
flex-direction: column;
}
@media (max-width: 400px){
@media (max-width: 400px) {
.tabs-tab {
font-size: 14px;
}
@ -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

@ -1,31 +1,31 @@
.stream-providers-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.stream-providers-grid>div {
display: flex;
flex-direction: column;
gap: 16px;
.stream-providers-grid > div {
display: flex;
flex-direction: column;
gap: 16px;
}
.stream-providers-grid>div img {
height: 64px;
.stream-providers-grid > div img {
height: 64px;
}
.owncast-config {
display: flex;
gap: 16px;
padding: 40px;
display: flex;
gap: 16px;
padding: 40px;
}
.owncast-config>div {
flex: 1;
.owncast-config > div {
flex: 1;
}
.owncast-config>div:nth-child(2) {
display: flex;
flex-direction: column;
gap: 16px;
}
.owncast-config > div:nth-child(2) {
display: flex;
flex-direction: column;
gap: 16px;
}

View File

@ -8,55 +8,71 @@ import { ConfigureOwncast } from "./owncast";
import { ConfigureNostrType } from "./nostr";
export function StreamProvidersPage() {
const navigate = useNavigate();
const { id } = useParams();
const navigate = useNavigate();
const { id } = useParams();
function mapName(p: StreamProviders) {
switch (p) {
case StreamProviders.Owncast: return "Owncast"
case StreamProviders.Cloudflare: return "Cloudflare"
case StreamProviders.NostrType: return "Nostr Native"
}
return "Unknown"
function mapName(p: StreamProviders) {
switch (p) {
case StreamProviders.Owncast:
return "Owncast";
case StreamProviders.Cloudflare:
return "Cloudflare";
case StreamProviders.NostrType:
return "Nostr Native";
}
return "Unknown";
}
function mapLogo(p: StreamProviders) {
switch (p) {
case StreamProviders.Owncast: return <img src={Owncast} />
case StreamProviders.Cloudflare: return <img src={Cloudflare} />
}
function mapLogo(p: StreamProviders) {
switch (p) {
case StreamProviders.Owncast:
return <img src={Owncast} />;
case StreamProviders.Cloudflare:
return <img src={Cloudflare} />;
}
}
function providerLink(p: StreamProviders) {
return <div className="paper">
<h3>{mapName(p)}</h3>
{mapLogo(p)}
<button className="btn btn-border" onClick={() => navigate(p)}>
+ Configure
</button>
function providerLink(p: StreamProviders) {
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">
<h1>Providers</h1>
<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))}
</div>
}
</div>
);
}
function index() {
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>
<div className="stream-providers-grid">
{[StreamProviders.NostrType, StreamProviders.Owncast, StreamProviders.Cloudflare].map(v => providerLink(v))}
</div>
</div >
if (!id) {
return index();
} else {
switch (id) {
case StreamProviders.Owncast: {
return <ConfigureOwncast />;
}
case StreamProviders.NostrType: {
return <ConfigureNostrType />;
}
}
if (!id) {
return index();
} else {
switch (id) {
case StreamProviders.Owncast: {
return <ConfigureOwncast />
}
case StreamProviders.NostrType: {
return <ConfigureNostrType />
}
}
}
}
}
}

View File

@ -8,77 +8,85 @@ import { StreamProviderInfo, StreamProviderStore } from "providers";
import { Nip103StreamProvider } from "providers/nip103";
export function ConfigureNostrType() {
const [url, setUrl] = useState("");
const [info, setInfo] = useState<StreamProviderInfo>();
const navigate = useNavigate();
const [url, setUrl] = useState("");
const [info, setInfo] = useState<StreamProviderInfo>();
const navigate = useNavigate();
async function tryConnect() {
try {
const api = new Nip103StreamProvider(url);
const inf = await api.info();
setInfo(inf);
} catch (e) {
console.error(e);
}
async function tryConnect() {
try {
const api = new Nip103StreamProvider(url);
const inf = await api.info();
setInfo(inf);
} catch (e) {
console.error(e);
}
}
function status() {
if (!info) return;
function status() {
if (!info) return;
return <>
<h3>Status</h3>
<div>
<StatePill state={info?.state ?? StreamState.Ended} />
</div>
<div>
<p>Name</p>
<div className="paper">
{info?.name}
</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>}
<div>
<button className="btn btn-border" onClick={() => {
StreamProviderStore.add(new Nip103StreamProvider(url));
navigate("/");
}}>
Save
</button>
</div>
</>
}
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)} />
</div>
</div>
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
Connect
</AsyncButton>
return (
<>
<h3>Status</h3>
<div>
<StatePill state={info?.state ?? StreamState.Ended} />
</div>
<div>
{status()}
<p>Name</p>
<div className="paper">{info?.name}</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>
)}
<div>
<button
className="btn btn-border"
onClick={() => {
StreamProviderStore.add(new Nip103StreamProvider(url));
navigate("/");
}}
>
Save
</button>
</div>
</>
);
}
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)}
/>
</div>
</div>
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
Connect
</AsyncButton>
</div>
<div>{status()}</div>
</div>
}
);
}

View File

@ -7,85 +7,96 @@ import { useState } from "react";
import { useNavigate } from "react-router-dom";
export function ConfigureOwncast() {
const [url, setUrl] = useState("");
const [token, setToken] = useState("");
const [info, setInfo] = useState<StreamProviderInfo>();
const navigate = useNavigate();
const [url, setUrl] = useState("");
const [token, setToken] = useState("");
const [info, setInfo] = useState<StreamProviderInfo>();
const navigate = useNavigate();
async function tryConnect() {
try {
const api = new OwncastProvider(url, token);
const i = await api.info();
setInfo(i);
}
catch (e) {
console.debug(e);
}
async function tryConnect() {
try {
const api = new OwncastProvider(url, token);
const i = await api.info();
setInfo(i);
} catch (e) {
console.debug(e);
}
}
function status() {
if (!info) return;
function status() {
if (!info) return;
return <>
<h3>Status</h3>
<div>
<StatePill state={info?.state ?? StreamState.Ended} />
</div>
<div>
<p>Name</p>
<div className="paper">
{info?.name}
</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>}
<div>
<button className="btn btn-border" onClick={() => {
StreamProviderStore.add(new OwncastProvider(url, token));
navigate("/");
}}>
Save
</button>
</div>
</>
}
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)} />
</div>
</div>
<div>
<p>API token</p>
<div className="paper">
<input type="password" value={token} onChange={e => setToken(e.target.value)} />
</div>
</div>
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
Connect
</AsyncButton>
return (
<>
<h3>Status</h3>
<div>
<StatePill state={info?.state ?? StreamState.Ended} />
</div>
<div>
{status()}
<p>Name</p>
<div className="paper">{info?.name}</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>
)}
<div>
<button
className="btn btn-border"
onClick={() => {
StreamProviderStore.add(new OwncastProvider(url, token));
navigate("/");
}}
>
Save
</button>
</div>
</>
);
}
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)}
/>
</div>
</div>
<div>
<p>API token</p>
<div className="paper">
<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>
}
);
}

View File

@ -1,70 +1,70 @@
.video-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--gap-l);
padding: 40px 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--gap-l);
padding: 40px 0;
}
@media (max-width: 1020px) {
.video-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
padding: 16px;
}
.video-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
padding: 16px;
}
}
@media (max-width: 720px) {
.video-grid {
display: grid;
grid-template-columns: 1fr;
padding: 16px;
}
.video-grid {
display: grid;
grid-template-columns: 1fr;
padding: 16px;
}
}
@media(min-width: 1600px) {
.video-grid {
grid-template-columns: repeat(6, 1fr);
}
@media (min-width: 1600px) {
.video-grid {
grid-template-columns: repeat(6, 1fr);
}
}
@media(min-width: 2000px) {
.video-grid {
grid-template-columns: repeat(8, 1fr);
}
@media (min-width: 2000px) {
.video-grid {
grid-template-columns: repeat(8, 1fr);
}
}
.divider {
display: flex;
display: flex;
}
.divider:after {
content: "";
flex: 1;
content: "";
flex: 1;
}
.line {
align-items: center;
margin: 1em 0;
align-items: center;
margin: 1em 0;
}
.line:after {
height: 1px;
margin: 0 1em;
height: 1px;
margin: 0 1em;
}
.one-line:before,
.one-line:after {
background-color: #171717;
background-color: #171717;
}
::-webkit-scrollbar {
width: 10px;
background: #111;
width: 10px;
background: #111;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 100px;
min-height: 24px;
}
background: #333;
border-radius: 100px;
min-height: 24px;
}

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

@ -3,32 +3,32 @@ import { System } from "index";
import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers";
export class ManualProvider implements StreamProvider {
get name(): string {
return "Manual"
}
get name(): string {
return "Manual";
}
get type() {
return StreamProviders.Manual
}
get type() {
return StreamProviders.Manual;
}
info(): Promise<StreamProviderInfo> {
return Promise.resolve({
name: this.name
} as StreamProviderInfo)
}
info(): Promise<StreamProviderInfo> {
return Promise.resolve({
name: this.name,
} as StreamProviderInfo);
}
createConfig() {
return {
type: StreamProviders.Manual
}
}
createConfig() {
return {
type: StreamProviders.Manual,
};
}
updateStreamInfo(ev: NostrEvent): Promise<void> {
System.BroadcastEvent(ev);
return Promise.resolve();
}
updateStreamInfo(ev: NostrEvent): Promise<void> {
System.BroadcastEvent(ev);
return Promise.resolve();
}
topup(): Promise<string> {
throw new Error("Method not implemented.");
}
}
topup(): Promise<string> {
throw new Error("Method not implemented.");
}
}

View File

@ -1,116 +1,133 @@
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;
}
constructor(url: string) {
this.#url = url;
}
get name() {
return new URL(this.#url).host;
}
get name() {
return new URL(this.#url).host;
}
get type() {
return StreamProviders.NostrType
}
get type() {
return StreamProviders.NostrType;
}
async info() {
const rsp = await this.#getJson<AccountResponse>("GET", "account");
const title = findTag(rsp.event, "title");
const state = findTag(rsp.event, "status");
async info() {
const rsp = await this.#getJson<AccountResponse>("GET", "account");
const title = findTag(rsp.event, "title");
const state = findTag(rsp.event, "status");
return {
type: StreamProviders.NostrType,
name: title ?? "",
state: state,
viewers: 0,
publishedEvent: rsp.event,
balance: rsp.balance,
endpoints: rsp.endpoints.map((a) => {
return {
type: StreamProviders.NostrType,
name: title ?? "",
state: state,
viewers: 0,
publishedEvent: rsp.event,
balance: rsp.balance,
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
}
name: a.name,
url: a.url,
key: a.key,
rate: a.cost.rate,
unit: a.cost.unit,
capabilities: a.capabilities,
} as StreamProviderEndpoint;
}),
} as StreamProviderInfo;
}
createConfig() {
return {
type: StreamProviders.NostrType,
url: this.#url
}
}
createConfig() {
return {
type: StreamProviders.NostrType,
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 contentWarning = findTag(ev, "content-warning");
await this.#getJson("PATCH", "event", {
title, summary, image, tags, content_warning: contentWarning
});
}
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 contentWarning = findTag(ev, "content-warning");
await this.#getJson("PATCH", "event", {
title,
summary,
image,
tags,
content_warning: contentWarning,
});
}
async topup(amount: number): Promise<string> {
const rsp = await this.#getJson<TopUpResponse>("GET", `topup?amount=${amount}`);
return rsp.pr;
}
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 login = Login.snapshot();
const pub = login && getPublisher(login);
if (!pub) throw new Error("No signer");
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)
.content("")
.tag(["u", u])
.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))}`
},
});
const json = await rsp.text();
if (!rsp.ok) {
throw new Error(json);
}
return json.length > 0 ? JSON.parse(json) as T : {} as T;
const u = `${this.#url}${path}`;
const token = await pub.generic((eb) => {
return eb
.kind(EventKind.HttpAuthentication)
.content("")
.tag(["u", u])
.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))}`,
},
});
const json = await rsp.text();
if (!rsp.ok) {
throw new Error(json);
}
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
cost: {
unit: string
rate: number
}
capabilities: Array<string>
name: string;
url: string;
key: string;
cost: {
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}`;
}

View File

@ -282,7 +282,7 @@ export class WISH extends TypedEventTarget {
relAddr: candidate.relatedAddress || undefined,
relPort:
typeof candidate.relatedPort !== "undefined" &&
candidate.relatedPort !== null
candidate.relatedPort !== null
? candidate.relatedPort.toString()
: undefined,
});