Skip to content
256 changes: 105 additions & 151 deletions src/pages/NewChatPage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import _ from 'underscore';
import React, {Component} from 'react';
import React, {useState, useEffect, useMemo} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
Expand Down Expand Up @@ -44,148 +44,98 @@ const defaultProps = {
reports: {},
};

class NewChatPage extends Component {
constructor(props) {
super(props);

this.toggleOption = this.toggleOption.bind(this);
this.createChat = this.createChat.bind(this);
this.createGroup = this.createGroup.bind(this);
this.updateOptionsWithSearchTerm = this.updateOptionsWithSearchTerm.bind(this);
this.excludedGroupEmails = _.without(CONST.EXPENSIFY_EMAILS, CONST.EMAIL.CONCIERGE);

const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions(
props.reports,
props.personalDetails,
props.betas,
'',
[],
this.props.isGroupChat ? this.excludedGroupEmails : [],
);
this.state = {
searchTerm: '',
recentReports,
personalDetails,
selectedOptions: [],
userToInvite,
};
}

componentDidUpdate(prevProps) {
if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) {
return;
}
this.updateOptionsWithSearchTerm(this.state.searchTerm);
}

/**
* Returns the sections needed for the OptionsSelector
*
* @param {Boolean} maxParticipantsReached
* @returns {Array}
*/
getSections(maxParticipantsReached) {
const sections = [];
const excludedGroupEmails = _.without(CONST.EXPENSIFY_EMAILS, CONST.EMAIL.CONCIERGE);

function NewChatPage(props) {

@Nikhil-Vats Nikhil-Vats Jun 5, 2023

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.

using function based on the discussion here

const [searchTerm, setSearchTerm] = useState('');
const [filteredRecentReports, setFilteredRecentReports] = useState([]);
const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]);
const [filteredUserToInvite, setFilteredUserToInvite] = useState();
const [selectedOptions, setSelectedOptions] = useState([]);

const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
const headerMessage = OptionsListUtils.getHeaderMessage(
filteredPersonalDetails.length + filteredRecentReports.length !== 0,
Boolean(filteredUserToInvite),
searchTerm,
maxParticipantsReached,
);
const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(props.personalDetails);

