From 7cc9c9a4321d192beeaf3d0730724a53908dac47 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Fri, 14 Jul 2023 18:23:38 -0500 Subject: [PATCH] performance improvements --- .changeset/ninety-otters-nail.md | 5 + .changeset/tasty-buckets-dream.md | 5 + src/components/embed-types/nostr.tsx | 8 +- src/components/embeded-content.tsx | 3 +- src/components/layout/index.tsx | 2 +- src/components/note/embedded-note.tsx | 34 +- src/components/note/index.tsx | 5 +- src/components/note/note-contents.tsx | 8 +- .../generic-note-timeline/repost-note.tsx | 10 +- src/components/timeline-page/index.tsx | 5 +- .../timeline-page/media-timeline/index.tsx | 11 +- src/helpers/embeds.ts | 15 +- src/helpers/regexp.ts | 2 +- src/helpers/zaps.ts | 19 +- src/hooks/use-open-graph-data.ts | 11 +- src/providers/intersection-observer.tsx | 32 +- src/views/messages/chat.tsx | 3 +- src/views/note/index.tsx | 6 +- src/views/notifications/index.tsx | 5 +- src/views/streams/index.tsx | 5 +- src/views/user/likes.tsx | 5 +- src/views/user/streams.tsx | 6 +- src/views/user/zaps.tsx | 5 +- yarn.lock | 1968 ++++++++--------- 24 files changed, 1003 insertions(+), 1175 deletions(-) create mode 100644 .changeset/ninety-otters-nail.md create mode 100644 .changeset/tasty-buckets-dream.md diff --git a/.changeset/ninety-otters-nail.md b/.changeset/ninety-otters-nail.md new file mode 100644 index 000000000..acbdea5e4 --- /dev/null +++ b/.changeset/ninety-otters-nail.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +cache url open graph data diff --git a/.changeset/tasty-buckets-dream.md b/.changeset/tasty-buckets-dream.md new file mode 100644 index 000000000..478690fcc --- /dev/null +++ b/.changeset/tasty-buckets-dream.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Performance improvements diff --git a/src/components/embed-types/nostr.tsx b/src/components/embed-types/nostr.tsx index f0a36b0f9..48518ebf5 100644 --- a/src/components/embed-types/nostr.tsx +++ b/src/components/embed-types/nostr.tsx @@ -32,10 +32,10 @@ export function embedNostrLinks(content: EmbedableContent) { return ; } default: - return match[0]; + return null; } } catch (e) { - return match[0]; + return null; } }, }); @@ -58,7 +58,7 @@ export function embedNostrMentions(content: EmbedableContent, event: NostrEvent } } - return match[0]; + return null; }, }); } @@ -87,7 +87,7 @@ export function embedNostrHashtags(content: EmbedableContent, event: NostrEvent ); } - return match[0]; + return null; }, }); } diff --git a/src/components/embeded-content.tsx b/src/components/embeded-content.tsx index f85615a61..d111b3b57 100644 --- a/src/components/embeded-content.tsx +++ b/src/components/embeded-content.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { EmbedableContent } from "../helpers/embeds"; import { Text } from "@chakra-ui/react"; @@ -11,7 +10,7 @@ export default function EmbeddedContent({ content }: { content: EmbedableContent {part} ) : ( - React.cloneElement(part, { key: "part-" + i }) + part ) )} diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index 5612dee19..aa8c1e471 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -15,7 +15,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { {!isMobile && } - + {children} {isMobile && ( diff --git a/src/components/note/embedded-note.tsx b/src/components/note/embedded-note.tsx index 8f0590c80..a00f0db68 100644 --- a/src/components/note/embedded-note.tsx +++ b/src/components/note/embedded-note.tsx @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { Card, CardBody, CardHeader, Flex, Heading } from "@chakra-ui/react"; +import { Button, Card, CardBody, CardHeader, Spacer, useDisclosure } from "@chakra-ui/react"; import { NoteContents } from "./note-contents"; import { NostrEvent } from "../../types/nostr-event"; @@ -11,31 +11,29 @@ import appSettings from "../../services/app-settings"; import EventVerificationIcon from "../event-verification-icon"; import { TrustProvider } from "../../providers/trust"; import { NoteLink } from "../note-link"; +import { ArrowDownSIcon, ArrowUpSIcon } from "../icons"; export default function EmbeddedNote({ note }: { note: NostrEvent }) { const { showSignatureVerification } = useSubject(appSettings); + const expand = useDisclosure(); return ( - - - - - - - - - - {showSignatureVerification && } - - {dayjs.unix(note.created_at).fromNow()} - - + + + + + + + {showSignatureVerification && } + + {dayjs.unix(note.created_at).fromNow()} + - - - + {expand.isOpen && } ); diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index e6f7e028c..52d32ee4a 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -59,10 +59,7 @@ export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteP - - - - + {showSignatureVerification && } diff --git a/src/components/note/note-contents.tsx b/src/components/note/note-contents.tsx index 0168dedbf..0f1d5b14d 100644 --- a/src/components/note/note-contents.tsx +++ b/src/components/note/note-contents.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from "react"; import { Box } from "@chakra-ui/react"; import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event"; -import styled from "@emotion/styled"; +import { css } from "@emotion/react"; import { useExpand } from "./expanded"; import { EmbedableContent, embedUrls } from "../../helpers/embeds"; import { @@ -53,7 +53,7 @@ function buildContents(event: NostrEvent | DraftNostrEvent) { return content; } -const GradientOverlay = styled.div` +const gradientOverlayStyles = css` position: absolute; left: 0; right: 0; @@ -100,7 +100,7 @@ export const NoteContents = React.memo(({ event, maxHeight }: NoteContentsProps)
- {showOverlay && } + {showOverlay && } ); diff --git a/src/components/timeline-page/generic-note-timeline/repost-note.tsx b/src/components/timeline-page/generic-note-timeline/repost-note.tsx index f75631dfb..7b7f4aef0 100644 --- a/src/components/timeline-page/generic-note-timeline/repost-note.tsx +++ b/src/components/timeline-page/generic-note-timeline/repost-note.tsx @@ -11,14 +11,16 @@ import { UserDnsIdentityIcon } from "../../user-dns-identity-icon"; import { UserLink } from "../../user-link"; import { TrustProvider } from "../../../providers/trust"; import { safeJson } from "../../../helpers/parse"; -import { verifySignature } from "nostr-tools"; import { useReadRelayUrls } from "../../../hooks/use-client-relays"; import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; -function parseHardcodedNoteContent(event: NostrEvent): NostrEvent | null { +function parseHardcodedNoteContent(event: NostrEvent) { const json = safeJson(event.content, null); - if (json) verifySignature(json); - return null; + + // TODO: disabled until signature verification can be done in another thread + // if (json && !verifySignature(json)) return null; + + return (json as NostrEvent) ?? null; } export default function RepostNote({ event, maxHeight }: { event: NostrEvent; maxHeight?: number }) { diff --git a/src/components/timeline-page/index.tsx b/src/components/timeline-page/index.tsx index 4be2a6554..1392b8160 100644 --- a/src/components/timeline-page/index.tsx +++ b/src/components/timeline-page/index.tsx @@ -30,7 +30,6 @@ export type TimelineViewType = "timeline" | "images"; export default function TimelinePage({ timeline, header }: { timeline: TimelineLoader; header?: React.ReactNode }) { const isMobile = useIsMobile(); - const scrollBox = useRef(null); const callback = useTimelineCurserIntersectionCallback(timeline); const [params, setParams] = useSearchParams(); @@ -54,8 +53,8 @@ export default function TimelinePage({ timeline, header }: { timeline: TimelineL } }; return ( - root={scrollBox} callback={callback}> - + callback={callback}> + {header} {renderTimeline()} diff --git a/src/components/timeline-page/media-timeline/index.tsx b/src/components/timeline-page/media-timeline/index.tsx index 3a7c26a1d..b86e31be7 100644 --- a/src/components/timeline-page/media-timeline/index.tsx +++ b/src/components/timeline-page/media-timeline/index.tsx @@ -9,18 +9,11 @@ import { useNavigate } from "react-router-dom"; import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; import { getSharableNoteId } from "../../../helpers/nip19"; import { ExternalLinkIcon } from "../../icons"; -import styled from "@emotion/styled"; const matchAllImages = new RegExp(matchImageUrls, "ig"); type ImagePreview = { eventId: string; src: string; index: number }; -const StyledImageGalleryLink = styled(ImageGalleryLink)` - &:not(:hover) > button { - display: none; - } -`; - const ImagePreview = React.memo(({ image }: { image: ImagePreview }) => { const navigate = useNavigate(); @@ -28,7 +21,7 @@ const ImagePreview = React.memo(({ image }: { image: ImagePreview }) => { useRegisterIntersectionEntity(ref, image.eventId); return ( - + } @@ -44,7 +37,7 @@ const ImagePreview = React.memo(({ image }: { image: ImagePreview }) => { navigate(`/n/${getSharableNoteId(image.eventId)}`); }} /> - + ); }); diff --git a/src/helpers/embeds.ts b/src/helpers/embeds.ts index 01e8ef41d..d13149178 100644 --- a/src/helpers/embeds.ts +++ b/src/helpers/embeds.ts @@ -26,15 +26,20 @@ export function embedJSX(content: EmbedableContent, embed: EmbedType): Embedable const { start, end } = (embed.getLocation || defaultGetLocation)(match); const before = subContent.slice(0, start); const after = subContent.slice(end, subContent.length); - let embedRender = embed.render(match); + let render = embed.render(match); - if (embedRender === null) return subContent; + if (render === null) return subContent; - if (typeof embedRender !== "string" && !embedRender.props.key) { - embedRender = cloneElement(embedRender, { key: embed.name + i }); + if (typeof render !== "string" && !render.props.key) { + render = cloneElement(render, { key: embed.name + i }); } - return [...embedJSX([before], embed), embedRender, ...embedJSX([after], embed)]; + const newContent: EmbedableContent = []; + if (before.length > 0) newContent.push(...embedJSX([before], embed)); + newContent.push(render); + if (after.length > 0) newContent.push(...embedJSX([after], embed)); + + return newContent; } } diff --git a/src/helpers/regexp.ts b/src/helpers/regexp.ts index 3ad504635..d6df77802 100644 --- a/src/helpers/regexp.ts +++ b/src/helpers/regexp.ts @@ -3,4 +3,4 @@ export const matchImageUrls = /https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,12})((?:\/[\+~%\/\.\w\-_]*)?\.(?:svg|gif|png|jpg|jpeg|webp|avif))(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/i; export const matchNostrLink = /(nostr:|@)?((npub|note|nprofile|nevent)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,})/i; -export const matchHashtag = /(^|[^\p{L}])#(\p{L}+)/iu; +export const matchHashtag = /(^|[^\p{L}])#([\p{L}\p{N}]+)/iu; diff --git a/src/helpers/zaps.ts b/src/helpers/zaps.ts index 997298e2b..e07308c8f 100644 --- a/src/helpers/zaps.ts +++ b/src/helpers/zaps.ts @@ -52,15 +52,17 @@ export type ParsedZap = { eventId?: string; }; -function parseZapEvent(event: NostrEvent): ParsedZap { +export function parseZapEvent(event: NostrEvent): ParsedZap { const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1]; if (!zapRequestStr) throw new Error("no description tag"); const bolt11 = event.tags.find((t) => t[0] === "bolt11")?.[1]; if (!bolt11) throw new Error("missing bolt11 invoice"); - const error = nip57.validateZapRequest(zapRequestStr); - if (error) throw new Error(error); + // TODO: disabled until signature verification can be offloaded to a web worker + + // const error = nip57.validateZapRequest(zapRequestStr); + // if (error) throw new Error(error); const request = JSON.parse(zapRequestStr) as NostrEvent; const payment = parsePaymentRequest(bolt11); @@ -75,15 +77,6 @@ function parseZapEvent(event: NostrEvent): ParsedZap { }; } -const zapEventCache = new Map>(); -function cachedParseZapEvent(event: NostrEvent) { - let result = zapEventCache.get(event.id); - if (result) return result; - result = parseZapEvent(event); - if (result) zapEventCache.set(event.id, result); - return result; -} - export async function requestZapInvoice(zapRequest: NostrEvent, lnurl: string) { const amount = zapRequest.tags.find((t) => t[0] === "amount")?.[1]; if (!amount) throw new Error("missing amount"); @@ -101,5 +94,3 @@ export async function requestZapInvoice(zapRequest: NostrEvent, lnurl: string) { return payRequest as string; } else throw new Error("Failed to get invoice"); } - -export { cachedParseZapEvent as parseZapEvent }; diff --git a/src/hooks/use-open-graph-data.ts b/src/hooks/use-open-graph-data.ts index 40ee59c81..8231144df 100644 --- a/src/hooks/use-open-graph-data.ts +++ b/src/hooks/use-open-graph-data.ts @@ -1,22 +1,29 @@ import { useAsync } from "react-use"; import extractMetaTags from "../lib/open-graph-scraper/extract"; import { fetchWithCorsFallback } from "../helpers/cors"; +import { OgObjectInteral } from "../lib/open-graph-scraper/types"; const pageExtensions = [".html", ".php", "htm"]; +const openGraphDataCache = new Map(); + export default function useOpenGraphData(url: URL) { return useAsync(async () => { - const controller = new AbortController(); + if (openGraphDataCache.has(url.toString())) return openGraphDataCache.get(url.toString()); + const ext = url.pathname.match(/\.[\w+d]+$/)?.[0]; if (ext && !pageExtensions.includes(ext)) return null; try { + const controller = new AbortController(); const res = await fetchWithCorsFallback(url, { signal: controller.signal }); const contentType = res.headers.get("content-type"); if (contentType?.includes("text/html")) { const html = await res.text(); - return extractMetaTags(html); + const data = extractMetaTags(html); + openGraphDataCache.set(url.toString(), data); + return data; } else controller.abort(); } catch (e) {} return null; diff --git a/src/providers/intersection-observer.tsx b/src/providers/intersection-observer.tsx index 1b4f97be9..6e8106891 100644 --- a/src/providers/intersection-observer.tsx +++ b/src/providers/intersection-observer.tsx @@ -64,29 +64,29 @@ export default function IntersectionObserverProvider({ threshold, callback, }: PropsWithChildren & { - root: MutableRefObject; + root?: MutableRefObject; rootMargin?: IntersectionObserverInit["rootMargin"]; threshold?: IntersectionObserverInit["threshold"]; callback: ExtendedIntersectionObserverCallback; }) { const elementIds = useMemo(() => new WeakMap(), []); - const [observer, setObserver] = useState(); + + const handleIntersection = useCallback((entries, observer) => { + callback( + entries.map((entry) => { + return { entry, id: elementIds.get(entry.target) }; + }), + observer + ); + }, []); + const [observer, setObserver] = useState( + () => new IntersectionObserver(handleIntersection, { rootMargin, threshold }) + ); useMount(() => { - if (root.current) { - const observer = new IntersectionObserver( - (entries, observer) => { - callback( - entries.map((entry) => { - return { entry, id: elementIds.get(entry.target) }; - }), - observer - ); - }, - { rootMargin, threshold } - ); - - setObserver(observer); + if (root?.current) { + // recreate observer with root + setObserver(new IntersectionObserver(handleIntersection, { rootMargin, threshold, root: root.current })); } }); useUnmount(() => { diff --git a/src/views/messages/chat.tsx b/src/views/messages/chat.tsx index 3f38e9dae..40eee36ac 100644 --- a/src/views/messages/chat.tsx +++ b/src/views/messages/chat.tsx @@ -63,11 +63,10 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) { setContent(""); }; - const scrollBox = useRef(null); const callback = useTimelineCurserIntersectionCallback(timeline); return ( - + diff --git a/src/views/note/index.tsx b/src/views/note/index.tsx index 80f0383ad..c3e76b874 100644 --- a/src/views/note/index.tsx +++ b/src/views/note/index.tsx @@ -21,7 +21,7 @@ function useNotePointer() { } } -const NoteView = () => { +export default function NoteView() { const pointer = useNotePointer(); const { thread, events, rootId, focusId, loading } = useThreadLoader(pointer.id, pointer.relays, { @@ -65,6 +65,4 @@ const NoteView = () => { {pageContent} ); -}; - -export default NoteView; +} diff --git a/src/views/notifications/index.tsx b/src/views/notifications/index.tsx index 130d67970..056c95508 100644 --- a/src/views/notifications/index.tsx +++ b/src/views/notifications/index.tsx @@ -115,12 +115,11 @@ function NotificationsPage() { const events = useSubject(timeline?.timeline) ?? []; - const scrollBox = useRef(null); const callback = useTimelineCurserIntersectionCallback(timeline); return ( - - + + {events.map((event) => ( ))} diff --git a/src/views/streams/index.tsx b/src/views/streams/index.tsx index e4eb59c68..88ede7af5 100644 --- a/src/views/streams/index.tsx +++ b/src/views/streams/index.tsx @@ -31,7 +31,6 @@ function StreamsPage() { useRelaysChanged(readRelays, () => timeline.reset()); - const scrollBox = useRef(null); const callback = useTimelineCurserIntersectionCallback(timeline); const events = useSubject(timeline.timeline); @@ -58,8 +57,8 @@ function StreamsPage() { - - + + {streams.map((stream) => ( ))} diff --git a/src/views/user/likes.tsx b/src/views/user/likes.tsx index d15dab427..2c33efb6d 100644 --- a/src/views/user/likes.tsx +++ b/src/views/user/likes.tsx @@ -60,13 +60,12 @@ export default function UserLikesTab() { const lines = useSubject(timeline.timeline); - const scrollBox = useRef(null); const callback = useTimelineCurserIntersectionCallback(timeline); return ( - + - + {lines.map((event) => ( ))} diff --git a/src/views/user/streams.tsx b/src/views/user/streams.tsx index 4ba1e96f1..98b6cb879 100644 --- a/src/views/user/streams.tsx +++ b/src/views/user/streams.tsx @@ -1,4 +1,3 @@ -import { useRef } from "react"; import { Flex } from "@chakra-ui/react"; import { useOutletContext } from "react-router-dom"; import { truncatedId } from "../../helpers/nostr-event"; @@ -22,12 +21,11 @@ export default function UserStreamsTab() { { "#p": [pubkey], kinds: [STREAM_KIND] }, ]); - const scrollBox = useRef(null); const callback = useTimelineCurserIntersectionCallback(timeline); return ( - root={scrollBox} callback={callback}> - + callback={callback}> + diff --git a/src/views/user/zaps.tsx b/src/views/user/zaps.tsx index 6099356bd..b5d8f94b1 100644 --- a/src/views/user/zaps.tsx +++ b/src/views/user/zaps.tsx @@ -102,12 +102,11 @@ const UserZapsTab = () => { return parsed; }, [events]); - const scrollBox = useRef(null); const callback = useTimelineCurserIntersectionCallback(timeline); return ( - - + +