From 20fb8fb94e8032e2f99456d11af091abd4b9230f Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Sun, 24 Sep 2023 11:19:37 -0500 Subject: [PATCH] Fix freezing when navigating back to main timeline --- .changeset/curly-fans-poke.md | 5 ++ src/components/note/index.tsx | 17 ++++- .../generic-note-timeline/index.tsx | 70 +++++++++++++++++-- .../generic-note-timeline/repost-note.tsx | 3 +- src/components/timeline-page/index.tsx | 2 +- ...e-timeline-cursor-intersection-callback.ts | 2 +- src/providers/intersection-observer.tsx | 64 +++++++++-------- src/views/home/index.tsx | 2 +- src/views/tools/network-mute-graph.tsx | 2 +- 9 files changed, 128 insertions(+), 39 deletions(-) create mode 100644 .changeset/curly-fans-poke.md diff --git a/.changeset/curly-fans-poke.md b/.changeset/curly-fans-poke.md new file mode 100644 index 000000000..f3e50a8db --- /dev/null +++ b/.changeset/curly-fans-poke.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Fix issue with freezing when navigating back to main timeline diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index 5b1b1d1aa..c7e2d40c2 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -46,9 +46,17 @@ export type NoteProps = Omit & { 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 ( - + diff --git a/src/components/timeline-page/generic-note-timeline/index.tsx b/src/components/timeline-page/generic-note-timeline/index.tsx index a4b0ad443..c2b805c1e 100644 --- a/src/components/timeline-page/generic-note-timeline/index.tsx +++ b/src/components/timeline-page/generic-note-timeline/index.tsx @@ -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()); + 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 && ( + + + + )} {notes.map((note) => ( ))} 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 ebcb66686..3a7367d4a 100644 --- a/src/components/timeline-page/generic-note-timeline/repost-note.tsx +++ b/src/components/timeline-page/generic-note-timeline/repost-note.tsx @@ -57,7 +57,8 @@ export default function RepostNote({ event }: { event: NostrEvent }) { {!note ? ( ) : note.kind === Kind.Text ? ( - + // 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 + ) : ( )} diff --git a/src/components/timeline-page/index.tsx b/src/components/timeline-page/index.tsx index 6546deb6d..fb6fc950b 100644 --- a/src/components/timeline-page/index.tsx +++ b/src/components/timeline-page/index.tsx @@ -52,7 +52,7 @@ export default function TimelinePage({ } }; return ( - callback={callback}> + {header} {renderTimeline()} diff --git a/src/hooks/use-timeline-cursor-intersection-callback.ts b/src/hooks/use-timeline-cursor-intersection-callback.ts index 3000df21d..230005017 100644 --- a/src/hooks/use-timeline-cursor-intersection-callback.ts +++ b/src/hooks/use-timeline-cursor-intersection-callback.ts @@ -10,7 +10,7 @@ export function useTimelineCurserIntersectionCallback(timeline: TimelineLoader) timeline.loadNextBlocks(); }, 1000); - return useIntersectionMapCallback( + return useIntersectionMapCallback( (map) => { // find oldest event that is visible let oldestEvent: NostrEvent | undefined = undefined; diff --git a/src/providers/intersection-observer.tsx b/src/providers/intersection-observer.tsx index 33ca877f3..0f674688f 100644 --- a/src/providers/intersection-observer.tsx +++ b/src/providers/intersection-observer.tsx @@ -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 = { entry: IntersectionObserverEntry; id: T | undefined }; -export type ExtendedIntersectionObserverCallback = ( - entries: ExtendedIntersectionObserverEntry[], - observer: IntersectionObserver, -) => void; + // NOTE: hard codded string type + subject: Subject; +}>({ setElementId: () => {}, subject: new Subject() }); export function useIntersectionObserver() { return useContext(IntersectionObserverContext); } -export function useRegisterIntersectionEntity(ref: MutableRefObject, id?: T) { +export function useRegisterIntersectionEntity(ref: MutableRefObject, id?: string) { const { observer, setElementId } = useIntersectionObserver(); useEffect(() => { @@ -40,24 +44,22 @@ export function useRegisterIntersectionEntity(ref: MutableRefObject( - callback: (map: Map) => void, +/** @deprecated */ +export function useIntersectionMapCallback( + callback: (map: Map) => void, watch: DependencyList, ) { - const map = useMemo(() => new Map(), []); - return useCallback>( + const map = useMemo(() => new Map(), []); + return useCallback( (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({ +export default function IntersectionObserverProvider({ children, root, rootMargin, @@ -67,18 +69,23 @@ export default function IntersectionObserverProvider({ root?: MutableRefObject; rootMargin?: IntersectionObserverInit["rootMargin"]; threshold?: IntersectionObserverInit["threshold"]; - callback: ExtendedIntersectionObserverCallback; + callback: ExtendedIntersectionObserverCallback; }) { - const elementIds = useMemo(() => new WeakMap(), []); + const elementIds = useMemo(() => new WeakMap(), []); + const [subject] = useState(() => new Subject([], false)); - const handleIntersection = useCallback((entries, observer) => { - callback( - entries.map((entry) => { + const handleIntersection = useCallback( + (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( () => new IntersectionObserver(handleIntersection, { rootMargin, threshold }), ); @@ -94,7 +101,7 @@ export default function IntersectionObserverProvider({ }); const setElementId = useCallback( - (element: Element, id: T) => { + (element: Element, id: string) => { elementIds.set(element, id); }, [elementIds], @@ -104,8 +111,9 @@ export default function IntersectionObserverProvider({ () => ({ observer, setElementId, + subject, }), - [observer, setElementId], + [observer, setElementId, subject], ); return {children}; diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx index 4169d9e05..34b4dad7f 100644 --- a/src/views/home/index.tsx +++ b/src/views/home/index.tsx @@ -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(() => { if (filter === undefined) return { kinds }; return { ...filter, kinds }; diff --git a/src/views/tools/network-mute-graph.tsx b/src/views/tools/network-mute-graph.tsx index 0cf75a584..5b33ac7af 100644 --- a/src/views/tools/network-mute-graph.tsx +++ b/src/views/tools/network-mute-graph.tsx @@ -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();