web/timeline: review suggestions

This commit is contained in:
Jade Ellis 2025-01-02 15:33:17 +00:00
parent 15e92411d3
commit 1e339d1fbd
No known key found for this signature in database
GPG key ID: 8705A2A3EBF77BD2
3 changed files with 54 additions and 28 deletions

View file

@ -77,7 +77,9 @@ const EventSendStatus = ({ evt }: { evt: MemDBEvent }) => {
} }
} }
const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused, virtualIndex, ref }: TimelineEventProps) => { const TimelineEvent = ({
evt, prevEvt, disableMenu, smallReplies, isFocused, virtualIndex, ref,
}: TimelineEventProps) => {
const roomCtx = useRoomContext() const roomCtx = useRoomContext()
const client = use(ClientContext)! const client = use(ClientContext)!
const mainScreen = use(MainScreenContext) const mainScreen = use(MainScreenContext)

View file

@ -16,5 +16,15 @@ div.timeline-view {
> div.timeline-list { > div.timeline-list {
padding-bottom: 2rem; padding-bottom: 2rem;
width: 100%;
position: relative;
> div.timeline-virtual-items {
position: absolute;
top: 0;
left: 0;
width: 100%
}
} }
} }

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { Virtualizer, useVirtualizer } from "@tanstack/react-virtual" import { Virtualizer, useVirtualizer } from "@tanstack/react-virtual"
import { use, useEffect, useLayoutEffect, useRef, useState } from "react" import { use, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
import { ScaleLoader } from "react-spinners" import { ScaleLoader } from "react-spinners"
import { usePreference, useRoomTimeline } from "@/api/statestore" import { usePreference, useRoomTimeline } from "@/api/statestore"
import { EventRowID, MemDBEvent } from "@/api/types" import { EventRowID, MemDBEvent } from "@/api/types"
@ -25,7 +25,11 @@ import TimelineEvent from "./TimelineEvent.tsx"
import { getBodyType, isSmallEvent } from "./content/index.ts" import { getBodyType, isSmallEvent } from "./content/index.ts"
import "./TimelineView.css" import "./TimelineView.css"
const measureElement = (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<HTMLDivElement, Element>) => { // This is necessary to take into account margin, which the default measurement
// (using getBoundingClientRect) doesn't by default
const measureElement = (
element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<HTMLDivElement, Element>,
) => {
const horizontal = instance.options.horizontal const horizontal = instance.options.horizontal
const style = window.getComputedStyle(element) const style = window.getComputedStyle(element)
if (entry == null ? void 0 : entry.borderBoxSize) { if (entry == null ? void 0 : entry.borderBoxSize) {
@ -34,15 +38,23 @@ const measureElement = (element: Element, entry: ResizeObserverEntry | undefined
const size = Math.round( const size = Math.round(
box[horizontal ? "inlineSize" : "blockSize"], box[horizontal ? "inlineSize" : "blockSize"],
) )
return size + parseFloat(style[horizontal ? "marginInlineStart" : "marginBlockStart"]) + parseFloat(style[horizontal ? "marginInlineEnd" : "marginBlockEnd"]) return size
+ parseFloat(style[horizontal ? "marginInlineStart" : "marginBlockStart"])
+ parseFloat(style[horizontal ? "marginInlineEnd" : "marginBlockEnd"])
} }
} }
return Math.round( return Math.round(
element.getBoundingClientRect()[instance.options.horizontal ? "width" : "height"] + parseFloat(style[horizontal ? "marginLeft" : "marginTop"]) + parseFloat(style[horizontal ? "marginRight" : "marginBottom"]), element.getBoundingClientRect()[instance.options.horizontal ? "width" : "height"]
+ parseFloat(style[horizontal ? "marginLeft" : "marginTop"])
+ parseFloat(style[horizontal ? "marginRight" : "marginBottom"]),
) )
} }
const estimateEventHeight = (event: MemDBEvent) => isSmallEvent(getBodyType(event)) ? (event?.reactions ? 26 : 0) + (event?.content.body ? (event?.local_content?.big_emoji ? 92 : 44) : 0) + (event?.content.info?.h || 0) : 26 const estimateEventHeight = (event: MemDBEvent) => isSmallEvent(getBodyType(event)) ?
(event.reactions ? 26 : 0)
+ (event.content.body ? (event.local_content?.big_emoji ? 92 : 44) : 0)
+ (event.content.info?.h || 0)
: 26
const TimelineView = () => { const TimelineView = () => {
const roomCtx = useRoomContext() const roomCtx = useRoomContext()
@ -51,20 +63,6 @@ const TimelineView = () => {
const client = use(ClientContext)! const client = use(ClientContext)!
const [isLoadingHistory, setLoadingHistory] = useState(false) const [isLoadingHistory, setLoadingHistory] = useState(false)
const [focusedEventRowID, directSetFocusedEventRowID] = useState<EventRowID | null>(null) const [focusedEventRowID, directSetFocusedEventRowID] = useState<EventRowID | null>(null)
const loadHistory = () => {
setLoadingHistory(true)
client.loadMoreHistory(room.roomID)
.catch(err => console.error("Failed to load history", err))
.then((loadedEventCount) => {
// Prevent scroll getting stuck loading more history
if (loadedEventCount && timelineViewRef.current && timelineViewRef.current.scrollTop <= virtualListOffsetRef.current) {
virtualizer.scrollToIndex(loadedEventCount, { align: "end" })
}
})
.finally(() => {
setLoadingHistory(false)
})
}
const bottomRef = roomCtx.timelineBottomRef const bottomRef = roomCtx.timelineBottomRef
const timelineViewRef = useRef<HTMLDivElement>(null) const timelineViewRef = useRef<HTMLDivElement>(null)
const focused = useFocus() const focused = useFocus()
@ -72,12 +70,6 @@ const TimelineView = () => {
const virtualListRef = useRef<HTMLDivElement>(null) const virtualListRef = useRef<HTMLDivElement>(null)
const virtualListOffsetRef = useRef(0)
useLayoutEffect(() => {
virtualListOffsetRef.current = virtualListRef.current?.offsetTop ?? 0
}, [])
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
count: timeline.length, count: timeline.length,
getScrollElement: () => timelineViewRef.current, getScrollElement: () => timelineViewRef.current,
@ -89,12 +81,32 @@ const TimelineView = () => {
const items = virtualizer.getVirtualItems() const items = virtualizer.getVirtualItems()
const loadHistory = useCallback(() => {
setLoadingHistory(true)
client.loadMoreHistory(room.roomID)
.catch(err => console.error("Failed to load history", err))
.then((loadedEventCount) => {
// Prevent scroll getting stuck loading more history
if (loadedEventCount &&
timelineViewRef.current &&
timelineViewRef.current.scrollTop <= (virtualListRef.current?.offsetTop ?? 0)) {
// FIXME: This seems to run before the events are measured,
// resulting in a jump in the timeline of the difference in
// height when scrolling very fast
virtualizer.scrollToIndex(loadedEventCount, { align: "end" })
}
})
.finally(() => {
setLoadingHistory(false)
})
}, [client, room, virtualizer])
useLayoutEffect(() => { useLayoutEffect(() => {
if (roomCtx.scrolledToBottom) { if (roomCtx.scrolledToBottom) {
// timelineViewRef.current && (timelineViewRef.current.scrollTop = timelineViewRef.current.scrollHeight) // timelineViewRef.current && (timelineViewRef.current.scrollTop = timelineViewRef.current.scrollHeight)
bottomRef.current?.scrollIntoView() bottomRef.current?.scrollIntoView()
} }
}, [roomCtx, timeline, virtualizer.getTotalSize()]) }, [roomCtx, timeline, virtualizer.getTotalSize(), bottomRef])
// When the user scrolls the timeline manually, remember if they were at the bottom, // When the user scrolls the timeline manually, remember if they were at the bottom,
// so that we can keep them at the bottom when new events are added. // so that we can keep them at the bottom when new events are added.
@ -147,7 +159,7 @@ const TimelineView = () => {
return return
} }
// Load more history when the virtualiser loads the last item // Load more history when the virtualizer loads the last item
if (firstItem.index == 0) { if (firstItem.index == 0) {
console.log("Loading more history...") console.log("Loading more history...")
loadHistory() loadHistory()
@ -156,6 +168,8 @@ const TimelineView = () => {
}, [ }, [
room.hasMoreHistory, loadHistory, room.hasMoreHistory, loadHistory,
virtualizer.getVirtualItems(), virtualizer.getVirtualItems(),
room.paginating,
virtualizer,
]) ])
return <div className="timeline-view" onScroll={handleScroll} ref={timelineViewRef}> return <div className="timeline-view" onScroll={handleScroll} ref={timelineViewRef}>