mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-18 16:41:20 +02:00
Fix freezing when navigating back to main timeline
This commit is contained in:
parent
6276c4739e
commit
20fb8fb94e
5
.changeset/curly-fans-poke.md
Normal file
5
.changeset/curly-fans-poke.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix issue with freezing when navigating back to main timeline
|
@ -46,9 +46,17 @@ export type NoteProps = Omit<CardProps, "children"> & {
|
|||||||
variant?: CardProps["variant"];
|
variant?: CardProps["variant"];
|
||||||
showReplyButton?: boolean;
|
showReplyButton?: boolean;
|
||||||
hideDrawerButton?: boolean;
|
hideDrawerButton?: boolean;
|
||||||
|
registerIntersectionEntity?: boolean;
|
||||||
};
|
};
|
||||||
export const Note = React.memo(
|
export const Note = React.memo(
|
||||||
({ event, variant = "outline", showReplyButton, hideDrawerButton, ...props }: NoteProps) => {
|
({
|
||||||
|
event,
|
||||||
|
variant = "outline",
|
||||||
|
showReplyButton,
|
||||||
|
hideDrawerButton,
|
||||||
|
registerIntersectionEntity = true,
|
||||||
|
...props
|
||||||
|
}: NoteProps) => {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const { showReactions, showSignatureVerification } = useSubject(appSettings);
|
const { showReactions, showSignatureVerification } = useSubject(appSettings);
|
||||||
const replyForm = useDisclosure();
|
const replyForm = useDisclosure();
|
||||||
@ -67,7 +75,12 @@ export const Note = React.memo(
|
|||||||
return (
|
return (
|
||||||
<TrustProvider event={event}>
|
<TrustProvider event={event}>
|
||||||
<ExpandProvider>
|
<ExpandProvider>
|
||||||
<Card variant={variant} ref={ref} data-event-id={event.id} {...props}>
|
<Card
|
||||||
|
variant={variant}
|
||||||
|
ref={registerIntersectionEntity ? ref : undefined}
|
||||||
|
data-event-id={event.id}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<CardHeader padding="2">
|
<CardHeader padding="2">
|
||||||
<Flex flex="1" gap="2" alignItems="center">
|
<Flex flex="1" gap="2" alignItems="center">
|
||||||
<UserAvatarLink pubkey={event.pubkey} size={["xs", "sm"]} />
|
<UserAvatarLink pubkey={event.pubkey} size={["xs", "sm"]} />
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ReactNode, memo } from "react";
|
import { ReactNode, memo, useEffect, useState } from "react";
|
||||||
import { Text } from "@chakra-ui/react";
|
import { Box, Button, Text } from "@chakra-ui/react";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import useSubject from "../../../hooks/use-subject";
|
import useSubject from "../../../hooks/use-subject";
|
||||||
import { TimelineLoader } from "../../../classes/timeline-loader";
|
import { TimelineLoader } from "../../../classes/timeline-loader";
|
||||||
@ -11,9 +12,10 @@ import { STREAM_KIND } from "../../../helpers/nostr/stream";
|
|||||||
import StreamNote from "./stream-note";
|
import StreamNote from "./stream-note";
|
||||||
import { ErrorBoundary } from "../../error-boundary";
|
import { ErrorBoundary } from "../../error-boundary";
|
||||||
import EmbeddedArticle from "../../embed-event/event-types/embedded-article";
|
import EmbeddedArticle from "../../embed-event/event-types/embedded-article";
|
||||||
import { isReply } from "../../../helpers/nostr/events";
|
import { getEventUID, isReply } from "../../../helpers/nostr/events";
|
||||||
import ReplyNote from "./reply-note";
|
import ReplyNote from "./reply-note";
|
||||||
import RelayRecommendation from "./relay-recommendation";
|
import RelayRecommendation from "./relay-recommendation";
|
||||||
|
import { ExtendedIntersectionObserverEntry, useIntersectionObserver } from "../../../providers/intersection-observer";
|
||||||
|
|
||||||
function RenderEvent({ event }: { event: NostrEvent }) {
|
function RenderEvent({ event }: { event: NostrEvent }) {
|
||||||
let content: ReactNode | null = null;
|
let content: ReactNode | null = null;
|
||||||
@ -42,11 +44,71 @@ function RenderEvent({ event }: { event: NostrEvent }) {
|
|||||||
}
|
}
|
||||||
const RenderEventMemo = memo(RenderEvent);
|
const RenderEventMemo = memo(RenderEvent);
|
||||||
|
|
||||||
|
const PRELOAD_NOTES = 5;
|
||||||
function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
|
function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
|
||||||
const notes = useSubject(timeline.timeline);
|
const notesArray = useSubject(timeline.timeline);
|
||||||
|
const [latest, setLatest] = useState(() => dayjs().unix());
|
||||||
|
const { subject } = useIntersectionObserver();
|
||||||
|
|
||||||
|
const [minDate, setMinDate] = useState(timeline.timeline.value[PRELOAD_NOTES]?.created_at ?? 0);
|
||||||
|
|
||||||
|
const newNotes: NostrEvent[] = [];
|
||||||
|
const notes: NostrEvent[] = [];
|
||||||
|
for (const note of notesArray) {
|
||||||
|
if (note.created_at > latest) newNotes.push(note);
|
||||||
|
else if (note.created_at > minDate) notes.push(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [intersectionEntryCache] = useState(() => new Map<string, IntersectionObserverEntry>());
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (entities: ExtendedIntersectionObserverEntry[]) => {
|
||||||
|
for (const entity of entities) entity.id && intersectionEntryCache.set(entity.id, entity.entry);
|
||||||
|
|
||||||
|
let min: number = Infinity;
|
||||||
|
let preload = PRELOAD_NOTES;
|
||||||
|
let foundVisible = false;
|
||||||
|
for (const event of timeline.timeline.value) {
|
||||||
|
if (event.created_at > latest) continue;
|
||||||
|
const entry = intersectionEntryCache.get(getEventUID(event));
|
||||||
|
if (!entry || !entry.isIntersecting) {
|
||||||
|
if (foundVisible) {
|
||||||
|
// found and event below the view
|
||||||
|
if (preload-- < 0) break;
|
||||||
|
if (event.created_at < min) min = event.created_at;
|
||||||
|
} else {
|
||||||
|
// found and event above the view
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// found visible event
|
||||||
|
foundVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMinDate((v) => Math.min(v, min));
|
||||||
|
};
|
||||||
|
|
||||||
|
subject.subscribe(listener);
|
||||||
|
return () => {
|
||||||
|
subject.unsubscribe(listener);
|
||||||
|
};
|
||||||
|
}, [setMinDate, intersectionEntryCache, latest]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{newNotes.length > 0 && (
|
||||||
|
<Box h="0" overflow="visible" w="full" zIndex={100} display="flex" position="relative">
|
||||||
|
<Button
|
||||||
|
onClick={() => setLatest(timeline.timeline.value[0].created_at + 10)}
|
||||||
|
colorScheme="brand"
|
||||||
|
size="lg"
|
||||||
|
mx="auto"
|
||||||
|
w={["50%", null, "30%"]}
|
||||||
|
>
|
||||||
|
Show {newNotes.length} new notes
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{notes.map((note) => (
|
{notes.map((note) => (
|
||||||
<RenderEventMemo key={note.id} event={note} />
|
<RenderEventMemo key={note.id} event={note} />
|
||||||
))}
|
))}
|
||||||
|
@ -57,7 +57,8 @@ export default function RepostNote({ event }: { event: NostrEvent }) {
|
|||||||
{!note ? (
|
{!note ? (
|
||||||
<SkeletonText />
|
<SkeletonText />
|
||||||
) : note.kind === Kind.Text ? (
|
) : note.kind === Kind.Text ? (
|
||||||
<Note event={note} showReplyButton />
|
// NOTE: tell the note not to register itself with the intersection observer. since this is an older note it will break the order of the timeline
|
||||||
|
<Note event={note} showReplyButton registerIntersectionEntity={false} />
|
||||||
) : (
|
) : (
|
||||||
<EmbedEvent event={note} />
|
<EmbedEvent event={note} />
|
||||||
)}
|
)}
|
||||||
|
@ -52,7 +52,7 @@ export default function TimelinePage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider<string> callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<Flex direction="column" gap="2" {...props}>
|
<Flex direction="column" gap="2" {...props}>
|
||||||
{header}
|
{header}
|
||||||
{renderTimeline()}
|
{renderTimeline()}
|
||||||
|
@ -10,7 +10,7 @@ export function useTimelineCurserIntersectionCallback(timeline: TimelineLoader)
|
|||||||
timeline.loadNextBlocks();
|
timeline.loadNextBlocks();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return useIntersectionMapCallback<string>(
|
return useIntersectionMapCallback(
|
||||||
(map) => {
|
(map) => {
|
||||||
// find oldest event that is visible
|
// find oldest event that is visible
|
||||||
let oldestEvent: NostrEvent | undefined = undefined;
|
let oldestEvent: NostrEvent | undefined = undefined;
|
||||||
|
@ -11,22 +11,26 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useMount, useUnmount } from "react-use";
|
import { useMount, useUnmount } from "react-use";
|
||||||
|
|
||||||
|
import Subject from "../classes/subject";
|
||||||
|
|
||||||
|
export type ExtendedIntersectionObserverEntry = { entry: IntersectionObserverEntry; id: string | undefined };
|
||||||
|
export type ExtendedIntersectionObserverCallback = (
|
||||||
|
entries: ExtendedIntersectionObserverEntry[],
|
||||||
|
observer: IntersectionObserver,
|
||||||
|
) => void;
|
||||||
|
|
||||||
const IntersectionObserverContext = createContext<{
|
const IntersectionObserverContext = createContext<{
|
||||||
observer?: IntersectionObserver;
|
observer?: IntersectionObserver;
|
||||||
setElementId: (element: Element, id: any) => void;
|
setElementId: (element: Element, id: any) => void;
|
||||||
}>({ setElementId: () => {} });
|
// NOTE: hard codded string type
|
||||||
|
subject: Subject<ExtendedIntersectionObserverEntry[]>;
|
||||||
export type ExtendedIntersectionObserverEntry<T> = { entry: IntersectionObserverEntry; id: T | undefined };
|
}>({ setElementId: () => {}, subject: new Subject() });
|
||||||
export type ExtendedIntersectionObserverCallback<T> = (
|
|
||||||
entries: ExtendedIntersectionObserverEntry<T>[],
|
|
||||||
observer: IntersectionObserver,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
export function useIntersectionObserver() {
|
export function useIntersectionObserver() {
|
||||||
return useContext(IntersectionObserverContext);
|
return useContext(IntersectionObserverContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRegisterIntersectionEntity<T>(ref: MutableRefObject<Element | null>, id?: T) {
|
export function useRegisterIntersectionEntity(ref: MutableRefObject<Element | null>, id?: string) {
|
||||||
const { observer, setElementId } = useIntersectionObserver();
|
const { observer, setElementId } = useIntersectionObserver();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -40,24 +44,22 @@ export function useRegisterIntersectionEntity<T>(ref: MutableRefObject<Element |
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIntersectionMapCallback<T>(
|
/** @deprecated */
|
||||||
callback: (map: Map<T, IntersectionObserverEntry>) => void,
|
export function useIntersectionMapCallback(
|
||||||
|
callback: (map: Map<string, IntersectionObserverEntry>) => void,
|
||||||
watch: DependencyList,
|
watch: DependencyList,
|
||||||
) {
|
) {
|
||||||
const map = useMemo(() => new Map<T, IntersectionObserverEntry>(), []);
|
const map = useMemo(() => new Map<string, IntersectionObserverEntry>(), []);
|
||||||
return useCallback<ExtendedIntersectionObserverCallback<T>>(
|
return useCallback<ExtendedIntersectionObserverCallback>(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
for (const { id, entry } of entries) {
|
for (const { id, entry } of entries) id && map.set(id, entry);
|
||||||
if (id) map.set(id, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(map);
|
callback(map);
|
||||||
},
|
},
|
||||||
[callback, ...watch],
|
[callback, ...watch],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IntersectionObserverProvider<T = undefined>({
|
export default function IntersectionObserverProvider({
|
||||||
children,
|
children,
|
||||||
root,
|
root,
|
||||||
rootMargin,
|
rootMargin,
|
||||||
@ -67,18 +69,23 @@ export default function IntersectionObserverProvider<T = undefined>({
|
|||||||
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;
|
||||||
}) {
|
}) {
|
||||||
const elementIds = useMemo(() => new WeakMap<Element, T>(), []);
|
const elementIds = useMemo(() => new WeakMap<Element, string>(), []);
|
||||||
|
const [subject] = useState(() => new Subject<ExtendedIntersectionObserverEntry[]>([], false));
|
||||||
|
|
||||||
const handleIntersection = useCallback<IntersectionObserverCallback>((entries, observer) => {
|
const handleIntersection = useCallback<IntersectionObserverCallback>(
|
||||||
callback(
|
(entries, observer) => {
|
||||||
entries.map((entry) => {
|
const extendedEntries = entries.map((entry) => {
|
||||||
return { entry, id: elementIds.get(entry.target) };
|
return { entry, id: elementIds.get(entry.target) };
|
||||||
}),
|
});
|
||||||
observer,
|
callback(extendedEntries, observer);
|
||||||
);
|
|
||||||
}, []);
|
subject.next(extendedEntries);
|
||||||
|
},
|
||||||
|
[subject],
|
||||||
|
);
|
||||||
|
|
||||||
const [observer, setObserver] = useState<IntersectionObserver>(
|
const [observer, setObserver] = useState<IntersectionObserver>(
|
||||||
() => new IntersectionObserver(handleIntersection, { rootMargin, threshold }),
|
() => new IntersectionObserver(handleIntersection, { rootMargin, threshold }),
|
||||||
);
|
);
|
||||||
@ -94,7 +101,7 @@ export default function IntersectionObserverProvider<T = undefined>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const setElementId = useCallback(
|
const setElementId = useCallback(
|
||||||
(element: Element, id: T) => {
|
(element: Element, id: string) => {
|
||||||
elementIds.set(element, id);
|
elementIds.set(element, id);
|
||||||
},
|
},
|
||||||
[elementIds],
|
[elementIds],
|
||||||
@ -104,8 +111,9 @@ export default function IntersectionObserverProvider<T = undefined>({
|
|||||||
() => ({
|
() => ({
|
||||||
observer,
|
observer,
|
||||||
setElementId,
|
setElementId,
|
||||||
|
subject,
|
||||||
}),
|
}),
|
||||||
[observer, setElementId],
|
[observer, setElementId, subject],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <IntersectionObserverContext.Provider value={context}>{children}</IntersectionObserverContext.Provider>;
|
return <IntersectionObserverContext.Provider value={context}>{children}</IntersectionObserverContext.Provider>;
|
||||||
|
@ -30,7 +30,7 @@ function HomePage() {
|
|||||||
const { relays } = useRelaySelectionContext();
|
const { relays } = useRelaySelectionContext();
|
||||||
const { listId, filter } = usePeopleListContext();
|
const { listId, filter } = usePeopleListContext();
|
||||||
|
|
||||||
const kinds = [Kind.Text, Kind.Repost, Kind.Article, 2];
|
const kinds = [Kind.Text, Kind.Repost, Kind.Article, Kind.RecommendRelay];
|
||||||
const query = useMemo<NostrRequestFilter>(() => {
|
const query = useMemo<NostrRequestFilter>(() => {
|
||||||
if (filter === undefined) return { kinds };
|
if (filter === undefined) return { kinds };
|
||||||
return { ...filter, kinds };
|
return { ...filter, kinds };
|
||||||
|
@ -95,7 +95,7 @@ function NetworkGraphPage() {
|
|||||||
linkCurvature={0.25}
|
linkCurvature={0.25}
|
||||||
nodeThreeObject={(node: NodeType) => {
|
nodeThreeObject={(node: NodeType) => {
|
||||||
if (!node.image) {
|
if (!node.image) {
|
||||||
return new Mesh(new SphereGeometry(10, 12, 6), new MeshBasicMaterial({ color: 0xaa0f0f }));
|
return new Mesh(new SphereGeometry(5, 12, 6), new MeshBasicMaterial({ color: 0xaa0f0f }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = new Group();
|
const group = new Group();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user