Skip to content

Commit 1e339d1

Browse files
committed
web/timeline: review suggestions
1 parent 15e9241 commit 1e339d1

File tree

3 files changed

+54
-28
lines changed

3 files changed

+54
-28
lines changed

web/src/ui/timeline/TimelineEvent.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ const EventSendStatus = ({ evt }: { evt: MemDBEvent }) => {
7777
}
7878
}
7979

80-
const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused, virtualIndex, ref }: TimelineEventProps) => {
80+
const TimelineEvent = ({
81+
evt, prevEvt, disableMenu, smallReplies, isFocused, virtualIndex, ref,
82+
}: TimelineEventProps) => {
8183
const roomCtx = useRoomContext()
8284
const client = use(ClientContext)!
8385
const mainScreen = use(MainScreenContext)

web/src/ui/timeline/TimelineView.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,15 @@ div.timeline-view {
1616

1717
> div.timeline-list {
1818
padding-bottom: 2rem;
19+
20+
width: 100%;
21+
position: relative;
22+
23+
> div.timeline-virtual-items {
24+
position: absolute;
25+
top: 0;
26+
left: 0;
27+
width: 100%
28+
}
1929
}
2030
}

web/src/ui/timeline/TimelineView.tsx

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1616
import { Virtualizer, useVirtualizer } from "@tanstack/react-virtual"
17-
import { use, useEffect, useLayoutEffect, useRef, useState } from "react"
17+
import { use, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
1818
import { ScaleLoader } from "react-spinners"
1919
import { usePreference, useRoomTimeline } from "@/api/statestore"
2020
import { EventRowID, MemDBEvent } from "@/api/types"
@@ -25,7 +25,11 @@ import TimelineEvent from "./TimelineEvent.tsx"
2525
import { getBodyType, isSmallEvent } from "./content/index.ts"
2626
import "./TimelineView.css"
2727

28-
const measureElement = (element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<HTMLDivElement, Element>) => {
28+
// This is necessary to take into account margin, which the default measurement
29+
// (using getBoundingClientRect) doesn't by default
30+
const measureElement = (
31+
element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<HTMLDivElement, Element>,
32+
) => {
2933
const horizontal = instance.options.horizontal
3034
const style = window.getComputedStyle(element)
3135
if (entry == null ? void 0 : entry.borderBoxSize) {
@@ -34,15 +38,23 @@ const measureElement = (element: Element, entry: ResizeObserverEntry | undefined
3438
const size = Math.round(
3539
box[horizontal ? "inlineSize" : "blockSize"],
3640
)
37-
return size + parseFloat(style[horizontal ? "marginInlineStart" : "marginBlockStart"]) + parseFloat(style[horizontal ? "marginInlineEnd" : "marginBlockEnd"])
41+
return size
42+
+ parseFloat(style[horizontal ? "marginInlineStart" : "marginBlockStart"])
43+
+ parseFloat(style[horizontal ? "marginInlineEnd" : "marginBlockEnd"])
3844
}
3945
}
4046
return Math.round(
41-
element.getBoundingClientRect()[instance.options.horizontal ? "width" : "height"] + parseFloat(style[horizontal ? "marginLeft" : "marginTop"]) + parseFloat(style[horizontal ? "marginRight" : "marginBottom"]),
47+
element.getBoundingClientRect()[instance.options.horizontal ? "width" : "height"]
48+
+ parseFloat(style[horizontal ? "marginLeft" : "marginTop"])
49+
+ parseFloat(style[horizontal ? "marginRight" : "marginBottom"]),
4250
)
4351
}
4452

45-
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
53+
const estimateEventHeight = (event: MemDBEvent) => isSmallEvent(getBodyType(event)) ?
54+
(event.reactions ? 26 : 0)
55+
+ (event.content.body ? (event.local_content?.big_emoji ? 92 : 44) : 0)
56+
+ (event.content.info?.h || 0)
57+
: 26
4658

4759
const TimelineView = () => {
4860
const roomCtx = useRoomContext()
@@ -51,33 +63,13 @@ const TimelineView = () => {
5163
const client = use(ClientContext)!
5264
const [isLoadingHistory, setLoadingHistory] = useState(false)
5365
const [focusedEventRowID, directSetFocusedEventRowID] = useState<EventRowID | null>(null)
54-
const loadHistory = () => {
55-
setLoadingHistory(true)
56-
client.loadMoreHistory(room.roomID)
57-
.catch(err => console.error("Failed to load history", err))
58-
.then((loadedEventCount) => {
59-
// Prevent scroll getting stuck loading more history
60-
if (loadedEventCount && timelineViewRef.current && timelineViewRef.current.scrollTop <= virtualListOffsetRef.current) {
61-
virtualizer.scrollToIndex(loadedEventCount, { align: "end" })
62-
}
63-
})
64-
.finally(() => {
65-
setLoadingHistory(false)
66-
})
67-
}
6866
const bottomRef = roomCtx.timelineBottomRef
6967
const timelineViewRef = useRef<HTMLDivElement>(null)
7068
const focused = useFocus()
7169
const smallReplies = usePreference(client.store, room, "small_replies")
7270

7371
const virtualListRef = useRef<HTMLDivElement>(null)
7472

75-
const virtualListOffsetRef = useRef(0)
76-
77-
useLayoutEffect(() => {
78-
virtualListOffsetRef.current = virtualListRef.current?.offsetTop ?? 0
79-
}, [])
80-
8173
const virtualizer = useVirtualizer({
8274
count: timeline.length,
8375
getScrollElement: () => timelineViewRef.current,
@@ -89,12 +81,32 @@ const TimelineView = () => {
8981

9082
const items = virtualizer.getVirtualItems()
9183

84+
const loadHistory = useCallback(() => {
85+
setLoadingHistory(true)
86+
client.loadMoreHistory(room.roomID)
87+
.catch(err => console.error("Failed to load history", err))
88+
.then((loadedEventCount) => {
89+
// Prevent scroll getting stuck loading more history
90+
if (loadedEventCount &&
91+
timelineViewRef.current &&
92+
timelineViewRef.current.scrollTop <= (virtualListRef.current?.offsetTop ?? 0)) {
93+
// FIXME: This seems to run before the events are measured,
94+
// resulting in a jump in the timeline of the difference in
95+
// height when scrolling very fast
96+
virtualizer.scrollToIndex(loadedEventCount, { align: "end" })
97+
}
98+
})
99+
.finally(() => {
100+
setLoadingHistory(false)
101+
})
102+
}, [client, room, virtualizer])
103+
92104
useLayoutEffect(() => {
93105
if (roomCtx.scrolledToBottom) {
94106
// timelineViewRef.current && (timelineViewRef.current.scrollTop = timelineViewRef.current.scrollHeight)
95107
bottomRef.current?.scrollIntoView()
96108
}
97-
}, [roomCtx, timeline, virtualizer.getTotalSize()])
109+
}, [roomCtx, timeline, virtualizer.getTotalSize(), bottomRef])
98110

99111
// When the user scrolls the timeline manually, remember if they were at the bottom,
100112
// so that we can keep them at the bottom when new events are added.
@@ -147,7 +159,7 @@ const TimelineView = () => {
147159
return
148160
}
149161

150-
// Load more history when the virtualiser loads the last item
162+
// Load more history when the virtualizer loads the last item
151163
if (firstItem.index == 0) {
152164
console.log("Loading more history...")
153165
loadHistory()
@@ -156,6 +168,8 @@ const TimelineView = () => {
156168
}, [
157169
room.hasMoreHistory, loadHistory,
158170
virtualizer.getVirtualItems(),
171+
room.paginating,
172+
virtualizer,
159173
])
160174

161175
return <div className="timeline-view" onScroll={handleScroll} ref={timelineViewRef}>

0 commit comments

Comments
 (0)