diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..648bbd0 --- /dev/null +++ b/.eslintrc.cjs @@ -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, + }, +}; diff --git a/d.ts b/d.ts index 125a621..2bf2072 100644 --- a/d.ts +++ b/d.ts @@ -1,14 +1,14 @@ declare module "*.jpg" { - const value: any; + const value: unknown; export default value; } declare module "*.svg" { - const value: any; + const value: unknown; export default value; } declare module "*.webp" { - const value: any; + const value: string; export default value; } diff --git a/src/Const.ts b/src/Const.ts index 012f6e2..ac6c829 100644 --- a/src/Const.ts +++ b/src/Const.ts @@ -83,17 +83,20 @@ export const RecommendedFollows = [ * Regex to match email address */ 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,}))$/; /** * Generic URL regex */ export const UrlRegex = + // eslint-disable-next-line no-useless-escape /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/i; /** * Extract file extensions regex */ +// eslint-disable-next-line no-useless-escape export const FileExtensionRegex = /\.([\w]+)$/i; /** @@ -121,6 +124,7 @@ export const TweetUrlRegex = /** * Hashtag regex */ +// eslint-disable-next-line no-useless-escape export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/; /** diff --git a/src/Element/AsyncButton.tsx b/src/Element/AsyncButton.tsx index d420aa3..068c40c 100644 --- a/src/Element/AsyncButton.tsx +++ b/src/Element/AsyncButton.tsx @@ -1,14 +1,20 @@ import { useState } from "react"; -export default function AsyncButton(props: any) { +interface AsyncButtonProps + extends React.ButtonHTMLAttributes { + onClick(e: React.MouseEvent): Promise | void; + children?: React.ReactNode; +} + +export default function AsyncButton(props: AsyncButtonProps) { const [loading, setLoading] = useState(false); - async function handle(e: any) { + async function handle(e: React.MouseEvent) { if (loading) return; setLoading(true); try { if (typeof props.onClick === "function") { - let f = props.onClick(e); + const f = props.onClick(e); if (f instanceof Promise) { await f; } @@ -19,12 +25,7 @@ export default function AsyncButton(props: any) { } return ( - ); diff --git a/src/Element/BlockList.tsx b/src/Element/BlockList.tsx index b408c7d..e6ba6c8 100644 --- a/src/Element/BlockList.tsx +++ b/src/Element/BlockList.tsx @@ -1,13 +1,7 @@ -import { useMemo } from "react"; -import { useSelector } from "react-redux"; import { FormattedMessage } from "react-intl"; - -import { HexKey } from "Nostr"; -import type { RootState } from "State/Store"; import MuteButton from "Element/MuteButton"; import BlockButton from "Element/BlockButton"; import ProfilePreview from "Element/ProfilePreview"; -import useMutedFeed, { getMuted } from "Feed/MuteList"; import useModeration from "Hooks/useModeration"; import messages from "./messages"; @@ -17,7 +11,6 @@ interface BlockListProps { } export default function BlockList({ variant }: BlockListProps) { - const { publicKey } = useSelector((s: RootState) => s.login); const { blocked, muted } = useModeration(); return ( diff --git a/src/Element/Collapsed.tsx b/src/Element/Collapsed.tsx index 569fb32..b1f282e 100644 --- a/src/Element/Collapsed.tsx +++ b/src/Element/Collapsed.tsx @@ -1,4 +1,4 @@ -import { useState, ReactNode } from "react"; +import { ReactNode } from "react"; import ShowMore from "Element/ShowMore"; diff --git a/src/Element/Copy.tsx b/src/Element/Copy.tsx index df6848f..10ea231 100644 --- a/src/Element/Copy.tsx +++ b/src/Element/Copy.tsx @@ -8,7 +8,7 @@ export interface CopyProps { maxSize?: number; } export default function Copy({ text, maxSize = 32 }: CopyProps) { - const { copy, copied, error } = useCopy(); + const { copy, copied } = useCopy(); const sliceLength = maxSize / 2; const trimmed = text.length > maxSize diff --git a/src/Element/DM.tsx b/src/Element/DM.tsx index 19ea59f..0c0d871 100644 --- a/src/Element/DM.tsx +++ b/src/Element/DM.tsx @@ -12,6 +12,7 @@ import { setLastReadDm } from "Pages/MessagesPage"; import { RootState } from "State/Store"; import { HexKey, TaggedRawEvent } from "Nostr"; import { incDmInteraction } from "State/Login"; +import { unwrap } from "Util"; import messages from "./messages"; @@ -32,11 +33,11 @@ export default function DM(props: DMProps) { const isMe = props.data.pubkey === pubKey; const otherPubkey = isMe ? pubKey - : props.data.tags.find((a) => a[0] === "p")![1]; + : unwrap(props.data.tags.find((a) => a[0] === "p")?.[1]); async function decrypt() { - let e = new Event(props.data); - let decrypted = await publisher.decryptDm(e); + const e = new Event(props.data); + const decrypted = await publisher.decryptDm(e); setContent(decrypted || ""); if (!isMe) { setLastReadDm(e.PubKey); diff --git a/src/Element/FollowButton.tsx b/src/Element/FollowButton.tsx index 2339e07..da7b03a 100644 --- a/src/Element/FollowButton.tsx +++ b/src/Element/FollowButton.tsx @@ -21,12 +21,12 @@ export default function FollowButton(props: FollowButtonProps) { const baseClassname = `${props.className} follow-button`; async function follow(pubkey: HexKey) { - let ev = await publiser.addFollow(pubkey); + const ev = await publiser.addFollow(pubkey); publiser.broadcast(ev); } async function unfollow(pubkey: HexKey) { - let ev = await publiser.removeFollow(pubkey); + const ev = await publiser.removeFollow(pubkey); publiser.broadcast(ev); } diff --git a/src/Element/FollowListBase.tsx b/src/Element/FollowListBase.tsx index 4479a2d..d421d1d 100644 --- a/src/Element/FollowListBase.tsx +++ b/src/Element/FollowListBase.tsx @@ -17,7 +17,7 @@ export default function FollowListBase({ const publisher = useEventPublisher(); async function followAll() { - let ev = await publisher.addFollow(pubkeys); + const ev = await publisher.addFollow(pubkeys); publisher.broadcast(ev); } diff --git a/src/Element/FollowersList.tsx b/src/Element/FollowersList.tsx index cc90351..7fb169e 100644 --- a/src/Element/FollowersList.tsx +++ b/src/Element/FollowersList.tsx @@ -17,7 +17,7 @@ export default function FollowersList({ pubkey }: FollowersListProps) { const feed = useFollowersFeed(pubkey); const pubkeys = useMemo(() => { - let contactLists = feed?.store.notes.filter( + const contactLists = feed?.store.notes.filter( (a) => a.kind === EventKind.ContactList && a.tags.some((b) => b[0] === "p" && b[1] === pubkey) diff --git a/src/Element/FollowsYou.tsx b/src/Element/FollowsYou.tsx index fdf32b1..cc49dd4 100644 --- a/src/Element/FollowsYou.tsx +++ b/src/Element/FollowsYou.tsx @@ -25,7 +25,7 @@ export default function FollowsYou({ pubkey }: FollowsYouProps) { return getFollowers(feed.store, pubkey); }, [feed, pubkey]); - const followsMe = pubkeys.includes(loginPubKey!) ?? false; + const followsMe = loginPubKey ? pubkeys.includes(loginPubKey) : false; return followsMe ? ( {formatMessage(messages.FollowsYou)} diff --git a/src/Element/HyperText.tsx b/src/Element/HyperText.tsx index 72683fe..2d4f1b0 100644 --- a/src/Element/HyperText.tsx +++ b/src/Element/HyperText.tsx @@ -135,7 +135,9 @@ export default function HyperText({ ); } - } catch (error) {} + } catch (error) { + // Ignore the error. + } return ( { try { - let parsed = invoiceDecode(invoice); + const parsed = invoiceDecode(invoice); - let amount = parseInt( - parsed.sections.find((a: any) => a.name === "amount")?.value + const amount = parseInt( + parsed.sections.find((a: Section) => a.name === "amount")?.value ); - let timestamp = parseInt( - parsed.sections.find((a: any) => a.name === "timestamp")?.value + const timestamp = parseInt( + parsed.sections.find((a: Section) => a.name === "timestamp")?.value ); - let expire = parseInt( - parsed.sections.find((a: any) => a.name === "expiry")?.value + const expire = parseInt( + parsed.sections.find((a: Section) => a.name === "expiry")?.value ); - let description = parsed.sections.find( - (a: any) => a.name === "description" + const description = parsed.sections.find( + (a: Section) => a.name === "description" )?.value; - let ret = { + const ret = { amount: !isNaN(amount) ? amount / 1000 : 0, expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null, description, @@ -72,7 +77,7 @@ export default function Invoice(props: InvoiceProps) { ); } - async function payInvoice(e: any) { + async function payInvoice(e: React.MouseEvent) { e.stopPropagation(); if (webln?.enabled) { try { diff --git a/src/Element/LNURLTip.css b/src/Element/LNURLTip.css new file mode 100644 index 0000000..917f4c2 --- /dev/null +++ b/src/Element/LNURLTip.css @@ -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; +} diff --git a/src/Element/LNURLTip.tsx b/src/Element/LNURLTip.tsx new file mode 100644 index 0000000..2ff53b0 --- /dev/null +++ b/src/Element/LNURLTip.tsx @@ -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(); + const [amount, setAmount] = useState(); + const [customAmount, setCustomAmount] = useState(0); + const [invoice, setInvoice] = useState(); + const [comment, setComment] = useState(); + const [error, setError] = useState(); + const [success, setSuccess] = useState(); + 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(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 { + 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 ( +
+ setCustomAmount(parseInt(e.target.value))} + /> +
selectAmount(customAmount)}> + Confirm +
+
+ ); + } + + 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 ( + <> +
+ {metadata?.description ?? service} +
+
+ {(payService?.commentAllowed ?? 0) > 0 ? ( + setComment(e.target.value)} + /> + ) : null} +
+
+ {serviceAmounts.map((a) => ( + selectAmount(a)} + > + {a.toLocaleString()} + + ))} + {payService ? ( + selectAmount(-1)} + > + Custom + + ) : null} +
+ {amount === -1 ? custom() : null} + {(amount ?? 0) > 0 && ( + + )} + + ); + } + + function payInvoice() { + if (success) return null; + const pr = invoice?.pr; + return ( + <> +
+ {props.notice && {props.notice}} + +
+ {pr && ( + <> +
+ +
+
+ +
+ + )} +
+
+ + ); + } + + function successAction() { + if (!success) return null; + return ( + <> +

{success?.description ?? "Paid!"}

+ {success.url ? ( +
+ {success.url} + + ) : null} + + ); + } + + const defaultTitle = payService?.nostrPubkey + ? "⚡️ Send Zap!" + : "⚡️ Send sats"; + if (!show) return null; + return ( + +
e.stopPropagation()}> +

{props.title || defaultTitle}

