@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
1414limitations under the License.
1515*/
1616
17- import React , { ReactNode , useContext , useMemo , useState } from "react" ;
17+ import React , { ReactNode , useContext , useMemo , useRef , useState } from "react" ;
1818import classNames from "classnames" ;
1919import { Room } from "matrix-js-sdk/src/models/room" ;
2020import { sleep } from "matrix-js-sdk/src/utils" ;
2121import { EventType } from "matrix-js-sdk/src/@types/event" ;
2222import { logger } from "matrix-js-sdk/src/logger" ;
2323
24- import { _t } from '../../../languageHandler' ;
24+ import { _t , _td } from '../../../languageHandler' ;
2525import BaseDialog from "./BaseDialog" ;
2626import Dropdown from "../elements/Dropdown" ;
2727import SearchBox from "../../structures/SearchBox" ;
@@ -38,9 +38,12 @@ import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/Rece
3838import ProgressBar from "../elements/ProgressBar" ;
3939import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar" ;
4040import 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 ;
4447
4548interface IProps {
4649 space : Room ;
@@ -64,31 +67,56 @@ export const Entry = ({ room, checked, onChange }) => {
6467 </ label > ;
6568} ;
6669
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+
6779interface IAddExistingToSpaceProps {
6880 space : Room ;
6981 footerPrompt ?: ReactNode ;
7082 filterPlaceholder : string ;
7183 emptySelectionButton ?: ReactNode ;
7284 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 ;
9088}
9189
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+
92120export const AddExistingToSpace : React . FC < IAddExistingToSpaceProps > = ( {
93121 space,
94122 footerPrompt,
@@ -102,6 +130,13 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
102130 const cli = useContext ( MatrixClientContext ) ;
103131 const visibleRooms = useMemo ( ( ) => cli . getVisibleRooms ( ) . filter ( r => r . getMyMembership ( ) === "join" ) , [ cli ] ) ;
104132
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+
105140 const [ selectedToAdd , setSelectedToAdd ] = useState ( new Set < Room > ( ) ) ;
106141 const [ progress , setProgress ] = useState < number > ( null ) ;
107142 const [ error , setError ] = useState < Error > ( null ) ;
@@ -229,49 +264,56 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
229264 setSelectedToAdd ( new Set ( selectedToAdd ) ) ;
230265 } : null ;
231266
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 ;
248269
249270 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 ) ) {
254272 noResults = false ;
255273 }
256274
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+
257294 return < div className = "mx_AddExistingToSpace" >
258295 < SearchBox
259296 className = "mx_textinput_icon mx_textinput_search"
260297 placeholder = { filterPlaceholder }
261298 onSearch = { setQuery }
262299 autoFocus = { true }
263300 />
264- < AutoHideScrollbar className = "mx_AddExistingToSpace_content" >
301+ < AutoHideScrollbar
302+ className = "mx_AddExistingToSpace_content"
303+ onScroll = { onScroll }
304+ wrappedRef = { wrappedRef }
305+ ref = { scrollRef }
306+ >
265307 { rooms . length > 0 && roomsRenderer ? (
266- roomsRenderer ( rooms , selectedToAdd , onChange , truncateAt , overflowTile )
308+ roomsRenderer ( rooms , selectedToAdd , roomsScrollState , onChange )
267309 ) : undefined }
268310
269311 { spaces . length > 0 && spacesRenderer ? (
270- spacesRenderer ( spaces , selectedToAdd , onChange )
312+ spacesRenderer ( spaces , selectedToAdd , spacesScrollState , onChange )
271313 ) : null }
272314
273315 { dms . length > 0 && dmsRenderer ? (
274- dmsRenderer ( dms , selectedToAdd , onChange )
316+ dmsRenderer ( dms , selectedToAdd , dmsScrollState , onChange )
275317 ) : null }
276318
277319 { noResults ? < span className = "mx_AddExistingToSpace_noResults" >
@@ -285,59 +327,36 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
285327 </ div > ;
286328} ;
287329
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 ,
290335) => (
291336 < 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 => (
297344 < Entry
298345 key = { room . roomId }
299346 room = { room }
300347 checked = { selectedToAdd . has ( room ) }
301348 onChange = { onChange ? ( checked : boolean ) => {
302349 onChange ( checked , room ) ;
303350 } : null }
304- /> ,
351+ />
305352 ) }
306- getChildCount = { ( ) => rooms . length }
307353 />
308354 </ div >
309355) ;
310356
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" ) ) ;
341360
342361interface ISubspaceSelectorProps {
343362 title : string ;
0 commit comments