Regression: Fix TeamsChannels reactivity (#21384)

pull/21371/head^2
Douglas Fabris 5 years ago committed by GitHub
parent 72229773d4
commit 1f606f5ef2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      client/contexts/ServerContext/endpoints.ts
  2. 11
      client/contexts/ServerContext/endpoints/v1/teams/listRooms.ts
  3. 15
      client/hooks/lists/useScrollableRecordList.ts
  4. 4
      client/sidebar/header/CreateChannel.js
  5. 27
      client/views/teams/contextualBar/channels/AddExistingModal/AddExistingModal.tsx
  6. 8
      client/views/teams/contextualBar/channels/AddExistingModal/RoomsInput.tsx
  7. 0
      client/views/teams/contextualBar/channels/AddExistingModal/index.ts
  8. 2
      client/views/teams/contextualBar/channels/ConfirmationModal/ConfirmationModal.tsx
  9. 0
      client/views/teams/contextualBar/channels/ConfirmationModal/index.ts
  10. 33
      client/views/teams/contextualBar/channels/TeamsChannelItem.js
  11. 106
      client/views/teams/contextualBar/channels/TeamsChannels.js
  12. 69
      client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts
  13. 5
      client/views/teams/contextualBar/channels/index.js
  14. 4
      client/views/teams/contextualBar/channels/tabBar.ts
  15. 2
      client/views/teams/index.js

@ -15,6 +15,7 @@ import { ListEndpoint as EmojiCustomListEndpoint } from './endpoints/v1/emoji-cu
import { GetDiscussionsEndpoint as ChatGetDiscussionsEndpoint } from './endpoints/v1/chat/getDiscussions';
import { GetThreadsListEndpoint as ChatGetThreadsListEndpoint } from './endpoints/v1/chat/getThreadsList';
import { LivechatVisitorInfoEndpoint } from './endpoints/v1/livechat/visitorInfo';
import { ListRoomsEndpoint } from './endpoints/v1/teams/listRooms';
import { LivechatRoomOnHoldEndpoint } from './endpoints/v1/livechat/onHold';
export type ServerEndpoints = {
@ -33,6 +34,7 @@ export type ServerEndpoints = {
'custom-user-status.list': CustomUserStatusListEndpoint;
'/apps/externalComponents': AppsExternalComponentsEndpoint;
'rooms.autocomplete.channelAndPrivate': RoomsAutocompleteEndpoint;
'teams.listRooms': ListRoomsEndpoint;
'teams.addRooms': TeamsAddRoomsEndpoint;
'livechat/visitors.info': LivechatVisitorInfoEndpoint;
'livechat/room.onHold': LivechatRoomOnHoldEndpoint;

@ -0,0 +1,11 @@
import { IRoom } from '../../../../../../definition/IRoom';
import { IRecordsWithTotal } from '../../../../../../definition/ITeam';
export type ListRoomsEndpoint = {
GET: (params: { teamId: string; offset?: number; count?: number; query: string }) => Omit<IRecordsWithTotal<IRoom>, 'records'> & {
count: number;
offset: number;
rooms: IRecordsWithTotal<IRoom>['records'];
};
}

@ -2,6 +2,7 @@ import { useCallback, useEffect } from 'react';
import { RecordList, RecordListBatchChanges } from '../../lib/lists/RecordList';
import { IRocketChatRecord } from '../../../definition/IRocketChatRecord';
import { AsyncStatePhase } from '../../lib/asyncState';
const INITIAL_ITEM_COUNT = 25;
@ -10,19 +11,21 @@ export const useScrollableRecordList = <T extends IRocketChatRecord>(
fetchBatchChanges: (start: number, end: number) => Promise<RecordListBatchChanges<T>>,
initialItemCount: number = INITIAL_ITEM_COUNT,
): {
loadMoreItems: (start: number, end: number) => void;
loadMoreItems: (start: number) => void;
initialItemCount: number;
} => {
const loadMoreItems = useCallback(
(start: number, end: number) => {
recordList.batchHandle(() => fetchBatchChanges(start, end));
(start: number) => {
if (recordList.phase === AsyncStatePhase.LOADING || start + 1 < recordList.itemCount) {
recordList.batchHandle(() => fetchBatchChanges(start, initialItemCount));
}
},
[recordList, fetchBatchChanges],
[recordList, fetchBatchChanges, initialItemCount],
);
useEffect(() => {
loadMoreItems(0, initialItemCount ?? INITIAL_ITEM_COUNT);
}, [loadMoreItems, initialItemCount]);
loadMoreItems(0);
}, [recordList, loadMoreItems, initialItemCount]);
return { loadMoreItems, initialItemCount };
};

@ -130,6 +130,7 @@ export const CreateChannel = ({
export default memo(({
onClose,
teamId = '',
reload,
}) => {
const createChannel = useEndpointActionExperimental('POST', 'channels.create');
const createPrivateChannel = useEndpointActionExperimental('POST', 'groups.create');
@ -146,7 +147,6 @@ export default memo(({
return false;
}, [canCreateChannel, canCreatePrivateChannel]);
const initialValues = {
users: [],
name: '',
@ -223,6 +223,7 @@ export default memo(({
}
onClose();
reload();
}, [broadcast,
createChannel,
createPrivateChannel,
@ -234,6 +235,7 @@ export default memo(({
teamId,
type,
users,
reload,
]);
return <CreateChannel

@ -1,12 +1,12 @@
import React, { memo, FC, useCallback } from 'react';
import { ButtonGroup, Button, Field, Modal } from '@rocket.chat/fuselage';
import { useForm } from '../../../../hooks/useForm';
import { useTranslation } from '../../../../contexts/TranslationContext';
import { useEndpoint } from '../../../../contexts/ServerContext';
import { useForm } from '../../../../../hooks/useForm';
import { useTranslation } from '../../../../../contexts/TranslationContext';
import { useEndpoint } from '../../../../../contexts/ServerContext';
import RoomsInput from './RoomsInput';
import { IRoom } from '../../../../../definition/IRoom';
import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext';
import { IRoom } from '../../../../../../definition/IRoom';
import { useToastMessageDispatch } from '../../../../../contexts/ToastMessagesContext';
type AddExistingModalState = {
onAdd: any;
@ -18,11 +18,13 @@ type AddExistingModalState = {
type AddExistingModalProps = {
onClose: () => void;
teamId: string;
reload: () => void;
};
const useAddExistingModalState = (onClose: () => void, teamId: string): AddExistingModalState => {
const useAddExistingModalState = (onClose: () => void, teamId: string, reload: () => void): AddExistingModalState => {
const t = useTranslation();
const addRoomEndpoint = useEndpoint('POST', 'teams.addRooms');
const dispatchToastMessage = useToastMessageDispatch();
const { values, handlers, hasUnsavedChanges } = useForm({
rooms: [] as IRoom[],
@ -37,16 +39,12 @@ const useAddExistingModalState = (onClose: () => void, teamId: string): AddExist
return;
}
handleRooms([...rooms, value]);
return;
return handleRooms([...rooms, value]);
}
handleRooms(rooms.filter((current: any) => current._id !== value._id));
}, [handleRooms, rooms]);
const dispatchToastMessage = useToastMessageDispatch();
const onAdd = useCallback(async () => {
try {
await addRoomEndpoint({
@ -56,22 +54,23 @@ const useAddExistingModalState = (onClose: () => void, teamId: string): AddExist
dispatchToastMessage({ type: 'success', message: t('Channels_added') });
onClose();
reload();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
}, [addRoomEndpoint, rooms, teamId, onClose, dispatchToastMessage, t]);
}, [addRoomEndpoint, rooms, teamId, onClose, dispatchToastMessage, t, reload]);
return { onAdd, rooms, onChange, hasUnsavedChanges };
};
const AddExistingModal: FC<AddExistingModalProps> = ({ onClose, teamId }) => {
const AddExistingModal: FC<AddExistingModalProps> = ({ onClose, teamId, reload }) => {
const t = useTranslation();
const {
rooms,
onAdd,
onChange,
hasUnsavedChanges,
} = useAddExistingModalState(onClose, teamId);
} = useAddExistingModalState(onClose, teamId, reload);
const isAddButtonEnabled = hasUnsavedChanges;

@ -2,10 +2,10 @@ import { AutoComplete, Box, Icon, Option, Options, Chip, AutoCompleteProps } fro
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { FC, memo, useCallback, useMemo, useState } from 'react';
import { IRoom } from '../../../../../definition/IRoom';
import RoomAvatar from '../../../../components/avatar/RoomAvatar';
import { useEndpointData } from '../../../../hooks/useEndpointData';
import { roomTypes } from '../../../../../app/utils/client';
import { IRoom } from '../../../../../../definition/IRoom';
import RoomAvatar from '../../../../../components/avatar/RoomAvatar';
import { useEndpointData } from '../../../../../hooks/useEndpointData';
import { roomTypes } from '../../../../../../app/utils/client';
type RoomsInputProps = {
value: IRoom[];

@ -1,7 +1,7 @@
import React, { memo, FC } from 'react';
import { Box, ButtonGroup, Button, Icon, Modal } from '@rocket.chat/fuselage';
import { useTranslation } from '../../../../contexts/TranslationContext';
import { useTranslation } from '../../../../../contexts/TranslationContext';
type ConfirmationModalProps = {
onClose: () => void;

@ -1,15 +1,14 @@
import React, { useMemo, useState } from 'react';
import { ActionButton, Box, CheckBox, Icon, Menu, Option } from '@rocket.chat/fuselage';
import { ActionButton, Box, CheckBox, Icon, Menu, Option, Tag } from '@rocket.chat/fuselage';
import { usePrefersReducedMotion, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpoint } from '../../../contexts/ServerContext';
import { useSetModal } from '../../../contexts/ModalContext';
import RoomAvatar from '../../../components/avatar/RoomAvatar';
import ConfirmationModal from '../modals/ConfirmationModal';
import { roomTypes } from '../../../../app/utils/client';
import { usePreventProgation } from '../../../hooks/usePreventProgation';
import Breadcrumbs from '../../../components/Breadcrumbs';
import { useTranslation } from '../../../../contexts/TranslationContext';
import { useEndpoint } from '../../../../contexts/ServerContext';
import { useSetModal } from '../../../../contexts/ModalContext';
import RoomAvatar from '../../../../components/avatar/RoomAvatar';
import ConfirmationModal from './ConfirmationModal';
import { roomTypes } from '../../../../../app/utils/client';
import { usePreventProgation } from '../../../../hooks/usePreventProgation';
export const useReactModal = (Component, props) => {
const setModal = useSetModal();
@ -26,7 +25,7 @@ export const useReactModal = (Component, props) => {
});
};
const RoomActions = ({ room }) => {
const RoomActions = ({ room, reload }) => {
const t = useTranslation();
const updateRoomEndpoint = useEndpoint('POST', 'teams.updateRoom');
const removeRoomEndpoint = useEndpoint('POST', 'teams.removeRoom');
@ -35,6 +34,7 @@ const RoomActions = ({ room }) => {
const RemoveFromTeamAction = useReactModal(ConfirmationModal, {
onConfirmAction: () => {
removeRoomEndpoint({ teamId: room.teamId, roomId: room._id });
reload();
},
labelButton: t('Remove'),
content: <Box is='span' size='14px'>{t('Team_Remove_from_team_modal_content', { teamName: roomTypes.getRoomName(room.t, room) })}</Box>,
@ -43,6 +43,7 @@ const RoomActions = ({ room }) => {
const DeleteChannelAction = useReactModal(ConfirmationModal, {
onConfirmAction: () => {
deleteRoomEndpoint({ roomId: room._id });
reload();
},
labelButton: t('Delete'),
content: <>
@ -57,6 +58,8 @@ const RoomActions = ({ room }) => {
roomId: room._id,
isDefault: !room.teamDefault,
});
reload();
};
return [{
@ -80,7 +83,7 @@ const RoomActions = ({ room }) => {
},
action: DeleteChannelAction,
}];
}, [DeleteChannelAction, RemoveFromTeamAction, room._id, room.t, room.teamDefault, t, updateRoomEndpoint]);
}, [DeleteChannelAction, RemoveFromTeamAction, room._id, room.t, room.teamDefault, t, updateRoomEndpoint, reload]);
return <Menu
flexShrink={0}
@ -94,7 +97,7 @@ const RoomActions = ({ room }) => {
/>;
};
export const TeamChannelItem = ({ room, onClickView }) => {
export const TeamsChannelItem = ({ room, onClickView, reload }) => {
const t = useTranslation();
const [showButton, setShowButton] = useState();
@ -114,9 +117,9 @@ export const TeamChannelItem = ({ room, onClickView }) => {
<RoomAvatar room={room} size='x28' />
</Option.Avatar>
<Option.Column>{room.t === 'c' ? <Icon name='hash' size='x15'/> : <Icon name='hashtag-lock' size='x15'/>}</Option.Column>
<Option.Content><Box display='inline-flex'>{roomTypes.getRoomName(room.t, room)} {room.teamDefault ? <Breadcrumbs.Tag>{t('Team_Auto-join')}</Breadcrumbs.Tag> : ''}</Box></Option.Content>
<Option.Content><Box display='inline-flex'>{roomTypes.getRoomName(room.t, room)} {room.teamDefault ? <Box mi='x8'><Tag>{t('Team_Auto-join')}</Tag></Box> : ''}</Box></Option.Content>
<Option.Menu onClick={onClick}>
{showButton ? <RoomActions room={room} /> : <ActionButton
{showButton ? <RoomActions room={room} reload={reload} /> : <ActionButton
ghost
tiny
icon='kebab'
@ -126,4 +129,4 @@ export const TeamChannelItem = ({ room, onClickView }) => {
);
};
TeamChannelItem.Skeleton = Option.Skeleton;
TeamsChannelItem.Skeleton = Option.Skeleton;

@ -1,36 +1,35 @@
import React, { memo, useCallback, useMemo, useState } from 'react';
import { Box, Icon, TextInput, Margins, Select, Throbber, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { Virtuoso } from 'react-virtuoso';
import { useMutableCallback, useLocalStorage, useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpoint } from '../../../contexts/ServerContext';
import { useSetModal } from '../../../contexts/ModalContext';
import { useScrollableRecordList } from '../../../hooks/lists/useScrollableRecordList';
import { useRecordList } from '../../../hooks/lists/useRecordList';
import { RecordList } from '../../../lib/lists/RecordList.ts';
import { TeamChannelItem } from './TeamChannelItem';
import { useTabBarClose } from '../../room/providers/ToolboxProvider';
import { roomTypes } from '../../../../app/utils';
import ScrollableContentWrapper from '../../../components/ScrollableContentWrapper';
import VerticalBar from '../../../components/VerticalBar';
import AddExistingModal from '../modals/AddExistingModal';
import CreateChannel from '../../../sidebar/header/CreateChannel';
import RoomInfo from '../../room/contextualBar/Info';
const Row = memo(function Row({ room, onClickView }) {
import { useMutableCallback, useLocalStorage, useAutoFocus, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '../../../../contexts/TranslationContext';
import { useSetModal } from '../../../../contexts/ModalContext';
import { useRecordList } from '../../../../hooks/lists/useRecordList';
import { TeamsChannelItem } from './TeamsChannelItem';
import { useTabBarClose } from '../../../room/providers/ToolboxProvider';
import { roomTypes } from '../../../../../app/utils';
import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper';
import VerticalBar from '../../../../components/VerticalBar';
import AddExistingModal from './AddExistingModal';
import CreateChannel from '../../../../sidebar/header/CreateChannel';
import RoomInfo from '../../../room/contextualBar/Info';
import { useTeamsChannelList } from './hooks/useTeamsChannelList';
import { AsyncStatePhase } from '../../../../lib/asyncState';
const Row = memo(function Row({ room, onClickView, reload }) {
if (!room) {
return <BaseTeamChannels.Option.Skeleton />;
return <BaseTeamsChannels.Option.Skeleton />;
}
return <BaseTeamChannels.Option
return <BaseTeamsChannels.Option
room={room}
onClickView={onClickView}
reload={reload}
/>;
});
const BaseTeamChannels = ({
const BaseTeamsChannels = ({
loading,
channels = [],
text,
@ -43,6 +42,7 @@ const BaseTeamChannels = ({
total,
loadMoreItems,
onClickView,
reload,
}) => {
const t = useTranslation();
const inputRef = useAutoFocus(true);
@ -52,13 +52,13 @@ const BaseTeamChannels = ({
['autoJoin', t('Auto-join')],
], [t]);
const lm = useMutableCallback((start) => loadMoreItems(start + 1, Math.min(50, start + 1 - channels.length)));
const lm = useMutableCallback((start) => !loading && loadMoreItems(start));
return (
<>
<VerticalBar.Header>
<VerticalBar.Icon name='hash'/>
<VerticalBar.Text>{t('Channels')}</VerticalBar.Text>
<VerticalBar.Text>{t('Team_Channels')}</VerticalBar.Text>
{ onClickClose && <VerticalBar.Close onClick={onClickClose} /> }
</VerticalBar.Header>
@ -78,25 +78,20 @@ const BaseTeamChannels = ({
</Box>
{loading && <Box pi='x24' pb='x12'><Throbber size='x12' /></Box>}
{!loading && channels.length <= 0 && <Box pi='x24' pb='x12'>{t('No_results_found')}</Box>}
<Box w='full' h='full' overflow='hidden' flexShrink={1}>
{!loading && channels && channels.length > 0 && <Virtuoso
style={{
height: '100%',
width: '100%',
}}
{!loading && channels.length === 0 && <Box pi='x24' pb='x12'>{t('No_results_found')}</Box>}
{!loading && <Box w='full' h='full' overflow='hidden' flexShrink={1}>
<Virtuoso
totalCount={total}
endReached={lm}
overscan={50}
data={channels}
components={{ Scroller: ScrollableContentWrapper }}
itemContent={(index, data) => <Row
onClickView={onClickView}
room={data}
reload={reload}
/>}
/>}
</Box>
/>
</Box>}
</VerticalBar.Content>
<VerticalBar.Footer>
@ -109,8 +104,6 @@ const BaseTeamChannels = ({
);
};
BaseTeamChannels.Option = TeamChannelItem;
export const useReactModal = (Component, props) => {
const setModal = useSetModal();
@ -128,41 +121,25 @@ export const useReactModal = (Component, props) => {
});
};
const TeamChannels = ({ teamId }) => {
const TeamsChannels = ({ teamId }) => {
const [state, setState] = useState({});
const onClickClose = useTabBarClose();
const [type, setType] = useLocalStorage('channels-list-type', 'all');
const [text, setText] = useState('');
const [roomList] = useState(() => new RecordList());
const roomListEndpoint = useEndpoint('GET', 'teams.listRooms');
const debouncedText = useDebouncedValue(text, 800);
const fetchData = useCallback(async () => {
const { rooms, total } = await roomListEndpoint({ teamId });
const { teamsChannelList, loadMoreItems, reload } = useTeamsChannelList(useMemo(() => ({ teamId, text: debouncedText, type }), [teamId, debouncedText, type]));
const roomsDated = rooms.map((rooms) => {
rooms._updatedAt = new Date(rooms._updatedAt);
return { ...rooms };
});
return {
items: roomsDated,
itemCount: total,
};
}, [roomListEndpoint, teamId]);
const { loadMoreItems } = useScrollableRecordList(
roomList,
fetchData,
);
const { phase, items, itemCount } = useRecordList(roomList);
const { phase, items, itemCount: total } = useRecordList(teamsChannelList);
const handleTextChange = useCallback((event) => {
setText(event.currentTarget.value);
}, []);
const addExisting = useReactModal(AddExistingModal, { teamId });
const createNew = useReactModal(CreateChannel, { teamId });
const addExisting = useReactModal(AddExistingModal, { teamId, reload });
const createNew = useReactModal(CreateChannel, { teamId, reload });
const goToRoom = useCallback((room) => roomTypes.openRouteLink(room.t, room), []);
const handleBack = useCallback(() => setState({}), [setState]);
@ -180,21 +157,24 @@ const TeamChannels = ({ teamId }) => {
}
return (
<BaseTeamChannels
loading={phase === 'loading'}
<BaseTeamsChannels
loading={phase === AsyncStatePhase.LOADING}
type={type}
text={text}
setType={setType}
setText={handleTextChange}
channels={items}
total={itemCount}
total={total}
onClickClose={onClickClose}
onClickAddExisting={addExisting}
onClickCreateNew={createNew}
onClickView={viewRoom}
loadMoreItems={loadMoreItems}
reload={reload}
/>
);
};
export default TeamChannels;
BaseTeamsChannels.Option = TeamsChannelItem;
export default TeamsChannels;

@ -0,0 +1,69 @@
import { useCallback, useMemo, useState } from 'react';
import { getConfig } from '../../../../../../app/ui-utils/client/config';
import { IRoom } from '../../../../../../definition/IRoom';
import { useEndpoint } from '../../../../../contexts/ServerContext';
import { useScrollableRecordList } from '../../../../../hooks/lists/useScrollableRecordList';
import { useComponentDidUpdate } from '../../../../../hooks/useComponentDidUpdate';
import { RecordList } from '../../../../../lib/lists/RecordList';
type TeamsChannelListOptions = {
teamId: string;
type: 'all' | 'autoJoin';
text: string;
}
export const useTeamsChannelList = (
options: TeamsChannelListOptions,
): {
teamsChannelList: RecordList<IRoom>;
initialItemCount: number;
reload: () => void;
loadMoreItems: (start: number, end: number) => void;
} => {
const apiEndPoint = useEndpoint('GET', 'teams.listRooms');
const [teamsChannelList, setTeamsChannelList] = useState(() => new RecordList<IRoom>());
const reload = useCallback(() => setTeamsChannelList(new RecordList<IRoom>()), []);
useComponentDidUpdate(() => {
options && reload();
}, [options, reload]);
const fetchData = useCallback(
async (start, end) => {
const { rooms, total } = await apiEndPoint({
teamId: options.teamId,
offset: start,
count: end - start,
query: JSON.stringify({
name: { $regex: options.text || '', $options: 'i' },
...options.type !== 'all' && {
teamDefault: true,
},
}),
});
return {
items: rooms.map((rooms) => {
rooms._updatedAt = new Date(rooms._updatedAt);
return { ...rooms };
}),
itemCount: total,
};
},
[apiEndPoint, options],
);
const { loadMoreItems, initialItemCount } = useScrollableRecordList(teamsChannelList, fetchData, useMemo(() => {
const filesListSize = getConfig('teamsChannelListSize');
return filesListSize ? parseInt(filesListSize, 10) : undefined;
}, []));
return {
reload,
teamsChannelList,
loadMoreItems,
initialItemCount,
};
};

@ -0,0 +1,5 @@
import React from 'react';
import TeamsChannels from './TeamsChannels';
export default ({ teamId }) => <TeamsChannels teamId={teamId} />;

@ -1,6 +1,6 @@
import { FC, lazy, LazyExoticComponent } from 'react';
import { addAction } from '../../room/lib/Toolbox';
import { addAction } from '../../../room/lib/Toolbox';
addAction('team-channels', {
groups: ['team'],
@ -9,6 +9,6 @@ addAction('team-channels', {
full: true,
title: 'Team_Channels',
icon: 'hash',
template: lazy(() => import('./TeamChannels')) as LazyExoticComponent<FC>,
template: lazy(() => import('./index')) as LazyExoticComponent<FC>,
order: 2,
});

@ -1,4 +1,4 @@
import './contextualBar/tabBar';
import './contextualBar/channels/tabBar';
import './info/tabBar.ts';
import './members/tabBar';
import './info';

Loading…
Cancel
Save