14
14
// You should have received a copy of the GNU Affero General Public License
15
15
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
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"
18
18
import { ScaleLoader } from "react-spinners"
19
19
import { usePreference , useRoomTimeline } from "@/api/statestore"
20
20
import { EventRowID , MemDBEvent } from "@/api/types"
@@ -25,7 +25,11 @@ import TimelineEvent from "./TimelineEvent.tsx"
25
25
import { getBodyType , isSmallEvent } from "./content/index.ts"
26
26
import "./TimelineView.css"
27
27
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
+ ) => {
29
33
const horizontal = instance . options . horizontal
30
34
const style = window . getComputedStyle ( element )
31
35
if ( entry == null ? void 0 : entry . borderBoxSize ) {
@@ -34,15 +38,23 @@ const measureElement = (element: Element, entry: ResizeObserverEntry | undefined
34
38
const size = Math . round (
35
39
box [ horizontal ? "inlineSize" : "blockSize" ] ,
36
40
)
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" ] )
38
44
}
39
45
}
40
46
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" ] ) ,
42
50
)
43
51
}
44
52
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
46
58
47
59
const TimelineView = ( ) => {
48
60
const roomCtx = useRoomContext ( )
@@ -51,33 +63,13 @@ const TimelineView = () => {
51
63
const client = use ( ClientContext ) !
52
64
const [ isLoadingHistory , setLoadingHistory ] = useState ( false )
53
65
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
- }
68
66
const bottomRef = roomCtx . timelineBottomRef
69
67
const timelineViewRef = useRef < HTMLDivElement > ( null )
70
68
const focused = useFocus ( )
71
69
const smallReplies = usePreference ( client . store , room , "small_replies" )
72
70
73
71
const virtualListRef = useRef < HTMLDivElement > ( null )
74
72
75
- const virtualListOffsetRef = useRef ( 0 )
76
-
77
- useLayoutEffect ( ( ) => {
78
- virtualListOffsetRef . current = virtualListRef . current ?. offsetTop ?? 0
79
- } , [ ] )
80
-
81
73
const virtualizer = useVirtualizer ( {
82
74
count : timeline . length ,
83
75
getScrollElement : ( ) => timelineViewRef . current ,
@@ -89,12 +81,32 @@ const TimelineView = () => {
89
81
90
82
const items = virtualizer . getVirtualItems ( )
91
83
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
+
92
104
useLayoutEffect ( ( ) => {
93
105
if ( roomCtx . scrolledToBottom ) {
94
106
// timelineViewRef.current && (timelineViewRef.current.scrollTop = timelineViewRef.current.scrollHeight)
95
107
bottomRef . current ?. scrollIntoView ( )
96
108
}
97
- } , [ roomCtx , timeline , virtualizer . getTotalSize ( ) ] )
109
+ } , [ roomCtx , timeline , virtualizer . getTotalSize ( ) , bottomRef ] )
98
110
99
111
// When the user scrolls the timeline manually, remember if they were at the bottom,
100
112
// so that we can keep them at the bottom when new events are added.
@@ -147,7 +159,7 @@ const TimelineView = () => {
147
159
return
148
160
}
149
161
150
- // Load more history when the virtualiser loads the last item
162
+ // Load more history when the virtualizer loads the last item
151
163
if ( firstItem . index == 0 ) {
152
164
console . log ( "Loading more history..." )
153
165
loadHistory ( )
@@ -156,6 +168,8 @@ const TimelineView = () => {
156
168
} , [
157
169
room . hasMoreHistory , loadHistory ,
158
170
virtualizer . getVirtualItems ( ) ,
171
+ room . paginating ,
172
+ virtualizer ,
159
173
] )
160
174
161
175
return < div className = "timeline-view" onScroll = { handleScroll } ref = { timelineViewRef } >
0 commit comments