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/.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/.github/workflows/eslint.yaml b/.github/workflows/eslint.yaml new file mode 100644 index 00000000..af3ef571 --- /dev/null +++ b/.github/workflows/eslint.yaml @@ -0,0 +1,20 @@ +name: Linting +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 + with: + node-version: 16 + - name: Install Dependencies + run: yarn install + - name: Check Eslint + run: yarn eslint diff --git a/.github/workflows/formatting.yaml b/.github/workflows/formatting.yaml new file mode 100644 index 00000000..849110cd --- /dev/null +++ b/.github/workflows/formatting.yaml @@ -0,0 +1,18 @@ +name: Formatting +on: + pull_request: +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 + with: + node-version: 16 + - name: Install Dependencies + run: yarn install + - name: Check Formatting + run: yarn prettier --check . diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..567609b1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +build/ 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 125a621e..8efeccf6 100644 --- a/d.ts +++ b/d.ts @@ -1,14 +1,28 @@ 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; } + +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/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": [ 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 012f6e2e..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 @@ -83,17 +79,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; /** @@ -115,12 +114,12 @@ 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 */ +// eslint-disable-next-line no-useless-escape export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/; /** @@ -131,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 d420aa37..59bdaf86 100644 --- a/src/Element/AsyncButton.tsx +++ b/src/Element/AsyncButton.tsx @@ -1,14 +1,19 @@ 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 +24,7 @@ export default function AsyncButton(props: any) { } return ( - ); 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 b408c7d3..33e07e61 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 ( @@ -25,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 569fb32b..8a69e989 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"; @@ -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 df6848f9..cc3f6765 100644 --- a/src/Element/Copy.tsx +++ b/src/Element/Copy.tsx @@ -8,25 +8,15 @@ 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 - ? `${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 19ea59fd..fdf59a4c 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"; @@ -21,22 +22,18 @@ 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 - : 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() { - 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); @@ -54,18 +51,10 @@ export default function DM(props: DMProps) { return (
- +
- +
); diff --git a/src/Element/FollowButton.tsx b/src/Element/FollowButton.tsx index 2339e072..050234ad 100644 --- a/src/Element/FollowButton.tsx +++ b/src/Element/FollowButton.tsx @@ -15,18 +15,16 @@ 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) { - 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); } @@ -34,13 +32,8 @@ export default function FollowButton(props: FollowButtonProps) { ); } diff --git a/src/Element/FollowListBase.tsx b/src/Element/FollowListBase.tsx index 4479a2dc..50931357 100644 --- a/src/Element/FollowListBase.tsx +++ b/src/Element/FollowListBase.tsx @@ -10,14 +10,11 @@ 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() { - let ev = await publisher.addFollow(pubkeys); + const ev = await publisher.addFollow(pubkeys); publisher.broadcast(ev); } @@ -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 cc903511..9e1eaaa7 100644 --- a/src/Element/FollowersList.tsx +++ b/src/Element/FollowersList.tsx @@ -17,18 +17,11 @@ export default function FollowersList({ pubkey }: FollowersListProps) { const feed = useFollowersFeed(pubkey); const pubkeys = useMemo(() => { - let contactLists = feed?.store.notes.filter( - (a) => - a.kind === EventKind.ContactList && - a.tags.some((b) => b[0] === "p" && b[1] === pubkey) + const contactLists = feed?.store.notes.filter( + 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 fdf32b16..581dbda5 100644 --- a/src/Element/FollowsYou.tsx +++ b/src/Element/FollowsYou.tsx @@ -17,17 +17,13 @@ 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); }, [feed, pubkey]); - const followsMe = pubkeys.includes(loginPubKey!) ?? false; + 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 72683fe5..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,26 +109,16 @@ export default function HyperText({ return ; } else { return ( - e.stopPropagation()} - target="_blank" - rel="noreferrer" - className="ext" - > + e.stopPropagation()} target="_blank" rel="noreferrer" className="ext"> {a} ); } - } catch (error) {} + } catch (error) { + // 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 b960dd12..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 import { decode as invoiceDecode } from "light-bolt11-decoder"; import { useMemo } from "react"; import SendSats from "Element/SendSats"; @@ -13,6 +12,7 @@ import messages from "./messages"; export interface InvoiceProps { invoice: string; } + export default function Invoice(props: InvoiceProps) { const invoice = props.invoice; const webln = useWebln(); @@ -21,24 +21,21 @@ export default function Invoice(props: InvoiceProps) { const info = useMemo(() => { try { - let parsed = invoiceDecode(invoice); + const parsed = invoiceDecode(invoice); - let amount = parseInt( - parsed.sections.find((a: any) => a.name === "amount")?.value - ); - let timestamp = parseInt( - parsed.sections.find((a: any) => a.name === "timestamp")?.value - ); - let expire = parseInt( - parsed.sections.find((a: any) => a.name === "expiry")?.value - ); - let description = parsed.sections.find( - (a: any) => a.name === "description" - )?.value; - let ret = { + 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) { @@ -72,7 +69,7 @@ export default function Invoice(props: InvoiceProps) { ); } - async function payInvoice(e: any) { + async function payInvoice(e: React.MouseEvent) { e.stopPropagation(); if (webln?.enabled) { try { @@ -88,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"} )}

@@ -112,11 +104,7 @@ export default function Invoice(props: InvoiceProps) {
) : ( )}
diff --git a/src/Element/LoadMore.tsx b/src/Element/LoadMore.tsx index e5cbf2a7..8982c375 100644 --- a/src/Element/LoadMore.tsx +++ b/src/Element/LoadMore.tsx @@ -23,8 +23,8 @@ export default function LoadMore({ }, [inView, shouldLoadMore, tick]); useEffect(() => { - let t = setInterval(() => { - setTick((x) => (x += 1)); + const t = setInterval(() => { + 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 67ed27f3..1c03115a 100644 --- a/src/Element/Mention.tsx +++ b/src/Element/Mention.tsx @@ -9,16 +9,16 @@ 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]); 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 641511bd..15c6b9fd 100644 --- a/src/Element/Modal.tsx +++ b/src/Element/Modal.tsx @@ -8,10 +8,10 @@ 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 +24,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..8a700548 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"; @@ -25,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 dd2c2a5e..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,45 +30,34 @@ type Nip05ServiceProps = { supportLink: string; }; -type ReduxStore = any; - 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 { - let svc = a as ServiceConfig; + const svc = a as ServiceConfig; setServiceConfig(svc); - let 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); } }) @@ -86,10 +76,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { setAvailabilityResponse({ available: false, why: "TOO_LONG" }); return; } - let 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; @@ -97,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 { @@ -111,14 +98,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 +118,11 @@ 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)], @@ -144,12 +134,11 @@ export default function Nip5Service(props: Nip05ServiceProps) { } async function startBuy(handle: string, domain: string) { - if (registerResponse) { - setShowInvoice(true); + if (!pubkey) { 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 +149,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"); } @@ -194,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 => ( ))} @@ -207,10 +196,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { {availabilityResponse?.available && !registerStatus && (
- +
{availabilityResponse.quote?.data.type}
@@ -230,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 a8ffe5d3..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"; @@ -17,7 +11,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 +32,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} ) : (
@@ -59,30 +52,19 @@ const HiddenNote = ({ children }: any) => { export default function Note(props: NoteProps) { const navigate = useNavigate(); - const { - data, - className, - 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 }); 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 +75,7 @@ export default function Note(props: NoteProps) { }; const transformBody = useCallback(() => { - let body = ev?.Content ?? ""; + const body = ev?.Content ?? ""; if (deletions?.length > 0) { return ( @@ -101,26 +83,19 @@ export default function Note(props: NoteProps) { ); } - return ( - - ); + return ; }, [ev]); 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 +106,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); @@ -141,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({ @@ -153,9 +126,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 && ", "} @@ -164,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:  @@ -180,11 +148,7 @@ export default function Note(props: NoteProps) { {others} ) : ( - replyId && ( - - {hexToBech32("note", replyId)?.substring(0, 12)} - - ) + replyId && {hexToBech32("note", replyId)?.substring(0, 12)} )}
); @@ -194,10 +158,7 @@ export default function Note(props: NoteProps) { return ( <>

- +

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

- +

{translated.text} @@ -232,10 +190,7 @@ export default function Note(props: NoteProps) { <> {options.showHeader && (
- + {options.showTime && (
@@ -243,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 dab6991d..b28cbdbe 100644 --- a/src/Element/NoteCreator.tsx +++ b/src/Element/NoteCreator.tsx @@ -33,14 +33,14 @@ export interface NoteCreatorProps { show: boolean; setShow: (s: boolean) => 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,9 +48,7 @@ export function NoteCreator(props: NoteCreatorProps) { async function sendNote() { if (note) { - let 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(""); @@ -64,21 +62,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}`); + 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 +88,7 @@ export function NoteCreator(props: NoteCreatorProps) { } } - function cancel(ev: any) { + function cancel() { setShow(false); setNote(""); } @@ -112,11 +112,7 @@ export function NoteCreator(props: NoteCreatorProps) { value={note} onFocus={() => setActive(true)} /> -
@@ -127,11 +123,7 @@ export function NoteCreator(props: NoteCreatorProps) {
diff --git a/src/Element/NoteFooter.tsx b/src/Element/NoteFooter.tsx index 3f10b91b..610835ac 100644 --- a/src/Element/NoteFooter.tsx +++ b/src/Element/NoteFooter.tsx @@ -21,17 +21,11 @@ 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"; -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"; @@ -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) => { - let kind = normalizeReaction(reaction.content); + 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,63 +100,46 @@ 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) { if (!hasReacted(content)) { - let evLike = await publisher.react(ev, content); + const evLike = await publisher.react(ev, content); publisher.broadcast(evLike); } } async function deleteEvent() { - if ( - window.confirm( - formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) }) - ) - ) { - let evDelete = await publisher.delete(ev.Id); + if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) }))) { + const evDelete = await publisher.delete(ev.Id); publisher.broadcast(evDelete); } } async function repost() { if (!hasReposted()) { - if ( - !prefs.confirmReposts || - window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id })) - ) { - let evRepost = await publisher.repost(ev); + if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))) { + 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 ( <> -
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", @@ -246,7 +199,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, @@ -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} />
diff --git a/src/Element/NoteReaction.tsx b/src/Element/NoteReaction.tsx index 1d5fe0ec..26028e82 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; } @@ -39,13 +39,9 @@ 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 { - 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); @@ -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 d494a09b..26d1bcba 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", @@ -31,14 +31,11 @@ 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 { - let mins = Math.floor(absAgo / MinuteInMs); + const mins = Math.floor(absAgo / MinuteInMs); if (ago < 0) { return `in ${mins}m`; } @@ -48,9 +45,9 @@ export default function NoteTime(props: NoteTimeProps) { useEffect(() => { setTime(calcTime()); - let t = setInterval(() => { - setTime((s) => { - let newTime = calcTime(); + const t = setInterval(() => { + setTime(s => { + const newTime = calcTime(); if (newTime !== s) { return newTime; } diff --git a/src/Element/NoteToSelf.tsx b/src/Element/NoteToSelf.tsx index dc1949d3..d4ba54d1 100644 --- a/src/Element/NoteToSelf.tsx +++ b/src/Element/NoteToSelf.tsx @@ -16,23 +16,17 @@ export interface NoteToSelfProps { link?: string; } -function NoteLabel({ pubkey, link }: NoteToSelfProps) { +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 0048fee0..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,15 +45,12 @@ 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?.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/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 8a63b409..37893c13 100644 --- a/src/Element/ProxyImg.tsx +++ b/src/Element/ProxyImg.tsx @@ -1,7 +1,11 @@ import useImgProxy from "Feed/ImgProxy"; import { useEffect, useState } from "react"; -export const ProxyImg = (props: any) => { +interface ProxyImgProps extends React.DetailedHTMLProps, HTMLImageElement> { + size?: number; +} + +export const ProxyImg = (props: ProxyImgProps) => { const { src, size, ...rest } = props; const [url, setUrl] = useState(); const { proxy } = useImgProxy(); @@ -9,7 +13,7 @@ export const ProxyImg = (props: any) => { useEffect(() => { if (src) { proxy(src, size) - .then((a) => setUrl(a)) + .then(a => setUrl(a)) .catch(console.warn); } }, [src]); 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/Reactions.tsx b/src/Element/Reactions.tsx index 44e87d94..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 badb6bbb..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]); @@ -47,7 +44,7 @@ export default function Relay(props: RelayProps) { ); } - let latency = Math.floor(state?.avgLatency ?? 0); + const latency = Math.floor(state?.avgLatency ?? 0); return ( <>
@@ -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,7 +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 34719536..8c4545a9 100644 --- a/src/Element/SendSats.tsx +++ b/src/Element/SendSats.tsx @@ -50,13 +50,11 @@ 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; - 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!)) + .then(a => setPayService(a ?? undefined)) .catch(() => setError(formatMessage(messages.LNURLFail))); } else { setPayService(undefined); @@ -97,26 +94,13 @@ export default function LNURLTip(props: LNURLTipProps) { const serviceAmounts = useMemo(() => { if (payService) { - let min = (payService.minSendable ?? 0) / 1000; - let max = (payService.maxSendable ?? 0) / 1000; - return amounts.filter((a) => a >= min && a <= max); + 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]); - const selectAmount = (a: number) => { setError(undefined); setInvoice(undefined); @@ -124,9 +108,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 +120,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]}`); } } @@ -152,22 +136,18 @@ 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}`; } 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 +165,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 (
setCustomAmount(parseInt(e.target.value))} + onChange={e => setCustomAmount(parseInt(e.target.value))} />
@@ -213,13 +192,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()); + } } } @@ -231,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)} @@ -250,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 && ( - @@ -294,11 +261,7 @@ export default function LNURLTip(props: LNURLTipProps) {
- @@ -328,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, @@ -340,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 27f19c59..fa87e2c4 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; } @@ -33,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 ; } @@ -49,35 +50,28 @@ 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) => { - let matchTag = match.match(/#\[(\d+)\]/); + return f.split(MentionRegex).map(match => { + 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()} - > + e.stopPropagation()}> #{eText} ); } case "t": { - return ; + return ; } } } @@ -94,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 { @@ -111,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 { @@ -127,22 +121,19 @@ export default function Text({ content, tags, creator, users }: TextProps) { } function transformLi(frag: TextFragment) { - let fragments = transformText(frag); + const fragments = transformText(frag); return
  • {fragments}
  • ; } 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}; } function transformText(frag: TextFragment) { - if (frag.body === undefined) { - debugger; - } let fragments = extractMentions(frag); fragments = extractLinks(fragments); fragments = extractInvoices(fragments); @@ -152,15 +143,18 @@ export default function Text({ content, tags, creator, users }: TextProps) { const components = useMemo(() => { return { - p: (x: any) => - transformParagraph({ body: x.children ?? [], tags, users }), - a: (x: any) => , - li: (x: any) => 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]); + interface Node extends unist.Node { + value: string; + } + const disableMarkdownLinks = useCallback( - () => (tree: any) => { + () => (tree: Node) => { visit(tree, (node, index, parent) => { if ( parent && @@ -172,9 +166,8 @@ export default function Text({ content, tags, creator, users }: TextProps) { node.type === "definition") ) { node.type = "text"; - node.value = content - .slice(node.position.start.offset, node.position.end.offset) - .replace(/\)$/, " )"); + const position = unwrap(node.position); + node.value = content.slice(position.start.offset, position.end.offset).replace(/\)$/, " )"); return SKIP; } }); @@ -182,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 36463baa..7d5923e2 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,23 @@ 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.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 9c9f20dd..cc82312b 100644 --- a/src/Element/Thread.tsx +++ b/src/Element/Thread.tsx @@ -6,20 +6,16 @@ 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()) { - const fs = vs.map((a) => a.Id); +function getParent(ev: HexKey, chains: Map): HexKey | undefined { + for (const [k, vs] of chains.entries()) { + const fs = vs.map(a => a.Id); if (fs.includes(ev)) { return k; } @@ -50,31 +46,17 @@ interface SubthreadProps { onNavigate: (e: u256) => void; } -const Subthread = ({ - active, - path, - from, - 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 ( <>
    @@ -151,11 +131,7 @@ const ThreadNote = ({ onNavigate={onNavigate} /> ) : ( - + { +const TierTwo = ({ active, isLastSubthread, path, from, notes, related, chains, onNavigate }: SubthreadProps) => { const [first, ...rest] = notes; return ( @@ -218,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 && (
    -
    @@ -286,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); - 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) + ?.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; + .forEach(v => { + 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 +310,7 @@ export default function Thread(props: ThreadProps) { return; } - let subthreadPath = []; + const subthreadPath = []; let parent = getParent(urlNoteHex, chains); while (parent) { subthreadPath.unshift(parent); @@ -381,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); } } @@ -414,7 +341,7 @@ export default function Thread(props: ThreadProps) { if (!from || !chains) { return; } - let replies = chains.get(from); + const replies = chains.get(from); if (replies) { 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)}
    @@ -476,6 +396,6 @@ function getReplies(from: u256, chains?: Map): 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..dc0676ee 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 @@ -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 ( -