+ {invoiceForm()} + {error ?

{error}

: null} + {payInvoice()} + {successAction()} +
+
+ ); +} diff --git a/src/Element/LoadMore.tsx b/src/Element/LoadMore.tsx index e5cbf2a..4bbfad7 100644 --- a/src/Element/LoadMore.tsx +++ b/src/Element/LoadMore.tsx @@ -23,7 +23,7 @@ export default function LoadMore({ }, [inView, shouldLoadMore, tick]); useEffect(() => { - let t = setInterval(() => { + const t = setInterval(() => { setTick((x) => (x += 1)); }, 500); return () => clearInterval(t); diff --git a/src/Element/Mention.tsx b/src/Element/Mention.tsx index 67ed27f..c38140c 100644 --- a/src/Element/Mention.tsx +++ b/src/Element/Mention.tsx @@ -9,10 +9,10 @@ export default function Mention({ pubkey }: { pubkey: HexKey }) { const name = useMemo(() => { let name = hexToBech32("npub", pubkey).substring(0, 12); - if ((user?.display_name?.length ?? 0) > 0) { - name = user!.display_name!; - } else if ((user?.name?.length ?? 0) > 0) { - name = user!.name!; + if (user?.display_name !== undefined && user.display_name.length > 0) { + name = user.display_name; + } else if (user?.name !== undefined && user.name.length > 0) { + name = user.name; } return name; }, [user, pubkey]); diff --git a/src/Element/Modal.tsx b/src/Element/Modal.tsx index 641511b..d136c07 100644 --- a/src/Element/Modal.tsx +++ b/src/Element/Modal.tsx @@ -8,10 +8,13 @@ export interface ModalProps { children: React.ReactNode; } -function useOnClickOutside(ref: any, onClickOutside: () => void) { +function useOnClickOutside( + ref: React.MutableRefObject, + onClickOutside: () => void +) { useEffect(() => { - function handleClickOutside(ev: any) { - if (ref && ref.current && !ref.current.contains(ev.target)) { + function handleClickOutside(ev: MouseEvent) { + if (ref && ref.current && !ref.current.contains(ev.target as Node)) { onClickOutside(); } } @@ -24,7 +27,7 @@ function useOnClickOutside(ref: any, onClickOutside: () => void) { export default function Modal(props: ModalProps) { const ref = useRef(null); - const onClose = props.onClose || (() => {}); + const onClose = props.onClose || (() => undefined); const className = props.className || ""; useOnClickOutside(ref, onClose); diff --git a/src/Element/MutedList.tsx b/src/Element/MutedList.tsx index 4d51a2c..ee59ac0 100644 --- a/src/Element/MutedList.tsx +++ b/src/Element/MutedList.tsx @@ -1,6 +1,5 @@ import { useMemo } from "react"; import { FormattedMessage } from "react-intl"; - import { HexKey } from "Nostr"; import MuteButton from "Element/MuteButton"; import ProfilePreview from "Element/ProfilePreview"; diff --git a/src/Element/Nip5Service.tsx b/src/Element/Nip5Service.tsx index dd2c2a5..a41711c 100644 --- a/src/Element/Nip5Service.tsx +++ b/src/Element/Nip5Service.tsx @@ -29,7 +29,9 @@ type Nip05ServiceProps = { supportLink: string; }; -type ReduxStore = any; +interface ReduxStore { + login: { publicKey: string }; +} export default function Nip5Service(props: Nip05ServiceProps) { const navigate = useNavigate(); @@ -64,9 +66,9 @@ export default function Nip5Service(props: Nip05ServiceProps) { if ("error" in a) { setError(a as ServiceError); } else { - let svc = a as ServiceConfig; + const svc = a as ServiceConfig; setServiceConfig(svc); - let defaultDomain = + const defaultDomain = svc.domains.find((a) => a.default)?.name || svc.domains[0].name; setDomain(defaultDomain); } @@ -86,7 +88,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { setAvailabilityResponse({ available: false, why: "TOO_LONG" }); return; } - let rx = new RegExp( + const rx = new RegExp( domainConfig?.regex[0] ?? "", domainConfig?.regex[1] ?? "" ); @@ -111,14 +113,14 @@ export default function Nip5Service(props: Nip05ServiceProps) { useEffect(() => { if (registerResponse && showInvoice) { - let t = setInterval(async () => { - let status = await svc.CheckRegistration(registerResponse.token); + const t = setInterval(async () => { + const status = await svc.CheckRegistration(registerResponse.token); if ("error" in status) { setError(status); setRegisterResponse(undefined); setShowInvoice(false); } else { - let result: CheckRegisterResponse = status; + const result: CheckRegisterResponse = status; if (result.available && result.paid) { setShowInvoice(false); setRegisterStatus(status); @@ -131,8 +133,14 @@ export default function Nip5Service(props: Nip05ServiceProps) { } }, [registerResponse, showInvoice, svc]); - function mapError(e: ServiceErrorCode, t: string | null): string | undefined { - let whyMap = new Map([ + function mapError( + e: ServiceErrorCode | undefined, + t: string | null + ): string | undefined { + if (e === undefined) { + return undefined; + } + const whyMap = new Map([ ["TOO_SHORT", formatMessage(messages.TooShort)], ["TOO_LONG", formatMessage(messages.TooLong)], ["REGEX", formatMessage(messages.Regex)], @@ -149,7 +157,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { return; } - let rsp = await svc.RegisterHandle(handle, domain, pubkey); + const rsp = await svc.RegisterHandle(handle, domain, pubkey); if ("error" in rsp) { setError(rsp); } else { @@ -160,11 +168,11 @@ export default function Nip5Service(props: Nip05ServiceProps) { async function updateProfile(handle: string, domain: string) { if (user) { - let newProfile = { + const newProfile = { ...user, nip05: `${handle}@${domain}`, } as UserMetadata; - let ev = await publisher.metadata(newProfile); + const ev = await publisher.metadata(newProfile); publisher.broadcast(ev); navigate("/settings"); } @@ -231,7 +239,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { {" "} {mapError( - availabilityResponse.why!, + availabilityResponse.why, availabilityResponse.reasonTag || null )} diff --git a/src/Element/Note.tsx b/src/Element/Note.tsx index a8ffe5d..9818dcf 100644 --- a/src/Element/Note.tsx +++ b/src/Element/Note.tsx @@ -17,7 +17,6 @@ import Text from "Element/Text"; import { eventLink, getReactions, hexToBech32 } from "Util"; import NoteFooter, { Translation } from "Element/NoteFooter"; import NoteTime from "Element/NoteTime"; -import ShowMore from "Element/ShowMore"; import EventKind from "Nostr/EventKind"; import { useUserProfiles } from "Feed/ProfileFeed"; import { TaggedRawEvent, u256 } from "Nostr"; @@ -39,10 +38,10 @@ export interface NoteProps { ["data-ev"]?: NEvent; } -const HiddenNote = ({ children }: any) => { +const HiddenNote = ({ children }: { children: React.ReactNode }) => { const [show, setShow] = useState(false); return show ? ( - children + <>{children} ) : (
@@ -61,7 +60,6 @@ export default function Note(props: NoteProps) { const navigate = useNavigate(); const { data, - className, related, highlight, options: opt, @@ -80,9 +78,9 @@ export default function Note(props: NoteProps) { const { ref, inView, entry } = useInView({ triggerOnce: true }); const [extendable, setExtendable] = useState(false); const [showMore, setShowMore] = useState(false); - const baseClassname = `note card ${props.className ? props.className : ""}`; + const baseClassName = `note card ${props.className ? props.className : ""}`; const [translated, setTranslated] = useState(); - const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event; + // TODO Why was this unused? Was this a mistake? const { formatMessage } = useIntl(); const options = { @@ -93,7 +91,7 @@ export default function Note(props: NoteProps) { }; const transformBody = useCallback(() => { - let body = ev?.Content ?? ""; + const body = ev?.Content ?? ""; if (deletions?.length > 0) { return ( @@ -113,14 +111,14 @@ export default function Note(props: NoteProps) { useLayoutEffect(() => { if (entry && inView && extendable === false) { - let h = entry?.target.clientHeight ?? 0; + const h = entry?.target.clientHeight ?? 0; if (h > 650) { setExtendable(true); } } }, [inView, entry, extendable]); - function goToEvent(e: any, id: u256) { + function goToEvent(e: React.MouseEvent, id: u256) { e.stopPropagation(); navigate(eventLink(id)); } @@ -131,9 +129,9 @@ export default function Note(props: NoteProps) { } const maxMentions = 2; - let replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event; - let mentions: { pk: string; name: string; link: ReactNode }[] = []; - for (let pk of ev.Thread?.PubKeys) { + const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event; + const mentions: { pk: string; name: string; link: ReactNode }[] = []; + for (const pk of ev.Thread?.PubKeys ?? []) { const u = users?.get(pk); const npub = hexToBech32("npub", pk); 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)); - let othersLength = mentions.length - maxMentions; - const renderMention = (m: any, idx: number) => { + mentions.sort((a) => (a.name.startsWith("npub") ? 1 : -1)); + const othersLength = mentions.length - maxMentions; + const renderMention = (m: { link: React.ReactNode }, idx: number) => { return ( <> {idx > 0 && ", "} @@ -268,7 +266,7 @@ export default function Note(props: NoteProps) { const note = (
void; replyTo?: NEvent; - onSend?: Function; + onSend?: () => void; autoFocus: boolean; } export function NoteCreator(props: NoteCreatorProps) { const { show, setShow, replyTo, onSend, autoFocus } = props; const publisher = useEventPublisher(); - const [note, setNote] = useState(); + const [note, setNote] = useState(""); const [error, setError] = useState(); const [active, setActive] = useState(false); const uploader = useFileUpload(); @@ -48,7 +48,7 @@ export function NoteCreator(props: NoteCreatorProps) { async function sendNote() { if (note) { - let ev = replyTo + const ev = replyTo ? await publisher.reply(replyTo, note) : await publisher.note(note); console.debug("Sending note: ", ev); @@ -64,21 +64,23 @@ export function NoteCreator(props: NoteCreatorProps) { async function attachFile() { try { - let file = await openFile(); + const file = await openFile(); if (file) { - let rx = await uploader.upload(file, file.name); + const rx = await uploader.upload(file, file.name); if (rx.url) { setNote((n) => `${n ? `${n}\n` : ""}${rx.url}`); } else if (rx?.error) { setError(rx.error); } } - } catch (error: any) { - setError(error?.message); + } catch (error: unknown) { + if (error instanceof Error) { + setError(error?.message); + } } } - function onChange(ev: any) { + function onChange(ev: React.ChangeEvent) { const { value } = ev.target; setNote(value); if (value) { @@ -88,7 +90,7 @@ export function NoteCreator(props: NoteCreatorProps) { } } - function cancel(ev: any) { + function cancel() { setShow(false); setNote(""); } @@ -112,11 +114,7 @@ export function NoteCreator(props: NoteCreatorProps) { value={note} onFocus={() => setActive(true)} /> -
diff --git a/src/Element/NoteFooter.tsx b/src/Element/NoteFooter.tsx index 3f10b91..54859a6 100644 --- a/src/Element/NoteFooter.tsx +++ b/src/Element/NoteFooter.tsx @@ -95,7 +95,7 @@ export default function NoteFooter(props: NoteFooterProps) { const groupReactions = useMemo(() => { const result = reactions?.reduce( (acc, reaction) => { - let kind = normalizeReaction(reaction.content); + const kind = normalizeReaction(reaction.content); const rs = acc[kind] || []; if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) { return acc; @@ -128,7 +128,7 @@ export default function NoteFooter(props: NoteFooterProps) { async function react(content: string) { if (!hasReacted(content)) { - let evLike = await publisher.react(ev, content); + const evLike = await publisher.react(ev, content); publisher.broadcast(evLike); } } @@ -139,7 +139,7 @@ export default function NoteFooter(props: NoteFooterProps) { 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); } } @@ -150,14 +150,14 @@ export default function NoteFooter(props: NoteFooterProps) { !prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id })) ) { - let evRepost = await publisher.repost(ev); + const evRepost = await publisher.repost(ev); publisher.broadcast(evRepost); } } } function tipButton() { - let service = author?.lud16 || author?.lud06; + const service = author?.lud16 || author?.lud06; if (service) { return ( <> @@ -246,7 +246,7 @@ export default function NoteFooter(props: NoteFooterProps) { }); if (res.ok) { - let result = await res.json(); + const result = await res.json(); if (typeof props.onTranslated === "function" && result) { props.onTranslated({ text: result.translatedText, @@ -332,7 +332,7 @@ export default function NoteFooter(props: NoteFooterProps) { {reactionIcons()}
setReply((s) => !s)} + onClick={() => setReply((s) => !s)} >
diff --git a/src/Element/NoteGhost.tsx b/src/Element/NoteGhost.tsx index b0d60f6..1604f51 100644 --- a/src/Element/NoteGhost.tsx +++ b/src/Element/NoteGhost.tsx @@ -1,8 +1,13 @@ import "./Note.css"; import ProfileImage from "Element/ProfileImage"; -export default function NoteGhost(props: any) { - const className = `note card ${props.className ? props.className : ""}`; +interface NoteGhostProps { + className?: string; + children: React.ReactNode; +} + +export default function NoteGhost(props: NoteGhostProps) { + const className = `note card ${props.className ?? ""}`; return (
diff --git a/src/Element/NoteReaction.tsx b/src/Element/NoteReaction.tsx index 1d5fe0e..054bfc7 100644 --- a/src/Element/NoteReaction.tsx +++ b/src/Element/NoteReaction.tsx @@ -23,7 +23,7 @@ export default function NoteReaction(props: NoteReactionProps) { const refEvent = useMemo(() => { if (ev) { - let eTags = ev.Tags.filter((a) => a.Key === "e"); + const eTags = ev.Tags.filter((a) => a.Key === "e"); if (eTags.length > 0) { return eTags[0].Event; } @@ -45,7 +45,7 @@ export default function NoteReaction(props: NoteReactionProps) { ev.Content !== "#[0]" ) { try { - let r: RawEvent = JSON.parse(ev.Content); + const r: RawEvent = JSON.parse(ev.Content); return r as TaggedRawEvent; } catch (e) { console.error("Could not load reposted content", e); diff --git a/src/Element/NoteTime.tsx b/src/Element/NoteTime.tsx index d494a09..42ce88a 100644 --- a/src/Element/NoteTime.tsx +++ b/src/Element/NoteTime.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from "react"; -import { FormattedRelativeTime } from "react-intl"; const MinuteInMs = 1_000 * 60; const HourInMs = MinuteInMs * 60; @@ -19,10 +18,11 @@ export default function NoteTime(props: NoteTimeProps) { }).format(from); const fromDate = new Date(from); const isoDate = fromDate.toISOString(); - const ago = new Date().getTime() - from; - const absAgo = Math.abs(ago); function calcTime() { + const fromDate = new Date(from); + const ago = new Date().getTime() - from; + const absAgo = Math.abs(ago); if (absAgo > DayInMs) { return fromDate.toLocaleDateString(undefined, { year: "2-digit", @@ -38,7 +38,7 @@ export default function NoteTime(props: NoteTimeProps) { } else if (absAgo < MinuteInMs) { return fallback; } else { - let mins = Math.floor(absAgo / MinuteInMs); + const mins = Math.floor(absAgo / MinuteInMs); if (ago < 0) { return `in ${mins}m`; } @@ -48,9 +48,9 @@ export default function NoteTime(props: NoteTimeProps) { useEffect(() => { setTime(calcTime()); - let t = setInterval(() => { + const t = setInterval(() => { setTime((s) => { - let newTime = calcTime(); + const newTime = calcTime(); if (newTime !== s) { return newTime; } diff --git a/src/Element/NoteToSelf.tsx b/src/Element/NoteToSelf.tsx index dc1949d..a701238 100644 --- a/src/Element/NoteToSelf.tsx +++ b/src/Element/NoteToSelf.tsx @@ -16,7 +16,7 @@ export interface NoteToSelfProps { link?: string; } -function NoteLabel({ pubkey, link }: NoteToSelfProps) { +function NoteLabel({ pubkey }: NoteToSelfProps) { const user = useUserProfile(pubkey); return (
diff --git a/src/Element/ProfileImage.tsx b/src/Element/ProfileImage.tsx index 0048fee..db123b0 100644 --- a/src/Element/ProfileImage.tsx +++ b/src/Element/ProfileImage.tsx @@ -63,10 +63,10 @@ export function getDisplayName( pubkey: HexKey ) { let name = hexToBech32("npub", pubkey).substring(0, 12); - if ((user?.display_name?.length ?? 0) > 0) { - name = user!.display_name!; - } else if ((user?.name?.length ?? 0) > 0) { - name = user!.name!; + if (user?.display_name !== undefined && user.display_name.length > 0) { + name = user.display_name; + } else if (user?.name !== undefined && user.name.length > 0) { + name = user.name; } return name; } diff --git a/src/Element/ProxyImg.tsx b/src/Element/ProxyImg.tsx index 8a63b40..7f7d819 100644 --- a/src/Element/ProxyImg.tsx +++ b/src/Element/ProxyImg.tsx @@ -1,7 +1,15 @@ import useImgProxy from "Feed/ImgProxy"; import { useEffect, useState } from "react"; -export const ProxyImg = (props: any) => { +interface ProxyImgProps + extends React.DetailedHTMLProps< + React.ImgHTMLAttributes, + HTMLImageElement + > { + size?: number; +} + +export const ProxyImg = (props: ProxyImgProps) => { const { src, size, ...rest } = props; const [url, setUrl] = useState(); const { proxy } = useImgProxy(); diff --git a/src/Element/QrCode.tsx b/src/Element/QrCode.tsx index b5586d3..7843066 100644 --- a/src/Element/QrCode.tsx +++ b/src/Element/QrCode.tsx @@ -15,7 +15,7 @@ export default function QrCode(props: QrCodeProps) { useEffect(() => { if ((props.data?.length ?? 0) > 0 && qrRef.current) { - let qr = new QRCodeStyling({ + const qr = new QRCodeStyling({ width: props.width || 256, height: props.height || 256, data: props.data, @@ -35,9 +35,9 @@ export default function QrCode(props: QrCodeProps) { qrRef.current.innerHTML = ""; qr.append(qrRef.current); if (props.link) { - qrRef.current.onclick = function (e) { - let elm = document.createElement("a"); - elm.href = props.link!; + qrRef.current.onclick = function () { + const elm = document.createElement("a"); + elm.href = props.link ?? ""; elm.click(); }; } @@ -46,10 +46,5 @@ export default function QrCode(props: QrCodeProps) { } }, [props.data, props.link]); - return ( -
- ); + return
; } diff --git a/src/Element/Relay.tsx b/src/Element/Relay.tsx index badb6bb..ed486a6 100644 --- a/src/Element/Relay.tsx +++ b/src/Element/Relay.tsx @@ -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 ( <>
@@ -104,7 +104,10 @@ export default function Relay(props: RelayProps) { {state?.disconnects}
- navigate(state!.id)}> + navigate(state?.id ?? "")} + >
diff --git a/src/Element/SendSats.tsx b/src/Element/SendSats.tsx index 3471953..5d2a69b 100644 --- a/src/Element/SendSats.tsx +++ b/src/Element/SendSats.tsx @@ -50,7 +50,7 @@ export interface LNURLTipProps { } export default function LNURLTip(props: LNURLTipProps) { - const onClose = props.onClose || (() => {}); + const onClose = props.onClose || (() => undefined); const service = props.svc; const show = props.show || false; const { note, author, target } = props; @@ -83,7 +83,7 @@ export default function LNURLTip(props: LNURLTipProps) { useEffect(() => { if (show && !props.invoice) { loadService() - .then((a) => setPayService(a!)) + .then((a) => setPayService(a ?? undefined)) .catch(() => setError(formatMessage(messages.LNURLFail))); } else { setPayService(undefined); @@ -97,25 +97,14 @@ export default function LNURLTip(props: LNURLTipProps) { const serviceAmounts = useMemo(() => { if (payService) { - let min = (payService.minSendable ?? 0) / 1000; - let max = (payService.maxSendable ?? 0) / 1000; + 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) { - 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]); + // TODO Why was this never used? I think this might be a bug, or was it just an oversight? const selectAmount = (a: number) => { setError(undefined); @@ -124,9 +113,9 @@ export default function LNURLTip(props: LNURLTipProps) { }; async function fetchJson(url: string) { - let rsp = await fetch(url); + const rsp = await fetch(url); if (rsp.ok) { - let data: T = await rsp.json(); + const data: T = await rsp.json(); console.log(data); setError(undefined); return data; @@ -136,12 +125,12 @@ export default function LNURLTip(props: LNURLTipProps) { async function loadService(): Promise { if (service) { - let isServiceUrl = service.toLowerCase().startsWith("lnurl"); + const isServiceUrl = service.toLowerCase().startsWith("lnurl"); if (isServiceUrl) { - let serviceUrl = bech32ToText(service); + const serviceUrl = bech32ToText(service); return await fetchJson(serviceUrl); } else { - let ns = service.split("@"); + const ns = service.split("@"); 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}`; } try { - let rsp = await fetch(url); + const rsp = await fetch(url); if (rsp.ok) { - let data = await rsp.json(); + const data = await rsp.json(); console.log(data); if (data.status === "ERROR") { setError(data.reason); @@ -185,8 +174,8 @@ export default function LNURLTip(props: LNURLTipProps) { } function custom() { - let min = (payService?.minSendable ?? 1000) / 1000; - let max = (payService?.maxSendable ?? 21_000_000_000) / 1000; + const min = (payService?.minSendable ?? 1000) / 1000; + const max = (payService?.maxSendable ?? 21_000_000_000) / 1000; return (
selectAmount(customAmount!)} + disabled={!customAmount} + onClick={() => selectAmount(customAmount ?? 0)} > @@ -213,13 +202,15 @@ export default function LNURLTip(props: LNURLTipProps) { async function payWebLNIfEnabled(invoice: LNURLInvoice) { try { if (webln?.enabled) { - let res = await webln.sendPayment(invoice!.pr); + const res = await webln.sendPayment(invoice?.pr ?? ""); console.log(res); - setSuccess(invoice!.successAction || {}); + setSuccess(invoice?.successAction ?? {}); } - } catch (e: any) { - setError(e.toString()); + } catch (e: unknown) { console.warn(e); + if (e instanceof Error) { + setError(e.toString()); + } } } diff --git a/src/Element/Text.tsx b/src/Element/Text.tsx index 27f19c5..2e249af 100644 --- a/src/Element/Text.tsx +++ b/src/Element/Text.tsx @@ -5,7 +5,7 @@ import ReactMarkdown from "react-markdown"; import { visit, SKIP } from "unist-util-visit"; import { UrlRegex, MentionRegex, InvoiceRegex, HashtagRegex } from "Const"; -import { eventLink, hexToBech32 } from "Util"; +import { eventLink, hexToBech32, unwrap } from "Util"; import Invoice from "Element/Invoice"; import Hashtag from "Element/Hashtag"; @@ -14,11 +14,12 @@ import { MetadataCache } from "State/Users"; import Mention from "Element/Mention"; import HyperText from "Element/HyperText"; import { HexKey } from "Nostr"; +import * as unist from "unist"; -export type Fragment = string | JSX.Element; +export type Fragment = string | React.ReactNode; export interface TextFragment { - body: Fragment[]; + body: React.ReactNode[]; tags: Tag[]; users: Map; } @@ -52,24 +53,24 @@ export default function Text({ content, tags, creator, users }: TextProps) { .map((f) => { if (typeof f === "string") { return f.split(MentionRegex).map((match) => { - let matchTag = match.match(/#\[(\d+)\]/); + const matchTag = match.match(/#\[(\d+)\]/); if (matchTag && matchTag.length === 2) { - let idx = parseInt(matchTag[1]); - let ref = frag.tags?.find((a) => a.Index === idx); + const idx = parseInt(matchTag[1]); + const ref = frag.tags?.find((a) => a.Index === idx); if (ref) { switch (ref.Key) { case "p": { - return ; + return ; } case "e": { - let eText = hexToBech32("note", ref.Event!).substring( - 0, - 12 - ); + const eText = hexToBech32( + "note", + ref.Event ?? "" + ).substring(0, 12); return ( e.stopPropagation()} > #{eText} @@ -77,7 +78,7 @@ export default function Text({ content, tags, creator, users }: TextProps) { ); } case "t": { - return ; + return ; } } } @@ -127,7 +128,7 @@ export default function Text({ content, tags, creator, users }: TextProps) { } function transformLi(frag: TextFragment) { - let fragments = transformText(frag); + const fragments = transformText(frag); return
  • {fragments}
  • ; } @@ -140,9 +141,6 @@ export default function Text({ content, tags, creator, users }: TextProps) { } function transformText(frag: TextFragment) { - if (frag.body === undefined) { - debugger; - } let fragments = extractMentions(frag); fragments = extractLinks(fragments); fragments = extractInvoices(fragments); @@ -152,15 +150,22 @@ export default function Text({ content, tags, creator, users }: TextProps) { const components = useMemo(() => { return { - p: (x: any) => + p: (x: { children?: React.ReactNode[] }) => transformParagraph({ body: x.children ?? [], tags, users }), - a: (x: any) => , - li: (x: any) => transformLi({ body: x.children ?? [], tags, users }), + a: (x: { href?: string }) => ( + + ), + li: (x: { children?: Fragment[] }) => + transformLi({ body: x.children ?? [], tags, users }), }; }, [content]); + interface Node extends unist.Node { + value: string; + } + const disableMarkdownLinks = useCallback( - () => (tree: any) => { + () => (tree: Node) => { visit(tree, (node, index, parent) => { if ( parent && @@ -172,8 +177,9 @@ export default function Text({ content, tags, creator, users }: TextProps) { node.type === "definition") ) { node.type = "text"; + const position = unwrap(node.position); node.value = content - .slice(node.position.start.offset, node.position.end.offset) + .slice(position.start.offset, position.end.offset) .replace(/\)$/, " )"); return SKIP; } diff --git a/src/Element/Textarea.tsx b/src/Element/Textarea.tsx index 36463ba..6502180 100644 --- a/src/Element/Textarea.tsx +++ b/src/Element/Textarea.tsx @@ -2,7 +2,7 @@ import "@webscopeio/react-textarea-autocomplete/style.css"; import "./Textarea.css"; import { useState } from "react"; -import { useIntl, FormattedMessage } from "react-intl"; +import { useIntl } from "react-intl"; import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; import emoji from "@jukben/emoji-search"; import TextareaAutosize from "react-textarea-autosize"; @@ -30,7 +30,7 @@ const EmojiItem = ({ entity: { name, char } }: { entity: EmojiItemProps }) => { }; const UserItem = (metadata: MetadataCache) => { - const { pubkey, display_name, picture, nip05, ...rest } = metadata; + const { pubkey, display_name, nip05, ...rest } = metadata; return (
    @@ -44,7 +44,15 @@ const UserItem = (metadata: MetadataCache) => { ); }; -const Textarea = ({ users, onChange, ...rest }: any) => { +interface TextareaProps { + autoFocus: boolean; + className: string; + onChange(ev: React.ChangeEvent): void; + value: string; + onFocus(): void; +} + +const Textarea = (props: TextareaProps) => { const [query, setQuery] = useState(""); const { formatMessage } = useIntl(); @@ -52,7 +60,7 @@ const Textarea = ({ users, onChange, ...rest }: any) => { const userDataProvider = (token: string) => { setQuery(token); - return allUsers; + return allUsers ?? []; }; const emojiDataProvider = (token: string) => { @@ -62,23 +70,26 @@ const Textarea = ({ users, onChange, ...rest }: any) => { }; return ( + // @ts-expect-error If anybody can figure out how to type this, please do Loading....} + {...props} + loadingComponent={() => Loading...} placeholder={formatMessage(messages.NotePlaceholder)} - onChange={onChange} textAreaComponent={TextareaAutosize} trigger={{ ":": { dataProvider: emojiDataProvider, component: EmojiItem, - output: (item: EmojiItemProps, trigger) => item.char, + output: (item: EmojiItemProps) => item.char, }, "@": { afterWhitespace: true, dataProvider: userDataProvider, - component: (props: any) => , - output: (item: any) => `@${hexToBech32("npub", item.pubkey)}`, + component: (props: { entity: MetadataCache }) => ( + + ), + output: (item: { pubkey: string }) => + `@${hexToBech32("npub", item.pubkey)}`, }, }} /> diff --git a/src/Element/Thread.tsx b/src/Element/Thread.tsx index 9c9f20d..2645a84 100644 --- a/src/Element/Thread.tsx +++ b/src/Element/Thread.tsx @@ -6,19 +6,18 @@ import { useNavigate, useLocation, Link } from "react-router-dom"; import { TaggedRawEvent, u256, HexKey } from "Nostr"; import { default as NEvent } from "Nostr/Event"; import EventKind from "Nostr/EventKind"; -import { eventLink, hexToBech32, bech32ToHex } from "Util"; +import { eventLink, bech32ToHex, unwrap } from "Util"; import BackButton from "Element/BackButton"; import Note from "Element/Note"; import NoteGhost from "Element/NoteGhost"; import Collapsed from "Element/Collapsed"; - import messages from "./messages"; function getParent( ev: HexKey, chains: Map ): HexKey | undefined { - for (let [k, vs] of chains.entries()) { + for (const [k, vs] of chains.entries()) { const fs = vs.map((a) => a.Id); if (fs.includes(ev)) { return k; @@ -53,7 +52,6 @@ interface SubthreadProps { const Subthread = ({ active, path, - from, notes, related, chains, @@ -332,20 +330,19 @@ export default function Thread(props: ThreadProps) { const location = useLocation(); const urlNoteId = location?.pathname.slice(3); const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId); - const rootNoteId = root && hexToBech32("note", root.Id); const chains = useMemo(() => { - let chains = new Map(); + const chains = new Map(); parsedNotes ?.filter((a) => a.Kind === EventKind.TextNote) .sort((a, b) => b.CreatedAt - a.CreatedAt) .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 (!chains.has(replyTo)) { chains.set(replyTo, [v]); } else { - chains.get(replyTo)!.push(v); + unwrap(chains.get(replyTo)).push(v); } } else if (v.Tags.length > 0) { console.log("Not replying to anything: ", v); @@ -370,7 +367,7 @@ export default function Thread(props: ThreadProps) { return; } - let subthreadPath = []; + const subthreadPath = []; let parent = getParent(urlNoteHex, chains); while (parent) { subthreadPath.unshift(parent); @@ -414,7 +411,7 @@ export default function Thread(props: ThreadProps) { if (!from || !chains) { return; } - let replies = chains.get(from); + const replies = chains.get(from); if (replies) { return ( ): NEvent[] { if (!from || !chains) { return []; } - let replies = chains.get(from); + const replies = chains.get(from); return replies ? replies : []; } diff --git a/src/Element/TidalEmbed.tsx b/src/Element/TidalEmbed.tsx index 2d25517..f838f0a 100644 --- a/src/Element/TidalEmbed.tsx +++ b/src/Element/TidalEmbed.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { TidalRegex } from "Const"; // Re-use dom parser across instances of TidalEmbed diff --git a/src/Element/Timeline.tsx b/src/Element/Timeline.tsx index 6317b30..5c9f507 100644 --- a/src/Element/Timeline.tsx +++ b/src/Element/Timeline.tsx @@ -83,7 +83,7 @@ export default function Timeline({ } case EventKind.Reaction: 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 ( section.name === "amount" + (section: Section) => section.name === "amount" )?.value; const hash = decoded.sections.find( - (section: any) => section.name === "description_hash" + (section: Section) => section.name === "description_hash" )?.value; return { amount, hash: hash ? bytesToHex(hash) : undefined }; @@ -72,7 +76,7 @@ export function parseZap(zap: TaggedRawEvent): ParsedZap { const { amount, hash } = getInvoice(zap); const zapper = hash ? getZapper(zap, hash) : { isValid: false }; const e = findTag(zap, "e"); - const p = findTag(zap, "p")!; + const p = unwrap(findTag(zap, "p")); return { id: zap.id, e, diff --git a/src/Element/ZapButton.tsx b/src/Element/ZapButton.tsx index 41d2f2e..20ed724 100644 --- a/src/Element/ZapButton.tsx +++ b/src/Element/ZapButton.tsx @@ -6,8 +6,8 @@ import { useUserProfile } from "Feed/ProfileFeed"; import { HexKey } from "Nostr"; import SendSats from "Element/SendSats"; -const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => { - const profile = useUserProfile(pubkey!); +const ZapButton = ({ pubkey, svc }: { pubkey: HexKey; svc?: string }) => { + const profile = useUserProfile(pubkey); const [zap, setZap] = useState(false); const service = svc ?? (profile?.lud16 || profile?.lud06); @@ -15,7 +15,7 @@ const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => { return ( <> -
    setZap(true)}> +
    setZap(true)}>
    { if (hasNip07 && !privKey) { ev.Id = await ev.CreateId(); - let tmpEv = await barierNip07(() => + const tmpEv = (await barrierNip07(() => window.nostr.signEvent(ev.ToObject()) - ); + )) as TaggedRawEvent; + if (!tmpEv.relays) { + tmpEv.relays = []; + } return new NEvent(tmpEv); } else if (privKey) { await ev.Sign(privKey); @@ -111,14 +115,14 @@ export default function useEventPublisher() { */ broadcastForBootstrap: (ev: NEvent | undefined) => { if (ev) { - for (let [k, _] of DefaultRelays) { + for (const [k] of DefaultRelays) { System.WriteOnceToRelay(k, ev); } } }, muted: async (keys: HexKey[], priv: HexKey[]) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.Lists; ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length)); keys.forEach((p) => { @@ -129,7 +133,7 @@ export default function useEventPublisher() { const ps = priv.map((p) => ["p", p]); const plaintext = JSON.stringify(ps); if (hasNip07 && !privKey) { - content = await barierNip07(() => + content = await barrierNip07(() => window.nostr.nip04.encrypt(pubKey, plaintext) ); } else if (privKey) { @@ -142,7 +146,7 @@ export default function useEventPublisher() { }, metadata: async (obj: UserMetadata) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.SetMetadata; ev.Content = JSON.stringify(obj); return await signEvent(ev); @@ -150,7 +154,7 @@ export default function useEventPublisher() { }, note: async (msg: string) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.TextNote; processContent(ev, msg); return await signEvent(ev); @@ -158,18 +162,14 @@ export default function useEventPublisher() { }, zap: async (author: HexKey, note?: HexKey, msg?: string) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.ZapRequest; if (note) { - // @ts-ignore - ev.Tags.push(new Tag(["e", note])); + ev.Tags.push(new Tag(["e", note], 0)); } - // @ts-ignore - ev.Tags.push(new Tag(["p", author])); - // @ts-ignore + ev.Tags.push(new Tag(["p", author], 0)); const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)]; - // @ts-ignore - ev.Tags.push(new Tag(relayTag)); + ev.Tags.push(new Tag(relayTag, 0)); processContent(ev, msg || ""); return await signEvent(ev); } @@ -179,15 +179,20 @@ export default function useEventPublisher() { */ reply: async (replyTo: NEvent, msg: string) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.TextNote; - let thread = replyTo.Thread; + const thread = replyTo.Thread; if (thread) { if (thread.Root || thread.ReplyTo) { ev.Tags.push( new Tag( - ["e", thread.Root?.Event ?? thread.ReplyTo?.Event!, "", "root"], + [ + "e", + thread.Root?.Event ?? thread.ReplyTo?.Event ?? "", + "", + "root", + ], ev.Tags.length ) ); @@ -199,7 +204,7 @@ export default function useEventPublisher() { 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) { continue; // dont tag self in replies } @@ -218,7 +223,7 @@ export default function useEventPublisher() { }, react: async (evRef: NEvent, content = "+") => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.Reaction; ev.Content = content; ev.Tags.push(new Tag(["e", evRef.Id], 0)); @@ -228,10 +233,10 @@ export default function useEventPublisher() { }, saveRelays: async () => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.ContactList; 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)); } @@ -243,16 +248,16 @@ export default function useEventPublisher() { newRelays?: Record ) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.ContactList; ev.Content = JSON.stringify(newRelays ?? relays); - let temp = new Set(follows); + const temp = new Set(follows); if (Array.isArray(pkAdd)) { pkAdd.forEach((a) => temp.add(a)); } else { temp.add(pkAdd); } - for (let pk of temp) { + for (const pk of temp) { if (pk.length !== 64) { continue; } @@ -264,10 +269,10 @@ export default function useEventPublisher() { }, removeFollow: async (pkRemove: HexKey) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.ContactList; ev.Content = JSON.stringify(relays); - for (let pk of follows) { + for (const pk of follows) { if (pk === pkRemove || pk.length !== 64) { continue; } @@ -282,7 +287,7 @@ export default function useEventPublisher() { */ delete: async (id: u256) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.Deletion; ev.Content = ""; 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) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.Repost; ev.Content = JSON.stringify(note.Original); ev.Tags.push(new Tag(["e", note.Id], 0)); @@ -311,12 +316,12 @@ export default function useEventPublisher() { return ""; } try { - let otherPubKey = + const otherPubKey = note.PubKey === pubKey - ? note.Tags.filter((a) => a.Key === "p")[0].PubKey! + ? unwrap(note.Tags.filter((a) => a.Key === "p")[0].PubKey) : note.PubKey; if (hasNip07 && !privKey) { - return await barierNip07(() => + return await barrierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.Content) ); } else if (privKey) { @@ -324,21 +329,21 @@ export default function useEventPublisher() { return note.Content; } } catch (e) { - console.error("Decyrption failed", e); + console.error("Decryption failed", e); return ""; } } }, sendDm: async (content: string, to: HexKey) => { if (pubKey) { - let ev = NEvent.ForPubKey(pubKey); + const ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.DirectMessage; ev.Content = content; ev.Tags.push(new Tag(["p", to], 0)); try { if (hasNip07 && !privKey) { - let cx: string = await barierNip07(() => + const cx: string = await barrierNip07(() => window.nostr.nip04.encrypt(to, content) ); ev.Content = cx; @@ -358,12 +363,12 @@ export default function useEventPublisher() { let isNip07Busy = false; const delay = (t: number) => { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { setTimeout(resolve, t); }); }; -export const barierNip07 = async (then: () => Promise) => { +export const barrierNip07 = async (then: () => Promise): Promise => { while (isNip07Busy) { await delay(10); } diff --git a/src/Feed/FollowersFeed.ts b/src/Feed/FollowersFeed.ts index 8c302fa..ebb2880 100644 --- a/src/Feed/FollowersFeed.ts +++ b/src/Feed/FollowersFeed.ts @@ -6,7 +6,7 @@ import useSubscription from "Feed/Subscription"; export default function useFollowersFeed(pubkey: HexKey) { const sub = useMemo(() => { - let x = new Subscriptions(); + const x = new Subscriptions(); x.Id = `followers:${pubkey.slice(0, 12)}`; x.Kinds = new Set([EventKind.ContactList]); x.PTags = new Set([pubkey]); diff --git a/src/Feed/FollowsFeed.ts b/src/Feed/FollowsFeed.ts index bd8a833..f948bf0 100644 --- a/src/Feed/FollowsFeed.ts +++ b/src/Feed/FollowsFeed.ts @@ -6,7 +6,7 @@ import useSubscription, { NoteStore } from "Feed/Subscription"; export default function useFollowsFeed(pubkey: HexKey) { const sub = useMemo(() => { - let x = new Subscriptions(); + const x = new Subscriptions(); x.Id = `follows:${pubkey.slice(0, 12)}`; x.Kinds = new Set([EventKind.ContactList]); x.Authors = new Set([pubkey]); @@ -18,10 +18,10 @@ export default function useFollowsFeed(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 ); - let pTags = contactLists?.map((a) => + const pTags = contactLists?.map((a) => a.tags.filter((b) => b[0] === "p").map((c) => c[1]) ); return [...new Set(pTags?.flat())]; diff --git a/src/Feed/ImgProxy.ts b/src/Feed/ImgProxy.ts index be28272..491679a 100644 --- a/src/Feed/ImgProxy.ts +++ b/src/Feed/ImgProxy.ts @@ -2,6 +2,7 @@ import * as secp from "@noble/secp256k1"; import * as base64 from "@protobufjs/base64"; import { useSelector } from "react-redux"; import { RootState } from "State/Store"; +import { unwrap } from "Util"; export interface ImgProxySettings { url: string; @@ -21,8 +22,8 @@ export default function useImgProxy() { async function signUrl(u: string) { const result = await secp.utils.hmacSha256( - secp.utils.hexToBytes(settings!.key), - secp.utils.hexToBytes(settings!.salt), + secp.utils.hexToBytes(unwrap(settings).key), + secp.utils.hexToBytes(unwrap(settings).salt), te.encode(u) ); return urlSafe(base64.encode(result, 0, result.byteLength)); diff --git a/src/Feed/LoginFeed.ts b/src/Feed/LoginFeed.ts index a9e55fb..e7e1f94 100644 --- a/src/Feed/LoginFeed.ts +++ b/src/Feed/LoginFeed.ts @@ -19,9 +19,10 @@ import { RootState } from "State/Store"; import { mapEventToProfile, MetadataCache } from "State/Users"; import { useDb } from "State/Users/Db"; import useSubscription from "Feed/Subscription"; -import { barierNip07 } from "Feed/EventPublisher"; +import { barrierNip07 } from "Feed/EventPublisher"; import { getMutedKeys, getNewest } from "Feed/MuteList"; import useModeration from "Hooks/useModeration"; +import { unwrap } from "Util"; /** * Managed loading data for the current logged in user @@ -40,7 +41,7 @@ export default function useLoginFeed() { const subMetadata = useMemo(() => { if (!pubKey) return null; - let sub = new Subscriptions(); + const sub = new Subscriptions(); sub.Id = `login:meta`; sub.Authors = new Set([pubKey]); sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]); @@ -52,7 +53,7 @@ export default function useLoginFeed() { const subNotification = useMemo(() => { if (!pubKey) return null; - let sub = new Subscriptions(); + const sub = new Subscriptions(); sub.Id = "login:notifications"; // todo: add zaps sub.Kinds = new Set([EventKind.TextNote]); @@ -64,7 +65,7 @@ export default function useLoginFeed() { const subMuted = useMemo(() => { if (!pubKey) return null; - let sub = new Subscriptions(); + const sub = new Subscriptions(); sub.Id = "login:muted"; sub.Kinds = new Set([EventKind.Lists]); sub.Authors = new Set([pubKey]); @@ -77,12 +78,12 @@ export default function useLoginFeed() { const subDms = useMemo(() => { if (!pubKey) return null; - let dms = new Subscriptions(); + const dms = new Subscriptions(); dms.Id = "login:dms"; dms.Kinds = new Set([EventKind.DirectMessage]); dms.PTags = new Set([pubKey]); - let dmsFromME = new Subscriptions(); + const dmsFromME = new Subscriptions(); dmsFromME.Authors = new Set([pubKey]); dmsFromME.Kinds = new Set([EventKind.DirectMessage]); dms.AddSubscription(dmsFromME); @@ -102,28 +103,28 @@ export default function useLoginFeed() { const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true }); useEffect(() => { - let contactList = metadataFeed.store.notes.filter( + const contactList = metadataFeed.store.notes.filter( (a) => a.kind === EventKind.ContactList ); - let metadata = metadataFeed.store.notes.filter( + const metadata = metadataFeed.store.notes.filter( (a) => a.kind === EventKind.SetMetadata ); - let profiles = metadata + const profiles = metadata .map((a) => mapEventToProfile(a)) .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 !== "{}") { - let relays = JSON.parse(cl.content); + const relays = JSON.parse(cl.content); 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 })); } (async () => { - let maxProfile = profiles.reduce( + const maxProfile = profiles.reduce( (acc, v) => { if (v.created > acc.created) { acc.profile = v; @@ -134,7 +135,7 @@ export default function useLoginFeed() { { created: 0, profile: null as MetadataCache | null } ); 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) { await db.put(maxProfile.profile); } @@ -153,7 +154,7 @@ export default function useLoginFeed() { dispatch(setLatestNotifications(nx.created_at)); makeNotification(db, nx).then((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)); } }); @@ -176,8 +177,8 @@ export default function useLoginFeed() { try { const blocked = JSON.parse(plaintext); const keys = blocked - .filter((p: any) => p && p.length === 2 && p[0] === "p") - .map((p: any) => p[1]); + .filter((p: string) => p && p.length === 2 && p[0] === "p") + .map((p: string) => p[1]); dispatch( setBlocked({ keys, @@ -193,7 +194,7 @@ export default function useLoginFeed() { }, [dispatch, mutedFeed.store]); useEffect(() => { - let dms = dmsFeed.store.notes.filter( + const dms = dmsFeed.store.notes.filter( (a) => a.kind === EventKind.DirectMessage ); dispatch(addDirectMessage(dms)); @@ -209,7 +210,7 @@ async function decryptBlocked( if (pubKey && privKey) { return await ev.DecryptData(raw.content, privKey, pubKey); } else { - return await barierNip07(() => + return await barrierNip07(() => window.nostr.nip04.decrypt(pubKey, raw.content) ); } diff --git a/src/Feed/MuteList.ts b/src/Feed/MuteList.ts index 142d5a2..62679d9 100644 --- a/src/Feed/MuteList.ts +++ b/src/Feed/MuteList.ts @@ -7,7 +7,7 @@ import useSubscription, { NoteStore } from "Feed/Subscription"; export default function useMutedFeed(pubkey: HexKey) { const sub = useMemo(() => { - let sub = new Subscriptions(); + const sub = new Subscriptions(); sub.Id = `muted:${pubkey.slice(0, 12)}`; sub.Kinds = new Set([EventKind.Lists]); sub.Authors = new Set([pubkey]); @@ -44,7 +44,7 @@ export function getMutedKeys(rawNotes: TaggedRawEvent[]): { } 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 ); return getMutedKeys(lists).keys; diff --git a/src/Feed/RelayState.ts b/src/Feed/RelayState.ts index c7de65e..70141c5 100644 --- a/src/Feed/RelayState.ts +++ b/src/Feed/RelayState.ts @@ -1,16 +1,16 @@ import { useSyncExternalStore } from "react"; import { System } from "Nostr/System"; -import { CustomHook, StateSnapshot } from "Nostr/Connection"; +import { StateSnapshot } from "Nostr/Connection"; -const noop = (f: CustomHook) => { - return () => {}; +const noop = () => { + return () => undefined; }; const noopState = (): StateSnapshot | undefined => { return undefined; }; export default function useRelayState(addr: string) { - let c = System.Sockets.get(addr); + const c = System.Sockets.get(addr); return useSyncExternalStore( c?.StatusHook.bind(c) ?? noop, c?.GetState.bind(c) ?? noopState diff --git a/src/Feed/Subscription.ts b/src/Feed/Subscription.ts index 69f3925..79b1aba 100644 --- a/src/Feed/Subscription.ts +++ b/src/Feed/Subscription.ts @@ -2,7 +2,7 @@ import { useEffect, useMemo, useReducer, useState } from "react"; import { System } from "Nostr/System"; import { TaggedRawEvent } from "Nostr"; import { Subscriptions } from "Nostr/Subscriptions"; -import { debounce } from "Util"; +import { debounce, unwrap } from "Util"; import { db } from "Db"; export type NoteStore = { @@ -17,7 +17,7 @@ export type UseSubscriptionOptions = { interface ReducerArg { type: "END" | "EVENT" | "CLEAR"; - ev?: TaggedRawEvent | Array; + ev?: TaggedRawEvent | TaggedRawEvent[]; end?: boolean; } @@ -25,7 +25,7 @@ function notesReducer(state: NoteStore, arg: ReducerArg) { if (arg.type === "END") { return { notes: state.notes, - end: arg.end!, + end: arg.end ?? false, } as NoteStore; } @@ -36,11 +36,11 @@ function notesReducer(state: NoteStore, arg: ReducerArg) { } as NoteStore; } - let evs = arg.ev!; - if (!Array.isArray(evs)) { - evs = [evs]; + let evs = arg.ev; + if (!(evs instanceof Array)) { + 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)); if (evs.length === 0) { return state; @@ -175,7 +175,7 @@ const PreloadNotes = async (id: string): Promise => { const feed = await db.feeds.get(id); if (feed) { 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 []; }; diff --git a/src/Feed/ThreadFeed.ts b/src/Feed/ThreadFeed.ts index 93dcd25..0bca181 100644 --- a/src/Feed/ThreadFeed.ts +++ b/src/Feed/ThreadFeed.ts @@ -16,9 +16,9 @@ export default function useThreadFeed(id: u256) { function addId(id: u256[]) { setTrackingEvent((s) => { - let orig = new Set(s); + const orig = new Set(s); if (id.some((a) => !orig.has(a))) { - let tmp = new Set([...s, ...id]); + const tmp = new Set([...s, ...id]); return Array.from(tmp); } else { return s; @@ -55,16 +55,16 @@ export default function useThreadFeed(id: u256) { useEffect(() => { if (main.store) { return debounce(200, () => { - let mainNotes = main.store.notes.filter( + const mainNotes = main.store.notes.filter( (a) => a.kind === EventKind.TextNote ); - let eTags = mainNotes + const eTags = mainNotes .filter((a) => a.kind === EventKind.TextNote) .map((a) => a.tags.filter((b) => b[0] === "e").map((b) => b[1])) .flat(); - let ids = mainNotes.map((a) => a.id); - let allEvents = new Set([...eTags, ...ids]); + const ids = mainNotes.map((a) => a.id); + const allEvents = new Set([...eTags, ...ids]); addId(Array.from(allEvents)); }); } diff --git a/src/Feed/TimelineFeed.ts b/src/Feed/TimelineFeed.ts index ba7630e..dfc7057 100644 --- a/src/Feed/TimelineFeed.ts +++ b/src/Feed/TimelineFeed.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { u256 } from "Nostr"; import EventKind from "Nostr/EventKind"; import { Subscriptions } from "Nostr/Subscriptions"; -import { unixNow } from "Util"; +import { unixNow, unwrap } from "Util"; import useSubscription from "Feed/Subscription"; import { useSelector } from "react-redux"; import { RootState } from "State/Store"; @@ -38,7 +38,7 @@ export default function useTimelineFeed( return null; } - let sub = new Subscriptions(); + const sub = new Subscriptions(); sub.Id = `timeline:${subject.type}:${subject.discriminator}`; sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]); switch (subject.type) { @@ -64,7 +64,7 @@ export default function useTimelineFeed( }, [subject.type, subject.items, subject.discriminator]); const sub = useMemo(() => { - let sub = createSub(); + const sub = createSub(); if (sub) { if (options.method === "LIMIT_UNTIL") { sub.Until = until; @@ -80,7 +80,7 @@ export default function useTimelineFeed( if (pref.autoShowLatest) { // copy properties of main sub but with limit 0 // this will put latest directly into main feed - let latestSub = new Subscriptions(); + const latestSub = new Subscriptions(); latestSub.Authors = sub.Authors; latestSub.HashTags = sub.HashTags; latestSub.PTags = sub.PTags; @@ -97,7 +97,7 @@ export default function useTimelineFeed( const main = useSubscription(sub, { leaveOpen: true, cache: true }); const subRealtime = useMemo(() => { - let subLatest = createSub(); + const subLatest = createSub(); if (subLatest && !pref.autoShowLatest) { subLatest.Id = `${subLatest.Id}:latest`; subLatest.Limit = 1; @@ -131,7 +131,7 @@ export default function useTimelineFeed( const subParents = useMemo(() => { if (trackingParentEvents.length > 0) { - let parents = new Subscriptions(); + const parents = new Subscriptions(); parents.Id = `timeline-parent:${subject.type}`; parents.Ids = new Set(trackingParentEvents); return parents; @@ -144,21 +144,21 @@ export default function useTimelineFeed( useEffect(() => { if (main.store.notes.length > 0) { 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))) { return Array.from(new Set([...s, ...ids])); } return s; }); - let reposts = main.store.notes + const reposts = main.store.notes .filter((a) => a.kind === EventKind.Repost && a.content === "") .map((a) => a.tags.find((b) => b[0] === "e")) .filter((a) => a) - .map((a) => a![1]); + .map((a) => unwrap(a)[1]); if (reposts.length > 0) { setTrackingParentEvents((s) => { if (reposts.some((a) => !s.includes(a))) { - let temp = new Set([...s, ...reposts]); + const temp = new Set([...s, ...reposts]); return Array.from(temp); } return s; @@ -175,7 +175,7 @@ export default function useTimelineFeed( loadMore: () => { console.debug("Timeline load more!"); 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), unixNow() ); diff --git a/src/Feed/ZapsFeed.ts b/src/Feed/ZapsFeed.ts index 03569e4..4880168 100644 --- a/src/Feed/ZapsFeed.ts +++ b/src/Feed/ZapsFeed.ts @@ -6,7 +6,7 @@ import useSubscription from "./Subscription"; export default function useZapsFeed(pubkey: HexKey) { const sub = useMemo(() => { - let x = new Subscriptions(); + const x = new Subscriptions(); x.Id = `zaps:${pubkey.slice(0, 12)}`; x.Kinds = new Set([EventKind.ZapReceipt]); x.PTags = new Set([pubkey]); diff --git a/src/Hooks/useHorizontalScroll.tsx b/src/Hooks/useHorizontalScroll.tsx index 72208f5..8af753c 100644 --- a/src/Hooks/useHorizontalScroll.tsx +++ b/src/Hooks/useHorizontalScroll.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, WheelEvent, LegacyRef } from "react"; +import { useEffect, useRef, LegacyRef } from "react"; function useHorizontalScroll() { const elRef = useRef(); @@ -10,9 +10,7 @@ function useHorizontalScroll() { ev.preventDefault(); el.scrollTo({ left: el.scrollLeft + ev.deltaY, behavior: "smooth" }); }; - // @ts-ignore el.addEventListener("wheel", onWheel); - // @ts-ignore return () => el.removeEventListener("wheel", onWheel); } }, []); diff --git a/src/Hooks/useWebln.ts b/src/Hooks/useWebln.ts index 2b7a6e0..4ffc144 100644 --- a/src/Hooks/useWebln.ts +++ b/src/Hooks/useWebln.ts @@ -5,7 +5,7 @@ declare global { webln?: { enabled: boolean; enable: () => Promise; - sendPayment: (pr: string) => Promise; + sendPayment: (pr: string) => Promise; }; } } @@ -15,7 +15,7 @@ export default function useWebln(enable = true) { useEffect(() => { if (maybeWebLn && !maybeWebLn.enabled && enable) { - maybeWebLn.enable().catch((error) => { + maybeWebLn.enable().catch(() => { console.debug("Couldn't enable WebLN"); }); } diff --git a/src/Icons/Attachment.tsx b/src/Icons/Attachment.tsx index 943f32c..88c4294 100644 --- a/src/Icons/Attachment.tsx +++ b/src/Icons/Attachment.tsx @@ -1,6 +1,4 @@ -import IconProps from "./IconProps"; - -const Attachment = (props: IconProps) => { +const Attachment = () => { return ( { +const Logout = () => { return ( { return ( ( path: string, method?: "GET" | string, - body?: any, - headers?: any + body?: { [key: string]: string }, + headers?: { [key: string]: string } ): Promise { try { - let rsp = await fetch(`${this.url}${path}`, { + const rsp = await fetch(`${this.url}${path}`, { method: method, body: body ? JSON.stringify(body) : undefined, headers: { @@ -121,7 +121,7 @@ export class ServiceProvider { }, }); - let obj = await rsp.json(); + const obj = await rsp.json(); if ("error" in obj) { return obj; } diff --git a/src/Nostr/Connection.ts b/src/Nostr/Connection.ts index b569e68..bd2d89f 100644 --- a/src/Nostr/Connection.ts +++ b/src/Nostr/Connection.ts @@ -9,6 +9,7 @@ import { RawEvent, TaggedRawEvent, u256 } from "Nostr"; import { RelayInfo } from "./RelayInfo"; import Nips from "./Nips"; import { System } from "./System"; +import { unwrap } from "Util"; export type CustomHook = (state: Readonly) => void; @@ -51,7 +52,7 @@ export default class Connection { LastState: Readonly; IsClosed: boolean; ReconnectTimer: ReturnType | null; - EventsCallback: Map void>; + EventsCallback: Map void>; AwaitingAuth: Map; Authed: boolean; @@ -87,15 +88,15 @@ export default class Connection { async Connect() { try { if (this.Info === undefined) { - let u = new URL(this.Address); - let rsp = await fetch(`https://${u.host}`, { + const u = new URL(this.Address); + const rsp = await fetch(`https://${u.host}`, { headers: { accept: "application/nostr+json", }, }); if (rsp.ok) { - let data = await rsp.json(); - for (let [k, v] of Object.entries(data)) { + const data = await rsp.json(); + for (const [k, v] of Object.entries(data)) { if (v === "unset" || v === "") { data[k] = undefined; } @@ -114,7 +115,7 @@ export default class Connection { this.IsClosed = false; 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.onerror = (e) => this.OnError(e); this.Socket.onclose = (e) => this.OnClose(e); @@ -130,7 +131,7 @@ export default class Connection { this._UpdateState(); } - OnOpen(e: Event) { + OnOpen() { this.ConnectTimeout = DefaultConnectTimeout; this._InitSubscriptions(); console.log(`[${this.Address}] Open!`); @@ -157,10 +158,10 @@ export default class Connection { this._UpdateState(); } - OnMessage(e: MessageEvent) { + OnMessage(e: MessageEvent) { if (e.data.length > 0) { - let msg = JSON.parse(e.data); - let tag = msg[0]; + const msg = JSON.parse(e.data); + const tag = msg[0]; switch (tag) { case "AUTH": { this._OnAuthAsync(msg[1]); @@ -183,7 +184,7 @@ export default class Connection { console.debug("OK: ", msg); const id = msg[1]; if (this.EventsCallback.has(id)) { - let cb = this.EventsCallback.get(id)!; + const cb = unwrap(this.EventsCallback.get(id)); this.EventsCallback.delete(id); cb(msg); } @@ -213,7 +214,7 @@ export default class Connection { if (!this.Settings.write) { return; } - let req = ["EVENT", e.ToObject()]; + const req = ["EVENT", e.ToObject()]; this._SendJson(req); this.Stats.EventsSent++; this._UpdateState(); @@ -222,13 +223,13 @@ export default class Connection { /** * Send event on this connection and wait for OK response */ - async SendAsync(e: NEvent, timeout: number = 5000) { - return new Promise((resolve, reject) => { + async SendAsync(e: NEvent, timeout = 5000) { + return new Promise((resolve) => { if (!this.Settings.write) { resolve(); return; } - let t = setTimeout(() => { + const t = setTimeout(() => { resolve(); }, timeout); this.EventsCallback.set(e.Id, () => { @@ -236,7 +237,7 @@ export default class Connection { resolve(); }); - let req = ["EVENT", e.ToObject()]; + const req = ["EVENT", e.ToObject()]; this._SendJson(req); this.Stats.EventsSent++; this._UpdateState(); @@ -269,7 +270,7 @@ export default class Connection { */ RemoveSubscription(subId: string) { if (this.Subscriptions.has(subId)) { - let req = ["CLOSE", subId]; + const req = ["CLOSE", subId]; this._SendJson(req); this.Subscriptions.delete(subId); return true; @@ -281,7 +282,7 @@ export default class Connection { * Hook status for connection */ StatusHook(fnHook: CustomHook) { - let id = uuid(); + const id = uuid(); this.StateHooks.set(id, fnHook); return () => { this.StateHooks.delete(id); @@ -324,20 +325,20 @@ export default class Connection { } _NotifyState() { - let state = this.GetState(); - for (let [_, h] of this.StateHooks) { + const state = this.GetState(); + for (const [, h] of this.StateHooks) { h(state); } } _InitSubscriptions() { // send pending - for (let p of this.Pending) { + for (const p of this.Pending) { this._SendJson(p); } this.Pending = []; - for (let [_, s] of this.Subscriptions) { + for (const [, s] of this.Subscriptions) { this._SendSubscription(s); } this._UpdateState(); @@ -357,19 +358,20 @@ export default class Connection { this._SendJson(req); } - _SendJson(obj: any) { + _SendJson(obj: Subscriptions | object) { 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); return; } - let json = JSON.stringify(obj); + const json = JSON.stringify(obj); this.Socket.send(json); } _OnEvent(subId: string, ev: RawEvent) { if (this.Subscriptions.has(subId)) { //this._VerifySig(ev); - let tagged: TaggedRawEvent = { + const tagged: TaggedRawEvent = { ...ev, relays: [this.Address], }; @@ -386,18 +388,18 @@ export default class Connection { }; this.AwaitingAuth.set(challenge, true); const authEvent = await System.nip42Auth(challenge, this.Address); - return new Promise((resolve, _) => { + return new Promise((resolve) => { if (!authEvent) { authCleanup(); return Promise.reject("no event"); } - let t = setTimeout(() => { + const t = setTimeout(() => { authCleanup(); resolve(); }, 10_000); - this.EventsCallback.set(authEvent.Id, (msg: any[]) => { + this.EventsCallback.set(authEvent.Id, (msg: boolean[]) => { clearTimeout(t); authCleanup(); if (msg.length > 3 && msg[2] === true) { @@ -407,7 +409,7 @@ export default class Connection { resolve(); }); - let req = ["AUTH", authEvent.ToObject()]; + const req = ["AUTH", authEvent.ToObject()]; this._SendJson(req); this.Stats.EventsSent++; this._UpdateState(); @@ -415,13 +417,13 @@ export default class Connection { } _OnEnd(subId: string) { - let sub = this.Subscriptions.get(subId); + const sub = this.Subscriptions.get(subId); if (sub) { - let now = new Date().getTime(); - let started = sub.Started.get(this.Address); + const now = new Date().getTime(); + const started = sub.Started.get(this.Address); sub.Finished.set(this.Address, now); if (started) { - let responseTime = now - started; + const responseTime = now - started; if (responseTime > 10_000) { console.warn( `[${this.Address}][${subId}] Slow response time ${( @@ -441,14 +443,14 @@ export default class Connection { } _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) { throw "Cannot verify event, no sync sha256 method"; } - let data = secp.utils.sha256Sync(payloadData); - let hash = secp.utils.bytesToHex(data); + const data = secp.utils.sha256Sync(payloadData); + const hash = secp.utils.bytesToHex(data); if (!secp.schnorr.verifySync(ev.sig, hash, ev.pubkey)) { throw "Sig verify failed"; } diff --git a/src/Nostr/Event.ts b/src/Nostr/Event.ts index e3f994b..000a8cc 100644 --- a/src/Nostr/Event.ts +++ b/src/Nostr/Event.ts @@ -67,7 +67,7 @@ export default class Event { * Get the pub key of the creator of this event NIP-26 */ get RootPubKey() { - let delegation = this.Tags.find((a) => a.Key === "delegation"); + const delegation = this.Tags.find((a) => a.Key === "delegation"); if (delegation?.PubKey) { return delegation.PubKey; } @@ -80,7 +80,7 @@ export default class Event { async Sign(key: HexKey) { 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); if (!(await this.Verify())) { throw "Signing failed"; @@ -92,13 +92,13 @@ export default class Event { * @returns True if valid signature */ async Verify() { - let id = await this.CreateId(); - let result = await secp.schnorr.verify(this.Signature, id, this.PubKey); + const id = await this.CreateId(); + const result = await secp.schnorr.verify(this.Signature, id, this.PubKey); return result; } async CreateId() { - let payload = [ + const payload = [ 0, this.PubKey, this.CreatedAt, @@ -107,9 +107,9 @@ export default class Event { this.Content, ]; - let payloadData = new TextEncoder().encode(JSON.stringify(payload)); - let data = await secp.utils.sha256(payloadData); - let hash = secp.utils.bytesToHex(data); + const payloadData = new TextEncoder().encode(JSON.stringify(payload)); + const data = await secp.utils.sha256(payloadData); + const hash = secp.utils.bytesToHex(data); if (this.Id !== "" && hash !== this.Id) { console.debug(payload); throw "ID doesnt match!"; @@ -135,7 +135,7 @@ export default class Event { * Create a new event for a specific pubkey */ static ForPubKey(pubKey: HexKey) { - let ev = new Event(); + const ev = new Event(); ev.PubKey = pubKey; return ev; } @@ -144,10 +144,10 @@ export default class Event { * Encrypt the given message content */ async EncryptData(content: string, pubkey: HexKey, privkey: HexKey) { - let key = await this._GetDmSharedKey(pubkey, privkey); - let iv = window.crypto.getRandomValues(new Uint8Array(16)); - let data = new TextEncoder().encode(content); - let result = await window.crypto.subtle.encrypt( + const key = await this._GetDmSharedKey(pubkey, privkey); + const iv = window.crypto.getRandomValues(new Uint8Array(16)); + const data = new TextEncoder().encode(content); + const result = await window.crypto.subtle.encrypt( { name: "AES-CBC", iv: iv, @@ -155,7 +155,7 @@ export default class Event { key, data ); - let uData = new Uint8Array(result); + const uData = new Uint8Array(result); return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode( iv, 0, @@ -174,15 +174,15 @@ export default class Event { * Decrypt the content of the message */ async DecryptData(cyphertext: string, privkey: HexKey, pubkey: HexKey) { - let key = await this._GetDmSharedKey(pubkey, privkey); - let cSplit = cyphertext.split("?iv="); - let data = new Uint8Array(base64.length(cSplit[0])); + const key = await this._GetDmSharedKey(pubkey, privkey); + const cSplit = cyphertext.split("?iv="); + const data = new Uint8Array(base64.length(cSplit[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); - let result = await window.crypto.subtle.decrypt( + const result = await window.crypto.subtle.decrypt( { name: "AES-CBC", iv: iv, @@ -201,8 +201,8 @@ export default class Event { } async _GetDmSharedKey(pubkey: HexKey, privkey: HexKey) { - let sharedPoint = secp.getSharedSecret(privkey, "02" + pubkey); - let sharedX = sharedPoint.slice(1, 33); + const sharedPoint = secp.getSharedSecret(privkey, "02" + pubkey); + const sharedX = sharedPoint.slice(1, 33); return await window.crypto.subtle.importKey( "raw", sharedX, diff --git a/src/Nostr/Subscriptions.ts b/src/Nostr/Subscriptions.ts index 905474c..dc57283 100644 --- a/src/Nostr/Subscriptions.ts +++ b/src/Nostr/Subscriptions.ts @@ -104,10 +104,10 @@ export class Subscriptions { this.Since = sub?.since ?? undefined; this.Until = sub?.until ?? undefined; this.Limit = sub?.limit ?? undefined; - this.OnEvent = (e) => { + this.OnEvent = () => { console.warn(`No event handler was set on subscription: ${this.Id}`); }; - this.OnEnd = (c) => {}; + this.OnEnd = () => undefined; this.OrSubs = []; this.Started = new Map(); this.Finished = new Map(); @@ -128,7 +128,7 @@ export class Subscriptions { } ToObject(): RawReqFilter { - let ret: RawReqFilter = {}; + const ret: RawReqFilter = {}; if (this.Ids) { ret.ids = Array.from(this.Ids); } diff --git a/src/Nostr/System.ts b/src/Nostr/System.ts index 3b01275..d2e3cca 100644 --- a/src/Nostr/System.ts +++ b/src/Nostr/System.ts @@ -5,9 +5,10 @@ import Connection, { RelaySettings } from "Nostr/Connection"; import Event from "Nostr/Event"; import EventKind from "Nostr/EventKind"; import { Subscriptions } from "Nostr/Subscriptions"; +import { unwrap } from "Util"; /** - * Manages nostr content retrival system + * Manages nostr content retrieval system */ export class NostrSystem { /** @@ -49,14 +50,14 @@ export class NostrSystem { ConnectToRelay(address: string, options: RelaySettings) { try { if (!this.Sockets.has(address)) { - let c = new Connection(address, options); + const c = new Connection(address, options); this.Sockets.set(address, c); - for (let [_, s] of this.Subscriptions) { + for (const [, s] of this.Subscriptions) { c.AddSubscription(s); } } else { // update settings if already connected - this.Sockets.get(address)!.Settings = options; + unwrap(this.Sockets.get(address)).Settings = options; } } catch (e) { console.error(e); @@ -67,7 +68,7 @@ export class NostrSystem { * Disconnect from a relay */ DisconnectRelay(address: string) { - let c = this.Sockets.get(address); + const c = this.Sockets.get(address); if (c) { this.Sockets.delete(address); c.Close(); @@ -75,14 +76,14 @@ export class NostrSystem { } AddSubscription(sub: Subscriptions) { - for (let [a, s] of this.Sockets) { + for (const [, s] of this.Sockets) { s.AddSubscription(sub); } this.Subscriptions.set(sub.Id, sub); } RemoveSubscription(subId: string) { - for (let [a, s] of this.Sockets) { + for (const [, s] of this.Sockets) { s.RemoveSubscription(subId); } this.Subscriptions.delete(subId); @@ -92,7 +93,7 @@ export class NostrSystem { * Send events to writable relays */ BroadcastEvent(ev: Event) { - for (let [_, s] of this.Sockets) { + for (const [, s] of this.Sockets) { s.SendEvent(ev); } } @@ -101,7 +102,7 @@ export class NostrSystem { * Write an event to a relay then disconnect */ 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); c.Close(); } @@ -110,7 +111,7 @@ export class NostrSystem { * Request profile metadata for a set of pubkeys */ TrackMetadata(pk: HexKey | Array) { - for (let p of Array.isArray(pk) ? pk : [pk]) { + for (const p of Array.isArray(pk) ? pk : [pk]) { if (p.length > 0) { this.WantsMetadata.add(p); } @@ -121,7 +122,7 @@ export class NostrSystem { * Stop tracking metadata for a set of pubkeys */ UntrackMetadata(pk: HexKey | Array) { - for (let p of Array.isArray(pk) ? pk : [pk]) { + for (const p of Array.isArray(pk) ? pk : [pk]) { if (p.length > 0) { this.WantsMetadata.delete(p); } @@ -132,16 +133,16 @@ export class NostrSystem { * Request/Response pattern */ RequestSubscription(sub: Subscriptions) { - return new Promise((resolve, reject) => { - let events: TaggedRawEvent[] = []; + return new Promise((resolve) => { + const events: TaggedRawEvent[] = []; // force timeout returning current results - let timeout = setTimeout(() => { + const timeout = setTimeout(() => { this.RemoveSubscription(sub.Id); resolve(events); }, 10_000); - let onEventPassthrough = sub.OnEvent; + const onEventPassthrough = sub.OnEvent; sub.OnEvent = (ev) => { if (typeof onEventPassthrough === "function") { onEventPassthrough(ev); @@ -149,9 +150,9 @@ export class NostrSystem { if (!events.some((a) => a.id === ev.id)) { events.push(ev); } else { - let existing = events.find((a) => a.id === ev.id); + const existing = events.find((a) => a.id === ev.id); if (existing) { - for (let v of ev.relays) { + for (const v of ev.relays) { existing.relays.push(v); } } @@ -171,11 +172,11 @@ export class NostrSystem { async _FetchMetadata() { if (this.UserDb) { - let missing = new Set(); - let meta = await this.UserDb.bulkGet(Array.from(this.WantsMetadata)); - let expire = new Date().getTime() - ProfileCacheExpire; - for (let pk of this.WantsMetadata) { - let m = meta.find((a) => a?.pubkey === pk); + const missing = new Set(); + const meta = await this.UserDb.bulkGet(Array.from(this.WantsMetadata)); + const expire = new Date().getTime() - ProfileCacheExpire; + for (const pk of this.WantsMetadata) { + const m = meta.find((a) => a?.pubkey === pk); if (!m || m.loaded < expire) { missing.add(pk); // cap 100 missing profiles @@ -188,35 +189,38 @@ export class NostrSystem { if (missing.size > 0) { console.debug("Wants profiles: ", missing); - let sub = new Subscriptions(); + const sub = new Subscriptions(); sub.Id = `profiles:${sub.Id.slice(0, 8)}`; sub.Kinds = new Set([EventKind.SetMetadata]); sub.Authors = missing; sub.OnEvent = async (e) => { - let profile = mapEventToProfile(e); + const profile = mapEventToProfile(e); + const userDb = unwrap(this.UserDb); if (profile) { - let existing = await this.UserDb!.find(profile.pubkey); + const existing = await userDb.find(profile.pubkey); if ((existing?.created ?? 0) < profile.created) { - await this.UserDb!.put(profile); + await userDb.put(profile); } else if (existing) { - await this.UserDb!.update(profile.pubkey, { + await userDb.update(profile.pubkey, { loaded: profile.loaded, }); } } }; - let results = await this.RequestSubscription(sub); - let couldNotFetch = Array.from(missing).filter( + const results = await this.RequestSubscription(sub); + const couldNotFetch = Array.from(missing).filter( (a) => !results.some((b) => b.pubkey === a) ); console.debug("No profiles: ", couldNotFetch); if (couldNotFetch.length > 0) { - let updates = couldNotFetch.map((a) => { - return { - pubkey: a, - loaded: new Date().getTime(), - }; - }).map(a => this.UserDb!.update(a.pubkey, a)); + const updates = couldNotFetch + .map((a) => { + return { + pubkey: a, + loaded: new Date().getTime(), + }; + }) + .map((a) => unwrap(this.UserDb).update(a.pubkey, a)); await Promise.all(updates); } } @@ -224,12 +228,8 @@ export class NostrSystem { setTimeout(() => this._FetchMetadata(), 500); } - async nip42Auth( - challenge: string, - relay: string - ): Promise { - return; - } + nip42Auth: (challenge: string, relay: string) => Promise = + async () => undefined; } export const System = new NostrSystem(); diff --git a/src/Nostr/Tag.ts b/src/Nostr/Tag.ts index e084fb6..d0438ee 100644 --- a/src/Nostr/Tag.ts +++ b/src/Nostr/Tag.ts @@ -56,7 +56,7 @@ export default class Tag { switch (this.Key) { case "e": { 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); return ret; } @@ -64,10 +64,10 @@ export default class Tag { return this.PubKey ? ["p", this.PubKey] : null; } case "t": { - return ["t", this.Hashtag!]; + return ["t", this.Hashtag ?? ""]; } case "d": { - return ["d", this.DTag!]; + return ["d", this.DTag ?? ""]; } default: { return this.Original; diff --git a/src/Nostr/Thread.ts b/src/Nostr/Thread.ts index e29d5e2..a3a678d 100644 --- a/src/Nostr/Thread.ts +++ b/src/Nostr/Thread.ts @@ -19,15 +19,15 @@ export default class Thread { * @param ev Event to extract thread from */ static ExtractThread(ev: NEvent) { - let isThread = ev.Tags.some((a) => a.Key === "e"); + const isThread = ev.Tags.some((a) => a.Key === "e"); if (!isThread) { return null; } - let shouldWriteMarkers = ev.Kind === EventKind.TextNote; - let ret = new Thread(); - let eTags = ev.Tags.filter((a) => a.Key === "e"); - let marked = eTags.some((a) => a.Marker !== undefined); + const shouldWriteMarkers = ev.Kind === EventKind.TextNote; + const ret = new Thread(); + const eTags = ev.Tags.filter((a) => a.Key === "e"); + const marked = eTags.some((a) => a.Marker !== undefined); if (!marked) { ret.Root = eTags[0]; ret.Root.Marker = shouldWriteMarkers ? "root" : undefined; @@ -42,8 +42,8 @@ export default class Thread { } } } else { - let root = eTags.find((a) => a.Marker === "root"); - let reply = eTags.find((a) => a.Marker === "reply"); + const root = eTags.find((a) => a.Marker === "root"); + const reply = eTags.find((a) => a.Marker === "reply"); ret.Root = root; ret.ReplyTo = reply; ret.Mentions = eTags.filter((a) => a.Marker === "mention"); diff --git a/src/Notifications.ts b/src/Notifications.ts index 79b4f65..fd91f3c 100644 --- a/src/Notifications.ts +++ b/src/Notifications.ts @@ -15,13 +15,12 @@ export async function makeNotification( case EventKind.TextNote: { const pubkeys = new Set([ 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 fromUser = users.find((a) => a?.pubkey === ev.pubkey); const name = getDisplayName(fromUser, ev.pubkey); - const avatarUrl = - (fromUser?.picture?.length ?? 0) === 0 ? Nostrich : fromUser?.picture; + const avatarUrl = fromUser?.picture || Nostrich; return { title: `Reply from ${name}`, body: replaceTagsWithUser(ev, users).substring(0, 50), @@ -37,12 +36,12 @@ function replaceTagsWithUser(ev: TaggedRawEvent, users: MetadataCache[]) { return ev.content .split(MentionRegex) .map((match) => { - let matchTag = match.match(/#\[(\d+)\]/); + const matchTag = match.match(/#\[(\d+)\]/); if (matchTag && matchTag.length === 2) { - let idx = parseInt(matchTag[1]); - let ref = ev.tags[idx]; + const idx = parseInt(matchTag[1]); + const ref = ev.tags[idx]; 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])}`; } } diff --git a/src/Pages/ChatPage.tsx b/src/Pages/ChatPage.tsx index a869cb3..5d53ea8 100644 --- a/src/Pages/ChatPage.tsx +++ b/src/Pages/ChatPage.tsx @@ -9,7 +9,7 @@ import { bech32ToHex } from "Util"; import useEventPublisher from "Feed/EventPublisher"; import DM from "Element/DM"; -import { RawEvent } from "Nostr"; +import { RawEvent, TaggedRawEvent } from "Nostr"; import { dmsInChat, isToSelf } from "Pages/MessagesPage"; import NoteToSelf from "Element/NoteToSelf"; @@ -17,24 +17,32 @@ type RouterParams = { id: string; }; +interface State { + login: { + dms: TaggedRawEvent[]; + }; +} + export default function ChatPage() { const params = useParams(); const publisher = useEventPublisher(); const id = bech32ToHex(params.id ?? ""); - const pubKey = useSelector((s) => s.login.publicKey); - const dms = useSelector((s) => filterDms(s.login.dms)); + const pubKey = useSelector<{ login: { publicKey: string } }>( + (s) => s.login.publicKey + ); + const dms = useSelector((s) => filterDms(s.login.dms)); const [content, setContent] = useState(); - const { ref, inView, entry } = useInView(); + const { ref, inView } = useInView(); const dmListRef = useRef(null); - function filterDms(dms: RawEvent[]) { + function filterDms(dms: TaggedRawEvent[]) { return dmsInChat( id === pubKey ? dms.filter((d) => isToSelf(d, pubKey)) : dms, id ); } - const sortedDms = useMemo(() => { + const sortedDms = useMemo(() => { return [...dms].sort((a, b) => a.created_at - b.created_at); }, [dms]); @@ -46,7 +54,7 @@ export default function ChatPage() { async function sendDm() { if (content) { - let ev = await publisher.sendDm(content, id); + const ev = await publisher.sendDm(content, id); console.debug(ev); publisher.broadcast(ev); setContent(""); @@ -54,7 +62,7 @@ export default function ChatPage() { } async function onEnter(e: KeyboardEvent) { - let isEnter = e.code === "Enter"; + const isEnter = e.code === "Enter"; if (isEnter && !e.shiftKey) { await sendDm(); } @@ -67,8 +75,9 @@ export default function ChatPage() { )) || }
    + {/* TODO I need to look into this again, something's being bricked with the RawEvent and TaggedRawEvent */} {sortedDms.map((a) => ( - + ))}
    diff --git a/src/Pages/DonatePage.tsx b/src/Pages/DonatePage.tsx index ed2e2b1..fa9a690 100644 --- a/src/Pages/DonatePage.tsx +++ b/src/Pages/DonatePage.tsx @@ -45,11 +45,11 @@ const DonatePage = () => { const [today, setSumToday] = useState(); async function loadData() { - let rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`); + const rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`); if (rsp.ok) { 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) { setSumToday(await rsp2.json()); } @@ -60,7 +60,7 @@ const DonatePage = () => { }, []); function actions(pk: HexKey) { - let split = splits.find((a) => bech32ToHex(a.pubKey) === pk); + const split = splits.find((a) => bech32ToHex(a.pubKey) === pk); if (split) { return <>{(100 * split.split).toLocaleString()}%; } diff --git a/src/Pages/EventPage.tsx b/src/Pages/EventPage.tsx index 71aa23a..f3f1218 100644 --- a/src/Pages/EventPage.tsx +++ b/src/Pages/EventPage.tsx @@ -5,7 +5,7 @@ import { parseId } from "Util"; export default function EventPage() { const params = useParams(); - const id = parseId(params.id!); + const id = parseId(params.id ?? ""); const thread = useThreadFeed(id); return ; diff --git a/src/Pages/HashTagsPage.tsx b/src/Pages/HashTagsPage.tsx index 4091422..b2b85f9 100644 --- a/src/Pages/HashTagsPage.tsx +++ b/src/Pages/HashTagsPage.tsx @@ -1,9 +1,10 @@ import { useParams } from "react-router-dom"; import Timeline from "Element/Timeline"; +import { unwrap } from "Util"; const HashTagsPage = () => { const params = useParams(); - const tag = params.tag!.toLowerCase(); + const tag = unwrap(params.tag).toLowerCase(); return ( <> diff --git a/src/Pages/Layout.tsx b/src/Pages/Layout.tsx index 8403b3d..0a56edd 100644 --- a/src/Pages/Layout.tsx +++ b/src/Pages/Layout.tsx @@ -75,10 +75,10 @@ export default function Layout() { useEffect(() => { if (relays) { - for (let [k, v] of Object.entries(relays)) { + for (const [k, v] of Object.entries(relays)) { System.ConnectToRelay(k, v); } - for (let [k] of System.Sockets) { + for (const [k] of System.Sockets) { if (!relays[k] && !SearchRelays.has(k)) { System.DisconnectRelay(k); } @@ -96,7 +96,7 @@ export default function Layout() { } useEffect(() => { - let osTheme = window.matchMedia("(prefers-color-scheme: light)"); + const osTheme = window.matchMedia("(prefers-color-scheme: light)"); setTheme( preferences.theme === "system" && osTheme.matches ? "light" @@ -139,24 +139,24 @@ export default function Layout() { }, []); async function handleNewUser() { - let newRelays: Record | undefined; + let newRelays: Record = {}; try { - let rsp = await fetch("https://api.nostr.watch/v1/online"); + const rsp = await fetch("https://api.nostr.watch/v1/online"); if (rsp.ok) { - let online: string[] = await rsp.json(); - let pickRandom = online - .sort((a, b) => (Math.random() >= 0.5 ? 1 : -1)) + const online: string[] = await rsp.json(); + const pickRandom = online + .sort(() => (Math.random() >= 0.5 ? 1 : -1)) .slice(0, 4); // pick 4 random relays - let relayObjects = pickRandom.map((a) => [ + const relayObjects = pickRandom.map((a) => [ a, { read: true, write: true }, ]); newRelays = Object.fromEntries(relayObjects); dispatch( setRelays({ - relays: newRelays!, + relays: newRelays, createdAt: 1, }) ); @@ -175,13 +175,13 @@ export default function Layout() { } }, [newUserKey]); - async function goToNotifications(e: any) { + async function goToNotifications(e: React.MouseEvent) { e.stopPropagation(); // request permissions to send notifications if ("Notification" in window) { try { if (Notification.permission !== "granted") { - let res = await Notification.requestPermission(); + const res = await Notification.requestPermission(); console.debug(res); } } catch (e) { @@ -194,14 +194,14 @@ export default function Layout() { function accountHeader() { return (
    -
    navigate("/search")}> +
    navigate("/search")}>
    -
    navigate("/messages")}> +
    navigate("/messages")}> {unreadDms > 0 && }
    -
    goToNotifications(e)}> +
    {hasNotifications && }
    diff --git a/src/Pages/Login.tsx b/src/Pages/Login.tsx index 20327fc..7cd3396 100644 --- a/src/Pages/Login.tsx +++ b/src/Pages/Login.tsx @@ -30,15 +30,15 @@ export default function LoginPage() { }, [publicKey, navigate]); async function getNip05PubKey(addr: string) { - let [username, domain] = addr.split("@"); - let rsp = await fetch( + const [username, domain] = addr.split("@"); + const rsp = await fetch( `https://${domain}/.well-known/nostr.json?name=${encodeURIComponent( username )}` ); if (rsp.ok) { - let data = await rsp.json(); - let pKey = data.names[username]; + const data = await rsp.json(); + const pKey = data.names[username]; if (pKey) { return pKey; } @@ -49,17 +49,17 @@ export default function LoginPage() { async function doLogin() { try { if (key.startsWith("nsec")) { - let hexKey = bech32ToHex(key); + const hexKey = bech32ToHex(key); if (secp.utils.isValidPrivateKey(hexKey)) { dispatch(setPrivateKey(hexKey)); } else { throw new Error("INVALID PRIVATE KEY"); } } else if (key.startsWith("npub")) { - let hexKey = bech32ToHex(key); + const hexKey = bech32ToHex(key); dispatch(setPublicKey(hexKey)); } else if (key.match(EmailRegex)) { - let hexKey = await getNip05PubKey(key); + const hexKey = await getNip05PubKey(key); dispatch(setPublicKey(hexKey)); } else { if (secp.utils.isValidPrivateKey(key)) { @@ -75,17 +75,17 @@ export default function LoginPage() { } async function makeRandomKey() { - let newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey()); + const newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey()); dispatch(setGeneratedPrivateKey(newKey)); navigate("/new"); } async function doNip07Login() { - let pubKey = await window.nostr.getPublicKey(); + const pubKey = await window.nostr.getPublicKey(); dispatch(setPublicKey(pubKey)); if ("getRelays" in window.nostr) { - let relays = await window.nostr.getRelays(); + const relays = await window.nostr.getRelays(); dispatch( setRelays({ relays: { @@ -99,7 +99,7 @@ export default function LoginPage() { } function altLogins() { - let nip07 = "nostr" in window; + const nip07 = "nostr" in window; if (!nip07) { return null; } @@ -108,7 +108,7 @@ export default function LoginPage() { <>

    Other Login Methods

    -
    @@ -129,7 +129,7 @@ export default function LoginPage() {
    {error.length > 0 ? {error} : null}
    - - {sortedReccomends.length > 0 && ( - + {sortedRecommends.length > 0 && ( + )} diff --git a/src/Pages/new/ImportFollows.tsx b/src/Pages/new/ImportFollows.tsx index 7d31b27..a19ae55 100644 --- a/src/Pages/new/ImportFollows.tsx +++ b/src/Pages/new/ImportFollows.tsx @@ -20,15 +20,17 @@ export default function ImportFollows() { const sortedTwitterFollows = useMemo(() => { return follows .map((a) => bech32ToHex(a)) - .sort((a, b) => (currentFollows.includes(a) ? 1 : -1)); + .sort((a) => (currentFollows.includes(a) ? 1 : -1)); }, [follows, currentFollows]); async function loadFollows() { setFollows([]); setError(""); try { - let rsp = await fetch(`${TwitterFollowsApi}?username=${twitterUsername}`); - let data = await rsp.json(); + const rsp = await fetch( + `${TwitterFollowsApi}?username=${twitterUsername}` + ); + const data = await rsp.json(); if (rsp.ok) { if (Array.isArray(data) && data.length === 0) { setError(`No nostr users found for "${twitterUsername}"`); diff --git a/src/Pages/settings/Preferences.tsx b/src/Pages/settings/Preferences.tsx index fef772a..c75f771 100644 --- a/src/Pages/settings/Preferences.tsx +++ b/src/Pages/settings/Preferences.tsx @@ -7,6 +7,8 @@ import { DefaultImgProxy, setPreferences, UserPreferences } from "State/Login"; import { RootState } from "State/Store"; import messages from "./messages"; +import { unwrap } from "Util"; +import "./Preferences.css"; const PreferencesPage = () => { const dispatch = useDispatch(); @@ -124,7 +126,7 @@ const PreferencesPage = () => { setPreferences({ ...perf, imgProxyConfig: { - ...perf.imgProxyConfig!, + ...unwrap(perf.imgProxyConfig), url: e.target.value, }, }) @@ -147,7 +149,7 @@ const PreferencesPage = () => { setPreferences({ ...perf, imgProxyConfig: { - ...perf.imgProxyConfig!, + ...unwrap(perf.imgProxyConfig), key: e.target.value, }, }) @@ -170,7 +172,7 @@ const PreferencesPage = () => { setPreferences({ ...perf, imgProxyConfig: { - ...perf.imgProxyConfig!, + ...unwrap(perf.imgProxyConfig), salt: e.target.value, }, }) diff --git a/src/Pages/settings/Profile.tsx b/src/Pages/settings/Profile.tsx index 0be2233..fb2a84d 100644 --- a/src/Pages/settings/Profile.tsx +++ b/src/Pages/settings/Profile.tsx @@ -31,7 +31,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) { const privKey = useSelector( (s) => s.login.privateKey ); - const user = useUserProfile(id!); + const user = useUserProfile(id ?? ""); const publisher = useEventPublisher(); const uploader = useFileUpload(); @@ -61,7 +61,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) { async function saveProfile() { // copy user object and delete internal fields - let userCopy = { + const userCopy = { ...user, name, display_name: displayName, @@ -78,16 +78,16 @@ export default function ProfileSettings(props: ProfileSettingsProps) { delete userCopy["npub"]; console.debug(userCopy); - let ev = await publisher.metadata(userCopy); + const ev = await publisher.metadata(userCopy); console.debug(ev); publisher.broadcast(ev); } async function uploadFile() { - let file = await openFile(); + const file = await openFile(); if (file) { console.log(file); - let rsp = await uploader.upload(file, file.name); + const rsp = await uploader.upload(file, file.name); console.log(rsp); if (typeof rsp?.error === "string") { throw new Error(`Upload failed ${rsp.error}`); diff --git a/src/Pages/settings/RelayInfo.tsx b/src/Pages/settings/RelayInfo.tsx index b677641..59aad7c 100644 --- a/src/Pages/settings/RelayInfo.tsx +++ b/src/Pages/settings/RelayInfo.tsx @@ -5,7 +5,7 @@ import { System } from "Nostr/System"; import { useDispatch } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; import { removeRelay } from "State/Login"; -import { parseId } from "Util"; +import { parseId, unwrap } from "Util"; import messages from "./messages"; @@ -100,7 +100,7 @@ const RelayInfo = () => {
    { - dispatch(removeRelay(conn!.Address)); + dispatch(removeRelay(unwrap(conn).Address)); navigate("/settings/relays"); }} > diff --git a/src/Pages/settings/Relays.tsx b/src/Pages/settings/Relays.tsx index d4ef7fe..bac4d47 100644 --- a/src/Pages/settings/Relays.tsx +++ b/src/Pages/settings/Relays.tsx @@ -19,7 +19,7 @@ const RelaySettingsPage = () => { const [newRelay, setNewRelay] = useState(); async function saveRelays() { - let ev = await publisher.saveRelays(); + const ev = await publisher.saveRelays(); publisher.broadcast(ev); publisher.broadcastForBootstrap(ev); } @@ -48,7 +48,7 @@ const RelaySettingsPage = () => { function addNewRelay() { if ((newRelay?.length ?? 0) > 0) { - const parsed = new URL(newRelay!); + const parsed = new URL(newRelay ?? ""); const payload = { relays: { ...relays, diff --git a/src/State/Login.ts b/src/State/Login.ts index 4e408b7..c1c54de 100644 --- a/src/State/Login.ts +++ b/src/State/Login.ts @@ -215,26 +215,26 @@ const LoginSlice = createSlice({ } // check pub key only - let pubKey = window.localStorage.getItem(PublicKeyItem); + const pubKey = window.localStorage.getItem(PublicKeyItem); if (pubKey && !state.privateKey) { state.publicKey = pubKey; state.loggedOut = false; } - let lastRelayList = window.localStorage.getItem(RelayListKey); + const lastRelayList = window.localStorage.getItem(RelayListKey); if (lastRelayList) { state.relays = JSON.parse(lastRelayList); } else { state.relays = Object.fromEntries(DefaultRelays.entries()); } - let lastFollows = window.localStorage.getItem(FollowList); + const lastFollows = window.localStorage.getItem(FollowList); if (lastFollows) { state.follows = JSON.parse(lastFollows); } // notifications - let readNotif = parseInt( + const readNotif = parseInt( window.localStorage.getItem(NotificationsReadItem) ?? "0" ); if (!isNaN(readNotif)) { @@ -242,7 +242,7 @@ const LoginSlice = createSlice({ } // preferences - let pref = window.localStorage.getItem(UserPreferencesKey); + const pref = window.localStorage.getItem(UserPreferencesKey); if (pref) { state.preferences = JSON.parse(pref); } @@ -270,15 +270,15 @@ const LoginSlice = createSlice({ state.publicKey = action.payload; }, setRelays: (state, action: PayloadAction) => { - let relays = action.payload.relays; - let createdAt = action.payload.createdAt; + const relays = action.payload.relays; + const createdAt = action.payload.createdAt; if (state.latestRelays > createdAt) { return; } // filter out non-websocket urls - let filtered = new Map(); - for (let [k, v] of Object.entries(relays)) { + const filtered = new Map(); + for (const [k, v] of Object.entries(relays)) { if (k.startsWith("wss://") || k.startsWith("ws://")) { filtered.set(k, v as RelaySettings); } @@ -299,17 +299,17 @@ const LoginSlice = createSlice({ return; } - let existing = new Set(state.follows); - let update = Array.isArray(keys) ? keys : [keys]; + const existing = new Set(state.follows); + const update = Array.isArray(keys) ? keys : [keys]; 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)) { existing.add(pk); changes = true; } } - for (let pk of existing) { + for (const pk of existing) { if (!update.includes(pk)) { existing.delete(pk); changes = true; @@ -355,7 +355,7 @@ const LoginSlice = createSlice({ } let didChange = false; - for (let x of n) { + for (const x of n) { if (!state.dms.some((a) => a.id === x.id)) { state.dms.push(x); didChange = true; @@ -370,7 +370,7 @@ const LoginSlice = createSlice({ state.dmInteraction += 1; }, logout: (state) => { - let relays = { ...state.relays }; + const relays = { ...state.relays }; Object.assign(state, InitState); state.loggedOut = true; window.localStorage.clear(); @@ -430,7 +430,7 @@ export function sendNotification({ hasPermission && timestamp > readNotifications; if (shouldShowNotification) { try { - let worker = await navigator.serviceWorker.ready; + const worker = await navigator.serviceWorker.ready; worker.showNotification(title, { tag: "notification", vibrate: [500], diff --git a/src/State/Users.ts b/src/State/Users.ts index b36ef1b..0168d9b 100644 --- a/src/State/Users.ts +++ b/src/State/Users.ts @@ -26,7 +26,7 @@ export interface MetadataCache extends UserMetadata { export function mapEventToProfile(ev: TaggedRawEvent) { try { - let data: UserMetadata = JSON.parse(ev.content); + const data: UserMetadata = JSON.parse(ev.content); return { pubkey: ev.pubkey, npub: hexToBech32("npub", ev.pubkey), @@ -43,12 +43,12 @@ export interface UsersDb { isAvailable(): Promise; query(str: string): Promise; find(key: HexKey): Promise; - add(user: MetadataCache): Promise; - put(user: MetadataCache): Promise; - bulkAdd(users: MetadataCache[]): Promise; + add(user: MetadataCache): Promise; + put(user: MetadataCache): Promise; + bulkAdd(users: MetadataCache[]): Promise; bulkGet(keys: HexKey[]): Promise; - bulkPut(users: MetadataCache[]): Promise; - update(key: HexKey, fields: Record): Promise; + bulkPut(users: MetadataCache[]): Promise; + update(key: HexKey, fields: Record): Promise; } export interface UsersStore { diff --git a/src/State/Users/Db.ts b/src/State/Users/Db.ts index 7dab5c2..590c4d2 100644 --- a/src/State/Users/Db.ts +++ b/src/State/Users/Db.ts @@ -4,18 +4,19 @@ import { db as idb } from "Db"; import { UsersDb, MetadataCache, setUsers } from "State/Users"; import store, { RootState } from "State/Store"; import { useSelector } from "react-redux"; +import { unwrap } from "Util"; class IndexedUsersDb implements UsersDb { - ready: boolean = false; + ready = false; isAvailable() { if ("indexedDB" in window) { return new Promise((resolve) => { const req = window.indexedDB.open("dummy", 1); - req.onsuccess = (ev) => { + req.onsuccess = () => { resolve(true); }; - req.onerror = (ev) => { + req.onerror = () => { resolve(false); }; }); @@ -41,30 +42,29 @@ class IndexedUsersDb implements UsersDb { .toArray(); } - bulkGet(keys: HexKey[]) { - return idb.users - .bulkGet(keys) - .then((ret) => ret.filter((a) => a !== undefined).map((a) => a!)); + async bulkGet(keys: HexKey[]) { + const ret = await idb.users.bulkGet(keys); + return ret.filter((a) => a !== undefined).map((a_1) => unwrap(a_1)); } - add(user: MetadataCache) { - return idb.users.add(user); + async add(user: MetadataCache) { + await idb.users.add(user); } - put(user: MetadataCache) { - return idb.users.put(user); + async put(user: MetadataCache) { + await idb.users.put(user); } - bulkAdd(users: MetadataCache[]) { - return idb.users.bulkAdd(users); + async bulkAdd(users: MetadataCache[]) { + await idb.users.bulkAdd(users); } - bulkPut(users: MetadataCache[]) { - return idb.users.bulkPut(users); + async bulkPut(users: MetadataCache[]) { + await idb.users.bulkPut(users); } - update(key: HexKey, fields: Record) { - return idb.users.update(key, fields); + async update(key: HexKey, fields: Record) { + await idb.users.update(key, fields); } } @@ -128,7 +128,7 @@ class ReduxUsersDb implements UsersDb { }); } - async update(key: HexKey, fields: Record) { + async update(key: HexKey, fields: Record) { const state = store.getState(); const { users } = state.users; const current = users[key]; diff --git a/src/State/Users/Hooks.ts b/src/State/Users/Hooks.ts index 635d6d9..e0a1292 100644 --- a/src/State/Users/Hooks.ts +++ b/src/State/Users/Hooks.ts @@ -4,8 +4,9 @@ import { MetadataCache } from "State/Users"; import type { RootState } from "State/Store"; import { HexKey } from "Nostr"; import { useDb } from "./Db"; +import { unwrap } from "Util"; -export function useQuery(query: string, limit: number = 5) { +export function useQuery(query: string) { const db = useDb(); return useLiveQuery(async () => db.query(query), [query]); } @@ -46,5 +47,5 @@ export function useKeys(pubKeys: HexKey[]): Map { return new Map(); }, [pubKeys, users]); - return dbUsers!; + return dbUsers ?? new Map(); } diff --git a/src/Upload/NostrBuild.ts b/src/Upload/NostrBuild.ts index 4ede6d7..225f59f 100644 --- a/src/Upload/NostrBuild.ts +++ b/src/Upload/NostrBuild.ts @@ -3,11 +3,11 @@ import { UploadResult } from "Upload"; export default async function NostrBuild( file: File | Blob ): Promise { - let fd = new FormData(); + const fd = new FormData(); fd.append("fileToUpload", file); 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, method: "POST", headers: { @@ -15,7 +15,7 @@ export default async function NostrBuild( }, }); if (rsp.ok) { - let data = await rsp.json(); + const data = await rsp.json(); return { url: new URL(data).toString(), }; diff --git a/src/Upload/NostrImg.ts b/src/Upload/NostrImg.ts index 39a4316..dd6e5b3 100644 --- a/src/Upload/NostrImg.ts +++ b/src/Upload/NostrImg.ts @@ -3,10 +3,10 @@ import { UploadResult } from "Upload"; export default async function NostrImg( file: File | Blob ): Promise { - let fd = new FormData(); + const fd = new FormData(); fd.append("image", file); - let rsp = await fetch("https://nostrimg.com/api/upload", { + const rsp = await fetch("https://nostrimg.com/api/upload", { body: fd, method: "POST", headers: { @@ -14,7 +14,7 @@ export default async function NostrImg( }, }); if (rsp.ok) { - let data: UploadResponse = await rsp.json(); + const data: UploadResponse = await rsp.json(); if (typeof data?.imageUrl === "string" && data.success) { return { url: new URL(data.imageUrl).toString(), diff --git a/src/Upload/VoidCat.ts b/src/Upload/VoidCat.ts index 1053583..db41806 100644 --- a/src/Upload/VoidCat.ts +++ b/src/Upload/VoidCat.ts @@ -13,7 +13,7 @@ export default async function VoidCat( const buf = await file.arrayBuffer(); const digest = await crypto.subtle.digest("SHA-256", buf); - let req = await fetch(`${VoidCatHost}/upload`, { + const req = await fetch(`${VoidCatHost}/upload`, { mode: "cors", method: "POST", body: buf, @@ -28,7 +28,7 @@ export default async function VoidCat( }); if (req.ok) { - let rsp: VoidUploadResponse = await req.json(); + const rsp: VoidUploadResponse = await req.json(); if (rsp.ok) { let ext = filename.match(FileExtensionRegex); if (rsp.file?.metadata?.mimeType === "image/webp") { diff --git a/src/Util.ts b/src/Util.ts index cf377c4..e64eb58 100644 --- a/src/Util.ts +++ b/src/Util.ts @@ -1,7 +1,7 @@ import * as secp from "@noble/secp256k1"; import { sha256 as hash } from "@noble/hashes/sha256"; import { bech32 } from "bech32"; -import { HexKey, RawEvent, TaggedRawEvent, u256 } from "Nostr"; +import { HexKey, TaggedRawEvent, u256 } from "Nostr"; import EventKind from "Nostr/EventKind"; import { MessageDescriptor } from "react-intl"; @@ -10,11 +10,11 @@ export const sha256 = (str: string) => { }; export async function openFile(): Promise { - return new Promise((resolve, reject) => { - let elm = document.createElement("input"); + return new Promise((resolve) => { + const elm = document.createElement("input"); elm.type = "file"; elm.onchange = (e: Event) => { - let elm = e.target as HTMLInputElement; + const elm = e.target as HTMLInputElement; if (elm.files) { resolve(elm.files[0]); } else { @@ -36,13 +36,15 @@ export function parseId(id: string) { if (hrp.some((a) => id.startsWith(a))) { return bech32ToHex(id); } - } catch (e) {} + } catch (e) { + // Ignore the error. + } return id; } export function bech32ToHex(str: string) { - let nKey = bech32.decode(str); - let buff = bech32.fromWords(nKey.words); + const nKey = bech32.decode(str); + const buff = bech32.fromWords(nKey.words); return secp.utils.bytesToHex(Uint8Array.from(buff)); } @@ -52,8 +54,8 @@ export function bech32ToHex(str: string) { * @returns */ export function bech32ToText(str: string) { - let decoded = bech32.decode(str, 1000); - let buf = bech32.fromWords(decoded.words); + const decoded = bech32.decode(str, 1000); + const buf = bech32.fromWords(decoded.words); return new TextDecoder().decode(Uint8Array.from(buf)); } @@ -76,7 +78,7 @@ export function hexToBech32(hrp: string, hex: string) { } try { - let buf = secp.utils.hexToBytes(hex); + const buf = secp.utils.hexToBytes(hex); return bech32.encode(hrp, bech32.toWords(buf)); } catch (e) { console.warn("Invalid hex", hex, e); @@ -140,13 +142,13 @@ export function getReactions( export function extractLnAddress(lnurl: string) { // some clients incorrectly set this to LNURL service, patch this if (lnurl.toLowerCase().startsWith("lnurl")) { - let url = bech32ToText(lnurl); + const url = bech32ToText(lnurl); if (url.startsWith("http")) { - let parsedUri = new URL(url); + const parsedUri = new URL(url); // is lightning address if (parsedUri.pathname.startsWith("/.well-known/lnurlp/")) { - let pathParts = parsedUri.pathname.split("/"); - let username = pathParts[pathParts.length - 1]; + const pathParts = parsedUri.pathname.split("/"); + const username = pathParts[pathParts.length - 1]; return `${username}@${parsedUri.hostname}`; } } @@ -165,7 +167,7 @@ export function unixNow() { * @returns Cancel timeout function */ export function debounce(timeout: number, fn: () => void) { - let t = setTimeout(fn, timeout); + const t = setTimeout(fn, timeout); return () => clearTimeout(t); } @@ -201,3 +203,10 @@ export function dedupeByPubkey(events: TaggedRawEvent[]) { ); return deduped.list as TaggedRawEvent[]; } + +export function unwrap(v: T | undefined | null): T { + if (v === undefined || v === null) { + throw new Error("missing value"); + } + return v; +} diff --git a/src/index.tsx b/src/index.tsx index 68a801a..d6b4a7c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -26,6 +26,7 @@ import SearchPage from "Pages/SearchPage"; import HelpPage from "Pages/HelpPage"; import { NewUserRoutes } from "Pages/new"; import { IntlProvider } from "./IntlProvider"; +import { unwrap } from "Util"; /** * 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(