@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
14
14
limitations under the License.
15
15
*/
16
16
17
- import React , { ReactNode , useContext , useMemo , useState } from "react" ;
17
+ import React , { ReactNode , useContext , useMemo , useRef , useState } from "react" ;
18
18
import classNames from "classnames" ;
19
19
import { Room } from "matrix-js-sdk/src/models/room" ;
20
20
import { sleep } from "matrix-js-sdk/src/utils" ;
21
21
import { EventType } from "matrix-js-sdk/src/@types/event" ;
22
22
import { logger } from "matrix-js-sdk/src/logger" ;
23
23
24
- import { _t } from '../../../languageHandler' ;
24
+ import { _t , _td } from '../../../languageHandler' ;
25
25
import BaseDialog from "./BaseDialog" ;
26
26
import Dropdown from "../elements/Dropdown" ;
27
27
import SearchBox from "../../structures/SearchBox" ;
@@ -38,9 +38,12 @@ import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/Rece
38
38
import ProgressBar from "../elements/ProgressBar" ;
39
39
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar" ;
40
40
import QueryMatcher from "../../../autocomplete/QueryMatcher" ;
41
- import TruncatedList from "../elements/TruncatedList" ;
42
- import EntityTile from "../rooms/EntityTile" ;
43
- import BaseAvatar from "../avatars/BaseAvatar" ;
41
+ import LazyRenderList from "../elements/LazyRenderList" ;
42
+
43
+ // These values match CSS
44
+ const ROW_HEIGHT = 32 + 12 ;
45
+ const HEADER_HEIGHT = 15 ;
46
+ const GROUP_MARGIN = 24 ;
44
47
45
48
interface IProps {
46
49
space : Room ;
@@ -64,31 +67,56 @@ export const Entry = ({ room, checked, onChange }) => {
64
67
</ label > ;
65
68
} ;
66
69
70
+ type OnChangeFn = ( checked : boolean , room : Room ) => void ;
71
+
72
+ type Renderer = (
73
+ rooms : Room [ ] ,
74
+ selectedToAdd : Set < Room > ,
75
+ scrollState : IScrollState ,
76
+ onChange : undefined | OnChangeFn ,
77
+ ) => ReactNode ;
78
+
67
79
interface IAddExistingToSpaceProps {
68
80
space : Room ;
69
81
footerPrompt ?: ReactNode ;
70
82
filterPlaceholder : string ;
71
83
emptySelectionButton ?: ReactNode ;
72
84
onFinished ( added : boolean ) : void ;
73
- roomsRenderer ?(
74
- rooms : Room [ ] ,
75
- selectedToAdd : Set < Room > ,
76
- onChange : undefined | ( ( checked : boolean , room : Room ) => void ) ,
77
- truncateAt : number ,
78
- overflowTile : ( overflowCount : number , totalCount : number ) => JSX . Element ,
79
- ) : ReactNode ;
80
- spacesRenderer ?(
81
- spaces : Room [ ] ,
82
- selectedToAdd : Set < Room > ,
83
- onChange ?: ( checked : boolean , room : Room ) => void ,
84
- ) : ReactNode ;
85
- dmsRenderer ?(
86
- dms : Room [ ] ,
87
- selectedToAdd : Set < Room > ,
88
- onChange ?: ( checked : boolean , room : Room ) => void ,
89
- ) : ReactNode ;
85
+ roomsRenderer ?: Renderer ;
86
+ spacesRenderer ?: Renderer ;
87
+ dmsRenderer ?: Renderer ;
90
88
}
91
89
90
+ interface IScrollState {
91
+ scrollTop : number ;
92
+ height : number ;
93
+ }
94
+
95
+ const getScrollState = (
96
+ { scrollTop, height } : IScrollState ,
97
+ numItems : number ,
98
+ ...prevGroupSizes : number [ ]
99
+ ) : IScrollState => {
100
+ let heightBefore = 0 ;
101
+ prevGroupSizes . forEach ( size => {
102
+ heightBefore += GROUP_MARGIN + HEADER_HEIGHT + ( size * ROW_HEIGHT ) ;
103
+ } ) ;
104
+
105
+ const viewportTop = scrollTop ;
106
+ const viewportBottom = viewportTop + height ;
107
+ const listTop = heightBefore + HEADER_HEIGHT ;
108
+ const listBottom = listTop + ( numItems * ROW_HEIGHT ) ;
109
+ const top = Math . max ( viewportTop , listTop ) ;
110
+ const bottom = Math . min ( viewportBottom , listBottom ) ;
111
+ // the viewport height and scrollTop passed to the LazyRenderList
112
+ // is capped at the intersection with the real viewport, so lists
113
+ // out of view are passed height 0, so they won't render any items.
114
+ return {
115
+ scrollTop : Math . max ( 0 , scrollTop - listTop ) ,
116
+ height : Math . max ( 0 , bottom - top ) ,
117
+ } ;
118
+ } ;
119
+
92
120
export const AddExistingToSpace : React . FC < IAddExistingToSpaceProps > = ( {
93
121
space,
94
122
footerPrompt,
@@ -102,6 +130,13 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
102
130
const cli = useContext ( MatrixClientContext ) ;
103
131
const visibleRooms = useMemo ( ( ) => cli . getVisibleRooms ( ) . filter ( r => r . getMyMembership ( ) === "join" ) , [ cli ] ) ;
104
132
133
+ const scrollRef = useRef < AutoHideScrollbar > ( ) ;
134
+ const [ scrollState , setScrollState ] = useState < IScrollState > ( {
135
+ // these are estimates which update as soon as it mounts
136
+ scrollTop : 0 ,
137
+ height : 600 ,
138
+ } ) ;
139
+
105
140
const [ selectedToAdd , setSelectedToAdd ] = useState ( new Set < Room > ( ) ) ;
106
141
const [ progress , setProgress ] = useState < number > ( null ) ;
107
142
const [ error , setError ] = useState < Error > ( null ) ;
@@ -229,49 +264,56 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
229
264
setSelectedToAdd ( new Set ( selectedToAdd ) ) ;
230
265
} : null ;
231
266
232
- const [ truncateAt , setTruncateAt ] = useState ( 20 ) ;
233
- function overflowTile ( overflowCount : number , totalCount : number ) : JSX . Element {
234
- const text = _t ( "and %(count)s others..." , { count : overflowCount } ) ;
235
- return (
236
- < EntityTile
237
- className = "mx_EntityTile_ellipsis"
238
- avatarJsx = {
239
- < BaseAvatar url = { require ( "../../../../res/img/ellipsis.svg" ) } name = "..." width = { 36 } height = { 36 } />
240
- }
241
- name = { text }
242
- presenceState = "online"
243
- suppressOnHover = { true }
244
- onClick = { ( ) => setTruncateAt ( totalCount ) }
245
- />
246
- ) ;
247
- }
267
+ // only count spaces when alone as they're shown on a separate modal all on their own
268
+ const numSpaces = ( spacesRenderer && ! dmsRenderer && ! roomsRenderer ) ? spaces . length : 0 ;
248
269
249
270
let noResults = true ;
250
- if ( ( roomsRenderer && rooms . length > 0 ) ||
251
- ( dmsRenderer && dms . length > 0 ) ||
252
- ( ! roomsRenderer && ! dmsRenderer && spacesRenderer && spaces . length > 0 ) // only count spaces when alone
253
- ) {
271
+ if ( ( roomsRenderer && rooms . length > 0 ) || ( dmsRenderer && dms . length > 0 ) || ( numSpaces > 0 ) ) {
254
272
noResults = false ;
255
273
}
256
274
275
+ const onScroll = ( ) => {
276
+ const body = scrollRef . current ?. containerRef . current ;
277
+ setScrollState ( {
278
+ scrollTop : body . scrollTop ,
279
+ height : body . clientHeight ,
280
+ } ) ;
281
+ } ;
282
+
283
+ const wrappedRef = ( body : HTMLDivElement ) => {
284
+ setScrollState ( {
285
+ scrollTop : body . scrollTop ,
286
+ height : body . clientHeight ,
287
+ } ) ;
288
+ } ;
289
+
290
+ const roomsScrollState = getScrollState ( scrollState , rooms . length ) ;
291
+ const spacesScrollState = getScrollState ( scrollState , numSpaces , rooms . length ) ;
292
+ const dmsScrollState = getScrollState ( scrollState , dms . length , numSpaces , rooms . length ) ;
293
+
257
294
return < div className = "mx_AddExistingToSpace" >
258
295
< SearchBox
259
296
className = "mx_textinput_icon mx_textinput_search"
260
297
placeholder = { filterPlaceholder }
261
298
onSearch = { setQuery }
262
299
autoFocus = { true }
263
300
/>
264
- < AutoHideScrollbar className = "mx_AddExistingToSpace_content" >
301
+ < AutoHideScrollbar
302
+ className = "mx_AddExistingToSpace_content"
303
+ onScroll = { onScroll }
304
+ wrappedRef = { wrappedRef }
305
+ ref = { scrollRef }
306
+ >
265
307
{ rooms . length > 0 && roomsRenderer ? (
266
- roomsRenderer ( rooms , selectedToAdd , onChange , truncateAt , overflowTile )
308
+ roomsRenderer ( rooms , selectedToAdd , roomsScrollState , onChange )
267
309
) : undefined }
268
310
269
311
{ spaces . length > 0 && spacesRenderer ? (
270
- spacesRenderer ( spaces , selectedToAdd , onChange )
312
+ spacesRenderer ( spaces , selectedToAdd , spacesScrollState , onChange )
271
313
) : null }
272
314
273
315
{ dms . length > 0 && dmsRenderer ? (
274
- dmsRenderer ( dms , selectedToAdd , onChange )
316
+ dmsRenderer ( dms , selectedToAdd , dmsScrollState , onChange )
275
317
) : null }
276
318
277
319
{ noResults ? < span className = "mx_AddExistingToSpace_noResults" >
@@ -285,59 +327,36 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
285
327
</ div > ;
286
328
} ;
287
329
288
- export const defaultRoomsRenderer : IAddExistingToSpaceProps [ "roomsRenderer" ] = (
289
- rooms , selectedToAdd , onChange , truncateAt , overflowTile ,
330
+ const defaultRendererFactory = ( title : string ) : Renderer => (
331
+ rooms ,
332
+ selectedToAdd ,
333
+ { scrollTop, height } ,
334
+ onChange ,
290
335
) => (
291
336
< div className = "mx_AddExistingToSpace_section" >
292
- < h3 > { _t ( "Rooms" ) } </ h3 >
293
- < TruncatedList
294
- truncateAt = { truncateAt }
295
- createOverflowElement = { overflowTile }
296
- getChildren = { ( start , end ) => rooms . slice ( start , end ) . map ( room =>
337
+ < h3 > { _t ( title ) } </ h3 >
338
+ < LazyRenderList
339
+ itemHeight = { ROW_HEIGHT }
340
+ items = { rooms }
341
+ scrollTop = { scrollTop }
342
+ height = { height }
343
+ renderItem = { room => (
297
344
< Entry
298
345
key = { room . roomId }
299
346
room = { room }
300
347
checked = { selectedToAdd . has ( room ) }
301
348
onChange = { onChange ? ( checked : boolean ) => {
302
349
onChange ( checked , room ) ;
303
350
} : null }
304
- /> ,
351
+ />
305
352
) }
306
- getChildCount = { ( ) => rooms . length }
307
353
/>
308
354
</ div >
309
355
) ;
310
356
311
- export const defaultSpacesRenderer : IAddExistingToSpaceProps [ "spacesRenderer" ] = ( spaces , selectedToAdd , onChange ) => (
312
- < div className = "mx_AddExistingToSpace_section" >
313
- { spaces . map ( space => {
314
- return < Entry
315
- key = { space . roomId }
316
- room = { space }
317
- checked = { selectedToAdd . has ( space ) }
318
- onChange = { onChange ? ( checked ) => {
319
- onChange ( checked , space ) ;
320
- } : null }
321
- /> ;
322
- } ) }
323
- </ div >
324
- ) ;
325
-
326
- export const defaultDmsRenderer : IAddExistingToSpaceProps [ "dmsRenderer" ] = ( dms , selectedToAdd , onChange ) => (
327
- < div className = "mx_AddExistingToSpace_section" >
328
- < h3 > { _t ( "Direct Messages" ) } </ h3 >
329
- { dms . map ( room => {
330
- return < Entry
331
- key = { room . roomId }
332
- room = { room }
333
- checked = { selectedToAdd . has ( room ) }
334
- onChange = { onChange ? ( checked : boolean ) => {
335
- onChange ( checked , room ) ;
336
- } : null }
337
- /> ;
338
- } ) }
339
- </ div >
340
- ) ;
357
+ export const defaultRoomsRenderer = defaultRendererFactory ( _td ( "Rooms" ) ) ;
358
+ export const defaultSpacesRenderer = defaultRendererFactory ( _td ( "Spaces" ) ) ;
359
+ export const defaultDmsRenderer = defaultRendererFactory ( _td ( "Direct Messages" ) ) ;
341
360
342
361
interface ISubspaceSelectorProps {
343
362
title : string ;
0 commit comments