mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-07-09 15:50:01 +02:00
performance improvements
This commit is contained in:
5
.changeset/ninety-otters-nail.md
Normal file
5
.changeset/ninety-otters-nail.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
cache url open graph data
|
5
.changeset/tasty-buckets-dream.md
Normal file
5
.changeset/tasty-buckets-dream.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Performance improvements
|
@ -32,10 +32,10 @@ export function embedNostrLinks(content: EmbedableContent) {
|
|||||||
return <QuoteNote noteId={pointer.id} relay={pointer.relays?.[0]} />;
|
return <QuoteNote noteId={pointer.id} relay={pointer.relays?.[0]} />;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return match[0];
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { EmbedableContent } from "../helpers/embeds";
|
import { EmbedableContent } from "../helpers/embeds";
|
||||||
import { Text } from "@chakra-ui/react";
|
import { Text } from "@chakra-ui/react";
|
||||||
|
|
||||||
@ -11,7 +10,7 @@ export default function EmbeddedContent({ content }: { content: EmbedableContent
|
|||||||
{part}
|
{part}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
React.cloneElement(part, { key: "part-" + i })
|
part
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -15,7 +15,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<ReloadPrompt mb="2" />
|
<ReloadPrompt mb="2" />
|
||||||
<Container size="lg" display="flex" padding="0" gap="4" alignItems="flex-start">
|
<Container size="lg" display="flex" padding="0" gap="4" alignItems="flex-start">
|
||||||
{!isMobile && <DesktopSideNav position="sticky" top="0" />}
|
{!isMobile && <DesktopSideNav position="sticky" top="0" />}
|
||||||
<Flex flexGrow={1} direction="column" w="full" overflowX="hidden" pb={isMobile ? "14" : 0}>
|
<Flex flexGrow={1} direction="column" w="full" overflowX="hidden" overflowY="visible" pb={isMobile ? "14" : 0}>
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</Flex>
|
</Flex>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import dayjs from "dayjs";
|
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 { NoteContents } from "./note-contents";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
@ -11,31 +11,29 @@ import appSettings from "../../services/app-settings";
|
|||||||
import EventVerificationIcon from "../event-verification-icon";
|
import EventVerificationIcon from "../event-verification-icon";
|
||||||
import { TrustProvider } from "../../providers/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
import { NoteLink } from "../note-link";
|
import { NoteLink } from "../note-link";
|
||||||
|
import { ArrowDownSIcon, ArrowUpSIcon } from "../icons";
|
||||||
|
|
||||||
export default function EmbeddedNote({ note }: { note: NostrEvent }) {
|
export default function EmbeddedNote({ note }: { note: NostrEvent }) {
|
||||||
const { showSignatureVerification } = useSubject(appSettings);
|
const { showSignatureVerification } = useSubject(appSettings);
|
||||||
|
const expand = useDisclosure();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrustProvider event={note}>
|
<TrustProvider event={note}>
|
||||||
<Card variant="outline">
|
<Card variant="outline">
|
||||||
<CardHeader padding="2">
|
<CardHeader padding="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
|
||||||
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
|
<UserAvatarLink pubkey={note.pubkey} size="sm" />
|
||||||
<UserAvatarLink pubkey={note.pubkey} size="xs" />
|
<UserLink pubkey={note.pubkey} fontWeight="bold" isTruncated fontSize="lg" />
|
||||||
|
|
||||||
<Heading size="sm" display="inline">
|
|
||||||
<UserLink pubkey={note.pubkey} />
|
|
||||||
</Heading>
|
|
||||||
<UserDnsIdentityIcon pubkey={note.pubkey} onlyIcon />
|
<UserDnsIdentityIcon pubkey={note.pubkey} onlyIcon />
|
||||||
<Flex grow={1} />
|
<Button size="sm" onClick={expand.onToggle} leftIcon={expand.isOpen ? <ArrowUpSIcon /> : <ArrowDownSIcon />}>
|
||||||
|
Expand
|
||||||
|
</Button>
|
||||||
|
<Spacer />
|
||||||
{showSignatureVerification && <EventVerificationIcon event={note} />}
|
{showSignatureVerification && <EventVerificationIcon event={note} />}
|
||||||
<NoteLink noteId={note.id} color="current" whiteSpace="nowrap">
|
<NoteLink noteId={note.id} color="current" whiteSpace="nowrap">
|
||||||
{dayjs.unix(note.created_at).fromNow()}
|
{dayjs.unix(note.created_at).fromNow()}
|
||||||
</NoteLink>
|
</NoteLink>
|
||||||
</Flex>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody p="0">
|
<CardBody p="0">{expand.isOpen && <NoteContents event={note} />}</CardBody>
|
||||||
<NoteContents event={note} maxHeight={200} />
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
</Card>
|
||||||
</TrustProvider>
|
</TrustProvider>
|
||||||
);
|
);
|
||||||
|
@ -59,10 +59,7 @@ export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteP
|
|||||||
<CardHeader padding="2">
|
<CardHeader padding="2">
|
||||||
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
|
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
|
||||||
<UserAvatarLink pubkey={event.pubkey} size={isMobile ? "xs" : "sm"} />
|
<UserAvatarLink pubkey={event.pubkey} size={isMobile ? "xs" : "sm"} />
|
||||||
|
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||||
<Heading size="sm" display="inline">
|
|
||||||
<UserLink pubkey={event.pubkey} />
|
|
||||||
</Heading>
|
|
||||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||||
<Flex grow={1} />
|
<Flex grow={1} />
|
||||||
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
||||||
|
@ -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 { Box } from "@chakra-ui/react";
|
||||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||||
import styled from "@emotion/styled";
|
import { css } from "@emotion/react";
|
||||||
import { useExpand } from "./expanded";
|
import { useExpand } from "./expanded";
|
||||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||||
import {
|
import {
|
||||||
@ -53,7 +53,7 @@ function buildContents(event: NostrEvent | DraftNostrEvent) {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GradientOverlay = styled.div`
|
const gradientOverlayStyles = css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -100,7 +100,7 @@ export const NoteContents = React.memo(({ event, maxHeight }: NoteContentsProps)
|
|||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<EmbeddedContent content={content} />
|
<EmbeddedContent content={content} />
|
||||||
</div>
|
</div>
|
||||||
{showOverlay && <GradientOverlay onClick={expand?.onExpand} />}
|
{showOverlay && <Box css={gradientOverlayStyles} onClick={expand?.onExpand} />}
|
||||||
</Box>
|
</Box>
|
||||||
</ImageGalleryProvider>
|
</ImageGalleryProvider>
|
||||||
);
|
);
|
||||||
|
@ -11,14 +11,16 @@ import { UserDnsIdentityIcon } from "../../user-dns-identity-icon";
|
|||||||
import { UserLink } from "../../user-link";
|
import { UserLink } from "../../user-link";
|
||||||
import { TrustProvider } from "../../../providers/trust";
|
import { TrustProvider } from "../../../providers/trust";
|
||||||
import { safeJson } from "../../../helpers/parse";
|
import { safeJson } from "../../../helpers/parse";
|
||||||
import { verifySignature } from "nostr-tools";
|
|
||||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
|
|
||||||
function parseHardcodedNoteContent(event: NostrEvent): NostrEvent | null {
|
function parseHardcodedNoteContent(event: NostrEvent) {
|
||||||
const json = safeJson(event.content, null);
|
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 }) {
|
export default function RepostNote({ event, maxHeight }: { event: NostrEvent; maxHeight?: number }) {
|
||||||
|
@ -30,7 +30,6 @@ export type TimelineViewType = "timeline" | "images";
|
|||||||
export default function TimelinePage({ timeline, header }: { timeline: TimelineLoader; header?: React.ReactNode }) {
|
export default function TimelinePage({ timeline, header }: { timeline: TimelineLoader; header?: React.ReactNode }) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
const [params, setParams] = useSearchParams();
|
const [params, setParams] = useSearchParams();
|
||||||
@ -54,8 +53,8 @@ export default function TimelinePage({ timeline, header }: { timeline: TimelineL
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider<string> root={scrollBox} callback={callback}>
|
<IntersectionObserverProvider<string> callback={callback}>
|
||||||
<Flex direction="column" gap="2" pt="4" pb="8" h="full" overflowY="auto" overflowX="hidden" ref={scrollBox}>
|
<Flex direction="column" gap="2" pt="4" pb="8">
|
||||||
{header}
|
{header}
|
||||||
{renderTimeline()}
|
{renderTimeline()}
|
||||||
<TimelineActionAndStatus timeline={timeline} />
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
|
@ -9,18 +9,11 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
import { getSharableNoteId } from "../../../helpers/nip19";
|
import { getSharableNoteId } from "../../../helpers/nip19";
|
||||||
import { ExternalLinkIcon } from "../../icons";
|
import { ExternalLinkIcon } from "../../icons";
|
||||||
import styled from "@emotion/styled";
|
|
||||||
|
|
||||||
const matchAllImages = new RegExp(matchImageUrls, "ig");
|
const matchAllImages = new RegExp(matchImageUrls, "ig");
|
||||||
|
|
||||||
type ImagePreview = { eventId: string; src: string; index: number };
|
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 ImagePreview = React.memo(({ image }: { image: ImagePreview }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -28,7 +21,7 @@ const ImagePreview = React.memo(({ image }: { image: ImagePreview }) => {
|
|||||||
useRegisterIntersectionEntity(ref, image.eventId);
|
useRegisterIntersectionEntity(ref, image.eventId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledImageGalleryLink href={image.src} position="relative" ref={ref}>
|
<ImageGalleryLink href={image.src} position="relative" ref={ref}>
|
||||||
<Box aspectRatio={1} backgroundImage={`url(${image.src})`} backgroundSize="cover" backgroundPosition="center" />
|
<Box aspectRatio={1} backgroundImage={`url(${image.src})`} backgroundSize="cover" backgroundPosition="center" />
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ExternalLinkIcon />}
|
icon={<ExternalLinkIcon />}
|
||||||
@ -44,7 +37,7 @@ const ImagePreview = React.memo(({ image }: { image: ImagePreview }) => {
|
|||||||
navigate(`/n/${getSharableNoteId(image.eventId)}`);
|
navigate(`/n/${getSharableNoteId(image.eventId)}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledImageGalleryLink>
|
</ImageGalleryLink>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,15 +26,20 @@ export function embedJSX(content: EmbedableContent, embed: EmbedType): Embedable
|
|||||||
const { start, end } = (embed.getLocation || defaultGetLocation)(match);
|
const { start, end } = (embed.getLocation || defaultGetLocation)(match);
|
||||||
const before = subContent.slice(0, start);
|
const before = subContent.slice(0, start);
|
||||||
const after = subContent.slice(end, subContent.length);
|
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) {
|
if (typeof render !== "string" && !render.props.key) {
|
||||||
embedRender = cloneElement(embedRender, { key: embed.name + i });
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
/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 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;
|
||||||
|
@ -52,15 +52,17 @@ export type ParsedZap = {
|
|||||||
eventId?: string;
|
eventId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseZapEvent(event: NostrEvent): ParsedZap {
|
export function parseZapEvent(event: NostrEvent): ParsedZap {
|
||||||
const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
|
const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
|
||||||
if (!zapRequestStr) throw new Error("no description tag");
|
if (!zapRequestStr) throw new Error("no description tag");
|
||||||
|
|
||||||
const bolt11 = event.tags.find((t) => t[0] === "bolt11")?.[1];
|
const bolt11 = event.tags.find((t) => t[0] === "bolt11")?.[1];
|
||||||
if (!bolt11) throw new Error("missing bolt11 invoice");
|
if (!bolt11) throw new Error("missing bolt11 invoice");
|
||||||
|
|
||||||
const error = nip57.validateZapRequest(zapRequestStr);
|
// TODO: disabled until signature verification can be offloaded to a web worker
|
||||||
if (error) throw new Error(error);
|
|
||||||
|
// const error = nip57.validateZapRequest(zapRequestStr);
|
||||||
|
// if (error) throw new Error(error);
|
||||||
|
|
||||||
const request = JSON.parse(zapRequestStr) as NostrEvent;
|
const request = JSON.parse(zapRequestStr) as NostrEvent;
|
||||||
const payment = parsePaymentRequest(bolt11);
|
const payment = parsePaymentRequest(bolt11);
|
||||||
@ -75,15 +77,6 @@ function parseZapEvent(event: NostrEvent): ParsedZap {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const zapEventCache = new Map<string, ReturnType<typeof parseZapEvent>>();
|
|
||||||
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) {
|
export async function requestZapInvoice(zapRequest: NostrEvent, lnurl: string) {
|
||||||
const amount = zapRequest.tags.find((t) => t[0] === "amount")?.[1];
|
const amount = zapRequest.tags.find((t) => t[0] === "amount")?.[1];
|
||||||
if (!amount) throw new Error("missing amount");
|
if (!amount) throw new Error("missing amount");
|
||||||
@ -101,5 +94,3 @@ export async function requestZapInvoice(zapRequest: NostrEvent, lnurl: string) {
|
|||||||
return payRequest as string;
|
return payRequest as string;
|
||||||
} else throw new Error("Failed to get invoice");
|
} else throw new Error("Failed to get invoice");
|
||||||
}
|
}
|
||||||
|
|
||||||
export { cachedParseZapEvent as parseZapEvent };
|
|
||||||
|
@ -1,22 +1,29 @@
|
|||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
import extractMetaTags from "../lib/open-graph-scraper/extract";
|
import extractMetaTags from "../lib/open-graph-scraper/extract";
|
||||||
import { fetchWithCorsFallback } from "../helpers/cors";
|
import { fetchWithCorsFallback } from "../helpers/cors";
|
||||||
|
import { OgObjectInteral } from "../lib/open-graph-scraper/types";
|
||||||
|
|
||||||
const pageExtensions = [".html", ".php", "htm"];
|
const pageExtensions = [".html", ".php", "htm"];
|
||||||
|
|
||||||
|
const openGraphDataCache = new Map<string, OgObjectInteral>();
|
||||||
|
|
||||||
export default function useOpenGraphData(url: URL) {
|
export default function useOpenGraphData(url: URL) {
|
||||||
return useAsync(async () => {
|
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];
|
const ext = url.pathname.match(/\.[\w+d]+$/)?.[0];
|
||||||
if (ext && !pageExtensions.includes(ext)) return null;
|
if (ext && !pageExtensions.includes(ext)) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
const res = await fetchWithCorsFallback(url, { signal: controller.signal });
|
const res = await fetchWithCorsFallback(url, { signal: controller.signal });
|
||||||
const contentType = res.headers.get("content-type");
|
const contentType = res.headers.get("content-type");
|
||||||
|
|
||||||
if (contentType?.includes("text/html")) {
|
if (contentType?.includes("text/html")) {
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
return extractMetaTags(html);
|
const data = extractMetaTags(html);
|
||||||
|
openGraphDataCache.set(url.toString(), data);
|
||||||
|
return data;
|
||||||
} else controller.abort();
|
} else controller.abort();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return null;
|
return null;
|
||||||
|
@ -64,29 +64,29 @@ export default function IntersectionObserverProvider<T = undefined>({
|
|||||||
threshold,
|
threshold,
|
||||||
callback,
|
callback,
|
||||||
}: PropsWithChildren & {
|
}: PropsWithChildren & {
|
||||||
root: MutableRefObject<HTMLElement | null>;
|
root?: MutableRefObject<HTMLElement | null>;
|
||||||
rootMargin?: IntersectionObserverInit["rootMargin"];
|
rootMargin?: IntersectionObserverInit["rootMargin"];
|
||||||
threshold?: IntersectionObserverInit["threshold"];
|
threshold?: IntersectionObserverInit["threshold"];
|
||||||
callback: ExtendedIntersectionObserverCallback<T>;
|
callback: ExtendedIntersectionObserverCallback<T>;
|
||||||
}) {
|
}) {
|
||||||
const elementIds = useMemo(() => new WeakMap<Element, T>(), []);
|
const elementIds = useMemo(() => new WeakMap<Element, T>(), []);
|
||||||
const [observer, setObserver] = useState<IntersectionObserver>();
|
|
||||||
|
|
||||||
useMount(() => {
|
const handleIntersection = useCallback<IntersectionObserverCallback>((entries, observer) => {
|
||||||
if (root.current) {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries, observer) => {
|
|
||||||
callback(
|
callback(
|
||||||
entries.map((entry) => {
|
entries.map((entry) => {
|
||||||
return { entry, id: elementIds.get(entry.target) };
|
return { entry, id: elementIds.get(entry.target) };
|
||||||
}),
|
}),
|
||||||
observer
|
observer
|
||||||
);
|
);
|
||||||
},
|
}, []);
|
||||||
{ rootMargin, threshold }
|
const [observer, setObserver] = useState<IntersectionObserver>(
|
||||||
|
() => new IntersectionObserver(handleIntersection, { rootMargin, threshold })
|
||||||
);
|
);
|
||||||
|
|
||||||
setObserver(observer);
|
useMount(() => {
|
||||||
|
if (root?.current) {
|
||||||
|
// recreate observer with root
|
||||||
|
setObserver(new IntersectionObserver(handleIntersection, { rootMargin, threshold, root: root.current }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
useUnmount(() => {
|
useUnmount(() => {
|
||||||
|
@ -63,11 +63,10 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
|||||||
setContent("");
|
setContent("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider root={scrollBox} callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<Flex height="100%" overflow="hidden" direction="column">
|
<Flex height="100%" overflow="hidden" direction="column">
|
||||||
<Card size="sm" flexShrink={0}>
|
<Card size="sm" flexShrink={0}>
|
||||||
<CardBody display="flex" gap="2" alignItems="center">
|
<CardBody display="flex" gap="2" alignItems="center">
|
||||||
|
@ -21,7 +21,7 @@ function useNotePointer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoteView = () => {
|
export default function NoteView() {
|
||||||
const pointer = useNotePointer();
|
const pointer = useNotePointer();
|
||||||
|
|
||||||
const { thread, events, rootId, focusId, loading } = useThreadLoader(pointer.id, pointer.relays, {
|
const { thread, events, rootId, focusId, loading } = useThreadLoader(pointer.id, pointer.relays, {
|
||||||
@ -65,6 +65,4 @@ const NoteView = () => {
|
|||||||
{pageContent}
|
{pageContent}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default NoteView;
|
|
||||||
|
@ -115,12 +115,11 @@ function NotificationsPage() {
|
|||||||
|
|
||||||
const events = useSubject(timeline?.timeline) ?? [];
|
const events = useSubject(timeline?.timeline) ?? [];
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<Flex direction="column" overflowX="hidden" overflowY="auto" gap="2" ref={scrollBox}>
|
<Flex direction="column" gap="2">
|
||||||
{events.map((event) => (
|
{events.map((event) => (
|
||||||
<NotificationItem key={event.id} event={event} />
|
<NotificationItem key={event.id} event={event} />
|
||||||
))}
|
))}
|
||||||
|
@ -31,7 +31,6 @@ function StreamsPage() {
|
|||||||
|
|
||||||
useRelaysChanged(readRelays, () => timeline.reset());
|
useRelaysChanged(readRelays, () => timeline.reset());
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline);
|
const events = useSubject(timeline.timeline);
|
||||||
@ -58,8 +57,8 @@ function StreamsPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
<RelaySelectionButton ml="auto" />
|
<RelaySelectionButton ml="auto" />
|
||||||
</Flex>
|
</Flex>
|
||||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<Flex gap="2" wrap="wrap" overflowY="auto" overflowX="hidden" ref={scrollBox}>
|
<Flex gap="2" wrap="wrap">
|
||||||
{streams.map((stream) => (
|
{streams.map((stream) => (
|
||||||
<StreamCard key={stream.event.id} stream={stream} w="sm" />
|
<StreamCard key={stream.event.id} stream={stream} w="sm" />
|
||||||
))}
|
))}
|
||||||
|
@ -60,13 +60,12 @@ export default function UserLikesTab() {
|
|||||||
|
|
||||||
const lines = useSubject(timeline.timeline);
|
const lines = useSubject(timeline.timeline);
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<TrustProvider trust>
|
<TrustProvider trust>
|
||||||
<Flex direction="column" gap="2" p="2" pb="8" h="full" overflowY="auto" ref={scrollBox}>
|
<Flex direction="column" gap="2" p="2" pb="8">
|
||||||
{lines.map((event) => (
|
{lines.map((event) => (
|
||||||
<Like event={event} />
|
<Like event={event} />
|
||||||
))}
|
))}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useRef } from "react";
|
|
||||||
import { Flex } from "@chakra-ui/react";
|
import { Flex } from "@chakra-ui/react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
import { truncatedId } from "../../helpers/nostr-event";
|
||||||
@ -22,12 +21,11 @@ export default function UserStreamsTab() {
|
|||||||
{ "#p": [pubkey], kinds: [STREAM_KIND] },
|
{ "#p": [pubkey], kinds: [STREAM_KIND] },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider<string> root={scrollBox} callback={callback}>
|
<IntersectionObserverProvider<string> callback={callback}>
|
||||||
<Flex direction="column" gap="2" pt="4" pb="8" h="full" overflowY="auto" overflowX="hidden" ref={scrollBox}>
|
<Flex direction="column" gap="2" pt="4" pb="8">
|
||||||
<GenericNoteTimeline timeline={timeline} />
|
<GenericNoteTimeline timeline={timeline} />
|
||||||
<TimelineActionAndStatus timeline={timeline} />
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -102,12 +102,11 @@ const UserZapsTab = () => {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}, [events]);
|
}, [events]);
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<Flex direction="column" gap="2" p="2" pb="8" h="full" overflowY="auto" ref={scrollBox}>
|
<Flex direction="column" gap="2" p="2" pb="8">
|
||||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
<Select value={filter} onChange={(e) => setFilter(e.target.value)} maxW="md">
|
<Select value={filter} onChange={(e) => setFilter(e.target.value)} maxW="md">
|
||||||
<option value="both">Note & Profile Zaps</option>
|
<option value="both">Note & Profile Zaps</option>
|
||||||
|
Reference in New Issue
Block a user