mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +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"];
|
||||
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"]} />
|
||||
|
@ -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} />
|
||||
))}
|
||||
|
@ -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} />
|
||||
)}
|
||||
|
@ -52,7 +52,7 @@ export default function TimelinePage({
|
||||
}
|
||||
};
|
||||
return (
|
||||
<IntersectionObserverProvider<string> callback={callback}>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex direction="column" gap="2" {...props}>
|
||||
{header}
|
||||
{renderTimeline()}
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
|
@ -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 };
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user