fix bugs with timeline scroll restoration

This commit is contained in:
hzrd149 2023-12-16 15:40:20 -06:00
parent 7632bcac63
commit 0396b8d2ad
2 changed files with 64 additions and 40 deletions

View File

@ -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 { Kind } from "nostr-tools";
import dayjs from "dayjs";
@ -22,6 +22,8 @@ import {
} from "../../../providers/intersection-observer";
import BadgeAwardCard from "../../../views/badges/components/badge-award-card";
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 }) {
const ref = useRef<HTMLDivElement | null>(null);
@ -66,11 +68,13 @@ const NOTE_BUFFER = 5;
const timelineNoteMinHeightCache = new WeakMap<TimelineLoader, Record<string, Record<string, number>>>();
function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
const notesArray = useSubject(timeline.timeline);
const events = useThrottle(useSubject(timeline.timeline), 100);
const [latest, setLatest] = useState(() => dayjs().unix());
const { subject } = useIntersectionObserver();
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(
(id: string, value: number) => {
let timelineData = timelineNoteMinHeightCache.get(timeline);
@ -78,96 +82,113 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
timelineData = {};
timelineNoteMinHeightCache.set(timeline, timelineData);
}
if (!timelineData[location.key]) timelineData[location.key] = {};
timelineData[location.key][id] = value;
if (!timelineData[cachedLocationKey]) timelineData[cachedLocationKey] = {};
timelineData[cachedLocationKey][id] = value;
},
[location.key, timeline],
[cachedLocationKey, timeline],
);
const getCachedNumber = useCallback(
(id: string) => {
const timelineData = timelineNoteMinHeightCache.get(timeline);
if (!timelineData) return undefined;
return timelineData[location.key]?.[id] ?? undefined;
return timelineData[cachedLocationKey]?.[id] ?? undefined;
},
[location.key, timeline],
);
const [maxDate, setMaxDate] = useState(getCachedNumber("max") ?? -Infinity);
const [minDate, setMinDate] = useState(
getCachedNumber("min") ?? timeline.timeline.value[NOTE_BUFFER]?.created_at ?? 0,
[cachedLocationKey, timeline],
);
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
useEffect(() => {
setLatest(dayjs().unix());
setMaxDate(getCachedNumber("max") ?? -Infinity);
setMaxDate(getCachedNumber("max") ?? Infinity);
setMinDate(getCachedNumber("min") ?? timeline.timeline.value[NOTE_BUFFER]?.created_at ?? 0);
}, [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(
(id: string, element: Element) => {
const rect = element.getBoundingClientRect();
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],
);
// 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(() => {
const listener = (entities: ExtendedIntersectionObserverEntry[]) => {
for (const entity of entities) {
if (entity.id) {
intersectionEntryCache.set(entity.id, entity.entry);
intersectionEntryCache.set(entity.id, entity.entry.isIntersecting);
updateNoteMinHeight(entity.id, entity.entry.target);
}
}
let min: number = Infinity;
let max: number = -Infinity;
let min: number = Infinity;
let preload = NOTE_BUFFER;
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) {
const isIntersecting = intersectionEntryCache.get(getEventUID(event));
if (!isIntersecting) {
if (foundVisible) {
// found and event below the view
// found an event below the view
if (preload-- < 0) break;
if (event.created_at < min) min = event.created_at;
} else {
// found and event above the view
// found an event above the view
continue;
}
} else {
// found visible event
foundVisible = true;
const bufferEvent =
timeline.timeline.value[Math.max(timeline.timeline.value.indexOf(event) - NOTE_BUFFER)] || event;
if (bufferEvent.created_at > max) max = bufferEvent.created_at;
const bufferEvent = timeline.timeline.value[timeline.timeline.value.indexOf(event) - NOTE_BUFFER];
if (bufferEvent && bufferEvent.created_at > max) max = bufferEvent.created_at;
}
}
setMinDate((v) => Math.min(v, min));
setMaxDate(max);
setCachedNumber("max", max);
setCachedNumber("min", Math.min(getCachedNumber("min") ?? Infinity, min));
if (min !== Infinity) {
setMinDate((v) => {
const value = Math.min(v, min);
setCachedNumber("min", value);
return value;
});
}
if (max !== -Infinity) {
setMaxDate(max);
setCachedNumber("max", max);
} else if (foundVisible) {
setMaxDate(Infinity);
setCachedNumber("max", Infinity);
}
};
subject.subscribe(listener);
intersectionSubject.subscribe(listener);
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 (
<>

View File

@ -36,7 +36,10 @@ export function useRegisterIntersectionEntity(ref: MutableRefObject<Element | nu
useEffect(() => {
if (observer && 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]);
useUnmount(() => {