Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 85 additions & 118 deletions src/pages/RoomMembersPage.js → src/pages/RoomMembersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import PropTypes from 'prop-types';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import type {OnyxEntry} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import Button from '@components/Button';
import ConfirmModal from '@components/ConfirmModal';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {usePersonalDetails} from '@components/OnyxProvider';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
import type {User} from '@components/SelectionList/types';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import type {RoomMembersNavigatorParamList} from '@libs/Navigation/types';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
Expand All @@ -27,51 +28,25 @@ import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {Session} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound';
import withReportOrNotFound from './home/report/withReportOrNotFound';
import reportPropTypes from './reportPropTypes';

const propTypes = {
/** The report currently being looked at */
report: reportPropTypes.isRequired,

/** The policies which the user has access to and which the report could be tied to */
policies: PropTypes.shape({
/** ID of the policy */
id: PropTypes.string,
}),

/** URL Route params */
route: PropTypes.shape({
/** Params from the URL path */
params: PropTypes.shape({
/** reportID passed via route: /workspace/:reportID/members */
reportID: PropTypes.string,
}),
}).isRequired,

/** Session info for the currently logged in user. */
session: PropTypes.shape({
/** Currently logged in user accountID */
accountID: PropTypes.number,
}),

...withLocalizePropTypes,
...windowDimensionsPropTypes,
...withCurrentUserPersonalDetailsPropTypes,
type RoomMembersPageOnyxProps = {
session: OnyxEntry<Session>;
};

const defaultProps = {
session: {
accountID: 0,
},
report: {},
policies: {},
...withCurrentUserPersonalDetailsDefaultProps,
};
type RoomMembersPageProps = WithReportOrNotFoundProps &
WithCurrentUserPersonalDetailsProps &
RoomMembersPageOnyxProps &
StackScreenProps<RoomMembersNavigatorParamList, typeof SCREENS.ROOM_MEMBERS_ROOT>;

function RoomMembersPage(props) {
function RoomMembersPage({report, session, policies}: RoomMembersPageProps) {
const styles = useThemeStyles();
const [selectedMembers, setSelectedMembers] = useState([]);
const {formatPhoneNumber, translate} = useLocalize();
const [selectedMembers, setSelectedMembers] = useState<number[]>([]);
const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [didLoadRoomMembers, setDidLoadRoomMembers] = useState(false);
Expand All @@ -81,9 +56,12 @@ function RoomMembersPage(props) {
* Get members for the current room
*/
const getRoomMembers = useCallback(() => {
Report.openRoomMembersPage(props.report.reportID);
if (!report) {
return;
}
Report.openRoomMembersPage(report.reportID);
setDidLoadRoomMembers(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we setting this true even if report is not set?

}, [props.report.reportID]);
}, [report]);

useEffect(() => {
getRoomMembers();
Expand All @@ -94,73 +72,70 @@ function RoomMembersPage(props) {
* Open the modal to invite a user
*/
const inviteUser = () => {
if (!report) {
return;
}
setSearchValue('');
Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(props.report.reportID));
Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(report.reportID));
};

/**
* Remove selected users from the room
* Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
*/
const removeUsers = () => {
Report.removeFromRoom(props.report.reportID, selectedMembers);
if (report) {
Report.removeFromRoom(report.reportID, selectedMembers);
}
setSelectedMembers([]);
setRemoveMembersConfirmModalVisible(false);
};

/**
* Add user from the selectedMembers list
Comment thread
SzymczakJ marked this conversation as resolved.
*
* @param {String} login
*/
const addUser = useCallback((accountID) => {
const addUser = useCallback((accountID: number) => {
setSelectedMembers((prevSelected) => [...prevSelected, accountID]);
}, []);

/**
* Remove user from the selectedEmployees list
Comment thread
SzymczakJ marked this conversation as resolved.
*
* @param {String} login
*/
const removeUser = useCallback((accountID) => {
setSelectedMembers((prevSelected) => _.without(prevSelected, accountID));
const removeUser = useCallback((accountID: number) => {
setSelectedMembers((prevSelected) => prevSelected.filter((selected) => selected !== accountID));
}, []);

/**
* Toggle user from the selectedMembers list
*
* @param {String} accountID
* @param {String} pendingAction
*
*/
/** Toggle user from the selectedMembers list */
const toggleUser = useCallback(
(accountID, pendingAction) => {
if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
({accountID, pendingAction}: User) => {
if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !accountID) {
return;
}

// Add or remove the user if the checkbox is enabled
if (_.contains(selectedMembers, Number(accountID))) {
removeUser(Number(accountID));
if (selectedMembers.includes(accountID)) {
removeUser(accountID);
} else {
addUser(Number(accountID));
addUser(accountID);
}
},
[selectedMembers, addUser, removeUser],
);

/**
* Add or remove all users passed from the selectedMembers list
* @param {Object} memberList
*/
const toggleAllUsers = (memberList) => {
const enabledAccounts = _.filter(memberList, (member) => !member.isDisabled);
const everyoneSelected = _.every(enabledAccounts, (member) => _.contains(selectedMembers, Number(member.keyForList)));
/** Add or remove all users passed from the selectedMembers list */
const toggleAllUsers = (memberList: User[]) => {
const enabledAccounts = memberList.filter((member) => !member.isDisabled);
const everyoneSelected = enabledAccounts.every((member) => {
if (!member.accountID) {
return false;
}
return selectedMembers.includes(member.accountID);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you updated the logic (use accountID instead of keyForList), make sure the functionality works as expected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, keyForList is actually string created by String(accountId)
result.push({ keyForList: String(accountID),
so instead of doing Number(keyForList) I'm doing accountId
Made sure everything works as expected 😃

});

if (everyoneSelected) {
setSelectedMembers([]);
} else {
const everyAccountId = _.map(enabledAccounts, (member) => Number(member.keyForList));
const everyAccountId = enabledAccounts.map((member) => member.accountID).filter((accountID): accountID is number => !!accountID);
setSelectedMembers(everyAccountId);
}
};
Expand All @@ -172,10 +147,10 @@ function RoomMembersPage(props) {
setRemoveMembersConfirmModalVisible(true);
};

const getMemberOptions = () => {
let result = [];
const getMemberOptions = (): User[] => {
let result: User[] = [];

_.each(props.report.visibleChatMemberAccountIDs, (accountID) => {
report?.visibleChatMemberAccountIDs?.forEach((accountID) => {
const details = personalDetails[accountID];

if (!details) {
Expand Down Expand Up @@ -209,86 +184,93 @@ function RoomMembersPage(props) {

result.push({
keyForList: String(accountID),
accountID: Number(accountID),
isSelected: _.contains(selectedMembers, Number(accountID)),
isDisabled: accountID === props.session.accountID,
text: props.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)),
alternateText: props.formatPhoneNumber(details.login),
accountID,
isSelected: selectedMembers.includes(accountID),
isDisabled: accountID === session?.accountID,
text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)),
alternateText: formatPhoneNumber(details.login),
icons: [
{
source: UserUtils.getAvatar(details.avatar, accountID),
name: details.login,
name: details.login ?? '',
type: CONST.ICON_TYPE_AVATAR,
id: Number(accountID),
},
],
});
});

result = _.sortBy(result, (value) => value.text.toLowerCase());
result = result.sort((value1, value2) => value1.text.localeCompare(value2.text.toLowerCase()));

return result;
};

const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(props.report.policyID, props.policies), [props.report.policyID, props.policies]);
const isPolicyMember = useMemo(() => {
if (!report?.policyID || policies === null) {
return false;
}
return PolicyUtils.isPolicyMember(report.policyID, policies);
}, [report?.policyID, policies]);
const data = getMemberOptions();
const headerMessage = searchValue.trim() && !data.length ? props.translate('roomMembersPage.memberNotFound') : '';
const headerMessage = searchValue.trim() && !data.length ? translate('roomMembersPage.memberNotFound') : '';
return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={RoomMembersPage.displayName}
>
<FullPageNotFoundView
shouldShow={_.isEmpty(props.report) || !isPolicyMember}
Comment thread
SzymczakJ marked this conversation as resolved.
subtitleKey={_.isEmpty(props.report) ? undefined : 'roomMembersPage.notAuthorized'}
onBackButtonPress={() => Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID))}
shouldShow={isEmptyObject(report) || !isPolicyMember}
subtitleKey={isEmptyObject(report) ? undefined : 'roomMembersPage.notAuthorized'}
onBackButtonPress={() => {
Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
}}
>
<HeaderWithBackButton
title={props.translate('workspace.common.members')}
subtitle={ReportUtils.getReportName(props.report)}
title={translate('workspace.common.members')}
subtitle={ReportUtils.getReportName(report)}
onBackButtonPress={() => {
setSearchValue('');
Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID));
Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
}}
/>
<ConfirmModal
danger
title={props.translate('workspace.people.removeMembersTitle')}
title={translate('workspace.people.removeMembersTitle')}
isVisible={removeMembersConfirmModalVisible}
onConfirm={removeUsers}
onCancel={() => setRemoveMembersConfirmModalVisible(false)}
prompt={props.translate('roomMembersPage.removeMembersPrompt')}
confirmText={props.translate('common.remove')}
cancelText={props.translate('common.cancel')}
prompt={translate('roomMembersPage.removeMembersPrompt')}
confirmText={translate('common.remove')}
cancelText={translate('common.cancel')}
/>
<View style={[styles.w100, styles.flex1]}>
<View style={[styles.w100, styles.flexRow, styles.pt3, styles.ph5]}>
<Button
medium
success
text={props.translate('common.invite')}
text={translate('common.invite')}
onPress={inviteUser}
/>
<Button
medium
danger
style={[styles.ml2]}
isDisabled={selectedMembers.length === 0}
text={props.translate('common.remove')}
text={translate('common.remove')}
onPress={askForConfirmationToRemove}
/>
</View>
<View style={[styles.w100, styles.mt4, styles.flex1]}>
<SelectionList
canSelectMultiple
sections={[{data, indexOffset: 0, isDisabled: false}]}
textInputLabel={props.translate('optionsSelector.findMember')}
textInputLabel={translate('optionsSelector.findMember')}
disableKeyboardShortcuts={removeMembersConfirmModalVisible}
textInputValue={searchValue}
onChangeText={setSearchValue}
headerMessage={headerMessage}
onSelectRow={(item) => toggleUser(item.keyForList)}
onSelectRow={(item) => toggleUser(item)}
onSelectAll={() => toggleAllUsers(data)}
showLoadingPlaceholder={!OptionsListUtils.isPersonalDetailsReady(personalDetails) || !didLoadRoomMembers}
showScrollIndicator
Expand All @@ -301,21 +283,6 @@ function RoomMembersPage(props) {
);
}

RoomMembersPage.propTypes = propTypes;
RoomMembersPage.defaultProps = defaultProps;
RoomMembersPage.displayName = 'RoomMembersPage';

export default compose(
withLocalize,
withWindowDimensions,
withReportOrNotFound(),
withOnyx({
session: {
key: ONYXKEYS.SESSION,
},
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
},
}),
withCurrentUserPersonalDetails,
)(RoomMembersPage);
export default withReportOrNotFound()(withCurrentUserPersonalDetails(withOnyx<RoomMembersPageProps, RoomMembersPageOnyxProps>({session: {key: ONYXKEYS.SESSION}})(RoomMembersPage)));