Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion src/components/ReportWelcomeText.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const ReportWelcomeText = (props) => {
{roomWelcomeMessage.phrase1}
</Text>
<Text style={[styles.textStrong]} onPress={() => Navigation.navigate(ROUTES.getReportDetailsRoute(props.report.reportID))}>
{ReportUtils.getReportName(props.report, props.personalDetails, props.policies)}
{ReportUtils.getReportName(props.report, props.policies)}
</Text>
<Text>
{roomWelcomeMessage.phrase2}
Expand Down
4 changes: 3 additions & 1 deletion src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, {
const personalDetail = personalDetailList[0] || {};
let hasMultipleParticipants = personalDetailList.length > 1;
let subtitle;
let reportName;

result.participantsList = personalDetailList;

Expand Down Expand Up @@ -349,7 +350,9 @@ function createOption(logins, personalDetails, report, reportActions = {}, {
? lastMessageText
: Str.removeSMSDomain(personalDetail.login);
}
reportName = ReportUtils.getReportName(report, policies);
} else {
reportName = ReportUtils.getDisplayNameForParticipant(logins[0]);

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.

Just wondering, why use logins[0] here instead of personalDetail.login like the following 2 lines?

result.keyForList = personalDetail.login;
result.alternateText = Str.removeSMSDomain(personalDetail.login);
}
Expand All @@ -368,7 +371,6 @@ function createOption(logins, personalDetails, report, reportActions = {}, {
result.payPalMeAddress = personalDetail.payPalMeAddress;
}

const reportName = ReportUtils.getReportName(report, personalDetailMap, policies);
result.text = reportName;
result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom || result.isPolicyExpenseChat);
result.icons = ReportUtils.getIcons(report, personalDetails, policies, personalDetail.avatar);
Expand Down
53 changes: 33 additions & 20 deletions src/libs/ReportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,27 +465,42 @@ function getIcons(report, personalDetails, policies, defaultIcon = null) {
return _.map(sortedParticipants, item => item.avatar);
}

/**
* Gets the personal details for a login by looking in the ONYXKEYS.PERSONAL_DETAILS Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx,
* then a default object is constructed.
* @param {String} login
* @returns {Object}
*/
function getPersonalDetailsForLogin(login) {
if (!login) {
return {};
}
return (allPersonalDetails && allPersonalDetails[login]) || {
login,
displayName: Str.removeSMSDomain(login),
avatar: getDefaultAvatar(login),
};
Comment thread
Beamanator marked this conversation as resolved.
}

/**
* Get the displayName for a single report participant.
*
* @param {Object} participant
* @param {String} participant.displayName
* @param {String} participant.firstName
* @param {String} participant.login
* @param {String} login
* @param {Boolean} [shouldUseShortForm]
* @returns {String}
*/
function getDisplayNameForParticipant(participant, shouldUseShortForm = false) {
if (!participant) {
function getDisplayNameForParticipant(login, shouldUseShortForm = false) {
if (!login) {
return '';
}
const personalDetails = getPersonalDetailsForLogin(login);

const loginWithoutSMSDomain = Str.removeSMSDomain(participant.login);
let longName = participant.displayName || loginWithoutSMSDomain;
const loginWithoutSMSDomain = Str.removeSMSDomain(personalDetails.login);
let longName = personalDetails.displayName || loginWithoutSMSDomain;
if (longName === loginWithoutSMSDomain && Str.isSMSLogin(longName)) {
longName = LocalePhoneNumber.toLocalPhone(preferredLocale, longName);
}
const shortName = participant.firstName || longName;
const shortName = personalDetails.firstName || longName;

return shouldUseShortForm ? shortName : longName;
}
Expand All @@ -497,7 +512,7 @@ function getDisplayNameForParticipant(participant, shouldUseShortForm = false) {
*/
function getDisplayNamesWithTooltips(participants, isMultipleParticipantReport) {
return _.map(participants, (participant) => {
const displayName = getDisplayNameForParticipant(participant, isMultipleParticipantReport);
const displayName = getDisplayNameForParticipant(participant.login, isMultipleParticipantReport);
const tooltip = Str.removeSMSDomain(participant.login);

let pronouns = participant.pronouns;
Expand All @@ -518,19 +533,17 @@ function getDisplayNamesWithTooltips(participants, isMultipleParticipantReport)
* Get the title for a report.
*
* @param {Object} report
* @param {Object} [personalDetailsForParticipants]
* @param {Object} [policies]
* @returns {String}
*/
function getReportName(report, personalDetailsForParticipants = {}, policies = {}) {
function getReportName(report, policies = {}) {
let formattedName;
if (isChatRoom(report)) {
formattedName = report.reportName;
}

if (isPolicyExpenseChat(report)) {
const reportOwnerPersonalDetails = personalDetailsForParticipants[report.ownerEmail];
const reportOwnerDisplayName = getDisplayNameForParticipant(reportOwnerPersonalDetails) || report.ownerEmail || report.reportName;
const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerEmail) || report.ownerEmail || report.reportName;
formattedName = report.isOwnPolicyExpenseChat ? getPolicyName(report, policies) : reportOwnerDisplayName;
}

Expand All @@ -543,10 +556,10 @@ function getReportName(report, personalDetailsForParticipants = {}, policies = {
}

// Not a room or PolicyExpenseChat, generate title from participants
const participants = _.without(lodashGet(report, 'participants', []), sessionEmail);
const isMultipleParticipantReport = participants.length > 1;
const participantsToGetTheNamesOf = _.isEmpty(personalDetailsForParticipants) ? participants : personalDetailsForParticipants;
return _.map(participantsToGetTheNamesOf, participant => getDisplayNameForParticipant(participant, isMultipleParticipantReport)).join(', ');
const participants = (report && report.participants) || [];
const participantsWithoutCurrentUser = _.without(participants, sessionEmail);
const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1;
return _.map(participantsWithoutCurrentUser, login => getDisplayNameForParticipant(login, isMultipleParticipantReport)).join(', ');
}

/**
Expand Down Expand Up @@ -690,7 +703,7 @@ function buildOptimisticIOUReport(ownerEmail, userEmail, total, chatReportID, cu
function getIOUReportActionMessage(type, total, participants, comment, currency) {
const amount = NumberFormatUtils.format(preferredLocale, total / 100, {style: 'currency', currency});
const isMultipleParticipantReport = participants.length > 1;
const displayNames = _.map(participants, participant => getDisplayNameForParticipant(allPersonalDetails[participant.login], isMultipleParticipantReport) || participant.login);
const displayNames = _.map(participants, participant => getDisplayNameForParticipant(participant.login, isMultipleParticipantReport) || participant.login);
const from = displayNames.length < 3
? displayNames.join(' and ')
: `${displayNames.slice(0, -1).join(', ')}, and ${_.last(displayNames)}`;
Expand Down Expand Up @@ -1083,7 +1096,6 @@ export {
isPolicyExpenseChat,
getDefaultAvatar,
getIcons,
getDisplayNameForParticipant,
getRoomWelcomeMessage,
getDisplayNamesWithTooltips,
getReportName,
Expand All @@ -1100,4 +1112,5 @@ export {
shouldReportBeInOptionList,
getChatByParticipants,
getIOUReportActionMessage,
getDisplayNameForParticipant,
};
13 changes: 7 additions & 6 deletions src/libs/SidebarUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ function getOrderedReportIDs(reportIDFromRoute) {
// Get all the display names for our reports in an easy to access property so we don't have to keep
// re-running the logic
const filteredReportsWithReportName = _.map(filteredReports, (report) => {
const personalDetailMap = OptionsListUtils.getPersonalDetailsForLogins(report.participants, personalDetails);
return {
...report,
reportDisplayName: ReportUtils.getReportName(report, personalDetailMap, policies),
};
// Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params.
// However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add
// the reportDisplayName property to the report object directly.
// eslint-disable-next-line no-param-reassign
Comment on lines +103 to +106

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.

Nifty 👍 I can't think of any downside to this, worst case is reportDisplayName stays on the report object even when it's not needed, but I can't see how that would cause any problems 👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here we are 😁 This tricked us that we were using report.reportDisplayName (or report.displayName) across the App thinking it's a natural prop. This backfired once we started passing cloned reports to this function. Cloned reports are due to the use of withOnyx selector #21406

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 strongly disagree with allowing mutations into our code, it creates hard to debug bugs and is hard to track the states of objects.

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.

Now there is a second property getting mutated below this one:

// eslint-disable-next-line no-param-reassign
report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReportsDict);

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'm fine with finding a way to remove the mutations as long as the code remains performant.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not totally sure but I think we can leave the mutation now since we are mutating a clone. I don't see much benefit in cloning a clone.

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 guess that makes it a bit better because we are only mutating the copy we pass down this tree, right?

const reportActionsSelector = (reportActions) =>
reportActions &&
_.map(reportActions, (reportAction) => ({
errors: lodashGet(reportAction, 'errors', []),
message: [
{
moderationDecision: {decision: lodashGet(reportAction, 'message[0].moderationDecision.decision')},
},
],
}));

Still a bad practice in react to mutate props in general, you have to follow up from where the prop is coming to verify that this is not causing more side effects and it is also hard to assure that things are getting rendered when they should down the line.

I'm fine with finding a way to remove the mutations as long as the code remains performant.

That is fair, do you know how to reproduce the performance issues?

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.

The best way to ensure it doesn't regress on performance is to add some timers around this code, and then time it before/after making the change.

report.reportDisplayName = ReportUtils.getReportName(report, policies);
return report;
});

// Sorting the reports works like this:
Expand Down Expand Up @@ -296,7 +297,7 @@ function getOptionData(reportID) {
result.payPalMeAddress = personalDetail.payPalMeAddress;
}

const reportName = ReportUtils.getReportName(report, personalDetailMap, policies);
const reportName = ReportUtils.getReportName(report, policies);
result.text = reportName;
result.subtitle = subtitle;
result.participantsList = personalDetailList;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ReportDetailsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class ReportDetailsPage extends Component {
<View style={[styles.reportDetailsRoomInfo, styles.mw100]}>
<View style={[styles.alignSelfCenter, styles.w100]}>
<DisplayNames
fullTitle={ReportUtils.getReportName(this.props.report, this.props.personalDetails, this.props.policies)}
fullTitle={ReportUtils.getReportName(this.props.report, this.props.policies)}
displayNamesWithTooltips={displayNamesWithTooltips}
tooltipEnabled
numberOfLines={1}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/home/HeaderView.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const HeaderView = (props) => {
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant);
const isChatRoom = ReportUtils.isChatRoom(props.report);
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report);
const title = ReportUtils.getReportName(props.report, participantPersonalDetails, props.policies);
const title = ReportUtils.getReportName(props.report, props.policies);

const subtitle = ReportUtils.getChatRoomSubtitle(props.report, props.policies);
const isConcierge = participants.length === 1 && _.contains(participants, CONST.EMAIL.CONCIERGE);
Expand Down
23 changes: 14 additions & 9 deletions tests/unit/OptionsListUtilsTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ describe('OptionsListUtils', () => {
},
});
Onyx.registerLogger(() => {});
return waitForPromisesToResolve();
return waitForPromisesToResolve()
.then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS, PERSONAL_DETAILS));
});

it('getSearchOptions()', () => {
Expand All @@ -299,14 +300,18 @@ describe('OptionsListUtils', () => {
// Value with latest lastMessageTimestamp should be at the top.
expect(results.recentReports.length).toBe(2);
expect(results.recentReports[0].text).toBe('Mister Fantastic');
expect(results.recentReports[1].text).toBe('Iron Man, Mister Fantastic');

// When we filter again but provide a searchValue that should match with periods
results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS_WITH_PERIODS, 'barryallen@expensify.com');

// Then we expect to have the personal detail with period filtered
expect(results.recentReports.length).toBe(1);
expect(results.recentReports[0].text).toBe('The Flash');
expect(results.recentReports[1].text).toBe('Mister Fantastic');

return waitForPromisesToResolve()
.then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS, PERSONAL_DETAILS_WITH_PERIODS))
.then(() => {
// When we filter again but provide a searchValue that should match with periods
results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS_WITH_PERIODS, 'barryallen@expensify.com');

// Then we expect to have the personal detail with period filtered
expect(results.recentReports.length).toBe(1);
expect(results.recentReports[0].text).toBe('The Flash');
});
});

it('getNewChatOptions()', () => {
Expand Down
25 changes: 13 additions & 12 deletions tests/unit/ReportUtilsTest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import _ from 'underscore';
import Onyx from 'react-native-onyx';
import CONST from '../../src/CONST';
import ONYXKEYS from '../../src/ONYXKEYS';
Expand Down Expand Up @@ -36,7 +35,9 @@ const policies = {

Onyx.init({keys: ONYXKEYS});

beforeAll(() => Onyx.set(ONYXKEYS.SESSION, {email: currentUserEmail}));
beforeAll(() => waitForPromisesToResolve()
.then(() => Onyx.set(ONYXKEYS.PERSONAL_DETAILS, participantsPersonalDetails))
.then(() => Onyx.set(ONYXKEYS.SESSION, {email: currentUserEmail})));
beforeEach(() => Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.DEFAULT_LOCALE).then(waitForPromisesToResolve));

describe('ReportUtils', () => {
Expand Down Expand Up @@ -97,26 +98,26 @@ describe('ReportUtils', () => {
test('with displayName', () => {
expect(ReportUtils.getReportName({
participants: [currentUserEmail, 'ragnar@vikings.net'],
}, _.pick(participantsPersonalDetails, 'ragnar@vikings.net'))).toBe('Ragnar Lothbrok');
})).toBe('Ragnar Lothbrok');
});

test('no displayName', () => {
expect(ReportUtils.getReportName({
participants: [currentUserEmail, 'floki@vikings.net'],
}, _.pick(participantsPersonalDetails, 'floki@vikings.net'))).toBe('floki@vikings.net');
})).toBe('floki@vikings.net');
});

test('SMS', () => {
expect(ReportUtils.getReportName({
participants: [currentUserEmail, '+12223334444@expensify.sms'],
}, _.pick(participantsPersonalDetails, '+12223334444@expensify.sms'))).toBe('2223334444');
})).toBe('2223334444');
});
});

test('Group DM', () => {
expect(ReportUtils.getReportName({
participants: [currentUserEmail, 'ragnar@vikings.net', 'floki@vikings.net', 'lagertha@vikings.net', '+12223334444@expensify.sms'],
}, participantsPersonalDetails)).toBe('Ragnar, floki@vikings.net, Lagertha, 2223334444');
})).toBe('Ragnar, floki@vikings.net, Lagertha, 2223334444');
});

describe('Default Policy Room', () => {
Expand Down Expand Up @@ -175,7 +176,7 @@ describe('ReportUtils', () => {
policyID: policy.policyID,
isOwnPolicyExpenseChat: true,
ownerEmail: 'ragnar@vikings.net',
}, participantsPersonalDetails, policies)).toBe('Vikings Policy');
}, policies)).toBe('Vikings Policy');
});

test('as admin', () => {
Expand All @@ -184,7 +185,7 @@ describe('ReportUtils', () => {
policyID: policy.policyID,
isOwnPolicyExpenseChat: false,
ownerEmail: 'ragnar@vikings.net',
}, participantsPersonalDetails, policies)).toBe('Ragnar Lothbrok');
}, policies)).toBe('Ragnar Lothbrok');
});
});

Expand All @@ -204,10 +205,10 @@ describe('ReportUtils', () => {
isOwnPolicyExpenseChat: true,
};

expect(ReportUtils.getReportName(memberArchivedPolicyExpenseChat, {}, policies)).toBe('Vikings Policy (archived)');
expect(ReportUtils.getReportName(memberArchivedPolicyExpenseChat, policies)).toBe('Vikings Policy (archived)');

return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, 'es')
.then(() => expect(ReportUtils.getReportName(memberArchivedPolicyExpenseChat, {}, policies)).toBe('Vikings Policy (archivado)'));
.then(() => expect(ReportUtils.getReportName(memberArchivedPolicyExpenseChat, policies)).toBe('Vikings Policy (archivado)'));
});

test('as admin', () => {
Expand All @@ -216,10 +217,10 @@ describe('ReportUtils', () => {
isOwnPolicyExpenseChat: false,
};

expect(ReportUtils.getReportName(adminArchivedPolicyExpenseChat, participantsPersonalDetails)).toBe('Ragnar Lothbrok (archived)');
expect(ReportUtils.getReportName(adminArchivedPolicyExpenseChat)).toBe('Ragnar Lothbrok (archived)');

return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, 'es')
.then(() => expect(ReportUtils.getReportName(adminArchivedPolicyExpenseChat, participantsPersonalDetails)).toBe('Ragnar Lothbrok (archivado)'));
.then(() => expect(ReportUtils.getReportName(adminArchivedPolicyExpenseChat)).toBe('Ragnar Lothbrok (archivado)'));
});
});
});
Expand Down