From 441983b8aefa4b8f3462391cdf5ad07097690203 Mon Sep 17 00:00:00 2001 From: ennmichael Date: Tue, 7 Feb 2023 20:47:57 +0100 Subject: [PATCH 1/8] fix eslint warnings --- .eslintrc.cjs | 13 ++ d.ts | 6 +- src/Const.ts | 4 + src/Element/AsyncButton.tsx | 19 +- src/Element/BlockList.tsx | 7 - src/Element/Collapsed.tsx | 2 +- src/Element/Copy.tsx | 2 +- src/Element/DM.tsx | 7 +- src/Element/FollowButton.tsx | 4 +- src/Element/FollowListBase.tsx | 2 +- src/Element/FollowersList.tsx | 2 +- src/Element/FollowsYou.tsx | 2 +- src/Element/HyperText.tsx | 4 +- src/Element/Invoice.tsx | 29 +-- src/Element/LNURLTip.css | 59 ++++++ src/Element/LNURLTip.tsx | 301 +++++++++++++++++++++++++++++ src/Element/LoadMore.tsx | 2 +- src/Element/Mention.tsx | 8 +- src/Element/Modal.tsx | 11 +- src/Element/MutedList.tsx | 1 - src/Element/Nip5Service.tsx | 34 ++-- src/Element/Note.tsx | 30 ++- src/Element/NoteCreator.tsx | 26 ++- src/Element/NoteFooter.tsx | 14 +- src/Element/NoteGhost.tsx | 9 +- src/Element/NoteReaction.tsx | 4 +- src/Element/NoteTime.tsx | 12 +- src/Element/NoteToSelf.tsx | 2 +- src/Element/ProfileImage.tsx | 8 +- src/Element/ProxyImg.tsx | 10 +- src/Element/QrCode.tsx | 15 +- src/Element/Relay.tsx | 7 +- src/Element/SendSats.tsx | 53 +++-- src/Element/Text.tsx | 50 ++--- src/Element/Textarea.tsx | 31 ++- src/Element/Thread.tsx | 19 +- src/Element/TidalEmbed.tsx | 2 +- src/Element/Timeline.tsx | 2 +- src/Element/Zap.tsx | 14 +- src/Element/ZapButton.tsx | 6 +- src/Feed/EventPublisher.ts | 83 ++++---- src/Feed/FollowersFeed.ts | 2 +- src/Feed/FollowsFeed.ts | 6 +- src/Feed/ImgProxy.ts | 5 +- src/Feed/LoginFeed.ts | 41 ++-- src/Feed/MuteList.ts | 4 +- src/Feed/RelayState.ts | 8 +- src/Feed/Subscription.ts | 16 +- src/Feed/ThreadFeed.ts | 12 +- src/Feed/TimelineFeed.ts | 22 +-- src/Feed/ZapsFeed.ts | 2 +- src/Hooks/useHorizontalScroll.tsx | 4 +- src/Hooks/useWebln.ts | 4 +- src/Icons/Attachment.tsx | 4 +- src/Icons/Logout.tsx | 4 +- src/Icons/Reply.tsx | 2 - src/Nip05/ServiceProvider.ts | 8 +- src/Nostr/Connection.ts | 76 ++++---- src/Nostr/Event.ts | 42 ++-- src/Nostr/Subscriptions.ts | 6 +- src/Nostr/System.ts | 82 ++++---- src/Nostr/Tag.ts | 6 +- src/Nostr/Thread.ts | 14 +- src/Notifications.ts | 13 +- src/Pages/ChatPage.tsx | 27 ++- src/Pages/DonatePage.tsx | 6 +- src/Pages/EventPage.tsx | 2 +- src/Pages/HashTagsPage.tsx | 3 +- src/Pages/Layout.tsx | 30 +-- src/Pages/Login.tsx | 26 +-- src/Pages/MessagesPage.tsx | 14 +- src/Pages/Notifications.tsx | 4 +- src/Pages/ProfilePage.tsx | 2 +- src/Pages/SearchPage.tsx | 8 +- src/Pages/new/DiscoverFollows.tsx | 8 +- src/Pages/new/ImportFollows.tsx | 8 +- src/Pages/settings/Preferences.tsx | 8 +- src/Pages/settings/Profile.tsx | 10 +- src/Pages/settings/RelayInfo.tsx | 4 +- src/Pages/settings/Relays.tsx | 4 +- src/State/Login.ts | 32 +-- src/State/Users.ts | 12 +- src/State/Users/Db.ts | 36 ++-- src/State/Users/Hooks.ts | 5 +- src/Upload/NostrBuild.ts | 6 +- src/Upload/NostrImg.ts | 6 +- src/Upload/VoidCat.ts | 4 +- src/Util.ts | 39 ++-- src/index.tsx | 3 +- 89 files changed, 1018 insertions(+), 588 deletions(-) create mode 100644 .eslintrc.cjs create mode 100644 src/Element/LNURLTip.css create mode 100644 src/Element/LNURLTip.tsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..648bbd07 --- /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 125a621e..2bf20721 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 012f6e2e..ac6c8290 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 d420aa37..068c40c3 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 b408c7d3..e6ba6c83 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 569fb32b..b1f282e6 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 df6848f9..10ea2311 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 19ea59fd..0c0d871c 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 2339e072..da7b03a5 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 4479a2dc..d421d1d6 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 cc903511..7fb169eb 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 fdf32b16..cc49dd48 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 72683fe5..2d4f1b05 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 00000000..917f4c2c --- /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 00000000..2ff53b01 --- /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 e5cbf2a7..4bbfad79 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 67ed27f3..c38140c8 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 641511bd..d136c076 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 4d51a2c8..ee59ac05 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 dd2c2a5e..a41711c6 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 a8ffe5d3..9818dcff 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 3f10b91b..54859a64 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 b0d60f63..1604f51c 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 1d5fe0ec..054bfc78 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 d494a09b..42ce88a7 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 dc1949d3..a701238c 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 0048fee0..db123b09 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 8a63b409..7f7d8193 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 b5586d31..78430662 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 badb6bbb..ed486a60 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 34719536..5d2a69bc 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 27f19c59..2e249af3 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 36463baa..65021808 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 9c9f20dd..2645a845 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 2d25517e..f838f0a7 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 6317b30d..5c9f5076 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 41d2f2e9..20ed7246 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 8c302fa3..ebb28806 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 bd8a8332..f948bf05 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 be28272f..491679a4 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 a9e55fb4..e7e1f948 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 142d5a25..62679d90 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 c7de65e9..70141c59 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 69f3925d..79b1aba6 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 93dcd258..0bca1815 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 ba7630e3..dfc70576 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 03569e43..48801682 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 72208f58..8af753c5 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 2b7a6e05..4ffc144f 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 943f32c5..88c4294b 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 b569e682..bd2d89f5 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 e3f994b8..000a8cc1 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 905474c0..dc57283a 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 3b01275f..d2e3cca7 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 e084fb62..d0438eec 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 e29d5e21..a3a678dc 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 79b4f651..fd91f3c7 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 a869cb39..5d53ea81 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 ed2e2b1b..fa9a690c 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 71aa23a4..f3f12181 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 4091422f..b2b85f92 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 8403b3d3..0a56eddb 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 20327fc0..7cd3396d 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 7d31b274..a19ae551 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 fef772a3..c75f771f 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 0be22333..fb2a84df 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 b677641d..59aad7cf 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 d4ef7fe4..bac4d475 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 4e408b73..c1c54def 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 b36ef1b3..0168d9bf 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 7dab5c2b..590c4d2d 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 635d6d99..e0a12923 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 4ede6d7a..225f59fc 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 39a43169..dd6e5b3c 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 10535832..db41806f 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 cf377c42..e64eb581 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 68a801ac..d6b4a7c0 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( From 8bacda27a905123bc305a9a28a06b9a6793a0d6e Mon Sep 17 00:00:00 2001 From: ennmichael Date: Wed, 8 Feb 2023 21:11:03 +0100 Subject: [PATCH 2/8] github workflow --- .github/workflows/eslint.yaml | 23 +++++++++++++++++++++++ .github/workflows/formatting.yaml | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/workflows/eslint.yaml create mode 100644 .github/workflows/formatting.yaml diff --git a/.github/workflows/eslint.yaml b/.github/workflows/eslint.yaml new file mode 100644 index 00000000..56905c02 --- /dev/null +++ b/.github/workflows/eslint.yaml @@ -0,0 +1,23 @@ +name: pull request + +on: + pull_request: + push: + branches: [main] + +jobs: + formatting: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + + - name: Install Dependencies + run: npm i + + - name: Check Eslint + run: npx eslint . diff --git a/.github/workflows/formatting.yaml b/.github/workflows/formatting.yaml new file mode 100644 index 00000000..6e51f36b --- /dev/null +++ b/.github/workflows/formatting.yaml @@ -0,0 +1,23 @@ +name: pull request + +on: + pull_request: + push: + branches: [main] + +jobs: + formatting: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + + - name: Install Dependencies + run: npm i + + - name: Check Formatting + run: npx prettier --check . From 1033ccfeede9e73731a82f04cf067834cdced388 Mon Sep 17 00:00:00 2001 From: ennmichael Date: Wed, 8 Feb 2023 21:16:35 +0100 Subject: [PATCH 3/8] rebase --- .prettierignore | 1 + src/IntlProvider.tsx | 70 ++++++++++++++++++++-------------------- src/Nostr/System.ts | 2 +- src/Pages/Layout.tsx | 2 +- src/State/Users/Hooks.ts | 1 - 5 files changed, 38 insertions(+), 38 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..567609b1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +build/ diff --git a/src/IntlProvider.tsx b/src/IntlProvider.tsx index 8f71bdd1..9cb70ee8 100644 --- a/src/IntlProvider.tsx +++ b/src/IntlProvider.tsx @@ -1,42 +1,42 @@ -import { type ReactNode } from 'react' -import { IntlProvider as ReactIntlProvider } from 'react-intl' -import enMessages from 'translations/en.json' -import esMessages from 'translations/es.json' -import zhMessages from 'translations/zh.json' -import jaMessages from 'translations/ja.json' +import { type ReactNode } from "react"; +import { IntlProvider as ReactIntlProvider } from "react-intl"; +import enMessages from "translations/en.json"; +import esMessages from "translations/es.json"; +import zhMessages from "translations/zh.json"; +import jaMessages from "translations/ja.json"; -const DEFAULT_LOCALE = 'en-US' +const DEFAULT_LOCALE = "en-US"; const getMessages = (locale: string) => { - const truncatedLocale = locale.toLowerCase().split(/[_-]+/)[0] + const truncatedLocale = locale.toLowerCase().split(/[_-]+/)[0]; - switch (truncatedLocale) { - case 'en': - return enMessages - case 'es': - return esMessages - case 'zh': - return zhMessages - case 'ja': - return jaMessages - default: - return enMessages - } -} + switch (truncatedLocale) { + case "en": + return enMessages; + case "es": + return esMessages; + case "zh": + return zhMessages; + case "ja": + return jaMessages; + default: + return enMessages; + } +}; export const IntlProvider = ({ children }: { children: ReactNode }) => { - const getLocale = () => { - return ( - (navigator.languages && navigator.languages[0]) || - navigator.language || - DEFAULT_LOCALE - ) - } - const locale = getLocale() - + const getLocale = () => { return ( - - {children} - - ) -} + (navigator.languages && navigator.languages[0]) || + navigator.language || + DEFAULT_LOCALE + ); + }; + const locale = getLocale(); + + return ( + + {children} + + ); +}; diff --git a/src/Nostr/System.ts b/src/Nostr/System.ts index d2e3cca7..33ca8f0a 100644 --- a/src/Nostr/System.ts +++ b/src/Nostr/System.ts @@ -1,6 +1,6 @@ import { HexKey, TaggedRawEvent } from "Nostr"; import { ProfileCacheExpire } from "Const"; -import { mapEventToProfile, MetadataCache, UsersDb } from "State/Users"; +import { mapEventToProfile, UsersDb } from "State/Users"; import Connection, { RelaySettings } from "Nostr/Connection"; import Event from "Nostr/Event"; import EventKind from "Nostr/EventKind"; diff --git a/src/Pages/Layout.tsx b/src/Pages/Layout.tsx index 0a56eddb..939e9789 100644 --- a/src/Pages/Layout.tsx +++ b/src/Pages/Layout.tsx @@ -22,7 +22,7 @@ import { NoteCreator } from "Element/NoteCreator"; import Plus from "Icons/Plus"; import { RelaySettings } from "Nostr/Connection"; import { FormattedMessage } from "react-intl"; -import messages from './messages' +import messages from "./messages"; export default function Layout() { const location = useLocation(); diff --git a/src/State/Users/Hooks.ts b/src/State/Users/Hooks.ts index e0a12923..cbd01409 100644 --- a/src/State/Users/Hooks.ts +++ b/src/State/Users/Hooks.ts @@ -4,7 +4,6 @@ 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) { const db = useDb(); From b45bcef1fb6f7cd18cd4bff3288198488efebccc Mon Sep 17 00:00:00 2001 From: ennmichael Date: Wed, 8 Feb 2023 22:33:04 +0100 Subject: [PATCH 4/8] add scripts to package.json --- .github/workflows/eslint.yaml | 2 +- .github/workflows/formatting.yaml | 2 +- package.json | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/eslint.yaml b/.github/workflows/eslint.yaml index 56905c02..f6361175 100644 --- a/.github/workflows/eslint.yaml +++ b/.github/workflows/eslint.yaml @@ -20,4 +20,4 @@ jobs: run: npm i - name: Check Eslint - run: npx eslint . + run: npm run eslint diff --git a/.github/workflows/formatting.yaml b/.github/workflows/formatting.yaml index 6e51f36b..97608b5f 100644 --- a/.github/workflows/formatting.yaml +++ b/.github/workflows/formatting.yaml @@ -20,4 +20,4 @@ jobs: run: npm i - name: Check Formatting - run: npx prettier --check . + run: npm run format diff --git a/package.json b/package.json index a1266c3c..1c23b81d 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,9 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "generate-messages": "extract-messages -l=en,es,zh,ja -o src/translations -d en --flat true **/messages.js" + "generate-messages": "extract-messages -l=en,es,zh,ja -o src/translations -d en --flat true **/messages.js", + "format": "prettier --write .", + "eslint": "eslint ." }, "eslintConfig": { "extends": [ From 45a210fdb528fd17bc0d757b828c17e7af6fadd0 Mon Sep 17 00:00:00 2001 From: ennmichael Date: Wed, 8 Feb 2023 23:04:55 +0100 Subject: [PATCH 5/8] rebase 2 --- src/Element/NoteFooter.tsx | 2 +- src/Element/Reactions.tsx | 4 ++-- src/Pages/settings/messages.js | 1 - transifex.yml | 8 ++++---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Element/NoteFooter.tsx b/src/Element/NoteFooter.tsx index 54859a64..519d82f3 100644 --- a/src/Element/NoteFooter.tsx +++ b/src/Element/NoteFooter.tsx @@ -31,7 +31,7 @@ import { import { NoteCreator } from "Element/NoteCreator"; import Reactions from "Element/Reactions"; import SendSats from "Element/SendSats"; -import { parseZap, ParsedZap, ZapsSummary } from "Element/Zap"; +import { parseZap, ZapsSummary } from "Element/Zap"; import { useUserProfile } from "Feed/ProfileFeed"; import { default as NEvent } from "Nostr/Event"; import { RootState } from "State/Store"; diff --git a/src/Element/Reactions.tsx b/src/Element/Reactions.tsx index 44e87d94..cee3721d 100644 --- a/src/Element/Reactions.tsx +++ b/src/Element/Reactions.tsx @@ -126,10 +126,10 @@ const Reactions = ({ {formatShort(z.amount)}
    {z.content}} /> - +
    ); })} diff --git a/src/Pages/settings/messages.js b/src/Pages/settings/messages.js index 72b3f144..ac2bc762 100644 --- a/src/Pages/settings/messages.js +++ b/src/Pages/settings/messages.js @@ -49,7 +49,6 @@ const messages = defineMessages({ Add: "Add", AddRelays: "Add Relays", Name: "Name", - Profile: "Profile", Website: "Website", Save: "Save", DisplayName: "Display name", diff --git a/transifex.yml b/transifex.yml index 38ed5470..40f96479 100644 --- a/transifex.yml +++ b/transifex.yml @@ -1,6 +1,6 @@ git: filters: - - filter_type: dir - file_format: KEYVALUEJSON - source_file_dir: 'src/translations' - translation_files_expression: 'src/translations/.json' + - filter_type: dir + file_format: KEYVALUEJSON + source_file_dir: "src/translations" + translation_files_expression: "src/translations/.json" From a03b385e555b0c8583f744b7d1404c22be9d741b Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 9 Feb 2023 10:28:42 +0000 Subject: [PATCH 6/8] update workflows --- .github/workflows/eslint.yaml | 13 +++++-------- .github/workflows/formatting.yaml | 15 +++++---------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/.github/workflows/eslint.yaml b/.github/workflows/eslint.yaml index f6361175..af3ef571 100644 --- a/.github/workflows/eslint.yaml +++ b/.github/workflows/eslint.yaml @@ -1,10 +1,8 @@ -name: pull request - +name: Linting on: pull_request: push: branches: [main] - jobs: formatting: timeout-minutes: 15 @@ -12,12 +10,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Node.js uses: actions/setup-node@v3 - + with: + node-version: 16 - name: Install Dependencies - run: npm i - + run: yarn install - name: Check Eslint - run: npm run eslint + run: yarn eslint diff --git a/.github/workflows/formatting.yaml b/.github/workflows/formatting.yaml index 97608b5f..25177e3a 100644 --- a/.github/workflows/formatting.yaml +++ b/.github/workflows/formatting.yaml @@ -1,10 +1,6 @@ -name: pull request - +name: Formatting on: pull_request: - push: - branches: [main] - jobs: formatting: timeout-minutes: 15 @@ -12,12 +8,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup Node.js uses: actions/setup-node@v3 - + with: + node-version: 16 - name: Install Dependencies - run: npm i - + run: yarn Install - name: Check Formatting - run: npm run format + run: yarn prettier --check . From dbae89837fefa1d2966b2b3c5ea337a041804396 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 9 Feb 2023 12:26:54 +0000 Subject: [PATCH 7/8] review / cleanup --- .dockerignore | 5 + .prettierrc.json | 6 +- Dockerfile | 0 d.ts | 14 ++ public/index.html | 8 +- src/Const.ts | 22 +-- src/Db/index.ts | 4 +- src/Element/AsyncButton.tsx | 3 +- src/Element/Avatar.tsx | 19 +- src/Element/BlockList.tsx | 30 +-- src/Element/Collapsed.tsx | 7 +- src/Element/Copy.tsx | 16 +- src/Element/DM.tsx | 20 +- src/Element/FollowButton.tsx | 13 +- src/Element/FollowListBase.tsx | 13 +- src/Element/FollowersList.tsx | 13 +- src/Element/FollowsList.tsx | 7 +- src/Element/FollowsYou.tsx | 8 +- src/Element/Hashtag.tsx | 2 +- src/Element/HyperText.tsx | 43 +---- src/Element/Invoice.tsx | 43 ++--- src/Element/LNURLTip.css | 59 ------ src/Element/LNURLTip.tsx | 301 ----------------------------- src/Element/LoadMore.tsx | 2 +- src/Element/LogoutButton.tsx | 3 +- src/Element/Mention.tsx | 2 +- src/Element/MixCloudEmbed.tsx | 9 +- src/Element/Modal.tsx | 5 +- src/Element/MutedList.tsx | 19 +- src/Element/Nip05.tsx | 64 ++---- src/Element/Nip5Service.tsx | 59 ++---- src/Element/Note.tsx | 95 ++------- src/Element/NoteCreator.tsx | 12 +- src/Element/NoteFooter.tsx | 115 +++-------- src/Element/NoteReaction.tsx | 12 +- src/Element/NoteTime.tsx | 7 +- src/Element/NoteToSelf.tsx | 17 +- src/Element/ProfileImage.tsx | 24 +-- src/Element/ProfilePreview.tsx | 13 +- src/Element/ProxyImg.tsx | 8 +- src/Element/Reactions.tsx | 50 ++--- src/Element/Relay.tsx | 24 +-- src/Element/SendSats.tsx | 64 ++---- src/Element/Skeleton.css | 8 +- src/Element/Skeleton.tsx | 13 +- src/Element/SoundCloudEmded.tsx | 3 +- src/Element/SpotifyEmbed.tsx | 8 +- src/Element/Tabs.tsx | 9 +- src/Element/Text.tsx | 51 ++--- src/Element/Textarea.tsx | 7 +- src/Element/Thread.css | 6 +- src/Element/Thread.tsx | 141 +++----------- src/Element/TidalEmbed.tsx | 24 +-- src/Element/Timeline.tsx | 41 ++-- src/Element/Zap.tsx | 55 ++---- src/Element/messages.js | 3 +- src/Feed/EventPublisher.ts | 75 ++----- src/Feed/FollowsFeed.ts | 8 +- src/Feed/ImgProxy.ts | 8 +- src/Feed/LoginFeed.ts | 58 ++---- src/Feed/MuteList.ts | 6 +- src/Feed/ProfileFeed.ts | 6 +- src/Feed/Subscription.ts | 30 ++- src/Feed/ThreadFeed.ts | 26 +-- src/Feed/TimelineFeed.ts | 43 ++--- src/Hooks/useModeration.tsx | 4 +- src/Icons/ArrowBack.tsx | 8 +- src/Icons/ArrowFront.tsx | 16 +- src/Icons/Attachment.tsx | 8 +- src/Icons/Bell.tsx | 8 +- src/Icons/Check.tsx | 17 +- src/Icons/Close.tsx | 9 +- src/Icons/Copy.tsx | 9 +- src/Icons/Dislike.tsx | 9 +- src/Icons/Dots.tsx | 8 +- src/Icons/Envelope.tsx | 9 +- src/Icons/Gear.tsx | 9 +- src/Icons/Heart.tsx | 9 +- src/Icons/Link.tsx | 8 +- src/Icons/Logout.tsx | 8 +- src/Icons/Plus.tsx | 16 +- src/Icons/Profile.tsx | 9 +- src/Icons/Qr.tsx | 9 +- src/Icons/Relay.tsx | 9 +- src/Icons/Reply.tsx | 8 +- src/Icons/Search.tsx | 8 +- src/Icons/Zap.tsx | 9 +- src/Icons/ZapCircle.tsx | 9 +- src/IntlProvider.tsx | 6 +- src/Nip05/ServiceProvider.ts | 26 +-- src/Nostr/Connection.ts | 38 ++-- src/Nostr/Event.ts | 22 +-- src/Nostr/System.ts | 25 ++- src/Nostr/Tag.ts | 7 +- src/Nostr/Thread.ts | 18 +- src/Nostr/index.ts | 5 + src/Notifications.ts | 16 +- src/Pages/ChatPage.tsx | 36 ++-- src/Pages/DonatePage.tsx | 57 ++---- src/Pages/HashTagsPage.tsx | 3 +- src/Pages/HelpPage.tsx | 3 +- src/Pages/Layout.tsx | 48 ++--- src/Pages/Login.tsx | 19 +- src/Pages/MessagesPage.tsx | 48 ++--- src/Pages/Notifications.tsx | 4 +- src/Pages/ProfilePage.tsx | 67 ++----- src/Pages/Root.tsx | 15 +- src/Pages/SearchPage.tsx | 2 +- src/Pages/Verification.tsx | 2 +- src/Pages/messages.js | 6 +- src/Pages/new/DiscoverFollows.tsx | 4 +- src/Pages/new/ImportFollows.tsx | 14 +- src/Pages/new/index.tsx | 39 ++-- src/Pages/settings/Preferences.tsx | 51 ++--- src/Pages/settings/Profile.tsx | 57 ++---- src/Pages/settings/RelayInfo.tsx | 25 +-- src/Pages/settings/Relays.tsx | 8 +- src/Pages/settings/messages.js | 3 +- src/State/Login.ts | 64 ++---- src/State/Users/Db.ts | 13 +- src/State/Users/Hooks.ts | 10 +- src/Upload/NostrBuild.ts | 4 +- src/Upload/NostrImg.ts | 4 +- src/Upload/VoidCat.ts | 9 +- src/Upload/index.ts | 4 +- src/Util.ts | 23 +-- src/index.css | 31 +-- src/service-worker.js | 15 +- src/serviceWorkerRegistration.js | 25 +-- 129 files changed, 678 insertions(+), 2303 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile delete mode 100644 src/Element/LNURLTip.css delete mode 100644 src/Element/LNURLTip.tsx diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..5cda1d84 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules/ +.github/ +.vscode/ +build/ +yarn-error.log \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 0967ef42..e1280035 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1 +1,5 @@ -{} +{ + "printWidth": 120, + "bracketSameLine": true, + "arrowParens": "avoid" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e69de29b diff --git a/d.ts b/d.ts index 2bf20721..8efeccf6 100644 --- a/d.ts +++ b/d.ts @@ -12,3 +12,17 @@ declare module "*.webp" { const value: string; export default value; } + +declare module "light-bolt11-decoder" { + export function decode(pr?: string): ParsedInvoice; + + export interface ParsedInvoice { + paymentRequest: string; + sections: Section[]; + } + + export interface Section { + name: string; + value: string | Uint8Array | number | undefined; + } +} diff --git a/public/index.html b/public/index.html index e8779ebf..1cbe83b2 100644 --- a/public/index.html +++ b/public/index.html @@ -3,16 +3,12 @@ - + + content="default-src 'self'; child-src 'none'; worker-src 'self'; frame-src youtube.com www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self' https://*; img-src * data:; font-src https://fonts.gstatic.com; media-src *; script-src 'self' https://static.cloudflareinsights.com https://platform.twitter.com https://embed.tidal.com;" /> diff --git a/src/Const.ts b/src/Const.ts index ac6c8290..a037de0d 100644 --- a/src/Const.ts +++ b/src/Const.ts @@ -18,14 +18,12 @@ export const VoidCatHost = "https://void.cat"; /** * Kierans pubkey */ -export const KieranPubKey = - "npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49"; +export const KieranPubKey = "npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49"; /** * Official snort account */ -export const SnortPubKey = - "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws"; +export const SnortPubKey = "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws"; /** * Websocket re-connect timeout @@ -49,9 +47,7 @@ export const DefaultRelays = new Map([ /** * Default search relays */ -export const SearchRelays = new Map([ - ["wss://relay.nostr.band", { read: true, write: false }], -]); +export const SearchRelays = new Map([["wss://relay.nostr.band", { read: true, write: false }]]); /** * List of recommended follows for new users @@ -118,8 +114,7 @@ export const YoutubeUrlRegex = /** * Tweet Regex */ -export const TweetUrlRegex = - /https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/; +export const TweetUrlRegex = /https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/; /** * Hashtag regex @@ -135,15 +130,12 @@ export const TidalRegex = /tidal\.com\/(?:browse\/)?(\w+)\/([a-z0-9-]+)/i; /** * SoundCloud regex */ -export const SoundCloudRegex = - /soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/; +export const SoundCloudRegex = /soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/; /** * Mixcloud regex */ -export const MixCloudRegex = - /mixcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/; +export const MixCloudRegex = /mixcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/; -export const SpotifyRegex = - /open\.spotify\.com\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/; +export const SpotifyRegex = /open\.spotify\.com\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/; diff --git a/src/Db/index.ts b/src/Db/index.ts index 59cf5fdf..89276478 100644 --- a/src/Db/index.ts +++ b/src/Db/index.ts @@ -28,11 +28,11 @@ export class SnortDB extends Dexie { super(NAME); this.version(VERSION) .stores(STORES) - .upgrade(async (tx) => { + .upgrade(async tx => { await tx .table("users") .toCollection() - .modify((user) => { + .modify(user => { user.npub = hexToBech32("npub", user.pubkey); }); }); diff --git a/src/Element/AsyncButton.tsx b/src/Element/AsyncButton.tsx index 068c40c3..59bdaf86 100644 --- a/src/Element/AsyncButton.tsx +++ b/src/Element/AsyncButton.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; -interface AsyncButtonProps - extends React.ButtonHTMLAttributes { +interface AsyncButtonProps extends React.ButtonHTMLAttributes { onClick(e: React.MouseEvent): Promise | void; children?: React.ReactNode; } diff --git a/src/Element/Avatar.tsx b/src/Element/Avatar.tsx index a4f7b905..97f5a9ea 100644 --- a/src/Element/Avatar.tsx +++ b/src/Element/Avatar.tsx @@ -4,20 +4,14 @@ import { CSSProperties, useEffect, useState } from "react"; import type { UserMetadata } from "Nostr"; import useImgProxy from "Feed/ImgProxy"; -const Avatar = ({ - user, - ...rest -}: { - user?: UserMetadata; - onClick?: () => void; -}) => { +const Avatar = ({ user, ...rest }: { user?: UserMetadata; onClick?: () => void }) => { const [url, setUrl] = useState(Nostrich); const { proxy } = useImgProxy(); useEffect(() => { if (user?.picture) { proxy(user.picture, 120) - .then((a) => setUrl(a)) + .then(a => setUrl(a)) .catch(console.warn); } }, [user]); @@ -25,14 +19,7 @@ const Avatar = ({ const backgroundImage = `url(${url})`; const style = { "--img-url": backgroundImage } as CSSProperties; const domain = user?.nip05 && user.nip05.split("@")[1]; - return ( -
    - ); + return
    ; }; export default Avatar; diff --git a/src/Element/BlockList.tsx b/src/Element/BlockList.tsx index e6ba6c83..33e07e61 100644 --- a/src/Element/BlockList.tsx +++ b/src/Element/BlockList.tsx @@ -18,39 +18,21 @@ export default function BlockList({ variant }: BlockListProps) { {variant === "muted" && ( <>

    - +

    - {muted.map((a) => { - return ( - } - pubkey={a} - options={{ about: false }} - key={a} - /> - ); + {muted.map(a => { + return } pubkey={a} options={{ about: false }} key={a} />; })} )} {variant === "blocked" && ( <>

    - +

    - {blocked.map((a) => { + {blocked.map(a => { return ( - } - pubkey={a} - options={{ about: false }} - key={a} - /> + } pubkey={a} options={{ about: false }} key={a} /> ); })} diff --git a/src/Element/Collapsed.tsx b/src/Element/Collapsed.tsx index b1f282e6..8a69e989 100644 --- a/src/Element/Collapsed.tsx +++ b/src/Element/Collapsed.tsx @@ -9,12 +9,7 @@ interface CollapsedProps { setCollapsed(b: boolean): void; } -const Collapsed = ({ - text, - children, - collapsed, - setCollapsed, -}: CollapsedProps) => { +const Collapsed = ({ text, children, collapsed, setCollapsed }: CollapsedProps) => { return collapsed ? (
    setCollapsed(false)} /> diff --git a/src/Element/Copy.tsx b/src/Element/Copy.tsx index 10ea2311..cc3f6765 100644 --- a/src/Element/Copy.tsx +++ b/src/Element/Copy.tsx @@ -10,23 +10,13 @@ export interface CopyProps { export default function Copy({ text, maxSize = 32 }: CopyProps) { const { copy, copied } = useCopy(); const sliceLength = maxSize / 2; - const trimmed = - text.length > maxSize - ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` - : text; + const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text; return (
    copy(text)}> {trimmed} - - {copied ? ( - - ) : ( - - )} + + {copied ? : }
    ); diff --git a/src/Element/DM.tsx b/src/Element/DM.tsx index 0c0d871c..fdf59a4c 100644 --- a/src/Element/DM.tsx +++ b/src/Element/DM.tsx @@ -22,18 +22,14 @@ export type DMProps = { export default function DM(props: DMProps) { const dispatch = useDispatch(); - const pubKey = useSelector( - (s) => s.login.publicKey - ); + const pubKey = useSelector(s => s.login.publicKey); const publisher = useEventPublisher(); const [content, setContent] = useState("Loading..."); const [decrypted, setDecrypted] = useState(false); const { ref, inView } = useInView(); const { formatMessage } = useIntl(); const isMe = props.data.pubkey === pubKey; - const otherPubkey = isMe - ? pubKey - : unwrap(props.data.tags.find((a) => a[0] === "p")?.[1]); + const otherPubkey = isMe ? pubKey : unwrap(props.data.tags.find(a => a[0] === "p")?.[1]); async function decrypt() { const e = new Event(props.data); @@ -55,18 +51,10 @@ export default function DM(props: DMProps) { return (
    - +
    - +
    ); diff --git a/src/Element/FollowButton.tsx b/src/Element/FollowButton.tsx index da7b03a5..050234ad 100644 --- a/src/Element/FollowButton.tsx +++ b/src/Element/FollowButton.tsx @@ -15,9 +15,7 @@ export interface FollowButtonProps { export default function FollowButton(props: FollowButtonProps) { const pubkey = parseId(props.pubkey); const publiser = useEventPublisher(); - const isFollowing = useSelector( - (s) => s.login.follows?.includes(pubkey) ?? false - ); + const isFollowing = useSelector(s => s.login.follows?.includes(pubkey) ?? false); const baseClassname = `${props.className} follow-button`; async function follow(pubkey: HexKey) { @@ -34,13 +32,8 @@ export default function FollowButton(props: FollowButtonProps) { ); } diff --git a/src/Element/FollowListBase.tsx b/src/Element/FollowListBase.tsx index d421d1d6..50931357 100644 --- a/src/Element/FollowListBase.tsx +++ b/src/Element/FollowListBase.tsx @@ -10,10 +10,7 @@ export interface FollowListBaseProps { pubkeys: HexKey[]; title?: string; } -export default function FollowListBase({ - pubkeys, - title, -}: FollowListBaseProps) { +export default function FollowListBase({ pubkeys, title }: FollowListBaseProps) { const publisher = useEventPublisher(); async function followAll() { @@ -25,15 +22,11 @@ export default function FollowListBase({
    {title}
    -
    - {pubkeys?.map((a) => ( + {pubkeys?.map(a => ( ))}
    diff --git a/src/Element/FollowersList.tsx b/src/Element/FollowersList.tsx index 7fb169eb..9e1eaaa7 100644 --- a/src/Element/FollowersList.tsx +++ b/src/Element/FollowersList.tsx @@ -18,17 +18,10 @@ export default function FollowersList({ pubkey }: FollowersListProps) { const pubkeys = useMemo(() => { const contactLists = feed?.store.notes.filter( - (a) => - a.kind === EventKind.ContactList && - a.tags.some((b) => b[0] === "p" && b[1] === pubkey) + a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey) ); - return [...new Set(contactLists?.map((a) => a.pubkey))]; + return [...new Set(contactLists?.map(a => a.pubkey))]; }, [feed, pubkey]); - return ( - - ); + return ; } diff --git a/src/Element/FollowsList.tsx b/src/Element/FollowsList.tsx index a6557d97..42931845 100644 --- a/src/Element/FollowsList.tsx +++ b/src/Element/FollowsList.tsx @@ -20,10 +20,5 @@ export default function FollowsList({ pubkey }: FollowsListProps) { return getFollowers(feed.store, pubkey); }, [feed, pubkey]); - return ( - - ); + return ; } diff --git a/src/Element/FollowsYou.tsx b/src/Element/FollowsYou.tsx index cc49dd48..581dbda5 100644 --- a/src/Element/FollowsYou.tsx +++ b/src/Element/FollowsYou.tsx @@ -17,9 +17,7 @@ export interface FollowsYouProps { export default function FollowsYou({ pubkey }: FollowsYouProps) { const { formatMessage } = useIntl(); const feed = useFollowsFeed(pubkey); - const loginPubKey = useSelector( - (s) => s.login.publicKey - ); + const loginPubKey = useSelector(s => s.login.publicKey); const pubkeys = useMemo(() => { return getFollowers(feed.store, pubkey); @@ -27,7 +25,5 @@ export default function FollowsYou({ pubkey }: FollowsYouProps) { const followsMe = loginPubKey ? pubkeys.includes(loginPubKey) : false; - return followsMe ? ( - {formatMessage(messages.FollowsYou)} - ) : null; + return followsMe ? {formatMessage(messages.FollowsYou)} : null; } diff --git a/src/Element/Hashtag.tsx b/src/Element/Hashtag.tsx index f6046425..0bc5a2ad 100644 --- a/src/Element/Hashtag.tsx +++ b/src/Element/Hashtag.tsx @@ -4,7 +4,7 @@ import "./Hashtag.css"; const Hashtag = ({ tag }: { tag: string }) => { return ( - e.stopPropagation()}> + e.stopPropagation()}> #{tag} diff --git a/src/Element/HyperText.tsx b/src/Element/HyperText.tsx index 2d4f1b05..1f096e44 100644 --- a/src/Element/HyperText.tsx +++ b/src/Element/HyperText.tsx @@ -19,30 +19,17 @@ import TidalEmbed from "Element/TidalEmbed"; import { ProxyImg } from "Element/ProxyImg"; import { HexKey } from "Nostr"; -export default function HyperText({ - link, - creator, -}: { - link: string; - creator: HexKey; -}) { +export default function HyperText({ link, creator }: { link: string; creator: HexKey }) { const pref = useSelector((s: RootState) => s.login.preferences); const follows = useSelector((s: RootState) => s.login.follows); const render = useCallback(() => { const a = link; try { - const hideNonFollows = - pref.autoLoadMedia === "follows-only" && !follows.includes(creator); + const hideNonFollows = pref.autoLoadMedia === "follows-only" && !follows.includes(creator); if (pref.autoLoadMedia === "none" || hideNonFollows) { return ( - e.stopPropagation()} - target="_blank" - rel="noreferrer" - className="ext" - > + e.stopPropagation()} target="_blank" rel="noreferrer" className="ext"> {a} ); @@ -54,8 +41,7 @@ export default function HyperText({ const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1; const mixcloudId = MixCloudRegex.test(a) && RegExp.$1; const spotifyId = SpotifyRegex.test(a); - const extension = - FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1; + const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1; if (extension) { switch (extension) { case "gif": @@ -83,11 +69,10 @@ export default function HyperText({ e.stopPropagation()} + onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" - className="ext" - > + className="ext"> {url.toString()} ); @@ -124,13 +109,7 @@ export default function HyperText({ return ; } else { return ( - e.stopPropagation()} - target="_blank" - rel="noreferrer" - className="ext" - > + e.stopPropagation()} target="_blank" rel="noreferrer" className="ext"> {a} ); @@ -139,13 +118,7 @@ export default function HyperText({ // Ignore the error. } return ( - e.stopPropagation()} - target="_blank" - rel="noreferrer" - className="ext" - > + e.stopPropagation()} target="_blank" rel="noreferrer" className="ext"> {a} ); diff --git a/src/Element/Invoice.tsx b/src/Element/Invoice.tsx index 56958597..85539bf1 100644 --- a/src/Element/Invoice.tsx +++ b/src/Element/Invoice.tsx @@ -1,7 +1,6 @@ import "./Invoice.css"; import { useState } from "react"; import { useIntl, FormattedMessage } from "react-intl"; -// @ts-expect-error No types available import { decode as invoiceDecode } from "light-bolt11-decoder"; import { useMemo } from "react"; import SendSats from "Element/SendSats"; @@ -14,10 +13,6 @@ export interface InvoiceProps { invoice: string; } -interface Section { - name: string; -} - export default function Invoice(props: InvoiceProps) { const invoice = props.invoice; const webln = useWebln(); @@ -28,22 +23,19 @@ export default function Invoice(props: InvoiceProps) { try { const parsed = invoiceDecode(invoice); - const amount = parseInt( - parsed.sections.find((a: Section) => a.name === "amount")?.value - ); - const timestamp = parseInt( - parsed.sections.find((a: Section) => a.name === "timestamp")?.value - ); - const expire = parseInt( - parsed.sections.find((a: Section) => a.name === "expiry")?.value - ); - const description = parsed.sections.find( - (a: Section) => a.name === "description" - )?.value; + const amountSection = parsed.sections.find(a => a.name === "amount"); + const amount = amountSection ? (amountSection.value as number) : NaN; + + const timestampSection = parsed.sections.find(a => a.name === "timestamp"); + const timestamp = timestampSection ? (timestampSection.value as number) : NaN; + + const expirySection = parsed.sections.find(a => a.name === "expiry"); + const expire = expirySection ? (expirySection.value as number) : NaN; + const descriptionSection = parsed.sections.find(a => a.name === "description")?.value; const ret = { amount: !isNaN(amount) ? amount / 1000 : 0, expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null, - description, + description: descriptionSection as string | undefined, expired: false, }; if (ret.expire) { @@ -93,18 +85,13 @@ export default function Invoice(props: InvoiceProps) { return ( <> -
    +
    {header()}

    {amount > 0 && ( <> - {amount.toLocaleString()}{" "} - sat{amount === 1 ? "" : "s"} + {amount.toLocaleString()} sat{amount === 1 ? "" : "s"} )}

    @@ -117,11 +104,7 @@ export default function Invoice(props: InvoiceProps) {
    ) : ( )}
    diff --git a/src/Element/LNURLTip.css b/src/Element/LNURLTip.css deleted file mode 100644 index 917f4c2c..00000000 --- a/src/Element/LNURLTip.css +++ /dev/null @@ -1,59 +0,0 @@ -.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 deleted file mode 100644 index 2ff53b01..00000000 --- a/src/Element/LNURLTip.tsx +++ /dev/null @@ -1,301 +0,0 @@ -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 4bbfad79..8982c375 100644 --- a/src/Element/LoadMore.tsx +++ b/src/Element/LoadMore.tsx @@ -24,7 +24,7 @@ export default function LoadMore({ useEffect(() => { const t = setInterval(() => { - setTick((x) => (x += 1)); + setTick(x => (x += 1)); }, 500); return () => clearInterval(t); }, []); diff --git a/src/Element/LogoutButton.tsx b/src/Element/LogoutButton.tsx index f3947f7c..53659aad 100644 --- a/src/Element/LogoutButton.tsx +++ b/src/Element/LogoutButton.tsx @@ -16,8 +16,7 @@ export default function LogoutButton() { onClick={() => { dispatch(logout()); navigate("/"); - }} - > + }}> ); diff --git a/src/Element/Mention.tsx b/src/Element/Mention.tsx index c38140c8..1c03115a 100644 --- a/src/Element/Mention.tsx +++ b/src/Element/Mention.tsx @@ -18,7 +18,7 @@ export default function Mention({ pubkey }: { pubkey: HexKey }) { }, [user, pubkey]); return ( - e.stopPropagation()}> + e.stopPropagation()}> @{name} ); diff --git a/src/Element/MixCloudEmbed.tsx b/src/Element/MixCloudEmbed.tsx index 2c8a4369..c24629a0 100644 --- a/src/Element/MixCloudEmbed.tsx +++ b/src/Element/MixCloudEmbed.tsx @@ -3,14 +3,9 @@ import { useSelector } from "react-redux"; import { RootState } from "State/Store"; const MixCloudEmbed = ({ link }: { link: string }) => { - const feedPath = - (MixCloudRegex.test(link) && RegExp.$1) + - "%2F" + - (MixCloudRegex.test(link) && RegExp.$2); + const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + (MixCloudRegex.test(link) && RegExp.$2); - const lightTheme = useSelector( - (s) => s.login.preferences.theme === "light" - ); + const lightTheme = useSelector(s => s.login.preferences.theme === "light"); const lightParams = lightTheme ? "light=1" : "light=0"; diff --git a/src/Element/Modal.tsx b/src/Element/Modal.tsx index d136c076..15c6b9fd 100644 --- a/src/Element/Modal.tsx +++ b/src/Element/Modal.tsx @@ -8,10 +8,7 @@ export interface ModalProps { children: React.ReactNode; } -function useOnClickOutside( - ref: React.MutableRefObject, - onClickOutside: () => void -) { +function useOnClickOutside(ref: React.MutableRefObject, onClickOutside: () => void) { useEffect(() => { function handleClickOutside(ev: MouseEvent) { if (ref && ref.current && !ref.current.contains(ev.target as Node)) { diff --git a/src/Element/MutedList.tsx b/src/Element/MutedList.tsx index ee59ac05..8a700548 100644 --- a/src/Element/MutedList.tsx +++ b/src/Element/MutedList.tsx @@ -24,29 +24,18 @@ export default function MutedList({ pubkey }: MutedListProps) {
    - +
    - {pubkeys?.map((a) => { - return ( - } - pubkey={a} - options={{ about: false }} - key={a} - /> - ); + {pubkeys?.map(a => { + return } pubkey={a} options={{ about: false }} key={a} />; })}
    ); diff --git a/src/Element/Nip05.tsx b/src/Element/Nip05.tsx index fadd5c6f..0feaaf12 100644 --- a/src/Element/Nip05.tsx +++ b/src/Element/Nip05.tsx @@ -1,11 +1,7 @@ import { useQuery } from "react-query"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faCircleCheck, - faSpinner, - faTriangleExclamation, -} from "@fortawesome/free-solid-svg-icons"; +import { faCircleCheck, faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import "./Nip05.css"; import { HexKey } from "Nostr"; @@ -19,13 +15,9 @@ async function fetchNip05Pubkey(name: string, domain: string) { return undefined; } try { - const res = await fetch( - `https://${domain}/.well-known/nostr.json?name=${encodeURIComponent( - name - )}` - ); + const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`); const data: NostrJson = await res.json(); - const match = Object.keys(data.names).find((n) => { + const match = Object.keys(data.names).find(n => { return n.toLowerCase() === name.toLowerCase(); }); return match ? data.names[match] : undefined; @@ -39,16 +31,12 @@ const VERIFICATION_STALE_TIMEOUT = 10 * 60 * 1000; export function useIsVerified(pubkey: HexKey, nip05?: string) { const [name, domain] = nip05 ? nip05.split("@") : []; - const { isError, isSuccess, data } = useQuery( - ["nip05", nip05], - () => fetchNip05Pubkey(name, domain), - { - retry: false, - retryOnMount: false, - cacheTime: VERIFICATION_CACHE_TIME, - staleTime: VERIFICATION_STALE_TIMEOUT, - } - ); + const { isError, isSuccess, data } = useQuery(["nip05", nip05], () => fetchNip05Pubkey(name, domain), { + retry: false, + retryOnMount: false, + cacheTime: VERIFICATION_CACHE_TIME, + staleTime: VERIFICATION_STALE_TIMEOUT, + }); const isVerified = isSuccess && data === pubkey; const cantVerify = isSuccess && data !== pubkey; return { isVerified, couldNotVerify: isError || cantVerify }; @@ -62,42 +50,18 @@ export interface Nip05Params { const Nip05 = (props: Nip05Params) => { const [name, domain] = props.nip05 ? props.nip05.split("@") : []; const isDefaultUser = name === "_"; - const { isVerified, couldNotVerify } = useIsVerified( - props.pubkey, - props.nip05 - ); + const { isVerified, couldNotVerify } = useIsVerified(props.pubkey, props.nip05); return ( -
    ev.stopPropagation()} - > +
    ev.stopPropagation()}> {!isDefaultUser &&
    {`${name}@`}
    } {domain} - {isVerified && ( - - )} - {!isVerified && !couldNotVerify && ( - - )} - {couldNotVerify && ( - - )} + {isVerified && } + {!isVerified && !couldNotVerify && } + {couldNotVerify && }
    ); diff --git a/src/Element/Nip5Service.tsx b/src/Element/Nip5Service.tsx index a41711c6..f5289760 100644 --- a/src/Element/Nip5Service.tsx +++ b/src/Element/Nip5Service.tsx @@ -20,6 +20,7 @@ import { debounce, hexToBech32 } from "Util"; import { UserMetadata } from "Nostr"; import messages from "./messages"; +import { RootState } from "State/Store"; type Nip05ServiceProps = { name: string; @@ -29,47 +30,34 @@ type Nip05ServiceProps = { supportLink: string; }; -interface ReduxStore { - login: { publicKey: string }; -} - export default function Nip5Service(props: Nip05ServiceProps) { const navigate = useNavigate(); const { formatMessage } = useIntl(); - const pubkey = useSelector((s) => s.login.publicKey); + const pubkey = useSelector((s: RootState) => s.login.publicKey); const user = useUserProfile(pubkey); const publisher = useEventPublisher(); - const svc = useMemo( - () => new ServiceProvider(props.service), - [props.service] - ); + const svc = useMemo(() => new ServiceProvider(props.service), [props.service]); const [serviceConfig, setServiceConfig] = useState(); const [error, setError] = useState(); const [handle, setHandle] = useState(""); const [domain, setDomain] = useState(""); - const [availabilityResponse, setAvailabilityResponse] = - useState(); - const [registerResponse, setRegisterResponse] = - useState(); + const [availabilityResponse, setAvailabilityResponse] = useState(); + const [registerResponse, setRegisterResponse] = useState(); const [showInvoice, setShowInvoice] = useState(false); const [registerStatus, setRegisterStatus] = useState(); - const domainConfig = useMemo( - () => serviceConfig?.domains.find((a) => a.name === domain), - [domain, serviceConfig] - ); + const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain, serviceConfig]); useEffect(() => { svc .GetConfig() - .then((a) => { + .then(a => { if ("error" in a) { setError(a as ServiceError); } else { const svc = a as ServiceConfig; setServiceConfig(svc); - const defaultDomain = - svc.domains.find((a) => a.default)?.name || svc.domains[0].name; + const defaultDomain = svc.domains.find(a => a.default)?.name || svc.domains[0].name; setDomain(defaultDomain); } }) @@ -88,10 +76,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { setAvailabilityResponse({ available: false, why: "TOO_LONG" }); return; } - const rx = new RegExp( - domainConfig?.regex[0] ?? "", - domainConfig?.regex[1] ?? "" - ); + const rx = new RegExp(domainConfig?.regex[0] ?? "", domainConfig?.regex[1] ?? ""); if (!rx.test(handle)) { setAvailabilityResponse({ available: false, why: "REGEX" }); return; @@ -99,7 +84,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { return debounce(500, () => { svc .CheckAvailable(handle, domain) - .then((a) => { + .then(a => { if ("error" in a) { setError(a as ServiceError); } else { @@ -133,10 +118,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { } }, [registerResponse, showInvoice, svc]); - function mapError( - e: ServiceErrorCode | undefined, - t: string | null - ): string | undefined { + function mapError(e: ServiceErrorCode | undefined, t: string | null): string | undefined { if (e === undefined) { return undefined; } @@ -152,8 +134,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { } async function startBuy(handle: string, domain: string) { - if (registerResponse) { - setShowInvoice(true); + if (!pubkey) { return; } @@ -202,11 +183,11 @@ export default function Nip5Service(props: Nip05ServiceProps) { type="text" placeholder="Handle" value={handle} - onChange={(e) => setHandle(e.target.value.toLowerCase())} + onChange={e => setHandle(e.target.value.toLowerCase())} />  @  - setDomain(e.target.value)}> + {serviceConfig?.domains.map(a => ( ))} @@ -215,10 +196,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { {availabilityResponse?.available && !registerStatus && (
    - +
    {availabilityResponse.quote?.data.type}
    @@ -238,10 +216,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
    {" "} - {mapError( - availabilityResponse.why, - availabilityResponse.reasonTag || null - )} + {mapError(availabilityResponse.why, availabilityResponse.reasonTag || null)}
    )} diff --git a/src/Element/Note.tsx b/src/Element/Note.tsx index 9818dcff..7a02a892 100644 --- a/src/Element/Note.tsx +++ b/src/Element/Note.tsx @@ -1,11 +1,5 @@ import "./Note.css"; -import { - useCallback, - useMemo, - useState, - useLayoutEffect, - ReactNode, -} from "react"; +import { useCallback, useMemo, useState, useLayoutEffect, ReactNode } from "react"; import { useNavigate, Link } from "react-router-dom"; import { useInView } from "react-intersection-observer"; import { useIntl, FormattedMessage } from "react-intl"; @@ -58,21 +52,11 @@ const HiddenNote = ({ children }: { children: React.ReactNode }) => { export default function Note(props: NoteProps) { const navigate = useNavigate(); - const { - data, - related, - highlight, - options: opt, - ["data-ev"]: parsedEvent, - ignoreModeration = false, - } = props; + const { data, related, highlight, options: opt, ["data-ev"]: parsedEvent, ignoreModeration = false } = props; const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]); const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]); const users = useUserProfiles(pubKeys); - const deletions = useMemo( - () => getReactions(related, ev.Id, EventKind.Deletion), - [related] - ); + const deletions = useMemo(() => getReactions(related, ev.Id, EventKind.Deletion), [related]); const { isMuted } = useModeration(); const isOpMuted = isMuted(ev.PubKey); const { ref, inView, entry } = useInView({ triggerOnce: true }); @@ -99,14 +83,7 @@ export default function Note(props: NoteProps) { ); } - return ( - - ); + return ; }, [ev]); useLayoutEffect(() => { @@ -139,9 +116,7 @@ export default function Note(props: NoteProps) { mentions.push({ pk, name: u.name ?? shortNpub, - link: ( - {u.name ? `@${u.name}` : shortNpub} - ), + link: {u.name ? `@${u.name}` : shortNpub}, }); } else { mentions.push({ @@ -151,7 +126,7 @@ export default function Note(props: NoteProps) { }); } } - mentions.sort((a) => (a.name.startsWith("npub") ? 1 : -1)); + mentions.sort(a => (a.name.startsWith("npub") ? 1 : -1)); const othersLength = mentions.length - maxMentions; const renderMention = (m: { link: React.ReactNode }, idx: number) => { return ( @@ -162,13 +137,8 @@ export default function Note(props: NoteProps) { ); }; const pubMentions = - mentions.length > maxMentions - ? mentions?.slice(0, maxMentions).map(renderMention) - : mentions?.map(renderMention); - const others = - mentions.length > maxMentions - ? formatMessage(messages.Others, { n: othersLength }) - : ""; + mentions.length > maxMentions ? mentions?.slice(0, maxMentions).map(renderMention) : mentions?.map(renderMention); + const others = mentions.length > maxMentions ? formatMessage(messages.Others, { n: othersLength }) : ""; return (
    re:  @@ -178,11 +148,7 @@ export default function Note(props: NoteProps) { {others} ) : ( - replyId && ( - - {hexToBech32("note", replyId)?.substring(0, 12)} - - ) + replyId && {hexToBech32("note", replyId)?.substring(0, 12)} )}
    ); @@ -192,10 +158,7 @@ export default function Note(props: NoteProps) { return ( <>

    - +

    {JSON.stringify(ev.ToObject(), undefined, "  ")}
    @@ -207,10 +170,7 @@ export default function Note(props: NoteProps) { return ( <>

    - +

    {translated.text} @@ -230,10 +190,7 @@ export default function Note(props: NoteProps) { <> {options.showHeader && (
    - + {options.showTime && (
    @@ -241,43 +198,27 @@ export default function Note(props: NoteProps) { )}
    )} -
    goToEvent(e, ev.Id)}> +
    goToEvent(e, ev.Id)}> {transformBody()} {translation()}
    {extendable && !showMore && ( - setShowMore(true)} - > + setShowMore(true)}> )} - {options.showFooter && ( - setTranslated(t)} - /> - )} + {options.showFooter && setTranslated(t)} />} ); } const note = (
    + className={`${baseClassName}${highlight ? " active " : " "}${extendable && !showMore ? " note-expand" : ""}`} + ref={ref}> {content()}
    ); - return !ignoreModeration && isOpMuted ? ( - {note} - ) : ( - note - ); + return !ignoreModeration && isOpMuted ? {note} : note; } diff --git a/src/Element/NoteCreator.tsx b/src/Element/NoteCreator.tsx index b8ee1e80..b28cbdbe 100644 --- a/src/Element/NoteCreator.tsx +++ b/src/Element/NoteCreator.tsx @@ -48,9 +48,7 @@ export function NoteCreator(props: NoteCreatorProps) { async function sendNote() { if (note) { - const ev = replyTo - ? await publisher.reply(replyTo, note) - : await publisher.note(note); + const ev = replyTo ? await publisher.reply(replyTo, note) : await publisher.note(note); console.debug("Sending note: ", ev); publisher.broadcast(ev); setNote(""); @@ -68,7 +66,7 @@ export function NoteCreator(props: NoteCreatorProps) { if (file) { const rx = await uploader.upload(file, file.name); if (rx.url) { - setNote((n) => `${n ? `${n}\n` : ""}${rx.url}`); + setNote(n => `${n ? `${n}\n` : ""}${rx.url}`); } else if (rx?.error) { setError(rx.error); } @@ -125,11 +123,7 @@ export function NoteCreator(props: NoteCreatorProps) {
    diff --git a/src/Element/NoteFooter.tsx b/src/Element/NoteFooter.tsx index 519d82f3..610835ac 100644 --- a/src/Element/NoteFooter.tsx +++ b/src/Element/NoteFooter.tsx @@ -21,13 +21,7 @@ import Zap from "Icons/Zap"; import Reply from "Icons/Reply"; import { formatShort } from "Number"; import useEventPublisher from "Feed/EventPublisher"; -import { - getReactions, - dedupeByPubkey, - hexToBech32, - normalizeReaction, - Reaction, -} from "Util"; +import { getReactions, dedupeByPubkey, hexToBech32, normalizeReaction, Reaction } from "Util"; import { NoteCreator } from "Element/NoteCreator"; import Reactions from "Element/Reactions"; import SendSats from "Element/SendSats"; @@ -58,13 +52,9 @@ export interface NoteFooterProps { export default function NoteFooter(props: NoteFooterProps) { const { related, ev } = props; const { formatMessage } = useIntl(); - const login = useSelector( - (s) => s.login.publicKey - ); + const login = useSelector(s => s.login.publicKey); const { mute, block } = useModeration(); - const prefs = useSelector( - (s) => s.login.preferences - ); + const prefs = useSelector(s => s.login.preferences); const author = useUserProfile(ev.RootPubKey); const publisher = useEventPublisher(); const [reply, setReply] = useState(false); @@ -75,29 +65,23 @@ export default function NoteFooter(props: NoteFooterProps) { const langNames = new Intl.DisplayNames([...window.navigator.languages], { type: "language", }); - const reactions = useMemo( - () => getReactions(related, ev.Id, EventKind.Reaction), - [related, ev] - ); - const reposts = useMemo( - () => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)), - [related, ev] - ); + const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related, ev]); + const reposts = useMemo(() => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)), [related, ev]); const zaps = useMemo(() => { const sortedZaps = getReactions(related, ev.Id, EventKind.ZapReceipt) .map(parseZap) - .filter((z) => z.valid && z.zapper !== ev.PubKey); + .filter(z => z.valid && z.zapper !== ev.PubKey); sortedZaps.sort((a, b) => b.amount - a.amount); return sortedZaps; }, [related]); const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0); - const didZap = zaps.some((a) => a.zapper === login); + const didZap = zaps.some(a => a.zapper === login); const groupReactions = useMemo(() => { const result = reactions?.reduce( (acc, reaction) => { const kind = normalizeReaction(reaction.content); const rs = acc[kind] || []; - if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) { + if (rs.map(e => e.pubkey).includes(reaction.pubkey)) { return acc; } return { ...acc, [kind]: [...rs, reaction] }; @@ -116,14 +100,11 @@ export default function NoteFooter(props: NoteFooterProps) { const negative = groupReactions[Reaction.Negative]; function hasReacted(emoji: string) { - return reactions?.some( - ({ pubkey, content }) => - normalizeReaction(content) === emoji && pubkey === login - ); + return reactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login); } function hasReposted() { - return reposts.some((a) => a.pubkey === login); + return reposts.some(a => a.pubkey === login); } async function react(content: string) { @@ -134,11 +115,7 @@ export default function NoteFooter(props: NoteFooterProps) { } async function deleteEvent() { - if ( - window.confirm( - formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) }) - ) - ) { + if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) }))) { const evDelete = await publisher.delete(ev.Id); publisher.broadcast(evDelete); } @@ -146,10 +123,7 @@ export default function NoteFooter(props: NoteFooterProps) { async function repost() { if (!hasReposted()) { - if ( - !prefs.confirmReposts || - window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id })) - ) { + if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))) { const evRepost = await publisher.repost(ev); publisher.broadcast(evRepost); } @@ -161,18 +135,11 @@ export default function NoteFooter(props: NoteFooterProps) { if (service) { return ( <> -
    setTip(true)} - > +
    setTip(true)}>
    - {zapTotal > 0 && ( -
    - {formatShort(zapTotal)} -
    - )} + {zapTotal > 0 &&
    {formatShort(zapTotal)}
    }
    ); @@ -182,18 +149,11 @@ export default function NoteFooter(props: NoteFooterProps) { function repostIcon() { return ( -
    repost()} - > +
    repost()}>
    - {reposts.length > 0 && ( -
    - {formatShort(reposts.length)} -
    - )} + {reposts.length > 0 &&
    {formatShort(reposts.length)}
    }
    ); } @@ -204,16 +164,11 @@ export default function NoteFooter(props: NoteFooterProps) { } return ( <> -
    react("+")} - > +
    react("+")}>
    -
    - {formatShort(positive.length)} -
    +
    {formatShort(positive.length)}
    {repostIcon()} @@ -221,9 +176,7 @@ export default function NoteFooter(props: NoteFooterProps) { } async function share() { - const url = `${window.location.protocol}//${ - window.location.host - }/e/${hexToBech32("note", ev.Id)}`; + const url = `${window.location.protocol}//${window.location.host}/e/${hexToBech32("note", ev.Id)}`; if ("share" in window.navigator) { await window.navigator.share({ title: "Snort", @@ -262,9 +215,7 @@ export default function NoteFooter(props: NoteFooterProps) { } async function copyEvent() { - await navigator.clipboard.writeText( - JSON.stringify(ev.Original, undefined, " ") - ); + await navigator.clipboard.writeText(JSON.stringify(ev.Original, undefined, " ")); } function menuItems() { @@ -291,10 +242,7 @@ export default function NoteFooter(props: NoteFooterProps) { {prefs.enableReactions && ( react("-")}> - + )} block(ev.PubKey)}> @@ -303,10 +251,7 @@ export default function NoteFooter(props: NoteFooterProps) { translate()}> - + {prefs.showDebugMenus && ( copyEvent()}> @@ -330,10 +275,7 @@ export default function NoteFooter(props: NoteFooterProps) {
    {tipButton()} {reactionIcons()} -
    setReply((s) => !s)} - > +
    setReply(s => !s)}>
    @@ -346,18 +288,11 @@ export default function NoteFooter(props: NoteFooterProps) {
    } - menuClassName="ctx-menu" - > + menuClassName="ctx-menu"> {menuItems()}
    - setReply(false)} - show={reply} - setShow={setReply} - /> + setReply(false)} show={reply} setShow={setReply} /> { if (ev) { - const 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; } @@ -39,11 +39,7 @@ export default function NoteReaction(props: NoteReactionProps) { * Some clients embed the reposted note in the content */ function extractRoot() { - if ( - ev?.Kind === EventKind.Repost && - ev.Content.length > 0 && - ev.Content !== "#[0]" - ) { + if (ev?.Kind === EventKind.Repost && ev.Content.length > 0 && ev.Content !== "#[0]") { try { const r: RawEvent = JSON.parse(ev.Content); return r as TaggedRawEvent; @@ -73,9 +69,7 @@ export default function NoteReaction(props: NoteReactionProps) { {root ? : null} {!root && refEvent ? (

    - - #{hexToBech32("note", refEvent).substring(0, 12)} - + #{hexToBech32("note", refEvent).substring(0, 12)}

    ) : null}
    diff --git a/src/Element/NoteTime.tsx b/src/Element/NoteTime.tsx index 42ce88a7..26d1bcba 100644 --- a/src/Element/NoteTime.tsx +++ b/src/Element/NoteTime.tsx @@ -31,10 +31,7 @@ export default function NoteTime(props: NoteTimeProps) { weekday: "short", }); } else if (absAgo > HourInMs) { - return `${fromDate.getHours().toString().padStart(2, "0")}:${fromDate - .getMinutes() - .toString() - .padStart(2, "0")}`; + return `${fromDate.getHours().toString().padStart(2, "0")}:${fromDate.getMinutes().toString().padStart(2, "0")}`; } else if (absAgo < MinuteInMs) { return fallback; } else { @@ -49,7 +46,7 @@ export default function NoteTime(props: NoteTimeProps) { useEffect(() => { setTime(calcTime()); const t = setInterval(() => { - setTime((s) => { + setTime(s => { const newTime = calcTime(); if (newTime !== s) { return newTime; diff --git a/src/Element/NoteToSelf.tsx b/src/Element/NoteToSelf.tsx index a701238c..d4ba54d1 100644 --- a/src/Element/NoteToSelf.tsx +++ b/src/Element/NoteToSelf.tsx @@ -20,19 +20,13 @@ function NoteLabel({ pubkey }: NoteToSelfProps) { const user = useUserProfile(pubkey); return (
    - {" "} - + {user?.nip05 && }
    ); } -export default function NoteToSelf({ - pubkey, - clickable, - className, - link, -}: NoteToSelfProps) { +export default function NoteToSelf({ pubkey, clickable, className, link }: NoteToSelfProps) { const navigate = useNavigate(); const clickLink = () => { @@ -45,12 +39,7 @@ export default function NoteToSelf({
    - +
    diff --git a/src/Element/ProfileImage.tsx b/src/Element/ProfileImage.tsx index db123b09..4c529e63 100644 --- a/src/Element/ProfileImage.tsx +++ b/src/Element/ProfileImage.tsx @@ -17,13 +17,7 @@ export interface ProfileImageProps { link?: string; } -export default function ProfileImage({ - pubkey, - subHeader, - showUsername = true, - className, - link, -}: ProfileImageProps) { +export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }: ProfileImageProps) { const navigate = useNavigate(); const user = useUserProfile(pubkey); @@ -34,19 +28,12 @@ export default function ProfileImage({ return (
    - navigate(link ?? profileLink(pubkey))} - /> + navigate(link ?? profileLink(pubkey))} />
    {showUsername && (
    - + {name} {user?.nip05 && } @@ -58,10 +45,7 @@ export default function ProfileImage({ ); } -export function getDisplayName( - user: MetadataCache | undefined, - pubkey: HexKey -) { +export function getDisplayName(user: MetadataCache | undefined, pubkey: HexKey) { let name = hexToBech32("npub", pubkey).substring(0, 12); if (user?.display_name !== undefined && user.display_name.length > 0) { name = user.display_name; diff --git a/src/Element/ProfilePreview.tsx b/src/Element/ProfilePreview.tsx index 415b02ce..69aba61d 100644 --- a/src/Element/ProfilePreview.tsx +++ b/src/Element/ProfilePreview.tsx @@ -25,21 +25,12 @@ export default function ProfilePreview(props: ProfilePreviewProps) { }; return ( -
    +
    {inView && ( <> {user?.about}
    - ) : undefined - } + subHeader={options.about ?
    {user?.about}
    : undefined} /> {props.actions ?? (
    diff --git a/src/Element/ProxyImg.tsx b/src/Element/ProxyImg.tsx index 7f7d8193..37893c13 100644 --- a/src/Element/ProxyImg.tsx +++ b/src/Element/ProxyImg.tsx @@ -1,11 +1,7 @@ import useImgProxy from "Feed/ImgProxy"; import { useEffect, useState } from "react"; -interface ProxyImgProps - extends React.DetailedHTMLProps< - React.ImgHTMLAttributes, - HTMLImageElement - > { +interface ProxyImgProps extends React.DetailedHTMLProps, HTMLImageElement> { size?: number; } @@ -17,7 +13,7 @@ export const ProxyImg = (props: ProxyImgProps) => { useEffect(() => { if (src) { proxy(src, size) - .then((a) => setUrl(a)) + .then(a => setUrl(a)) .catch(console.warn); } }, [src]); diff --git a/src/Element/Reactions.tsx b/src/Element/Reactions.tsx index cee3721d..dbeabf85 100644 --- a/src/Element/Reactions.tsx +++ b/src/Element/Reactions.tsx @@ -28,14 +28,7 @@ interface ReactionsProps { zaps: ParsedZap[]; } -const Reactions = ({ - show, - setShow, - positive, - negative, - reposts, - zaps, -}: ReactionsProps) => { +const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: ReactionsProps) => { const { formatMessage } = useIntl(); const onClose = () => setShow(false); const likes = useMemo(() => { @@ -48,8 +41,7 @@ const Reactions = ({ sorted.sort((a, b) => b.created_at - a.created_at); return sorted; }, [negative]); - const total = - positive.length + negative.length + zaps.length + reposts.length; + const total = positive.length + negative.length + zaps.length + reposts.length; const defaultTabs: Tab[] = [ { text: formatMessage(messages.Likes, { n: likes.length }), @@ -93,24 +85,17 @@ const Reactions = ({

    - +

    {tab.value === 0 && - likes.map((ev) => { + likes.map(ev => { return (
    - {ev.content === "+" ? ( - - ) : ( - ev.content - )} + {ev.content === "+" ? : ev.content}
    @@ -118,23 +103,22 @@ const Reactions = ({ ); })} {tab.value === 1 && - zaps.map((z) => { + zaps.map(z => { return ( -
    -
    - - {formatShort(z.amount)} + z.zapper && ( +
    +
    + + {formatShort(z.amount)} +
    + {z.content}} /> +
    - {z.content}} - /> - -
    + ) ); })} {tab.value === 2 && - reposts.map((ev) => { + reposts.map(ev => { return (
    @@ -146,7 +130,7 @@ const Reactions = ({ ); })} {tab.value === 3 && - dislikes.map((ev) => { + dislikes.map(ev => { return (
    diff --git a/src/Element/Relay.tsx b/src/Element/Relay.tsx index ed486a60..3fd138f8 100644 --- a/src/Element/Relay.tsx +++ b/src/Element/Relay.tsx @@ -27,10 +27,7 @@ export default function Relay(props: RelayProps) { const dispatch = useDispatch(); const { formatMessage } = useIntl(); const navigate = useNavigate(); - const allRelaySettings = useSelector< - RootState, - Record - >((s) => s.login.relays); + const allRelaySettings = useSelector>(s => s.login.relays); const relaySettings = allRelaySettings[props.addr]; const state = useRelayState(props.addr); const name = useMemo(() => new URL(props.addr).host, [props.addr]); @@ -66,11 +63,8 @@ export default function Relay(props: RelayProps) { write: !relaySettings.write, read: relaySettings.read, }) - } - > - + }> +
    @@ -82,11 +76,8 @@ export default function Relay(props: RelayProps) { write: relaySettings.write, read: !relaySettings.read, }) - } - > - + }> +
    @@ -104,10 +95,7 @@ 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 5d2a69bc..8c4545a9 100644 --- a/src/Element/SendSats.tsx +++ b/src/Element/SendSats.tsx @@ -54,9 +54,7 @@ export default function LNURLTip(props: LNURLTipProps) { const service = props.svc; const show = props.show || false; const { note, author, target } = props; - const amounts = [ - 500, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000, - ]; + const amounts = [500, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000]; const emojis: Record = { 1_000: "👍", 5_000: "💜", @@ -77,13 +75,12 @@ export default function LNURLTip(props: LNURLTipProps) { const { formatMessage } = useIntl(); const publisher = useEventPublisher(); const horizontalScroll = useHorizontalScroll(); - const canComment = - (payService?.commentAllowed ?? 0) > 0 || payService?.nostrPubkey; + const canComment = (payService?.commentAllowed ?? 0) > 0 || payService?.nostrPubkey; useEffect(() => { if (show && !props.invoice) { loadService() - .then((a) => setPayService(a ?? undefined)) + .then(a => setPayService(a ?? undefined)) .catch(() => setError(formatMessage(messages.LNURLFail))); } else { setPayService(undefined); @@ -99,13 +96,11 @@ export default function LNURLTip(props: LNURLTipProps) { if (payService) { const min = (payService.minSendable ?? 0) / 1000; const max = (payService.maxSendable ?? 0) / 1000; - return amounts.filter((a) => a >= min && a <= max); + return amounts.filter(a => a >= min && a <= max); } return []; }, [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); setInvoice(undefined); @@ -141,14 +136,10 @@ export default function LNURLTip(props: LNURLTipProps) { if (!amount || !payService) return null; let url = ""; const amountParam = `amount=${Math.floor(amount * 1000)}`; - const commentParam = - comment && payService?.commentAllowed - ? `&comment=${encodeURIComponent(comment)}` - : ""; + const commentParam = comment && payService?.commentAllowed ? `&comment=${encodeURIComponent(comment)}` : ""; if (payService.nostrPubkey && author) { const ev = await publisher.zap(author, note, comment); - const nostrParam = - ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`; + const nostrParam = ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`; url = `${payService.callback}?${amountParam}${commentParam}${nostrParam}`; } else { url = `${payService.callback}?${amountParam}${commentParam}`; @@ -185,14 +176,13 @@ export default function LNURLTip(props: LNURLTipProps) { className="f-grow mr10" placeholder={formatMessage(messages.Custom)} value={customAmount} - onChange={(e) => setCustomAmount(parseInt(e.target.value))} + onChange={e => setCustomAmount(parseInt(e.target.value))} />
    @@ -222,12 +212,8 @@ export default function LNURLTip(props: LNURLTipProps) {
    - {serviceAmounts.map((a) => ( - selectAmount(a)} - > + {serviceAmounts.map(a => ( + selectAmount(a)}> {emojis[a] && <>{emojis[a]} } {formatShort(a)} @@ -241,28 +227,18 @@ export default function LNURLTip(props: LNURLTipProps) { placeholder={formatMessage(messages.Comment)} className="f-grow" maxLength={payService?.commentAllowed || 120} - onChange={(e) => setComment(e.target.value)} + onChange={e => setComment(e.target.value)} /> )}
    {(amount ?? 0) > 0 && ( - @@ -285,11 +261,7 @@ export default function LNURLTip(props: LNURLTipProps) {
    - @@ -319,9 +291,7 @@ export default function LNURLTip(props: LNURLTipProps) { ); } - const defaultTitle = payService?.nostrPubkey - ? formatMessage(messages.SendZap) - : formatMessage(messages.SendSats); + const defaultTitle = payService?.nostrPubkey ? formatMessage(messages.SendZap) : formatMessage(messages.SendSats); const title = target ? formatMessage(messages.ToTarget, { action: defaultTitle, @@ -331,7 +301,7 @@ export default function LNURLTip(props: LNURLTipProps) { if (!show) return null; return ( -
    e.stopPropagation()}> +
    e.stopPropagation()}>
    diff --git a/src/Element/Skeleton.css b/src/Element/Skeleton.css index 157162c0..09213ac4 100644 --- a/src/Element/Skeleton.css +++ b/src/Element/Skeleton.css @@ -37,12 +37,6 @@ } .skeleton::after { - background-image: linear-gradient( - 90deg, - #50535a 0%, - #656871 20%, - #50535a 40%, - #50535a 100% - ); + background-image: linear-gradient(90deg, #50535a 0%, #656871 20%, #50535a 40%, #50535a 100%); } } diff --git a/src/Element/Skeleton.tsx b/src/Element/Skeleton.tsx index c74e3ef7..1fa311fe 100644 --- a/src/Element/Skeleton.tsx +++ b/src/Element/Skeleton.tsx @@ -8,22 +8,13 @@ interface ISkepetonProps { margin?: string; } -export default function Skeleton({ - children, - width, - height, - margin, - loading = true, -}: ISkepetonProps) { +export default function Skeleton({ children, width, height, margin, loading = true }: ISkepetonProps) { if (!loading) { return <>{children}; } return ( -
    +
    {children}
    ); diff --git a/src/Element/SoundCloudEmded.tsx b/src/Element/SoundCloudEmded.tsx index a3c6f11a..424ce49b 100644 --- a/src/Element/SoundCloudEmded.tsx +++ b/src/Element/SoundCloudEmded.tsx @@ -5,8 +5,7 @@ const SoundCloudEmbed = ({ link }: { link: string }) => { height="166" scrolling="no" allow="autoplay" - src={`https://w.soundcloud.com/player/?url=${link}`} - > + src={`https://w.soundcloud.com/player/?url=${link}`}> ); }; diff --git a/src/Element/SpotifyEmbed.tsx b/src/Element/SpotifyEmbed.tsx index e15a3fa1..93fc14d2 100644 --- a/src/Element/SpotifyEmbed.tsx +++ b/src/Element/SpotifyEmbed.tsx @@ -1,8 +1,5 @@ const SpotifyEmbed = ({ link }: { link: string }) => { - const convertedUrl = link.replace( - /\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, - "/embed/$1/$2" - ); + const convertedUrl = link.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2"); return ( + loading="lazy"> ); }; diff --git a/src/Element/Tabs.tsx b/src/Element/Tabs.tsx index 4fff9f39..3da948e4 100644 --- a/src/Element/Tabs.tsx +++ b/src/Element/Tabs.tsx @@ -20,11 +20,8 @@ interface TabElementProps extends Omit { export const TabElement = ({ t, tab, setTab }: TabElementProps) => { return (
    !t.disabled && setTab(t)} - > + className={`tab ${tab.value === t.value ? "active" : ""} ${t.disabled ? "disabled" : ""}`} + onClick={() => !t.disabled && setTab(t)}> {t.text}
    ); @@ -33,7 +30,7 @@ export const TabElement = ({ t, tab, setTab }: TabElementProps) => { const Tabs = ({ tabs, tab, setTab }: TabsProps) => { return (
    - {tabs.map((t) => ( + {tabs.map(t => ( ))}
    diff --git a/src/Element/Text.tsx b/src/Element/Text.tsx index 2e249af3..fa87e2c4 100644 --- a/src/Element/Text.tsx +++ b/src/Element/Text.tsx @@ -34,9 +34,9 @@ export interface TextProps { export default function Text({ content, tags, creator, users }: TextProps) { function extractLinks(fragments: Fragment[]) { return fragments - .map((f) => { + .map(f => { if (typeof f === "string") { - return f.split(UrlRegex).map((a) => { + return f.split(UrlRegex).map(a => { if (a.startsWith("http")) { return ; } @@ -50,29 +50,22 @@ export default function Text({ content, tags, creator, users }: TextProps) { function extractMentions(frag: TextFragment) { return frag.body - .map((f) => { + .map(f => { if (typeof f === "string") { - return f.split(MentionRegex).map((match) => { + return f.split(MentionRegex).map(match => { const matchTag = match.match(/#\[(\d+)\]/); if (matchTag && matchTag.length === 2) { const idx = parseInt(matchTag[1]); - const ref = frag.tags?.find((a) => a.Index === idx); + const ref = frag.tags?.find(a => a.Index === idx); if (ref) { switch (ref.Key) { case "p": { return ; } case "e": { - const eText = hexToBech32( - "note", - ref.Event ?? "" - ).substring(0, 12); + const eText = hexToBech32("note", ref.Event).substring(0, 12); return ( - e.stopPropagation()} - > + e.stopPropagation()}> #{eText} ); @@ -95,9 +88,9 @@ export default function Text({ content, tags, creator, users }: TextProps) { function extractInvoices(fragments: Fragment[]) { return fragments - .map((f) => { + .map(f => { if (typeof f === "string") { - return f.split(InvoiceRegex).map((i) => { + return f.split(InvoiceRegex).map(i => { if (i.toLowerCase().startsWith("lnbc")) { return ; } else { @@ -112,9 +105,9 @@ export default function Text({ content, tags, creator, users }: TextProps) { function extractHashtags(fragments: Fragment[]) { return fragments - .map((f) => { + .map(f => { if (typeof f === "string") { - return f.split(HashtagRegex).map((i) => { + return f.split(HashtagRegex).map(i => { if (i.toLowerCase().startsWith("#")) { return ; } else { @@ -134,7 +127,7 @@ export default function Text({ content, tags, creator, users }: TextProps) { function transformParagraph(frag: TextFragment) { const fragments = transformText(frag); - if (fragments.every((f) => typeof f === "string")) { + if (fragments.every(f => typeof f === "string")) { return

    {fragments}

    ; } return <>{fragments}; @@ -150,13 +143,9 @@ export default function Text({ content, tags, creator, users }: TextProps) { const components = useMemo(() => { return { - p: (x: { children?: React.ReactNode[] }) => - transformParagraph({ body: x.children ?? [], tags, users }), - a: (x: { href?: string }) => ( - - ), - li: (x: { children?: Fragment[] }) => - transformLi({ body: x.children ?? [], tags, users }), + p: (x: { children?: React.ReactNode[] }) => transformParagraph({ body: x.children ?? [], tags, users }), + a: (x: { href?: string }) => , + li: (x: { children?: Fragment[] }) => transformLi({ body: x.children ?? [], tags, users }), }; }, [content]); @@ -178,9 +167,7 @@ export default function Text({ content, tags, creator, users }: TextProps) { ) { node.type = "text"; const position = unwrap(node.position); - node.value = content - .slice(position.start.offset, position.end.offset) - .replace(/\)$/, " )"); + node.value = content.slice(position.start.offset, position.end.offset).replace(/\)$/, " )"); return SKIP; } }); @@ -188,11 +175,7 @@ export default function Text({ content, tags, creator, users }: TextProps) { [content] ); return ( - + {content} ); diff --git a/src/Element/Textarea.tsx b/src/Element/Textarea.tsx index 65021808..7d5923e2 100644 --- a/src/Element/Textarea.tsx +++ b/src/Element/Textarea.tsx @@ -85,11 +85,8 @@ const Textarea = (props: TextareaProps) => { "@": { afterWhitespace: true, dataProvider: userDataProvider, - component: (props: { entity: MetadataCache }) => ( - - ), - output: (item: { pubkey: string }) => - `@${hexToBech32("npub", item.pubkey)}`, + component: (props: { entity: MetadataCache }) => , + output: (item: { pubkey: string }) => `@${hexToBech32("npub", item.pubkey)}`, }, }} /> diff --git a/src/Element/Thread.css b/src/Element/Thread.css index 11363041..3049886e 100644 --- a/src/Element/Thread.css +++ b/src/Element/Thread.css @@ -87,8 +87,7 @@ } @media (min-width: 720px) { - .subthread-container.subthread-mid:not(.subthread-last) - .line-container:after { + .subthread-container.subthread-mid:not(.subthread-last) .line-container:after { left: 48px; } } @@ -103,8 +102,7 @@ } @media (min-width: 720px) { - .subthread-container.subthread-mid:not(.subthread-last) - .line-container:after { + .subthread-container.subthread-mid:not(.subthread-last) .line-container:after { left: 48px; } } diff --git a/src/Element/Thread.tsx b/src/Element/Thread.tsx index 2645a845..cc82312b 100644 --- a/src/Element/Thread.tsx +++ b/src/Element/Thread.tsx @@ -13,12 +13,9 @@ import NoteGhost from "Element/NoteGhost"; import Collapsed from "Element/Collapsed"; import messages from "./messages"; -function getParent( - ev: HexKey, - chains: Map -): HexKey | undefined { +function getParent(ev: HexKey, chains: Map): HexKey | undefined { for (const [k, vs] of chains.entries()) { - const fs = vs.map((a) => a.Id); + const fs = vs.map(a => a.Id); if (fs.includes(ev)) { return k; } @@ -49,30 +46,17 @@ interface SubthreadProps { onNavigate: (e: u256) => void; } -const Subthread = ({ - active, - path, - notes, - related, - chains, - onNavigate, -}: SubthreadProps) => { +const Subthread = ({ active, path, notes, related, chains, onNavigate }: SubthreadProps) => { const renderSubthread = (a: NEvent, idx: number) => { const isLastSubthread = idx === notes.length - 1; const replies = getReplies(a.Id, chains); return ( <> -
    0 ? "subthread-multi" : "" - }`} - > +
    0 ? "subthread-multi" : ""}`}> { const { formatMessage } = useIntl(); const replies = getReplies(note.Id, chains); - const activeInReplies = replies.map((r) => r.Id).includes(active); + const activeInReplies = replies.map(r => r.Id).includes(active); const [collapsed, setCollapsed] = useState(!activeInReplies); const hasMultipleNotes = replies.length > 0; const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes; - const className = `subthread-container ${ - isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid" - }`; + const className = `subthread-container ${isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid"}`; return ( <>
    @@ -149,11 +131,7 @@ const ThreadNote = ({ onNavigate={onNavigate} /> ) : ( - + { +const TierTwo = ({ active, isLastSubthread, path, from, notes, related, chains, onNavigate }: SubthreadProps) => { const [first, ...rest] = notes; return ( @@ -216,36 +185,22 @@ const TierTwo = ({ ); }; -const TierThree = ({ - active, - path, - isLastSubthread, - from, - notes, - related, - chains, - onNavigate, -}: SubthreadProps) => { +const TierThree = ({ active, path, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => { const [first, ...rest] = notes; const replies = getReplies(first.Id, chains); - const activeInReplies = - notes.map((r) => r.Id).includes(active) || - replies.map((r) => r.Id).includes(active); + const activeInReplies = notes.map(r => r.Id).includes(active) || replies.map(r => r.Id).includes(active); const hasMultipleNotes = rest.length > 0 || replies.length > 0; const isLast = replies.length === 0 && rest.length === 0; return ( <>
    + className={`subthread-container ${hasMultipleNotes ? "subthread-multi" : ""} ${ + isLast ? "subthread-last" : "subthread-mid" + }`}> 0 && (
    -
    @@ -284,10 +235,9 @@ const TierThree = ({ return (
    + className={`subthread-container ${lastReply ? "" : "subthread-multi"} ${ + lastReply ? "subthread-last" : "subthread-mid" + }`}> new NEvent(a)); + const parsedNotes = notes.map(a => new NEvent(a)); // root note has no thread info - const root = useMemo( - () => parsedNotes.find((a) => a.Thread === null), - [notes] - ); + const root = useMemo(() => parsedNotes.find(a => a.Thread === null), [notes]); const [path, setPath] = useState([]); const currentId = path.length > 0 && path[path.length - 1]; - const currentRoot = useMemo( - () => parsedNotes.find((a) => a.Id === currentId), - [notes, currentId] - ); + const currentRoot = useMemo(() => parsedNotes.find(a => a.Id === currentId), [notes, currentId]); const [navigated, setNavigated] = useState(false); const navigate = useNavigate(); - const isSingleNote = - parsedNotes.filter((a) => a.Kind === EventKind.TextNote).length === 1; + const isSingleNote = parsedNotes.filter(a => a.Kind === EventKind.TextNote).length === 1; const location = useLocation(); const urlNoteId = location?.pathname.slice(3); const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId); @@ -334,9 +277,9 @@ export default function Thread(props: ThreadProps) { const chains = useMemo(() => { const chains = new Map(); parsedNotes - ?.filter((a) => a.Kind === EventKind.TextNote) + ?.filter(a => a.Kind === EventKind.TextNote) .sort((a, b) => b.CreatedAt - a.CreatedAt) - .forEach((v) => { + .forEach(v => { const replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event; if (replyTo) { if (!chains.has(replyTo)) { @@ -378,28 +321,15 @@ export default function Thread(props: ThreadProps) { }, [root, navigated, urlNoteHex, chains]); const brokenChains = useMemo(() => { - return Array.from(chains?.keys()).filter( - (a) => !parsedNotes?.some((b) => b.Id === a) - ); + return Array.from(chains?.keys()).filter(a => !parsedNotes?.some(b => b.Id === a)); }, [chains]); function renderRoot(note: NEvent) { const className = `thread-root ${isSingleNote ? "thread-root-single" : ""}`; if (note) { - return ( - - ); + return ; } else { - return ( - - Loading thread root.. ({notes?.length} notes loaded) - - ); + return Loading thread root.. ({notes?.length} notes loaded); } } @@ -438,25 +368,18 @@ export default function Thread(props: ThreadProps) { return (
    - 1 ? "Parent" : "Back"} - /> + 1 ? "Parent" : "Back"} />
    {currentRoot && renderRoot(currentRoot)} {currentRoot && renderChain(currentRoot.Id)} {currentRoot === root && ( <> {brokenChains.length > 0 &&

    Other replies

    } - {brokenChains.map((a) => { + {brokenChains.map(a => { return (
    - - Missing event{" "} - {a.substring(0, 8)} + + Missing event {a.substring(0, 8)} {renderChain(a)}
    diff --git a/src/Element/TidalEmbed.tsx b/src/Element/TidalEmbed.tsx index f838f0a7..dc0676ee 100644 --- a/src/Element/TidalEmbed.tsx +++ b/src/Element/TidalEmbed.tsx @@ -34,13 +34,11 @@ async function oembedLookup(link: string) { const TidalEmbed = ({ link }: { link: string }) => { const [source, setSource] = useState(); const [height, setHeight] = useState(); - const extraStyles = link.includes("video") - ? { aspectRatio: "16 / 9" } - : { height }; + const extraStyles = link.includes("video") ? { aspectRatio: "16 / 9" } : { height }; useEffect(() => { oembedLookup(link) - .then((data) => { + .then(data => { setSource(data.source || undefined); setHeight(data.height); }) @@ -49,25 +47,11 @@ const TidalEmbed = ({ link }: { link: string }) => { if (!source) return ( - e.stopPropagation()} - className="ext" - > + e.stopPropagation()} className="ext"> {link} ); - return ( -