Fix freezing when navigating back to main timeline

This commit is contained in:
hzrd149 2023-09-24 11:19:37 -05:00
parent 6276c4739e
commit 20fb8fb94e
9 changed files with 128 additions and 39 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix issue with freezing when navigating back to main timeline

View File

@ -46,9 +46,17 @@ export type NoteProps = Omit<CardProps, "children"> & {
variant?: CardProps["variant"];
showReplyButton?: boolean;
hideDrawerButton?: boolean;
registerIntersectionEntity?: boolean;
};
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 { showReactions, showSignatureVerification } = useSubject(appSettings);
const replyForm = useDisclosure();
@ -67,7 +75,12 @@ export const Note = React.memo(
return (
<TrustProvider event={event}>
<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">
<Flex flex="1" gap="2" alignItems="center">
<UserAvatarLink pubkey={event.pubkey} size={["xs", "sm"]} />

View File

@ -1,6 +1,7 @@
import { ReactNode, memo } from "react";
import { Text } from "@chakra-ui/react";
import { ReactNode, memo, useEffect, useState } from "react";
import { Box, Button, Text } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import dayjs from "dayjs";
import useSubject from "../../../hooks/use-subject";
import { TimelineLoader } from "../../../classes/timeline-loader";
@ -11,9 +12,10 @@ import { STREAM_KIND } from "../../../helpers/nostr/stream";
import StreamNote from "./stream-note";
import { ErrorBoundary } from "../../error-boundary";
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 RelayRecommendation from "./relay-recommendation";
import { ExtendedIntersectionObserverEntry, useIntersectionObserver } from "../../../providers/intersection-observer";
function RenderEvent({ event }: { event: NostrEvent }) {
let content: ReactNode | null = null;
@ -42,11 +44,71 @@ function RenderEvent({ event }: { event: NostrEvent }) {
}
const RenderEventMemo = memo(RenderEvent);
const PRELOAD_NOTES = 5;
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 (
<>
{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) => (
<RenderEventMemo key={note.id} event={note} />
))}

View File

@ -57,7 +57,8 @@ export default function RepostNote({ event }: { event: NostrEvent }) {
{!note ? (
<SkeletonText />
) : 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} />
)}

View File

@ -52,7 +52,7 @@ export default function TimelinePage({
}
};
return (
<IntersectionObserverProvider<string> callback={callback}>
<IntersectionObserverProvider callback={callback}>
<Flex direction="column" gap="2" {...props}>
{header}
{renderTimeline()}

View File

@ -10,7 +10,7 @@ export function useTimelineCurserIntersectionCallback(timeline: TimelineLoader)
timeline.loadNextBlocks();
}, 1000);
return useIntersectionMapCallback<string>(
return useIntersectionMapCallback(
(map) => {
// find oldest event that is visible
let oldestEvent: NostrEvent | undefined = undefined;

View File

@ -11,22 +11,26 @@ import {
} from "react";
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<{
observer?: IntersectionObserver;
setElementId: (element: Element, id: any) => void;
}>({ setElementId: () => {} });
export type ExtendedIntersectionObserverEntry<T> = { entry: IntersectionObserverEntry; id: T | undefined };
export type ExtendedIntersectionObserverCallback<T> = (
entries: ExtendedIntersectionObserverEntry<T>[],
observer: IntersectionObserver,
) => void;
// NOTE: hard codded string type
subject: Subject<ExtendedIntersectionObserverEntry[]>;
}>({ setElementId: () => {}, subject: new Subject() });
export function useIntersectionObserver() {
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();
useEffect(() => {
@ -40,24 +44,22 @@ export function useRegisterIntersectionEntity<T>(ref: MutableRefObject<Element |
});
}
export function useIntersectionMapCallback<T>(
callback: (map: Map<T, IntersectionObserverEntry>) => void,
/** @deprecated */
export function useIntersectionMapCallback(
callback: (map: Map<string, IntersectionObserverEntry>) => void,
watch: DependencyList,
) {
const map = useMemo(() => new Map<T, IntersectionObserverEntry>(), []);
return useCallback<ExtendedIntersectionObserverCallback<T>>(
const map = useMemo(() => new Map<string, IntersectionObserverEntry>(), []);
return useCallback<ExtendedIntersectionObserverCallback>(
(entries) => {
for (const { id, entry } of entries) {
if (id) map.set(id, entry);
}
for (const { id, entry } of entries) id && map.set(id, entry);
callback(map);
},
[callback, ...watch],
);
}
export default function IntersectionObserverProvider<T = undefined>({
export default function IntersectionObserverProvider({
children,
root,
rootMargin,
@ -67,18 +69,23 @@ export default function IntersectionObserverProvider<T = undefined>({
root?: MutableRefObject<HTMLElement | null>;
rootMargin?: IntersectionObserverInit["rootMargin"];
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) => {
callback(
entries.map((entry) => {
const handleIntersection = useCallback<IntersectionObserverCallback>(
(entries, observer) => {
const extendedEntries = entries.map((entry) => {
return { entry, id: elementIds.get(entry.target) };
}),
observer,
);
}, []);
});
callback(extendedEntries, observer);
subject.next(extendedEntries);
},
[subject],
);
const [observer, setObserver] = useState<IntersectionObserver>(
() => new IntersectionObserver(handleIntersection, { rootMargin, threshold }),
);
@ -94,7 +101,7 @@ export default function IntersectionObserverProvider<T = undefined>({
});
const setElementId = useCallback(
(element: Element, id: T) => {
(element: Element, id: string) => {
elementIds.set(element, id);
},
[elementIds],
@ -104,8 +111,9 @@ export default function IntersectionObserverProvider<T = undefined>({
() => ({
observer,
setElementId,
subject,
}),
[observer, setElementId],
[observer, setElementId, subject],
);
return <IntersectionObserverContext.Provider value={context}>{children}</IntersectionObserverContext.Provider>;

View File

@ -30,7 +30,7 @@ function HomePage() {
const { relays } = useRelaySelectionContext();
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>(() => {
if (filter === undefined) return { kinds };
return { ...filter, kinds };

View File

@ -95,7 +95,7 @@ function NetworkGraphPage() {
linkCurvature={0.25}
nodeThreeObject={(node: NodeType) => {
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();