mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-19 12:00:32 +02:00
fix bugs with timeline scroll restoration
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode, memo, useCallback, useEffect, useRef, useState } from "react";
|
import { ReactNode, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Box, Button, 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 dayjs from "dayjs";
|
||||||
@@ -22,6 +22,8 @@ import {
|
|||||||
} from "../../../providers/intersection-observer";
|
} from "../../../providers/intersection-observer";
|
||||||
import BadgeAwardCard from "../../../views/badges/components/badge-award-card";
|
import BadgeAwardCard from "../../../views/badges/components/badge-award-card";
|
||||||
import ArticleNote from "./article-note";
|
import ArticleNote from "./article-note";
|
||||||
|
import SuperMap from "../../../classes/super-map";
|
||||||
|
import { useDebounce, useThrottle } from "react-use";
|
||||||
|
|
||||||
function RenderEvent({ event, visible, minHeight }: { event: NostrEvent; visible: boolean; minHeight?: number }) {
|
function RenderEvent({ event, visible, minHeight }: { event: NostrEvent; visible: boolean; minHeight?: number }) {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -66,11 +68,13 @@ const NOTE_BUFFER = 5;
|
|||||||
const timelineNoteMinHeightCache = new WeakMap<TimelineLoader, Record<string, Record<string, number>>>();
|
const timelineNoteMinHeightCache = new WeakMap<TimelineLoader, Record<string, Record<string, number>>>();
|
||||||
|
|
||||||
function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
|
function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
|
||||||
const notesArray = useSubject(timeline.timeline);
|
const events = useThrottle(useSubject(timeline.timeline), 100);
|
||||||
const [latest, setLatest] = useState(() => dayjs().unix());
|
const [latest, setLatest] = useState(() => dayjs().unix());
|
||||||
const { subject } = useIntersectionObserver();
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
// only update the location key when the timeline changes
|
||||||
|
// this fixes an issue with the key changes when the drawer opens
|
||||||
|
const cachedLocationKey = useMemo(() => location.key, [timeline]);
|
||||||
const setCachedNumber = useCallback(
|
const setCachedNumber = useCallback(
|
||||||
(id: string, value: number) => {
|
(id: string, value: number) => {
|
||||||
let timelineData = timelineNoteMinHeightCache.get(timeline);
|
let timelineData = timelineNoteMinHeightCache.get(timeline);
|
||||||
@@ -78,96 +82,113 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
|
|||||||
timelineData = {};
|
timelineData = {};
|
||||||
timelineNoteMinHeightCache.set(timeline, timelineData);
|
timelineNoteMinHeightCache.set(timeline, timelineData);
|
||||||
}
|
}
|
||||||
if (!timelineData[location.key]) timelineData[location.key] = {};
|
if (!timelineData[cachedLocationKey]) timelineData[cachedLocationKey] = {};
|
||||||
timelineData[location.key][id] = value;
|
timelineData[cachedLocationKey][id] = value;
|
||||||
},
|
},
|
||||||
[location.key, timeline],
|
[cachedLocationKey, timeline],
|
||||||
);
|
);
|
||||||
const getCachedNumber = useCallback(
|
const getCachedNumber = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const timelineData = timelineNoteMinHeightCache.get(timeline);
|
const timelineData = timelineNoteMinHeightCache.get(timeline);
|
||||||
if (!timelineData) return undefined;
|
if (!timelineData) return undefined;
|
||||||
return timelineData[location.key]?.[id] ?? undefined;
|
return timelineData[cachedLocationKey]?.[id] ?? undefined;
|
||||||
},
|
},
|
||||||
[location.key, timeline],
|
[cachedLocationKey, timeline],
|
||||||
);
|
|
||||||
const [maxDate, setMaxDate] = useState(getCachedNumber("max") ?? -Infinity);
|
|
||||||
const [minDate, setMinDate] = useState(
|
|
||||||
getCachedNumber("min") ?? timeline.timeline.value[NOTE_BUFFER]?.created_at ?? 0,
|
|
||||||
);
|
);
|
||||||
|
const [maxDate, setMaxDate] = useState(getCachedNumber("max") ?? Infinity);
|
||||||
|
const [minDate, setMinDate] = useState(getCachedNumber("min") ?? events[NOTE_BUFFER]?.created_at ?? -Infinity);
|
||||||
|
|
||||||
// reset the latest and minDate when timeline changes
|
// reset the latest and minDate when timeline changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLatest(dayjs().unix());
|
setLatest(dayjs().unix());
|
||||||
setMaxDate(getCachedNumber("max") ?? -Infinity);
|
setMaxDate(getCachedNumber("max") ?? Infinity);
|
||||||
setMinDate(getCachedNumber("min") ?? timeline.timeline.value[NOTE_BUFFER]?.created_at ?? 0);
|
setMinDate(getCachedNumber("min") ?? timeline.timeline.value[NOTE_BUFFER]?.created_at ?? 0);
|
||||||
}, [timeline, setMinDate, setLatest, getCachedNumber]);
|
}, [timeline, setMinDate, setLatest, getCachedNumber]);
|
||||||
|
|
||||||
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 updateNoteMinHeight = useCallback(
|
const updateNoteMinHeight = useCallback(
|
||||||
(id: string, element: Element) => {
|
(id: string, element: Element) => {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const current = getCachedNumber(id);
|
const current = getCachedNumber(id);
|
||||||
setCachedNumber(id, Math.max(current ?? 0, rect.height));
|
if (rect.height !== current) setCachedNumber(id, Math.max(current ?? 0, rect.height));
|
||||||
},
|
},
|
||||||
[setCachedNumber, getCachedNumber],
|
[setCachedNumber, getCachedNumber],
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: break this out into its own component or hook, this is pretty ugly
|
// TODO: break this out into its own component or hook, this is pretty ugly
|
||||||
const [intersectionEntryCache] = useState(() => new Map<string, IntersectionObserverEntry>());
|
const { subject: intersectionSubject } = useIntersectionObserver();
|
||||||
|
const [intersectionEntryCache] = useState(() => new Map<string, boolean>());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (entities: ExtendedIntersectionObserverEntry[]) => {
|
const listener = (entities: ExtendedIntersectionObserverEntry[]) => {
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (entity.id) {
|
if (entity.id) {
|
||||||
intersectionEntryCache.set(entity.id, entity.entry);
|
intersectionEntryCache.set(entity.id, entity.entry.isIntersecting);
|
||||||
updateNoteMinHeight(entity.id, entity.entry.target);
|
updateNoteMinHeight(entity.id, entity.entry.target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let min: number = Infinity;
|
|
||||||
let max: number = -Infinity;
|
let max: number = -Infinity;
|
||||||
|
let min: number = Infinity;
|
||||||
let preload = NOTE_BUFFER;
|
let preload = NOTE_BUFFER;
|
||||||
let foundVisible = false;
|
let foundVisible = false;
|
||||||
for (const event of timeline.timeline.value) {
|
for (const event of timeline.timeline.value) {
|
||||||
if (event.created_at > latest) continue;
|
if (event.created_at > latest) continue;
|
||||||
const entry = intersectionEntryCache.get(getEventUID(event));
|
const isIntersecting = intersectionEntryCache.get(getEventUID(event));
|
||||||
if (!entry || !entry.isIntersecting) {
|
|
||||||
|
if (!isIntersecting) {
|
||||||
if (foundVisible) {
|
if (foundVisible) {
|
||||||
// found and event below the view
|
// found an event below the view
|
||||||
if (preload-- < 0) break;
|
if (preload-- < 0) break;
|
||||||
if (event.created_at < min) min = event.created_at;
|
if (event.created_at < min) min = event.created_at;
|
||||||
} else {
|
} else {
|
||||||
// found and event above the view
|
// found an event above the view
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// found visible event
|
// found visible event
|
||||||
foundVisible = true;
|
foundVisible = true;
|
||||||
|
|
||||||
const bufferEvent =
|
const bufferEvent = timeline.timeline.value[timeline.timeline.value.indexOf(event) - NOTE_BUFFER];
|
||||||
timeline.timeline.value[Math.max(timeline.timeline.value.indexOf(event) - NOTE_BUFFER)] || event;
|
if (bufferEvent && bufferEvent.created_at > max) max = bufferEvent.created_at;
|
||||||
if (bufferEvent.created_at > max) max = bufferEvent.created_at;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setMinDate((v) => Math.min(v, min));
|
if (min !== Infinity) {
|
||||||
|
setMinDate((v) => {
|
||||||
|
const value = Math.min(v, min);
|
||||||
|
setCachedNumber("min", value);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (max !== -Infinity) {
|
||||||
setMaxDate(max);
|
setMaxDate(max);
|
||||||
|
|
||||||
setCachedNumber("max", max);
|
setCachedNumber("max", max);
|
||||||
setCachedNumber("min", Math.min(getCachedNumber("min") ?? Infinity, min));
|
} else if (foundVisible) {
|
||||||
|
setMaxDate(Infinity);
|
||||||
|
setCachedNumber("max", Infinity);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
subject.subscribe(listener);
|
intersectionSubject.subscribe(listener);
|
||||||
return () => {
|
return () => {
|
||||||
subject.unsubscribe(listener);
|
intersectionSubject.unsubscribe(listener);
|
||||||
};
|
};
|
||||||
}, [setMinDate, intersectionEntryCache, updateNoteMinHeight, setCachedNumber, getCachedNumber, latest, timeline]);
|
}, [
|
||||||
|
setMinDate,
|
||||||
|
setMaxDate,
|
||||||
|
intersectionSubject,
|
||||||
|
intersectionEntryCache,
|
||||||
|
updateNoteMinHeight,
|
||||||
|
setCachedNumber,
|
||||||
|
getCachedNumber,
|
||||||
|
latest,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const newNotes: NostrEvent[] = [];
|
||||||
|
const notes: NostrEvent[] = [];
|
||||||
|
for (const note of events) {
|
||||||
|
if (note.created_at > latest) newNotes.push(note);
|
||||||
|
else if (note.created_at >= minDate) notes.push(note);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@@ -36,7 +36,10 @@ export function useRegisterIntersectionEntity(ref: MutableRefObject<Element | nu
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (observer && ref.current) {
|
if (observer && ref.current) {
|
||||||
observer.observe(ref.current);
|
observer.observe(ref.current);
|
||||||
if (id) setElementId(ref.current, id);
|
if (id) {
|
||||||
|
setElementId(ref.current, id);
|
||||||
|
if (import.meta.env.DEV) ref.current.setAttribute("data-event-id", id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [observer]);
|
}, [observer]);
|
||||||
useUnmount(() => {
|
useUnmount(() => {
|
||||||
|
Reference in New Issue
Block a user