const sections = useMemo(() => {

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.

I used useMemo here as it was also used here -

const sections = useMemo(() => {

But I propose to remove it following the guidelines in React docs -

Optimizing with useMemo is only valuable in a few cases:

  1. The calculation you’re putting in useMemo is noticeably slow, and its dependencies rarely change.
  2. You pass it as a prop to a component wrapped in memo. You want to skip re-rendering if the value hasn’t changed. Memoization lets your component re-render only when dependencies aren’t the same.
  3. The value you’re passing is later used as a dependency of some Hook. For example, maybe another useMemo calculation value depends on it. Or maybe you are depending on this value from useEffect.

2 and 3 are not applicable here. For the first point, the dependencies change often in our case which would anyway recalculate the value.

If you agree then we can remove it from TaskAssigneeSelectorModal too because another issue there is that it uses props as a dependency when it only uses props.translate during calculation -

}, [filteredCurrentUserOption, filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, props]);

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.

discussion on useCallback, useMemo in slack

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.

@aimane-chnaif I will remove useMemo here if you agree.

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.

I think so. I don't see much benefit of performance improvement using useMemo here.

const sectionsList = [];
let indexOffset = 0;

if (this.props.isGroupChat) {
sections.push({
if (props.isGroupChat) {
sectionsList.push({
title: undefined,
data: this.state.selectedOptions,
shouldShow: !_.isEmpty(this.state.selectedOptions),
data: selectedOptions,
shouldShow: !_.isEmpty(selectedOptions),
indexOffset,
});
indexOffset += this.state.selectedOptions.length;
indexOffset += selectedOptions.length;

if (maxParticipantsReached) {
return sections;
return sectionsList;
}
}

// Filtering out selected users from the search results
const filterText = _.reduce(this.state.selectedOptions, (str, {login}) => `${str} ${login}`, '');
const recentReportsWithoutSelected = _.filter(this.state.recentReports, ({login}) => !filterText.includes(login));
const personalDetailsWithoutSelected = _.filter(this.state.personalDetails, ({login}) => !filterText.includes(login));
const hasUnselectedUserToInvite = this.state.userToInvite && !filterText.includes(this.state.userToInvite.login);
const filterText = _.reduce(selectedOptions, (str, {login}) => `${str} ${login}`, '');
const recentReportsWithoutSelected = _.filter(filteredRecentReports, ({login}) => !filterText.includes(login));
const personalDetailsWithoutSelected = _.filter(filteredPersonalDetails, ({login}) => !filterText.includes(login));
const hasUnselectedUserToInvite = filteredUserToInvite && !filterText.includes(filteredUserToInvite.login);

sections.push({
title: this.props.translate('common.recents'),
sectionsList.push({
title: props.translate('common.recents'),
data: recentReportsWithoutSelected,
shouldShow: !_.isEmpty(recentReportsWithoutSelected),
indexOffset,
});
indexOffset += recentReportsWithoutSelected.length;

sections.push({
title: this.props.translate('common.contacts'),
sectionsList.push({
title: props.translate('common.contacts'),
data: personalDetailsWithoutSelected,
shouldShow: !_.isEmpty(personalDetailsWithoutSelected),
indexOffset,
});
indexOffset += personalDetailsWithoutSelected.length;

if (hasUnselectedUserToInvite) {
sections.push({
sectionsList.push({
title: undefined,
data: [this.state.userToInvite],
data: [filteredUserToInvite],
shouldShow: true,
indexOffset,
});
}

return sections;
}

updateOptionsWithSearchTerm(searchTerm = '') {
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions(
this.props.reports,
this.props.personalDetails,
this.props.betas,
searchTerm,
[],
this.props.isGroupChat ? this.excludedGroupEmails : [],
);
this.setState({
searchTerm,
userToInvite,
recentReports,
personalDetails,
});
}
return sectionsList;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, maxParticipantsReached, props.isGroupChat, selectedOptions]);

/**
* Removes a selected option from list if already selected. If not already selected add this option to the list.
* @param {Object} option
*/
toggleOption(option) {
this.setState((prevState) => {
const isOptionInList = _.some(prevState.selectedOptions, (selectedOption) => selectedOption.login === option.login);
function toggleOption(option) {
const isOptionInList = _.some(selectedOptions, (selectedOption) => selectedOption.login === option.login);

let newSelectedOptions;
let newSelectedOptions;

if (isOptionInList) {
newSelectedOptions = _.reject(prevState.selectedOptions, (selectedOption) => selectedOption.login === option.login);
} else {
newSelectedOptions = [...prevState.selectedOptions, option];
}
if (isOptionInList) {
newSelectedOptions = _.reject(selectedOptions, (selectedOption) => selectedOption.login === option.login);
} else {
newSelectedOptions = [...selectedOptions, option];
}

const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions(
this.props.reports,
this.props.personalDetails,
this.props.betas,
prevState.searchTerm,
[],
this.excludedGroupEmails,
);
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions(props.reports, props.personalDetails, props.betas, searchTerm, [], excludedGroupEmails);

return {
selectedOptions: newSelectedOptions,
recentReports,
personalDetails,
userToInvite,
searchTerm: prevState.searchTerm,
};
});
setSelectedOptions(newSelectedOptions);
setFilteredRecentReports(recentReports);
setFilteredPersonalDetails(personalDetails);
setFilteredUserToInvite(userToInvite);
}

/**
Expand All @@ -194,73 +144,77 @@ class NewChatPage extends Component {
*
* @param {Object} option
*/
createChat(option) {
function createChat(option) {
Report.navigateToAndOpenReport([option.login]);
}

/**
* Creates a new group chat with all the selected options and the current user,
* or navigates to the existing chat if one with those participants already exists.
*/
createGroup() {
if (!this.props.isGroupChat) {
const createGroup = () => {

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.

Same here

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.

didn't use function here as it was passed down as prop

if (!props.isGroupChat) {
return;
}
const logins = _.pluck(this.state.selectedOptions, 'login');
const logins = _.pluck(selectedOptions, 'login');
if (logins.length < 1) {
return;
}
Report.navigateToAndOpenReport(logins);
}
};

render() {
const maxParticipantsReached = this.state.selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
const sections = this.getSections(maxParticipantsReached);
const headerMessage = OptionsListUtils.getHeaderMessage(
this.state.personalDetails.length + this.state.recentReports.length !== 0,
Boolean(this.state.userToInvite),
this.state.searchTerm,
maxParticipantsReached,
);
const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
>
{({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
<>
<HeaderWithBackButton title={this.props.isGroupChat ? this.props.translate('sidebarScreen.newGroup') : this.props.translate('sidebarScreen.newChat')} />

<View style={[styles.flex1, styles.w100, styles.pRelative, this.state.selectedOptions.length > 0 ? safeAreaPaddingBottomStyle : {}]}>
<OptionsSelector
canSelectMultipleOptions={this.props.isGroupChat}
sections={sections}
selectedOptions={this.state.selectedOptions}
value={this.state.searchTerm}
onSelectRow={(option) => (this.props.isGroupChat ? this.toggleOption(option) : this.createChat(option))}
onChangeText={this.updateOptionsWithSearchTerm}
headerMessage={headerMessage}
boldStyle
shouldFocusOnSelectRow={this.props.isGroupChat && !Browser.isMobile()}
shouldShowConfirmButton={this.props.isGroupChat}
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
confirmButtonText={this.props.translate('newChatPage.createGroup')}
onConfirmSelection={this.createGroup}
textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
/>
</View>
</>
)}
</ScreenWrapper>
useEffect(() => {
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions(
props.reports,
props.personalDetails,
props.betas,
searchTerm,
[],
props.isGroupChat ? excludedGroupEmails : [],
);
}
setFilteredRecentReports(recentReports);
setFilteredPersonalDetails(personalDetails);
setFilteredUserToInvite(userToInvite);
// props.betas and props.isGroupChat are not added as dependencies since they don't change during the component lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.reports, props.personalDetails, searchTerm]);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
>
{({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
<>
<HeaderWithBackButton title={props.isGroupChat ? props.translate('sidebarScreen.newGroup') : props.translate('sidebarScreen.newChat')} />
<View style={[styles.flex1, styles.w100, styles.pRelative, selectedOptions.length > 0 ? safeAreaPaddingBottomStyle : {}]}>
<OptionsSelector
canSelectMultipleOptions={props.isGroupChat}
sections={sections}
selectedOptions={selectedOptions}
value={searchTerm}
onSelectRow={(option) => (props.isGroupChat ? toggleOption(option) : createChat(option))}
onChangeText={setSearchTerm}
headerMessage={headerMessage}
boldStyle
shouldFocusOnSelectRow={props.isGroupChat && !Browser.isMobile()}
shouldShowConfirmButton={props.isGroupChat}
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
confirmButtonText={props.translate('newChatPage.createGroup')}
onConfirmSelection={createGroup}
textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
/>
</View>
</>
)}
</ScreenWrapper>
);
}

NewChatPage.propTypes = propTypes;
NewChatPage.defaultProps = defaultProps;
NewChatPage.displayName = 'NewChatPage';

export default compose(
withLocalize,
Expand Down