mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-02 08:58:36 +02:00
cache timelines
This commit is contained in:
parent
038d342ad1
commit
7a339ae03d
5
.changeset/soft-lions-cry.md
Normal file
5
.changeset/soft-lions-cry.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
cache timelines
|
20
src/app.tsx
20
src/app.tsx
@ -37,18 +37,14 @@ import UserAboutTab from "./views/user/about";
|
||||
// code split search view because QrScanner library is 400kB
|
||||
const SearchView = React.lazy(() => import("./views/search"));
|
||||
|
||||
const RootPage = () => {
|
||||
console.log(useLocation());
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ScrollRestoration />
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
const RootPage = () => (
|
||||
<Page>
|
||||
<ScrollRestoration />
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Page>
|
||||
);
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
|
@ -77,8 +77,8 @@ class RelayTimelineLoader {
|
||||
|
||||
export class TimelineLoader {
|
||||
cursor = dayjs().unix();
|
||||
query: NostrQuery;
|
||||
relays: string[];
|
||||
query?: NostrQuery;
|
||||
relays: string[] = [];
|
||||
|
||||
events = new PersistentSubject<NostrEvent[]>([]);
|
||||
timeline = new PersistentSubject<NostrEvent[]>([]);
|
||||
@ -92,14 +92,9 @@ export class TimelineLoader {
|
||||
|
||||
private relayTimelineLoaders = new Map<string, RelayTimelineLoader>();
|
||||
|
||||
constructor(relays: string[], query: NostrQuery, name?: string) {
|
||||
this.query = query;
|
||||
this.relays = relays;
|
||||
|
||||
this.subscription = new NostrMultiSubscription(relays, { ...query, limit: BLOCK_SIZE / 2 }, name);
|
||||
constructor(name?: string) {
|
||||
this.subscription = new NostrMultiSubscription([], undefined, name);
|
||||
this.subscription.onEvent.subscribe(this.handleEvent, this);
|
||||
|
||||
this.createLoaders();
|
||||
}
|
||||
|
||||
private seenEvents = new Set<string>();
|
||||
@ -115,6 +110,8 @@ export class TimelineLoader {
|
||||
}
|
||||
|
||||
private createLoaders() {
|
||||
if (!this.query) return;
|
||||
|
||||
for (const relay of this.relays) {
|
||||
if (!this.relayTimelineLoaders.has(relay)) {
|
||||
const loader = new RelayTimelineLoader(relay, this.query, this.subscription.name);
|
||||
@ -137,6 +134,8 @@ export class TimelineLoader {
|
||||
}
|
||||
|
||||
setRelays(relays: string[]) {
|
||||
if (this.relays.sort().join("|") === relays.sort().join("|")) return;
|
||||
|
||||
// remove loaders
|
||||
this.removeLoaders((loader) => !relays.includes(loader.relay));
|
||||
|
||||
@ -147,6 +146,8 @@ export class TimelineLoader {
|
||||
this.updateComplete();
|
||||
}
|
||||
setQuery(query: NostrQuery) {
|
||||
if (JSON.stringify(this.query) === JSON.stringify(query)) return;
|
||||
|
||||
this.removeLoaders();
|
||||
|
||||
this.query = query;
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { truncatedId } from "../helpers/nostr-event";
|
||||
import { TimelineLoader } from "./timeline-loader";
|
||||
|
||||
export default class UserTimeline extends TimelineLoader {
|
||||
constructor(pubkey: string) {
|
||||
super([], { authors: [pubkey], kinds: [1, 6] }, truncatedId(pubkey) + "-timeline");
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import React, { useMemo } from "react";
|
||||
import { Avatar, AvatarProps } from "@chakra-ui/react";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import { useAsync } from "react-use";
|
||||
import { getIdenticon } from "../services/identicon";
|
||||
import { getIdenticon } from "../helpers/identicon";
|
||||
import { safeUrl } from "../helpers/parse";
|
||||
import appSettings from "../services/app-settings";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
|
@ -17,8 +17,9 @@ export function isRepost(event: NostrEvent | DraftNostrEvent) {
|
||||
return event.kind === 6 || (match && match[0].length === event.content.length);
|
||||
}
|
||||
|
||||
export function truncatedId(id: string, keep = 6) {
|
||||
return id.substring(0, keep) + "..." + id.substring(id.length - keep);
|
||||
export function truncatedId(str: string, keep = 6) {
|
||||
if (str.length < keep * 2 + 3) return str;
|
||||
return str.substring(0, keep) + "..." + str.substring(str.length - keep);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,48 +1,44 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useUnmount } from "react-use";
|
||||
import { TimelineLoader } from "../classes/timeline-loader";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import timelineCacheService from "../services/timeline-cache";
|
||||
|
||||
type Options = {
|
||||
enabled?: boolean;
|
||||
eventFilter?: (event: NostrEvent) => boolean;
|
||||
cursor?: number;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export function useTimelineLoader(key: string, relays: string[], query: NostrQuery, opts?: Options) {
|
||||
if (opts && !opts.name) opts.name = key;
|
||||
|
||||
const ref = useRef<TimelineLoader | null>(null);
|
||||
const loader = (ref.current = ref.current || new TimelineLoader(relays, query, opts?.name));
|
||||
const timeline = useMemo(() => timelineCacheService.createTimeline(key), [key]);
|
||||
|
||||
useEffect(() => {
|
||||
loader.setQuery(query);
|
||||
}, [JSON.stringify(query)]);
|
||||
timeline.setQuery(query);
|
||||
}, [timeline, JSON.stringify(query)]);
|
||||
useEffect(() => {
|
||||
loader.setRelays(relays);
|
||||
}, [relays.join("|")]);
|
||||
timeline.setRelays(relays);
|
||||
}, [timeline, relays.join("|")]);
|
||||
useEffect(() => {
|
||||
loader.setFilter(opts?.eventFilter);
|
||||
}, [opts?.eventFilter]);
|
||||
timeline.setFilter(opts?.eventFilter);
|
||||
}, [timeline, opts?.eventFilter]);
|
||||
useEffect(() => {
|
||||
if (opts?.cursor !== undefined) {
|
||||
loader.setCursor(opts.cursor);
|
||||
timeline.setCursor(opts.cursor);
|
||||
}
|
||||
}, [opts?.cursor]);
|
||||
}, [timeline, opts?.cursor]);
|
||||
|
||||
const enabled = opts?.enabled ?? true;
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
loader.setQuery(query);
|
||||
loader.open();
|
||||
} else loader.close();
|
||||
}, [enabled]);
|
||||
timeline.setQuery(query);
|
||||
timeline.open();
|
||||
} else timeline.close();
|
||||
}, [timeline, enabled]);
|
||||
|
||||
useUnmount(() => {
|
||||
loader.close();
|
||||
timeline.close();
|
||||
});
|
||||
|
||||
return loader;
|
||||
return timeline;
|
||||
}
|
||||
|
32
src/services/timeline-cache.ts
Normal file
32
src/services/timeline-cache.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { TimelineLoader } from "../classes/timeline-loader";
|
||||
|
||||
const MAX_CACHE = 4;
|
||||
|
||||
class TimelineCacheService {
|
||||
private timelines = new Map<string, TimelineLoader>();
|
||||
private cacheQueue: string[] = [];
|
||||
|
||||
createTimeline(key: string) {
|
||||
let timeline = this.timelines.get(key);
|
||||
if (!timeline) {
|
||||
timeline = new TimelineLoader(key);
|
||||
this.timelines.set(key, timeline);
|
||||
}
|
||||
|
||||
this.cacheQueue = this.cacheQueue.filter((p) => p !== key).concat(key);
|
||||
while (this.cacheQueue.length > MAX_CACHE) {
|
||||
this.cacheQueue.shift();
|
||||
}
|
||||
|
||||
return timeline;
|
||||
}
|
||||
}
|
||||
|
||||
const timelineCacheService = new TimelineCacheService();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
//@ts-ignore
|
||||
window.timelineCacheService = timelineCacheService;
|
||||
}
|
||||
|
||||
export default timelineCacheService;
|
@ -1,33 +0,0 @@
|
||||
import UserTimeline from "../classes/user-timeline";
|
||||
|
||||
const MAX_CACHE = 4;
|
||||
|
||||
class UserTimelineService {
|
||||
timelines = new Map<string, UserTimeline>();
|
||||
|
||||
cacheQueue: string[] = [];
|
||||
|
||||
getTimeline(pubkey: string) {
|
||||
let timeline = this.timelines.get(pubkey);
|
||||
if (!timeline) {
|
||||
timeline = new UserTimeline(pubkey);
|
||||
this.timelines.set(pubkey, timeline);
|
||||
}
|
||||
|
||||
this.cacheQueue = this.cacheQueue.filter((p) => p !== pubkey).concat(pubkey);
|
||||
while (this.cacheQueue.length > MAX_CACHE) {
|
||||
this.cacheQueue.shift();
|
||||
}
|
||||
|
||||
return timeline;
|
||||
}
|
||||
}
|
||||
|
||||
const userTimelineService = new UserTimelineService();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
//@ts-ignore
|
||||
window.userTimelineService = userTimelineService;
|
||||
}
|
||||
|
||||
export default userTimelineService;
|
@ -33,9 +33,8 @@ export default function GlobalTab() {
|
||||
},
|
||||
[showReplies]
|
||||
);
|
||||
|
||||
const timeline = useTimelineLoader(
|
||||
[`global`, ...selectedRelay].join(","),
|
||||
[`global`, selectedRelay].join(","),
|
||||
selectedRelay ? [selectedRelay] : [],
|
||||
{ kinds: [1] },
|
||||
{ eventFilter }
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import React, { useCallback, useMemo, useRef } from "react";
|
||||
import { Box, Flex, Grid, IconButton } from "@chakra-ui/react";
|
||||
import { useNavigate, useOutletContext } from "react-router-dom";
|
||||
import { useMount, useUnmount } from "react-use";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import { matchImageUrls } from "../../helpers/regexp";
|
||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
@ -9,11 +8,12 @@ import { ImageGalleryLink, ImageGalleryProvider } from "../../components/image-g
|
||||
import { ExternalLinkIcon } from "../../components/icons";
|
||||
import { getSharableNoteId } from "../../helpers/nip19";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import userTimelineService from "../../services/user-timeline";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import TimelineActionAndStatus from "../../components/timeline-action-and-status";
|
||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { truncatedId } from "../../helpers/nostr-event";
|
||||
|
||||
type ImagePreview = { eventId: string; src: string; index: number };
|
||||
const matchAllImages = new RegExp(matchImageUrls, "ig");
|
||||
@ -49,22 +49,19 @@ const UserMediaTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
|
||||
const timeline = useMemo(() => userTimelineService.getTimeline(pubkey), [pubkey]);
|
||||
|
||||
const eventFilter = useCallback((e: NostrEvent) => e.kind === 1 && !!e.content.match(matchAllImages), []);
|
||||
useEffect(() => {
|
||||
timeline.setFilter(eventFilter);
|
||||
}, [timeline, eventFilter]);
|
||||
const timeline = useTimelineLoader(
|
||||
truncatedId(pubkey) + "-notes",
|
||||
contextRelays,
|
||||
{
|
||||
authors: [pubkey],
|
||||
kinds: [1, 6],
|
||||
},
|
||||
{ eventFilter }
|
||||
);
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
|
||||
useEffect(() => {
|
||||
timeline.setRelays(contextRelays);
|
||||
}, [timeline, contextRelays.join("|")]);
|
||||
|
||||
useMount(() => timeline.open());
|
||||
useUnmount(() => timeline.close());
|
||||
|
||||
const images = useMemo(() => {
|
||||
var images: { eventId: string; src: string; index: number }[] = [];
|
||||
|
||||
|
@ -1,20 +1,15 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Note } from "../../components/note";
|
||||
import RepostNote from "../../components/repost-note";
|
||||
import { isReply, isRepost } from "../../helpers/nostr-event";
|
||||
import { isReply, isRepost, truncatedId } from "../../helpers/nostr-event";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import userTimelineService from "../../services/user-timeline";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useMount, useUnmount } from "react-use";
|
||||
import { RelayIconStack } from "../../components/relay-icon-stack";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import TimelineActionAndStatus from "../../components/timeline-action-and-status";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import { TimelineLoader } from "../../classes/timeline-loader";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import GenericNoteTimeline from "../../components/generric-note-timeline";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
|
||||
const UserNotesTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@ -23,9 +18,6 @@ const UserNotesTab = () => {
|
||||
const { isOpen: showReplies, onToggle: toggleReplies } = useDisclosure();
|
||||
const { isOpen: hideReposts, onToggle: toggleReposts } = useDisclosure();
|
||||
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const timeline = useMemo(() => userTimelineService.getTimeline(pubkey), [pubkey]);
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (!showReplies && isReply(event)) return false;
|
||||
@ -34,16 +26,17 @@ const UserNotesTab = () => {
|
||||
},
|
||||
[showReplies, hideReposts]
|
||||
);
|
||||
useEffect(() => {
|
||||
timeline.setFilter(eventFilter);
|
||||
}, [timeline, eventFilter]);
|
||||
useEffect(() => {
|
||||
timeline.setRelays(readRelays);
|
||||
}, [timeline, readRelays.join("|")]);
|
||||
|
||||
useMount(() => timeline.open());
|
||||
useUnmount(() => timeline.close());
|
||||
const timeline = useTimelineLoader(
|
||||
truncatedId(pubkey) + "-notes",
|
||||
readRelays,
|
||||
{
|
||||
authors: [pubkey],
|
||||
kinds: [1, 6],
|
||||
},
|
||||
{ eventFilter }
|
||||
);
|
||||
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
|
Loading…
x
Reference in New Issue
Block a user