diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d108773ac0bd..ae924faa6fe3 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -375,7 +375,7 @@ type OnyxValues = { [ONYXKEYS.NETWORK]: OnyxTypes.Network; [ONYXKEYS.CUSTOM_STATUS_DRAFT]: OnyxTypes.CustomStatusDraft; [ONYXKEYS.INPUT_FOCUSED]: boolean; - [ONYXKEYS.PERSONAL_DETAILS_LIST]: OnyxTypes.PersonalDetailsList; + [ONYXKEYS.PERSONAL_DETAILS_LIST]: Record; [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: OnyxTypes.PrivatePersonalDetails; [ONYXKEYS.TASK]: OnyxTypes.Task; [ONYXKEYS.CURRENCY_LIST]: Record; diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 8604d20130c7..7dadd86debfe 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -30,14 +30,14 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} const originalMessage = reportClosedAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? reportClosedAction.originalMessage : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[report?.ownerAccountID ?? 0]?.displayName); + let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [report.ownerAccountID, 'displayName']); let oldDisplayName: string | undefined; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { const newAccountID = originalMessage?.newAccountID; const oldAccountID = originalMessage?.oldAccountID; - displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[newAccountID ?? 0]?.displayName); - oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[oldAccountID ?? 0]?.displayName); + displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [newAccountID, 'displayName']); + oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [oldAccountID, 'displayName']); } const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT; diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 289e2254952f..a97067c32c72 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -37,7 +37,7 @@ export default function (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}) as CurrentUserPersonalDetails, + () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}), [accountPersonalDetails, accountID], ); return ( diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 943bd4aa2c16..a807bdbe6a0f 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -519,7 +519,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) || CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(lastActorDetails, 'displayName')), + displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), policyName: ReportUtils.getPolicyName(report), }); } diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js new file mode 100644 index 000000000000..b5335eab0762 --- /dev/null +++ b/src/libs/PersonalDetailsUtils.js @@ -0,0 +1,222 @@ +import lodashGet from 'lodash/get'; +import Onyx from 'react-native-onyx'; +import _ from 'underscore'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as LocalePhoneNumber from './LocalePhoneNumber'; +import * as Localize from './Localize'; +import * as UserUtils from './UserUtils'; + +let personalDetails = []; +let allPersonalDetails = {}; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (val) => { + personalDetails = _.values(val); + allPersonalDetails = val; + }, +}); + +/** + * @param {Object | Null} passedPersonalDetails + * @param {Array | String} pathToDisplayName + * @param {String} [defaultValue] optional default display name value + * @returns {String} + */ +function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue = '') { + const displayName = lodashGet(passedPersonalDetails, pathToDisplayName); + + return displayName || defaultValue || Localize.translateLocal('common.hidden'); +} + +/** + * Given a list of account IDs (as number) it will return an array of personal details objects. + * @param {Array} accountIDs - Array of accountIDs + * @param {Number} currentUserAccountID + * @param {Boolean} shouldChangeUserDisplayName - It will replace the current user's personal detail object's displayName with 'You'. + * @returns {Array} - Array of personal detail objects + */ +function getPersonalDetailsByIDs(accountIDs, currentUserAccountID, shouldChangeUserDisplayName = false) { + return _.chain(accountIDs) + .filter((accountID) => !!allPersonalDetails[accountID]) + .map((accountID) => { + const detail = allPersonalDetails[accountID]; + + if (shouldChangeUserDisplayName && currentUserAccountID === detail.accountID) { + return { + ...detail, + displayName: Localize.translateLocal('common.you'), + }; + } + + return detail; + }) + .value(); +} + +/** + * Given a list of logins, find the associated personal detail and return related accountIDs. + * + * @param {Array} logins Array of user logins + * @returns {Array} - Array of accountIDs according to passed logins + */ +function getAccountIDsByLogins(logins) { + return _.reduce( + logins, + (foundAccountIDs, login) => { + const currentDetail = _.find(personalDetails, (detail) => detail.login === login); + if (!currentDetail) { + // generate an account ID because in this case the detail is probably new, so we don't have a real accountID yet + foundAccountIDs.push(UserUtils.generateAccountID(login)); + } else { + foundAccountIDs.push(Number(currentDetail.accountID)); + } + return foundAccountIDs; + }, + [], + ); +} + +/** + * Given a list of accountIDs, find the associated personal detail and return related logins. + * + * @param {Array} accountIDs Array of user accountIDs + * @returns {Array} - Array of logins according to passed accountIDs + */ +function getLoginsByAccountIDs(accountIDs) { + return _.reduce( + accountIDs, + (foundLogins, accountID) => { + const currentDetail = _.find(personalDetails, (detail) => Number(detail.accountID) === Number(accountID)) || {}; + if (currentDetail.login) { + foundLogins.push(currentDetail.login); + } + return foundLogins; + }, + [], + ); +} + +/** + * Given a list of logins and accountIDs, return Onyx data for users with no existing personal details stored + * + * @param {Array} logins Array of user logins + * @param {Array} accountIDs Array of user accountIDs + * @returns {Object} - Object with optimisticData, successData and failureData (object of personal details objects) + */ +function getNewPersonalDetailsOnyxData(logins, accountIDs) { + const optimisticData = {}; + const successData = {}; + const failureData = {}; + + _.each(logins, (login, index) => { + const accountID = accountIDs[index]; + + if (_.isEmpty(allPersonalDetails[accountID])) { + optimisticData[accountID] = { + login, + accountID, + avatar: UserUtils.getDefaultAvatarURL(accountID), + displayName: LocalePhoneNumber.formatPhoneNumber(login), + }; + + /** + * Cleanup the optimistic user to ensure it does not permanently persist. + * This is done to prevent duplicate entries (upon success) since the BE will return other personal details with the correct account IDs. + */ + successData[accountID] = null; + } + }); + + return { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: optimisticData, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: successData, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: failureData, + }, + ], + }; +} + +/** + * Applies common formatting to each piece of an address + * + * @param {String} piece - address piece to format + * @returns {String} - formatted piece + */ +function formatPiece(piece) { + return piece ? `${piece}, ` : ''; +} + +/** + * + * @param {String} street1 - street line 1 + * @param {String} street2 - street line 2 + * @returns {String} formatted street + */ +function getFormattedStreet(street1 = '', street2 = '') { + return `${street1}\n${street2}`; +} + +/** + * + * @param {*} street - formatted address + * @returns {[string, string]} [street1, street2] + */ +function getStreetLines(street = '') { + const streets = street.split('\n'); + return [streets[0], streets[1]]; +} + +/** + * Formats an address object into an easily readable string + * + * @param {OnyxTypes.PrivatePersonalDetails} privatePersonalDetails - details object + * @returns {String} - formatted address + */ +function getFormattedAddress(privatePersonalDetails) { + const {address} = privatePersonalDetails; + const [street1, street2] = getStreetLines(address.street); + const formattedAddress = formatPiece(street1) + formatPiece(street2) + formatPiece(address.city) + formatPiece(address.state) + formatPiece(address.zip) + formatPiece(address.country); + + // Remove the last comma of the address + return formattedAddress.trim().replace(/,$/, ''); +} + +/** + * @param {Object} personalDetail - details object + * @returns {String | undefined} - The effective display name + */ +function getEffectiveDisplayName(personalDetail) { + if (personalDetail) { + return LocalePhoneNumber.formatPhoneNumber(personalDetail.login) || personalDetail.displayName; + } + + return undefined; +} + +export { + getDisplayNameOrDefault, + getPersonalDetailsByIDs, + getAccountIDsByLogins, + getLoginsByAccountIDs, + getNewPersonalDetailsOnyxData, + getFormattedAddress, + getFormattedStreet, + getStreetLines, + getEffectiveDisplayName, +}; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts deleted file mode 100644 index 8bb4ac0aea3e..000000000000 --- a/src/libs/PersonalDetailsUtils.ts +++ /dev/null @@ -1,211 +0,0 @@ -import Onyx, {OnyxEntry} from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as OnyxTypes from '@src/types/onyx'; -import {PersonalDetails, PersonalDetailsList} from '@src/types/onyx'; -import * as LocalePhoneNumber from './LocalePhoneNumber'; -import * as Localize from './Localize'; -import * as UserUtils from './UserUtils'; - -let personalDetails: Array = []; -let allPersonalDetails: OnyxEntry = {}; -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - personalDetails = Object.values(val ?? {}); - allPersonalDetails = val; - }, -}); - -/** - * @param [defaultValue] optional default display name value - */ -function getDisplayNameOrDefault(displayName?: string, defaultValue = ''): string { - return displayName ?? defaultValue ?? Localize.translateLocal('common.hidden'); -} - -/** - * Given a list of account IDs (as number) it will return an array of personal details objects. - * @param accountIDs - Array of accountIDs - * @param currentUserAccountID - * @param shouldChangeUserDisplayName - It will replace the current user's personal detail object's displayName with 'You'. - * @returns - Array of personal detail objects - */ -function getPersonalDetailsByIDs(accountIDs: number[], currentUserAccountID: number, shouldChangeUserDisplayName = false): OnyxTypes.PersonalDetails[] { - const result: OnyxTypes.PersonalDetails[] = accountIDs - .filter((accountID) => !!allPersonalDetails?.[accountID]) - .map((accountID) => { - const detail = (allPersonalDetails?.[accountID] ?? {}) as OnyxTypes.PersonalDetails; - - if (shouldChangeUserDisplayName && currentUserAccountID === detail.accountID) { - return { - ...detail, - displayName: Localize.translateLocal('common.you'), - }; - } - - return detail; - }); - - return result; -} - -/** - * Given a list of logins, find the associated personal detail and return related accountIDs. - * - * @param logins Array of user logins - * @returns Array of accountIDs according to passed logins - */ -function getAccountIDsByLogins(logins: string[]): number[] { - return logins.reduce((foundAccountIDs, login) => { - const currentDetail = personalDetails.find((detail) => detail?.login === login); - if (!currentDetail) { - // generate an account ID because in this case the detail is probably new, so we don't have a real accountID yet - foundAccountIDs.push(UserUtils.generateAccountID(login)); - } else { - foundAccountIDs.push(Number(currentDetail.accountID)); - } - return foundAccountIDs; - }, []); -} - -/** - * Given a list of accountIDs, find the associated personal detail and return related logins. - * - * @param accountIDs Array of user accountIDs - * @returns Array of logins according to passed accountIDs - */ -function getLoginsByAccountIDs(accountIDs: number[]): string[] { - return accountIDs.reduce((foundLogins: string[], accountID) => { - const currentDetail: Partial = personalDetails.find((detail) => Number(detail?.accountID) === Number(accountID)) ?? {}; - if (currentDetail.login) { - foundLogins.push(currentDetail.login); - } - return foundLogins; - }, []); -} - -/** - * Given a list of logins and accountIDs, return Onyx data for users with no existing personal details stored - * - * @param logins Array of user logins - * @param accountIDs Array of user accountIDs - * @returns Object with optimisticData, successData and failureData (object of personal details objects) - */ -function getNewPersonalDetailsOnyxData(logins: string[], accountIDs: number[]) { - const optimisticData: PersonalDetailsList = {}; - const successData: PersonalDetailsList = {}; - const failureData: PersonalDetailsList = {}; - - logins.forEach((login, index) => { - const accountID = accountIDs[index]; - - if (allPersonalDetails && Object.keys(allPersonalDetails?.[accountID] ?? {}).length === 0) { - optimisticData[accountID] = { - login, - accountID, - avatar: UserUtils.getDefaultAvatarURL(accountID), - displayName: LocalePhoneNumber.formatPhoneNumber(login), - }; - - /** - * Cleanup the optimistic user to ensure it does not permanently persist. - * This is done to prevent duplicate entries (upon success) since the BE will return other personal details with the correct account IDs. - */ - successData[accountID] = null; - } - }); - - return { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: optimisticData, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: successData, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: failureData, - }, - ], - }; -} - -/** - * Applies common formatting to each piece of an address - * - * @param piece - address piece to format - * @returns - formatted piece - */ -function formatPiece(piece?: string): string { - return piece ? `${piece}, ` : ''; -} - -/** - * - * @param street1 - street line 1 - * @param street2 - street line 2 - * @returns formatted street - */ -function getFormattedStreet(street1 = '', street2 = '') { - return `${street1}\n${street2}`; -} - -/** - * - * @param - formatted address - * @returns [street1, street2] - */ -function getStreetLines(street = '') { - const streets = street.split('\n'); - return [streets[0], streets[1]]; -} - -/** - * Formats an address object into an easily readable string - * - * @param privatePersonalDetails - details object - * @returns - formatted address - */ -function getFormattedAddress(privatePersonalDetails: OnyxTypes.PrivatePersonalDetails): string { - const {address} = privatePersonalDetails; - const [street1, street2] = getStreetLines(address?.street); - const formattedAddress = - formatPiece(street1) + formatPiece(street2) + formatPiece(address?.city) + formatPiece(address?.state) + formatPiece(address?.zip) + formatPiece(address?.country); - - // Remove the last comma of the address - return formattedAddress.trim().replace(/,$/, ''); -} - -/** - * @param personalDetail - details object - * @returns - The effective display name - */ -function getEffectiveDisplayName(personalDetail?: PersonalDetails): string | undefined { - if (personalDetail) { - return LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') || personalDetail.displayName; - } - - return undefined; -} - -export { - getDisplayNameOrDefault, - getPersonalDetailsByIDs, - getAccountIDsByLogins, - getLoginsByAccountIDs, - getNewPersonalDetailsOnyxData, - getFormattedAddress, - getFormattedStreet, - getStreetLines, - getEffectiveDisplayName, -}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 55ceb4bba49e..2a08e5e9a4fb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1443,12 +1443,12 @@ function getDisplayNamesWithTooltips( return personalDetailsListArray .map((user) => { - const accountID = Number(user?.accountID); + const accountID = Number(user.accountID); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden) || user?.login || ''; + const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden) || user.login || ''; const avatar = UserUtils.getDefaultAvatar(accountID); - let pronouns = user?.pronouns ?? undefined; + let pronouns = user.pronouns; if (pronouns?.startsWith(CONST.PRONOUNS.PREFIX)) { const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}` as TranslationPaths); @@ -1457,7 +1457,7 @@ function getDisplayNamesWithTooltips( return { displayName, avatar, - login: user?.login ?? '', + login: user.login ?? '', accountID, pronouns, }; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 4744426ecfd3..1813d4f0a795 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -344,7 +344,7 @@ function getOptionData( case CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED: { lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { policyName: ReportUtils.getPolicyName(report, false, policy), - displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails?.displayName), + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), }); break; } diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 96339e3abe1b..ba19002345e7 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -7,7 +7,7 @@ import * as defaultAvatars from '@components/Icon/DefaultAvatars'; import {ConciergeAvatar, FallbackAvatar} from '@components/Icon/Expensicons'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetailsList} from '@src/types/onyx'; +import {PersonalDetails} from '@src/types/onyx'; import Login from '@src/types/onyx/Login'; import hashCode from './hashCode'; @@ -17,7 +17,7 @@ type AvatarSource = React.FC | string; type LoginListIndicator = ValueOf | ''; -let allPersonalDetails: OnyxEntry; +let allPersonalDetails: OnyxEntry>; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = _.isEmpty(val) ? {} : val), diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index e04ffbb352fc..4ec31b29f914 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -60,7 +60,7 @@ const getAllParticipants = (report, personalDetails, translate) => .map((accountID, index) => { const userPersonalDetail = lodashGet(personalDetails, accountID, {displayName: personalDetails.displayName || translate('common.hidden'), avatar: ''}); const userLogin = LocalePhoneNumber.formatPhoneNumber(userPersonalDetail.login || '') || translate('common.hidden'); - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(userPersonalDetail.displayName); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(userPersonalDetail, 'displayName'); return { alternateText: userLogin, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index d37f84e0c908..92e0e60acd1b 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -373,7 +373,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, [props.report.ownerAccountID, 'displayName'])); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [props.report.ownerAccountID, 'displayName']); const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 9f613cbf4f1e..bd2599fee0ca 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -73,7 +73,7 @@ type PersonalDetails = { status?: string; }; -type PersonalDetailsList = Record; +type PersonalDetailsList = Record; export default PersonalDetails; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 229fd0a53158..24dc595dafcc 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -60,6 +60,7 @@ import WalletTransfer from './WalletTransfer'; export type { Account, + UserLocation, AccountData, AddDebitCardForm, BankAccount, @@ -91,16 +92,16 @@ export type { PersonalDetailsList, PlaidData, Policy, - PolicyCategories, PolicyCategory, + PolicyCategories, PolicyMember, PolicyMembers, PolicyTag, PolicyTags, PrivatePersonalDetails, - RecentWaypoint, RecentlyUsedCategories, RecentlyUsedTags, + RecentWaypoint, ReimbursementAccount, ReimbursementAccountDraft, Report, @@ -119,7 +120,6 @@ export type { Transaction, TransactionViolation, User, - UserLocation, UserWallet, ViolationName, WalletAdditionalDetails,