Skip to content

Commit

Permalink
fix: Members/Channels list infinite scroll (#28636)
Browse files Browse the repository at this point in the history
Co-authored-by: Guilherme Jun Grillo <48109548+guijun13@users.noreply.github.com>
  • Loading branch information
yash-rajpal and guijun13 authored May 17, 2023
1 parent ec4394e commit abe5fb5
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 21 deletions.
39 changes: 39 additions & 0 deletions apps/meteor/client/components/InfiniteListAnchor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Box } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
import React, { useEffect, useRef } from 'react';

type InfiniteListAnchorProps = {
loadMore: () => void;
} & ComponentProps<typeof Box>;

const InfiniteListAnchor = ({ loadMore, ...props }: InfiniteListAnchorProps) => {
const ref = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const target = ref.current;

if (!target) {
return;
}

const observer = new IntersectionObserver(
(e) => {
if (e[0].isIntersecting) {
loadMore();
}
},
{
root: null,
threshold: 0.1,
},
);

observer.observe(target);

return () => observer.disconnect();
}, [loadMore]);

return <Box width={5} height={5} ref={ref} {...props} />;
};

export default InfiniteListAnchor;
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import type { SelectOption } from '@rocket.chat/fuselage';
import { Box, Icon, TextInput, Margins, Select, Throbber, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage';
import { useMutableCallback, useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { useMutableCallback, useAutoFocus, useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement, FormEventHandler, ComponentProps, MouseEvent } from 'react';
import React, { useMemo } from 'react';
import { Virtuoso } from 'react-virtuoso';

import InfiniteListAnchor from '../../../../components/InfiniteListAnchor';
import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper';
import VerticalBar from '../../../../components/VerticalBar';
import RoomMembersRow from './RoomMembersRow';
Expand Down Expand Up @@ -67,6 +68,18 @@ const RoomMembers = ({
[t],
);

const loadMoreMembers = useDebouncedCallback(
() => {
if (members.length >= total) {
return;
}

loadMore(members.length);
},
300,
[loadMore, members],
);

return (
<>
<VerticalBar.Header data-qa-id='RoomHeader-Members'>
Expand Down Expand Up @@ -135,10 +148,10 @@ const RoomMembers = ({
width: '100%',
}}
totalCount={total}
endReached={loadMore}
overscan={50}
data={members}
components={{ Scroller: ScrollableContentWrapper }}
// eslint-disable-next-line react/no-multi-comp
components={{ Scroller: ScrollableContentWrapper, Footer: () => <InfiniteListAnchor loadMore={loadMoreMembers} /> }}
itemContent={(index, data): ReactElement => <RowComponent data={itemData} user={data} index={index} reload={reload} />}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import type { IRoom } from '@rocket.chat/core-typings';
import type { SelectOption } from '@rocket.chat/fuselage';
import { Box, Icon, TextInput, Margins, Select, Throbber, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useMutableCallback, useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { useMutableCallback, useAutoFocus, useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ChangeEvent, Dispatch, SetStateAction, SyntheticEvent } from 'react';
import React, { useMemo } from 'react';
import { Virtuoso } from 'react-virtuoso';

import InfiniteListAnchor from '../../../../components/InfiniteListAnchor';
import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper';
import VerticalBar from '../../../../components/VerticalBar';
import Row from './Row';

type BaseTeamsChannelsProps = {
loading: boolean;
channels: IRoom[];
text: string;
type: 'all' | 'autoJoin';
setType: Dispatch<SetStateAction<'all' | 'autoJoin'>>;
setText: (e: ChangeEvent<HTMLInputElement>) => void;
onClickClose: () => void;
onClickAddExisting: false | ((e: SyntheticEvent) => void);
onClickCreateNew: false | ((e: SyntheticEvent) => void);
total: number;
loadMoreItems: (start: number, end: number) => void;
onClickView: (room: IRoom) => void;
reload: () => void;
};

const BaseTeamsChannels = ({
loading,
channels = [],
Expand All @@ -22,19 +42,31 @@ const BaseTeamsChannels = ({
loadMoreItems,
onClickView,
reload,
}) => {
}: BaseTeamsChannelsProps) => {
const t = useTranslation();
const inputRef = useAutoFocus(true);
const inputRef = useAutoFocus<HTMLInputElement>(true);

const options = useMemo(
const options: SelectOption[] = useMemo(
() => [
['all', t('All')],
['autoJoin', t('Auto-join')],
['autoJoin', t('Team_Auto-join')],
],
[t],
);

const lm = useMutableCallback((start) => !loading && loadMoreItems(start));
const lm = useMutableCallback((start) => !loading && loadMoreItems(start, Math.min(50, total - start)));

const loadMoreChannels = useDebouncedCallback(
() => {
if (channels.length >= total) {
return;
}

lm(channels.length);
},
300,
[lm, channels],
);

return (
<>
Expand All @@ -55,7 +87,7 @@ const BaseTeamsChannels = ({
onChange={setText}
addon={<Icon name='magnifier' size='x20' />}
/>
<Select flexGrow={0} width='110px' onChange={setType} value={type} options={options} />
<Select flexGrow={0} width='110px' onChange={(val) => setType(val as 'all' | 'autoJoin')} value={type} options={options} />
</Margins>
</Box>
</Box>
Expand All @@ -70,14 +102,27 @@ const BaseTeamsChannels = ({
{t('No_channels_in_team')}
</Box>
)}

{!loading && channels.length > 0 && (
<Box pi='x18' pb='x12'>
<Box is='span' color='hint' fontScale='p2'>
{t('Showing')}: {channels.length}
</Box>

<Box is='span' color='hint' fontScale='p2' mis='x8'>
{t('Total')}: {total}
</Box>
</Box>
)}

{!loading && (
<Box w='full' h='full' overflow='hidden' flexShrink={1}>
<Virtuoso
totalCount={total}
endReached={lm}
data={channels}
components={{ Scroller: ScrollableContentWrapper }}
itemContent={(index, data) => <Row onClickView={onClickView} room={data} reload={reload} />}
// eslint-disable-next-line react/no-multi-comp
components={{ Scroller: ScrollableContentWrapper, Footer: () => <InfiniteListAnchor loadMore={loadMoreChannels} /> }}
itemContent={(index, data) => <Row onClickView={onClickView} room={data} reload={reload} key={index} />}
/>
</Box>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useMutableCallback, useLocalStorage, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { useSetModal, usePermission } from '@rocket.chat/ui-contexts';
import type { FC, SyntheticEvent } from 'react';
import React, { useCallback, useMemo, useState } from 'react';

import { useRecordList } from '../../../../hooks/lists/useRecordList';
Expand All @@ -12,10 +14,15 @@ import AddExistingModal from './AddExistingModal';
import BaseTeamsChannels from './BaseTeamsChannels';
import { useTeamsChannelList } from './hooks/useTeamsChannelList';

const useReactModal = (Component, teamId, reload) => {
type TeamChannelsProps = {
teamId: string;
rid: string;
};

const useReactModal = (Component: FC<any>, teamId: string, reload: () => void) => {
const setModal = useSetModal();

return useMutableCallback((e) => {
return useMutableCallback((e: SyntheticEvent) => {
e.preventDefault();

const handleClose = () => {
Expand All @@ -27,11 +34,11 @@ const useReactModal = (Component, teamId, reload) => {
});
};

const TeamsChannels = ({ teamId, rid }) => {
const [state, setState] = useState({});
const TeamsChannels = ({ teamId, rid }: TeamChannelsProps) => {
const [state, setState] = useState<{ tab?: string; rid?: string }>({});
const onClickClose = useTabBarClose();

const [type, setType] = useLocalStorage('channels-list-type', 'all');
const [type, setType] = useLocalStorage<'all' | 'autoJoin'>('channels-list-type', 'all');
const [text, setText] = useState('');

const debouncedText = useDebouncedValue(text, 800);
Expand All @@ -52,12 +59,12 @@ const TeamsChannels = ({ teamId, rid }) => {

const goToRoom = useCallback((room) => roomCoordinator.openRouteLink(room.t, room), []);
const handleBack = useCallback(() => setState({}), [setState]);
const viewRoom = useMutableCallback((room) => {
const viewRoom = useMutableCallback((room: IRoom) => {
goToRoom(room);
});

if (state.tab === 'RoomInfo') {
return <RoomInfo rid={state.rid} onClickClose={onClickClose} onClickBack={handleBack} onEnterRoom={goToRoom} resetState={setState} />;
if (state?.tab === 'RoomInfo' && state?.rid) {
return <RoomInfo rid={state?.rid} onClickBack={handleBack} onEnterRoom={goToRoom} resetState={() => setState({})} />;
}

return (
Expand Down

0 comments on commit abe5fb5

Please sign in to comment.