fix eslint warnings

This commit is contained in:
ennmichael 2023-02-07 20:47:57 +01:00 committed by Kieran
parent 61e6876c6d
commit 441983b8ae
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
89 changed files with 1018 additions and 588 deletions

13
.eslintrc.cjs Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true,
ignorePatterns: ["build/"],
env: {
browser: true,
worker: true,
commonjs: true,
node: true,
},
};

6
d.ts
View File

@ -1,14 +1,14 @@
declare module "*.jpg" { declare module "*.jpg" {
const value: any; const value: unknown;
export default value; export default value;
} }
declare module "*.svg" { declare module "*.svg" {
const value: any; const value: unknown;
export default value; export default value;
} }
declare module "*.webp" { declare module "*.webp" {
const value: any; const value: string;
export default value; export default value;
} }

View File

@ -83,17 +83,20 @@ export const RecommendedFollows = [
* Regex to match email address * Regex to match email address
*/ */
export const EmailRegex = export const EmailRegex =
// eslint-disable-next-line no-useless-escape
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
/** /**
* Generic URL regex * Generic URL regex
*/ */
export const UrlRegex = export const UrlRegex =
// eslint-disable-next-line no-useless-escape
/((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/i; /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/i;
/** /**
* Extract file extensions regex * Extract file extensions regex
*/ */
// eslint-disable-next-line no-useless-escape
export const FileExtensionRegex = /\.([\w]+)$/i; export const FileExtensionRegex = /\.([\w]+)$/i;
/** /**
@ -121,6 +124,7 @@ export const TweetUrlRegex =
/** /**
* Hashtag regex * Hashtag regex
*/ */
// eslint-disable-next-line no-useless-escape
export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/; export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/;
/** /**

View File

@ -1,14 +1,20 @@
import { useState } from "react"; import { useState } from "react";
export default function AsyncButton(props: any) { interface AsyncButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onClick(e: React.MouseEvent): Promise<void> | void;
children?: React.ReactNode;
}
export default function AsyncButton(props: AsyncButtonProps) {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
async function handle(e: any) { async function handle(e: React.MouseEvent) {
if (loading) return; if (loading) return;
setLoading(true); setLoading(true);
try { try {
if (typeof props.onClick === "function") { if (typeof props.onClick === "function") {
let f = props.onClick(e); const f = props.onClick(e);
if (f instanceof Promise) { if (f instanceof Promise) {
await f; await f;
} }
@ -19,12 +25,7 @@ export default function AsyncButton(props: any) {
} }
return ( return (
<button <button type="button" disabled={loading} {...props} onClick={handle}>
type="button"
disabled={loading}
{...props}
onClick={(e) => handle(e)}
>
{props.children} {props.children}
</button> </button>
); );

View File

@ -1,13 +1,7 @@
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { HexKey } from "Nostr";
import type { RootState } from "State/Store";
import MuteButton from "Element/MuteButton"; import MuteButton from "Element/MuteButton";
import BlockButton from "Element/BlockButton"; import BlockButton from "Element/BlockButton";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";
import useMutedFeed, { getMuted } from "Feed/MuteList";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import messages from "./messages"; import messages from "./messages";
@ -17,7 +11,6 @@ interface BlockListProps {
} }
export default function BlockList({ variant }: BlockListProps) { export default function BlockList({ variant }: BlockListProps) {
const { publicKey } = useSelector((s: RootState) => s.login);
const { blocked, muted } = useModeration(); const { blocked, muted } = useModeration();
return ( return (

View File

@ -1,4 +1,4 @@
import { useState, ReactNode } from "react"; import { ReactNode } from "react";
import ShowMore from "Element/ShowMore"; import ShowMore from "Element/ShowMore";

View File

@ -8,7 +8,7 @@ export interface CopyProps {
maxSize?: number; maxSize?: number;
} }
export default function Copy({ text, maxSize = 32 }: CopyProps) { export default function Copy({ text, maxSize = 32 }: CopyProps) {
const { copy, copied, error } = useCopy(); const { copy, copied } = useCopy();
const sliceLength = maxSize / 2; const sliceLength = maxSize / 2;
const trimmed = const trimmed =
text.length > maxSize text.length > maxSize

View File

@ -12,6 +12,7 @@ import { setLastReadDm } from "Pages/MessagesPage";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import { HexKey, TaggedRawEvent } from "Nostr"; import { HexKey, TaggedRawEvent } from "Nostr";
import { incDmInteraction } from "State/Login"; import { incDmInteraction } from "State/Login";
import { unwrap } from "Util";
import messages from "./messages"; import messages from "./messages";
@ -32,11 +33,11 @@ export default function DM(props: DMProps) {
const isMe = props.data.pubkey === pubKey; const isMe = props.data.pubkey === pubKey;
const otherPubkey = isMe const otherPubkey = isMe
? pubKey ? pubKey
: props.data.tags.find((a) => a[0] === "p")![1]; : unwrap(props.data.tags.find((a) => a[0] === "p")?.[1]);
async function decrypt() { async function decrypt() {
let e = new Event(props.data); const e = new Event(props.data);
let decrypted = await publisher.decryptDm(e); const decrypted = await publisher.decryptDm(e);
setContent(decrypted || "<ERROR>"); setContent(decrypted || "<ERROR>");
if (!isMe) { if (!isMe) {
setLastReadDm(e.PubKey); setLastReadDm(e.PubKey);

View File

@ -21,12 +21,12 @@ export default function FollowButton(props: FollowButtonProps) {
const baseClassname = `${props.className} follow-button`; const baseClassname = `${props.className} follow-button`;
async function follow(pubkey: HexKey) { async function follow(pubkey: HexKey) {
let ev = await publiser.addFollow(pubkey); const ev = await publiser.addFollow(pubkey);
publiser.broadcast(ev); publiser.broadcast(ev);
} }
async function unfollow(pubkey: HexKey) { async function unfollow(pubkey: HexKey) {
let ev = await publiser.removeFollow(pubkey); const ev = await publiser.removeFollow(pubkey);
publiser.broadcast(ev); publiser.broadcast(ev);
} }

View File

@ -17,7 +17,7 @@ export default function FollowListBase({
const publisher = useEventPublisher(); const publisher = useEventPublisher();
async function followAll() { async function followAll() {
let ev = await publisher.addFollow(pubkeys); const ev = await publisher.addFollow(pubkeys);
publisher.broadcast(ev); publisher.broadcast(ev);
} }

View File

@ -17,7 +17,7 @@ export default function FollowersList({ pubkey }: FollowersListProps) {
const feed = useFollowersFeed(pubkey); const feed = useFollowersFeed(pubkey);
const pubkeys = useMemo(() => { const pubkeys = useMemo(() => {
let contactLists = feed?.store.notes.filter( const contactLists = feed?.store.notes.filter(
(a) => (a) =>
a.kind === EventKind.ContactList && a.kind === EventKind.ContactList &&
a.tags.some((b) => b[0] === "p" && b[1] === pubkey) a.tags.some((b) => b[0] === "p" && b[1] === pubkey)

View File

@ -25,7 +25,7 @@ export default function FollowsYou({ pubkey }: FollowsYouProps) {
return getFollowers(feed.store, pubkey); return getFollowers(feed.store, pubkey);
}, [feed, pubkey]); }, [feed, pubkey]);
const followsMe = pubkeys.includes(loginPubKey!) ?? false; const followsMe = loginPubKey ? pubkeys.includes(loginPubKey) : false;
return followsMe ? ( return followsMe ? (
<span className="follows-you">{formatMessage(messages.FollowsYou)}</span> <span className="follows-you">{formatMessage(messages.FollowsYou)}</span>

View File

@ -135,7 +135,9 @@ export default function HyperText({
</a> </a>
); );
} }
} catch (error) {} } catch (error) {
// Ignore the error.
}
return ( return (
<a <a
href={a} href={a}

View File

@ -1,7 +1,7 @@
import "./Invoice.css"; import "./Invoice.css";
import { useState } from "react"; import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
// @ts-expect-error // @ts-expect-error No types available
import { decode as invoiceDecode } from "light-bolt11-decoder"; import { decode as invoiceDecode } from "light-bolt11-decoder";
import { useMemo } from "react"; import { useMemo } from "react";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
@ -13,6 +13,11 @@ import messages from "./messages";
export interface InvoiceProps { export interface InvoiceProps {
invoice: string; invoice: string;
} }
interface Section {
name: string;
}
export default function Invoice(props: InvoiceProps) { export default function Invoice(props: InvoiceProps) {
const invoice = props.invoice; const invoice = props.invoice;
const webln = useWebln(); const webln = useWebln();
@ -21,21 +26,21 @@ export default function Invoice(props: InvoiceProps) {
const info = useMemo(() => { const info = useMemo(() => {
try { try {
let parsed = invoiceDecode(invoice); const parsed = invoiceDecode(invoice);
let amount = parseInt( const amount = parseInt(
parsed.sections.find((a: any) => a.name === "amount")?.value parsed.sections.find((a: Section) => a.name === "amount")?.value
); );
let timestamp = parseInt( const timestamp = parseInt(
parsed.sections.find((a: any) => a.name === "timestamp")?.value parsed.sections.find((a: Section) => a.name === "timestamp")?.value
); );
let expire = parseInt( const expire = parseInt(
parsed.sections.find((a: any) => a.name === "expiry")?.value parsed.sections.find((a: Section) => a.name === "expiry")?.value
); );
let description = parsed.sections.find( const description = parsed.sections.find(
(a: any) => a.name === "description" (a: Section) => a.name === "description"
)?.value; )?.value;
let ret = { const ret = {
amount: !isNaN(amount) ? amount / 1000 : 0, amount: !isNaN(amount) ? amount / 1000 : 0,
expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null, expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null,
description, description,
@ -72,7 +77,7 @@ export default function Invoice(props: InvoiceProps) {
); );
} }
async function payInvoice(e: any) { async function payInvoice(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation(); e.stopPropagation();
if (webln?.enabled) { if (webln?.enabled) {
try { try {

59
src/Element/LNURLTip.css Normal file
View File

@ -0,0 +1,59 @@
.lnurl-tip {
text-align: center;
}
.lnurl-tip .btn {
background-color: inherit;
width: 210px;
margin: 0 0 10px 0;
}
.lnurl-tip .btn:hover {
background-color: var(--gray);
}
.sat-amount {
display: inline-block;
background-color: var(--gray-secondary);
color: var(--font-color);
padding: 2px 10px;
border-radius: 10px;
user-select: none;
margin: 2px 5px;
}
.sat-amount:hover {
cursor: pointer;
}
.sat-amount.active {
font-weight: bold;
color: var(--note-bg);
background-color: var(--font-color);
}
.lnurl-tip .invoice {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.lnurl-tip .invoice .actions {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: center;
}
.lnurl-tip .invoice .actions .copy-action {
margin: 10px auto;
}
.lnurl-tip .invoice .actions .pay-actions {
margin: 10px auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

301
src/Element/LNURLTip.tsx Normal file
View File

@ -0,0 +1,301 @@
import "./LNURLTip.css";
import { useEffect, useMemo, useState } from "react";
import { bech32ToText, unwrap } from "Util";
import { HexKey } from "Nostr";
import useEventPublisher from "Feed/EventPublisher";
import Modal from "Element/Modal";
import QrCode from "Element/QrCode";
import Copy from "Element/Copy";
import useWebln from "Hooks/useWebln";
interface LNURLService {
nostrPubkey?: HexKey;
minSendable?: number;
maxSendable?: number;
metadata: string;
callback: string;
commentAllowed?: number;
}
interface LNURLInvoice {
pr: string;
successAction?: LNURLSuccessAction;
}
interface LNURLSuccessAction {
description?: string;
url?: string;
}
export interface LNURLTipProps {
onClose?: () => void;
svc?: string;
show?: boolean;
invoice?: string; // shortcut to invoice qr tab
title?: string;
notice?: string;
note?: HexKey;
author?: HexKey;
}
export default function LNURLTip(props: LNURLTipProps) {
const onClose = props.onClose || (() => undefined);
const service = props.svc;
const show = props.show || false;
const { note, author } = props;
const amounts = [50, 100, 500, 1_000, 5_000, 10_000, 50_000];
const [payService, setPayService] = useState<LNURLService>();
const [amount, setAmount] = useState<number>();
const [customAmount, setCustomAmount] = useState<number>(0);
const [invoice, setInvoice] = useState<LNURLInvoice>();
const [comment, setComment] = useState<string>();
const [error, setError] = useState<string>();
const [success, setSuccess] = useState<LNURLSuccessAction>();
const webln = useWebln(show);
const publisher = useEventPublisher();
useEffect(() => {
if (show && !props.invoice) {
loadService()
.then((a) => setPayService(unwrap(a)))
.catch(() => setError("Failed to load LNURL service"));
} else {
setPayService(undefined);
setError(undefined);
setInvoice(props.invoice ? { pr: props.invoice } : undefined);
setAmount(undefined);
setComment(undefined);
setSuccess(undefined);
}
}, [show, service]);
const serviceAmounts = useMemo(() => {
if (payService) {
const min = (payService.minSendable ?? 0) / 1000;
const max = (payService.maxSendable ?? 0) / 1000;
return amounts.filter((a) => a >= min && a <= max);
}
return [];
}, [payService]);
const metadata = useMemo(() => {
if (payService) {
const meta: string[][] = JSON.parse(payService.metadata);
const desc = meta.find((a) => a[0] === "text/plain");
const image = meta.find((a) => a[0] === "image/png;base64");
return {
description: desc ? desc[1] : null,
image: image ? image[1] : null,
};
}
return null;
}, [payService]);
const selectAmount = (a: number) => {
setError(undefined);
setInvoice(undefined);
setAmount(a);
};
async function fetchJson<T>(url: string) {
const rsp = await fetch(url);
if (rsp.ok) {
const data: T = await rsp.json();
console.log(data);
setError(undefined);
return data;
}
return null;
}
async function loadService(): Promise<LNURLService | null> {
if (service) {
const isServiceUrl = service.toLowerCase().startsWith("lnurl");
if (isServiceUrl) {
const serviceUrl = bech32ToText(service);
return await fetchJson(serviceUrl);
} else {
const ns = service.split("@");
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
}
}
return null;
}
async function loadInvoice() {
if (!amount || !payService) return null;
let url = "";
const amountParam = `amount=${Math.floor(amount * 1000)}`;
const commentParam = comment
? `&comment=${encodeURIComponent(comment)}`
: "";
if (payService.nostrPubkey && author) {
const ev = await publisher.zap(author, note, comment);
const nostrParam =
ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`;
url = `${payService.callback}?${amountParam}${commentParam}${nostrParam}`;
} else {
url = `${payService.callback}?${amountParam}${commentParam}`;
}
try {
const rsp = await fetch(url);
if (rsp.ok) {
const data = await rsp.json();
console.log(data);
if (data.status === "ERROR") {
setError(data.reason);
} else {
setInvoice(data);
setError("");
payWebLNIfEnabled(data);
}
} else {
setError("Failed to load invoice");
}
} catch (e) {
setError("Failed to load invoice");
}
}
function custom() {
const min = (payService?.minSendable ?? 0) / 1000;
const max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
return (
<div className="flex mb10">
<input
type="number"
min={min}
max={max}
className="f-grow mr10"
value={customAmount}
onChange={(e) => setCustomAmount(parseInt(e.target.value))}
/>
<div className="btn" onClick={() => selectAmount(customAmount)}>
Confirm
</div>
</div>
);
}
async function payWebLNIfEnabled(invoice: LNURLInvoice) {
try {
if (webln?.enabled) {
const res = await webln.sendPayment(invoice.pr);
console.log(res);
setSuccess(invoice.successAction || {});
}
} catch (e: unknown) {
console.warn(e);
if (e instanceof Error) {
setError(e.toString());
}
}
}
function invoiceForm() {
if (invoice) return null;
return (
<>
<div className="f-ellipsis mb10">
{metadata?.description ?? service}
</div>
<div className="flex">
{(payService?.commentAllowed ?? 0) > 0 ? (
<input
type="text"
placeholder="Comment"
className="mb10 f-grow"
maxLength={payService?.commentAllowed}
onChange={(e) => setComment(e.target.value)}
/>
) : null}
</div>
<div className="mb10">
{serviceAmounts.map((a) => (
<span
className={`sat-amount ${amount === a ? "active" : ""}`}
key={a}
onClick={() => selectAmount(a)}
>
{a.toLocaleString()}
</span>
))}
{payService ? (
<span
className={`sat-amount ${amount === -1 ? "active" : ""}`}
onClick={() => selectAmount(-1)}
>
Custom
</span>
) : null}
</div>
{amount === -1 ? custom() : null}
{(amount ?? 0) > 0 && (
<button type="button" className="mb10" onClick={() => loadInvoice()}>
Get Invoice
</button>
)}
</>
);
}
function payInvoice() {
if (success) return null;
const pr = invoice?.pr;
return (
<>
<div className="invoice">
{props.notice && <b className="error">{props.notice}</b>}
<QrCode data={pr} link={`lightning:${pr}`} />
<div className="actions">
{pr && (
<>
<div className="copy-action">
<Copy text={pr} maxSize={26} />
</div>
<div className="pay-actions">
<button
type="button"
onClick={() => window.open(`lightning:${pr}`)}
>
Open Wallet
</button>
</div>
</>
)}
</div>
</div>
</>
);
}
function successAction() {
if (!success) return null;
return (
<>
<p>{success?.description ?? "Paid!"}</p>
{success.url ? (
<a href={success.url} rel="noreferrer" target="_blank">
{success.url}
</a>
) : null}
</>
);
}
const defaultTitle = payService?.nostrPubkey
? "⚡️ Send Zap!"
: "⚡️ Send sats";
if (!show) return null;
return (
<Modal onClose={onClose}>
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
<h2>{props.title || defaultTitle}</h2>
{invoiceForm()}
{error ? <p className="error">{error}</p> : null}
{payInvoice()}
{successAction()}
</div>
</Modal>
);
}

View File

@ -23,7 +23,7 @@ export default function LoadMore({
}, [inView, shouldLoadMore, tick]); }, [inView, shouldLoadMore, tick]);
useEffect(() => { useEffect(() => {
let t = setInterval(() => { const t = setInterval(() => {
setTick((x) => (x += 1)); setTick((x) => (x += 1));
}, 500); }, 500);
return () => clearInterval(t); return () => clearInterval(t);

View File

@ -9,10 +9,10 @@ export default function Mention({ pubkey }: { pubkey: HexKey }) {
const name = useMemo(() => { const name = useMemo(() => {
let name = hexToBech32("npub", pubkey).substring(0, 12); let name = hexToBech32("npub", pubkey).substring(0, 12);
if ((user?.display_name?.length ?? 0) > 0) { if (user?.display_name !== undefined && user.display_name.length > 0) {
name = user!.display_name!; name = user.display_name;
} else if ((user?.name?.length ?? 0) > 0) { } else if (user?.name !== undefined && user.name.length > 0) {
name = user!.name!; name = user.name;
} }
return name; return name;
}, [user, pubkey]); }, [user, pubkey]);

View File

@ -8,10 +8,13 @@ export interface ModalProps {
children: React.ReactNode; children: React.ReactNode;
} }
function useOnClickOutside(ref: any, onClickOutside: () => void) { function useOnClickOutside(
ref: React.MutableRefObject<Element | null>,
onClickOutside: () => void
) {
useEffect(() => { useEffect(() => {
function handleClickOutside(ev: any) { function handleClickOutside(ev: MouseEvent) {
if (ref && ref.current && !ref.current.contains(ev.target)) { if (ref && ref.current && !ref.current.contains(ev.target as Node)) {
onClickOutside(); onClickOutside();
} }
} }
@ -24,7 +27,7 @@ function useOnClickOutside(ref: any, onClickOutside: () => void) {
export default function Modal(props: ModalProps) { export default function Modal(props: ModalProps) {
const ref = useRef(null); const ref = useRef(null);
const onClose = props.onClose || (() => {}); const onClose = props.onClose || (() => undefined);
const className = props.className || ""; const className = props.className || "";
useOnClickOutside(ref, onClose); useOnClickOutside(ref, onClose);

View File

@ -1,6 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
import MuteButton from "Element/MuteButton"; import MuteButton from "Element/MuteButton";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";

View File

@ -29,7 +29,9 @@ type Nip05ServiceProps = {
supportLink: string; supportLink: string;
}; };
type ReduxStore = any; interface ReduxStore {
login: { publicKey: string };
}
export default function Nip5Service(props: Nip05ServiceProps) { export default function Nip5Service(props: Nip05ServiceProps) {
const navigate = useNavigate(); const navigate = useNavigate();
@ -64,9 +66,9 @@ export default function Nip5Service(props: Nip05ServiceProps) {
if ("error" in a) { if ("error" in a) {
setError(a as ServiceError); setError(a as ServiceError);
} else { } else {
let svc = a as ServiceConfig; const svc = a as ServiceConfig;
setServiceConfig(svc); setServiceConfig(svc);
let defaultDomain = const defaultDomain =
svc.domains.find((a) => a.default)?.name || svc.domains[0].name; svc.domains.find((a) => a.default)?.name || svc.domains[0].name;
setDomain(defaultDomain); setDomain(defaultDomain);
} }
@ -86,7 +88,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
setAvailabilityResponse({ available: false, why: "TOO_LONG" }); setAvailabilityResponse({ available: false, why: "TOO_LONG" });
return; return;
} }
let rx = new RegExp( const rx = new RegExp(
domainConfig?.regex[0] ?? "", domainConfig?.regex[0] ?? "",
domainConfig?.regex[1] ?? "" domainConfig?.regex[1] ?? ""
); );
@ -111,14 +113,14 @@ export default function Nip5Service(props: Nip05ServiceProps) {
useEffect(() => { useEffect(() => {
if (registerResponse && showInvoice) { if (registerResponse && showInvoice) {
let t = setInterval(async () => { const t = setInterval(async () => {
let status = await svc.CheckRegistration(registerResponse.token); const status = await svc.CheckRegistration(registerResponse.token);
if ("error" in status) { if ("error" in status) {
setError(status); setError(status);
setRegisterResponse(undefined); setRegisterResponse(undefined);
setShowInvoice(false); setShowInvoice(false);
} else { } else {
let result: CheckRegisterResponse = status; const result: CheckRegisterResponse = status;
if (result.available && result.paid) { if (result.available && result.paid) {
setShowInvoice(false); setShowInvoice(false);
setRegisterStatus(status); setRegisterStatus(status);
@ -131,8 +133,14 @@ export default function Nip5Service(props: Nip05ServiceProps) {
} }
}, [registerResponse, showInvoice, svc]); }, [registerResponse, showInvoice, svc]);
function mapError(e: ServiceErrorCode, t: string | null): string | undefined { function mapError(
let whyMap = new Map([ e: ServiceErrorCode | undefined,
t: string | null
): string | undefined {
if (e === undefined) {
return undefined;
}
const whyMap = new Map([
["TOO_SHORT", formatMessage(messages.TooShort)], ["TOO_SHORT", formatMessage(messages.TooShort)],
["TOO_LONG", formatMessage(messages.TooLong)], ["TOO_LONG", formatMessage(messages.TooLong)],
["REGEX", formatMessage(messages.Regex)], ["REGEX", formatMessage(messages.Regex)],
@ -149,7 +157,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
return; return;
} }
let rsp = await svc.RegisterHandle(handle, domain, pubkey); const rsp = await svc.RegisterHandle(handle, domain, pubkey);
if ("error" in rsp) { if ("error" in rsp) {
setError(rsp); setError(rsp);
} else { } else {
@ -160,11 +168,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
async function updateProfile(handle: string, domain: string) { async function updateProfile(handle: string, domain: string) {
if (user) { if (user) {
let newProfile = { const newProfile = {
...user, ...user,
nip05: `${handle}@${domain}`, nip05: `${handle}@${domain}`,
} as UserMetadata; } as UserMetadata;
let ev = await publisher.metadata(newProfile); const ev = await publisher.metadata(newProfile);
publisher.broadcast(ev); publisher.broadcast(ev);
navigate("/settings"); navigate("/settings");
} }
@ -231,7 +239,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
<b className="error"> <b className="error">
<FormattedMessage {...messages.NotAvailable} />{" "} <FormattedMessage {...messages.NotAvailable} />{" "}
{mapError( {mapError(
availabilityResponse.why!, availabilityResponse.why,
availabilityResponse.reasonTag || null availabilityResponse.reasonTag || null
)} )}
</b> </b>

View File

@ -17,7 +17,6 @@ import Text from "Element/Text";
import { eventLink, getReactions, hexToBech32 } from "Util"; import { eventLink, getReactions, hexToBech32 } from "Util";
import NoteFooter, { Translation } from "Element/NoteFooter"; import NoteFooter, { Translation } from "Element/NoteFooter";
import NoteTime from "Element/NoteTime"; import NoteTime from "Element/NoteTime";
import ShowMore from "Element/ShowMore";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import { useUserProfiles } from "Feed/ProfileFeed"; import { useUserProfiles } from "Feed/ProfileFeed";
import { TaggedRawEvent, u256 } from "Nostr"; import { TaggedRawEvent, u256 } from "Nostr";
@ -39,10 +38,10 @@ export interface NoteProps {
["data-ev"]?: NEvent; ["data-ev"]?: NEvent;
} }
const HiddenNote = ({ children }: any) => { const HiddenNote = ({ children }: { children: React.ReactNode }) => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
return show ? ( return show ? (
children <>{children}</>
) : ( ) : (
<div className="card note hidden-note"> <div className="card note hidden-note">
<div className="header"> <div className="header">
@ -61,7 +60,6 @@ export default function Note(props: NoteProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
data, data,
className,
related, related,
highlight, highlight,
options: opt, options: opt,
@ -80,9 +78,9 @@ export default function Note(props: NoteProps) {
const { ref, inView, entry } = useInView({ triggerOnce: true }); const { ref, inView, entry } = useInView({ triggerOnce: true });
const [extendable, setExtendable] = useState<boolean>(false); const [extendable, setExtendable] = useState<boolean>(false);
const [showMore, setShowMore] = useState<boolean>(false); const [showMore, setShowMore] = useState<boolean>(false);
const baseClassname = `note card ${props.className ? props.className : ""}`; const baseClassName = `note card ${props.className ? props.className : ""}`;
const [translated, setTranslated] = useState<Translation>(); const [translated, setTranslated] = useState<Translation>();
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event; // TODO Why was this unused? Was this a mistake?
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const options = { const options = {
@ -93,7 +91,7 @@ export default function Note(props: NoteProps) {
}; };
const transformBody = useCallback(() => { const transformBody = useCallback(() => {
let body = ev?.Content ?? ""; const body = ev?.Content ?? "";
if (deletions?.length > 0) { if (deletions?.length > 0) {
return ( return (
<b className="error"> <b className="error">
@ -113,14 +111,14 @@ export default function Note(props: NoteProps) {
useLayoutEffect(() => { useLayoutEffect(() => {
if (entry && inView && extendable === false) { if (entry && inView && extendable === false) {
let h = entry?.target.clientHeight ?? 0; const h = entry?.target.clientHeight ?? 0;
if (h > 650) { if (h > 650) {
setExtendable(true); setExtendable(true);
} }
} }
}, [inView, entry, extendable]); }, [inView, entry, extendable]);
function goToEvent(e: any, id: u256) { function goToEvent(e: React.MouseEvent, id: u256) {
e.stopPropagation(); e.stopPropagation();
navigate(eventLink(id)); navigate(eventLink(id));
} }
@ -131,9 +129,9 @@ export default function Note(props: NoteProps) {
} }
const maxMentions = 2; const maxMentions = 2;
let replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event; const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
let mentions: { pk: string; name: string; link: ReactNode }[] = []; const mentions: { pk: string; name: string; link: ReactNode }[] = [];
for (let pk of ev.Thread?.PubKeys) { for (const pk of ev.Thread?.PubKeys ?? []) {
const u = users?.get(pk); const u = users?.get(pk);
const npub = hexToBech32("npub", pk); const npub = hexToBech32("npub", pk);
const shortNpub = npub.substring(0, 12); const shortNpub = npub.substring(0, 12);
@ -153,9 +151,9 @@ export default function Note(props: NoteProps) {
}); });
} }
} }
mentions.sort((a, b) => (a.name.startsWith("npub") ? 1 : -1)); mentions.sort((a) => (a.name.startsWith("npub") ? 1 : -1));
let othersLength = mentions.length - maxMentions; const othersLength = mentions.length - maxMentions;
const renderMention = (m: any, idx: number) => { const renderMention = (m: { link: React.ReactNode }, idx: number) => {
return ( return (
<> <>
{idx > 0 && ", "} {idx > 0 && ", "}
@ -268,7 +266,7 @@ export default function Note(props: NoteProps) {
const note = ( const note = (
<div <div
className={`${baseClassname}${highlight ? " active " : " "}${ className={`${baseClassName}${highlight ? " active " : " "}${
extendable && !showMore ? " note-expand" : "" extendable && !showMore ? " note-expand" : ""
}`} }`}
ref={ref} ref={ref}

View File

@ -33,14 +33,14 @@ export interface NoteCreatorProps {
show: boolean; show: boolean;
setShow: (s: boolean) => void; setShow: (s: boolean) => void;
replyTo?: NEvent; replyTo?: NEvent;
onSend?: Function; onSend?: () => void;
autoFocus: boolean; autoFocus: boolean;
} }
export function NoteCreator(props: NoteCreatorProps) { export function NoteCreator(props: NoteCreatorProps) {
const { show, setShow, replyTo, onSend, autoFocus } = props; const { show, setShow, replyTo, onSend, autoFocus } = props;
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const [note, setNote] = useState<string>(); const [note, setNote] = useState<string>("");
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [active, setActive] = useState<boolean>(false); const [active, setActive] = useState<boolean>(false);
const uploader = useFileUpload(); const uploader = useFileUpload();
@ -48,7 +48,7 @@ export function NoteCreator(props: NoteCreatorProps) {
async function sendNote() { async function sendNote() {
if (note) { if (note) {
let ev = replyTo const ev = replyTo
? await publisher.reply(replyTo, note) ? await publisher.reply(replyTo, note)
: await publisher.note(note); : await publisher.note(note);
console.debug("Sending note: ", ev); console.debug("Sending note: ", ev);
@ -64,21 +64,23 @@ export function NoteCreator(props: NoteCreatorProps) {
async function attachFile() { async function attachFile() {
try { try {
let file = await openFile(); const file = await openFile();
if (file) { if (file) {
let rx = await uploader.upload(file, file.name); const rx = await uploader.upload(file, file.name);
if (rx.url) { if (rx.url) {
setNote((n) => `${n ? `${n}\n` : ""}${rx.url}`); setNote((n) => `${n ? `${n}\n` : ""}${rx.url}`);
} else if (rx?.error) { } else if (rx?.error) {
setError(rx.error); setError(rx.error);
} }
} }
} catch (error: any) { } catch (error: unknown) {
setError(error?.message); if (error instanceof Error) {
setError(error?.message);
}
} }
} }
function onChange(ev: any) { function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
const { value } = ev.target; const { value } = ev.target;
setNote(value); setNote(value);
if (value) { if (value) {
@ -88,7 +90,7 @@ export function NoteCreator(props: NoteCreatorProps) {
} }
} }
function cancel(ev: any) { function cancel() {
setShow(false); setShow(false);
setNote(""); setNote("");
} }
@ -112,11 +114,7 @@ export function NoteCreator(props: NoteCreatorProps) {
value={note} value={note}
onFocus={() => setActive(true)} onFocus={() => setActive(true)}
/> />
<button <button type="button" className="attachment" onClick={attachFile}>
type="button"
className="attachment"
onClick={(e) => attachFile()}
>
<Attachment /> <Attachment />
</button> </button>
</div> </div>

View File

@ -95,7 +95,7 @@ export default function NoteFooter(props: NoteFooterProps) {
const groupReactions = useMemo(() => { const groupReactions = useMemo(() => {
const result = reactions?.reduce( const result = reactions?.reduce(
(acc, reaction) => { (acc, reaction) => {
let kind = normalizeReaction(reaction.content); const kind = normalizeReaction(reaction.content);
const rs = acc[kind] || []; const rs = acc[kind] || [];
if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) { if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) {
return acc; return acc;
@ -128,7 +128,7 @@ export default function NoteFooter(props: NoteFooterProps) {
async function react(content: string) { async function react(content: string) {
if (!hasReacted(content)) { if (!hasReacted(content)) {
let evLike = await publisher.react(ev, content); const evLike = await publisher.react(ev, content);
publisher.broadcast(evLike); publisher.broadcast(evLike);
} }
} }
@ -139,7 +139,7 @@ export default function NoteFooter(props: NoteFooterProps) {
formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) }) formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) })
) )
) { ) {
let evDelete = await publisher.delete(ev.Id); const evDelete = await publisher.delete(ev.Id);
publisher.broadcast(evDelete); publisher.broadcast(evDelete);
} }
} }
@ -150,14 +150,14 @@ export default function NoteFooter(props: NoteFooterProps) {
!prefs.confirmReposts || !prefs.confirmReposts ||
window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id })) window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))
) { ) {
let evRepost = await publisher.repost(ev); const evRepost = await publisher.repost(ev);
publisher.broadcast(evRepost); publisher.broadcast(evRepost);
} }
} }
} }
function tipButton() { function tipButton() {
let service = author?.lud16 || author?.lud06; const service = author?.lud16 || author?.lud06;
if (service) { if (service) {
return ( return (
<> <>
@ -246,7 +246,7 @@ export default function NoteFooter(props: NoteFooterProps) {
}); });
if (res.ok) { if (res.ok) {
let result = await res.json(); const result = await res.json();
if (typeof props.onTranslated === "function" && result) { if (typeof props.onTranslated === "function" && result) {
props.onTranslated({ props.onTranslated({
text: result.translatedText, text: result.translatedText,
@ -332,7 +332,7 @@ export default function NoteFooter(props: NoteFooterProps) {
{reactionIcons()} {reactionIcons()}
<div <div
className={`reaction-pill ${reply ? "reacted" : ""}`} className={`reaction-pill ${reply ? "reacted" : ""}`}
onClick={(e) => setReply((s) => !s)} onClick={() => setReply((s) => !s)}
> >
<div className="reaction-pill-icon"> <div className="reaction-pill-icon">
<Reply /> <Reply />

View File

@ -1,8 +1,13 @@
import "./Note.css"; import "./Note.css";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
export default function NoteGhost(props: any) { interface NoteGhostProps {
const className = `note card ${props.className ? props.className : ""}`; className?: string;
children: React.ReactNode;
}
export default function NoteGhost(props: NoteGhostProps) {
const className = `note card ${props.className ?? ""}`;
return ( return (
<div className={className}> <div className={className}>
<div className="header"> <div className="header">

View File

@ -23,7 +23,7 @@ export default function NoteReaction(props: NoteReactionProps) {
const refEvent = useMemo(() => { const refEvent = useMemo(() => {
if (ev) { if (ev) {
let eTags = ev.Tags.filter((a) => a.Key === "e"); const eTags = ev.Tags.filter((a) => a.Key === "e");
if (eTags.length > 0) { if (eTags.length > 0) {
return eTags[0].Event; return eTags[0].Event;
} }
@ -45,7 +45,7 @@ export default function NoteReaction(props: NoteReactionProps) {
ev.Content !== "#[0]" ev.Content !== "#[0]"
) { ) {
try { try {
let r: RawEvent = JSON.parse(ev.Content); const r: RawEvent = JSON.parse(ev.Content);
return r as TaggedRawEvent; return r as TaggedRawEvent;
} catch (e) { } catch (e) {
console.error("Could not load reposted content", e); console.error("Could not load reposted content", e);

View File

@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FormattedRelativeTime } from "react-intl";
const MinuteInMs = 1_000 * 60; const MinuteInMs = 1_000 * 60;
const HourInMs = MinuteInMs * 60; const HourInMs = MinuteInMs * 60;
@ -19,10 +18,11 @@ export default function NoteTime(props: NoteTimeProps) {
}).format(from); }).format(from);
const fromDate = new Date(from); const fromDate = new Date(from);
const isoDate = fromDate.toISOString(); const isoDate = fromDate.toISOString();
const ago = new Date().getTime() - from;
const absAgo = Math.abs(ago);
function calcTime() { function calcTime() {
const fromDate = new Date(from);
const ago = new Date().getTime() - from;
const absAgo = Math.abs(ago);
if (absAgo > DayInMs) { if (absAgo > DayInMs) {
return fromDate.toLocaleDateString(undefined, { return fromDate.toLocaleDateString(undefined, {
year: "2-digit", year: "2-digit",
@ -38,7 +38,7 @@ export default function NoteTime(props: NoteTimeProps) {
} else if (absAgo < MinuteInMs) { } else if (absAgo < MinuteInMs) {
return fallback; return fallback;
} else { } else {
let mins = Math.floor(absAgo / MinuteInMs); const mins = Math.floor(absAgo / MinuteInMs);
if (ago < 0) { if (ago < 0) {
return `in ${mins}m`; return `in ${mins}m`;
} }
@ -48,9 +48,9 @@ export default function NoteTime(props: NoteTimeProps) {
useEffect(() => { useEffect(() => {
setTime(calcTime()); setTime(calcTime());
let t = setInterval(() => { const t = setInterval(() => {
setTime((s) => { setTime((s) => {
let newTime = calcTime(); const newTime = calcTime();
if (newTime !== s) { if (newTime !== s) {
return newTime; return newTime;
} }

View File

@ -16,7 +16,7 @@ export interface NoteToSelfProps {
link?: string; link?: string;
} }
function NoteLabel({ pubkey, link }: NoteToSelfProps) { function NoteLabel({ pubkey }: NoteToSelfProps) {
const user = useUserProfile(pubkey); const user = useUserProfile(pubkey);
return ( return (
<div> <div>

View File

@ -63,10 +63,10 @@ export function getDisplayName(
pubkey: HexKey pubkey: HexKey
) { ) {
let name = hexToBech32("npub", pubkey).substring(0, 12); let name = hexToBech32("npub", pubkey).substring(0, 12);
if ((user?.display_name?.length ?? 0) > 0) { if (user?.display_name !== undefined && user.display_name.length > 0) {
name = user!.display_name!; name = user.display_name;
} else if ((user?.name?.length ?? 0) > 0) { } else if (user?.name !== undefined && user.name.length > 0) {
name = user!.name!; name = user.name;
} }
return name; return name;
} }

View File

@ -1,7 +1,15 @@
import useImgProxy from "Feed/ImgProxy"; import useImgProxy from "Feed/ImgProxy";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const ProxyImg = (props: any) => { interface ProxyImgProps
extends React.DetailedHTMLProps<
React.ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
> {
size?: number;
}
export const ProxyImg = (props: ProxyImgProps) => {
const { src, size, ...rest } = props; const { src, size, ...rest } = props;
const [url, setUrl] = useState<string>(); const [url, setUrl] = useState<string>();
const { proxy } = useImgProxy(); const { proxy } = useImgProxy();

View File

@ -15,7 +15,7 @@ export default function QrCode(props: QrCodeProps) {
useEffect(() => { useEffect(() => {
if ((props.data?.length ?? 0) > 0 && qrRef.current) { if ((props.data?.length ?? 0) > 0 && qrRef.current) {
let qr = new QRCodeStyling({ const qr = new QRCodeStyling({
width: props.width || 256, width: props.width || 256,
height: props.height || 256, height: props.height || 256,
data: props.data, data: props.data,
@ -35,9 +35,9 @@ export default function QrCode(props: QrCodeProps) {
qrRef.current.innerHTML = ""; qrRef.current.innerHTML = "";
qr.append(qrRef.current); qr.append(qrRef.current);
if (props.link) { if (props.link) {
qrRef.current.onclick = function (e) { qrRef.current.onclick = function () {
let elm = document.createElement("a"); const elm = document.createElement("a");
elm.href = props.link!; elm.href = props.link ?? "";
elm.click(); elm.click();
}; };
} }
@ -46,10 +46,5 @@ export default function QrCode(props: QrCodeProps) {
} }
}, [props.data, props.link]); }, [props.data, props.link]);
return ( return <div className={`qr${props.className ?? ""}`} ref={qrRef}></div>;
<div
className={`qr${props.className ? ` ${props.className}` : ""}`}
ref={qrRef}
></div>
);
} }

View File

@ -47,7 +47,7 @@ export default function Relay(props: RelayProps) {
); );
} }
let latency = Math.floor(state?.avgLatency ?? 0); const latency = Math.floor(state?.avgLatency ?? 0);
return ( return (
<> <>
<div className={`relay w-max`}> <div className={`relay w-max`}>
@ -104,7 +104,10 @@ export default function Relay(props: RelayProps) {
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects} <FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
</div> </div>
<div> <div>
<span className="icon-btn" onClick={() => navigate(state!.id)}> <span
className="icon-btn"
onClick={() => navigate(state?.id ?? "")}
>
<FontAwesomeIcon icon={faGear} /> <FontAwesomeIcon icon={faGear} />
</span> </span>
</div> </div>

View File

@ -50,7 +50,7 @@ export interface LNURLTipProps {
} }
export default function LNURLTip(props: LNURLTipProps) { export default function LNURLTip(props: LNURLTipProps) {
const onClose = props.onClose || (() => {}); const onClose = props.onClose || (() => undefined);
const service = props.svc; const service = props.svc;
const show = props.show || false; const show = props.show || false;
const { note, author, target } = props; const { note, author, target } = props;
@ -83,7 +83,7 @@ export default function LNURLTip(props: LNURLTipProps) {
useEffect(() => { useEffect(() => {
if (show && !props.invoice) { if (show && !props.invoice) {
loadService() loadService()
.then((a) => setPayService(a!)) .then((a) => setPayService(a ?? undefined))
.catch(() => setError(formatMessage(messages.LNURLFail))); .catch(() => setError(formatMessage(messages.LNURLFail)));
} else { } else {
setPayService(undefined); setPayService(undefined);
@ -97,25 +97,14 @@ export default function LNURLTip(props: LNURLTipProps) {
const serviceAmounts = useMemo(() => { const serviceAmounts = useMemo(() => {
if (payService) { if (payService) {
let min = (payService.minSendable ?? 0) / 1000; const min = (payService.minSendable ?? 0) / 1000;
let max = (payService.maxSendable ?? 0) / 1000; const max = (payService.maxSendable ?? 0) / 1000;
return amounts.filter((a) => a >= min && a <= max); return amounts.filter((a) => a >= min && a <= max);
} }
return []; return [];
}, [payService]); }, [payService]);
const metadata = useMemo(() => { // TODO Why was this never used? I think this might be a bug, or was it just an oversight?
if (payService) {
let meta: string[][] = JSON.parse(payService.metadata);
let desc = meta.find((a) => a[0] === "text/plain");
let image = meta.find((a) => a[0] === "image/png;base64");
return {
description: desc ? desc[1] : null,
image: image ? image[1] : null,
};
}
return null;
}, [payService]);
const selectAmount = (a: number) => { const selectAmount = (a: number) => {
setError(undefined); setError(undefined);
@ -124,9 +113,9 @@ export default function LNURLTip(props: LNURLTipProps) {
}; };
async function fetchJson<T>(url: string) { async function fetchJson<T>(url: string) {
let rsp = await fetch(url); const rsp = await fetch(url);
if (rsp.ok) { if (rsp.ok) {
let data: T = await rsp.json(); const data: T = await rsp.json();
console.log(data); console.log(data);
setError(undefined); setError(undefined);
return data; return data;
@ -136,12 +125,12 @@ export default function LNURLTip(props: LNURLTipProps) {
async function loadService(): Promise<LNURLService | null> { async function loadService(): Promise<LNURLService | null> {
if (service) { if (service) {
let isServiceUrl = service.toLowerCase().startsWith("lnurl"); const isServiceUrl = service.toLowerCase().startsWith("lnurl");
if (isServiceUrl) { if (isServiceUrl) {
let serviceUrl = bech32ToText(service); const serviceUrl = bech32ToText(service);
return await fetchJson(serviceUrl); return await fetchJson(serviceUrl);
} else { } else {
let ns = service.split("@"); const ns = service.split("@");
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`); return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
} }
} }
@ -165,9 +154,9 @@ export default function LNURLTip(props: LNURLTipProps) {
url = `${payService.callback}?${amountParam}${commentParam}`; url = `${payService.callback}?${amountParam}${commentParam}`;
} }
try { try {
let rsp = await fetch(url); const rsp = await fetch(url);
if (rsp.ok) { if (rsp.ok) {
let data = await rsp.json(); const data = await rsp.json();
console.log(data); console.log(data);
if (data.status === "ERROR") { if (data.status === "ERROR") {
setError(data.reason); setError(data.reason);
@ -185,8 +174,8 @@ export default function LNURLTip(props: LNURLTipProps) {
} }
function custom() { function custom() {
let min = (payService?.minSendable ?? 1000) / 1000; const min = (payService?.minSendable ?? 1000) / 1000;
let max = (payService?.maxSendable ?? 21_000_000_000) / 1000; const max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
return ( return (
<div className="custom-amount flex"> <div className="custom-amount flex">
<input <input
@ -201,8 +190,8 @@ export default function LNURLTip(props: LNURLTipProps) {
<button <button
className="secondary" className="secondary"
type="button" type="button"
disabled={!Boolean(customAmount)} disabled={!customAmount}
onClick={() => selectAmount(customAmount!)} onClick={() => selectAmount(customAmount ?? 0)}
> >
<FormattedMessage {...messages.Confirm} /> <FormattedMessage {...messages.Confirm} />
</button> </button>
@ -213,13 +202,15 @@ export default function LNURLTip(props: LNURLTipProps) {
async function payWebLNIfEnabled(invoice: LNURLInvoice) { async function payWebLNIfEnabled(invoice: LNURLInvoice) {
try { try {
if (webln?.enabled) { if (webln?.enabled) {
let res = await webln.sendPayment(invoice!.pr); const res = await webln.sendPayment(invoice?.pr ?? "");
console.log(res); console.log(res);
setSuccess(invoice!.successAction || {}); setSuccess(invoice?.successAction ?? {});
} }
} catch (e: any) { } catch (e: unknown) {
setError(e.toString());
console.warn(e); console.warn(e);
if (e instanceof Error) {
setError(e.toString());
}
} }
} }

View File

@ -5,7 +5,7 @@ import ReactMarkdown from "react-markdown";
import { visit, SKIP } from "unist-util-visit"; import { visit, SKIP } from "unist-util-visit";
import { UrlRegex, MentionRegex, InvoiceRegex, HashtagRegex } from "Const"; import { UrlRegex, MentionRegex, InvoiceRegex, HashtagRegex } from "Const";
import { eventLink, hexToBech32 } from "Util"; import { eventLink, hexToBech32, unwrap } from "Util";
import Invoice from "Element/Invoice"; import Invoice from "Element/Invoice";
import Hashtag from "Element/Hashtag"; import Hashtag from "Element/Hashtag";
@ -14,11 +14,12 @@ import { MetadataCache } from "State/Users";
import Mention from "Element/Mention"; import Mention from "Element/Mention";
import HyperText from "Element/HyperText"; import HyperText from "Element/HyperText";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
import * as unist from "unist";
export type Fragment = string | JSX.Element; export type Fragment = string | React.ReactNode;
export interface TextFragment { export interface TextFragment {
body: Fragment[]; body: React.ReactNode[];
tags: Tag[]; tags: Tag[];
users: Map<string, MetadataCache>; users: Map<string, MetadataCache>;
} }
@ -52,24 +53,24 @@ export default function Text({ content, tags, creator, users }: TextProps) {
.map((f) => { .map((f) => {
if (typeof f === "string") { if (typeof f === "string") {
return f.split(MentionRegex).map((match) => { return f.split(MentionRegex).map((match) => {
let matchTag = match.match(/#\[(\d+)\]/); const matchTag = match.match(/#\[(\d+)\]/);
if (matchTag && matchTag.length === 2) { if (matchTag && matchTag.length === 2) {
let idx = parseInt(matchTag[1]); const idx = parseInt(matchTag[1]);
let ref = frag.tags?.find((a) => a.Index === idx); const ref = frag.tags?.find((a) => a.Index === idx);
if (ref) { if (ref) {
switch (ref.Key) { switch (ref.Key) {
case "p": { case "p": {
return <Mention pubkey={ref.PubKey!} />; return <Mention pubkey={ref.PubKey ?? ""} />;
} }
case "e": { case "e": {
let eText = hexToBech32("note", ref.Event!).substring( const eText = hexToBech32(
0, "note",
12 ref.Event ?? ""
); ).substring(0, 12);
return ( return (
<Link <Link
key={ref.Event} key={ref.Event}
to={eventLink(ref.Event!)} to={eventLink(ref.Event ?? "")}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
#{eText} #{eText}
@ -77,7 +78,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
); );
} }
case "t": { case "t": {
return <Hashtag tag={ref.Hashtag!} />; return <Hashtag tag={ref.Hashtag ?? ""} />;
} }
} }
} }
@ -127,7 +128,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
} }
function transformLi(frag: TextFragment) { function transformLi(frag: TextFragment) {
let fragments = transformText(frag); const fragments = transformText(frag);
return <li>{fragments}</li>; return <li>{fragments}</li>;
} }
@ -140,9 +141,6 @@ export default function Text({ content, tags, creator, users }: TextProps) {
} }
function transformText(frag: TextFragment) { function transformText(frag: TextFragment) {
if (frag.body === undefined) {
debugger;
}
let fragments = extractMentions(frag); let fragments = extractMentions(frag);
fragments = extractLinks(fragments); fragments = extractLinks(fragments);
fragments = extractInvoices(fragments); fragments = extractInvoices(fragments);
@ -152,15 +150,22 @@ export default function Text({ content, tags, creator, users }: TextProps) {
const components = useMemo(() => { const components = useMemo(() => {
return { return {
p: (x: any) => p: (x: { children?: React.ReactNode[] }) =>
transformParagraph({ body: x.children ?? [], tags, users }), transformParagraph({ body: x.children ?? [], tags, users }),
a: (x: any) => <HyperText link={x.href} creator={creator} />, a: (x: { href?: string }) => (
li: (x: any) => transformLi({ body: x.children ?? [], tags, users }), <HyperText link={x.href ?? ""} creator={creator} />
),
li: (x: { children?: Fragment[] }) =>
transformLi({ body: x.children ?? [], tags, users }),
}; };
}, [content]); }, [content]);
interface Node extends unist.Node<unist.Data> {
value: string;
}
const disableMarkdownLinks = useCallback( const disableMarkdownLinks = useCallback(
() => (tree: any) => { () => (tree: Node) => {
visit(tree, (node, index, parent) => { visit(tree, (node, index, parent) => {
if ( if (
parent && parent &&
@ -172,8 +177,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
node.type === "definition") node.type === "definition")
) { ) {
node.type = "text"; node.type = "text";
const position = unwrap(node.position);
node.value = content node.value = content
.slice(node.position.start.offset, node.position.end.offset) .slice(position.start.offset, position.end.offset)
.replace(/\)$/, " )"); .replace(/\)$/, " )");
return SKIP; return SKIP;
} }

View File

@ -2,7 +2,7 @@ import "@webscopeio/react-textarea-autocomplete/style.css";
import "./Textarea.css"; import "./Textarea.css";
import { useState } from "react"; import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl } from "react-intl";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
import emoji from "@jukben/emoji-search"; import emoji from "@jukben/emoji-search";
import TextareaAutosize from "react-textarea-autosize"; import TextareaAutosize from "react-textarea-autosize";
@ -30,7 +30,7 @@ const EmojiItem = ({ entity: { name, char } }: { entity: EmojiItemProps }) => {
}; };
const UserItem = (metadata: MetadataCache) => { const UserItem = (metadata: MetadataCache) => {
const { pubkey, display_name, picture, nip05, ...rest } = metadata; const { pubkey, display_name, nip05, ...rest } = metadata;
return ( return (
<div key={pubkey} className="user-item"> <div key={pubkey} className="user-item">
<div className="user-picture"> <div className="user-picture">
@ -44,7 +44,15 @@ const UserItem = (metadata: MetadataCache) => {
); );
}; };
const Textarea = ({ users, onChange, ...rest }: any) => { interface TextareaProps {
autoFocus: boolean;
className: string;
onChange(ev: React.ChangeEvent<HTMLTextAreaElement>): void;
value: string;
onFocus(): void;
}
const Textarea = (props: TextareaProps) => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -52,7 +60,7 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
const userDataProvider = (token: string) => { const userDataProvider = (token: string) => {
setQuery(token); setQuery(token);
return allUsers; return allUsers ?? [];
}; };
const emojiDataProvider = (token: string) => { const emojiDataProvider = (token: string) => {
@ -62,23 +70,26 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
}; };
return ( return (
// @ts-expect-error If anybody can figure out how to type this, please do
<ReactTextareaAutocomplete <ReactTextareaAutocomplete
{...rest} {...props}
loadingComponent={() => <span>Loading....</span>} loadingComponent={() => <span>Loading...</span>}
placeholder={formatMessage(messages.NotePlaceholder)} placeholder={formatMessage(messages.NotePlaceholder)}
onChange={onChange}
textAreaComponent={TextareaAutosize} textAreaComponent={TextareaAutosize}
trigger={{ trigger={{
":": { ":": {
dataProvider: emojiDataProvider, dataProvider: emojiDataProvider,
component: EmojiItem, component: EmojiItem,
output: (item: EmojiItemProps, trigger) => item.char, output: (item: EmojiItemProps) => item.char,
}, },
"@": { "@": {
afterWhitespace: true, afterWhitespace: true,
dataProvider: userDataProvider, dataProvider: userDataProvider,
component: (props: any) => <UserItem {...props.entity} />, component: (props: { entity: MetadataCache }) => (
output: (item: any) => `@${hexToBech32("npub", item.pubkey)}`, <UserItem {...props.entity} />
),
output: (item: { pubkey: string }) =>
`@${hexToBech32("npub", item.pubkey)}`,
}, },
}} }}
/> />

View File

@ -6,19 +6,18 @@ import { useNavigate, useLocation, Link } from "react-router-dom";
import { TaggedRawEvent, u256, HexKey } from "Nostr"; import { TaggedRawEvent, u256, HexKey } from "Nostr";
import { default as NEvent } from "Nostr/Event"; import { default as NEvent } from "Nostr/Event";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import { eventLink, hexToBech32, bech32ToHex } from "Util"; import { eventLink, bech32ToHex, unwrap } from "Util";
import BackButton from "Element/BackButton"; import BackButton from "Element/BackButton";
import Note from "Element/Note"; import Note from "Element/Note";
import NoteGhost from "Element/NoteGhost"; import NoteGhost from "Element/NoteGhost";
import Collapsed from "Element/Collapsed"; import Collapsed from "Element/Collapsed";
import messages from "./messages"; import messages from "./messages";
function getParent( function getParent(
ev: HexKey, ev: HexKey,
chains: Map<HexKey, NEvent[]> chains: Map<HexKey, NEvent[]>
): HexKey | undefined { ): HexKey | undefined {
for (let [k, vs] of chains.entries()) { for (const [k, vs] of chains.entries()) {
const fs = vs.map((a) => a.Id); const fs = vs.map((a) => a.Id);
if (fs.includes(ev)) { if (fs.includes(ev)) {
return k; return k;
@ -53,7 +52,6 @@ interface SubthreadProps {
const Subthread = ({ const Subthread = ({
active, active,
path, path,
from,
notes, notes,
related, related,
chains, chains,
@ -332,20 +330,19 @@ export default function Thread(props: ThreadProps) {
const location = useLocation(); const location = useLocation();
const urlNoteId = location?.pathname.slice(3); const urlNoteId = location?.pathname.slice(3);
const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId); const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId);
const rootNoteId = root && hexToBech32("note", root.Id);
const chains = useMemo(() => { const chains = useMemo(() => {
let chains = new Map<u256, NEvent[]>(); const chains = new Map<u256, NEvent[]>();
parsedNotes parsedNotes
?.filter((a) => a.Kind === EventKind.TextNote) ?.filter((a) => a.Kind === EventKind.TextNote)
.sort((a, b) => b.CreatedAt - a.CreatedAt) .sort((a, b) => b.CreatedAt - a.CreatedAt)
.forEach((v) => { .forEach((v) => {
let replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event; const replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
if (replyTo) { if (replyTo) {
if (!chains.has(replyTo)) { if (!chains.has(replyTo)) {
chains.set(replyTo, [v]); chains.set(replyTo, [v]);
} else { } else {
chains.get(replyTo)!.push(v); unwrap(chains.get(replyTo)).push(v);
} }
} else if (v.Tags.length > 0) { } else if (v.Tags.length > 0) {
console.log("Not replying to anything: ", v); console.log("Not replying to anything: ", v);
@ -370,7 +367,7 @@ export default function Thread(props: ThreadProps) {
return; return;
} }
let subthreadPath = []; const subthreadPath = [];
let parent = getParent(urlNoteHex, chains); let parent = getParent(urlNoteHex, chains);
while (parent) { while (parent) {
subthreadPath.unshift(parent); subthreadPath.unshift(parent);
@ -414,7 +411,7 @@ export default function Thread(props: ThreadProps) {
if (!from || !chains) { if (!from || !chains) {
return; return;
} }
let replies = chains.get(from); const replies = chains.get(from);
if (replies) { if (replies) {
return ( return (
<Subthread <Subthread
@ -476,6 +473,6 @@ function getReplies(from: u256, chains?: Map<u256, NEvent[]>): NEvent[] {
if (!from || !chains) { if (!from || !chains) {
return []; return [];
} }
let replies = chains.get(from); const replies = chains.get(from);
return replies ? replies : []; return replies ? replies : [];
} }

View File

@ -1,4 +1,4 @@
import { CSSProperties, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TidalRegex } from "Const"; import { TidalRegex } from "Const";
// Re-use dom parser across instances of TidalEmbed // Re-use dom parser across instances of TidalEmbed

View File

@ -83,7 +83,7 @@ export default function Timeline({
} }
case EventKind.Reaction: case EventKind.Reaction:
case EventKind.Repost: { case EventKind.Repost: {
let eRef = e.tags.find((a) => a[0] === "e")?.at(1); const eRef = e.tags.find((a) => a[0] === "e")?.at(1);
return ( return (
<NoteReaction <NoteReaction
data={e} data={e}

View File

@ -2,10 +2,10 @@ import "./Zap.css";
import { useMemo } from "react"; import { useMemo } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
// @ts-expect-error // @ts-expect-error No types available
import { decode as invoiceDecode } from "light-bolt11-decoder"; import { decode as invoiceDecode } from "light-bolt11-decoder";
import { bytesToHex } from "@noble/hashes/utils"; import { bytesToHex } from "@noble/hashes/utils";
import { sha256 } from "Util"; import { sha256, unwrap } from "Util";
//import { sha256 } from "Util"; //import { sha256 } from "Util";
import { formatShort } from "Number"; import { formatShort } from "Number";
@ -24,15 +24,19 @@ function findTag(e: TaggedRawEvent, tag: string) {
return maybeTag && maybeTag[1]; return maybeTag && maybeTag[1];
} }
interface Section {
name: string;
}
function getInvoice(zap: TaggedRawEvent) { function getInvoice(zap: TaggedRawEvent) {
const bolt11 = findTag(zap, "bolt11"); const bolt11 = findTag(zap, "bolt11");
const decoded = invoiceDecode(bolt11); const decoded = invoiceDecode(bolt11);
const amount = decoded.sections.find( const amount = decoded.sections.find(
(section: any) => section.name === "amount" (section: Section) => section.name === "amount"
)?.value; )?.value;
const hash = decoded.sections.find( const hash = decoded.sections.find(
(section: any) => section.name === "description_hash" (section: Section) => section.name === "description_hash"
)?.value; )?.value;
return { amount, hash: hash ? bytesToHex(hash) : undefined }; return { amount, hash: hash ? bytesToHex(hash) : undefined };
@ -72,7 +76,7 @@ export function parseZap(zap: TaggedRawEvent): ParsedZap {
const { amount, hash } = getInvoice(zap); const { amount, hash } = getInvoice(zap);
const zapper = hash ? getZapper(zap, hash) : { isValid: false }; const zapper = hash ? getZapper(zap, hash) : { isValid: false };
const e = findTag(zap, "e"); const e = findTag(zap, "e");
const p = findTag(zap, "p")!; const p = unwrap(findTag(zap, "p"));
return { return {
id: zap.id, id: zap.id,
e, e,

View File

@ -6,8 +6,8 @@ import { useUserProfile } from "Feed/ProfileFeed";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => { const ZapButton = ({ pubkey, svc }: { pubkey: HexKey; svc?: string }) => {
const profile = useUserProfile(pubkey!); const profile = useUserProfile(pubkey);
const [zap, setZap] = useState(false); const [zap, setZap] = useState(false);
const service = svc ?? (profile?.lud16 || profile?.lud06); const service = svc ?? (profile?.lud16 || profile?.lud06);
@ -15,7 +15,7 @@ const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => {
return ( return (
<> <>
<div className="zap-button" onClick={(e) => setZap(true)}> <div className="zap-button" onClick={() => setZap(true)}>
<FontAwesomeIcon icon={faBolt} /> <FontAwesomeIcon icon={faBolt} />
</div> </div>
<SendSats <SendSats

View File

@ -1,12 +1,13 @@
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { TaggedRawEvent } from "Nostr";
import { System } from "Nostr/System"; import { System } from "Nostr/System";
import { default as NEvent } from "Nostr/Event"; import { default as NEvent } from "Nostr/Event";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import Tag from "Nostr/Tag"; import Tag from "Nostr/Tag";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr"; import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr";
import { bech32ToHex } from "Util"; import { bech32ToHex, unwrap } from "Util";
import { DefaultRelays, HashtagRegex } from "Const"; import { DefaultRelays, HashtagRegex } from "Const";
import { RelaySettings } from "Nostr/Connection"; import { RelaySettings } from "Nostr/Connection";
@ -40,9 +41,12 @@ export default function useEventPublisher() {
async function signEvent(ev: NEvent): Promise<NEvent> { async function signEvent(ev: NEvent): Promise<NEvent> {
if (hasNip07 && !privKey) { if (hasNip07 && !privKey) {
ev.Id = await ev.CreateId(); ev.Id = await ev.CreateId();
let tmpEv = await barierNip07(() => const tmpEv = (await barrierNip07(() =>
window.nostr.signEvent(ev.ToObject()) window.nostr.signEvent(ev.ToObject())
); )) as TaggedRawEvent;
if (!tmpEv.relays) {
tmpEv.relays = [];
}
return new NEvent(tmpEv); return new NEvent(tmpEv);
} else if (privKey) { } else if (privKey) {
await ev.Sign(privKey); await ev.Sign(privKey);
@ -111,14 +115,14 @@ export default function useEventPublisher() {
*/ */
broadcastForBootstrap: (ev: NEvent | undefined) => { broadcastForBootstrap: (ev: NEvent | undefined) => {
if (ev) { if (ev) {
for (let [k, _] of DefaultRelays) { for (const [k] of DefaultRelays) {
System.WriteOnceToRelay(k, ev); System.WriteOnceToRelay(k, ev);
} }
} }
}, },
muted: async (keys: HexKey[], priv: HexKey[]) => { muted: async (keys: HexKey[], priv: HexKey[]) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Lists; ev.Kind = EventKind.Lists;
ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length)); ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length));
keys.forEach((p) => { keys.forEach((p) => {
@ -129,7 +133,7 @@ export default function useEventPublisher() {
const ps = priv.map((p) => ["p", p]); const ps = priv.map((p) => ["p", p]);
const plaintext = JSON.stringify(ps); const plaintext = JSON.stringify(ps);
if (hasNip07 && !privKey) { if (hasNip07 && !privKey) {
content = await barierNip07(() => content = await barrierNip07(() =>
window.nostr.nip04.encrypt(pubKey, plaintext) window.nostr.nip04.encrypt(pubKey, plaintext)
); );
} else if (privKey) { } else if (privKey) {
@ -142,7 +146,7 @@ export default function useEventPublisher() {
}, },
metadata: async (obj: UserMetadata) => { metadata: async (obj: UserMetadata) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.SetMetadata; ev.Kind = EventKind.SetMetadata;
ev.Content = JSON.stringify(obj); ev.Content = JSON.stringify(obj);
return await signEvent(ev); return await signEvent(ev);
@ -150,7 +154,7 @@ export default function useEventPublisher() {
}, },
note: async (msg: string) => { note: async (msg: string) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.TextNote; ev.Kind = EventKind.TextNote;
processContent(ev, msg); processContent(ev, msg);
return await signEvent(ev); return await signEvent(ev);
@ -158,18 +162,14 @@ export default function useEventPublisher() {
}, },
zap: async (author: HexKey, note?: HexKey, msg?: string) => { zap: async (author: HexKey, note?: HexKey, msg?: string) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ZapRequest; ev.Kind = EventKind.ZapRequest;
if (note) { if (note) {
// @ts-ignore ev.Tags.push(new Tag(["e", note], 0));
ev.Tags.push(new Tag(["e", note]));
} }
// @ts-ignore ev.Tags.push(new Tag(["p", author], 0));
ev.Tags.push(new Tag(["p", author]));
// @ts-ignore
const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)]; const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)];
// @ts-ignore ev.Tags.push(new Tag(relayTag, 0));
ev.Tags.push(new Tag(relayTag));
processContent(ev, msg || ""); processContent(ev, msg || "");
return await signEvent(ev); return await signEvent(ev);
} }
@ -179,15 +179,20 @@ export default function useEventPublisher() {
*/ */
reply: async (replyTo: NEvent, msg: string) => { reply: async (replyTo: NEvent, msg: string) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.TextNote; ev.Kind = EventKind.TextNote;
let thread = replyTo.Thread; const thread = replyTo.Thread;
if (thread) { if (thread) {
if (thread.Root || thread.ReplyTo) { if (thread.Root || thread.ReplyTo) {
ev.Tags.push( ev.Tags.push(
new Tag( new Tag(
["e", thread.Root?.Event ?? thread.ReplyTo?.Event!, "", "root"], [
"e",
thread.Root?.Event ?? thread.ReplyTo?.Event ?? "",
"",
"root",
],
ev.Tags.length ev.Tags.length
) )
); );
@ -199,7 +204,7 @@ export default function useEventPublisher() {
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length)); ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
} }
for (let pk of thread.PubKeys) { for (const pk of thread.PubKeys) {
if (pk === pubKey) { if (pk === pubKey) {
continue; // dont tag self in replies continue; // dont tag self in replies
} }
@ -218,7 +223,7 @@ export default function useEventPublisher() {
}, },
react: async (evRef: NEvent, content = "+") => { react: async (evRef: NEvent, content = "+") => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Reaction; ev.Kind = EventKind.Reaction;
ev.Content = content; ev.Content = content;
ev.Tags.push(new Tag(["e", evRef.Id], 0)); ev.Tags.push(new Tag(["e", evRef.Id], 0));
@ -228,10 +233,10 @@ export default function useEventPublisher() {
}, },
saveRelays: async () => { saveRelays: async () => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList; ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays); ev.Content = JSON.stringify(relays);
for (let pk of follows) { for (const pk of follows) {
ev.Tags.push(new Tag(["p", pk], ev.Tags.length)); ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
} }
@ -243,16 +248,16 @@ export default function useEventPublisher() {
newRelays?: Record<string, RelaySettings> newRelays?: Record<string, RelaySettings>
) => { ) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList; ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(newRelays ?? relays); ev.Content = JSON.stringify(newRelays ?? relays);
let temp = new Set(follows); const temp = new Set(follows);
if (Array.isArray(pkAdd)) { if (Array.isArray(pkAdd)) {
pkAdd.forEach((a) => temp.add(a)); pkAdd.forEach((a) => temp.add(a));
} else { } else {
temp.add(pkAdd); temp.add(pkAdd);
} }
for (let pk of temp) { for (const pk of temp) {
if (pk.length !== 64) { if (pk.length !== 64) {
continue; continue;
} }
@ -264,10 +269,10 @@ export default function useEventPublisher() {
}, },
removeFollow: async (pkRemove: HexKey) => { removeFollow: async (pkRemove: HexKey) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList; ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays); ev.Content = JSON.stringify(relays);
for (let pk of follows) { for (const pk of follows) {
if (pk === pkRemove || pk.length !== 64) { if (pk === pkRemove || pk.length !== 64) {
continue; continue;
} }
@ -282,7 +287,7 @@ export default function useEventPublisher() {
*/ */
delete: async (id: u256) => { delete: async (id: u256) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Deletion; ev.Kind = EventKind.Deletion;
ev.Content = ""; ev.Content = "";
ev.Tags.push(new Tag(["e", id], 0)); ev.Tags.push(new Tag(["e", id], 0));
@ -290,11 +295,11 @@ export default function useEventPublisher() {
} }
}, },
/** /**
* Respot a note (NIP-18) * Repost a note (NIP-18)
*/ */
repost: async (note: NEvent) => { repost: async (note: NEvent) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.Repost; ev.Kind = EventKind.Repost;
ev.Content = JSON.stringify(note.Original); ev.Content = JSON.stringify(note.Original);
ev.Tags.push(new Tag(["e", note.Id], 0)); ev.Tags.push(new Tag(["e", note.Id], 0));
@ -311,12 +316,12 @@ export default function useEventPublisher() {
return "<CANT DECRYPT>"; return "<CANT DECRYPT>";
} }
try { try {
let otherPubKey = const otherPubKey =
note.PubKey === pubKey note.PubKey === pubKey
? note.Tags.filter((a) => a.Key === "p")[0].PubKey! ? unwrap(note.Tags.filter((a) => a.Key === "p")[0].PubKey)
: note.PubKey; : note.PubKey;
if (hasNip07 && !privKey) { if (hasNip07 && !privKey) {
return await barierNip07(() => return await barrierNip07(() =>
window.nostr.nip04.decrypt(otherPubKey, note.Content) window.nostr.nip04.decrypt(otherPubKey, note.Content)
); );
} else if (privKey) { } else if (privKey) {
@ -324,21 +329,21 @@ export default function useEventPublisher() {
return note.Content; return note.Content;
} }
} catch (e) { } catch (e) {
console.error("Decyrption failed", e); console.error("Decryption failed", e);
return "<DECRYPTION FAILED>"; return "<DECRYPTION FAILED>";
} }
} }
}, },
sendDm: async (content: string, to: HexKey) => { sendDm: async (content: string, to: HexKey) => {
if (pubKey) { if (pubKey) {
let ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
ev.Kind = EventKind.DirectMessage; ev.Kind = EventKind.DirectMessage;
ev.Content = content; ev.Content = content;
ev.Tags.push(new Tag(["p", to], 0)); ev.Tags.push(new Tag(["p", to], 0));
try { try {
if (hasNip07 && !privKey) { if (hasNip07 && !privKey) {
let cx: string = await barierNip07(() => const cx: string = await barrierNip07(() =>
window.nostr.nip04.encrypt(to, content) window.nostr.nip04.encrypt(to, content)
); );
ev.Content = cx; ev.Content = cx;
@ -358,12 +363,12 @@ export default function useEventPublisher() {
let isNip07Busy = false; let isNip07Busy = false;
const delay = (t: number) => { const delay = (t: number) => {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
setTimeout(resolve, t); setTimeout(resolve, t);
}); });
}; };
export const barierNip07 = async (then: () => Promise<any>) => { export const barrierNip07 = async <T>(then: () => Promise<T>): Promise<T> => {
while (isNip07Busy) { while (isNip07Busy) {
await delay(10); await delay(10);
} }

View File

@ -6,7 +6,7 @@ import useSubscription from "Feed/Subscription";
export default function useFollowersFeed(pubkey: HexKey) { export default function useFollowersFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let x = new Subscriptions(); const x = new Subscriptions();
x.Id = `followers:${pubkey.slice(0, 12)}`; x.Id = `followers:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]); x.Kinds = new Set([EventKind.ContactList]);
x.PTags = new Set([pubkey]); x.PTags = new Set([pubkey]);

View File

@ -6,7 +6,7 @@ import useSubscription, { NoteStore } from "Feed/Subscription";
export default function useFollowsFeed(pubkey: HexKey) { export default function useFollowsFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let x = new Subscriptions(); const x = new Subscriptions();
x.Id = `follows:${pubkey.slice(0, 12)}`; x.Id = `follows:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ContactList]); x.Kinds = new Set([EventKind.ContactList]);
x.Authors = new Set([pubkey]); x.Authors = new Set([pubkey]);
@ -18,10 +18,10 @@ export default function useFollowsFeed(pubkey: HexKey) {
} }
export function getFollowers(feed: NoteStore, pubkey: HexKey) { export function getFollowers(feed: NoteStore, pubkey: HexKey) {
let contactLists = feed?.notes.filter( const contactLists = feed?.notes.filter(
(a) => a.kind === EventKind.ContactList && a.pubkey === pubkey (a) => a.kind === EventKind.ContactList && a.pubkey === pubkey
); );
let pTags = contactLists?.map((a) => const pTags = contactLists?.map((a) =>
a.tags.filter((b) => b[0] === "p").map((c) => c[1]) a.tags.filter((b) => b[0] === "p").map((c) => c[1])
); );
return [...new Set(pTags?.flat())]; return [...new Set(pTags?.flat())];

View File

@ -2,6 +2,7 @@ import * as secp from "@noble/secp256k1";
import * as base64 from "@protobufjs/base64"; import * as base64 from "@protobufjs/base64";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import { unwrap } from "Util";
export interface ImgProxySettings { export interface ImgProxySettings {
url: string; url: string;
@ -21,8 +22,8 @@ export default function useImgProxy() {
async function signUrl(u: string) { async function signUrl(u: string) {
const result = await secp.utils.hmacSha256( const result = await secp.utils.hmacSha256(
secp.utils.hexToBytes(settings!.key), secp.utils.hexToBytes(unwrap(settings).key),
secp.utils.hexToBytes(settings!.salt), secp.utils.hexToBytes(unwrap(settings).salt),
te.encode(u) te.encode(u)
); );
return urlSafe(base64.encode(result, 0, result.byteLength)); return urlSafe(base64.encode(result, 0, result.byteLength));

View File

@ -19,9 +19,10 @@ import { RootState } from "State/Store";
import { mapEventToProfile, MetadataCache } from "State/Users"; import { mapEventToProfile, MetadataCache } from "State/Users";
import { useDb } from "State/Users/Db"; import { useDb } from "State/Users/Db";
import useSubscription from "Feed/Subscription"; import useSubscription from "Feed/Subscription";
import { barierNip07 } from "Feed/EventPublisher"; import { barrierNip07 } from "Feed/EventPublisher";
import { getMutedKeys, getNewest } from "Feed/MuteList"; import { getMutedKeys, getNewest } from "Feed/MuteList";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import { unwrap } from "Util";
/** /**
* Managed loading data for the current logged in user * Managed loading data for the current logged in user
@ -40,7 +41,7 @@ export default function useLoginFeed() {
const subMetadata = useMemo(() => { const subMetadata = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = `login:meta`; sub.Id = `login:meta`;
sub.Authors = new Set([pubKey]); sub.Authors = new Set([pubKey]);
sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]); sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]);
@ -52,7 +53,7 @@ export default function useLoginFeed() {
const subNotification = useMemo(() => { const subNotification = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = "login:notifications"; sub.Id = "login:notifications";
// todo: add zaps // todo: add zaps
sub.Kinds = new Set([EventKind.TextNote]); sub.Kinds = new Set([EventKind.TextNote]);
@ -64,7 +65,7 @@ export default function useLoginFeed() {
const subMuted = useMemo(() => { const subMuted = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = "login:muted"; sub.Id = "login:muted";
sub.Kinds = new Set([EventKind.Lists]); sub.Kinds = new Set([EventKind.Lists]);
sub.Authors = new Set([pubKey]); sub.Authors = new Set([pubKey]);
@ -77,12 +78,12 @@ export default function useLoginFeed() {
const subDms = useMemo(() => { const subDms = useMemo(() => {
if (!pubKey) return null; if (!pubKey) return null;
let dms = new Subscriptions(); const dms = new Subscriptions();
dms.Id = "login:dms"; dms.Id = "login:dms";
dms.Kinds = new Set([EventKind.DirectMessage]); dms.Kinds = new Set([EventKind.DirectMessage]);
dms.PTags = new Set([pubKey]); dms.PTags = new Set([pubKey]);
let dmsFromME = new Subscriptions(); const dmsFromME = new Subscriptions();
dmsFromME.Authors = new Set([pubKey]); dmsFromME.Authors = new Set([pubKey]);
dmsFromME.Kinds = new Set([EventKind.DirectMessage]); dmsFromME.Kinds = new Set([EventKind.DirectMessage]);
dms.AddSubscription(dmsFromME); dms.AddSubscription(dmsFromME);
@ -102,28 +103,28 @@ export default function useLoginFeed() {
const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true }); const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true });
useEffect(() => { useEffect(() => {
let contactList = metadataFeed.store.notes.filter( const contactList = metadataFeed.store.notes.filter(
(a) => a.kind === EventKind.ContactList (a) => a.kind === EventKind.ContactList
); );
let metadata = metadataFeed.store.notes.filter( const metadata = metadataFeed.store.notes.filter(
(a) => a.kind === EventKind.SetMetadata (a) => a.kind === EventKind.SetMetadata
); );
let profiles = metadata const profiles = metadata
.map((a) => mapEventToProfile(a)) .map((a) => mapEventToProfile(a))
.filter((a) => a !== undefined) .filter((a) => a !== undefined)
.map((a) => a!); .map((a) => unwrap(a));
for (let cl of contactList) { for (const cl of contactList) {
if (cl.content !== "" && cl.content !== "{}") { if (cl.content !== "" && cl.content !== "{}") {
let relays = JSON.parse(cl.content); const relays = JSON.parse(cl.content);
dispatch(setRelays({ relays, createdAt: cl.created_at })); dispatch(setRelays({ relays, createdAt: cl.created_at }));
} }
let pTags = cl.tags.filter((a) => a[0] === "p").map((a) => a[1]); const pTags = cl.tags.filter((a) => a[0] === "p").map((a) => a[1]);
dispatch(setFollows({ keys: pTags, createdAt: cl.created_at })); dispatch(setFollows({ keys: pTags, createdAt: cl.created_at }));
} }
(async () => { (async () => {
let maxProfile = profiles.reduce( const maxProfile = profiles.reduce(
(acc, v) => { (acc, v) => {
if (v.created > acc.created) { if (v.created > acc.created) {
acc.profile = v; acc.profile = v;
@ -134,7 +135,7 @@ export default function useLoginFeed() {
{ created: 0, profile: null as MetadataCache | null } { created: 0, profile: null as MetadataCache | null }
); );
if (maxProfile.profile) { if (maxProfile.profile) {
let existing = await db.find(maxProfile.profile.pubkey); const existing = await db.find(maxProfile.profile.pubkey);
if ((existing?.created ?? 0) < maxProfile.created) { if ((existing?.created ?? 0) < maxProfile.created) {
await db.put(maxProfile.profile); await db.put(maxProfile.profile);
} }
@ -153,7 +154,7 @@ export default function useLoginFeed() {
dispatch(setLatestNotifications(nx.created_at)); dispatch(setLatestNotifications(nx.created_at));
makeNotification(db, nx).then((notification) => { makeNotification(db, nx).then((notification) => {
if (notification) { if (notification) {
// @ts-ignore // @ts-expect-error This is typed wrong, but I don't have the time to fix it right now
dispatch(sendNotification(notification)); dispatch(sendNotification(notification));
} }
}); });
@ -176,8 +177,8 @@ export default function useLoginFeed() {
try { try {
const blocked = JSON.parse(plaintext); const blocked = JSON.parse(plaintext);
const keys = blocked const keys = blocked
.filter((p: any) => p && p.length === 2 && p[0] === "p") .filter((p: string) => p && p.length === 2 && p[0] === "p")
.map((p: any) => p[1]); .map((p: string) => p[1]);
dispatch( dispatch(
setBlocked({ setBlocked({
keys, keys,
@ -193,7 +194,7 @@ export default function useLoginFeed() {
}, [dispatch, mutedFeed.store]); }, [dispatch, mutedFeed.store]);
useEffect(() => { useEffect(() => {
let dms = dmsFeed.store.notes.filter( const dms = dmsFeed.store.notes.filter(
(a) => a.kind === EventKind.DirectMessage (a) => a.kind === EventKind.DirectMessage
); );
dispatch(addDirectMessage(dms)); dispatch(addDirectMessage(dms));
@ -209,7 +210,7 @@ async function decryptBlocked(
if (pubKey && privKey) { if (pubKey && privKey) {
return await ev.DecryptData(raw.content, privKey, pubKey); return await ev.DecryptData(raw.content, privKey, pubKey);
} else { } else {
return await barierNip07(() => return await barrierNip07(() =>
window.nostr.nip04.decrypt(pubKey, raw.content) window.nostr.nip04.decrypt(pubKey, raw.content)
); );
} }

View File

@ -7,7 +7,7 @@ import useSubscription, { NoteStore } from "Feed/Subscription";
export default function useMutedFeed(pubkey: HexKey) { export default function useMutedFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = `muted:${pubkey.slice(0, 12)}`; sub.Id = `muted:${pubkey.slice(0, 12)}`;
sub.Kinds = new Set([EventKind.Lists]); sub.Kinds = new Set([EventKind.Lists]);
sub.Authors = new Set([pubkey]); sub.Authors = new Set([pubkey]);
@ -44,7 +44,7 @@ export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
} }
export function getMuted(feed: NoteStore, pubkey: HexKey): HexKey[] { export function getMuted(feed: NoteStore, pubkey: HexKey): HexKey[] {
let lists = feed?.notes.filter( const lists = feed?.notes.filter(
(a) => a.kind === EventKind.Lists && a.pubkey === pubkey (a) => a.kind === EventKind.Lists && a.pubkey === pubkey
); );
return getMutedKeys(lists).keys; return getMutedKeys(lists).keys;

View File

@ -1,16 +1,16 @@
import { useSyncExternalStore } from "react"; import { useSyncExternalStore } from "react";
import { System } from "Nostr/System"; import { System } from "Nostr/System";
import { CustomHook, StateSnapshot } from "Nostr/Connection"; import { StateSnapshot } from "Nostr/Connection";
const noop = (f: CustomHook) => { const noop = () => {
return () => {}; return () => undefined;
}; };
const noopState = (): StateSnapshot | undefined => { const noopState = (): StateSnapshot | undefined => {
return undefined; return undefined;
}; };
export default function useRelayState(addr: string) { export default function useRelayState(addr: string) {
let c = System.Sockets.get(addr); const c = System.Sockets.get(addr);
return useSyncExternalStore<StateSnapshot | undefined>( return useSyncExternalStore<StateSnapshot | undefined>(
c?.StatusHook.bind(c) ?? noop, c?.StatusHook.bind(c) ?? noop,
c?.GetState.bind(c) ?? noopState c?.GetState.bind(c) ?? noopState

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useReducer, useState } from "react";
import { System } from "Nostr/System"; import { System } from "Nostr/System";
import { TaggedRawEvent } from "Nostr"; import { TaggedRawEvent } from "Nostr";
import { Subscriptions } from "Nostr/Subscriptions"; import { Subscriptions } from "Nostr/Subscriptions";
import { debounce } from "Util"; import { debounce, unwrap } from "Util";
import { db } from "Db"; import { db } from "Db";
export type NoteStore = { export type NoteStore = {
@ -17,7 +17,7 @@ export type UseSubscriptionOptions = {
interface ReducerArg { interface ReducerArg {
type: "END" | "EVENT" | "CLEAR"; type: "END" | "EVENT" | "CLEAR";
ev?: TaggedRawEvent | Array<TaggedRawEvent>; ev?: TaggedRawEvent | TaggedRawEvent[];
end?: boolean; end?: boolean;
} }
@ -25,7 +25,7 @@ function notesReducer(state: NoteStore, arg: ReducerArg) {
if (arg.type === "END") { if (arg.type === "END") {
return { return {
notes: state.notes, notes: state.notes,
end: arg.end!, end: arg.end ?? false,
} as NoteStore; } as NoteStore;
} }
@ -36,11 +36,11 @@ function notesReducer(state: NoteStore, arg: ReducerArg) {
} as NoteStore; } as NoteStore;
} }
let evs = arg.ev!; let evs = arg.ev;
if (!Array.isArray(evs)) { if (!(evs instanceof Array)) {
evs = [evs]; evs = evs === undefined ? [] : [evs];
} }
let existingIds = new Set(state.notes.map((a) => a.id)); const existingIds = new Set(state.notes.map((a) => a.id));
evs = evs.filter((a) => !existingIds.has(a.id)); evs = evs.filter((a) => !existingIds.has(a.id));
if (evs.length === 0) { if (evs.length === 0) {
return state; return state;
@ -175,7 +175,7 @@ const PreloadNotes = async (id: string): Promise<TaggedRawEvent[]> => {
const feed = await db.feeds.get(id); const feed = await db.feeds.get(id);
if (feed) { if (feed) {
const events = await db.events.bulkGet(feed.ids); const events = await db.events.bulkGet(feed.ids);
return events.filter((a) => a !== undefined).map((a) => a!); return events.filter((a) => a !== undefined).map((a) => unwrap(a));
} }
return []; return [];
}; };

View File

@ -16,9 +16,9 @@ export default function useThreadFeed(id: u256) {
function addId(id: u256[]) { function addId(id: u256[]) {
setTrackingEvent((s) => { setTrackingEvent((s) => {
let orig = new Set(s); const orig = new Set(s);
if (id.some((a) => !orig.has(a))) { if (id.some((a) => !orig.has(a))) {
let tmp = new Set([...s, ...id]); const tmp = new Set([...s, ...id]);
return Array.from(tmp); return Array.from(tmp);
} else { } else {
return s; return s;
@ -55,16 +55,16 @@ export default function useThreadFeed(id: u256) {
useEffect(() => { useEffect(() => {
if (main.store) { if (main.store) {
return debounce(200, () => { return debounce(200, () => {
let mainNotes = main.store.notes.filter( const mainNotes = main.store.notes.filter(
(a) => a.kind === EventKind.TextNote (a) => a.kind === EventKind.TextNote
); );
let eTags = mainNotes const eTags = mainNotes
.filter((a) => a.kind === EventKind.TextNote) .filter((a) => a.kind === EventKind.TextNote)
.map((a) => a.tags.filter((b) => b[0] === "e").map((b) => b[1])) .map((a) => a.tags.filter((b) => b[0] === "e").map((b) => b[1]))
.flat(); .flat();
let ids = mainNotes.map((a) => a.id); const ids = mainNotes.map((a) => a.id);
let allEvents = new Set([...eTags, ...ids]); const allEvents = new Set([...eTags, ...ids]);
addId(Array.from(allEvents)); addId(Array.from(allEvents));
}); });
} }

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { u256 } from "Nostr"; import { u256 } from "Nostr";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import { Subscriptions } from "Nostr/Subscriptions"; import { Subscriptions } from "Nostr/Subscriptions";
import { unixNow } from "Util"; import { unixNow, unwrap } from "Util";
import useSubscription from "Feed/Subscription"; import useSubscription from "Feed/Subscription";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
@ -38,7 +38,7 @@ export default function useTimelineFeed(
return null; return null;
} }
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = `timeline:${subject.type}:${subject.discriminator}`; sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]); sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
switch (subject.type) { switch (subject.type) {
@ -64,7 +64,7 @@ export default function useTimelineFeed(
}, [subject.type, subject.items, subject.discriminator]); }, [subject.type, subject.items, subject.discriminator]);
const sub = useMemo(() => { const sub = useMemo(() => {
let sub = createSub(); const sub = createSub();
if (sub) { if (sub) {
if (options.method === "LIMIT_UNTIL") { if (options.method === "LIMIT_UNTIL") {
sub.Until = until; sub.Until = until;
@ -80,7 +80,7 @@ export default function useTimelineFeed(
if (pref.autoShowLatest) { if (pref.autoShowLatest) {
// copy properties of main sub but with limit 0 // copy properties of main sub but with limit 0
// this will put latest directly into main feed // this will put latest directly into main feed
let latestSub = new Subscriptions(); const latestSub = new Subscriptions();
latestSub.Authors = sub.Authors; latestSub.Authors = sub.Authors;
latestSub.HashTags = sub.HashTags; latestSub.HashTags = sub.HashTags;
latestSub.PTags = sub.PTags; latestSub.PTags = sub.PTags;
@ -97,7 +97,7 @@ export default function useTimelineFeed(
const main = useSubscription(sub, { leaveOpen: true, cache: true }); const main = useSubscription(sub, { leaveOpen: true, cache: true });
const subRealtime = useMemo(() => { const subRealtime = useMemo(() => {
let subLatest = createSub(); const subLatest = createSub();
if (subLatest && !pref.autoShowLatest) { if (subLatest && !pref.autoShowLatest) {
subLatest.Id = `${subLatest.Id}:latest`; subLatest.Id = `${subLatest.Id}:latest`;
subLatest.Limit = 1; subLatest.Limit = 1;
@ -131,7 +131,7 @@ export default function useTimelineFeed(
const subParents = useMemo(() => { const subParents = useMemo(() => {
if (trackingParentEvents.length > 0) { if (trackingParentEvents.length > 0) {
let parents = new Subscriptions(); const parents = new Subscriptions();
parents.Id = `timeline-parent:${subject.type}`; parents.Id = `timeline-parent:${subject.type}`;
parents.Ids = new Set(trackingParentEvents); parents.Ids = new Set(trackingParentEvents);
return parents; return parents;
@ -144,21 +144,21 @@ export default function useTimelineFeed(
useEffect(() => { useEffect(() => {
if (main.store.notes.length > 0) { if (main.store.notes.length > 0) {
setTrackingEvent((s) => { setTrackingEvent((s) => {
let ids = main.store.notes.map((a) => a.id); const ids = main.store.notes.map((a) => a.id);
if (ids.some((a) => !s.includes(a))) { if (ids.some((a) => !s.includes(a))) {
return Array.from(new Set([...s, ...ids])); return Array.from(new Set([...s, ...ids]));
} }
return s; return s;
}); });
let reposts = main.store.notes const reposts = main.store.notes
.filter((a) => a.kind === EventKind.Repost && a.content === "") .filter((a) => a.kind === EventKind.Repost && a.content === "")
.map((a) => a.tags.find((b) => b[0] === "e")) .map((a) => a.tags.find((b) => b[0] === "e"))
.filter((a) => a) .filter((a) => a)
.map((a) => a![1]); .map((a) => unwrap(a)[1]);
if (reposts.length > 0) { if (reposts.length > 0) {
setTrackingParentEvents((s) => { setTrackingParentEvents((s) => {
if (reposts.some((a) => !s.includes(a))) { if (reposts.some((a) => !s.includes(a))) {
let temp = new Set([...s, ...reposts]); const temp = new Set([...s, ...reposts]);
return Array.from(temp); return Array.from(temp);
} }
return s; return s;
@ -175,7 +175,7 @@ export default function useTimelineFeed(
loadMore: () => { loadMore: () => {
console.debug("Timeline load more!"); console.debug("Timeline load more!");
if (options.method === "LIMIT_UNTIL") { if (options.method === "LIMIT_UNTIL") {
let oldest = main.store.notes.reduce( const oldest = main.store.notes.reduce(
(acc, v) => (acc = v.created_at < acc ? v.created_at : acc), (acc, v) => (acc = v.created_at < acc ? v.created_at : acc),
unixNow() unixNow()
); );

View File

@ -6,7 +6,7 @@ import useSubscription from "./Subscription";
export default function useZapsFeed(pubkey: HexKey) { export default function useZapsFeed(pubkey: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
let x = new Subscriptions(); const x = new Subscriptions();
x.Id = `zaps:${pubkey.slice(0, 12)}`; x.Id = `zaps:${pubkey.slice(0, 12)}`;
x.Kinds = new Set([EventKind.ZapReceipt]); x.Kinds = new Set([EventKind.ZapReceipt]);
x.PTags = new Set([pubkey]); x.PTags = new Set([pubkey]);

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, WheelEvent, LegacyRef } from "react"; import { useEffect, useRef, LegacyRef } from "react";
function useHorizontalScroll() { function useHorizontalScroll() {
const elRef = useRef<HTMLDivElement>(); const elRef = useRef<HTMLDivElement>();
@ -10,9 +10,7 @@ function useHorizontalScroll() {
ev.preventDefault(); ev.preventDefault();
el.scrollTo({ left: el.scrollLeft + ev.deltaY, behavior: "smooth" }); el.scrollTo({ left: el.scrollLeft + ev.deltaY, behavior: "smooth" });
}; };
// @ts-ignore
el.addEventListener("wheel", onWheel); el.addEventListener("wheel", onWheel);
// @ts-ignore
return () => el.removeEventListener("wheel", onWheel); return () => el.removeEventListener("wheel", onWheel);
} }
}, []); }, []);

View File

@ -5,7 +5,7 @@ declare global {
webln?: { webln?: {
enabled: boolean; enabled: boolean;
enable: () => Promise<void>; enable: () => Promise<void>;
sendPayment: (pr: string) => Promise<any>; sendPayment: (pr: string) => Promise<unknown>;
}; };
} }
} }
@ -15,7 +15,7 @@ export default function useWebln(enable = true) {
useEffect(() => { useEffect(() => {
if (maybeWebLn && !maybeWebLn.enabled && enable) { if (maybeWebLn && !maybeWebLn.enabled && enable) {
maybeWebLn.enable().catch((error) => { maybeWebLn.enable().catch(() => {
console.debug("Couldn't enable WebLN"); console.debug("Couldn't enable WebLN");
}); });
} }

View File

@ -1,6 +1,4 @@
import IconProps from "./IconProps"; const Attachment = () => {
const Attachment = (props: IconProps) => {
return ( return (
<svg <svg
width="21" width="21"

View File

@ -1,6 +1,4 @@
import IconProps from "./IconProps"; const Logout = () => {
const Logout = (props: IconProps) => {
return ( return (
<svg <svg
width="22" width="22"

View File

@ -1,5 +1,3 @@
import IconProps from "./IconProps";
const Reply = () => { const Reply = () => {
return ( return (
<svg <svg

View File

@ -107,11 +107,11 @@ export class ServiceProvider {
async _GetJson<T>( async _GetJson<T>(
path: string, path: string,
method?: "GET" | string, method?: "GET" | string,
body?: any, body?: { [key: string]: string },
headers?: any headers?: { [key: string]: string }
): Promise<T | ServiceError> { ): Promise<T | ServiceError> {
try { try {
let rsp = await fetch(`${this.url}${path}`, { const rsp = await fetch(`${this.url}${path}`, {
method: method, method: method,
body: body ? JSON.stringify(body) : undefined, body: body ? JSON.stringify(body) : undefined,
headers: { headers: {
@ -121,7 +121,7 @@ export class ServiceProvider {
}, },
}); });
let obj = await rsp.json(); const obj = await rsp.json();
if ("error" in obj) { if ("error" in obj) {
return <ServiceError>obj; return <ServiceError>obj;
} }

View File

@ -9,6 +9,7 @@ import { RawEvent, TaggedRawEvent, u256 } from "Nostr";
import { RelayInfo } from "./RelayInfo"; import { RelayInfo } from "./RelayInfo";
import Nips from "./Nips"; import Nips from "./Nips";
import { System } from "./System"; import { System } from "./System";
import { unwrap } from "Util";
export type CustomHook = (state: Readonly<StateSnapshot>) => void; export type CustomHook = (state: Readonly<StateSnapshot>) => void;
@ -51,7 +52,7 @@ export default class Connection {
LastState: Readonly<StateSnapshot>; LastState: Readonly<StateSnapshot>;
IsClosed: boolean; IsClosed: boolean;
ReconnectTimer: ReturnType<typeof setTimeout> | null; ReconnectTimer: ReturnType<typeof setTimeout> | null;
EventsCallback: Map<u256, (msg?: any) => void>; EventsCallback: Map<u256, (msg: boolean[]) => void>;
AwaitingAuth: Map<string, boolean>; AwaitingAuth: Map<string, boolean>;
Authed: boolean; Authed: boolean;
@ -87,15 +88,15 @@ export default class Connection {
async Connect() { async Connect() {
try { try {
if (this.Info === undefined) { if (this.Info === undefined) {
let u = new URL(this.Address); const u = new URL(this.Address);
let rsp = await fetch(`https://${u.host}`, { const rsp = await fetch(`https://${u.host}`, {
headers: { headers: {
accept: "application/nostr+json", accept: "application/nostr+json",
}, },
}); });
if (rsp.ok) { if (rsp.ok) {
let data = await rsp.json(); const data = await rsp.json();
for (let [k, v] of Object.entries(data)) { for (const [k, v] of Object.entries(data)) {
if (v === "unset" || v === "") { if (v === "unset" || v === "") {
data[k] = undefined; data[k] = undefined;
} }
@ -114,7 +115,7 @@ export default class Connection {
this.IsClosed = false; this.IsClosed = false;
this.Socket = new WebSocket(this.Address); this.Socket = new WebSocket(this.Address);
this.Socket.onopen = (e) => this.OnOpen(e); this.Socket.onopen = () => this.OnOpen();
this.Socket.onmessage = (e) => this.OnMessage(e); this.Socket.onmessage = (e) => this.OnMessage(e);
this.Socket.onerror = (e) => this.OnError(e); this.Socket.onerror = (e) => this.OnError(e);
this.Socket.onclose = (e) => this.OnClose(e); this.Socket.onclose = (e) => this.OnClose(e);
@ -130,7 +131,7 @@ export default class Connection {
this._UpdateState(); this._UpdateState();
} }
OnOpen(e: Event) { OnOpen() {
this.ConnectTimeout = DefaultConnectTimeout; this.ConnectTimeout = DefaultConnectTimeout;
this._InitSubscriptions(); this._InitSubscriptions();
console.log(`[${this.Address}] Open!`); console.log(`[${this.Address}] Open!`);
@ -157,10 +158,10 @@ export default class Connection {
this._UpdateState(); this._UpdateState();
} }
OnMessage(e: MessageEvent<any>) { OnMessage(e: MessageEvent) {
if (e.data.length > 0) { if (e.data.length > 0) {
let msg = JSON.parse(e.data); const msg = JSON.parse(e.data);
let tag = msg[0]; const tag = msg[0];
switch (tag) { switch (tag) {
case "AUTH": { case "AUTH": {
this._OnAuthAsync(msg[1]); this._OnAuthAsync(msg[1]);
@ -183,7 +184,7 @@ export default class Connection {
console.debug("OK: ", msg); console.debug("OK: ", msg);
const id = msg[1]; const id = msg[1];
if (this.EventsCallback.has(id)) { if (this.EventsCallback.has(id)) {
let cb = this.EventsCallback.get(id)!; const cb = unwrap(this.EventsCallback.get(id));
this.EventsCallback.delete(id); this.EventsCallback.delete(id);
cb(msg); cb(msg);
} }
@ -213,7 +214,7 @@ export default class Connection {
if (!this.Settings.write) { if (!this.Settings.write) {
return; return;
} }
let req = ["EVENT", e.ToObject()]; const req = ["EVENT", e.ToObject()];
this._SendJson(req); this._SendJson(req);
this.Stats.EventsSent++; this.Stats.EventsSent++;
this._UpdateState(); this._UpdateState();
@ -222,13 +223,13 @@ export default class Connection {
/** /**
* Send event on this connection and wait for OK response * Send event on this connection and wait for OK response
*/ */
async SendAsync(e: NEvent, timeout: number = 5000) { async SendAsync(e: NEvent, timeout = 5000) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve) => {
if (!this.Settings.write) { if (!this.Settings.write) {
resolve(); resolve();
return; return;
} }
let t = setTimeout(() => { const t = setTimeout(() => {
resolve(); resolve();
}, timeout); }, timeout);
this.EventsCallback.set(e.Id, () => { this.EventsCallback.set(e.Id, () => {
@ -236,7 +237,7 @@ export default class Connection {
resolve(); resolve();
}); });
let req = ["EVENT", e.ToObject()]; const req = ["EVENT", e.ToObject()];
this._SendJson(req); this._SendJson(req);
this.Stats.EventsSent++; this.Stats.EventsSent++;
this._UpdateState(); this._UpdateState();
@ -269,7 +270,7 @@ export default class Connection {
*/ */
RemoveSubscription(subId: string) { RemoveSubscription(subId: string) {
if (this.Subscriptions.has(subId)) { if (this.Subscriptions.has(subId)) {
let req = ["CLOSE", subId]; const req = ["CLOSE", subId];
this._SendJson(req); this._SendJson(req);
this.Subscriptions.delete(subId); this.Subscriptions.delete(subId);
return true; return true;
@ -281,7 +282,7 @@ export default class Connection {
* Hook status for connection * Hook status for connection
*/ */
StatusHook(fnHook: CustomHook) { StatusHook(fnHook: CustomHook) {
let id = uuid(); const id = uuid();
this.StateHooks.set(id, fnHook); this.StateHooks.set(id, fnHook);
return () => { return () => {
this.StateHooks.delete(id); this.StateHooks.delete(id);
@ -324,20 +325,20 @@ export default class Connection {
} }
_NotifyState() { _NotifyState() {
let state = this.GetState(); const state = this.GetState();
for (let [_, h] of this.StateHooks) { for (const [, h] of this.StateHooks) {
h(state); h(state);
} }
} }
_InitSubscriptions() { _InitSubscriptions() {
// send pending // send pending
for (let p of this.Pending) { for (const p of this.Pending) {
this._SendJson(p); this._SendJson(p);
} }
this.Pending = []; this.Pending = [];
for (let [_, s] of this.Subscriptions) { for (const [, s] of this.Subscriptions) {
this._SendSubscription(s); this._SendSubscription(s);
} }
this._UpdateState(); this._UpdateState();
@ -357,19 +358,20 @@ export default class Connection {
this._SendJson(req); this._SendJson(req);
} }
_SendJson(obj: any) { _SendJson(obj: Subscriptions | object) {
if (this.Socket?.readyState !== WebSocket.OPEN) { if (this.Socket?.readyState !== WebSocket.OPEN) {
// @ts-expect-error TODO @v0l please figure this out... what the hell is going on
this.Pending.push(obj); this.Pending.push(obj);
return; return;
} }
let json = JSON.stringify(obj); const json = JSON.stringify(obj);
this.Socket.send(json); this.Socket.send(json);
} }
_OnEvent(subId: string, ev: RawEvent) { _OnEvent(subId: string, ev: RawEvent) {
if (this.Subscriptions.has(subId)) { if (this.Subscriptions.has(subId)) {
//this._VerifySig(ev); //this._VerifySig(ev);
let tagged: TaggedRawEvent = { const tagged: TaggedRawEvent = {
...ev, ...ev,
relays: [this.Address], relays: [this.Address],
}; };
@ -386,18 +388,18 @@ export default class Connection {
}; };
this.AwaitingAuth.set(challenge, true); this.AwaitingAuth.set(challenge, true);
const authEvent = await System.nip42Auth(challenge, this.Address); const authEvent = await System.nip42Auth(challenge, this.Address);
return new Promise((resolve, _) => { return new Promise((resolve) => {
if (!authEvent) { if (!authEvent) {
authCleanup(); authCleanup();
return Promise.reject("no event"); return Promise.reject("no event");
} }
let t = setTimeout(() => { const t = setTimeout(() => {
authCleanup(); authCleanup();
resolve(); resolve();
}, 10_000); }, 10_000);
this.EventsCallback.set(authEvent.Id, (msg: any[]) => { this.EventsCallback.set(authEvent.Id, (msg: boolean[]) => {
clearTimeout(t); clearTimeout(t);
authCleanup(); authCleanup();
if (msg.length > 3 && msg[2] === true) { if (msg.length > 3 && msg[2] === true) {
@ -407,7 +409,7 @@ export default class Connection {
resolve(); resolve();
}); });
let req = ["AUTH", authEvent.ToObject()]; const req = ["AUTH", authEvent.ToObject()];
this._SendJson(req); this._SendJson(req);
this.Stats.EventsSent++; this.Stats.EventsSent++;
this._UpdateState(); this._UpdateState();
@ -415,13 +417,13 @@ export default class Connection {
} }
_OnEnd(subId: string) { _OnEnd(subId: string) {
let sub = this.Subscriptions.get(subId); const sub = this.Subscriptions.get(subId);
if (sub) { if (sub) {
let now = new Date().getTime(); const now = new Date().getTime();
let started = sub.Started.get(this.Address); const started = sub.Started.get(this.Address);
sub.Finished.set(this.Address, now); sub.Finished.set(this.Address, now);
if (started) { if (started) {
let responseTime = now - started; const responseTime = now - started;
if (responseTime > 10_000) { if (responseTime > 10_000) {
console.warn( console.warn(
`[${this.Address}][${subId}] Slow response time ${( `[${this.Address}][${subId}] Slow response time ${(
@ -441,14 +443,14 @@ export default class Connection {
} }
_VerifySig(ev: RawEvent) { _VerifySig(ev: RawEvent) {
let payload = [0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content]; const payload = [0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content];
let payloadData = new TextEncoder().encode(JSON.stringify(payload)); const payloadData = new TextEncoder().encode(JSON.stringify(payload));
if (secp.utils.sha256Sync === undefined) { if (secp.utils.sha256Sync === undefined) {
throw "Cannot verify event, no sync sha256 method"; throw "Cannot verify event, no sync sha256 method";
} }
let data = secp.utils.sha256Sync(payloadData); const data = secp.utils.sha256Sync(payloadData);
let hash = secp.utils.bytesToHex(data); const hash = secp.utils.bytesToHex(data);
if (!secp.schnorr.verifySync(ev.sig, hash, ev.pubkey)) { if (!secp.schnorr.verifySync(ev.sig, hash, ev.pubkey)) {
throw "Sig verify failed"; throw "Sig verify failed";
} }

View File

@ -67,7 +67,7 @@ export default class Event {
* Get the pub key of the creator of this event NIP-26 * Get the pub key of the creator of this event NIP-26
*/ */
get RootPubKey() { get RootPubKey() {
let delegation = this.Tags.find((a) => a.Key === "delegation"); const delegation = this.Tags.find((a) => a.Key === "delegation");
if (delegation?.PubKey) { if (delegation?.PubKey) {
return delegation.PubKey; return delegation.PubKey;
} }
@ -80,7 +80,7 @@ export default class Event {
async Sign(key: HexKey) { async Sign(key: HexKey) {
this.Id = await this.CreateId(); this.Id = await this.CreateId();
let sig = await secp.schnorr.sign(this.Id, key); const sig = await secp.schnorr.sign(this.Id, key);
this.Signature = secp.utils.bytesToHex(sig); this.Signature = secp.utils.bytesToHex(sig);
if (!(await this.Verify())) { if (!(await this.Verify())) {
throw "Signing failed"; throw "Signing failed";
@ -92,13 +92,13 @@ export default class Event {
* @returns True if valid signature * @returns True if valid signature
*/ */
async Verify() { async Verify() {
let id = await this.CreateId(); const id = await this.CreateId();
let result = await secp.schnorr.verify(this.Signature, id, this.PubKey); const result = await secp.schnorr.verify(this.Signature, id, this.PubKey);
return result; return result;
} }
async CreateId() { async CreateId() {
let payload = [ const payload = [
0, 0,
this.PubKey, this.PubKey,
this.CreatedAt, this.CreatedAt,
@ -107,9 +107,9 @@ export default class Event {
this.Content, this.Content,
]; ];
let payloadData = new TextEncoder().encode(JSON.stringify(payload)); const payloadData = new TextEncoder().encode(JSON.stringify(payload));
let data = await secp.utils.sha256(payloadData); const data = await secp.utils.sha256(payloadData);
let hash = secp.utils.bytesToHex(data); const hash = secp.utils.bytesToHex(data);
if (this.Id !== "" && hash !== this.Id) { if (this.Id !== "" && hash !== this.Id) {
console.debug(payload); console.debug(payload);
throw "ID doesnt match!"; throw "ID doesnt match!";
@ -135,7 +135,7 @@ export default class Event {
* Create a new event for a specific pubkey * Create a new event for a specific pubkey
*/ */
static ForPubKey(pubKey: HexKey) { static ForPubKey(pubKey: HexKey) {
let ev = new Event(); const ev = new Event();
ev.PubKey = pubKey; ev.PubKey = pubKey;
return ev; return ev;
} }
@ -144,10 +144,10 @@ export default class Event {
* Encrypt the given message content * Encrypt the given message content
*/ */
async EncryptData(content: string, pubkey: HexKey, privkey: HexKey) { async EncryptData(content: string, pubkey: HexKey, privkey: HexKey) {
let key = await this._GetDmSharedKey(pubkey, privkey); const key = await this._GetDmSharedKey(pubkey, privkey);
let iv = window.crypto.getRandomValues(new Uint8Array(16)); const iv = window.crypto.getRandomValues(new Uint8Array(16));
let data = new TextEncoder().encode(content); const data = new TextEncoder().encode(content);
let result = await window.crypto.subtle.encrypt( const result = await window.crypto.subtle.encrypt(
{ {
name: "AES-CBC", name: "AES-CBC",
iv: iv, iv: iv,
@ -155,7 +155,7 @@ export default class Event {
key, key,
data data
); );
let uData = new Uint8Array(result); const uData = new Uint8Array(result);
return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode( return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(
iv, iv,
0, 0,
@ -174,15 +174,15 @@ export default class Event {
* Decrypt the content of the message * Decrypt the content of the message
*/ */
async DecryptData(cyphertext: string, privkey: HexKey, pubkey: HexKey) { async DecryptData(cyphertext: string, privkey: HexKey, pubkey: HexKey) {
let key = await this._GetDmSharedKey(pubkey, privkey); const key = await this._GetDmSharedKey(pubkey, privkey);
let cSplit = cyphertext.split("?iv="); const cSplit = cyphertext.split("?iv=");
let data = new Uint8Array(base64.length(cSplit[0])); const data = new Uint8Array(base64.length(cSplit[0]));
base64.decode(cSplit[0], data, 0); base64.decode(cSplit[0], data, 0);
let iv = new Uint8Array(base64.length(cSplit[1])); const iv = new Uint8Array(base64.length(cSplit[1]));
base64.decode(cSplit[1], iv, 0); base64.decode(cSplit[1], iv, 0);
let result = await window.crypto.subtle.decrypt( const result = await window.crypto.subtle.decrypt(
{ {
name: "AES-CBC", name: "AES-CBC",
iv: iv, iv: iv,
@ -201,8 +201,8 @@ export default class Event {
} }
async _GetDmSharedKey(pubkey: HexKey, privkey: HexKey) { async _GetDmSharedKey(pubkey: HexKey, privkey: HexKey) {
let sharedPoint = secp.getSharedSecret(privkey, "02" + pubkey); const sharedPoint = secp.getSharedSecret(privkey, "02" + pubkey);
let sharedX = sharedPoint.slice(1, 33); const sharedX = sharedPoint.slice(1, 33);
return await window.crypto.subtle.importKey( return await window.crypto.subtle.importKey(
"raw", "raw",
sharedX, sharedX,

View File

@ -104,10 +104,10 @@ export class Subscriptions {
this.Since = sub?.since ?? undefined; this.Since = sub?.since ?? undefined;
this.Until = sub?.until ?? undefined; this.Until = sub?.until ?? undefined;
this.Limit = sub?.limit ?? undefined; this.Limit = sub?.limit ?? undefined;
this.OnEvent = (e) => { this.OnEvent = () => {
console.warn(`No event handler was set on subscription: ${this.Id}`); console.warn(`No event handler was set on subscription: ${this.Id}`);
}; };
this.OnEnd = (c) => {}; this.OnEnd = () => undefined;
this.OrSubs = []; this.OrSubs = [];
this.Started = new Map<string, number>(); this.Started = new Map<string, number>();
this.Finished = new Map<string, number>(); this.Finished = new Map<string, number>();
@ -128,7 +128,7 @@ export class Subscriptions {
} }
ToObject(): RawReqFilter { ToObject(): RawReqFilter {
let ret: RawReqFilter = {}; const ret: RawReqFilter = {};
if (this.Ids) { if (this.Ids) {
ret.ids = Array.from(this.Ids); ret.ids = Array.from(this.Ids);
} }

View File

@ -5,9 +5,10 @@ import Connection, { RelaySettings } from "Nostr/Connection";
import Event from "Nostr/Event"; import Event from "Nostr/Event";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import { Subscriptions } from "Nostr/Subscriptions"; import { Subscriptions } from "Nostr/Subscriptions";
import { unwrap } from "Util";
/** /**
* Manages nostr content retrival system * Manages nostr content retrieval system
*/ */
export class NostrSystem { export class NostrSystem {
/** /**
@ -49,14 +50,14 @@ export class NostrSystem {
ConnectToRelay(address: string, options: RelaySettings) { ConnectToRelay(address: string, options: RelaySettings) {
try { try {
if (!this.Sockets.has(address)) { if (!this.Sockets.has(address)) {
let c = new Connection(address, options); const c = new Connection(address, options);
this.Sockets.set(address, c); this.Sockets.set(address, c);
for (let [_, s] of this.Subscriptions) { for (const [, s] of this.Subscriptions) {
c.AddSubscription(s); c.AddSubscription(s);
} }
} else { } else {
// update settings if already connected // update settings if already connected
this.Sockets.get(address)!.Settings = options; unwrap(this.Sockets.get(address)).Settings = options;
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -67,7 +68,7 @@ export class NostrSystem {
* Disconnect from a relay * Disconnect from a relay
*/ */
DisconnectRelay(address: string) { DisconnectRelay(address: string) {
let c = this.Sockets.get(address); const c = this.Sockets.get(address);
if (c) { if (c) {
this.Sockets.delete(address); this.Sockets.delete(address);
c.Close(); c.Close();
@ -75,14 +76,14 @@ export class NostrSystem {
} }
AddSubscription(sub: Subscriptions) { AddSubscription(sub: Subscriptions) {
for (let [a, s] of this.Sockets) { for (const [, s] of this.Sockets) {
s.AddSubscription(sub); s.AddSubscription(sub);
} }
this.Subscriptions.set(sub.Id, sub); this.Subscriptions.set(sub.Id, sub);
} }
RemoveSubscription(subId: string) { RemoveSubscription(subId: string) {
for (let [a, s] of this.Sockets) { for (const [, s] of this.Sockets) {
s.RemoveSubscription(subId); s.RemoveSubscription(subId);
} }
this.Subscriptions.delete(subId); this.Subscriptions.delete(subId);
@ -92,7 +93,7 @@ export class NostrSystem {
* Send events to writable relays * Send events to writable relays
*/ */
BroadcastEvent(ev: Event) { BroadcastEvent(ev: Event) {
for (let [_, s] of this.Sockets) { for (const [, s] of this.Sockets) {
s.SendEvent(ev); s.SendEvent(ev);
} }
} }
@ -101,7 +102,7 @@ export class NostrSystem {
* Write an event to a relay then disconnect * Write an event to a relay then disconnect
*/ */
async WriteOnceToRelay(address: string, ev: Event) { async WriteOnceToRelay(address: string, ev: Event) {
let c = new Connection(address, { write: true, read: false }); const c = new Connection(address, { write: true, read: false });
await c.SendAsync(ev); await c.SendAsync(ev);
c.Close(); c.Close();
} }
@ -110,7 +111,7 @@ export class NostrSystem {
* Request profile metadata for a set of pubkeys * Request profile metadata for a set of pubkeys
*/ */
TrackMetadata(pk: HexKey | Array<HexKey>) { TrackMetadata(pk: HexKey | Array<HexKey>) {
for (let p of Array.isArray(pk) ? pk : [pk]) { for (const p of Array.isArray(pk) ? pk : [pk]) {
if (p.length > 0) { if (p.length > 0) {
this.WantsMetadata.add(p); this.WantsMetadata.add(p);
} }
@ -121,7 +122,7 @@ export class NostrSystem {
* Stop tracking metadata for a set of pubkeys * Stop tracking metadata for a set of pubkeys
*/ */
UntrackMetadata(pk: HexKey | Array<HexKey>) { UntrackMetadata(pk: HexKey | Array<HexKey>) {
for (let p of Array.isArray(pk) ? pk : [pk]) { for (const p of Array.isArray(pk) ? pk : [pk]) {
if (p.length > 0) { if (p.length > 0) {
this.WantsMetadata.delete(p); this.WantsMetadata.delete(p);
} }
@ -132,16 +133,16 @@ export class NostrSystem {
* Request/Response pattern * Request/Response pattern
*/ */
RequestSubscription(sub: Subscriptions) { RequestSubscription(sub: Subscriptions) {
return new Promise<TaggedRawEvent[]>((resolve, reject) => { return new Promise<TaggedRawEvent[]>((resolve) => {
let events: TaggedRawEvent[] = []; const events: TaggedRawEvent[] = [];
// force timeout returning current results // force timeout returning current results
let timeout = setTimeout(() => { const timeout = setTimeout(() => {
this.RemoveSubscription(sub.Id); this.RemoveSubscription(sub.Id);
resolve(events); resolve(events);
}, 10_000); }, 10_000);
let onEventPassthrough = sub.OnEvent; const onEventPassthrough = sub.OnEvent;
sub.OnEvent = (ev) => { sub.OnEvent = (ev) => {
if (typeof onEventPassthrough === "function") { if (typeof onEventPassthrough === "function") {
onEventPassthrough(ev); onEventPassthrough(ev);
@ -149,9 +150,9 @@ export class NostrSystem {
if (!events.some((a) => a.id === ev.id)) { if (!events.some((a) => a.id === ev.id)) {
events.push(ev); events.push(ev);
} else { } else {
let existing = events.find((a) => a.id === ev.id); const existing = events.find((a) => a.id === ev.id);
if (existing) { if (existing) {
for (let v of ev.relays) { for (const v of ev.relays) {
existing.relays.push(v); existing.relays.push(v);
} }
} }
@ -171,11 +172,11 @@ export class NostrSystem {
async _FetchMetadata() { async _FetchMetadata() {
if (this.UserDb) { if (this.UserDb) {
let missing = new Set<HexKey>(); const missing = new Set<HexKey>();
let meta = await this.UserDb.bulkGet(Array.from(this.WantsMetadata)); const meta = await this.UserDb.bulkGet(Array.from(this.WantsMetadata));
let expire = new Date().getTime() - ProfileCacheExpire; const expire = new Date().getTime() - ProfileCacheExpire;
for (let pk of this.WantsMetadata) { for (const pk of this.WantsMetadata) {
let m = meta.find((a) => a?.pubkey === pk); const m = meta.find((a) => a?.pubkey === pk);
if (!m || m.loaded < expire) { if (!m || m.loaded < expire) {
missing.add(pk); missing.add(pk);
// cap 100 missing profiles // cap 100 missing profiles
@ -188,35 +189,38 @@ export class NostrSystem {
if (missing.size > 0) { if (missing.size > 0) {
console.debug("Wants profiles: ", missing); console.debug("Wants profiles: ", missing);
let sub = new Subscriptions(); const sub = new Subscriptions();
sub.Id = `profiles:${sub.Id.slice(0, 8)}`; sub.Id = `profiles:${sub.Id.slice(0, 8)}`;
sub.Kinds = new Set([EventKind.SetMetadata]); sub.Kinds = new Set([EventKind.SetMetadata]);
sub.Authors = missing; sub.Authors = missing;
sub.OnEvent = async (e) => { sub.OnEvent = async (e) => {
let profile = mapEventToProfile(e); const profile = mapEventToProfile(e);
const userDb = unwrap(this.UserDb);
if (profile) { if (profile) {
let existing = await this.UserDb!.find(profile.pubkey); const existing = await userDb.find(profile.pubkey);
if ((existing?.created ?? 0) < profile.created) { if ((existing?.created ?? 0) < profile.created) {
await this.UserDb!.put(profile); await userDb.put(profile);
} else if (existing) { } else if (existing) {
await this.UserDb!.update(profile.pubkey, { await userDb.update(profile.pubkey, {
loaded: profile.loaded, loaded: profile.loaded,
}); });
} }
} }
}; };
let results = await this.RequestSubscription(sub); const results = await this.RequestSubscription(sub);
let couldNotFetch = Array.from(missing).filter( const couldNotFetch = Array.from(missing).filter(
(a) => !results.some((b) => b.pubkey === a) (a) => !results.some((b) => b.pubkey === a)
); );
console.debug("No profiles: ", couldNotFetch); console.debug("No profiles: ", couldNotFetch);
if (couldNotFetch.length > 0) { if (couldNotFetch.length > 0) {
let updates = couldNotFetch.map((a) => { const updates = couldNotFetch
return { .map((a) => {
pubkey: a, return {
loaded: new Date().getTime(), pubkey: a,
}; loaded: new Date().getTime(),
}).map(a => this.UserDb!.update(a.pubkey, a)); };
})
.map((a) => unwrap(this.UserDb).update(a.pubkey, a));
await Promise.all(updates); await Promise.all(updates);
} }
} }
@ -224,12 +228,8 @@ export class NostrSystem {
setTimeout(() => this._FetchMetadata(), 500); setTimeout(() => this._FetchMetadata(), 500);
} }
async nip42Auth( nip42Auth: (challenge: string, relay: string) => Promise<Event | undefined> =
challenge: string, async () => undefined;
relay: string
): Promise<Event | undefined> {
return;
}
} }
export const System = new NostrSystem(); export const System = new NostrSystem();

View File

@ -56,7 +56,7 @@ export default class Tag {
switch (this.Key) { switch (this.Key) {
case "e": { case "e": {
let ret = ["e", this.Event, this.Relay, this.Marker]; let ret = ["e", this.Event, this.Relay, this.Marker];
let trimEnd = ret.reverse().findIndex((a) => a !== undefined); const trimEnd = ret.reverse().findIndex((a) => a !== undefined);
ret = ret.reverse().slice(0, ret.length - trimEnd); ret = ret.reverse().slice(0, ret.length - trimEnd);
return <string[]>ret; return <string[]>ret;
} }
@ -64,10 +64,10 @@ export default class Tag {
return this.PubKey ? ["p", this.PubKey] : null; return this.PubKey ? ["p", this.PubKey] : null;
} }
case "t": { case "t": {
return ["t", this.Hashtag!]; return ["t", this.Hashtag ?? ""];
} }
case "d": { case "d": {
return ["d", this.DTag!]; return ["d", this.DTag ?? ""];
} }
default: { default: {
return this.Original; return this.Original;

View File

@ -19,15 +19,15 @@ export default class Thread {
* @param ev Event to extract thread from * @param ev Event to extract thread from
*/ */
static ExtractThread(ev: NEvent) { static ExtractThread(ev: NEvent) {
let isThread = ev.Tags.some((a) => a.Key === "e"); const isThread = ev.Tags.some((a) => a.Key === "e");
if (!isThread) { if (!isThread) {
return null; return null;
} }
let shouldWriteMarkers = ev.Kind === EventKind.TextNote; const shouldWriteMarkers = ev.Kind === EventKind.TextNote;
let ret = new Thread(); const ret = new Thread();
let eTags = ev.Tags.filter((a) => a.Key === "e"); const eTags = ev.Tags.filter((a) => a.Key === "e");
let marked = eTags.some((a) => a.Marker !== undefined); const marked = eTags.some((a) => a.Marker !== undefined);
if (!marked) { if (!marked) {
ret.Root = eTags[0]; ret.Root = eTags[0];
ret.Root.Marker = shouldWriteMarkers ? "root" : undefined; ret.Root.Marker = shouldWriteMarkers ? "root" : undefined;
@ -42,8 +42,8 @@ export default class Thread {
} }
} }
} else { } else {
let root = eTags.find((a) => a.Marker === "root"); const root = eTags.find((a) => a.Marker === "root");
let reply = eTags.find((a) => a.Marker === "reply"); const reply = eTags.find((a) => a.Marker === "reply");
ret.Root = root; ret.Root = root;
ret.ReplyTo = reply; ret.ReplyTo = reply;
ret.Mentions = eTags.filter((a) => a.Marker === "mention"); ret.Mentions = eTags.filter((a) => a.Marker === "mention");

View File

@ -15,13 +15,12 @@ export async function makeNotification(
case EventKind.TextNote: { case EventKind.TextNote: {
const pubkeys = new Set([ const pubkeys = new Set([
ev.pubkey, ev.pubkey,
...ev.tags.filter((a) => a[0] === "p").map((a) => a[1]!), ...ev.tags.filter((a) => a[0] === "p").map((a) => a[1]),
]); ]);
const users = await db.bulkGet(Array.from(pubkeys)); const users = await db.bulkGet(Array.from(pubkeys));
const fromUser = users.find((a) => a?.pubkey === ev.pubkey); const fromUser = users.find((a) => a?.pubkey === ev.pubkey);
const name = getDisplayName(fromUser, ev.pubkey); const name = getDisplayName(fromUser, ev.pubkey);
const avatarUrl = const avatarUrl = fromUser?.picture || Nostrich;
(fromUser?.picture?.length ?? 0) === 0 ? Nostrich : fromUser?.picture;
return { return {
title: `Reply from ${name}`, title: `Reply from ${name}`,
body: replaceTagsWithUser(ev, users).substring(0, 50), body: replaceTagsWithUser(ev, users).substring(0, 50),
@ -37,12 +36,12 @@ function replaceTagsWithUser(ev: TaggedRawEvent, users: MetadataCache[]) {
return ev.content return ev.content
.split(MentionRegex) .split(MentionRegex)
.map((match) => { .map((match) => {
let matchTag = match.match(/#\[(\d+)\]/); const matchTag = match.match(/#\[(\d+)\]/);
if (matchTag && matchTag.length === 2) { if (matchTag && matchTag.length === 2) {
let idx = parseInt(matchTag[1]); const idx = parseInt(matchTag[1]);
let ref = ev.tags[idx]; const ref = ev.tags[idx];
if (ref && ref[0] === "p" && ref.length > 1) { if (ref && ref[0] === "p" && ref.length > 1) {
let u = users.find((a) => a.pubkey === ref[1]); const u = users.find((a) => a.pubkey === ref[1]);
return `@${getDisplayName(u, ref[1])}`; return `@${getDisplayName(u, ref[1])}`;
} }
} }

View File

@ -9,7 +9,7 @@ import { bech32ToHex } from "Util";
import useEventPublisher from "Feed/EventPublisher"; import useEventPublisher from "Feed/EventPublisher";
import DM from "Element/DM"; import DM from "Element/DM";
import { RawEvent } from "Nostr"; import { RawEvent, TaggedRawEvent } from "Nostr";
import { dmsInChat, isToSelf } from "Pages/MessagesPage"; import { dmsInChat, isToSelf } from "Pages/MessagesPage";
import NoteToSelf from "Element/NoteToSelf"; import NoteToSelf from "Element/NoteToSelf";
@ -17,24 +17,32 @@ type RouterParams = {
id: string; id: string;
}; };
interface State {
login: {
dms: TaggedRawEvent[];
};
}
export default function ChatPage() { export default function ChatPage() {
const params = useParams<RouterParams>(); const params = useParams<RouterParams>();
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const id = bech32ToHex(params.id ?? ""); const id = bech32ToHex(params.id ?? "");
const pubKey = useSelector<any>((s) => s.login.publicKey); const pubKey = useSelector<{ login: { publicKey: string } }>(
const dms = useSelector<any, RawEvent[]>((s) => filterDms(s.login.dms)); (s) => s.login.publicKey
);
const dms = useSelector<State, RawEvent[]>((s) => filterDms(s.login.dms));
const [content, setContent] = useState<string>(); const [content, setContent] = useState<string>();
const { ref, inView, entry } = useInView(); const { ref, inView } = useInView();
const dmListRef = useRef<HTMLDivElement>(null); const dmListRef = useRef<HTMLDivElement>(null);
function filterDms(dms: RawEvent[]) { function filterDms(dms: TaggedRawEvent[]) {
return dmsInChat( return dmsInChat(
id === pubKey ? dms.filter((d) => isToSelf(d, pubKey)) : dms, id === pubKey ? dms.filter((d) => isToSelf(d, pubKey)) : dms,
id id
); );
} }
const sortedDms = useMemo<any[]>(() => { const sortedDms = useMemo<RawEvent[]>(() => {
return [...dms].sort((a, b) => a.created_at - b.created_at); return [...dms].sort((a, b) => a.created_at - b.created_at);
}, [dms]); }, [dms]);
@ -46,7 +54,7 @@ export default function ChatPage() {
async function sendDm() { async function sendDm() {
if (content) { if (content) {
let ev = await publisher.sendDm(content, id); const ev = await publisher.sendDm(content, id);
console.debug(ev); console.debug(ev);
publisher.broadcast(ev); publisher.broadcast(ev);
setContent(""); setContent("");
@ -54,7 +62,7 @@ export default function ChatPage() {
} }
async function onEnter(e: KeyboardEvent) { async function onEnter(e: KeyboardEvent) {
let isEnter = e.code === "Enter"; const isEnter = e.code === "Enter";
if (isEnter && !e.shiftKey) { if (isEnter && !e.shiftKey) {
await sendDm(); await sendDm();
} }
@ -67,8 +75,9 @@ export default function ChatPage() {
)) || <ProfileImage pubkey={id} className="f-grow mb10" />} )) || <ProfileImage pubkey={id} className="f-grow mb10" />}
<div className="dm-list" ref={dmListRef}> <div className="dm-list" ref={dmListRef}>
<div> <div>
{/* TODO I need to look into this again, something's being bricked with the RawEvent and TaggedRawEvent */}
{sortedDms.map((a) => ( {sortedDms.map((a) => (
<DM data={a} key={a.id} /> <DM data={a as TaggedRawEvent} key={a.id} />
))} ))}
<div ref={ref} className="mb10"></div> <div ref={ref} className="mb10"></div>
</div> </div>

View File

@ -45,11 +45,11 @@ const DonatePage = () => {
const [today, setSumToday] = useState<TotalToday>(); const [today, setSumToday] = useState<TotalToday>();
async function loadData() { async function loadData() {
let rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`); const rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`);
if (rsp.ok) { if (rsp.ok) {
setSplits(await rsp.json()); setSplits(await rsp.json());
} }
let rsp2 = await fetch(`${ApiHost}/api/v1/revenue/today`); const rsp2 = await fetch(`${ApiHost}/api/v1/revenue/today`);
if (rsp2.ok) { if (rsp2.ok) {
setSumToday(await rsp2.json()); setSumToday(await rsp2.json());
} }
@ -60,7 +60,7 @@ const DonatePage = () => {
}, []); }, []);
function actions(pk: HexKey) { function actions(pk: HexKey) {
let split = splits.find((a) => bech32ToHex(a.pubKey) === pk); const split = splits.find((a) => bech32ToHex(a.pubKey) === pk);
if (split) { if (split) {
return <>{(100 * split.split).toLocaleString()}%</>; return <>{(100 * split.split).toLocaleString()}%</>;
} }

View File

@ -5,7 +5,7 @@ import { parseId } from "Util";
export default function EventPage() { export default function EventPage() {
const params = useParams(); const params = useParams();
const id = parseId(params.id!); const id = parseId(params.id ?? "");
const thread = useThreadFeed(id); const thread = useThreadFeed(id);
return <Thread notes={thread.notes} this={id} />; return <Thread notes={thread.notes} this={id} />;

View File

@ -1,9 +1,10 @@
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import Timeline from "Element/Timeline"; import Timeline from "Element/Timeline";
import { unwrap } from "Util";
const HashTagsPage = () => { const HashTagsPage = () => {
const params = useParams(); const params = useParams();
const tag = params.tag!.toLowerCase(); const tag = unwrap(params.tag).toLowerCase();
return ( return (
<> <>

View File

@ -75,10 +75,10 @@ export default function Layout() {
useEffect(() => { useEffect(() => {
if (relays) { if (relays) {
for (let [k, v] of Object.entries(relays)) { for (const [k, v] of Object.entries(relays)) {
System.ConnectToRelay(k, v); System.ConnectToRelay(k, v);
} }
for (let [k] of System.Sockets) { for (const [k] of System.Sockets) {
if (!relays[k] && !SearchRelays.has(k)) { if (!relays[k] && !SearchRelays.has(k)) {
System.DisconnectRelay(k); System.DisconnectRelay(k);
} }
@ -96,7 +96,7 @@ export default function Layout() {
} }
useEffect(() => { useEffect(() => {
let osTheme = window.matchMedia("(prefers-color-scheme: light)"); const osTheme = window.matchMedia("(prefers-color-scheme: light)");
setTheme( setTheme(
preferences.theme === "system" && osTheme.matches preferences.theme === "system" && osTheme.matches
? "light" ? "light"
@ -139,24 +139,24 @@ export default function Layout() {
}, []); }, []);
async function handleNewUser() { async function handleNewUser() {
let newRelays: Record<string, RelaySettings> | undefined; let newRelays: Record<string, RelaySettings> = {};
try { try {
let rsp = await fetch("https://api.nostr.watch/v1/online"); const rsp = await fetch("https://api.nostr.watch/v1/online");
if (rsp.ok) { if (rsp.ok) {
let online: string[] = await rsp.json(); const online: string[] = await rsp.json();
let pickRandom = online const pickRandom = online
.sort((a, b) => (Math.random() >= 0.5 ? 1 : -1)) .sort(() => (Math.random() >= 0.5 ? 1 : -1))
.slice(0, 4); // pick 4 random relays .slice(0, 4); // pick 4 random relays
let relayObjects = pickRandom.map((a) => [ const relayObjects = pickRandom.map((a) => [
a, a,
{ read: true, write: true }, { read: true, write: true },
]); ]);
newRelays = Object.fromEntries(relayObjects); newRelays = Object.fromEntries(relayObjects);
dispatch( dispatch(
setRelays({ setRelays({
relays: newRelays!, relays: newRelays,
createdAt: 1, createdAt: 1,
}) })
); );
@ -175,13 +175,13 @@ export default function Layout() {
} }
}, [newUserKey]); }, [newUserKey]);
async function goToNotifications(e: any) { async function goToNotifications(e: React.MouseEvent) {
e.stopPropagation(); e.stopPropagation();
// request permissions to send notifications // request permissions to send notifications
if ("Notification" in window) { if ("Notification" in window) {
try { try {
if (Notification.permission !== "granted") { if (Notification.permission !== "granted") {
let res = await Notification.requestPermission(); const res = await Notification.requestPermission();
console.debug(res); console.debug(res);
} }
} catch (e) { } catch (e) {
@ -194,14 +194,14 @@ export default function Layout() {
function accountHeader() { function accountHeader() {
return ( return (
<div className="header-actions"> <div className="header-actions">
<div className="btn btn-rnd" onClick={(e) => navigate("/search")}> <div className="btn btn-rnd" onClick={() => navigate("/search")}>
<Search /> <Search />
</div> </div>
<div className="btn btn-rnd" onClick={(e) => navigate("/messages")}> <div className="btn btn-rnd" onClick={() => navigate("/messages")}>
<Envelope /> <Envelope />
{unreadDms > 0 && <span className="has-unread"></span>} {unreadDms > 0 && <span className="has-unread"></span>}
</div> </div>
<div className="btn btn-rnd" onClick={(e) => goToNotifications(e)}> <div className="btn btn-rnd" onClick={goToNotifications}>
<Bell /> <Bell />
{hasNotifications && <span className="has-unread"></span>} {hasNotifications && <span className="has-unread"></span>}
</div> </div>

View File

@ -30,15 +30,15 @@ export default function LoginPage() {
}, [publicKey, navigate]); }, [publicKey, navigate]);
async function getNip05PubKey(addr: string) { async function getNip05PubKey(addr: string) {
let [username, domain] = addr.split("@"); const [username, domain] = addr.split("@");
let rsp = await fetch( const rsp = await fetch(
`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent( `https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(
username username
)}` )}`
); );
if (rsp.ok) { if (rsp.ok) {
let data = await rsp.json(); const data = await rsp.json();
let pKey = data.names[username]; const pKey = data.names[username];
if (pKey) { if (pKey) {
return pKey; return pKey;
} }
@ -49,17 +49,17 @@ export default function LoginPage() {
async function doLogin() { async function doLogin() {
try { try {
if (key.startsWith("nsec")) { if (key.startsWith("nsec")) {
let hexKey = bech32ToHex(key); const hexKey = bech32ToHex(key);
if (secp.utils.isValidPrivateKey(hexKey)) { if (secp.utils.isValidPrivateKey(hexKey)) {
dispatch(setPrivateKey(hexKey)); dispatch(setPrivateKey(hexKey));
} else { } else {
throw new Error("INVALID PRIVATE KEY"); throw new Error("INVALID PRIVATE KEY");
} }
} else if (key.startsWith("npub")) { } else if (key.startsWith("npub")) {
let hexKey = bech32ToHex(key); const hexKey = bech32ToHex(key);
dispatch(setPublicKey(hexKey)); dispatch(setPublicKey(hexKey));
} else if (key.match(EmailRegex)) { } else if (key.match(EmailRegex)) {
let hexKey = await getNip05PubKey(key); const hexKey = await getNip05PubKey(key);
dispatch(setPublicKey(hexKey)); dispatch(setPublicKey(hexKey));
} else { } else {
if (secp.utils.isValidPrivateKey(key)) { if (secp.utils.isValidPrivateKey(key)) {
@ -75,17 +75,17 @@ export default function LoginPage() {
} }
async function makeRandomKey() { async function makeRandomKey() {
let newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey()); const newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
dispatch(setGeneratedPrivateKey(newKey)); dispatch(setGeneratedPrivateKey(newKey));
navigate("/new"); navigate("/new");
} }
async function doNip07Login() { async function doNip07Login() {
let pubKey = await window.nostr.getPublicKey(); const pubKey = await window.nostr.getPublicKey();
dispatch(setPublicKey(pubKey)); dispatch(setPublicKey(pubKey));
if ("getRelays" in window.nostr) { if ("getRelays" in window.nostr) {
let relays = await window.nostr.getRelays(); const relays = await window.nostr.getRelays();
dispatch( dispatch(
setRelays({ setRelays({
relays: { relays: {
@ -99,7 +99,7 @@ export default function LoginPage() {
} }
function altLogins() { function altLogins() {
let nip07 = "nostr" in window; const nip07 = "nostr" in window;
if (!nip07) { if (!nip07) {
return null; return null;
} }
@ -108,7 +108,7 @@ export default function LoginPage() {
<> <>
<h2>Other Login Methods</h2> <h2>Other Login Methods</h2>
<div className="flex"> <div className="flex">
<button type="button" onClick={(e) => doNip07Login()}> <button type="button" onClick={doNip07Login}>
Login with Extension (NIP-07) Login with Extension (NIP-07)
</button> </button>
</div> </div>
@ -129,7 +129,7 @@ export default function LoginPage() {
</div> </div>
{error.length > 0 ? <b className="error">{error}</b> : null} {error.length > 0 ? <b className="error">{error}</b> : null}
<div className="tabs"> <div className="tabs">
<button type="button" onClick={(e) => doLogin()}> <button type="button" onClick={doLogin}>
Login Login
</button> </button>
<button type="button" onClick={() => makeRandomKey()}> <button type="button" onClick={() => makeRandomKey()}>

View File

@ -33,7 +33,7 @@ export default function MessagesPage() {
const chats = useMemo(() => { const chats = useMemo(() => {
return extractChats( return extractChats(
dms.filter((a) => !isMuted(a.pubkey)), dms.filter((a) => !isMuted(a.pubkey)),
myPubKey! myPubKey ?? ""
); );
}, [dms, myPubKey, dmInteraction]); }, [dms, myPubKey, dmInteraction]);
@ -65,7 +65,7 @@ export default function MessagesPage() {
} }
function markAllRead() { function markAllRead() {
for (let c of chats) { for (const c of chats) {
setLastReadDm(c.pubkey); setLastReadDm(c.pubkey);
} }
dispatch(incDmInteraction()); dispatch(incDmInteraction());
@ -95,23 +95,23 @@ export default function MessagesPage() {
} }
export function lastReadDm(pk: HexKey) { export function lastReadDm(pk: HexKey) {
let k = `dm:seen:${pk}`; const k = `dm:seen:${pk}`;
return parseInt(window.localStorage.getItem(k) ?? "0"); return parseInt(window.localStorage.getItem(k) ?? "0");
} }
export function setLastReadDm(pk: HexKey) { export function setLastReadDm(pk: HexKey) {
const now = Math.floor(new Date().getTime() / 1000); const now = Math.floor(new Date().getTime() / 1000);
let current = lastReadDm(pk); const current = lastReadDm(pk);
if (current >= now) { if (current >= now) {
return; return;
} }
let k = `dm:seen:${pk}`; const k = `dm:seen:${pk}`;
window.localStorage.setItem(k, now.toString()); window.localStorage.setItem(k, now.toString());
} }
export function dmTo(e: RawEvent) { export function dmTo(e: RawEvent) {
let firstP = e.tags.find((b) => b[0] === "p"); const firstP = e.tags.find((b) => b[0] === "p");
return firstP ? firstP[1] : ""; return firstP ? firstP[1] : "";
} }
@ -132,7 +132,7 @@ export function totalUnread(dms: RawEvent[], myPubKey: HexKey) {
function unreadDms(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) { function unreadDms(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
if (pk === myPubKey) return 0; if (pk === myPubKey) return 0;
let lastRead = lastReadDm(pk); const lastRead = lastReadDm(pk);
return dmsInChat(dms, pk).filter( return dmsInChat(dms, pk).filter(
(a) => a.created_at >= lastRead && a.pubkey !== myPubKey (a) => a.created_at >= lastRead && a.pubkey !== myPubKey
).length; ).length;

View File

@ -21,8 +21,8 @@ export default function NotificationsPage() {
<Timeline <Timeline
subject={{ subject={{
type: "ptag", type: "ptag",
items: [pubkey!], items: [pubkey],
discriminator: pubkey!.slice(0, 12), discriminator: pubkey.slice(0, 12),
}} }}
postsOnly={false} postsOnly={false}
method={"TIME_RANGE"} method={"TIME_RANGE"}

View File

@ -51,7 +51,7 @@ export default function ProfilePage() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const id = useMemo(() => parseId(params.id!), [params]); const id = useMemo(() => parseId(params.id ?? ""), [params]);
const user = useUserProfile(id); const user = useUserProfile(id);
const loggedOut = useSelector<RootState, boolean | undefined>( const loggedOut = useSelector<RootState, boolean | undefined>(
(s) => s.login.loggedOut (s) => s.login.loggedOut

View File

@ -10,7 +10,7 @@ import { System } from "Nostr/System";
import messages from "./messages"; import messages from "./messages";
const SearchPage = () => { const SearchPage = () => {
const params: any = useParams(); const params = useParams();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [search, setSearch] = useState<string>(); const [search, setSearch] = useState<string>();
const [keyword, setKeyword] = useState<string | undefined>(params.keyword); const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
@ -27,15 +27,15 @@ const SearchPage = () => {
}, [search]); }, [search]);
useEffect(() => { useEffect(() => {
let addedRelays: string[] = []; const addedRelays: string[] = [];
for (let [k, v] of SearchRelays) { for (const [k, v] of SearchRelays) {
if (!System.Sockets.has(k)) { if (!System.Sockets.has(k)) {
System.ConnectToRelay(k, v); System.ConnectToRelay(k, v);
addedRelays.push(k); addedRelays.push(k);
} }
} }
return () => { return () => {
for (let r of addedRelays) { for (const r of addedRelays) {
System.DisconnectRelay(r); System.DisconnectRelay(r);
} }
}; };

View File

@ -6,16 +6,16 @@ import { useNavigate } from "react-router-dom";
export default function DiscoverFollows() { export default function DiscoverFollows() {
const navigate = useNavigate(); const navigate = useNavigate();
const sortedReccomends = useMemo(() => { const sortedRecommends = useMemo(() => {
return RecommendedFollows.sort((a) => (Math.random() >= 0.5 ? -1 : 1)); return RecommendedFollows.sort(() => (Math.random() >= 0.5 ? -1 : 1));
}, []); }, []);
return ( return (
<> <>
<h2>Follow some popular accounts</h2> <h2>Follow some popular accounts</h2>
<button onClick={() => navigate("/")}>Skip</button> <button onClick={() => navigate("/")}>Skip</button>
{sortedReccomends.length > 0 && ( {sortedRecommends.length > 0 && (
<FollowListBase pubkeys={sortedReccomends} /> <FollowListBase pubkeys={sortedRecommends} />
)} )}
<button onClick={() => navigate("/")}>Done!</button> <button onClick={() => navigate("/")}>Done!</button>
</> </>

View File

@ -20,15 +20,17 @@ export default function ImportFollows() {
const sortedTwitterFollows = useMemo(() => { const sortedTwitterFollows = useMemo(() => {
return follows return follows
.map((a) => bech32ToHex(a)) .map((a) => bech32ToHex(a))
.sort((a, b) => (currentFollows.includes(a) ? 1 : -1)); .sort((a) => (currentFollows.includes(a) ? 1 : -1));
}, [follows, currentFollows]); }, [follows, currentFollows]);
async function loadFollows() { async function loadFollows() {
setFollows([]); setFollows([]);
setError(""); setError("");
try { try {
let rsp = await fetch(`${TwitterFollowsApi}?username=${twitterUsername}`); const rsp = await fetch(
let data = await rsp.json(); `${TwitterFollowsApi}?username=${twitterUsername}`
);
const data = await rsp.json();
if (rsp.ok) { if (rsp.ok) {
if (Array.isArray(data) && data.length === 0) { if (Array.isArray(data) && data.length === 0) {
setError(`No nostr users found for "${twitterUsername}"`); setError(`No nostr users found for "${twitterUsername}"`);

View File

@ -7,6 +7,8 @@ import { DefaultImgProxy, setPreferences, UserPreferences } from "State/Login";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import messages from "./messages"; import messages from "./messages";
import { unwrap } from "Util";
import "./Preferences.css";
const PreferencesPage = () => { const PreferencesPage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -124,7 +126,7 @@ const PreferencesPage = () => {
setPreferences({ setPreferences({
...perf, ...perf,
imgProxyConfig: { imgProxyConfig: {
...perf.imgProxyConfig!, ...unwrap(perf.imgProxyConfig),
url: e.target.value, url: e.target.value,
}, },
}) })
@ -147,7 +149,7 @@ const PreferencesPage = () => {
setPreferences({ setPreferences({
...perf, ...perf,
imgProxyConfig: { imgProxyConfig: {
...perf.imgProxyConfig!, ...unwrap(perf.imgProxyConfig),
key: e.target.value, key: e.target.value,
}, },
}) })
@ -170,7 +172,7 @@ const PreferencesPage = () => {
setPreferences({ setPreferences({
...perf, ...perf,
imgProxyConfig: { imgProxyConfig: {
...perf.imgProxyConfig!, ...unwrap(perf.imgProxyConfig),
salt: e.target.value, salt: e.target.value,
}, },
}) })

View File

@ -31,7 +31,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
const privKey = useSelector<RootState, HexKey | undefined>( const privKey = useSelector<RootState, HexKey | undefined>(
(s) => s.login.privateKey (s) => s.login.privateKey
); );
const user = useUserProfile(id!); const user = useUserProfile(id ?? "");
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const uploader = useFileUpload(); const uploader = useFileUpload();
@ -61,7 +61,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
async function saveProfile() { async function saveProfile() {
// copy user object and delete internal fields // copy user object and delete internal fields
let userCopy = { const userCopy = {
...user, ...user,
name, name,
display_name: displayName, display_name: displayName,
@ -78,16 +78,16 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
delete userCopy["npub"]; delete userCopy["npub"];
console.debug(userCopy); console.debug(userCopy);
let ev = await publisher.metadata(userCopy); const ev = await publisher.metadata(userCopy);
console.debug(ev); console.debug(ev);
publisher.broadcast(ev); publisher.broadcast(ev);
} }
async function uploadFile() { async function uploadFile() {
let file = await openFile(); const file = await openFile();
if (file) { if (file) {
console.log(file); console.log(file);
let rsp = await uploader.upload(file, file.name); const rsp = await uploader.upload(file, file.name);
console.log(rsp); console.log(rsp);
if (typeof rsp?.error === "string") { if (typeof rsp?.error === "string") {
throw new Error(`Upload failed ${rsp.error}`); throw new Error(`Upload failed ${rsp.error}`);

View File

@ -5,7 +5,7 @@ import { System } from "Nostr/System";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { removeRelay } from "State/Login"; import { removeRelay } from "State/Login";
import { parseId } from "Util"; import { parseId, unwrap } from "Util";
import messages from "./messages"; import messages from "./messages";
@ -100,7 +100,7 @@ const RelayInfo = () => {
<div <div
className="btn error" className="btn error"
onClick={() => { onClick={() => {
dispatch(removeRelay(conn!.Address)); dispatch(removeRelay(unwrap(conn).Address));
navigate("/settings/relays"); navigate("/settings/relays");
}} }}
> >

View File

@ -19,7 +19,7 @@ const RelaySettingsPage = () => {
const [newRelay, setNewRelay] = useState<string>(); const [newRelay, setNewRelay] = useState<string>();
async function saveRelays() { async function saveRelays() {
let ev = await publisher.saveRelays(); const ev = await publisher.saveRelays();
publisher.broadcast(ev); publisher.broadcast(ev);
publisher.broadcastForBootstrap(ev); publisher.broadcastForBootstrap(ev);
} }
@ -48,7 +48,7 @@ const RelaySettingsPage = () => {
function addNewRelay() { function addNewRelay() {
if ((newRelay?.length ?? 0) > 0) { if ((newRelay?.length ?? 0) > 0) {
const parsed = new URL(newRelay!); const parsed = new URL(newRelay ?? "");
const payload = { const payload = {
relays: { relays: {
...relays, ...relays,

View File

@ -215,26 +215,26 @@ const LoginSlice = createSlice({
} }
// check pub key only // check pub key only
let pubKey = window.localStorage.getItem(PublicKeyItem); const pubKey = window.localStorage.getItem(PublicKeyItem);
if (pubKey && !state.privateKey) { if (pubKey && !state.privateKey) {
state.publicKey = pubKey; state.publicKey = pubKey;
state.loggedOut = false; state.loggedOut = false;
} }
let lastRelayList = window.localStorage.getItem(RelayListKey); const lastRelayList = window.localStorage.getItem(RelayListKey);
if (lastRelayList) { if (lastRelayList) {
state.relays = JSON.parse(lastRelayList); state.relays = JSON.parse(lastRelayList);
} else { } else {
state.relays = Object.fromEntries(DefaultRelays.entries()); state.relays = Object.fromEntries(DefaultRelays.entries());
} }
let lastFollows = window.localStorage.getItem(FollowList); const lastFollows = window.localStorage.getItem(FollowList);
if (lastFollows) { if (lastFollows) {
state.follows = JSON.parse(lastFollows); state.follows = JSON.parse(lastFollows);
} }
// notifications // notifications
let readNotif = parseInt( const readNotif = parseInt(
window.localStorage.getItem(NotificationsReadItem) ?? "0" window.localStorage.getItem(NotificationsReadItem) ?? "0"
); );
if (!isNaN(readNotif)) { if (!isNaN(readNotif)) {
@ -242,7 +242,7 @@ const LoginSlice = createSlice({
} }
// preferences // preferences
let pref = window.localStorage.getItem(UserPreferencesKey); const pref = window.localStorage.getItem(UserPreferencesKey);
if (pref) { if (pref) {
state.preferences = JSON.parse(pref); state.preferences = JSON.parse(pref);
} }
@ -270,15 +270,15 @@ const LoginSlice = createSlice({
state.publicKey = action.payload; state.publicKey = action.payload;
}, },
setRelays: (state, action: PayloadAction<SetRelaysPayload>) => { setRelays: (state, action: PayloadAction<SetRelaysPayload>) => {
let relays = action.payload.relays; const relays = action.payload.relays;
let createdAt = action.payload.createdAt; const createdAt = action.payload.createdAt;
if (state.latestRelays > createdAt) { if (state.latestRelays > createdAt) {
return; return;
} }
// filter out non-websocket urls // filter out non-websocket urls
let filtered = new Map<string, RelaySettings>(); const filtered = new Map<string, RelaySettings>();
for (let [k, v] of Object.entries(relays)) { for (const [k, v] of Object.entries(relays)) {
if (k.startsWith("wss://") || k.startsWith("ws://")) { if (k.startsWith("wss://") || k.startsWith("ws://")) {
filtered.set(k, v as RelaySettings); filtered.set(k, v as RelaySettings);
} }
@ -299,17 +299,17 @@ const LoginSlice = createSlice({
return; return;
} }
let existing = new Set(state.follows); const existing = new Set(state.follows);
let update = Array.isArray(keys) ? keys : [keys]; const update = Array.isArray(keys) ? keys : [keys];
let changes = false; let changes = false;
for (let pk of update.filter((a) => a.length === 64)) { for (const pk of update.filter((a) => a.length === 64)) {
if (!existing.has(pk)) { if (!existing.has(pk)) {
existing.add(pk); existing.add(pk);
changes = true; changes = true;
} }
} }
for (let pk of existing) { for (const pk of existing) {
if (!update.includes(pk)) { if (!update.includes(pk)) {
existing.delete(pk); existing.delete(pk);
changes = true; changes = true;
@ -355,7 +355,7 @@ const LoginSlice = createSlice({
} }
let didChange = false; let didChange = false;
for (let x of n) { for (const x of n) {
if (!state.dms.some((a) => a.id === x.id)) { if (!state.dms.some((a) => a.id === x.id)) {
state.dms.push(x); state.dms.push(x);
didChange = true; didChange = true;
@ -370,7 +370,7 @@ const LoginSlice = createSlice({
state.dmInteraction += 1; state.dmInteraction += 1;
}, },
logout: (state) => { logout: (state) => {
let relays = { ...state.relays }; const relays = { ...state.relays };
Object.assign(state, InitState); Object.assign(state, InitState);
state.loggedOut = true; state.loggedOut = true;
window.localStorage.clear(); window.localStorage.clear();
@ -430,7 +430,7 @@ export function sendNotification({
hasPermission && timestamp > readNotifications; hasPermission && timestamp > readNotifications;
if (shouldShowNotification) { if (shouldShowNotification) {
try { try {
let worker = await navigator.serviceWorker.ready; const worker = await navigator.serviceWorker.ready;
worker.showNotification(title, { worker.showNotification(title, {
tag: "notification", tag: "notification",
vibrate: [500], vibrate: [500],

View File

@ -26,7 +26,7 @@ export interface MetadataCache extends UserMetadata {
export function mapEventToProfile(ev: TaggedRawEvent) { export function mapEventToProfile(ev: TaggedRawEvent) {
try { try {
let data: UserMetadata = JSON.parse(ev.content); const data: UserMetadata = JSON.parse(ev.content);
return { return {
pubkey: ev.pubkey, pubkey: ev.pubkey,
npub: hexToBech32("npub", ev.pubkey), npub: hexToBech32("npub", ev.pubkey),
@ -43,12 +43,12 @@ export interface UsersDb {
isAvailable(): Promise<boolean>; isAvailable(): Promise<boolean>;
query(str: string): Promise<MetadataCache[]>; query(str: string): Promise<MetadataCache[]>;
find(key: HexKey): Promise<MetadataCache | undefined>; find(key: HexKey): Promise<MetadataCache | undefined>;
add(user: MetadataCache): Promise<any>; add(user: MetadataCache): Promise<void>;
put(user: MetadataCache): Promise<any>; put(user: MetadataCache): Promise<void>;
bulkAdd(users: MetadataCache[]): Promise<any>; bulkAdd(users: MetadataCache[]): Promise<void>;
bulkGet(keys: HexKey[]): Promise<MetadataCache[]>; bulkGet(keys: HexKey[]): Promise<MetadataCache[]>;
bulkPut(users: MetadataCache[]): Promise<any>; bulkPut(users: MetadataCache[]): Promise<void>;
update(key: HexKey, fields: Record<string, any>): Promise<any>; update(key: HexKey, fields: Record<string, string | number>): Promise<void>;
} }
export interface UsersStore { export interface UsersStore {

View File

@ -4,18 +4,19 @@ import { db as idb } from "Db";
import { UsersDb, MetadataCache, setUsers } from "State/Users"; import { UsersDb, MetadataCache, setUsers } from "State/Users";
import store, { RootState } from "State/Store"; import store, { RootState } from "State/Store";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { unwrap } from "Util";
class IndexedUsersDb implements UsersDb { class IndexedUsersDb implements UsersDb {
ready: boolean = false; ready = false;
isAvailable() { isAvailable() {
if ("indexedDB" in window) { if ("indexedDB" in window) {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
const req = window.indexedDB.open("dummy", 1); const req = window.indexedDB.open("dummy", 1);
req.onsuccess = (ev) => { req.onsuccess = () => {
resolve(true); resolve(true);
}; };
req.onerror = (ev) => { req.onerror = () => {
resolve(false); resolve(false);
}; };
}); });
@ -41,30 +42,29 @@ class IndexedUsersDb implements UsersDb {
.toArray(); .toArray();
} }
bulkGet(keys: HexKey[]) { async bulkGet(keys: HexKey[]) {
return idb.users const ret = await idb.users.bulkGet(keys);
.bulkGet(keys) return ret.filter((a) => a !== undefined).map((a_1) => unwrap(a_1));
.then((ret) => ret.filter((a) => a !== undefined).map((a) => a!));
} }
add(user: MetadataCache) { async add(user: MetadataCache) {
return idb.users.add(user); await idb.users.add(user);
} }
put(user: MetadataCache) { async put(user: MetadataCache) {
return idb.users.put(user); await idb.users.put(user);
} }
bulkAdd(users: MetadataCache[]) { async bulkAdd(users: MetadataCache[]) {
return idb.users.bulkAdd(users); await idb.users.bulkAdd(users);
} }
bulkPut(users: MetadataCache[]) { async bulkPut(users: MetadataCache[]) {
return idb.users.bulkPut(users); await idb.users.bulkPut(users);
} }
update(key: HexKey, fields: Record<string, any>) { async update(key: HexKey, fields: Record<string, string>) {
return idb.users.update(key, fields); await idb.users.update(key, fields);
} }
} }
@ -128,7 +128,7 @@ class ReduxUsersDb implements UsersDb {
}); });
} }
async update(key: HexKey, fields: Record<string, any>) { async update(key: HexKey, fields: Record<string, string>) {
const state = store.getState(); const state = store.getState();
const { users } = state.users; const { users } = state.users;
const current = users[key]; const current = users[key];

View File

@ -4,8 +4,9 @@ import { MetadataCache } from "State/Users";
import type { RootState } from "State/Store"; import type { RootState } from "State/Store";
import { HexKey } from "Nostr"; import { HexKey } from "Nostr";
import { useDb } from "./Db"; import { useDb } from "./Db";
import { unwrap } from "Util";
export function useQuery(query: string, limit: number = 5) { export function useQuery(query: string) {
const db = useDb(); const db = useDb();
return useLiveQuery(async () => db.query(query), [query]); return useLiveQuery(async () => db.query(query), [query]);
} }
@ -46,5 +47,5 @@ export function useKeys(pubKeys: HexKey[]): Map<HexKey, MetadataCache> {
return new Map(); return new Map();
}, [pubKeys, users]); }, [pubKeys, users]);
return dbUsers!; return dbUsers ?? new Map();
} }

View File

@ -3,11 +3,11 @@ import { UploadResult } from "Upload";
export default async function NostrBuild( export default async function NostrBuild(
file: File | Blob file: File | Blob
): Promise<UploadResult> { ): Promise<UploadResult> {
let fd = new FormData(); const fd = new FormData();
fd.append("fileToUpload", file); fd.append("fileToUpload", file);
fd.append("submit", "Upload Image"); fd.append("submit", "Upload Image");
let rsp = await fetch("https://nostr.build/api/upload/snort.php", { const rsp = await fetch("https://nostr.build/api/upload/snort.php", {
body: fd, body: fd,
method: "POST", method: "POST",
headers: { headers: {
@ -15,7 +15,7 @@ export default async function NostrBuild(
}, },
}); });
if (rsp.ok) { if (rsp.ok) {
let data = await rsp.json(); const data = await rsp.json();
return { return {
url: new URL(data).toString(), url: new URL(data).toString(),
}; };

View File

@ -3,10 +3,10 @@ import { UploadResult } from "Upload";
export default async function NostrImg( export default async function NostrImg(
file: File | Blob file: File | Blob
): Promise<UploadResult> { ): Promise<UploadResult> {
let fd = new FormData(); const fd = new FormData();
fd.append("image", file); fd.append("image", file);
let rsp = await fetch("https://nostrimg.com/api/upload", { const rsp = await fetch("https://nostrimg.com/api/upload", {
body: fd, body: fd,
method: "POST", method: "POST",
headers: { headers: {
@ -14,7 +14,7 @@ export default async function NostrImg(
}, },
}); });
if (rsp.ok) { if (rsp.ok) {
let data: UploadResponse = await rsp.json(); const data: UploadResponse = await rsp.json();
if (typeof data?.imageUrl === "string" && data.success) { if (typeof data?.imageUrl === "string" && data.success) {
return { return {
url: new URL(data.imageUrl).toString(), url: new URL(data.imageUrl).toString(),

View File

@ -13,7 +13,7 @@ export default async function VoidCat(
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256", buf); const digest = await crypto.subtle.digest("SHA-256", buf);
let req = await fetch(`${VoidCatHost}/upload`, { const req = await fetch(`${VoidCatHost}/upload`, {
mode: "cors", mode: "cors",
method: "POST", method: "POST",
body: buf, body: buf,
@ -28,7 +28,7 @@ export default async function VoidCat(
}); });
if (req.ok) { if (req.ok) {
let rsp: VoidUploadResponse = await req.json(); const rsp: VoidUploadResponse = await req.json();
if (rsp.ok) { if (rsp.ok) {
let ext = filename.match(FileExtensionRegex); let ext = filename.match(FileExtensionRegex);
if (rsp.file?.metadata?.mimeType === "image/webp") { if (rsp.file?.metadata?.mimeType === "image/webp") {

View File

@ -1,7 +1,7 @@
import * as secp from "@noble/secp256k1"; import * as secp from "@noble/secp256k1";
import { sha256 as hash } from "@noble/hashes/sha256"; import { sha256 as hash } from "@noble/hashes/sha256";
import { bech32 } from "bech32"; import { bech32 } from "bech32";
import { HexKey, RawEvent, TaggedRawEvent, u256 } from "Nostr"; import { HexKey, TaggedRawEvent, u256 } from "Nostr";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
import { MessageDescriptor } from "react-intl"; import { MessageDescriptor } from "react-intl";
@ -10,11 +10,11 @@ export const sha256 = (str: string) => {
}; };
export async function openFile(): Promise<File | undefined> { export async function openFile(): Promise<File | undefined> {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
let elm = document.createElement("input"); const elm = document.createElement("input");
elm.type = "file"; elm.type = "file";
elm.onchange = (e: Event) => { elm.onchange = (e: Event) => {
let elm = e.target as HTMLInputElement; const elm = e.target as HTMLInputElement;
if (elm.files) { if (elm.files) {
resolve(elm.files[0]); resolve(elm.files[0]);
} else { } else {
@ -36,13 +36,15 @@ export function parseId(id: string) {
if (hrp.some((a) => id.startsWith(a))) { if (hrp.some((a) => id.startsWith(a))) {
return bech32ToHex(id); return bech32ToHex(id);
} }
} catch (e) {} } catch (e) {
// Ignore the error.
}
return id; return id;
} }
export function bech32ToHex(str: string) { export function bech32ToHex(str: string) {
let nKey = bech32.decode(str); const nKey = bech32.decode(str);
let buff = bech32.fromWords(nKey.words); const buff = bech32.fromWords(nKey.words);
return secp.utils.bytesToHex(Uint8Array.from(buff)); return secp.utils.bytesToHex(Uint8Array.from(buff));
} }
@ -52,8 +54,8 @@ export function bech32ToHex(str: string) {
* @returns * @returns
*/ */
export function bech32ToText(str: string) { export function bech32ToText(str: string) {
let decoded = bech32.decode(str, 1000); const decoded = bech32.decode(str, 1000);
let buf = bech32.fromWords(decoded.words); const buf = bech32.fromWords(decoded.words);
return new TextDecoder().decode(Uint8Array.from(buf)); return new TextDecoder().decode(Uint8Array.from(buf));
} }
@ -76,7 +78,7 @@ export function hexToBech32(hrp: string, hex: string) {
} }
try { try {
let buf = secp.utils.hexToBytes(hex); const buf = secp.utils.hexToBytes(hex);
return bech32.encode(hrp, bech32.toWords(buf)); return bech32.encode(hrp, bech32.toWords(buf));
} catch (e) { } catch (e) {
console.warn("Invalid hex", hex, e); console.warn("Invalid hex", hex, e);
@ -140,13 +142,13 @@ export function getReactions(
export function extractLnAddress(lnurl: string) { export function extractLnAddress(lnurl: string) {
// some clients incorrectly set this to LNURL service, patch this // some clients incorrectly set this to LNURL service, patch this
if (lnurl.toLowerCase().startsWith("lnurl")) { if (lnurl.toLowerCase().startsWith("lnurl")) {
let url = bech32ToText(lnurl); const url = bech32ToText(lnurl);
if (url.startsWith("http")) { if (url.startsWith("http")) {
let parsedUri = new URL(url); const parsedUri = new URL(url);
// is lightning address // is lightning address
if (parsedUri.pathname.startsWith("/.well-known/lnurlp/")) { if (parsedUri.pathname.startsWith("/.well-known/lnurlp/")) {
let pathParts = parsedUri.pathname.split("/"); const pathParts = parsedUri.pathname.split("/");
let username = pathParts[pathParts.length - 1]; const username = pathParts[pathParts.length - 1];
return `${username}@${parsedUri.hostname}`; return `${username}@${parsedUri.hostname}`;
} }
} }
@ -165,7 +167,7 @@ export function unixNow() {
* @returns Cancel timeout function * @returns Cancel timeout function
*/ */
export function debounce(timeout: number, fn: () => void) { export function debounce(timeout: number, fn: () => void) {
let t = setTimeout(fn, timeout); const t = setTimeout(fn, timeout);
return () => clearTimeout(t); return () => clearTimeout(t);
} }
@ -201,3 +203,10 @@ export function dedupeByPubkey(events: TaggedRawEvent[]) {
); );
return deduped.list as TaggedRawEvent[]; return deduped.list as TaggedRawEvent[];
} }
export function unwrap<T>(v: T | undefined | null): T {
if (v === undefined || v === null) {
throw new Error("missing value");
}
return v;
}

View File

@ -26,6 +26,7 @@ import SearchPage from "Pages/SearchPage";
import HelpPage from "Pages/HelpPage"; import HelpPage from "Pages/HelpPage";
import { NewUserRoutes } from "Pages/new"; import { NewUserRoutes } from "Pages/new";
import { IntlProvider } from "./IntlProvider"; import { IntlProvider } from "./IntlProvider";
import { unwrap } from "Util";
/** /**
* HTTP query provider * HTTP query provider
@ -97,7 +98,7 @@ export const router = createBrowserRouter([
}, },
]); ]);
const root = ReactDOM.createRoot(document.getElementById("root")!); const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));
root.render( root.render(
<StrictMode> <StrictMode>
<Provider store={Store}> <Provider store={Store}>