diff --git a/src/languages/types.ts b/src/languages/types.ts index e2af3222a98f..a012ebdfb95b 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -111,17 +111,17 @@ type DidSplitAmountMessageParams = {formattedAmount: string; comment: string}; type AmountEachParams = {amount: number}; -type PayerOwesAmountParams = {payer: string; amount: number}; +type PayerOwesAmountParams = {payer: string; amount: number | string}; type PayerOwesParams = {payer: string}; -type PayerPaidAmountParams = {payer: string; amount: number}; +type PayerPaidAmountParams = {payer: string; amount: number | string}; type ManagerApprovedParams = {manager: string}; type PayerPaidParams = {payer: string}; -type PayerSettledParams = {amount: number}; +type PayerSettledParams = {amount: number | string}; type WaitingOnBankAccountParams = {submitterDisplayName: string}; diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts index dcb2b13f092c..db64f6574824 100644 --- a/src/libs/GroupChatUtils.ts +++ b/src/libs/GroupChatUtils.ts @@ -13,10 +13,11 @@ Onyx.connect({ /** * Returns the report name if the report is a group chat */ -function getGroupChatName(report: Report): string { +function getGroupChatName(report: Report): string | undefined { const participants = report.participantAccountIDs ?? []; const isMultipleParticipantReport = participants.length > 1; const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, allPersonalDetails ?? {}); + // @ts-expect-error Error will gone when OptionsListUtils will be migrated to Typescript const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipantReport); return ReportUtils.getDisplayNamesStringFromTooltips(displayNamesWithTooltips); } diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index ff4f2aafc8a8..afbbcc2684a0 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -1,3 +1,4 @@ +import {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import {Report, Transaction} from '@src/types/onyx'; import * as CurrencyUtils from './CurrencyUtils'; @@ -35,8 +36,8 @@ function calculateAmount(numberOfParticipants: number, total: number, currency: * * @param isDeleting - whether the user is deleting the request */ -function updateIOUOwnerAndTotal(iouReport: Report, actorAccountID: number, amount: number, currency: string, isDeleting = false): Report { - if (currency !== iouReport.currency) { +function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: number, amount: number, currency: string, isDeleting = false): OnyxEntry { + if (currency !== iouReport?.currency) { return iouReport; } diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index c3e01735fb07..43e7ef9fbbc8 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -1,20 +1,21 @@ +import {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import Beta from '@src/types/onyx/Beta'; -function canUseAllBetas(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.ALL); +function canUseAllBetas(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.ALL); } -function canUseChronos(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.CHRONOS_IN_CASH) || canUseAllBetas(betas); +function canUseChronos(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.CHRONOS_IN_CASH) || canUseAllBetas(betas); } -function canUseDefaultRooms(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas); +function canUseDefaultRooms(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas); } -function canUseCommentLinking(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas); +function canUseCommentLinking(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas); } /** @@ -22,12 +23,12 @@ function canUseCommentLinking(betas: Beta[]): boolean { * since contributors have been reporting a number of false issues related to the feature being under development. * See https://expensify.slack.com/archives/C01GTK53T8Q/p1641921996319400?thread_ts=1641598356.166900&cid=C01GTK53T8Q */ -function canUsePolicyRooms(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.POLICY_ROOMS) || canUseAllBetas(betas); +function canUsePolicyRooms(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.POLICY_ROOMS) || canUseAllBetas(betas); } -function canUseViolations(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); +function canUseViolations(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); } /** diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 62640a11311a..19129959d016 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -2,7 +2,8 @@ import Str from 'expensify-common/lib/str'; import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '@src/types/onyx'; +import {PersonalDetails, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; +import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; type MemberEmailsToAccountIDs = Record; type PersonalDetailsList = Record; @@ -157,8 +158,8 @@ function getIneligibleInvitees(policyMembers: OnyxEntry, personal /** * Gets the tag from policy tags, defaults to the first if no key is provided. */ -function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags) { - if (Object.keys(policyTags ?? {})?.length === 0) { +function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags): PolicyTag | undefined | EmptyObject { + if (isEmptyObject(policyTags)) { return {}; } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 333d621167b7..bd475a57954e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -8,6 +8,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {ActionName} from '@src/types/onyx/OriginalMessage'; import Report from '@src/types/onyx/Report'; import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction'; +import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CollectionUtils from './CollectionUtils'; import * as Environment from './Environment/Environment'; import isReportMessageAttachment from './isReportMessageAttachment'; @@ -72,7 +73,10 @@ function isReversedTransaction(reportAction: OnyxEntry) { return (reportAction?.message?.[0]?.isReversedTransaction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -function isPendingRemove(reportAction: OnyxEntry): boolean { +function isPendingRemove(reportAction: OnyxEntry | EmptyObject): boolean { + if (isEmptyObject(reportAction)) { + return false; + } return reportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; } @@ -695,3 +699,5 @@ export { getFirstVisibleReportActionID, isChannelLogMemberAction, }; + +export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.ts similarity index 56% rename from src/libs/ReportUtils.js rename to src/libs/ReportUtils.ts index 467c1502dd5c..d93661778b83 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.ts @@ -1,16 +1,26 @@ -/* eslint-disable rulesdir/prefer-underscore-method */ import {format} from 'date-fns'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; +import lodashEscape from 'lodash/escape'; +import lodashFindLastIndex from 'lodash/findLastIndex'; import lodashIntersection from 'lodash/intersection'; -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import lodashIsEqual from 'lodash/isEqual'; +import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import {SvgProps} from 'react-native-svg'; +import {ValueOf} from 'type-fest'; import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import CONST from '@src/CONST'; +import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Transaction} from '@src/types/onyx'; +import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; +import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; +import {Message, ReportActions} from '@src/types/onyx/ReportAction'; +import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; +import DeepValueOf from '@src/types/utils/DeepValueOf'; +import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import isReportMessageAttachment from './isReportMessageAttachment'; @@ -22,66 +32,375 @@ import * as NumberUtils from './NumberUtils'; import Permissions from './Permissions'; import * as PolicyUtils from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; +import {LastVisibleMessage} from './ReportActionsUtils'; import * as TransactionUtils from './TransactionUtils'; import * as Url from './Url'; import * as UserUtils from './UserUtils'; -let currentUserEmail; -let currentUserAccountID; -let isAnonymousUser; +type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string}; + +type ExpenseOriginalMessage = { + oldComment?: string; + newComment?: string; + comment?: string; + merchant?: string; + oldCreated?: string; + created?: string; + oldMerchant?: string; + oldAmount?: number; + amount?: number; + oldCurrency?: string; + currency?: string; + category?: string; + oldCategory?: string; + tag?: string; + oldTag?: string; + billable?: string; + oldBillable?: string; +}; + +type Participant = { + accountID: number; + alternateText: string; + firstName: string; + icons: Icon[]; + keyForList: string; + lastName: string; + login: string; + phoneNumber: string; + searchText: string; + selected: boolean; + text: string; +}; + +type SpendBreakdown = { + nonReimbursableSpend: number; + reimbursableSpend: number; + totalDisplaySpend: number; +}; + +type ParticipantDetails = [number, string, UserUtils.AvatarSource, UserUtils.AvatarSource]; + +type ReportAndWorkspaceName = { + rootReportName: string; + workspaceName?: string; +}; + +type OptimisticReportAction = { + commentText: string; + reportAction: Partial; +}; + +type UpdateOptimisticParentReportAction = { + childVisibleActionCount: number; + childCommenterCount: number; + childLastVisibleActionCreated: string; + childOldestFourAccountIDs: string | undefined; +}; + +type OptimisticExpenseReport = Pick< + Report, + | 'reportID' + | 'chatReportID' + | 'policyID' + | 'type' + | 'ownerAccountID' + | 'hasOutstandingIOU' + | 'currency' + | 'reportName' + | 'state' + | 'stateNum' + | 'total' + | 'notificationPreference' + | 'parentReportID' +>; + +type OptimisticIOUReportAction = Pick< + ReportAction, + | 'actionName' + | 'actorAccountID' + | 'automatic' + | 'avatar' + | 'isAttachment' + | 'originalMessage' + | 'message' + | 'person' + | 'reportActionID' + | 'shouldShow' + | 'created' + | 'pendingAction' + | 'receipt' + | 'whisperedToAccountIDs' +>; + +type OptimisticReportPreview = Pick< + ReportAction, + | 'actionName' + | 'reportActionID' + | 'pendingAction' + | 'originalMessage' + | 'message' + | 'created' + | 'actorAccountID' + | 'childMoneyRequestCount' + | 'childLastMoneyRequestComment' + | 'childRecentReceiptTransactionIDs' + | 'whisperedToAccountIDs' +> & {reportID?: string; accountID?: number}; + +type UpdateReportPreview = Pick< + ReportAction, + 'created' | 'message' | 'childLastMoneyRequestComment' | 'childMoneyRequestCount' | 'childRecentReceiptTransactionIDs' | 'whisperedToAccountIDs' +>; + +type ReportRouteParams = { + reportID: string; + isSubReportPageRoute: boolean; +}; + +type ReportOfflinePendingActionAndErrors = { + addWorkspaceRoomOrChatPendingAction: PendingAction | undefined; + addWorkspaceRoomOrChatErrors: Record | null | undefined; +}; + +type OptimisticApprovedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' +>; + +type OptimisticSubmittedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' +>; + +type OptimisticEditedTaskReportAction = Pick< + ReportAction, + 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person' +>; + +type OptimisticClosedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'pendingAction' | 'person' | 'reportActionID' | 'shouldShow' +>; + +type OptimisticCreatedReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' +>; + +type OptimisticChatReport = Pick< + Report, + | 'type' + | 'chatType' + | 'hasOutstandingIOU' + | 'isOwnPolicyExpenseChat' + | 'isPinned' + | 'lastActorAccountID' + | 'lastMessageTranslationKey' + | 'lastMessageHtml' + | 'lastMessageText' + | 'lastReadTime' + | 'lastVisibleActionCreated' + | 'notificationPreference' + | 'oldPolicyName' + | 'ownerAccountID' + | 'parentReportActionID' + | 'parentReportID' + | 'participantAccountIDs' + | 'policyID' + | 'reportID' + | 'reportName' + | 'stateNum' + | 'statusNum' + | 'visibility' + | 'welcomeMessage' + | 'writeCapability' +>; + +type OptimisticTaskReportAction = Pick< + ReportAction, + | 'actionName' + | 'actorAccountID' + | 'automatic' + | 'avatar' + | 'created' + | 'isAttachment' + | 'message' + | 'originalMessage' + | 'person' + | 'pendingAction' + | 'reportActionID' + | 'shouldShow' + | 'isFirstItem' +>; + +type OptimisticWorkspaceChats = { + announceChatReportID: string; + announceChatData: OptimisticChatReport; + announceReportActionData: Record; + announceCreatedReportActionID: string; + adminsChatReportID: string; + adminsChatData: OptimisticChatReport; + adminsReportActionData: Record; + adminsCreatedReportActionID: string; + expenseChatReportID: string; + expenseChatData: OptimisticChatReport; + expenseReportActionData: Record; + expenseCreatedReportActionID: string; +}; + +type OptimisticModifiedExpenseReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'isAttachment' | 'message' | 'originalMessage' | 'person' | 'pendingAction' | 'reportActionID' | 'shouldShow' +> & {reportID?: string}; + +type OptimisticTaskReport = Pick< + Report, + | 'reportID' + | 'reportName' + | 'description' + | 'ownerAccountID' + | 'participantAccountIDs' + | 'managerID' + | 'type' + | 'parentReportID' + | 'policyID' + | 'stateNum' + | 'statusNum' + | 'notificationPreference' +>; + +type TransactionDetails = + | { + created: string; + amount: number; + currency: string; + merchant: string; + waypoints?: WaypointCollection; + comment: string; + category: string; + billable: boolean; + tag: string; + mccGroup?: ValueOf; + cardID: number; + originalAmount: number; + originalCurrency: string; + } + | undefined; + +type OptimisticIOUReport = Pick< + Report, + | 'cachedTotal' + | 'hasOutstandingIOU' + | 'type' + | 'chatReportID' + | 'currency' + | 'managerID' + | 'ownerAccountID' + | 'participantAccountIDs' + | 'reportID' + | 'state' + | 'stateNum' + | 'total' + | 'reportName' + | 'notificationPreference' + | 'parentReportID' + | 'statusNum' +>; +type DisplayNameWithTooltips = Array>; + +type OptionData = { + alternateText?: string | null; + pendingAction?: PendingAction | null; + allReportErrors?: Errors | null; + brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; + tooltipText?: string | null; + subtitle?: string | null; + login?: string | null; + accountID?: number | null; + status?: string | null; + phoneNumber?: string | null; + isUnread?: boolean | null; + isUnreadWithMention?: boolean | null; + hasDraftComment?: boolean | null; + keyForList?: string | null; + searchText?: string | null; + isIOUReportOwner?: boolean | null; + isArchivedRoom?: boolean | null; + shouldShowSubscript?: boolean | null; + isPolicyExpenseChat?: boolean | null; + isMoneyRequestReport?: boolean | null; + isExpenseRequest?: boolean | null; + isAllowedToComment?: boolean | null; + isThread?: boolean | null; + isTaskReport?: boolean | null; + parentReportAction?: ReportAction; + displayNamesWithTooltips?: DisplayNameWithTooltips | null; +} & Report; + +type OnyxDataTaskAssigneeChat = { + optimisticData: OnyxUpdate[]; + successData: OnyxUpdate[]; + failureData: OnyxUpdate[]; + optimisticAssigneeAddComment?: OptimisticReportAction; + optimisticChatCreatedReportAction?: OptimisticCreatedReportAction; +}; + +let currentUserEmail: string | undefined; +let currentUserAccountID: number | undefined; +let isAnonymousUser = false; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { + callback: (value) => { // When signed out, val is undefined - if (!val) { + if (!value) { return; } - currentUserEmail = val.email; - currentUserAccountID = val.accountID; - isAnonymousUser = val.authTokenType === 'anonymousAccount'; + currentUserEmail = value.email; + currentUserAccountID = value.accountID; + isAnonymousUser = value.authTokenType === 'anonymousAccount'; }, }); -let allPersonalDetails; -let currentUserPersonalDetails; +let allPersonalDetails: OnyxCollection; +let currentUserPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - currentUserPersonalDetails = lodashGet(val, currentUserAccountID, {}); - allPersonalDetails = val || {}; + callback: (value) => { + currentUserPersonalDetails = value?.[currentUserAccountID ?? -1] ?? null; + allPersonalDetails = value ?? {}; }, }); -let allReports; +let allReports: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, - callback: (val) => (allReports = val), + callback: (value) => (allReports = value), }); -let doesDomainHaveApprovedAccountant; +let doesDomainHaveApprovedAccountant = false; Onyx.connect({ key: ONYXKEYS.ACCOUNT, - waitForCollectionCallback: true, - callback: (val) => (doesDomainHaveApprovedAccountant = lodashGet(val, 'doesDomainHaveApprovedAccountant', false)), + callback: (value) => (doesDomainHaveApprovedAccountant = value?.doesDomainHaveApprovedAccountant ?? false), }); -let allPolicies; +let allPolicies: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, waitForCollectionCallback: true, - callback: (val) => (allPolicies = val), + callback: (value) => (allPolicies = value), }); -let loginList; +let loginList: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, - callback: (val) => (loginList = val), + callback: (value) => (loginList = value), }); -let allPolicyTags = {}; +let allPolicyTags: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, @@ -96,119 +415,85 @@ Onyx.connect({ }, }); -function getPolicyTags(policyID) { - return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); +function getPolicyTags(policyID: string) { + return allPolicyTags[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; } -function getChatType(report) { - return report ? report.chatType : ''; +function getChatType(report: OnyxEntry): ValueOf | undefined { + return report?.chatType; } -/** - * @param {String} policyID - * @returns {Object} - */ -function getPolicy(policyID) { +function getPolicy(policyID: string): Policy | EmptyObject { if (!allPolicies || !policyID) { return {}; } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] || {}; + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; } /** * Get the policy type from a given report - * @param {Object} report - * @param {String} report.policyID - * @param {Object} policies must have Onyxkey prefix (i.e 'policy_') for keys - * @returns {String} + * @param policies must have Onyxkey prefix (i.e 'policy_') for keys */ -function getPolicyType(report, policies) { - return lodashGet(policies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'type'], ''); +function getPolicyType(report: OnyxEntry, policies: OnyxCollection): string { + return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.type ?? ''; } /** * Get the policy name from a given report - * @param {Object} report - * @param {String} [report.policyID] - * @param {String} [report.oldPolicyName] - * @param {String} [report.policyName] - * @param {Boolean} [returnEmptyIfNotFound] - * @param {Object} [policy] - * @returns {String} - */ -function getPolicyName(report, returnEmptyIfNotFound = false, policy = undefined) { + */ +function getPolicyName(report: OnyxEntry | undefined | EmptyObject, returnEmptyIfNotFound = false, policy: OnyxEntry | undefined = undefined): string { const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); - if (_.isEmpty(report)) { + if (isEmptyObject(report)) { return noPolicyFound; } - if ((!allPolicies || _.size(allPolicies) === 0) && !report.policyName) { + if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { return Localize.translateLocal('workspace.common.unavailable'); } - const finalPolicy = policy || _.get(allPolicies, `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`); + const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; // Public rooms send back the policy name with the reportSummary, // since they can also be accessed by people who aren't in the workspace - const policyName = lodashGet(finalPolicy, 'name') || report.policyName || report.oldPolicyName || noPolicyFound; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const policyName = finalPolicy?.name || report?.policyName || report?.oldPolicyName || noPolicyFound; return policyName; } /** * Returns the concatenated title for the PrimaryLogins of a report - * - * @param {Array} accountIDs - * @returns {string} */ -function getReportParticipantsTitle(accountIDs) { - return ( - _.chain(accountIDs) - - // Somehow it's possible for the logins coming from report.participantAccountIDs to contain undefined values so we use compact to remove them. - .compact() - .value() - .join(', ') - ); +function getReportParticipantsTitle(accountIDs: number[]): string { + // Somehow it's possible for the logins coming from report.participantAccountIDs to contain undefined values so we use .filter(Boolean) to remove them. + return accountIDs.filter(Boolean).join(', '); } /** * Checks if a report is a chat report. - * - * @param {Object} report - * @returns {Boolean} */ -function isChatReport(report) { - return report && report.type === CONST.REPORT.TYPE.CHAT; +function isChatReport(report: OnyxEntry): boolean { + return report?.type === CONST.REPORT.TYPE.CHAT; } /** * Checks if a report is an Expense report. - * - * @param {Object} report - * @returns {Boolean} */ -function isExpenseReport(report) { - return report && report.type === CONST.REPORT.TYPE.EXPENSE; +function isExpenseReport(report: OnyxEntry | EmptyObject): boolean { + return report?.type === CONST.REPORT.TYPE.EXPENSE; } /** * Checks if a report is an IOU report. - * - * @param {Object} report - * @returns {Boolean} */ -function isIOUReport(report) { - return report && report.type === CONST.REPORT.TYPE.IOU; +function isIOUReport(report: OnyxEntry): boolean { + return report?.type === CONST.REPORT.TYPE.IOU; } /** * Checks if a report is a task report. - * - * @param {Object} report - * @returns {Boolean} */ -function isTaskReport(report) { - return report && report.type === CONST.REPORT.TYPE.TASK; +function isTaskReport(report: OnyxEntry): boolean { + return report?.type === CONST.REPORT.TYPE.TASK; } /** @@ -217,17 +502,13 @@ function isTaskReport(report) { * This is because when you delete a task, we still allow you to chat on the report itself * There's another situation where you don't have access to the parentReportAction (because it was created in a chat you don't have access to) * In this case, we have added the key to the report itself - * - * @param {Object} report - * @param {Object} parentReportAction - * @returns {Boolean} */ -function isCanceledTaskReport(report = {}, parentReportAction = {}) { - if (!_.isEmpty(parentReportAction) && lodashGet(parentReportAction, ['message', 0, 'isDeletedParentAction'], false)) { +function isCanceledTaskReport(report: OnyxEntry | EmptyObject = {}, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { + if (isNotEmptyObject(parentReportAction) && (parentReportAction?.message?.[0]?.isDeletedParentAction ?? false)) { return true; } - if (!_.isEmpty(report) && report.isDeletedParentAction) { + if (isNotEmptyObject(report) && report?.isDeletedParentAction) { return true; } @@ -237,70 +518,56 @@ function isCanceledTaskReport(report = {}, parentReportAction = {}) { /** * Checks if a report is an open task report. * - * @param {Object} report - * @param {Object} parentReportAction - The parent report action of the report (Used to check if the task has been canceled) - * @returns {Boolean} + * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isOpenTaskReport(report, parentReportAction = {}) { - return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS.OPEN; +function isOpenTaskReport(report: OnyxEntry, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { + return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; } /** * Checks if a report is a completed task report. - * - * @param {Object} report - * @returns {Boolean} */ -function isCompletedTaskReport(report) { - return isTaskReport(report) && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.APPROVED; +function isCompletedTaskReport(report: OnyxEntry): boolean { + return isTaskReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS.APPROVED; } /** * Checks if the current user is the manager of the supplied report - * - * @param {Object} report - * @returns {Boolean} */ -function isReportManager(report) { - return report && report.managerID === currentUserAccountID; +function isReportManager(report: OnyxEntry): boolean { + return Boolean(report && report.managerID === currentUserAccountID); } /** * Checks if the supplied report has been approved - * - * @param {Object} report - * @returns {Boolean} */ -function isReportApproved(report) { - return report && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.APPROVED; +function isReportApproved(report: OnyxEntry): boolean { + return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS.APPROVED; } /** * Given a collection of reports returns them sorted by last read - * - * @param {Object} reports - * @returns {Array} */ -function sortReportsByLastRead(reports) { - return _.chain(reports) - .toArray() - .filter((report) => report && report.reportID && report.lastReadTime) - .sortBy('lastReadTime') - .value(); +function sortReportsByLastRead(reports: OnyxCollection): Array> { + return Object.values(reports ?? {}) + .filter((report) => !!report?.reportID && !!report?.lastReadTime) + .sort((a, b) => { + const aTime = new Date(a?.lastReadTime ?? ''); + const bTime = new Date(b?.lastReadTime ?? ''); + + return aTime.valueOf() - bTime.valueOf(); + }); } /** * Whether the Money Request report is settled - * - * @param {String} reportID - * @returns {Boolean} */ -function isSettled(reportID) { +function isSettled(reportID: string | undefined): boolean { if (!allReports) { return false; } - const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {}; - if ((typeof report === 'object' && Object.keys(report).length === 0) || report.isWaitingOnBankAccount) { + const report: Report | EmptyObject = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; + if (isEmptyObject(report) || report.isWaitingOnBankAccount) { return false; } @@ -310,221 +577,159 @@ function isSettled(reportID) { return true; } - return report.statusNum === CONST.REPORT.STATUS.REIMBURSED; + return report?.statusNum === CONST.REPORT.STATUS.REIMBURSED; } /** * Whether the current user is the submitter of the report - * - * @param {String} reportID - * @returns {Boolean} */ -function isCurrentUserSubmitter(reportID) { +function isCurrentUserSubmitter(reportID: string): boolean { if (!allReports) { return false; } - const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {}; - return report && report.ownerAccountID === currentUserAccountID; + const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + return Boolean(report && report.ownerAccountID === currentUserAccountID); } /** * Whether the provided report is an Admin room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isAdminRoom(report) { +function isAdminRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS; } /** * Whether the provided report is an Admin-only posting room - * @param {Object} report - * @param {String} report.writeCapability - * @returns {Boolean} */ -function isAdminsOnlyPostingRoom(report) { - return lodashGet(report, 'writeCapability', CONST.REPORT.WRITE_CAPABILITIES.ALL) === CONST.REPORT.WRITE_CAPABILITIES.ADMINS; +function isAdminsOnlyPostingRoom(report: OnyxEntry): boolean { + return report?.writeCapability === CONST.REPORT.WRITE_CAPABILITIES.ADMINS; } /** * Whether the provided report is a Announce room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isAnnounceRoom(report) { +function isAnnounceRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE; } /** * Whether the provided report is a default room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isDefaultRoom(report) { - return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].indexOf(getChatType(report)) > -1; +function isDefaultRoom(report: OnyxEntry): boolean { + return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].some((type) => type === getChatType(report)); } /** * Whether the provided report is a Domain room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isDomainRoom(report) { +function isDomainRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL; } /** * Whether the provided report is a user created policy room - * @param {Object} report - * @param {String} report.chatType - * @returns {Boolean} */ -function isUserCreatedPolicyRoom(report) { +function isUserCreatedPolicyRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ROOM; } /** * Whether the provided report is a Policy Expense chat. - * @param {Object} report - * @param {String} [report.chatType] - * @returns {Boolean} */ -function isPolicyExpenseChat(report) { +function isPolicyExpenseChat(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT; } /** Wether the provided report belongs to a Control policy and is an epxense chat - * @param {Object} report - * @returns {Boolean} */ -function isControlPolicyExpenseChat(report) { +function isControlPolicyExpenseChat(report: OnyxEntry): boolean { return isPolicyExpenseChat(report) && getPolicyType(report, allPolicies) === CONST.POLICY.TYPE.CORPORATE; } /** Wether the provided report belongs to a Control policy and is an epxense report - * @param {Object} report - * @returns {Boolean} */ -function isControlPolicyExpenseReport(report) { +function isControlPolicyExpenseReport(report: OnyxEntry): boolean { return isExpenseReport(report) && getPolicyType(report, allPolicies) === CONST.POLICY.TYPE.CORPORATE; } /** * Whether the provided report is a chat room - * @param {Object} report - * @param {String} [report.chatType] - * @returns {Boolean} */ -function isChatRoom(report) { +function isChatRoom(report: OnyxEntry): boolean { return isUserCreatedPolicyRoom(report) || isDefaultRoom(report); } /** * Whether the provided report is a public room - * @param {Object} report - * @param {String} report.visibility - * @returns {Boolean} */ -function isPublicRoom(report) { - return report && (report.visibility === CONST.REPORT.VISIBILITY.PUBLIC || report.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE); +function isPublicRoom(report: OnyxEntry): boolean { + return report?.visibility === CONST.REPORT.VISIBILITY.PUBLIC || report?.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE; } /** * Whether the provided report is a public announce room - * @param {Object} report - * @param {String} report.visibility - * @returns {Boolean} */ -function isPublicAnnounceRoom(report) { - return report && report.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE; +function isPublicAnnounceRoom(report: OnyxEntry): boolean { + return report?.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE; } /** * If the report is a policy expense, the route should be for adding bank account for that policy * else since the report is a personal IOU, the route should be for personal bank account. - * @param {Object} report - * @returns {String} */ -function getBankAccountRoute(report) { - return isPolicyExpenseChat(report) ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', report.policyID) : ROUTES.SETTINGS_ADD_BANK_ACCOUNT; +function getBankAccountRoute(report: OnyxEntry): string { + return isPolicyExpenseChat(report) ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', report?.policyID) : ROUTES.SETTINGS_ADD_BANK_ACCOUNT; } /** * Check if personal detail of accountID is empty or optimistic data - * @param {String} accountID user accountID - * @returns {Boolean} */ -function isOptimisticPersonalDetail(accountID) { - return _.isEmpty(allPersonalDetails[accountID]) || !!allPersonalDetails[accountID].isOptimisticPersonalDetail; +function isOptimisticPersonalDetail(accountID: number): boolean { + return isEmptyObject(allPersonalDetails?.[accountID]) || !!allPersonalDetails?.[accountID]?.isOptimisticPersonalDetail; } /** * Checks if a report is a task report from a policy expense chat. - * - * @param {Object} report - * @returns {Boolean} */ -function isWorkspaceTaskReport(report) { +function isWorkspaceTaskReport(report: OnyxEntry): boolean { if (!isTaskReport(report)) { return false; } - const parentReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; return isPolicyExpenseChat(parentReport); } /** * Returns true if report has a parent - * - * @param {Object} report - * @returns {Boolean} */ -function isThread(report) { - return Boolean(report && report.parentReportID && report.parentReportActionID); +function isThread(report: OnyxEntry): boolean { + return Boolean(report?.parentReportID && report?.parentReportActionID); } /** * Returns true if report is of type chat and has a parent and is therefore a Thread. - * - * @param {Object} report - * @returns {Boolean} */ -function isChatThread(report) { - return isThread(report) && report.type === CONST.REPORT.TYPE.CHAT; +function isChatThread(report: OnyxEntry): boolean { + return isThread(report) && report?.type === CONST.REPORT.TYPE.CHAT; } -/** - * Returns true if report is a DM/Group DM chat. - * - * @param {Object} report - * @returns {Boolean} - */ -function isDM(report) { +function isDM(report: OnyxEntry): boolean { return isChatReport(report) && !getChatType(report); } /** * Only returns true if this is our main 1:1 DM report with Concierge - * - * @param {Object} report - * @returns {Boolean} */ -function isConciergeChatReport(report) { - return lodashGet(report, 'participantAccountIDs', []).length === 1 && Number(report.participantAccountIDs[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); +function isConciergeChatReport(report: OnyxEntry): boolean { + return report?.participantAccountIDs?.length === 1 && Number(report.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } /** * Check if the report is a single chat report that isn't a thread * and personal detail of participant is optimistic data - * @param {Object} report - * @param {Array} report.participantAccountIDs - * @returns {Boolean} */ -function shouldDisableDetailPage(report) { - const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []); +function shouldDisableDetailPage(report: OnyxEntry): boolean { + const participantAccountIDs = report?.participantAccountIDs ?? []; if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report) || isTaskReport(report)) { return false; @@ -537,25 +742,20 @@ function shouldDisableDetailPage(report) { /** * Returns true if this report has only one participant and it's an Expensify account. - * @param {Object} report - * @returns {Boolean} */ -function isExpensifyOnlyParticipantInReport(report) { - const reportParticipants = _.without(lodashGet(report, 'participantAccountIDs', []), currentUserAccountID); - return reportParticipants.length === 1 && _.some(reportParticipants, (accountID) => _.contains(CONST.EXPENSIFY_ACCOUNT_IDS, accountID)); +function isExpensifyOnlyParticipantInReport(report: OnyxEntry): boolean { + const reportParticipants = report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserAccountID) ?? []; + return reportParticipants.length === 1 && reportParticipants.some((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); } /** * Returns whether a given report can have tasks created in it. * We only prevent the task option if it's a DM/group-DM and the other users are all special Expensify accounts * - * @param {Object} report - * @returns {Boolean} */ -function canCreateTaskInReport(report) { - const otherReportParticipants = _.without(lodashGet(report, 'participantAccountIDs', []), currentUserAccountID); - const areExpensifyAccountsOnlyOtherParticipants = - otherReportParticipants.length >= 1 && _.every(otherReportParticipants, (accountID) => _.contains(CONST.EXPENSIFY_ACCOUNT_IDS, accountID)); +function canCreateTaskInReport(report: OnyxEntry): boolean { + const otherReportParticipants = report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserAccountID) ?? []; + const areExpensifyAccountsOnlyOtherParticipants = otherReportParticipants?.length >= 1 && otherReportParticipants?.every((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); if (areExpensifyAccountsOnlyOtherParticipants && isDM(report)) { return false; } @@ -566,34 +766,27 @@ function canCreateTaskInReport(report) { /** * Returns true if there are any Expensify accounts (i.e. with domain 'expensify.com') in the set of accountIDs * by cross-referencing the accountIDs with personalDetails. - * - * @param {Array} accountIDs - * @return {Boolean} */ -function hasExpensifyEmails(accountIDs) { - return _.some(accountIDs, (accountID) => Str.extractEmailDomain(lodashGet(allPersonalDetails, [accountID, 'login'], '')) === CONST.EXPENSIFY_PARTNER_NAME); +function hasExpensifyEmails(accountIDs: number[]): boolean { + return accountIDs.some((accountID) => Str.extractEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EXPENSIFY_PARTNER_NAME); } /** * Returns true if there are any guides accounts (team.expensify.com) in a list of accountIDs * by cross-referencing the accountIDs with personalDetails since guides that are participants * of the user's chats should have their personal details in Onyx. - * @param {Array} accountIDs - * @returns {Boolean} */ -function hasExpensifyGuidesEmails(accountIDs) { - return _.some(accountIDs, (accountID) => Str.extractEmailDomain(lodashGet(allPersonalDetails, [accountID, 'login'], '')) === CONST.EMAIL.GUIDES_DOMAIN); +function hasExpensifyGuidesEmails(accountIDs: number[]): boolean { + return accountIDs.some((accountID) => Str.extractEmailDomain(allPersonalDetails?.[accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN); } -/** - * @param {Record|Array<{lastReadTime, reportID}>} reports - * @param {Boolean} [ignoreDomainRooms] - * @param {Object} policies - * @param {Boolean} isFirstTimeNewExpensifyUser - * @param {Boolean} openOnAdminRoom - * @returns {Object} - */ -function findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom = false) { +function findLastAccessedReport( + reports: OnyxCollection, + ignoreDomainRooms: boolean, + policies: OnyxCollection, + isFirstTimeNewExpensifyUser: boolean, + openOnAdminRoom = false, +): OnyxEntry { // If it's the user's first time using New Expensify, then they could either have: // - just a Concierge report, if so we'll return that // - their Concierge report, and a separate report that must have deeplinked them to the app before they created their account. @@ -601,9 +794,9 @@ function findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTim // since the Concierge report would be incorrectly selected over the deep-linked report in the logic below. let sortedReports = sortReportsByLastRead(reports); - let adminReport; + let adminReport: OnyxEntry | undefined; if (openOnAdminRoom) { - adminReport = _.find(sortedReports, (report) => { + adminReport = sortedReports.find((report) => { const chatType = getChatType(report); return chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS; }); @@ -614,42 +807,34 @@ function findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTim return sortedReports[0]; } - return adminReport || _.find(sortedReports, (report) => !isConciergeChatReport(report)); + return adminReport ?? sortedReports.find((report) => !isConciergeChatReport(report)) ?? null; } if (ignoreDomainRooms) { // We allow public announce rooms, admins, and announce rooms through since we bypass the default rooms beta for them. // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. // Domain rooms are now the only type of default room that are on the defaultRooms beta. - sortedReports = _.filter( - sortedReports, - (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(lodashGet(report, ['participantAccountIDs'], [])), + sortedReports = sortedReports.filter( + (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(report?.participantAccountIDs ?? []), ); } - return adminReport || _.last(sortedReports); + return adminReport ?? sortedReports.at(-1) ?? null; } /** * Whether the provided report is an archived room - * @param {Object} report - * @param {Number} [report.stateNum] - * @param {Number} [report.statusNum] - * @returns {Boolean} */ -function isArchivedRoom(report) { - return report && report.statusNum === CONST.REPORT.STATUS.CLOSED && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED; +function isArchivedRoom(report: OnyxEntry | EmptyObject): boolean { + return report?.statusNum === CONST.REPORT.STATUS.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED; } /** * Checks if the current user is allowed to comment on the given report. - * @param {Object} report - * @param {String} [report.writeCapability] - * @returns {Boolean} */ -function isAllowedToComment(report) { +function isAllowedToComment(report: Report): boolean { // Default to allowing all users to post - const capability = lodashGet(report, 'writeCapability', CONST.REPORT.WRITE_CAPABILITIES.ALL) || CONST.REPORT.WRITE_CAPABILITIES.ALL; + const capability = report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL; if (capability === CONST.REPORT.WRITE_CAPABILITIES.ALL) { return true; @@ -662,113 +847,78 @@ function isAllowedToComment(report) { // If we've made it here, commenting on this report is restricted. // If the user is an admin, allow them to post. - const policy = allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; - return lodashGet(policy, 'role', '') === CONST.POLICY.ROLE.ADMIN; + const policy = allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + return policy?.role === CONST.POLICY.ROLE.ADMIN; } /** * Checks if the current user is the admin of the policy given the policy expense chat. - * @param {Object} report - * @param {String} report.policyID - * @param {Object} policies must have OnyxKey prefix (i.e 'policy_') for keys - * @returns {Boolean} */ -function isPolicyExpenseChatAdmin(report, policies) { +function isPolicyExpenseChatAdmin(report: OnyxEntry, policies: OnyxCollection): boolean { if (!isPolicyExpenseChat(report)) { return false; } - const policyRole = lodashGet(policies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'role']); + const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.role; return policyRole === CONST.POLICY.ROLE.ADMIN; } /** * Checks if the current user is the admin of the policy. - * @param {String} policyID - * @param {Object} policies must have OnyxKey prefix (i.e 'policy_') for keys - * @returns {Boolean} */ -function isPolicyAdmin(policyID, policies) { - const policyRole = lodashGet(policies, [`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, 'role']); +function isPolicyAdmin(policyID: string, policies: OnyxCollection): boolean { + const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.role; return policyRole === CONST.POLICY.ROLE.ADMIN; } /** * Returns true if report has a single participant. - * - * @param {Object} report - * @returns {Boolean} */ -function hasSingleParticipant(report) { - return report && report.participantAccountIDs && report.participantAccountIDs.length === 1; +function hasSingleParticipant(report: OnyxEntry): boolean { + return report?.participantAccountIDs?.length === 1; } /** * Checks whether all the transactions linked to the IOU report are of the Distance Request type * - * @param {string|null} iouReportID - * @returns {boolean} */ -function hasOnlyDistanceRequestTransactions(iouReportID) { +function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): boolean { const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return _.all(allTransactions, (transaction) => TransactionUtils.isDistanceRequest(transaction)); + return allTransactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); } /** * If the report is a thread and has a chat type set, it is a workspace chat. - * - * @param {Object} report - * @returns {Boolean} */ -function isWorkspaceThread(report) { - return Boolean(isThread(report) && !isDM(report)); -} - -/** - * Returns true if reportAction has a child. - * - * @param {Object} reportAction - * @returns {Boolean} - */ -function isThreadParent(reportAction) { - return reportAction && reportAction.childReportID && reportAction.childReportID !== 0; +function isWorkspaceThread(report: OnyxEntry): boolean { + return isThread(report) && !isDM(report); } /** * Returns true if reportAction is the first chat preview of a Thread - * - * @param {Object} reportAction - * @param {String} reportID - * @returns {Boolean} */ -function isThreadFirstChat(reportAction, reportID) { - return !_.isUndefined(reportAction.childReportID) && reportAction.childReportID.toString() === reportID; +function isThreadFirstChat(reportAction: OnyxEntry, reportID: string): boolean { + return reportAction?.childReportID?.toString() === reportID; } /** * Checks if a report is a child report. - * - * @param {Object} report - * @returns {Boolean} */ -function isChildReport(report) { +function isChildReport(report: OnyxEntry): boolean { return isThread(report) || isTaskReport(report); } /** * An Expense Request is a thread where the parent report is an Expense Report and * the parentReportAction is a transaction. - * - * @param {Object} report - * @returns {Boolean} */ -function isExpenseRequest(report) { +function isExpenseRequest(report: OnyxEntry): boolean { if (isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); - return isExpenseReport(parentReport) && ReportActionsUtils.isTransactionThread(parentReportAction); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; + return isExpenseReport(parentReport) && isNotEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; } @@ -776,49 +926,38 @@ function isExpenseRequest(report) { /** * An IOU Request is a thread where the parent report is an IOU Report and * the parentReportAction is a transaction. - * - * @param {Object} report - * @returns {Boolean} */ -function isIOURequest(report) { +function isIOURequest(report: OnyxEntry): boolean { if (isThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const parentReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; - return isIOUReport(parentReport) && ReportActionsUtils.isTransactionThread(parentReportAction); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; + return isIOUReport(parentReport) && isNotEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction); } return false; } /** * Checks if a report is an IOU or expense request. - * - * @param {Object|String} reportOrID - * @returns {Boolean} */ -function isMoneyRequest(reportOrID) { - const report = _.isObject(reportOrID) ? reportOrID : allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`]; +function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { + const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return isIOURequest(report) || isExpenseRequest(report); } /** * Checks if a report is an IOU or expense report. - * - * @param {Object|String} reportOrID - * @returns {Boolean} */ -function isMoneyRequestReport(reportOrID) { - const report = typeof reportOrID === 'object' ? reportOrID : allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`]; +function isMoneyRequestReport(reportOrID: OnyxEntry | string): boolean { + const report = typeof reportOrID === 'object' ? reportOrID : allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null; return isIOUReport(report) || isExpenseReport(report); } /** * Should return true only for personal 1:1 report * - * @param {Object} report (chatReport or iouReport) - * @returns {boolean} */ -function isOneOnOneChat(report) { - const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []); +function isOneOnOneChat(report: OnyxEntry): boolean { + const participantAccountIDs = report?.participantAccountIDs ?? []; return ( !isThread(report) && !isChatRoom(report) && @@ -834,11 +973,8 @@ function isOneOnOneChat(report) { /** * Get the report given a reportID - * - * @param {String} reportID - * @returns {Object} */ -function getReport(reportID) { +function getReport(reportID: string | undefined): OnyxEntry | EmptyObject { /** * Using typical string concatenation here due to performance issues * with template literals. @@ -847,47 +983,38 @@ function getReport(reportID) { return {}; } - return allReports[ONYXKEYS.COLLECTION.REPORT + reportID] || {}; + return allReports?.[ONYXKEYS.COLLECTION.REPORT + reportID] ?? {}; } /** * Get the notification preference given a report - * - * @param {Object} report - * @returns {String} */ -function getReportNotificationPreference(report) { - return lodashGet(report, 'notificationPreference', ''); +function getReportNotificationPreference(report: OnyxEntry): string | number { + return report?.notificationPreference ?? ''; } /** * Returns whether or not the author of the action is this user * - * @param {Object} reportAction - * @returns {Boolean} */ -function isActionCreator(reportAction) { - return reportAction.actorAccountID === currentUserAccountID; +function isActionCreator(reportAction: OnyxEntry): boolean { + return reportAction?.actorAccountID === currentUserAccountID; } /** * Can only delete if the author is this user and the action is an ADDCOMMENT action or an IOU action in an unsettled report, or if the user is a * policy admin - * - * @param {Object} reportAction - * @param {String} reportID - * @returns {Boolean} */ -function canDeleteReportAction(reportAction, reportID) { +function canDeleteReportAction(reportAction: OnyxEntry, reportID: string): boolean { const report = getReport(reportID); - const isActionOwner = reportAction.actorAccountID === currentUserAccountID; + const isActionOwner = reportAction?.actorAccountID === currentUserAccountID; - if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { + if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { // For now, users cannot delete split actions - const isSplitAction = lodashGet(reportAction, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; + const isSplitAction = reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - if (isSplitAction || isSettled(reportAction.originalMessage.IOUReportID) || isReportApproved(report)) { + if (isSplitAction || isSettled(String(reportAction?.originalMessage?.IOUReportID)) || (isNotEmptyObject(report) && isReportApproved(report))) { return false; } @@ -897,36 +1024,32 @@ function canDeleteReportAction(reportAction, reportID) { } if ( - reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || - reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || + reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || + reportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || ReportActionsUtils.isCreatedTaskReportAction(reportAction) || - reportAction.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE + reportAction?.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE ) { return false; } - const policy = lodashGet(allPolicies, `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`) || {}; - const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN && !isDM(report); + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN && isNotEmptyObject(report) && !isDM(report); return isActionOwner || isAdmin; } /** * Get welcome message based on room type - * @param {Object} report - * @param {Boolean} isUserPolicyAdmin - * @returns {Object} */ - -function getRoomWelcomeMessage(report, isUserPolicyAdmin) { - const welcomeMessage = {showReportName: true}; +function getRoomWelcomeMessage(report: OnyxEntry, isUserPolicyAdmin: boolean): WelcomeMessage { + const welcomeMessage: WelcomeMessage = {showReportName: true}; const workspaceName = getPolicyName(report); if (isArchivedRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartOne'); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartTwo'); } else if (isDomainRoom(report)) { - welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report.reportName}); + welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report?.reportName ?? ''}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartTwo'); } else if (isAdminRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName}); @@ -948,87 +1071,65 @@ function getRoomWelcomeMessage(report, isUserPolicyAdmin) { /** * Returns true if Concierge is one of the chat participants (1:1 as well as group chats) - * @param {Object} report - * @returns {Boolean} */ -function chatIncludesConcierge(report) { - return !_.isEmpty(report.participantAccountIDs) && _.contains(report.participantAccountIDs, CONST.ACCOUNT_ID.CONCIERGE); +function chatIncludesConcierge(report: OnyxEntry): boolean { + return Boolean(report?.participantAccountIDs?.length && report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CONCIERGE)); } /** * Returns true if there is any automated expensify account `in accountIDs - * @param {Array} accountIDs - * @returns {Boolean} */ -function hasAutomatedExpensifyAccountIDs(accountIDs) { - return _.intersection(accountIDs, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; +function hasAutomatedExpensifyAccountIDs(accountIDs: number[]): boolean { + return accountIDs.some((accountID) => CONST.EXPENSIFY_ACCOUNT_IDS.includes(accountID)); } -/** - * @param {Object} report - * @param {Number} currentLoginAccountID - * @returns {Array} - */ -function getReportRecipientAccountIDs(report, currentLoginAccountID) { - let finalReport = report; +function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAccountID: number): number[] { + let finalReport: OnyxEntry = report; // In 1:1 chat threads, the participants will be the same as parent report. If a report is specifically a 1:1 chat thread then we will // get parent report and use its participants array. if (isThread(report) && !(isTaskReport(report) || isMoneyRequestReport(report))) { - const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; if (hasSingleParticipant(parentReport)) { finalReport = parentReport; } } - let finalParticipantAccountIDs = []; + let finalParticipantAccountIDs: number[] | undefined = []; if (isMoneyRequestReport(report)) { // For money requests i.e the IOU (1:1 person) and Expense (1:* person) reports, use the full `initialParticipantAccountIDs` array // and add the `ownerAccountId`. Money request reports don't add `ownerAccountId` in `participantAccountIDs` array - finalParticipantAccountIDs = _.union(lodashGet(finalReport, 'participantAccountIDs'), [report.ownerAccountID]); + const defaultParticipantAccountIDs = finalReport?.participantAccountIDs ?? []; + const setOfParticipantAccountIDs = new Set(report?.ownerAccountID ? [...defaultParticipantAccountIDs, report.ownerAccountID] : defaultParticipantAccountIDs); + finalParticipantAccountIDs = [...setOfParticipantAccountIDs]; } else if (isTaskReport(report)) { // Task reports `managerID` will change when assignee is changed, in that case the old `managerID` is still present in `participantAccountIDs` // array along with the new one. We only need the `managerID` as a participant here. - finalParticipantAccountIDs = [report.managerID]; + finalParticipantAccountIDs = report?.managerID ? [report?.managerID] : []; } else { - finalParticipantAccountIDs = lodashGet(finalReport, 'participantAccountIDs'); + finalParticipantAccountIDs = finalReport?.participantAccountIDs; } - const reportParticipants = _.without(finalParticipantAccountIDs, currentLoginAccountID); - const participantsWithoutExpensifyAccountIDs = _.difference(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS); + const reportParticipants = finalParticipantAccountIDs?.filter((accountID) => accountID !== currentLoginAccountID) ?? []; + const participantsWithoutExpensifyAccountIDs = reportParticipants.filter((participant) => !CONST.EXPENSIFY_ACCOUNT_IDS.includes(participant ?? 0)); return participantsWithoutExpensifyAccountIDs; } /** * Whether the time row should be shown for a report. - * @param {Array} personalDetails - * @param {Object} report - * @param {Number} accountID - * @return {Boolean} */ -function canShowReportRecipientLocalTime(personalDetails, report, accountID) { +function canShowReportRecipientLocalTime(personalDetails: OnyxCollection, report: OnyxEntry, accountID: number): boolean { const reportRecipientAccountIDs = getReportRecipientAccountIDs(report, accountID); const hasMultipleParticipants = reportRecipientAccountIDs.length > 1; - const reportRecipient = personalDetails[reportRecipientAccountIDs[0]]; - const reportRecipientTimezone = lodashGet(reportRecipient, 'timezone', CONST.DEFAULT_TIME_ZONE); - const isReportParticipantValidated = lodashGet(reportRecipient, 'validated', false); - return Boolean( - !hasMultipleParticipants && - !isChatRoom(report) && - !isPolicyExpenseChat(report) && - reportRecipient && - reportRecipientTimezone && - reportRecipientTimezone.selected && - isReportParticipantValidated, - ); + const reportRecipient = personalDetails?.[reportRecipientAccountIDs[0]]; + const reportRecipientTimezone = reportRecipient?.timezone ?? CONST.DEFAULT_TIME_ZONE; + const isReportParticipantValidated = reportRecipient?.validated ?? false; + return Boolean(!hasMultipleParticipants && !isChatRoom(report) && !isPolicyExpenseChat(report) && reportRecipient && reportRecipientTimezone?.selected && isReportParticipantValidated); } /** * Shorten last message text to fixed length and trim spaces. - * @param {String} lastMessageText - * @param {Boolean} isModifiedExpenseMessage - * @returns {String} */ -function formatReportLastMessageText(lastMessageText, isModifiedExpenseMessage = false) { +function formatReportLastMessageText(lastMessageText: string, isModifiedExpenseMessage = false): string { if (isModifiedExpenseMessage) { return String(lastMessageText).trim().replace(CONST.REGEX.LINE_BREAK, '').trim(); } @@ -1037,10 +1138,8 @@ function formatReportLastMessageText(lastMessageText, isModifiedExpenseMessage = /** * Helper method to return the default avatar associated with the given login - * @param {String} [workspaceName] - * @returns {String} */ -function getDefaultWorkspaceAvatar(workspaceName) { +function getDefaultWorkspaceAvatar(workspaceName?: string): React.FC { if (!workspaceName) { return defaultWorkspaceAvatars.WorkspaceBuilding; } @@ -1051,57 +1150,54 @@ function getDefaultWorkspaceAvatar(workspaceName) { .replace(/[^0-9a-z]/gi, '') .toUpperCase(); - return !alphaNumeric ? defaultWorkspaceAvatars.WorkspaceBuilding : defaultWorkspaceAvatars[`Workspace${alphaNumeric[0]}`]; + const workspace = `Workspace${alphaNumeric[0]}` as keyof typeof defaultWorkspaceAvatars; + const defaultWorkspaceAvatar = defaultWorkspaceAvatars[workspace]; + + return !alphaNumeric ? defaultWorkspaceAvatars.WorkspaceBuilding : defaultWorkspaceAvatar; } -function getWorkspaceAvatar(report) { - const workspaceName = getPolicyName(report, allPolicies); - return lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'avatar']) || getDefaultWorkspaceAvatar(workspaceName); +function getWorkspaceAvatar(report: OnyxEntry): UserUtils.AvatarSource { + const workspaceName = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]); + return allPolicies?.[`policy${report?.policyID}`]?.avatar ?? getDefaultWorkspaceAvatar(workspaceName); } /** * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. - * - * @param {Array} participants - * @param {Object} personalDetails - * @returns {Array<*>} */ -function getIconsForParticipants(participants, personalDetails) { - const participantDetails = []; +function getIconsForParticipants(participants: number[], personalDetails: OnyxCollection): Icon[] { + const participantDetails: ParticipantDetails[] = []; const participantsList = participants || []; - for (let i = 0; i < participantsList.length; i++) { - const accountID = participantsList[i]; - const avatarSource = UserUtils.getAvatar(lodashGet(personalDetails, [accountID, 'avatar'], ''), accountID); - const displayNameLogin = lodashGet(personalDetails, [accountID, 'displayName']) || lodashGet(personalDetails, [accountID, 'login'], ''); - participantDetails.push([accountID, displayNameLogin, avatarSource, lodashGet(personalDetails, [accountID, 'fallBackIcon'])]); + for (const accountID of participantsList) { + const avatarSource = UserUtils.getAvatar(personalDetails?.[accountID]?.avatar ?? '', accountID); + const displayNameLogin = personalDetails?.[accountID]?.displayName ? personalDetails?.[accountID]?.displayName : personalDetails?.[accountID]?.login; + participantDetails.push([accountID, displayNameLogin ?? '', avatarSource, personalDetails?.[accountID]?.fallbackIcon ?? '']); } - const sortedParticipantDetails = _.chain(participantDetails) - .sort((first, second) => { - // First sort by displayName/login - const displayNameLoginOrder = first[1].localeCompare(second[1]); - if (displayNameLoginOrder !== 0) { - return displayNameLoginOrder; - } + const sortedParticipantDetails = participantDetails.sort((first, second) => { + // First sort by displayName/login + const displayNameLoginOrder = first[1].localeCompare(second[1]); + if (displayNameLoginOrder !== 0) { + return displayNameLoginOrder; + } - // Then fallback on accountID as the final sorting criteria. - // This will ensure that the order of avatars with same login/displayName - // stay consistent across all users and devices - return first[0] > second[0]; - }) - .value(); + // Then fallback on accountID as the final sorting criteria. + // This will ensure that the order of avatars with same login/displayName + // stay consistent across all users and devices + return first[0] - second[0]; + }); // Now that things are sorted, gather only the avatars (second element in the array) and return those - const avatars = []; - for (let i = 0; i < sortedParticipantDetails.length; i++) { + const avatars: Icon[] = []; + + for (const sortedParticipantDetail of sortedParticipantDetails) { const userIcon = { - id: sortedParticipantDetails[i][0], - source: sortedParticipantDetails[i][2], + id: sortedParticipantDetail[0], + source: sortedParticipantDetail[2], type: CONST.ICON_TYPE_AVATAR, - name: sortedParticipantDetails[i][1], - fallBackIcon: sortedParticipantDetails[i][3], + name: sortedParticipantDetail[1], + fallbackIcon: sortedParticipantDetail[3], }; avatars.push(userIcon); } @@ -1111,16 +1207,15 @@ function getIconsForParticipants(participants, personalDetails) { /** * Given a report, return the associated workspace icon. - * - * @param {Object} report - * @param {Object} [policy] - * @returns {Object} */ -function getWorkspaceIcon(report, policy = undefined) { +function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = null): Icon { const workspaceName = getPolicyName(report, false, policy); - const policyExpenseChatAvatarSource = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'avatar']) || getDefaultWorkspaceAvatar(workspaceName); - const workspaceIcon = { - source: policyExpenseChatAvatarSource, + const policyExpenseChatAvatarSource = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar + ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar + : getDefaultWorkspaceAvatar(workspaceName); + + const workspaceIcon: Icon = { + source: policyExpenseChatAvatarSource ?? '', type: CONST.ICON_TYPE_WORKSPACE, name: workspaceName, id: -1, @@ -1131,19 +1226,18 @@ function getWorkspaceIcon(report, policy = undefined) { /** * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. - * - * @param {Object} report - * @param {Object} personalDetails - * @param {*} [defaultIcon] - * @param {String} [defaultName] - * @param {Number} [defaultAccountID] - * @param {Object} [policy] - * @returns {Array<*>} - */ -function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', defaultAccountID = -1, policy = undefined) { - if (_.isEmpty(report)) { - const fallbackIcon = { - source: defaultIcon || Expensicons.FallbackAvatar, + */ +function getIcons( + report: OnyxEntry, + personalDetails: OnyxCollection, + defaultIcon: UserUtils.AvatarSource | null = null, + defaultName = '', + defaultAccountID = -1, + policy: OnyxEntry = null, +): Icon[] { + if (isEmptyObject(report)) { + const fallbackIcon: Icon = { + source: defaultIcon ?? Expensicons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: defaultName, id: defaultAccountID, @@ -1154,11 +1248,11 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', const parentReportAction = ReportActionsUtils.getParentReportAction(report); const workspaceIcon = getWorkspaceIcon(report, policy); const memberIcon = { - source: UserUtils.getAvatar(lodashGet(personalDetails, [parentReportAction.actorAccountID, 'avatar']), parentReportAction.actorAccountID), + source: UserUtils.getAvatar(personalDetails?.[parentReportAction.actorAccountID ?? -1]?.avatar ?? '', parentReportAction.actorAccountID ?? -1), id: parentReportAction.actorAccountID, type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [parentReportAction.actorAccountID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [parentReportAction.actorAccountID, 'fallbackIcon']), + name: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.fallbackIcon, }; return [memberIcon, workspaceIcon]; @@ -1166,14 +1260,14 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', if (isChatThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const actorAccountID = lodashGet(parentReportAction, 'actorAccountID', -1); - const actorDisplayName = lodashGet(allPersonalDetails, [actorAccountID, 'displayName'], ''); + const actorAccountID = parentReportAction.actorAccountID; + const actorDisplayName = allPersonalDetails?.[actorAccountID ?? -1]?.displayName ?? ''; const actorIcon = { id: actorAccountID, - source: UserUtils.getAvatar(lodashGet(personalDetails, [actorAccountID, 'avatar']), actorAccountID), + source: UserUtils.getAvatar(personalDetails?.[actorAccountID ?? -1]?.avatar ?? '', actorAccountID ?? -1), name: actorDisplayName, type: CONST.ICON_TYPE_AVATAR, - fallbackIcon: lodashGet(personalDetails, [parentReportAction.actorAccountID, 'fallbackIcon']), + fallbackIcon: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.fallbackIcon, }; if (isWorkspaceThread(report)) { @@ -1184,11 +1278,11 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', } if (isTaskReport(report)) { const ownerIcon = { - id: report.ownerAccountID, - source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID), + id: report?.ownerAccountID, + source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [report.ownerAccountID, 'fallbackIcon']), + name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, }; if (isWorkspaceTaskReport(report)) { @@ -1200,12 +1294,12 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', } if (isDomainRoom(report)) { // Get domain name after the #. Domain Rooms use our default workspace avatar pattern. - const domainName = report.reportName.substring(1); + const domainName = report?.reportName?.substring(1); const policyExpenseChatAvatarSource = getDefaultWorkspaceAvatar(domainName); - const domainIcon = { + const domainIcon: Icon = { source: policyExpenseChatAvatarSource, type: CONST.ICON_TYPE_WORKSPACE, - name: domainName, + name: domainName ?? '', id: -1, }; return [domainIcon]; @@ -1217,43 +1311,42 @@ function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', if (isPolicyExpenseChat(report) || isExpenseReport(report)) { const workspaceIcon = getWorkspaceIcon(report, policy); const memberIcon = { - source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID), - id: report.ownerAccountID, + source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), + id: report?.ownerAccountID, type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [report.ownerAccountID, 'fallbackIcon']), + name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, }; return isExpenseReport(report) ? [memberIcon, workspaceIcon] : [workspaceIcon, memberIcon]; } if (isIOUReport(report)) { const managerIcon = { - source: UserUtils.getAvatar(lodashGet(personalDetails, [report.managerID, 'avatar']), report.managerID), - id: report.managerID, + source: UserUtils.getAvatar(personalDetails?.[report?.managerID ?? -1]?.avatar ?? '', report?.managerID ?? -1), + id: report?.managerID, type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [report.managerID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [report.managerID, 'fallbackIcon']), + name: personalDetails?.[report?.managerID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[report?.managerID ?? -1]?.fallbackIcon, }; const ownerIcon = { - id: report.ownerAccountID, - source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID), + id: report?.ownerAccountID, + source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), type: CONST.ICON_TYPE_AVATAR, - name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), - fallbackIcon: lodashGet(personalDetails, [report.ownerAccountID, 'fallbackIcon']), + name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, }; - const isPayer = currentUserAccountID === report.managerID; + const isPayer = currentUserAccountID === report?.managerID; return isPayer ? [managerIcon, ownerIcon] : [ownerIcon, managerIcon]; } - return getIconsForParticipants(report.participantAccountIDs, personalDetails); + + return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails); } /** * Gets the personal details for a login by looking in the ONYXKEYS.PERSONAL_DETAILS_LIST Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx, * then a default object is constructed. - * @param {Number} accountID - * @returns {Object} */ -function getPersonalDetailsForAccountID(accountID) { +function getPersonalDetailsForAccountID(accountID: number): Partial { if (!accountID) { return {}; } @@ -1266,7 +1359,7 @@ function getPersonalDetailsForAccountID(accountID) { }; } return ( - (allPersonalDetails && allPersonalDetails[accountID]) || { + allPersonalDetails?.[accountID] ?? { avatar: UserUtils.getDefaultAvatar(accountID), isOptimisticPersonalDetail: true, } @@ -1275,58 +1368,57 @@ function getPersonalDetailsForAccountID(accountID) { /** * Get the displayName for a single report participant. - * - * @param {Number} accountID - * @param {Boolean} [shouldUseShortForm] - * @param {Boolean} shouldFallbackToHidden - * @returns {String} */ -function getDisplayNameForParticipant(accountID, shouldUseShortForm = false, shouldFallbackToHidden = true) { +function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = false, shouldFallbackToHidden = true): string | undefined { if (!accountID) { return ''; } const personalDetails = getPersonalDetailsForAccountID(accountID); + // console.log(personalDetails); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetails.login || ''); - // This is to check if account is an invite/optimistically created one // and prevent from falling back to 'Hidden', so a correct value is shown - // when searching for a new user while offline - if (lodashGet(personalDetails, 'isOptimisticPersonalDetail') === true) { + // when searching for a new user + if (personalDetails.isOptimisticPersonalDetail === true) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return formattedLogin; } + const longName = personalDetails.displayName ? personalDetails.displayName : formattedLogin; + + const shortName = personalDetails.firstName ? personalDetails.firstName : longName; - const longName = personalDetails.displayName || formattedLogin; - const shortName = personalDetails.firstName || longName; if (!longName && shouldFallbackToHidden) { return Localize.translateLocal('common.hidden'); } return shouldUseShortForm ? shortName : longName; } -/** - * @param {Object} personalDetailsList - * @param {Boolean} isMultipleParticipantReport - * @param {Boolean} shouldFallbackToHidden - * @returns {Array} - */ -function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantReport, shouldFallbackToHidden = true) { - return _.chain(personalDetailsList) +function getDisplayNamesWithTooltips( + personalDetailsList: PersonalDetails[] | Record, + isMultipleParticipantReport: boolean, + shouldFallbackToHidden = true, +): DisplayNameWithTooltips { + const personalDetailsListArray = Array.isArray(personalDetailsList) ? personalDetailsList : Object.values(personalDetailsList); + + return personalDetailsListArray .map((user) => { const accountID = Number(user.accountID); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden) || user.login || ''; const avatar = UserUtils.getDefaultAvatar(accountID); let pronouns = user.pronouns; if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { const pronounTranslationKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); - pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}`); + pronouns = Localize.translateLocal(`pronouns.${pronounTranslationKey}` as TranslationPaths); } return { displayName, avatar, - login: user.login || '', + login: user.login ?? '', accountID, pronouns, }; @@ -1339,32 +1431,28 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR } // Then fallback on accountID as the final sorting criteria. - return first.accountID > second.accountID; - }) - .value(); + return first.accountID - second.accountID; + }); } /** * Gets a joined string of display names from the list of display name with tooltip objects. * - * @param {Object} displayNamesWithTooltips - * @returns {String} */ -function getDisplayNamesStringFromTooltips(displayNamesWithTooltips) { - return _.filter( - _.map(displayNamesWithTooltips, ({displayName}) => displayName), - (displayName) => !_.isEmpty(displayName), - ).join(', '); +function getDisplayNamesStringFromTooltips(displayNamesWithTooltips: DisplayNameWithTooltips | undefined) { + return displayNamesWithTooltips + ?.map(({displayName}) => displayName) + .filter(Boolean) + .join(', '); } /** * For a deleted parent report action within a chat report, * let us return the appropriate display message * - * @param {Object} reportAction - The deleted report action of a chat report for which we need to return message. - * @return {String} + * @param reportAction - The deleted report action of a chat report for which we need to return message. */ -function getDeletedParentActionMessageForChatReport(reportAction) { +function getDeletedParentActionMessageForChatReport(reportAction: OnyxEntry): string { // By default, let us display [Deleted message] let deletedMessageText = Localize.translateLocal('parentReportAction.deletedMessage'); if (ReportActionsUtils.isCreatedTaskReportAction(reportAction)) { @@ -1377,14 +1465,13 @@ function getDeletedParentActionMessageForChatReport(reportAction) { /** * Returns the preview message for `REIMBURSEMENTQUEUED` action * - * @param {Object} reportAction - * @param {Object} report - * @returns {String} - */ -function getReimbursementQueuedActionMessage(reportAction, report) { - const submitterDisplayName = getDisplayNameForParticipant(report.ownerAccountID, true); - let messageKey; - if (lodashGet(reportAction, 'originalMessage.paymentType', '') === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + + */ +function getReimbursementQueuedActionMessage(reportAction: OnyxEntry, report: OnyxEntry): string { + const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? ''; + const originalMessage = reportAction?.originalMessage as IOUMessage | undefined; + let messageKey: TranslationPaths; + if (originalMessage?.paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { messageKey = 'iou.waitingOnEnabledWallet'; } else { messageKey = 'iou.waitingOnBankAccount'; @@ -1396,16 +1483,16 @@ function getReimbursementQueuedActionMessage(reportAction, report) { /** * Returns the last visible message for a given report after considering the given optimistic actions * - * @param {String} reportID - the report for which last visible message has to be fetched - * @param {Object} [actionsToMerge] - the optimistic merge actions that needs to be considered while fetching last visible message - * @return {Object} + * @param reportID - the report for which last visible message has to be fetched + * @param [actionsToMerge] - the optimistic merge actions that needs to be considered while fetching last visible message + */ -function getLastVisibleMessage(reportID, actionsToMerge = {}) { +function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: ReportActions = {}): LastVisibleMessage { const report = getReport(reportID); - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, actionsToMerge); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '', actionsToMerge); // For Chat Report with deleted parent actions, let us fetch the correct message - if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && isChatReport(report)) { + if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && isNotEmptyObject(report) && isChatReport(report)) { const lastMessageText = getDeletedParentActionMessageForChatReport(lastVisibleAction); return { lastMessageText, @@ -1413,32 +1500,25 @@ function getLastVisibleMessage(reportID, actionsToMerge = {}) { } // Fetch the last visible message for report represented by reportID and based on actions to merge. - return ReportActionsUtils.getLastVisibleMessage(reportID, actionsToMerge); + return ReportActionsUtils.getLastVisibleMessage(reportID ?? '', actionsToMerge); } /** * Checks if a report is an open task report assigned to current user. * - * @param {Object} report - * @param {Object} [parentReportAction] - The parent report action of the report (Used to check if the task has been canceled) - * @returns {Boolean} + * @param [parentReportAction] - The parent report action of the report (Used to check if the task has been canceled) */ -function isWaitingForAssigneeToCompleteTask(report, parentReportAction = {}) { +function isWaitingForAssigneeToCompleteTask(report: OnyxEntry, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } -/** - * @param {Object} report - * @returns {Boolean} - */ -function isUnreadWithMention(report) { +function isUnreadWithMention(report: OnyxEntry | OptionData): boolean { if (!report) { return false; } - // lastMentionedTime and lastReadTime are both datetime strings and can be compared directly - const lastMentionedTime = report.lastMentionedTime || ''; - const lastReadTime = report.lastReadTime || ''; + const lastMentionedTime = report.lastMentionedTime ?? ''; + const lastReadTime = report.lastReadTime ?? ''; return lastReadTime < lastMentionedTime; } @@ -1448,11 +1528,10 @@ function isUnreadWithMention(report) { - is for an outstanding task waiting on the user - has an outstanding child money request that is waiting for an action from the current user (e.g. pay, approve, add bank account) * - * @param {Object} option (report or optionItem) - * @param {Object} parentReportAction (the report action the current report is a thread of) - * @returns {boolean} + * @param option (report or optionItem) + * @param parentReportAction (the report action the current report is a thread of) */ -function requiresAttentionFromCurrentUser(option, parentReportAction = {}) { +function requiresAttentionFromCurrentUser(option: OnyxEntry | OptionData, parentReportAction: EmptyObject | OnyxEntry = {}) { if (!option) { return false; } @@ -1465,7 +1544,7 @@ function requiresAttentionFromCurrentUser(option, parentReportAction = {}) { return false; } - if (option.isUnreadWithMention || isUnreadWithMention(option)) { + if (Boolean('isUnreadWithMention' in option && option.isUnreadWithMention) || isUnreadWithMention(option)) { return true; } @@ -1484,30 +1563,24 @@ function requiresAttentionFromCurrentUser(option, parentReportAction = {}) { /** * Returns number of transactions that are nonReimbursable * - * @param {Object|null} iouReportID - * @returns {Number} */ -function hasNonReimbursableTransactions(iouReportID) { +function hasNonReimbursableTransactions(iouReportID: string | undefined): boolean { const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return _.filter(allTransactions, (transaction) => transaction.reimbursable === false).length > 0; + return allTransactions.filter((transaction) => transaction.reimbursable === false).length > 0; } -/** - * @param {Object} report - * @param {Object} allReportsDict - * @returns {Number} - */ -function getMoneyRequestReimbursableTotal(report, allReportsDict = null) { - const allAvailableReports = allReportsDict || allReports; - let moneyRequestReport; +function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsDict: OnyxCollection = null): number { + const allAvailableReports = allReportsDict ?? allReports; + let moneyRequestReport: OnyxEntry | undefined; if (isMoneyRequestReport(report)) { moneyRequestReport = report; } - if (allAvailableReports && report.hasOutstandingIOU && report.iouReportID) { + if (allAvailableReports && report?.hasOutstandingIOU && report?.iouReportID) { moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; } if (moneyRequestReport) { - const total = lodashGet(moneyRequestReport, 'total', 0); + const total = moneyRequestReport?.total ?? 0; + if (total !== 0) { // There is a possibility that if the Expense report has a negative total. // This is because there are instances where you can get a credit back on your card, @@ -1518,23 +1591,18 @@ function getMoneyRequestReimbursableTotal(report, allReportsDict = null) { return 0; } -/** - * @param {Object} report - * @param {Object} allReportsDict - * @returns {Object} - */ -function getMoneyRequestSpendBreakdown(report, allReportsDict = null) { - const allAvailableReports = allReportsDict || allReports; +function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict: OnyxCollection = null): SpendBreakdown { + const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; if (isMoneyRequestReport(report)) { moneyRequestReport = report; } - if (allAvailableReports && report.hasOutstandingIOU && report.iouReportID) { + if (allAvailableReports && report?.hasOutstandingIOU && report?.iouReportID) { moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; } if (moneyRequestReport) { - let nonReimbursableSpend = lodashGet(moneyRequestReport, 'nonReimbursableTotal', 0); - let totalSpend = lodashGet(moneyRequestReport, 'total', 0); + let nonReimbursableSpend = moneyRequestReport.nonReimbursableTotal ?? 0; + let totalSpend = moneyRequestReport.total ?? 0; if (nonReimbursableSpend + totalSpend !== 0) { // There is a possibility that if the Expense report has a negative total. @@ -1562,19 +1630,16 @@ function getMoneyRequestSpendBreakdown(report, allReportsDict = null) { /** * Get the title for a policy expense chat which depends on the role of the policy member seeing this report - * - * @param {Object} report - * @param {Object} [policy] - * @returns {String} */ -function getPolicyExpenseChatName(report, policy = undefined) { - const ownerAccountID = report.ownerAccountID; - const personalDetails = allPersonalDetails[ownerAccountID]; +function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string | undefined { + const ownerAccountID = report?.ownerAccountID; + const personalDetails = allPersonalDetails?.[ownerAccountID ?? -1]; const login = personalDetails ? personalDetails.login : null; - const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || login || report.reportName; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const reportOwnerDisplayName = getDisplayNameForParticipant(ownerAccountID) || login || report?.reportName; // If the policy expense chat is owned by this user, use the name of the policy as the report name. - if (report.isOwnPolicyExpenseChat) { + if (report?.isOwnPolicyExpenseChat) { return getPolicyName(report, false, policy); } @@ -1583,7 +1648,7 @@ function getPolicyExpenseChatName(report, policy = undefined) { * Using typical string concatenation here due to performance issues * with template literals. */ - const policyItem = allPolicies[ONYXKEYS.COLLECTION.POLICY + report.policyID]; + const policyItem = allPolicies?.[ONYXKEYS.COLLECTION.POLICY + report?.policyID]; if (policyItem) { policyExpenseChatRole = policyItem.role || 'user'; } @@ -1591,8 +1656,8 @@ function getPolicyExpenseChatName(report, policy = undefined) { // If this user is not admin and this policy expense chat has been archived because of account merging, this must be an old workspace chat // of the account which was merged into the current user's account. Use the name of the policy as the name of the report. if (isArchivedRoom(report)) { - const lastAction = ReportActionsUtils.getLastVisibleAction(report.reportID); - const archiveReason = (lastAction && lastAction.originalMessage && lastAction.originalMessage.reason) || CONST.REPORT.ARCHIVE_REASON.DEFAULT; + const lastAction = ReportActionsUtils.getLastVisibleAction(report?.reportID ?? ''); + const archiveReason = lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? lastAction?.originalMessage?.reason : CONST.REPORT.ARCHIVE_REASON.DEFAULT; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED && policyExpenseChatRole !== CONST.POLICY.ROLE.ADMIN) { return getPolicyName(report, false, policy); } @@ -1605,28 +1670,25 @@ function getPolicyExpenseChatName(report, policy = undefined) { /** * Get the title for an IOU or expense chat which will be showing the payer and the amount * - * @param {Object} report - * @param {Object} [policy] - * @returns {String} */ -function getMoneyRequestReportName(report, policy = undefined) { +function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string { const moneyRequestTotal = getMoneyRequestReimbursableTotal(report); - const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report.currency, hasOnlyDistanceRequestTransactions(report.reportID)); - const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID); + const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); + const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerName, amount: formattedAmount, }); - if (report.isWaitingOnBankAccount) { + if (report?.isWaitingOnBankAccount) { return `${payerPaidAmountMessage} • ${Localize.translateLocal('iou.pending')}`; } - if (hasNonReimbursableTransactions(report.reportID)) { + if (hasNonReimbursableTransactions(report?.reportID)) { return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName, amount: formattedAmount}); } - if (report.hasOutstandingIOU || moneyRequestTotal === 0) { + if (!!report?.hasOutstandingIOU || moneyRequestTotal === 0) { return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); } @@ -1636,16 +1698,16 @@ function getMoneyRequestReportName(report, policy = undefined) { /** * Gets transaction created, amount, currency, comment, and waypoints (for distance request) * into a flat object. Used for displaying transactions and sending them in API commands - * - * @param {Object} transaction - * @param {Object} createdDateFormat - * @returns {Object} */ -function getTransactionDetails(transaction, createdDateFormat = CONST.DATE.FNS_FORMAT_STRING) { - const report = getReport(transaction.reportID); + +function getTransactionDetails(transaction: OnyxEntry, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING): TransactionDetails { + if (!transaction) { + return; + } + const report = getReport(transaction?.reportID); return { created: TransactionUtils.getCreated(transaction, createdDateFormat), - amount: TransactionUtils.getAmount(transaction, isExpenseReport(report)), + amount: TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)), currency: TransactionUtils.getCurrency(transaction), comment: TransactionUtils.getDescription(transaction), merchant: TransactionUtils.getMerchant(transaction), @@ -1668,12 +1730,8 @@ function getTransactionDetails(transaction, createdDateFormat = CONST.DATE.FNS_F * - in case of expense report * - the current user is the requestor and is not settled yet * - or the user is an admin on the policy the expense report is tied to - * - * @param {Object} reportAction - * @param {String} fieldToEdit - * @returns {Boolean} */ -function canEditMoneyRequest(reportAction, fieldToEdit = '') { +function canEditMoneyRequest(reportAction: OnyxEntry, fieldToEdit = ''): boolean { const isDeleted = ReportActionsUtils.isDeletedAction(reportAction); if (isDeleted) { @@ -1681,23 +1739,25 @@ function canEditMoneyRequest(reportAction, fieldToEdit = '') { } // If the report action is not IOU type, return true early - if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { + if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { return true; } - const moneyRequestReportID = lodashGet(reportAction, 'originalMessage.IOUReportID', 0); + const moneyRequestReportID = reportAction?.originalMessage?.IOUReportID ?? 0; if (!moneyRequestReportID) { return false; } - const moneyRequestReport = getReport(moneyRequestReportID); - const isReportSettled = isSettled(moneyRequestReport.reportID); - const isAdmin = isExpenseReport(moneyRequestReport) && lodashGet(getPolicy(moneyRequestReport.policyID), 'role', '') === CONST.POLICY.ROLE.ADMIN; - const isRequestor = currentUserAccountID === reportAction.actorAccountID; + const moneyRequestReport = getReport(String(moneyRequestReportID)); + const isReportSettled = isSettled(moneyRequestReport?.reportID); + const isAdmin = isExpenseReport(moneyRequestReport) && (getPolicy(moneyRequestReport?.policyID ?? '')?.role ?? '') === CONST.POLICY.ROLE.ADMIN; + const isRequestor = currentUserAccountID === reportAction?.actorAccountID; + if (isAdmin && !isRequestor && fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { return false; } + if (isAdmin) { return true; } @@ -1708,14 +1768,10 @@ function canEditMoneyRequest(reportAction, fieldToEdit = '') { /** * Checks if the current user can edit the provided property of a money request * - * @param {Object} reportAction - * @param {String} reportID - * @param {String} fieldToEdit - * @returns {Boolean} */ -function canEditFieldOfMoneyRequest(reportAction, reportID, fieldToEdit) { +function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf): boolean { // A list of fields that cannot be edited by anyone, once a money request has been settled - const nonEditableFieldsWhenSettled = [ + const nonEditableFieldsWhenSettled: string[] = [ CONST.EDIT_REQUEST_FIELD.AMOUNT, CONST.EDIT_REQUEST_FIELD.CURRENCY, CONST.EDIT_REQUEST_FIELD.DATE, @@ -1740,32 +1796,27 @@ function canEditFieldOfMoneyRequest(reportAction, reportID, fieldToEdit) { * - It's an ADDCOMMENT that is not an attachment * - It's money request where conditions for editability are defined in canEditMoneyRequest method * - It's not pending deletion - * - * @param {Object} reportAction - * @returns {Boolean} */ -function canEditReportAction(reportAction) { - const isCommentOrIOU = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; - return ( - reportAction.actorAccountID === currentUserAccountID && - isCommentOrIOU && - canEditMoneyRequest(reportAction) && // Returns true for non-IOU actions - !isReportMessageAttachment(lodashGet(reportAction, ['message', 0], {})) && - !ReportActionsUtils.isDeletedAction(reportAction) && - !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE +function canEditReportAction(reportAction: OnyxEntry): boolean { + const isCommentOrIOU = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; + + return Boolean( + reportAction?.actorAccountID === currentUserAccountID && + isCommentOrIOU && + canEditMoneyRequest(reportAction) && // Returns true for non-IOU actions + !isReportMessageAttachment(reportAction?.message?.[0] ?? {type: '', text: ''}) && + !ReportActionsUtils.isDeletedAction(reportAction) && + !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && + reportAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); } /** * Gets all transactions on an IOU report with a receipt - * - * @param {string|null} iouReportID - * @returns {[Object]} */ -function getTransactionsWithReceipts(iouReportID) { +function getTransactionsWithReceipts(iouReportID: string | undefined): Transaction[] { const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return _.filter(allTransactions, (transaction) => TransactionUtils.hasReceipt(transaction)); + return allTransactions.filter((transaction) => TransactionUtils.hasReceipt(transaction)); } /** @@ -1774,38 +1825,29 @@ function getTransactionsWithReceipts(iouReportID) { * all requests are receipts that are being SmartScanned. As soon as we have a non-receipt request, * or as soon as one receipt request is done scanning, we have at least one * "ready" money request, and we remove this indicator to show the partial report total. - * - * @param {Object|null} iouReportID - * @param {Object|null} reportPreviewAction the preview action associated with the IOU report - * @returns {Boolean} */ -function areAllRequestsBeingSmartScanned(iouReportID, reportPreviewAction) { +function areAllRequestsBeingSmartScanned(iouReportID: string, reportPreviewAction: OnyxEntry): boolean { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); // If we have more requests than requests with receipts, we have some manual requests if (ReportActionsUtils.getNumberOfMoneyRequests(reportPreviewAction) > transactionsWithReceipts.length) { return false; } - return _.all(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)); + return transactionsWithReceipts.every((transaction) => TransactionUtils.isReceiptBeingScanned(transaction)); } /** * Check if any of the transactions in the report has required missing fields * - * @param {Object|null} iouReportID - * @returns {Boolean} */ -function hasMissingSmartscanFields(iouReportID) { +function hasMissingSmartscanFields(iouReportID: string): boolean { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); - return _.some(transactionsWithReceipts, (transaction) => TransactionUtils.hasMissingSmartscanFields(transaction)); + return transactionsWithReceipts.some((transaction) => TransactionUtils.hasMissingSmartscanFields(transaction)); } /** * Given a parent IOU report action get report name for the LHN. - * - * @param {Object} reportAction - * @returns {String} */ -function getTransactionReportName(reportAction) { +function getTransactionReportName(reportAction: OnyxEntry): string { if (ReportActionsUtils.isReversedTransaction(reportAction)) { return Localize.translateLocal('parentReportAction.reversedTransaction'); } @@ -1815,6 +1857,9 @@ function getTransactionReportName(reportAction) { } const transaction = TransactionUtils.getLinkedTransaction(reportAction); + if (!isNotEmptyObject(transaction)) { + return ''; + } if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -1823,45 +1868,49 @@ function getTransactionReportName(reportAction) { return Localize.translateLocal('iou.receiptMissingDetails'); } - const {amount, currency, comment} = getTransactionDetails(transaction); + const transactionDetails = getTransactionDetails(transaction); return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(amount, currency, TransactionUtils.isDistanceRequest(transaction)), - comment, + formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency, TransactionUtils.isDistanceRequest(transaction)) ?? '', + comment: transactionDetails?.comment ?? '', }); } /** * Get money request message for an IOU report * - * @param {Object} report - * @param {Object} [reportAction={}] This can be either a report preview action or the IOU action - * @param {Boolean} [shouldConsiderReceiptBeingScanned=false] - * @param {Boolean} isPreviewMessageForParentChatReport - * @param {Object} [policy] - * @returns {String} - */ -function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceiptBeingScanned = false, isPreviewMessageForParentChatReport = false, policy = undefined) { - const reportActionMessage = lodashGet(reportAction, 'message[0].html', ''); - - if (_.isEmpty(report) || !report.reportID) { + * @param [reportAction] This can be either a report preview action or the IOU action + */ +function getReportPreviewMessage( + report: OnyxEntry, + reportAction: OnyxEntry | EmptyObject = {}, + shouldConsiderReceiptBeingScanned = false, + isPreviewMessageForParentChatReport = false, + policy: OnyxEntry = null, +): string { + const reportActionMessage = reportAction?.message?.[0].html ?? ''; + if (isEmptyObject(report) || !report?.reportID) { // The iouReport is not found locally after SignIn because the OpenApp API won't return iouReports if they're settled // As a temporary solution until we know how to solve this the best, we just use the message that returned from BE return reportActionMessage; } - if (!isIOUReport(report) && ReportActionsUtils.isSplitBillAction(reportAction)) { + if (isNotEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { // This covers group chats where the last action is a split bill action const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (_.isEmpty(linkedTransaction)) { + if (isEmptyObject(linkedTransaction)) { return reportActionMessage; } - if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { - return Localize.translateLocal('iou.receiptScanning'); + + if (isNotEmptyObject(linkedTransaction)) { + if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + return Localize.translateLocal('iou.receiptScanning'); + } + + const transactionDetails = getTransactionDetails(linkedTransaction); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); + return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); } - const {amount, currency, comment} = getTransactionDetails(linkedTransaction); - const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); - return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment}); } const totalAmount = getMoneyRequestReimbursableTotal(report); @@ -1872,10 +1921,10 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip return `approved ${formattedAmount}`; } - if (shouldConsiderReceiptBeingScanned && ReportActionsUtils.isMoneyRequestAction(reportAction)) { + if (isNotEmptyObject(reportAction) && shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (!_.isEmpty(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + if (isNotEmptyObject(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } } @@ -1883,38 +1932,32 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip // Show Paid preview message if it's settled or if the amount is paid & stuck at receivers end for only chat reports. if (isSettled(report.reportID) || (report.isWaitingOnBankAccount && isPreviewMessageForParentChatReport)) { // A settled report preview message can come in three formats "paid ... elsewhere" or "paid ... with Expensify" - let translatePhraseKey = 'iou.paidElsewhereWithAmount'; + let translatePhraseKey: TranslationPaths = 'iou.paidElsewhereWithAmount'; + const originalMessage = reportAction?.originalMessage as IOUMessage | undefined; if ( - _.contains([CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY], lodashGet(reportAction, 'originalMessage.paymentType')) || - reportActionMessage.match(/ (with Expensify|using Expensify)$/) || + [CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].some((paymentType) => paymentType === originalMessage?.paymentType) || + !!reportActionMessage.match(/ (with Expensify|using Expensify)$/) || report.isWaitingOnBankAccount ) { translatePhraseKey = 'iou.paidWithExpensifyWithAmount'; } - return Localize.translateLocal(translatePhraseKey, {amount: formattedAmount, payer: payerName}); + return Localize.translateLocal(translatePhraseKey, {amount: formattedAmount, payer: payerName ?? ''}); } if (report.isWaitingOnBankAccount) { - const submitterDisplayName = getDisplayNameForParticipant(report.ownerAccountID, true); + const submitterDisplayName = getDisplayNameForParticipant(report.ownerAccountID ?? -1, true) ?? ''; return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID); - return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); + return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); } /** * Get the proper message schema for modified expense message. - * - * @param {String} newValue - * @param {String} oldValue - * @param {String} valueName - * @param {Boolean} valueInQuotes - * @param {Boolean} shouldConvertToLowercase - * @returns {String} */ -function getProperSchemaForModifiedExpenseMessage(newValue, oldValue, valueName, valueInQuotes, shouldConvertToLowercase = true) { +function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true): string { const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; @@ -1930,15 +1973,8 @@ function getProperSchemaForModifiedExpenseMessage(newValue, oldValue, valueName, /** * Get the proper message schema for modified distance message. - * - * @param {String} newDistance - * @param {String} oldDistance - * @param {String} newAmount - * @param {String} oldAmount - * @returns {String} */ - -function getProperSchemaForModifiedDistanceMessage(newDistance, oldDistance, newAmount, oldAmount) { +function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string): string { if (!oldDistance) { return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); } @@ -1955,80 +1991,99 @@ function getProperSchemaForModifiedDistanceMessage(newDistance, oldDistance, new * * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. - * - * @param {Object} reportAction - * @returns {String} */ -function getModifiedExpenseMessage(reportAction) { - const reportActionOriginalMessage = lodashGet(reportAction, 'originalMessage', {}); - if (_.isEmpty(reportActionOriginalMessage)) { +function getModifiedExpenseMessage(reportAction: OnyxEntry): string | undefined { + const reportActionOriginalMessage = reportAction?.originalMessage as ExpenseOriginalMessage | undefined; + if (isEmptyObject(reportActionOriginalMessage)) { return Localize.translateLocal('iou.changedTheRequest'); } - const reportID = lodashGet(reportAction, 'reportID', ''); - const policyID = lodashGet(getReport(reportID), 'policyID', ''); + const reportID = reportAction?.reportID ?? ''; + const policyID = getReport(reportID)?.policyID ?? ''; const policyTags = getPolicyTags(policyID); const policyTag = PolicyUtils.getTag(policyTags); - const policyTagListName = lodashGet(policyTag, 'name', Localize.translateLocal('common.tag')); + const policyTagListName = policyTag?.name ?? Localize.translateLocal('common.tag'); const hasModifiedAmount = - _.has(reportActionOriginalMessage, 'oldAmount') && - _.has(reportActionOriginalMessage, 'oldCurrency') && - _.has(reportActionOriginalMessage, 'amount') && - _.has(reportActionOriginalMessage, 'currency'); + reportActionOriginalMessage && + 'oldAmount' in reportActionOriginalMessage && + 'oldCurrency' in reportActionOriginalMessage && + 'amount' in reportActionOriginalMessage && + 'currency' in reportActionOriginalMessage; - const hasModifiedMerchant = _.has(reportActionOriginalMessage, 'oldMerchant') && _.has(reportActionOriginalMessage, 'merchant'); + const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage; if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage.oldCurrency; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount, oldCurrency); + const oldCurrency = reportActionOriginalMessage?.oldCurrency; + const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency ?? ''); - const currency = reportActionOriginalMessage.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount, currency); + const currency = reportActionOriginalMessage?.currency; + const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.amount ?? 0, currency); // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && reportActionOriginalMessage.merchant.includes('@')) { - return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, amount, oldAmount); + if (hasModifiedMerchant && reportActionOriginalMessage?.merchant?.includes('@')) { + return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage?.merchant, reportActionOriginalMessage?.oldMerchant ?? '', amount, oldAmount); } return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); } - const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment'); + const hasModifiedComment = reportActionOriginalMessage && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; if (hasModifiedComment) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.newComment, reportActionOriginalMessage.oldComment, Localize.translateLocal('common.description'), true); + return getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage?.newComment ?? '', + reportActionOriginalMessage?.oldComment ?? '', + Localize.translateLocal('common.description'), + true, + ); } - const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created'); + const hasModifiedCreated = reportActionOriginalMessage && 'oldCreated' in reportActionOriginalMessage && 'created' in reportActionOriginalMessage; if (hasModifiedCreated) { // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated = new Date(reportActionOriginalMessage.oldCreated); + let formattedOldCreated: Date | string = new Date(reportActionOriginalMessage?.oldCreated ?? 0); formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.created, formattedOldCreated, Localize.translateLocal('common.date'), false); + + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.created ?? '', formattedOldCreated?.toString?.(), Localize.translateLocal('common.date'), false); } if (hasModifiedMerchant) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.merchant, reportActionOriginalMessage.oldMerchant, Localize.translateLocal('common.merchant'), true); + return getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage?.merchant ?? '', + reportActionOriginalMessage?.oldMerchant ?? '', + Localize.translateLocal('common.merchant'), + true, + ); } - const hasModifiedCategory = _.has(reportActionOriginalMessage, 'oldCategory') && _.has(reportActionOriginalMessage, 'category'); + const hasModifiedCategory = reportActionOriginalMessage && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; if (hasModifiedCategory) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); + return getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage?.category ?? '', + reportActionOriginalMessage?.oldCategory ?? '', + Localize.translateLocal('common.category'), + true, + ); } - const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); + const hasModifiedTag = reportActionOriginalMessage && 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; if (hasModifiedTag) { return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.tag, - reportActionOriginalMessage.oldTag, + reportActionOriginalMessage.tag ?? '', + reportActionOriginalMessage.oldTag ?? '', policyTagListName, true, policyTagListName === Localize.translateLocal('common.tag'), ); } - const hasModifiedBillable = _.has(reportActionOriginalMessage, 'oldBillable') && _.has(reportActionOriginalMessage, 'billable'); + const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; if (hasModifiedBillable) { - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.billable, reportActionOriginalMessage.oldBillable, Localize.translateLocal('iou.request'), true); + return getProperSchemaForModifiedExpenseMessage( + reportActionOriginalMessage?.billable ?? '', + reportActionOriginalMessage?.oldBillable ?? '', + Localize.translateLocal('iou.request'), + true, + ); } } @@ -2037,53 +2092,47 @@ function getModifiedExpenseMessage(reportAction) { * object of the modified expense action. * * At the moment, we only allow changing one transaction field at a time. - * - * @param {Object} oldTransaction - * @param {Object} transactionChanges - * @param {Boolean} isFromExpenseReport - * @returns {Object} */ -function getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport) { - const originalMessage = {}; - +function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, transactionChanges: ExpenseOriginalMessage, isFromExpenseReport: boolean): ExpenseOriginalMessage { + const originalMessage: ExpenseOriginalMessage = {}; // Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment), // all others have old/- pattern such as oldCreated/created - if (_.has(transactionChanges, 'comment')) { + if ('comment' in transactionChanges) { originalMessage.oldComment = TransactionUtils.getDescription(oldTransaction); - originalMessage.newComment = transactionChanges.comment; + originalMessage.newComment = transactionChanges?.comment; } - if (_.has(transactionChanges, 'created')) { + if ('created' in transactionChanges) { originalMessage.oldCreated = TransactionUtils.getCreated(oldTransaction); - originalMessage.created = transactionChanges.created; + originalMessage.created = transactionChanges?.created; } - if (_.has(transactionChanges, 'merchant')) { + if ('merchant' in transactionChanges) { originalMessage.oldMerchant = TransactionUtils.getMerchant(oldTransaction); - originalMessage.merchant = transactionChanges.merchant; + originalMessage.merchant = transactionChanges?.merchant; } // The amount is always a combination of the currency and the number value so when one changes we need to store both // to match how we handle the modified expense action in oldDot - if (_.has(transactionChanges, 'amount') || _.has(transactionChanges, 'currency')) { + if ('amount' in transactionChanges || 'currency' in transactionChanges) { originalMessage.oldAmount = TransactionUtils.getAmount(oldTransaction, isFromExpenseReport); - originalMessage.amount = lodashGet(transactionChanges, 'amount', originalMessage.oldAmount); + originalMessage.amount = transactionChanges?.amount ?? transactionChanges.oldAmount; originalMessage.oldCurrency = TransactionUtils.getCurrency(oldTransaction); - originalMessage.currency = lodashGet(transactionChanges, 'currency', originalMessage.oldCurrency); + originalMessage.currency = transactionChanges?.currency ?? transactionChanges.oldCurrency; } - if (_.has(transactionChanges, 'category')) { + if ('category' in transactionChanges) { originalMessage.oldCategory = TransactionUtils.getCategory(oldTransaction); - originalMessage.category = transactionChanges.category; + originalMessage.category = transactionChanges?.category; } - if (_.has(transactionChanges, 'tag')) { + if ('tag' in transactionChanges) { originalMessage.oldTag = TransactionUtils.getTag(oldTransaction); - originalMessage.tag = transactionChanges.tag; + originalMessage.tag = transactionChanges?.tag; } - if (_.has(transactionChanges, 'billable')) { + if ('billable' in transactionChanges) { const oldBillable = TransactionUtils.getBillable(oldTransaction); originalMessage.oldBillable = oldBillable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); - originalMessage.billable = transactionChanges.billable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); + originalMessage.billable = transactionChanges?.billable ? Localize.translateLocal('common.billable').toLowerCase() : Localize.translateLocal('common.nonBillable').toLowerCase(); } return originalMessage; @@ -2091,63 +2140,53 @@ function getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, i /** * Returns the parentReport if the given report is a thread. - * - * @param {Object} report - * @returns {Object} */ -function getParentReport(report) { - if (!report || !report.parentReportID) { +function getParentReport(report: OnyxEntry): OnyxEntry | EmptyObject { + if (!report?.parentReportID) { return {}; } - return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, {}); + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`] ?? {}; } /** * Returns the root parentReport if the given report is nested. * Uses recursion to iterate any depth of nested reports. - * - * @param {Object} report - * @returns {Object} */ -function getRootParentReport(report) { +function getRootParentReport(report: OnyxEntry): OnyxEntry | EmptyObject { if (!report) { return {}; } // Returns the current report as the root report, because it does not have a parentReportID - if (!report.parentReportID) { + if (!report?.parentReportID) { return report; } - const parentReport = getReport(report.parentReportID); + const parentReport = getReport(report?.parentReportID); // Runs recursion to iterate a parent report - return getRootParentReport(parentReport); + return getRootParentReport(isNotEmptyObject(parentReport) ? parentReport : null); } /** * Get the title for a report. - * - * @param {Object} report - * @param {Object} [policy] - * @returns {String} */ -function getReportName(report, policy = undefined) { - let formattedName; +function getReportName(report: OnyxEntry, policy: OnyxEntry = null): string { + let formattedName: string | undefined; const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (isChatThread(report)) { - if (ReportActionsUtils.isTransactionThread(parentReportAction)) { + if (isNotEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) { return getTransactionReportName(parentReportAction); } - const isAttachment = ReportActionsUtils.isReportActionAttachment(parentReportAction); - const parentReportActionMessage = lodashGet(parentReportAction, ['message', 0, 'text'], '').replace(/(\r\n|\n|\r)/gm, ' '); + const isAttachment = ReportActionsUtils.isReportActionAttachment(isNotEmptyObject(parentReportAction) ? parentReportAction : null); + const parentReportActionMessage = (parentReportAction?.message?.[0]?.text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { return `[${Localize.translateLocal('common.attachment')}]`; } if ( - lodashGet(parentReportAction, 'message[0].moderationDecision.decision') === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || - lodashGet(parentReportAction, 'message[0].moderationDecision.decision') === CONST.MODERATION.MODERATOR_DECISION_HIDDEN + parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || + parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN ) { return Localize.translateLocal('parentReportAction.hiddenMessage'); } @@ -2159,7 +2198,7 @@ function getReportName(report, policy = undefined) { } if (isChatRoom(report) || isTaskReport(report)) { - formattedName = report.reportName; + formattedName = report?.reportName; } if (isPolicyExpenseChat(report)) { @@ -2179,22 +2218,25 @@ function getReportName(report, policy = undefined) { } // Not a room or PolicyExpenseChat, generate title from participants - const participantAccountIDs = (report && report.participantAccountIDs) || []; - const participantsWithoutCurrentUser = _.without(participantAccountIDs, currentUserAccountID); + const participantAccountIDs = report?.participantAccountIDs ?? []; + const participantsWithoutCurrentUser = participantAccountIDs.filter((accountID) => accountID !== currentUserAccountID); const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1; - return _.map(participantsWithoutCurrentUser, (accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport)).join(', '); + return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport)).join(', '); } /** * Recursively navigates through thread parents to get the root report and workspace name. * The recursion stops when we find a non thread or money request report, whichever comes first. - * @param {Object} report - * @returns {Object} */ -function getRootReportAndWorkspaceName(report) { +function getRootReportAndWorkspaceName(report: OnyxEntry): ReportAndWorkspaceName { + if (!report) { + return { + rootReportName: '', + }; + } if (isChildReport(report) && !isMoneyRequestReport(report) && !isTaskReport(report)) { - const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; return getRootReportAndWorkspaceName(parentReport); } @@ -2218,10 +2260,8 @@ function getRootReportAndWorkspaceName(report) { /** * Get either the policyName or domainName the chat is tied to - * @param {Object} report - * @returns {String} */ -function getChatRoomSubtitle(report) { +function getChatRoomSubtitle(report: OnyxEntry): string | undefined { if (isChatThread(report)) { return ''; } @@ -2230,27 +2270,25 @@ function getChatRoomSubtitle(report) { } if (getChatType(report) === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL) { // The domainAll rooms are just #domainName, so we ignore the prefix '#' to get the domainName - return report.reportName.substring(1); + return report?.reportName?.substring(1) ?? ''; } - if ((isPolicyExpenseChat(report) && report.isOwnPolicyExpenseChat) || isExpenseReport(report)) { + if ((isPolicyExpenseChat(report) && !!report?.isOwnPolicyExpenseChat) || isExpenseReport(report)) { return Localize.translateLocal('workspace.common.workspace'); } if (isArchivedRoom(report)) { - return report.oldPolicyName || ''; + return report?.oldPolicyName ?? ''; } return getPolicyName(report); } /** * Gets the parent navigation subtitle for the report - * @param {Object} report - * @returns {Object} */ -function getParentNavigationSubtitle(report) { +function getParentNavigationSubtitle(report: OnyxEntry): ReportAndWorkspaceName | EmptyObject { if (isThread(report)) { - const parentReport = lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]); + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; const {rootReportName, workspaceName} = getRootReportAndWorkspaceName(parentReport); - if (_.isEmpty(rootReportName)) { + if (!rootReportName) { return {}; } @@ -2262,29 +2300,28 @@ function getParentNavigationSubtitle(report) { /** * Navigate to the details page of a given report * - * @param {Object} report */ -function navigateToDetailsPage(report) { - const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []); +function navigateToDetailsPage(report: OnyxEntry) { + const participantAccountIDs = report?.participantAccountIDs ?? []; if (isOneOnOneChat(report)) { Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountIDs[0])); return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID)); + if (report?.reportID) { + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID)); + } } /** * Go back to the details page of a given report - * - * @param {Object} report */ -function goBackToDetailsPage(report) { +function goBackToDetailsPage(report: OnyxEntry) { if (isOneOnOneChat(report)) { - Navigation.goBack(ROUTES.PROFILE.getRoute(report.participantAccountIDs[0])); + Navigation.goBack(ROUTES.PROFILE.getRoute(report?.participantAccountIDs?.[0] ?? '')); return; } - Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID)); + Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '')); } /** @@ -2294,48 +2331,33 @@ function goBackToDetailsPage(report) { * * In a test of 500M reports (28 years of reports at our current max rate) we got 20-40 collisions meaning that * this is more than random enough for our needs. - * - * @returns {String} */ -function generateReportID() { +function generateReportID(): string { return (Math.floor(Math.random() * 2 ** 21) * 2 ** 32 + Math.floor(Math.random() * 2 ** 32)).toString(); } -/** - * @param {Object} report - * @returns {Boolean} - */ -function hasReportNameError(report) { - return !_.isEmpty(lodashGet(report, 'errorFields.reportName', {})); +function hasReportNameError(report: OnyxEntry): boolean { + return !isEmptyObject(report?.errorFields?.reportName); } /** * For comments shorter than or equal to 10k chars, convert the comment from MD into HTML because that's how it is stored in the database * For longer comments, skip parsing, but still escape the text, and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! - * - * @param {String} text - * @returns {String} */ -function getParsedComment(text) { +function getParsedComment(text: string): string { const parser = new ExpensiMark(); - return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : _.escape(text); + return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : lodashEscape(text); } -/** - * @param {String} [text] - * @param {File} [file] - * @returns {Object} - */ -function buildOptimisticAddCommentReportAction(text, file) { +function buildOptimisticAddCommentReportAction(text?: string, file?: File & {source: string; uri: string}): OptimisticReportAction { const parser = new ExpensiMark(); - const commentText = getParsedComment(text); - const isAttachment = _.isEmpty(text) && file !== undefined; + const commentText = getParsedComment(text ?? ''); + const isAttachment = !text && file !== undefined; const attachmentInfo = isAttachment ? file : {}; const htmlForNewComment = isAttachment ? CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML : commentText; // Remove HTML from text when applying optimistic offline comment const textForNewComment = isAttachment ? CONST.ATTACHMENT_MESSAGE_TEXT : parser.htmlToText(htmlForNewComment); - return { commentText, reportAction: { @@ -2345,12 +2367,12 @@ function buildOptimisticAddCommentReportAction(text, file) { person: [ { style: 'strong', - text: lodashGet(allPersonalDetails, [currentUserAccountID, 'displayName'], currentUserEmail), + text: allPersonalDetails?.[currentUserAccountID ?? -1]?.displayName ?? currentUserEmail, type: 'TEXT', }, ], automatic: false, - avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: allPersonalDetails?.[currentUserAccountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), created: DateUtils.getDBTime(), message: [ { @@ -2371,25 +2393,24 @@ function buildOptimisticAddCommentReportAction(text, file) { /** * update optimistic parent reportAction when a comment is added or remove in the child report - * @param {String} parentReportAction - Parent report action of the child report - * @param {String} lastVisibleActionCreated - Last visible action created of the child report - * @param {String} type - The type of action in the child report - * @returns {Object} + * @param parentReportAction - Parent report action of the child report + * @param lastVisibleActionCreated - Last visible action created of the child report + * @param type - The type of action in the child report */ -function updateOptimisticParentReportAction(parentReportAction, lastVisibleActionCreated, type) { - let childVisibleActionCount = parentReportAction.childVisibleActionCount || 0; - let childCommenterCount = parentReportAction.childCommenterCount || 0; - let childOldestFourAccountIDs = parentReportAction.childOldestFourAccountIDs; +function updateOptimisticParentReportAction(parentReportAction: OnyxEntry, lastVisibleActionCreated: string, type: string): UpdateOptimisticParentReportAction { + let childVisibleActionCount = parentReportAction?.childVisibleActionCount ?? 0; + let childCommenterCount = parentReportAction?.childCommenterCount ?? 0; + let childOldestFourAccountIDs = parentReportAction?.childOldestFourAccountIDs; if (type === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { childVisibleActionCount += 1; const oldestFourAccountIDs = childOldestFourAccountIDs ? childOldestFourAccountIDs.split(',') : []; if (oldestFourAccountIDs.length < 4) { - const index = _.findIndex(oldestFourAccountIDs, (accountID) => accountID === currentUserAccountID.toString()); + const index = oldestFourAccountIDs.findIndex((accountID) => accountID === currentUserAccountID?.toString()); if (index === -1) { childCommenterCount += 1; - oldestFourAccountIDs.push(currentUserAccountID); + oldestFourAccountIDs.push(currentUserAccountID?.toString() ?? ''); } } childOldestFourAccountIDs = oldestFourAccountIDs.join(','); @@ -2414,48 +2435,51 @@ function updateOptimisticParentReportAction(parentReportAction, lastVisibleActio /** * Get optimistic data of parent report action - * @param {String} reportID The reportID of the report that is updated - * @param {String} lastVisibleActionCreated Last visible action created of the child report - * @param {String} type The type of action in the child report - * @param {String} parentReportID Custom reportID to be updated - * @param {String} parentReportActionID Custom reportActionID to be updated - * @returns {Object} - */ -function getOptimisticDataForParentReportAction(reportID, lastVisibleActionCreated, type, parentReportID = '', parentReportActionID = '') { + * @param reportID The reportID of the report that is updated + * @param lastVisibleActionCreated Last visible action created of the child report + * @param type The type of action in the child report + * @param parentReportID Custom reportID to be updated + * @param parentReportActionID Custom reportActionID to be updated + */ +function getOptimisticDataForParentReportAction(reportID: string, lastVisibleActionCreated: string, type: string, parentReportID = '', parentReportActionID = ''): OnyxUpdate | EmptyObject { const report = getReport(reportID); + if (!report || !isNotEmptyObject(report)) { + return {}; + } const parentReportAction = ReportActionsUtils.getParentReportAction(report); - if (_.isEmpty(parentReportAction)) { + if (!parentReportAction || !isNotEmptyObject(parentReportAction)) { return {}; } const optimisticParentReportAction = updateOptimisticParentReportAction(parentReportAction, lastVisibleActionCreated, type); return { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID || report.parentReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID || report?.parentReportID}`, value: { - [parentReportActionID || report.parentReportActionID]: optimisticParentReportAction, + [parentReportActionID || (report?.parentReportActionID ?? '')]: optimisticParentReportAction, }, }; } /** * Builds an optimistic reportAction for the parent report when a task is created - * @param {String} taskReportID - Report ID of the task - * @param {String} taskTitle - Title of the task - * @param {Number} taskAssigneeAccountID - AccountID of the person assigned to the task - * @param {String} text - Text of the comment - * @param {String} parentReportID - Report ID of the parent report - * @returns {Object} - */ -function buildOptimisticTaskCommentReportAction(taskReportID, taskTitle, taskAssigneeAccountID, text, parentReportID) { + * @param taskReportID - Report ID of the task + * @param taskTitle - Title of the task + * @param taskAssigneeAccountID - AccountID of the person assigned to the task + * @param text - Text of the comment + * @param parentReportID - Report ID of the parent report + */ +function buildOptimisticTaskCommentReportAction(taskReportID: string, taskTitle: string, taskAssigneeAccountID: number, text: string, parentReportID: string): OptimisticReportAction { const reportAction = buildOptimisticAddCommentReportAction(text); - reportAction.reportAction.message[0].taskReportID = taskReportID; + if (reportAction.reportAction.message) { + reportAction.reportAction.message[0].taskReportID = taskReportID; + } // These parameters are not saved on the reportAction, but are used to display the task in the UI // Added when we fetch the reportActions on a report reportAction.reportAction.originalMessage = { - html: reportAction.reportAction.message[0].html, - taskReportID: reportAction.reportAction.message[0].taskReportID, + html: reportAction.reportAction.message?.[0].html, + taskReportID: reportAction.reportAction.message?.[0].taskReportID, }; reportAction.reportAction.childReportID = taskReportID; reportAction.reportAction.parentReportID = parentReportID; @@ -2471,19 +2495,18 @@ function buildOptimisticTaskCommentReportAction(taskReportID, taskTitle, taskAss /** * Builds an optimistic IOU report with a randomly generated reportID * - * @param {Number} payeeAccountID - AccountID of the person generating the IOU. - * @param {Number} payerAccountID - AccountID of the other person participating in the IOU. - * @param {Number} total - IOU amount in the smallest unit of the currency. - * @param {String} chatReportID - Report ID of the chat where the IOU is. - * @param {String} currency - IOU currency. - * @param {Boolean} isSendingMoney - If we send money the IOU should be created as settled - * - * @returns {Object} + * @param payeeAccountID - AccountID of the person generating the IOU. + * @param payerAccountID - AccountID of the other person participating in the IOU. + * @param total - IOU amount in the smallest unit of the currency. + * @param chatReportID - Report ID of the chat where the IOU is. + * @param currency - IOU currency. + * @param isSendingMoney - If we send money the IOU should be created as settled */ -function buildOptimisticIOUReport(payeeAccountID, payerAccountID, total, chatReportID, currency, isSendingMoney = false) { + +function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number, total: number, chatReportID: string, currency: string, isSendingMoney = false): OptimisticIOUReport { const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency); const personalDetails = getPersonalDetailsForAccountID(payerAccountID); - const payerEmail = personalDetails.login; + const payerEmail = 'login' in personalDetails ? personalDetails.login : ''; return { // If we're sending money, hasOutstandingIOU should be false hasOutstandingIOU: !isSendingMoney, @@ -2510,22 +2533,21 @@ function buildOptimisticIOUReport(payeeAccountID, payerAccountID, total, chatRep /** * Builds an optimistic Expense report with a randomly generated reportID * - * @param {String} chatReportID - Report ID of the PolicyExpenseChat where the Expense Report is - * @param {String} policyID - The policy ID of the PolicyExpenseChat - * @param {Number} payeeAccountID - AccountID of the employee (payee) - * @param {Number} total - Amount in cents - * @param {String} currency - * - * @returns {Object} + * @param chatReportID - Report ID of the PolicyExpenseChat where the Expense Report is + * @param policyID - The policy ID of the PolicyExpenseChat + * @param payeeAccountID - AccountID of the employee (payee) + * @param total - Amount in cents + * @param currency */ -function buildOptimisticExpenseReport(chatReportID, policyID, payeeAccountID, total, currency) { + +function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string): OptimisticExpenseReport { // The amount for Expense reports are stored as negative value in the database const storedTotal = total * -1; - const policyName = getPolicyName(allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]); + const policyName = getPolicyName(allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]); const formattedTotal = CurrencyUtils.convertToDisplayString(storedTotal, currency); // The expense report is always created with the policy's output currency - const outputCurrency = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, 'outputCurrency'], CONST.CURRENCY.USD); + const outputCurrency = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.outputCurrency ?? CONST.CURRENCY.USD; return { reportID: generateReportID(), @@ -2547,19 +2569,19 @@ function buildOptimisticExpenseReport(chatReportID, policyID, payeeAccountID, to } /** - * @param {String} iouReportID - the report ID of the IOU report the action belongs to - * @param {String} type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split) - * @param {Number} total - IOU total in cents - * @param {String} comment - IOU comment - * @param {String} currency - IOU currency - * @param {String} paymentType - IOU paymentMethodType. Can be oneOf(Elsewhere, Expensify) - * @param {Boolean} isSettlingUp - Whether we are settling up an IOU - * @returns {Array} + * @param iouReportID - the report ID of the IOU report the action belongs to + * @param type - IOUReportAction type. Can be oneOf(create, decline, cancel, pay, split) + * @param total - IOU total in cents + * @param comment - IOU comment + * @param currency - IOU currency + * @param paymentType - IOU paymentMethodType. Can be oneOf(Elsewhere, Expensify) + * @param isSettlingUp - Whether we are settling up an IOU */ -function getIOUReportActionMessage(iouReportID, type, total, comment, currency, paymentType = '', isSettlingUp = false) { +function getIOUReportActionMessage(iouReportID: string, type: string, total: number, comment: string, currency: string, paymentType = '', isSettlingUp = false): [Message] { + const report = getReport(iouReportID); const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY - ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(getReport(iouReportID)), currency) + ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(isNotEmptyObject(report) ? report : null), currency) : CurrencyUtils.convertToDisplayString(total, currency); let paymentMethodMessage; @@ -2599,8 +2621,8 @@ function getIOUReportActionMessage(iouReportID, type, total, comment, currency, return [ { - html: _.escape(iouMessage), - text: iouMessage, + html: lodashEscape(iouMessage), + text: iouMessage ?? '', isEdited: false, type: CONST.REPORT.MESSAGE.TYPE.COMMENT, }, @@ -2610,39 +2632,38 @@ function getIOUReportActionMessage(iouReportID, type, total, comment, currency, /** * Builds an optimistic IOU reportAction object * - * @param {String} type - IOUReportAction type. Can be oneOf(create, delete, pay, split). - * @param {Number} amount - IOU amount in cents. - * @param {String} currency - * @param {String} comment - User comment for the IOU. - * @param {Array} participants - An array with participants details. - * @param {String} [transactionID] - Not required if the IOUReportAction type is 'pay' - * @param {String} [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, Expensify). - * @param {String} [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. - * @param {Boolean} [isSettlingUp] - Whether we are settling up an IOU. - * @param {Boolean} [isSendMoneyFlow] - Whether this is send money flow - * @param {Object} [receipt] - * @param {Boolean} [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat - * @param {String} [created] - Action created time - * @returns {Object} + * @param type - IOUReportAction type. Can be oneOf(create, delete, pay, split). + * @param amount - IOU amount in cents. + * @param currency + * @param comment - User comment for the IOU. + * @param participants - An array with participants details. + * @param [transactionID] - Not required if the IOUReportAction type is 'pay' + * @param [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, Expensify). + * @param [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. + * @param [isSettlingUp] - Whether we are settling up an IOU. + * @param [isSendMoneyFlow] - Whether this is send money flow + * @param [receipt] + * @param [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat */ + function buildOptimisticIOUReportAction( - type, - amount, - currency, - comment, - participants, - transactionID = '', - paymentType = '', + type: ValueOf, + amount: number, + currency: string, + comment: string, + participants: Participant[], + transactionID: string, + paymentType: DeepValueOf, iouReportID = '', isSettlingUp = false, isSendMoneyFlow = false, - receipt = {}, + receipt: Receipt = {}, isOwnPolicyExpenseChat = false, created = DateUtils.getDBTime(), -) { +): OptimisticIOUReportAction { const IOUReportID = iouReportID || generateReportID(); - const originalMessage = { + const originalMessage: IOUMessage = { amount, comment, currency, @@ -2654,7 +2675,8 @@ function buildOptimisticIOUReportAction( if (type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { // In send money flow, we store amount, comment, currency in IOUDetails when type = pay if (isSendMoneyFlow) { - _.each(['amount', 'comment', 'currency'], (key) => { + const keys = ['amount', 'comment', 'currency'] as const; + keys.forEach((key) => { delete originalMessage[key]; }); originalMessage.IOUDetails = {amount, comment, currency}; @@ -2673,9 +2695,11 @@ function buildOptimisticIOUReportAction( delete originalMessage.IOUReportID; // Split bill made from a policy expense chat only have the payee's accountID as the participant because the payer could be any policy admin if (isOwnPolicyExpenseChat) { - originalMessage.participantAccountIDs = [currentUserAccountID]; + originalMessage.participantAccountIDs = currentUserAccountID ? [currentUserAccountID] : []; } else { - originalMessage.participantAccountIDs = [currentUserAccountID, ..._.pluck(participants, 'accountID')]; + originalMessage.participantAccountIDs = currentUserAccountID + ? [currentUserAccountID, ...participants.map((participant) => participant.accountID)] + : participants.map((participant) => participant.accountID); } } @@ -2683,14 +2707,14 @@ function buildOptimisticIOUReportAction( actionName: CONST.REPORT.ACTIONS.TYPE.IOU, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), isAttachment: false, originalMessage, message: getIOUReportActionMessage(iouReportID, type, amount, comment, currency, paymentType, isSettlingUp), person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserEmail), + text: currentUserPersonalDetails?.displayName ?? currentUserEmail, type: 'TEXT', }, ], @@ -2698,20 +2722,14 @@ function buildOptimisticIOUReportAction( shouldShow: true, created, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - whisperedToAccountIDs: _.contains([CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING], receipt.state) ? [currentUserAccountID] : [], + whisperedToAccountIDs: [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === receipt?.state) ? [currentUserAccountID ?? -1] : [], }; } /** * Builds an optimistic APPROVED report action with a randomly generated reportActionID. - * - * @param {Number} amount - * @param {String} currency - * @param {Number} expenseReportID - * - * @returns {Object} */ -function buildOptimisticApprovedReportAction(amount, currency, expenseReportID) { +function buildOptimisticApprovedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticApprovedReportAction { const originalMessage = { amount, currency, @@ -2722,14 +2740,14 @@ function buildOptimisticApprovedReportAction(amount, currency, expenseReportID) actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), isAttachment: false, originalMessage, message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.APPROVED, Math.abs(amount), '', currency), person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserEmail), + text: currentUserPersonalDetails?.displayName ?? currentUserEmail, type: 'TEXT', }, ], @@ -2743,16 +2761,8 @@ function buildOptimisticApprovedReportAction(amount, currency, expenseReportID) /** * Builds an optimistic MOVED report action with a randomly generated reportActionID. * This action is used when we move reports across workspaces. - * - * @param {String} fromPolicyID - * @param {String} toPolicyID - * @param {Number} newParentReportID - * @param {Number} movedReportID - * @param {String} policyName - * - * @returns {Object} */ -function buildOptimisticMovedReportAction(fromPolicyID, toPolicyID, newParentReportID, movedReportID, policyName) { +function buildOptimisticMovedReportAction(fromPolicyID: string, toPolicyID: string, newParentReportID: string, movedReportID: string, policyName: string): ReportAction { const originalMessage = { fromPolicyID, toPolicyID, @@ -2772,14 +2782,14 @@ function buildOptimisticMovedReportAction(fromPolicyID, toPolicyID, newParentRep actionName: CONST.REPORT.ACTIONS.TYPE.MOVED, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), isAttachment: false, originalMessage, message: movedActionMessage, person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserEmail), + text: currentUserPersonalDetails?.displayName ?? currentUserEmail, type: 'TEXT', }, ], @@ -2793,13 +2803,8 @@ function buildOptimisticMovedReportAction(fromPolicyID, toPolicyID, newParentRep /** * Builds an optimistic SUBMITTED report action with a randomly generated reportActionID. * - * @param {Number} amount - * @param {String} currency - * @param {Number} expenseReportID - * - * @returns {Object} */ -function buildOptimisticSubmittedReportAction(amount, currency, expenseReportID) { +function buildOptimisticSubmittedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticSubmittedReportAction { const originalMessage = { amount, currency, @@ -2810,14 +2815,14 @@ function buildOptimisticSubmittedReportAction(amount, currency, expenseReportID) actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatar(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID), isAttachment: false, originalMessage, message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.SUBMITTED, Math.abs(amount), '', currency), person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserEmail), + text: currentUserPersonalDetails?.displayName ?? currentUserEmail, type: 'TEXT', }, ], @@ -2831,25 +2836,23 @@ function buildOptimisticSubmittedReportAction(amount, currency, expenseReportID) /** * Builds an optimistic report preview action with a randomly generated reportActionID. * - * @param {Object} chatReport - * @param {Object} iouReport - * @param {String} [comment] - User comment for the IOU. - * @param {Object} [transaction] - optimistic first transaction of preview - * - * @returns {Object} + * @param chatReport + * @param iouReport + * @param [comment] - User comment for the IOU. + * @param [transaction] - optimistic first transaction of preview */ -function buildOptimisticReportPreview(chatReport, iouReport, comment = '', transaction = undefined) { +function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: OnyxEntry, comment = '', transaction: OnyxEntry = null): OptimisticReportPreview { const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); const message = getReportPreviewMessage(iouReport); const created = DateUtils.getDBTime(); return { reportActionID: NumberUtils.rand64(), - reportID: chatReport.reportID, + reportID: chatReport?.reportID, actionName: CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, originalMessage: { - linkedReportID: iouReport.reportID, + linkedReportID: iouReport?.reportID, }, message: [ { @@ -2860,32 +2863,31 @@ function buildOptimisticReportPreview(chatReport, iouReport, comment = '', trans }, ], created, - accountID: iouReport.managerID || 0, + accountID: iouReport?.managerID ?? 0, // The preview is initially whispered if created with a receipt, so the actor is the current user as well - actorAccountID: hasReceipt ? currentUserAccountID : iouReport.managerID || 0, + actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? 0, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, - childRecentReceiptTransactionIDs: hasReceipt ? {[transaction.transactionID]: created} : [], - whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID] : [], + childRecentReceiptTransactionIDs: hasReceipt && isNotEmptyObject(transaction) ? {[transaction?.transactionID ?? '']: created} : undefined, + whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID ?? -1] : [], }; } /** * Builds an optimistic modified expense action with a randomly generated reportActionID. - * - * @param {Object} transactionThread - * @param {Object} oldTransaction - * @param {Object} transactionChanges - * @param {Object} isFromExpenseReport - * @returns {Object} */ -function buildOptimisticModifiedExpenseReportAction(transactionThread, oldTransaction, transactionChanges, isFromExpenseReport) { +function buildOptimisticModifiedExpenseReportAction( + transactionThread: OnyxEntry, + oldTransaction: OnyxEntry, + transactionChanges: ExpenseOriginalMessage, + isFromExpenseReport: boolean, +): OptimisticModifiedExpenseReportAction { const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport); return { actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), created: DateUtils.getDBTime(), isAttachment: false, message: [ @@ -2900,13 +2902,13 @@ function buildOptimisticModifiedExpenseReportAction(transactionThread, oldTransa person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserAccountID), + text: currentUserPersonalDetails?.displayName ?? String(currentUserAccountID), type: 'TEXT', }, ], pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, reportActionID: NumberUtils.rand64(), - reportID: transactionThread.reportID, + reportID: transactionThread?.reportID, shouldShow: true, }; } @@ -2914,19 +2916,30 @@ function buildOptimisticModifiedExpenseReportAction(transactionThread, oldTransa /** * Updates a report preview action that exists for an IOU report. * - * @param {Object} iouReport - * @param {Object} reportPreviewAction - * @param {Boolean} isPayRequest - * @param {String} [comment] - User comment for the IOU. - * @param {Object} [transaction] - optimistic newest transaction of a report preview + * @param [comment] - User comment for the IOU. + * @param [transaction] - optimistic newest transaction of a report preview * - * @returns {Object} */ -function updateReportPreview(iouReport, reportPreviewAction, isPayRequest = false, comment = '', transaction = undefined) { +function updateReportPreview( + iouReport: OnyxEntry, + reportPreviewAction: OnyxEntry, + isPayRequest = false, + comment = '', + transaction: OnyxEntry = null, +): UpdateReportPreview { const hasReceipt = TransactionUtils.hasReceipt(transaction); - const recentReceiptTransactions = lodashGet(reportPreviewAction, 'childRecentReceiptTransactionIDs', {}); + const recentReceiptTransactions = reportPreviewAction?.childRecentReceiptTransactionIDs ?? {}; const transactionsToKeep = TransactionUtils.getRecentTransactions(recentReceiptTransactions); - const previousTransactions = _.mapObject(recentReceiptTransactions, (value, key) => (_.contains(transactionsToKeep, key) ? value : null)); + const previousTransactionsArray = Object.entries(recentReceiptTransactions ?? {}).map(([key, value]) => (transactionsToKeep.includes(key) ? {[key]: value} : null)); + const previousTransactions: Record = {}; + + for (const obj of previousTransactionsArray) { + for (const key in obj) { + if (obj) { + previousTransactions[key] = obj[key]; + } + } + } const message = getReportPreviewMessage(iouReport, reportPreviewAction); return { @@ -2940,32 +2953,31 @@ function updateReportPreview(iouReport, reportPreviewAction, isPayRequest = fals type: CONST.REPORT.MESSAGE.TYPE.COMMENT, }, ], - childLastMoneyRequestComment: comment || reportPreviewAction.childLastMoneyRequestComment, - childMoneyRequestCount: reportPreviewAction.childMoneyRequestCount + (isPayRequest ? 0 : 1), + childLastMoneyRequestComment: comment || reportPreviewAction?.childLastMoneyRequestComment, + childMoneyRequestCount: (reportPreviewAction?.childMoneyRequestCount ?? 0) + (isPayRequest ? 0 : 1), childRecentReceiptTransactionIDs: hasReceipt ? { - [transaction.transactionID]: transaction.created, + ...(transaction && {[transaction.transactionID]: transaction?.created}), ...previousTransactions, } : recentReceiptTransactions, // As soon as we add a transaction without a receipt to the report, it will have ready money requests, // so we remove the whisper - whisperedToAccountIDs: hasReceipt ? reportPreviewAction.whisperedToAccountIDs : [], + whisperedToAccountIDs: hasReceipt ? reportPreviewAction?.whisperedToAccountIDs : [], }; } -function buildOptimisticTaskReportAction(taskReportID, actionName, message = '') { +function buildOptimisticTaskReportAction(taskReportID: string, actionName: OriginalMessageActionName, message = ''): OptimisticTaskReportAction { const originalMessage = { taskReportID, type: actionName, text: message, }; - return { actionName, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), isAttachment: false, originalMessage, message: [ @@ -2978,7 +2990,7 @@ function buildOptimisticTaskReportAction(taskReportID, actionName, message = '') person: [ { style: 'strong', - text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserAccountID), + text: currentUserPersonalDetails?.displayName ?? String(currentUserAccountID), type: 'TEXT', }, ], @@ -2992,37 +3004,22 @@ function buildOptimisticTaskReportAction(taskReportID, actionName, message = '') /** * Builds an optimistic chat report with a randomly generated reportID and as much information as we currently have - * - * @param {Array} participantList Array of participant accountIDs - * @param {String} reportName - * @param {String} chatType - * @param {String} policyID - * @param {Number} ownerAccountID - * @param {Boolean} isOwnPolicyExpenseChat - * @param {String} oldPolicyName - * @param {String} visibility - * @param {String} writeCapability - * @param {String} notificationPreference - * @param {String} parentReportActionID - * @param {String} parentReportID - * @param {String} welcomeMessage - * @returns {Object} */ function buildOptimisticChatReport( - participantList, - reportName = CONST.REPORT.DEFAULT_REPORT_NAME, - chatType = '', - policyID = CONST.POLICY.OWNER_EMAIL_FAKE, - ownerAccountID = CONST.REPORT.OWNER_ACCOUNT_ID_FAKE, + participantList: number[], + reportName: string = CONST.REPORT.DEFAULT_REPORT_NAME, + chatType: ValueOf | undefined = undefined, + policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, + ownerAccountID: number = CONST.REPORT.OWNER_ACCOUNT_ID_FAKE, isOwnPolicyExpenseChat = false, oldPolicyName = '', - visibility = undefined, - writeCapability = undefined, - notificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + visibility: ValueOf | undefined = undefined, + writeCapability: ValueOf | undefined = undefined, + notificationPreference: string | number = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, parentReportActionID = '', parentReportID = '', welcomeMessage = '', -) { +): OptimisticChatReport { const currentTime = DateUtils.getDBTime(); const isNewlyCreatedWorkspaceChat = chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT && isOwnPolicyExpenseChat; return { @@ -3034,7 +3031,7 @@ function buildOptimisticChatReport( lastActorAccountID: 0, lastMessageTranslationKey: '', lastMessageHtml: '', - lastMessageText: null, + lastMessageText: undefined, lastReadTime: currentTime, lastVisibleActionCreated: currentTime, notificationPreference, @@ -3056,11 +3053,9 @@ function buildOptimisticChatReport( /** * Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically - * @param {String} emailCreatingAction - * @param {String} [created] - Action created time - * @returns {Object} + * @param [created] - Action created time */ -function buildOptimisticCreatedReportAction(emailCreatingAction, created = DateUtils.getDBTime()) { +function buildOptimisticCreatedReportAction(emailCreatingAction: string, created = DateUtils.getDBTime()): OptimisticCreatedReportAction { return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, @@ -3082,11 +3077,11 @@ function buildOptimisticCreatedReportAction(emailCreatingAction, created = DateU { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: lodashGet(allPersonalDetails, [currentUserAccountID, 'displayName'], currentUserEmail), + text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, }, ], automatic: false, - avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), created, shouldShow: true, }; @@ -3094,12 +3089,8 @@ function buildOptimisticCreatedReportAction(emailCreatingAction, created = DateU /** * Returns the necessary reportAction onyx data to indicate that a task report has been edited - * - * @param {String} emailEditingTask - * @returns {Object} */ - -function buildOptimisticEditedTaskReportAction(emailEditingTask) { +function buildOptimisticEditedTaskReportAction(emailEditingTask: string): OptimisticEditedTaskReportAction { return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, @@ -3121,11 +3112,11 @@ function buildOptimisticEditedTaskReportAction(emailEditingTask) { { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: lodashGet(allPersonalDetails, [currentUserAccountID, 'displayName'], currentUserEmail), + text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, }, ], automatic: false, - avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), created: DateUtils.getDBTime(), shouldShow: false, }; @@ -3134,17 +3125,14 @@ function buildOptimisticEditedTaskReportAction(emailEditingTask) { /** * Returns the necessary reportAction onyx data to indicate that a chat has been archived * - * @param {String} emailClosingReport - * @param {String} policyName - * @param {String} reason - A reason why the chat has been archived - * @returns {Object} + * @param reason - A reason why the chat has been archived */ -function buildOptimisticClosedReportAction(emailClosingReport, policyName, reason = CONST.REPORT.ARCHIVE_REASON.DEFAULT) { +function buildOptimisticClosedReportAction(emailClosingReport: string, policyName: string, reason: string = CONST.REPORT.ARCHIVE_REASON.DEFAULT): OptimisticClosedReportAction { return { actionName: CONST.REPORT.ACTIONS.TYPE.CLOSED, actorAccountID: currentUserAccountID, automatic: false, - avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(currentUserAccountID)), + avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), created: DateUtils.getDBTime(), message: [ { @@ -3167,7 +3155,7 @@ function buildOptimisticClosedReportAction(emailClosingReport, policyName, reaso { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'strong', - text: lodashGet(allPersonalDetails, [currentUserAccountID, 'displayName'], currentUserEmail), + text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, }, ], reportActionID: NumberUtils.rand64(), @@ -3175,21 +3163,16 @@ function buildOptimisticClosedReportAction(emailClosingReport, policyName, reaso }; } -/** - * @param {String} policyID - * @param {String} policyName - * @returns {Object} - */ -function buildOptimisticWorkspaceChats(policyID, policyName) { +function buildOptimisticWorkspaceChats(policyID: string, policyName: string): OptimisticWorkspaceChats { const announceChatData = buildOptimisticChatReport( - [currentUserAccountID], + currentUserAccountID ? [currentUserAccountID] : [], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID, CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, policyName, - null, + undefined, undefined, // #announce contains all policy members so notifying always should be opt-in only. @@ -3202,7 +3185,7 @@ function buildOptimisticWorkspaceChats(policyID, policyName) { }; const adminsChatData = buildOptimisticChatReport( - [currentUserAccountID], + [currentUserAccountID ?? -1], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, policyID, @@ -3216,9 +3199,9 @@ function buildOptimisticWorkspaceChats(policyID, policyName) { [adminsCreatedAction.reportActionID]: adminsCreatedAction, }; - const expenseChatData = buildOptimisticChatReport([currentUserAccountID], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, currentUserAccountID, true, policyName); + const expenseChatData = buildOptimisticChatReport([currentUserAccountID ?? -1], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, currentUserAccountID, true, policyName); const expenseChatReportID = expenseChatData.reportID; - const expenseReportCreatedAction = buildOptimisticCreatedReportAction(currentUserEmail); + const expenseReportCreatedAction = buildOptimisticCreatedReportAction(currentUserEmail ?? ''); const expenseReportActionData = { [expenseReportCreatedAction.reportActionID]: expenseReportCreatedAction, }; @@ -3242,17 +3225,22 @@ function buildOptimisticWorkspaceChats(policyID, policyName) { /** * Builds an optimistic Task Report with a randomly generated reportID * - * @param {Number} ownerAccountID - Account ID of the person generating the Task. - * @param {String} assigneeAccountID - AccountID of the other person participating in the Task. - * @param {String} parentReportID - Report ID of the chat where the Task is. - * @param {String} title - Task title. - * @param {String} description - Task description. - * @param {String} policyID - PolicyID of the parent report - * - * @returns {Object} - */ - -function buildOptimisticTaskReport(ownerAccountID, assigneeAccountID = 0, parentReportID, title, description, policyID = CONST.POLICY.OWNER_EMAIL_FAKE) { + * @param ownerAccountID - Account ID of the person generating the Task. + * @param assigneeAccountID - AccountID of the other person participating in the Task. + * @param parentReportID - Report ID of the chat where the Task is. + * @param title - Task title. + * @param description - Task description. + * @param policyID - PolicyID of the parent report + */ + +function buildOptimisticTaskReport( + ownerAccountID: number, + assigneeAccountID = 0, + parentReportID?: string, + title?: string, + description?: string, + policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, +): OptimisticTaskReport { return { reportID: generateReportID(), reportName: title, @@ -3272,52 +3260,41 @@ function buildOptimisticTaskReport(ownerAccountID, assigneeAccountID = 0, parent /** * A helper method to create transaction thread * - * @param {Object} reportAction - the parent IOU report action from which to create the thread + * @param reportAction - the parent IOU report action from which to create the thread * - * @param {String} moneyRequestReportID - the reportID which the report action belong to - * - * @returns {Object} + * @param moneyRequestReportID - the reportID which the report action belong to */ -function buildTransactionThread(reportAction, moneyRequestReportID) { - const participantAccountIDs = _.uniq([currentUserAccountID, Number(reportAction.actorAccountID)]); +function buildTransactionThread(reportAction: OnyxEntry, moneyRequestReportID: string): OptimisticChatReport { + const participantAccountIDs = [...new Set([currentUserAccountID, Number(reportAction?.actorAccountID)])].filter(Boolean) as number[]; return buildOptimisticChatReport( participantAccountIDs, getTransactionReportName(reportAction), - '', - lodashGet(getReport(moneyRequestReportID), 'policyID', CONST.POLICY.OWNER_EMAIL_FAKE), + undefined, + getReport(moneyRequestReportID)?.policyID ?? CONST.POLICY.OWNER_EMAIL_FAKE, CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, '', undefined, undefined, CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, - reportAction.reportActionID, + reportAction?.reportActionID, moneyRequestReportID, ); } -/** - * @param {Object} report - * @returns {Boolean} - */ -function isUnread(report) { +function isUnread(report: OnyxEntry): boolean { if (!report) { return false; } // lastVisibleActionCreated and lastReadTime are both datetime strings and can be compared directly - const lastVisibleActionCreated = report.lastVisibleActionCreated || ''; - const lastReadTime = report.lastReadTime || ''; + const lastVisibleActionCreated = report.lastVisibleActionCreated ?? ''; + const lastReadTime = report.lastReadTime ?? ''; return lastReadTime < lastVisibleActionCreated; } -/** - * @param {Object} report - * @param {Object} allReportsDict - * @returns {Boolean} - */ -function isIOUOwnedByCurrentUser(report, allReportsDict = null) { - const allAvailableReports = allReportsDict || allReports; +function isIOUOwnedByCurrentUser(report: OnyxEntry, allReportsDict: OnyxCollection = null): boolean { + const allAvailableReports = allReportsDict ?? allReports; if (!report || !allAvailableReports) { return false; } @@ -3336,13 +3313,8 @@ function isIOUOwnedByCurrentUser(report, allReportsDict = null) { /** * Assuming the passed in report is a default room, lets us know whether we can see it or not, based on permissions and * the various subsets of users we've allowed to use default rooms. - * - * @param {Object} report - * @param {Array} policies - * @param {Array} betas - * @return {Boolean} */ -function canSeeDefaultRoom(report, policies, betas) { +function canSeeDefaultRoom(report: OnyxEntry, policies: OnyxCollection, betas: OnyxEntry): boolean { // Include archived rooms if (isArchivedRoom(report)) { return true; @@ -3354,12 +3326,12 @@ function canSeeDefaultRoom(report, policies, betas) { } // Include domain rooms with Partner Managers (Expensify accounts) in them for accounts that are on a domain with an Approved Accountant - if (isDomainRoom(report) && doesDomainHaveApprovedAccountant && hasExpensifyEmails(lodashGet(report, ['participantAccountIDs'], []))) { + if (isDomainRoom(report) && doesDomainHaveApprovedAccountant && hasExpensifyEmails(report?.participantAccountIDs ?? [])) { return true; } // If the room has an assigned guide, it can be seen. - if (hasExpensifyGuidesEmails(lodashGet(report, ['participantAccountIDs'], []))) { + if (hasExpensifyGuidesEmails(report?.participantAccountIDs ?? [])) { return true; } @@ -3369,17 +3341,10 @@ function canSeeDefaultRoom(report, policies, betas) { } // For all other cases, just check that the user belongs to the default rooms beta - return Permissions.canUseDefaultRooms(betas); + return Permissions.canUseDefaultRooms(betas ?? []); } -/** - * @param {Object} report - * @param {Object | null} policies - * @param {Array | null} betas - * @param {Object} allReportActions - * @returns {Boolean} - */ -function canAccessReport(report, policies, betas, allReportActions) { +function canAccessReport(report: OnyxEntry, policies: OnyxCollection, betas: OnyxEntry, allReportActions?: OnyxCollection): boolean { if (isThread(report) && ReportActionsUtils.isPendingRemove(ReportActionsUtils.getParentReportAction(report, allReportActions))) { return false; } @@ -3393,15 +3358,13 @@ function canAccessReport(report, policies, betas, allReportActions) { } /** * Check if the report is the parent report of the currently viewed report or at least one child report has report action - * @param {Object} report - * @param {String} currentReportId - * @returns {Boolean} */ -function shouldHideReport(report, currentReportId) { - const parentReport = getParentReport(getReport(currentReportId)); - const reportActions = ReportActionsUtils.getAllReportActions(report.reportID); - const isChildReportHasComment = _.some(reportActions, (reportAction) => (reportAction.childVisibleActionCount || 0) > 0); - return parentReport.reportID !== report.reportID && !isChildReportHasComment; +function shouldHideReport(report: OnyxEntry, currentReportId: string): boolean { + const currentReport = getReport(currentReportId); + const parentReport = getParentReport(isNotEmptyObject(currentReport) ? currentReport : null); + const reportActions = ReportActionsUtils.getAllReportActions(report?.reportID ?? ''); + const isChildReportHasComment = Object.values(reportActions ?? {})?.some((reportAction) => (reportAction?.childVisibleActionCount ?? 0) > 0); + return parentReport?.reportID !== report?.reportID && !isChildReportHasComment; } /** @@ -3410,30 +3373,27 @@ function shouldHideReport(report, currentReportId) { * * This logic is very specific and the order of the logic is very important. It should fail quickly in most cases and also * filter out the majority of reports before filtering out very specific minority of reports. - * - * @param {Object} report - * @param {String | Null | Undefined} currentReportId - * @param {Boolean} isInGSDMode - * @param {String[]} betas - * @param {Object} policies - * @param {Object} allReportActions - * @param {Boolean} excludeEmptyChats - * @returns {boolean} - */ -function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, excludeEmptyChats = false) { + */ +function shouldReportBeInOptionList( + report: OnyxEntry, + currentReportId: string, + isInGSDMode: boolean, + betas: Beta[], + policies: OnyxCollection, + allReportActions?: OnyxCollection, + excludeEmptyChats = false, +) { const isInDefaultMode = !isInGSDMode; - // Exclude reports that have no data because there wouldn't be anything to show in the option item. // This can happen if data is currently loading from the server or a report is in various stages of being created. // This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy. if ( - !report || - !report.reportID || - !report.type || - report.reportName === undefined || - report.isHidden || - (report.participantAccountIDs && - report.participantAccountIDs.length === 0 && + !report?.reportID || + !report?.type || + report?.reportName === undefined || + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + report?.isHidden || + (report?.participantAccountIDs?.length === 0 && !isChatThread(report) && !isPublicRoom(report) && !isUserCreatedPolicyRoom(report) && @@ -3443,7 +3403,6 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, ) { return false; } - if (!canAccessReport(report, policies, betas, allReportActions)) { return false; } @@ -3456,6 +3415,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, } // Include reports that are relevant to the user in any view mode. Criteria include having a draft or having a GBR showing. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (report.hasDraft || requiresAttentionFromCurrentUser(report)) { return true; } @@ -3475,7 +3435,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, // Include reports that have errors from trying to add a workspace // If we excluded it, then the red-brock-road pattern wouldn't work for the user to resolve the error - if (report.errorFields && report.errorFields.addWorkspaceRoom) { + if (report.errorFields?.addWorkspaceRoom) { return true; } @@ -3499,64 +3459,57 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, /** * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, money request, room, and policy expense chat. - * @param {Array} newParticipantList - * @returns {Array|undefined} */ -function getChatByParticipants(newParticipantList) { - const sortedNewParticipantList = _.sortBy(newParticipantList); - return _.find(allReports, (report) => { - // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it - if ( - !report || - _.isEmpty(report.participantAccountIDs) || - isChatThread(report) || - isTaskReport(report) || - isMoneyRequestReport(report) || - isChatRoom(report) || - isPolicyExpenseChat(report) - ) { - return false; - } +function getChatByParticipants(newParticipantList: number[]): OnyxEntry { + const sortedNewParticipantList = newParticipantList.sort(); + return ( + Object.values(allReports ?? {}).find((report) => { + // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it + if ( + !report || + report.participantAccountIDs?.length === 0 || + isChatThread(report) || + isTaskReport(report) || + isMoneyRequestReport(report) || + isChatRoom(report) || + isPolicyExpenseChat(report) + ) { + return false; + } - // Only return the chat if it has all the participants - return _.isEqual(sortedNewParticipantList, _.sortBy(report.participantAccountIDs)); - }); + // Only return the chat if it has all the participants + return lodashIsEqual(sortedNewParticipantList, report.participantAccountIDs?.sort()); + }) ?? null + ); } /** * Attempts to find a report in onyx with the provided list of participants in given policy - * @param {Array} newParticipantList - * @param {String} policyID - * @returns {object|undefined} */ -function getChatByParticipantsAndPolicy(newParticipantList, policyID) { +function getChatByParticipantsAndPolicy(newParticipantList: number[], policyID: string): OnyxEntry { newParticipantList.sort(); - return _.find(allReports, (report) => { - // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it - if (!report || !report.participantAccountIDs) { - return false; - } - - // Only return the room if it has all the participants and is not a policy room - return report.policyID === policyID && _.isEqual(newParticipantList, _.sortBy(report.participantAccountIDs)); - }); + return ( + Object.values(allReports ?? {}).find((report) => { + // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it + if (!report?.participantAccountIDs) { + return false; + } + const sortedParticipanctsAccountIDs = report.parentReportActionIDs?.sort(); + // Only return the room if it has all the participants and is not a policy room + return report.policyID === policyID && lodashIsEqual(newParticipantList, sortedParticipanctsAccountIDs); + }) ?? null + ); } -/** - * @param {String} policyID - * @returns {Array} - */ -function getAllPolicyReports(policyID) { - return _.filter(allReports, (report) => report && report.policyID === policyID); +function getAllPolicyReports(policyID: string): Array> { + return Object.values(allReports ?? {}).filter((report) => report?.policyID === policyID); } /** * Returns true if Chronos is one of the chat participants (1:1) - * @param {Object} report - * @returns {Boolean} */ -function chatIncludesChronos(report) { - return report.participantAccountIDs && _.contains(report.participantAccountIDs, CONST.ACCOUNT_ID.CHRONOS); +function chatIncludesChronos(report: OnyxEntry): boolean { + return Boolean(report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CHRONOS)); } /** @@ -3565,18 +3518,17 @@ function chatIncludesChronos(report) { * - It was written by someone else and isn't a whisper * - It's a welcome message whisper * - It's an ADDCOMMENT that is not an attachment - * - * @param {Object} reportAction - * @param {number} reportID - * @returns {Boolean} */ -function canFlagReportAction(reportAction, reportID) { +function canFlagReportAction(reportAction: OnyxEntry, reportID: string | undefined): boolean { const report = getReport(reportID); - const isCurrentUserAction = reportAction.actorAccountID === currentUserAccountID; - + const isCurrentUserAction = reportAction?.actorAccountID === currentUserAccountID; + const isOriginalMessageHaveHtml = + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST; if (ReportActionsUtils.isWhisperAction(reportAction)) { // Allow flagging welcome message whispers as they can be set by any room creator - if (report.welcomeMessage && !isCurrentUserAction && lodashGet(reportAction, 'originalMessage.html') === report.welcomeMessage) { + if (report?.welcomeMessage && !isCurrentUserAction && isOriginalMessageHaveHtml && reportAction?.originalMessage?.html === report.welcomeMessage) { return true; } @@ -3584,74 +3536,62 @@ function canFlagReportAction(reportAction, reportID) { return false; } - return ( + return Boolean( !isCurrentUserAction && - reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && - !ReportActionsUtils.isDeletedAction(reportAction) && - !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && - isAllowedToComment(report) + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && + !ReportActionsUtils.isDeletedAction(reportAction) && + !ReportActionsUtils.isCreatedTaskReportAction(reportAction) && + isNotEmptyObject(report) && + report && + isAllowedToComment(report), ); } /** * Whether flag comment page should show - * - * @param {Object} reportAction - * @param {Object} report - * @returns {Boolean} */ - -function shouldShowFlagComment(reportAction, report) { +function shouldShowFlagComment(reportAction: OnyxEntry, report: OnyxEntry): boolean { return ( - canFlagReportAction(reportAction, report.reportID) && + canFlagReportAction(reportAction, report?.reportID) && !isArchivedRoom(report) && !chatIncludesChronos(report) && - !isConciergeChatReport(report.reportID) && - reportAction.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE + !isConciergeChatReport(report) && + reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE ); } /** - * @param {Object} report - * @param {String} report.lastReadTime - * @param {Array} sortedAndFilteredReportActions - reportActions for the report, sorted newest to oldest, and filtered for only those that should be visible - * - * @returns {String|null} + * @param sortedAndFilteredReportActions - reportActions for the report, sorted newest to oldest, and filtered for only those that should be visible */ -function getNewMarkerReportActionID(report, sortedAndFilteredReportActions) { +function getNewMarkerReportActionID(report: OnyxEntry, sortedAndFilteredReportActions: ReportAction[]): string { if (!isUnread(report)) { return ''; } - const newMarkerIndex = _.findLastIndex(sortedAndFilteredReportActions, (reportAction) => (reportAction.created || '') > report.lastReadTime); + const newMarkerIndex = lodashFindLastIndex(sortedAndFilteredReportActions, (reportAction) => (reportAction.created ?? '') > (report?.lastReadTime ?? '')); - return _.has(sortedAndFilteredReportActions[newMarkerIndex], 'reportActionID') ? sortedAndFilteredReportActions[newMarkerIndex].reportActionID : ''; + return 'reportActionID' in sortedAndFilteredReportActions[newMarkerIndex] ? sortedAndFilteredReportActions[newMarkerIndex].reportActionID : ''; } /** * Performs the markdown conversion, and replaces code points > 127 with C escape sequences * Used for compatibility with the backend auth validator for AddComment, and to account for MD in comments - * @param {String} textComment - * @returns {Number} The comment's total length as seen from the backend + * @returns The comment's total length as seen from the backend */ -function getCommentLength(textComment) { +function getCommentLength(textComment: string): number { return getParsedComment(textComment) .replace(/[^ -~]/g, '\\u????') .trim().length; } -/** - * @param {String|null} url - * @returns {String} - */ -function getRouteFromLink(url) { +function getRouteFromLink(url: string | null): string { if (!url) { return ''; } // Get the reportID from URL let route = url; - _.each(linkingConfig.prefixes, (prefix) => { + linkingConfig.prefixes.forEach((prefix) => { const localWebAndroidRegEx = /^(http:\/\/([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3}))/; if (route.startsWith(prefix)) { route = route.replace(prefix, ''); @@ -3674,11 +3614,7 @@ function getRouteFromLink(url) { return route; } -/** - * @param {String} route - * @returns {Object} - */ -function parseReportRouteParams(route) { +function parseReportRouteParams(route: string): ReportRouteParams { let parsingRoute = route; if (parsingRoute.at(0) === '/') { // remove the first slash @@ -3704,11 +3640,7 @@ function parseReportRouteParams(route) { }; } -/** - * @param {String|null} url - * @returns {String} - */ -function getReportIDFromLink(url) { +function getReportIDFromLink(url: string | null): string { const route = getRouteFromLink(url); const {reportID, isSubReportPageRoute} = parseReportRouteParams(route); if (isSubReportPageRoute) { @@ -3720,14 +3652,11 @@ function getReportIDFromLink(url) { /** * Check if the chat report is linked to an iou that is waiting for the current user to add a credit bank account. - * - * @param {Object} chatReport - * @returns {Boolean} */ -function hasIOUWaitingOnCurrentUserBankAccount(chatReport) { - if (chatReport.iouReportID) { - const iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; - if (iouReport && iouReport.isWaitingOnBankAccount && iouReport.ownerAccountID === currentUserAccountID) { +function hasIOUWaitingOnCurrentUserBankAccount(chatReport: OnyxEntry): boolean { + if (chatReport?.iouReportID) { + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`]; + if (iouReport?.isWaitingOnBankAccount && iouReport?.ownerAccountID === currentUserAccountID) { return true; } } @@ -3742,12 +3671,8 @@ function hasIOUWaitingOnCurrentUserBankAccount(chatReport) { * - employee can request money in submitted expense report only if the policy has Instant Submit settings turned on * - in an IOU report, which is not settled yet * - in a 1:1 DM chat - * - * @param {Object} report - * @param {Array} otherParticipants - * @returns {Boolean} */ -function canRequestMoney(report, otherParticipants) { +function canRequestMoney(report: OnyxEntry, otherParticipants: number[]): boolean { // User cannot request money in chat thread or in task report or in chat room if (isChatThread(report) || isTaskReport(report) || isChatRoom(report)) { return false; @@ -3764,9 +3689,9 @@ function canRequestMoney(report, otherParticipants) { } // In case of expense reports, we have to look at the parent workspace chat to get the isOwnPolicyExpenseChat property - let isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat || false; + let isOwnPolicyExpenseChat = report?.isOwnPolicyExpenseChat ?? false; if (isExpenseReport(report) && getParentReport(report)) { - isOwnPolicyExpenseChat = getParentReport(report).isOwnPolicyExpenseChat; + isOwnPolicyExpenseChat = Boolean(getParentReport(report)?.isOwnPolicyExpenseChat); } // In case there are no other participants than the current user and it's not user's own policy expense chat, they can't request money from such report @@ -3777,7 +3702,7 @@ function canRequestMoney(report, otherParticipants) { // User can request money in any IOU report, unless paid, but user can only request money in an expense report // which is tied to their workspace chat. if (isMoneyRequestReport(report)) { - return ((isExpenseReport(report) && isOwnPolicyExpenseChat) || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report.reportID); + return ((isExpenseReport(report) && isOwnPolicyExpenseChat) || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report?.reportID); } // In case of policy expense chat, users can only request money from their own policy expense chat @@ -3801,12 +3726,8 @@ function canRequestMoney(report, otherParticipants) { * * None of the options should show in chat threads or if there is some special Expensify account * as a participant of the report. - * - * @param {Object} report - * @param {Array} reportParticipants - * @returns {Array} */ -function getMoneyRequestOptions(report, reportParticipants) { +function getMoneyRequestOptions(report: OnyxEntry, reportParticipants: number[]): Array> { // In any thread or task report, we do not allow any new money requests yet if (isChatThread(report) || isTaskReport(report)) { return []; @@ -3814,21 +3735,21 @@ function getMoneyRequestOptions(report, reportParticipants) { // We don't allow IOU actions if an Expensify account is a participant of the report, unless the policy that the report is on is owned by an Expensify account const doParticipantsIncludeExpensifyAccounts = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; - const isPolicyOwnedByExpensifyAccounts = report.policyID ? CONST.EXPENSIFY_ACCOUNT_IDS.includes(getPolicy(report.policyID).ownerAccountID || 0) : false; + const isPolicyOwnedByExpensifyAccounts = report?.policyID ? CONST.EXPENSIFY_ACCOUNT_IDS.includes(getPolicy(report?.policyID ?? '')?.ownerAccountID ?? 0) : false; if (doParticipantsIncludeExpensifyAccounts && !isPolicyOwnedByExpensifyAccounts) { return []; } - const otherParticipants = _.filter(reportParticipants, (accountID) => currentUserPersonalDetails.accountID !== accountID); + const otherParticipants = reportParticipants.filter((accountID) => currentUserPersonalDetails?.accountID !== accountID); const hasSingleOtherParticipantInReport = otherParticipants.length === 1; const hasMultipleOtherParticipants = otherParticipants.length > 1; - let options = []; + let options: Array> = []; // User created policy rooms and default rooms like #admins or #announce will always have the Split Bill option // unless there are no other participants at all (e.g. #admins room for a policy with only 1 admin) // DM chats will have the Split Bill option only when there are at least 2 other people in the chat. // Your own workspace chats will have the split bill option. - if ((isChatRoom(report) && otherParticipants.length > 0) || (isDM(report) && hasMultipleOtherParticipants) || (isPolicyExpenseChat(report) && report.isOwnPolicyExpenseChat)) { + if ((isChatRoom(report) && otherParticipants.length > 0) || (isDM(report) && hasMultipleOtherParticipants) || (isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat)) { options = [CONST.IOU.TYPE.SPLIT]; } @@ -3855,21 +3776,15 @@ function getMoneyRequestOptions(report, reportParticipants) { * `domain` - Nobody can leave (it's auto-shared with domain members) * `dm` - Nobody can leave (it's auto-shared with users) * `private` - Anybody can leave (though you can only be invited to join) - * - * @param {Object} report - * @param {String} report.visibility - * @param {String} report.chatType - * @param {Boolean} isPolicyMember - * @returns {Boolean} - */ -function canLeaveRoom(report, isPolicyMember) { - if (_.isEmpty(report.visibility)) { + */ +function canLeaveRoom(report: OnyxEntry, isPolicyMember: boolean): boolean { + if (!report?.visibility) { if ( - report.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS || - report.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE || - report.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || - report.chatType === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL || - _.isEmpty(report.chatType) + report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS || + report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE || + report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || + report?.chatType === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL || + !report?.chatType ) { // DM chats don't have a chatType return false; @@ -3880,22 +3795,15 @@ function canLeaveRoom(report, isPolicyMember) { return true; } -/** - * @param {Number[]} participantAccountIDs - * @returns {Boolean} - */ -function isCurrentUserTheOnlyParticipant(participantAccountIDs) { - return participantAccountIDs && participantAccountIDs.length === 1 && participantAccountIDs[0] === currentUserAccountID; +function isCurrentUserTheOnlyParticipant(participantAccountIDs?: number[]): boolean { + return Boolean(participantAccountIDs?.length === 1 && participantAccountIDs?.[0] === currentUserAccountID); } /** * Returns display names for those that can see the whisper. * However, it returns "you" if the current user is the only one who can see it besides the person that sent it. - * - * @param {Number[]} participantAccountIDs - * @returns {string} */ -function getWhisperDisplayNames(participantAccountIDs) { +function getWhisperDisplayNames(participantAccountIDs?: number[]): string | undefined { const isWhisperOnlyVisibleToCurrentUser = isCurrentUserTheOnlyParticipant(participantAccountIDs); // When the current user is the only participant, the display name needs to be "you" because that's the only person reading it @@ -3903,20 +3811,18 @@ function getWhisperDisplayNames(participantAccountIDs) { return Localize.translateLocal('common.youAfterPreposition'); } - return _.map(participantAccountIDs, (accountID) => getDisplayNameForParticipant(accountID, !isWhisperOnlyVisibleToCurrentUser)).join(', '); + return participantAccountIDs?.map((accountID) => getDisplayNameForParticipant(accountID, !isWhisperOnlyVisibleToCurrentUser)).join(', '); } /** * Show subscript on workspace chats / threads and expense requests - * @param {Object} report - * @returns {Boolean} */ -function shouldReportShowSubscript(report) { +function shouldReportShowSubscript(report: OnyxEntry): boolean { if (isArchivedRoom(report)) { return false; } - if (isPolicyExpenseChat(report) && !isChatThread(report) && !isTaskReport(report) && !report.isOwnPolicyExpenseChat) { + if (isPolicyExpenseChat(report) && !isChatThread(report) && !isTaskReport(report) && !report?.isOwnPolicyExpenseChat) { return true; } @@ -3941,78 +3847,60 @@ function shouldReportShowSubscript(report) { /** * Return true if reports data exists - * @returns {Boolean} */ -function isReportDataReady() { - return !_.isEmpty(allReports) && _.some(_.keys(allReports), (key) => allReports[key] && allReports[key].reportID); +function isReportDataReady(): boolean { + return !isEmptyObject(allReports) && Object.keys(allReports ?? {}).some((key) => allReports?.[key]?.reportID); } /** * Return true if reportID from path is valid - * @param {String} reportIDFromPath - * @returns {Boolean} */ -function isValidReportIDFromPath(reportIDFromPath) { - return typeof reportIDFromPath === 'string' && !['', 'null', '0'].includes(reportIDFromPath); +function isValidReportIDFromPath(reportIDFromPath: string): boolean { + return !['', 'null', '0'].includes(reportIDFromPath); } /** * Return the errors we have when creating a chat or a workspace room - * @param {Object} report - * @returns {Object} errors */ -function getAddWorkspaceRoomOrChatReportErrors(report) { +function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Record | null | undefined { // We are either adding a workspace room, or we're creating a chat, it isn't possible for both of these to have errors for the same report at the same time, so // simply looking up the first truthy value will get the relevant property if it's set. - return lodashGet(report, 'errorFields.addWorkspaceRoom') || lodashGet(report, 'errorFields.createChat'); + return report?.errorFields?.addWorkspaceRoom ?? report?.errorFields?.createChat; } -/** - * Returns true if write actions like assign task, money request, send message should be disabled on a report - * @param {Object} report - * @returns {Boolean} - */ -function canUserPerformWriteAction(report) { +function canUserPerformWriteAction(report: OnyxEntry) { const reportErrors = getAddWorkspaceRoomOrChatReportErrors(report); - return !isArchivedRoom(report) && _.isEmpty(reportErrors) && isAllowedToComment(report) && !isAnonymousUser; + return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser; } /** * Returns ID of the original report from which the given reportAction is first created. - * - * @param {String} reportID - * @param {Object} reportAction - * @returns {String} */ -function getOriginalReportID(reportID, reportAction) { - const currentReportAction = ReportActionsUtils.getReportAction(reportID, reportAction.reportActionID); - return isThreadFirstChat(reportAction, reportID) && _.isEmpty(currentReportAction) ? lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, 'parentReportID']) : reportID; +function getOriginalReportID(reportID: string, reportAction: OnyxEntry): string | undefined { + const currentReportAction = ReportActionsUtils.getReportAction(reportID, reportAction?.reportActionID ?? ''); + return isThreadFirstChat(reportAction, reportID) && Object.keys(currentReportAction ?? {}).length === 0 + ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.parentReportID + : reportID; } /** * Return the pendingAction and the errors we have when creating a chat or a workspace room offline - * @param {Object} report - * @returns {Object} pending action , errors */ -function getReportOfflinePendingActionAndErrors(report) { +function getReportOfflinePendingActionAndErrors(report: OnyxEntry): ReportOfflinePendingActionAndErrors { // We are either adding a workspace room, or we're creating a chat, it isn't possible for both of these to be pending, or to have errors for the same report at the same time, so // simply looking up the first truthy value for each case will get the relevant property if it's set. - const addWorkspaceRoomOrChatPendingAction = lodashGet(report, 'pendingFields.addWorkspaceRoom') || lodashGet(report, 'pendingFields.createChat'); + const addWorkspaceRoomOrChatPendingAction = report?.pendingFields?.addWorkspaceRoom ?? report?.pendingFields?.createChat; const addWorkspaceRoomOrChatErrors = getAddWorkspaceRoomOrChatReportErrors(report); return {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors}; } -/** - * @param {String} policyOwner - * @returns {String|null} - */ -function getPolicyExpenseChatReportIDByOwner(policyOwner) { - const policyWithOwner = _.find(allPolicies, (policy) => policy.owner === policyOwner); +function getPolicyExpenseChatReportIDByOwner(policyOwner: string): string | null { + const policyWithOwner = Object.values(allPolicies ?? {}).find((policy) => policy?.owner === policyOwner); if (!policyWithOwner) { return null; } - const expenseChat = _.find(allReports, (report) => isPolicyExpenseChat(report) && report.policyID === policyWithOwner.id); + const expenseChat = Object.values(allReports ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyWithOwner.id); if (!expenseChat) { return null; } @@ -4021,34 +3909,23 @@ function getPolicyExpenseChatReportIDByOwner(policyOwner) { /** * Check if the report can create the request with type is iouType - * @param {Object} report - * @param {Array} betas - * @param {String} iouType - * @returns {Boolean} */ -function canCreateRequest(report, betas, iouType) { - const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []); +function canCreateRequest(report: OnyxEntry, iouType: (typeof CONST.IOU.TYPE)[keyof typeof CONST.IOU.TYPE]): boolean { + const participantAccountIDs = report?.participantAccountIDs ?? []; if (!canUserPerformWriteAction(report)) { return false; } - return getMoneyRequestOptions(report, participantAccountIDs, betas).includes(iouType); + return getMoneyRequestOptions(report, participantAccountIDs).includes(iouType); } -/** - * @param {String} policyID - * @param {Array} accountIDs - * @returns {Array} - */ -function getWorkspaceChats(policyID, accountIDs) { - return _.filter(allReports, (report) => isPolicyExpenseChat(report) && lodashGet(report, 'policyID', '') === policyID && _.contains(accountIDs, lodashGet(report, 'ownerAccountID', ''))); +function getWorkspaceChats(policyID: string, accountIDs: number[]): Array> { + return Object.values(allReports ?? {}).filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1)); } /** - * @param {Object|null} report - * @param {Object|null} policy - the workspace the report is on, null if the user isn't a member of the workspace - * @returns {Boolean} + * @param policy - the workspace the report is on, null if the user isn't a member of the workspace */ -function shouldDisableRename(report, policy) { +function shouldDisableRename(report: OnyxEntry, policy: OnyxEntry): boolean { if (isDefaultRoom(report) || isArchivedRoom(report) || isThread(report) || isMoneyRequestReport(report) || isPolicyExpenseChat(report)) { return true; } @@ -4061,42 +3938,40 @@ function shouldDisableRename(report, policy) { // If there is a linked workspace, that means the user is a member of the workspace the report is in. // Still, we only want policy owners and admins to be able to modify the name. - return !_.keys(loginList).includes(policy.owner) && policy.role !== CONST.POLICY.ROLE.ADMIN; + return !Object.keys(loginList ?? {}).includes(policy.owner) && policy.role !== CONST.POLICY.ROLE.ADMIN; } /** - * @param {Object|null} report - * @param {Object|null} policy - the workspace the report is on, null if the user isn't a member of the workspace - * @returns {Boolean} + * @param policy - the workspace the report is on, null if the user isn't a member of the workspace */ -function canEditWriteCapability(report, policy) { +function canEditWriteCapability(report: OnyxEntry, policy: OnyxEntry): boolean { return PolicyUtils.isPolicyAdmin(policy) && !isAdminRoom(report) && !isArchivedRoom(report) && !isThread(report); } /** * Returns the onyx data needed for the task assignee chat - * @param {Number} accountID - * @param {Number} assigneeAccountID - * @param {String} taskReportID - * @param {String} assigneeChatReportID - * @param {String} parentReportID - * @param {String} title - * @param {Object} assigneeChatReport - * @returns {Object} - */ -function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, assigneeChatReportID, parentReportID, title, assigneeChatReport) { + */ +function getTaskAssigneeChatOnyxData( + accountID: number, + assigneeAccountID: number, + taskReportID: string, + assigneeChatReportID: string, + parentReportID: string, + title: string, + assigneeChatReport: OnyxEntry, +): OnyxDataTaskAssigneeChat { // Set if we need to add a comment to the assignee chat notifying them that they have been assigned a task - let optimisticAssigneeAddComment; + let optimisticAssigneeAddComment: OptimisticReportAction | undefined; // Set if this is a new chat that needs to be created for the assignee - let optimisticChatCreatedReportAction; + let optimisticChatCreatedReportAction: OptimisticCreatedReportAction | undefined; const currentTime = DateUtils.getDBTime(); - const optimisticData = []; - const successData = []; - const failureData = []; + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; // You're able to assign a task to someone you haven't chatted with before - so we need to optimistically create the chat and the chat reportActions // Only add the assignee chat report to onyx if we haven't already set it optimistically - if (assigneeChatReport.isOptimisticReport && lodashGet(assigneeChatReport, 'pendingFields.createChat') !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + if (assigneeChatReport?.isOptimisticReport && assigneeChatReport.pendingFields?.createChat !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { optimisticChatCreatedReportAction = buildOptimisticCreatedReportAction(assigneeChatReportID); optimisticData.push( { @@ -4112,7 +3987,7 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticChatCreatedReportAction.reportActionID]: optimisticChatCreatedReportAction}, + value: {[optimisticChatCreatedReportAction.reportActionID]: optimisticChatCreatedReportAction as Partial}, }, ); @@ -4151,9 +4026,10 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, // If you're choosing to share the task in the same DM as the assignee then we don't need to create another reportAction indicating that you've been assigned if (assigneeChatReportID !== parentReportID) { - const displayname = lodashGet(allPersonalDetails, [assigneeAccountID, 'displayName']) || lodashGet(allPersonalDetails, [assigneeAccountID, 'login'], ''); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const displayname = allPersonalDetails?.[assigneeAccountID]?.displayName || allPersonalDetails?.[assigneeAccountID]?.login || ''; optimisticAssigneeAddComment = buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeAccountID, `assigned to ${displayname}`, parentReportID); - const lastAssigneeCommentText = formatReportLastMessageText(optimisticAssigneeAddComment.reportAction.message[0].text); + const lastAssigneeCommentText = formatReportLastMessageText(optimisticAssigneeAddComment.reportAction.message?.[0].text ?? ''); const optimisticAssigneeReport = { lastVisibleActionCreated: currentTime, lastMessageText: lastAssigneeCommentText, @@ -4165,7 +4041,7 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: optimisticAssigneeAddComment.reportAction}, + value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: optimisticAssigneeAddComment.reportAction}, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -4176,7 +4052,7 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: {pendingAction: null}}, + value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: {pendingAction: null}}, }); } @@ -4191,46 +4067,41 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, /** * Returns an array of the participants Ids of a report - * - * @param {Object} report - * @returns {Array} */ -function getParticipantsIDs(report) { +function getParticipantsIDs(report: OnyxEntry): number[] { if (!report) { return []; } - const participants = report.participantAccountIDs || []; + const participants = report.participantAccountIDs ?? []; // Build participants list for IOU/expense reports if (isMoneyRequestReport(report)) { - return _.chain([report.managerID, report.ownerAccountID, ...participants]) - .compact() - .uniq() - .value(); + const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean) as number[]; + const onlyUnique = [...new Set([...onlyTruthyValues])]; + return onlyUnique; } return participants; } /** * Return iou report action display message - * - * @param {Object} reportAction report action - * @returns {String} */ -function getIOUReportActionDisplayMessage(reportAction) { - const originalMessage = _.get(reportAction, 'originalMessage', {}); - let translationKey; +function getIOUReportActionDisplayMessage(reportAction: OnyxEntry): string { + if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { + return ''; + } + const originalMessage = reportAction.originalMessage; + let translationKey: TranslationPaths; if (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { const {IOUReportID} = originalMessage; - // The `REPORT_ACTION_TYPE.PAY` action type is used for both fulfilling existing requests and sending money. To // differentiate between these two scenarios, we check if the `originalMessage` contains the `IOUDetails` // property. If it does, it indicates that this is a 'Send money' action. - const {amount, currency} = originalMessage.IOUDetails || originalMessage; - const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); + const {amount, currency} = originalMessage.IOUDetails ?? originalMessage; + const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency) ?? ''; const iouReport = getReport(IOUReportID); - const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport.managerID, true); + const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true); switch (originalMessage.paymentType) { case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: @@ -4238,18 +4109,18 @@ function getIOUReportActionDisplayMessage(reportAction) { break; case CONST.IOU.PAYMENT_TYPE.EXPENSIFY: case CONST.IOU.PAYMENT_TYPE.VBBA: - translationKey = 'iou.paidUsingExpensifyWithAmount'; + translationKey = 'iou.paidWithExpensifyWithAmount'; break; default: - translationKey = ''; + translationKey = 'iou.payerPaidAmount'; break; } - return Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName}); + return Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName ?? ''}); } - const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID); - const {amount, currency, comment} = getTransactionDetails(transaction); - const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency); + const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID ?? ''); + const transactionDetails = getTransactionDetails(isNotEmptyObject(transaction) ? transaction : null); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency); const isRequestSettled = isSettled(originalMessage.IOUReportID); if (isRequestSettled) { return Localize.translateLocal('iou.payerSettled', { @@ -4259,44 +4130,44 @@ function getIOUReportActionDisplayMessage(reportAction) { translationKey = ReportActionsUtils.isSplitBillAction(reportAction) ? 'iou.didSplitAmount' : 'iou.requestedAmount'; return Localize.translateLocal(translationKey, { formattedAmount, - comment, + comment: transactionDetails?.comment ?? '', }); } +function isReportDraft(report: OnyxEntry): boolean { + return isExpenseReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS.OPEN; +} + /** * Return room channel log display message - * - * @param {Object} reportAction - * @returns {String} */ -function getChannelLogMemberMessage(reportAction) { +function getChannelLogMemberMessage(reportAction: OnyxEntry): string { const verb = - reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM ? 'invited' : 'removed'; - const mentions = _.map(reportAction.originalMessage.targetAccountIDs, (accountID) => { - const personalDetail = lodashGet(allPersonalDetails, accountID); - const displayNameOrLogin = - LocalePhoneNumber.formatPhoneNumber(lodashGet(personalDetail, 'login', '')) || lodashGet(personalDetail, 'displayName', '') || Localize.translateLocal('common.hidden'); + const mentions = (reportAction?.originalMessage as ChangeLog)?.targetAccountIDs?.map(() => { + const personalDetail = allPersonalDetails?.accountID; + const displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') || (personalDetail?.displayName ?? '') || Localize.translateLocal('common.hidden'); return `@${displayNameOrLogin}`; }); - const lastMention = mentions.pop(); + const lastMention = mentions?.pop(); let message = ''; - if (mentions.length === 0) { + if (mentions?.length === 0) { message = `${verb} ${lastMention}`; - } else if (mentions.length === 1) { - message = `${verb} ${mentions[0]} and ${lastMention}`; + } else if (mentions?.length === 1) { + message = `${verb} ${mentions?.[0]} and ${lastMention}`; } else { - message = `${verb} ${mentions.join(', ')}, and ${lastMention}`; + message = `${verb} ${mentions?.join(', ')}, and ${lastMention}`; } - const roomName = lodashGet(reportAction, 'originalMessage.roomName', ''); + const roomName = (reportAction?.originalMessage as ChangeLog)?.roomName ?? ''; if (roomName) { const preposition = - reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM ? ' to' : ' from'; message += `${preposition} ${roomName}`; @@ -4316,54 +4187,32 @@ function getChannelLogMemberMessage(reportAction) { * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). * - More than 2 participants. * - * @param {Object} report - * @returns {Boolean} */ -function isGroupChat(report) { - return ( +function isGroupChat(report: OnyxEntry): boolean { + return Boolean( report && - !isChatThread(report) && - !isTaskReport(report) && - !isMoneyRequestReport(report) && - !isArchivedRoom(report) && - !Object.values(CONST.REPORT.CHAT_TYPE).includes(getChatType(report)) && - lodashGet(report, 'participantAccountIDs.length', 0) > 2 + !isChatThread(report) && + !isTaskReport(report) && + !isMoneyRequestReport(report) && + !isArchivedRoom(report) && + !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && + (report.participantAccountIDs?.length ?? 0) > 2, ); } -/** - * @param {Object} report - * @returns {Boolean} - */ -function isReportDraft(report) { - return isExpenseReport(report) && lodashGet(report, 'stateNum') === CONST.REPORT.STATE_NUM.OPEN && lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.OPEN; -} - -/** - * @param {Object} report - * @returns {Boolean} - */ -function shouldUseFullTitleToDisplay(report) { +function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } -/** - * - * @param {String} type - * @param {String} policyID - * @returns {Object} - */ -function getRoom(type, policyID) { - const room = _.find(allReports, (report) => report && report.policyID === policyID && report.chatType === type && !isThread(report)); +function getRoom(type: ValueOf, policyID: string): OnyxEntry | undefined { + const room = Object.values(allReports ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report)); return room; } + /** * We only want policy owners and admins to be able to modify the welcome message, but not in thread chat. - * @param {Object} report - * @param {Object} policy - * @return {Boolean} */ -function shouldDisableWelcomeMessage(report, policy) { +function shouldDisableWelcomeMessage(report: OnyxEntry, policy: OnyxEntry): boolean { return isMoneyRequestReport(report) || isArchivedRoom(report) || !isChatRoom(report) || isChatThread(report) || !PolicyUtils.isPolicyAdmin(policy); } @@ -4482,7 +4331,6 @@ export { getWorkspaceAvatar, isThread, isChatThread, - isThreadParent, isThreadFirstChat, isChildReport, shouldReportShowSubscript, @@ -4534,3 +4382,5 @@ export { shouldDisableWelcomeMessage, canEditWriteCapability, }; + +export type {OptionData}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 763a0000ba35..ff9486159947 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/prefer-underscore-method */ import Str from 'expensify-common/lib/str'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxCollection} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -117,11 +117,12 @@ function getOrderedReportIDs( betas: Beta[], policies: Record, priorityMode: ValueOf, - allReportActions: Record, + allReportActions: OnyxCollection, ): string[] { // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( - [currentReportId, allReports, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + [currentReportId, allReports, betas, policies, priorityMode, allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], (key, value: unknown) => { /** * Exclude 'participantAccountIDs', 'participants' and 'lastMessageText' not to overwhelm a cached key value with huge data, @@ -148,7 +149,9 @@ function getOrderedReportIDs( const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports); // Filter out all the reports that shouldn't be displayed - const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, true)); + const reportsToDisplay = allReportsDictValues.filter((report) => + ReportUtils.shouldReportBeInOptionList(report, currentReportId ?? '', isInGSDMode, betas, policies, allReportActions, true), + ); if (reportsToDisplay.length === 0) { // Display Concierge chat report when there is no report to be displayed @@ -219,75 +222,11 @@ function getOrderedReportIDs( return LHNReports; } -type OptionData = { - text?: string | null; - alternateText?: string | null; - pendingAction?: OnyxCommon.PendingAction | null; - allReportErrors?: OnyxCommon.Errors | null; - brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; - icons?: Icon[] | null; - tooltipText?: string | null; - ownerAccountID?: number | null; - subtitle?: string | null; - participantsList?: PersonalDetails[] | null; - login?: string | null; - accountID?: number | null; - managerID?: number | null; - reportID?: string | null; - policyID?: string | null; - status?: string | null; - type?: string | null; - stateNum?: ValueOf | null; - statusNum?: ValueOf | null; - phoneNumber?: string | null; - isUnread?: boolean | null; - isUnreadWithMention?: boolean | null; - hasDraftComment?: boolean | null; - keyForList?: string | null; - searchText?: string | null; - isPinned?: boolean | null; - hasOutstandingIOU?: boolean | null; - hasOutstandingChildRequest?: boolean | null; - iouReportID?: string | null; - isIOUReportOwner?: boolean | null; - iouReportAmount?: number | null; - isChatRoom?: boolean | null; - isArchivedRoom?: boolean | null; - shouldShowSubscript?: boolean | null; - isPolicyExpenseChat?: boolean | null; - isMoneyRequestReport?: boolean | null; - isExpenseRequest?: boolean | null; - isWaitingOnBankAccount?: boolean | null; - isAllowedToComment?: boolean | null; - isThread?: boolean | null; - isTaskReport?: boolean | null; - parentReportID?: string | null; - parentReportAction?: ReportAction; - notificationPreference?: string | number | null; - displayNamesWithTooltips?: DisplayNamesWithTooltip[] | null; - chatType?: ValueOf | null; -}; - -type DisplayNamesWithTooltip = { - displayName?: string; - avatar?: string; - login?: string; - accountID?: number; - pronouns?: string; -}; - type ActorDetails = { displayName?: string; accountID?: number; }; -type Icon = { - source?: string; - id?: number; - type?: string; - name?: string; -}; - /** * Gets all the data necessary for rendering an OptionRowLHN component */ @@ -298,7 +237,7 @@ function getOptionData( preferredLocale: ValueOf, policy: Policy, parentReportAction: ReportAction, -): OptionData | undefined { +): ReportUtils.OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do // a null check here and return early. @@ -306,24 +245,16 @@ function getOptionData( return; } - const result: OptionData = { - text: null, + const result: ReportUtils.OptionData = { alternateText: null, pendingAction: null, allReportErrors: null, brickRoadIndicator: null, - icons: null, tooltipText: null, - ownerAccountID: null, subtitle: null, - participantsList: null, login: null, accountID: null, - managerID: null, - reportID: null, - policyID: null, - statusNum: null, - stateNum: null, + reportID: '', phoneNumber: null, isUnread: null, isUnreadWithMention: null, @@ -333,7 +264,6 @@ function getOptionData( isPinned: false, hasOutstandingIOU: false, hasOutstandingChildRequest: false, - iouReportID: null, isIOUReportOwner: null, iouReportAmount: 0, isChatRoom: false, @@ -344,7 +274,6 @@ function getOptionData( isExpenseRequest: false, isWaitingOnBankAccount: false, isAllowedToComment: true, - chatType: null, }; const participantPersonalDetailList: PersonalDetails[] = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); const personalDetail = participantPersonalDetailList[0] ?? {}; @@ -376,9 +305,9 @@ function getOptionData( result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs ?? []); result.hasOutstandingIOU = report.hasOutstandingIOU; result.hasOutstandingChildRequest = report.hasOutstandingChildRequest; - result.parentReportID = report.parentReportID ?? null; + result.parentReportID = report.parentReportID ?? ''; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; - result.notificationPreference = report.notificationPreference ?? null; + result.notificationPreference = report.notificationPreference ?? ''; result.isAllowedToComment = ReportUtils.canUserPerformWriteAction(report); result.chatType = report.chatType; @@ -390,7 +319,7 @@ function getOptionData( const formattedLogin = Str.isSMSLogin(login) ? LocalePhoneNumber.formatPhoneNumber(login) : login; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. - const displayNamesWithTooltips: DisplayNamesWithTooltip[] = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report); // If the last actor's details are not currently saved in Onyx Collection, @@ -492,8 +421,8 @@ function getOptionData( result.alternateText = lastMessageText || formattedLogin; } - result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); - result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result); + result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result as Report); + result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result as Report); if (!hasMultipleParticipants) { result.accountID = personalDetail.accountID; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index baf4ba6fb2f8..c0e53a29c7f5 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -1,8 +1,10 @@ -import Onyx, {OnyxCollection} from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {RecentWaypoint, ReportAction, Transaction} from '@src/types/onyx'; import {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; +import {EmptyObject} from '@src/types/utils/EmptyObject'; import {isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; @@ -191,7 +193,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra * * @deprecated Use withOnyx() or Onyx.connect() instead */ -function getTransaction(transactionID: string): Transaction | Record { +function getTransaction(transactionID: string): OnyxEntry | EmptyObject { return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; } @@ -199,7 +201,7 @@ function getTransaction(transactionID: string): Transaction | Record): string { // Casting the description to string to avoid wrong data types (e.g. number) being returned from the API return transaction?.comment?.comment?.toString() ?? ''; } @@ -207,7 +209,7 @@ function getDescription(transaction: Transaction): string { /** * Return the amount field from the transaction, return the modifiedAmount if present. */ -function getAmount(transaction: Transaction, isFromExpenseReport: boolean): number { +function getAmount(transaction: OnyxEntry, isFromExpenseReport: boolean): number { // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value if (!isFromExpenseReport) { const amount = transaction?.modifiedAmount ?? 0; @@ -233,7 +235,7 @@ function getAmount(transaction: Transaction, isFromExpenseReport: boolean): numb /** * Return the currency field from the transaction, return the modifiedCurrency if present. */ -function getCurrency(transaction: Transaction): string { +function getCurrency(transaction: OnyxEntry): string { const currency = transaction?.modifiedCurrency ?? ''; if (currency) { return currency; @@ -259,28 +261,28 @@ function getOriginalAmount(transaction: Transaction): number { /** * Return the merchant field from the transaction, return the modifiedMerchant if present. */ -function getMerchant(transaction: Transaction): string { - return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant || ''; +function getMerchant(transaction: OnyxEntry): string { + return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant ?? ''; } /** * Return the mccGroup field from the transaction, return the modifiedMCCGroup if present. */ -function getMCCGroup(transaction: Transaction): string { - return transaction?.modifiedMCCGroup ? transaction.modifiedMCCGroup : transaction?.mccGroup ?? ''; +function getMCCGroup(transaction: Transaction): ValueOf | undefined { + return transaction?.modifiedMCCGroup ? transaction.modifiedMCCGroup : transaction?.mccGroup; } /** * Return the waypoints field from the transaction, return the modifiedWaypoints if present. */ -function getWaypoints(transaction: Transaction): WaypointCollection | undefined { +function getWaypoints(transaction: OnyxEntry): WaypointCollection | undefined { return transaction?.modifiedWaypoints ?? transaction?.comment?.waypoints; } /** * Return the category from the transaction. This "category" field has no "modified" complement. */ -function getCategory(transaction: Transaction): string { +function getCategory(transaction: OnyxEntry): string { return transaction?.category ?? ''; } @@ -294,27 +296,28 @@ function getCardID(transaction: Transaction): number { /** * Return the billable field from the transaction. This "billable" field has no "modified" complement. */ -function getBillable(transaction: Transaction): boolean { +function getBillable(transaction: OnyxEntry): boolean { return transaction?.billable ?? false; } /** * Return the tag from the transaction. This "tag" field has no "modified" complement. */ -function getTag(transaction: Transaction): string { +function getTag(transaction: OnyxEntry): string { return transaction?.tag ?? ''; } /** * Return the created field from the transaction, return the modifiedCreated if present. */ -function getCreated(transaction: Transaction, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING): string { +function getCreated(transaction: OnyxEntry, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING): string { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const created = transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || ''; return DateUtils.formatWithUTCTimeZone(created, dateFormat); } -function isDistanceRequest(transaction: Transaction): boolean { +function isDistanceRequest(transaction: OnyxEntry): boolean { const type = transaction?.comment?.type; const customUnitName = transaction?.comment?.customUnit?.name; return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; @@ -357,15 +360,15 @@ function isPosted(transaction: Transaction): boolean { return transaction.status === CONST.TRANSACTION.STATUS.POSTED; } -function isReceiptBeingScanned(transaction: Transaction): boolean { - return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction.receipt.state); +function isReceiptBeingScanned(transaction: OnyxEntry): boolean { + return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction?.receipt.state); } /** * Check if the transaction has a non-smartscanning receipt and is missing required fields */ -function hasMissingSmartscanFields(transaction: Transaction): boolean { - return hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction); +function hasMissingSmartscanFields(transaction: OnyxEntry): boolean { + return Boolean(transaction && hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction)); } /** @@ -381,7 +384,7 @@ function hasRoute(transaction: Transaction): boolean { * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getLinkedTransaction(reportAction: ReportAction): Transaction | Record { +function getLinkedTransaction(reportAction: OnyxEntry): Transaction | EmptyObject { let transactionID = ''; if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index b6d061432585..d091cca57df4 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -107,7 +107,7 @@ function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): * Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar * @param [avatarURL] - the avatar source from user's personalDetails */ -function isDefaultAvatar(avatarURL?: string): boolean { +function isDefaultAvatar(avatarURL?: string | React.FC): boolean { if (typeof avatarURL === 'string') { if (avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default')) { return true; @@ -134,7 +134,7 @@ function isDefaultAvatar(avatarURL?: string): boolean { * @param avatarURL - the avatar source from user's personalDetails * @param accountID - the accountID of the user */ -function getAvatar(avatarURL: string, accountID: number): React.FC | string { +function getAvatar(avatarURL: AvatarSource, accountID: number): AvatarSource { return isDefaultAvatar(avatarURL) ? getDefaultAvatar(accountID) : avatarURL; } @@ -153,7 +153,7 @@ function getAvatarUrl(avatarURL: string, accountID: number): string { * Avatars uploaded by users will have a _128 appended so that the asset server returns a small version. * This removes that part of the URL so the full version of the image can load. */ -function getFullSizeAvatar(avatarURL: string, accountID: number): React.FC | string { +function getFullSizeAvatar(avatarURL: string, accountID: number): AvatarSource { const source = getAvatar(avatarURL, accountID); if (typeof source !== 'string') { return source; @@ -165,7 +165,7 @@ function getFullSizeAvatar(avatarURL: string, accountID: number): React.FC. This adds the _128 at the end of the * source URL (before the file type) if it doesn't exist there already. */ -function getSmallSizeAvatar(avatarURL: string, accountID: number): React.FC | string { +function getSmallSizeAvatar(avatarURL: string, accountID: number): AvatarSource { const source = getAvatar(avatarURL, accountID); if (typeof source !== 'string') { return source; diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index e26cee71dc67..29d18d543a11 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -495,7 +495,7 @@ function updateAvatar(file: File | CustomRNImageManipulatorResult) { pendingFields: { avatar: null, }, - }, + } as OnyxEntry>, }, }, ]; diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index 49d2432277a0..7cd72fb4cd49 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -20,7 +20,7 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction) { }); // If there's a linked transaction, delete that too - const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(originalReportID, reportAction.reportActionID); + const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(originalReportID ?? '', reportAction.reportActionID); if (linkedTransactionID) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); } diff --git a/src/pages/home/report/withReportOrNotFound.tsx b/src/pages/home/report/withReportOrNotFound.tsx index 95997da71a2d..93d42ef6fd2b 100644 --- a/src/pages/home/report/withReportOrNotFound.tsx +++ b/src/pages/home/report/withReportOrNotFound.tsx @@ -1,7 +1,7 @@ /* eslint-disable rulesdir/no-negated-variables */ import {RouteProp} from '@react-navigation/native'; import React, {ComponentType, ForwardedRef, RefAttributes} from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import * as ReportUtils from '@libs/ReportUtils'; @@ -13,7 +13,7 @@ type OnyxProps = { /** The report currently being looked at */ report: OnyxEntry; /** The policies which the user has access to */ - policies: OnyxEntry; + policies: OnyxCollection; /** Beta features list */ betas: OnyxEntry; /** Indicated whether the report data is loading */ diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 37eaee513fb6..af52ea1222ed 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -45,15 +45,11 @@ const propTypes = { /** Which tab has been selected */ selectedTab: PropTypes.string, - - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), }; const defaultProps = { selectedTab: CONST.TAB.SCAN, report: {}, - betas: [], }; function MoneyRequestSelectorPage(props) { @@ -80,7 +76,7 @@ function MoneyRequestSelectorPage(props) { }; // Allow the user to create the request if we are creating the request in global menu or the report can create the request - const isAllowedToCreateRequest = _.isEmpty(props.report.reportID) || ReportUtils.canCreateRequest(props.report, props.betas, iouType); + const isAllowedToCreateRequest = _.isEmpty(props.report.reportID) || ReportUtils.canCreateRequest(props.report, iouType); const prevSelectedTab = usePrevious(props.selectedTab); useEffect(() => { diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index ac69baed3ef1..49d3428be532 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -21,7 +21,7 @@ type Icon = { name: string; /** Avatar id */ - id: number | string; + id?: number | string; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon?: AvatarSource; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index b5e4b25a6508..c5d9c27d34a1 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -3,7 +3,23 @@ import CONST from '@src/CONST'; import DeepValueOf from '@src/types/utils/DeepValueOf'; type ActionName = DeepValueOf; - +type OriginalMessageActionName = + | 'ADDCOMMENT' + | 'APPROVED' + | 'CHRONOSOOOLIST' + | 'CLOSED' + | 'CREATED' + | 'IOU' + | 'MODIFIEDEXPENSE' + | 'REIMBURSEMENTQUEUED' + | 'RENAMED' + | 'REPORTPREVIEW' + | 'SUBMITTED' + | 'TASKCANCELLED' + | 'TASKCOMPLETED' + | 'TASKEDITED' + | 'TASKREOPENED' + | ValueOf; type OriginalMessageApproved = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.APPROVED; originalMessage: unknown; @@ -15,24 +31,23 @@ type IOUDetails = { currency: string; }; +type IOUMessage = { + /** The ID of the iou transaction */ + IOUTransactionID?: string; + IOUReportID?: string; + amount: number; + comment?: string; + currency: string; + lastModified?: string; + participantAccountIDs?: number[]; + type: ValueOf; + paymentType?: DeepValueOf; + /** Only exists when we are sending money */ + IOUDetails?: IOUDetails; +}; type OriginalMessageIOU = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.IOU; - originalMessage: { - /** The ID of the iou transaction */ - IOUTransactionID?: string; - - IOUReportID?: number; - - /** Only exists when we are sending money */ - IOUDetails?: IOUDetails; - - amount: number; - comment?: string; - currency: string; - lastModified?: string; - participantAccountIDs?: number[]; - type: ValueOf; - }; + originalMessage: IOUMessage; }; type FlagSeverityName = ValueOf< @@ -67,6 +82,12 @@ type Reaction = { users: User[]; }; +type Closed = { + policyName: string; + reason: ValueOf; + lastModified?: string; +}; + type OriginalMessageAddComment = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT; originalMessage: { @@ -89,11 +110,7 @@ type OriginalMessageSubmitted = { type OriginalMessageClosed = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.CLOSED; - originalMessage: { - policyName: string; - reason: ValueOf; - lastModified?: string; - }; + originalMessage: Closed; }; type OriginalMessageCreated = { @@ -118,6 +135,11 @@ type ChronosOOOTimestamp = { timezone_type: number; }; +type ChangeLog = { + targetAccountIDs?: number[]; + roomName?: string; +}; + type ChronosOOOEvent = { id: string; lengthInDays: number; @@ -146,18 +168,12 @@ type OriginalMessageReportPreview = { type OriginalMessagePolicyChangeLog = { actionName: ValueOf; - originalMessage: { - targetAccountIDs?: number[]; - roomName?: string; - }; + originalMessage: ChangeLog; }; type OriginalMessageRoomChangeLog = { actionName: ValueOf; - originalMessage: { - targetAccountIDs?: number[]; - roomName?: string; - }; + originalMessage: ChangeLog; }; type OriginalMessagePolicyTask = { @@ -179,6 +195,16 @@ type OriginalMessageReimbursementQueued = { originalMessage: unknown; }; +type OriginalMessageMoved = { + actionName: typeof CONST.REPORT.ACTIONS.TYPE.MOVED; + originalMessage: { + fromPolicyID: string; + toPolicyID: string; + newParentReportID: string; + movedReportID: string; + }; +}; + type OriginalMessage = | OriginalMessageApproved | OriginalMessageIOU @@ -193,7 +219,8 @@ type OriginalMessage = | OriginalMessagePolicyChangeLog | OriginalMessagePolicyTask | OriginalMessageModifiedExpense - | OriginalMessageReimbursementQueued; + | OriginalMessageReimbursementQueued + | OriginalMessageMoved; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName, ChangeLog}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 51c1601be1e0..af559eafd0a1 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -1,3 +1,4 @@ +import {AvatarSource} from '@libs/UserUtils'; import TIMEZONES from '@src/TIMEZONES'; import * as OnyxCommon from './OnyxCommon'; @@ -31,7 +32,7 @@ type PersonalDetails = { phoneNumber?: string; /** Avatar URL of the current user from their personal details */ - avatar: string; + avatar: AvatarSource; /** Avatar thumbnail URL of the current user from their personal details */ avatarThumbnail?: string; @@ -53,6 +54,9 @@ type PersonalDetails = { /** Timezone of the current user from their personal details */ timezone?: Timezone; + /** Flag for checking if data is from optimistic data */ + isOptimisticPersonalDetail?: boolean; + /** Whether we are loading the data via the API */ isLoading?: boolean; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 4c37a87436eb..81a92c4bf603 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -1,6 +1,7 @@ import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import * as OnyxCommon from './OnyxCommon'; +import PersonalDetails from './PersonalDetails'; type Report = { /** The specific type of chat */ @@ -40,13 +41,13 @@ type Report = { lastReadSequenceNumber?: number; /** The time of the last mention of the report */ - lastMentionedTime?: string; + lastMentionedTime?: string | null; /** The current user's notification preference for this report */ notificationPreference?: string | number; /** The policy name to use */ - policyName?: string; + policyName?: string | null; /** The policy name to use for an archived report */ oldPolicyName?: string; @@ -91,7 +92,7 @@ type Report = { type?: string; /** The report visibility */ - visibility?: string; + visibility?: ValueOf; /** Report cached total */ cachedTotal?: string; @@ -111,6 +112,8 @@ type Report = { participantAccountIDs?: number[]; total?: number; currency?: string; + parentReportActionIDs?: number[]; + errorFields?: OnyxCommon.ErrorFields; /** Whether the report is waiting on a bank account */ isWaitingOnBankAccount?: boolean; @@ -132,6 +135,10 @@ type Report = { /** If the report contains nonreimbursable expenses, send the nonreimbursable total */ nonReimbursableTotal?: number; + isHidden?: boolean; + isChatRoom?: boolean; + participantsList?: Array>; + text?: string; }; export default Report; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 2195c4e3ff0b..891a0ffcb7b8 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,5 +1,9 @@ +import {SvgProps} from 'react-native-svg'; +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; import * as OnyxCommon from './OnyxCommon'; import OriginalMessage, {Decision, Reaction} from './OriginalMessage'; +import {Receipt} from './Transaction'; type Message = { /** The type of the action item fragment. Used to render a corresponding component */ @@ -81,26 +85,46 @@ type ReportActionBase = { /** accountIDs of the people to which the whisper was sent to (if any). Returns empty array if it is not a whisper */ whisperedToAccountIDs?: number[]; - /** Report action child status number */ - childStatusNum?: number; + avatar?: string | React.FC; - /** Report action child status name */ - childStateNum?: number; - - avatar?: string; automatic?: boolean; + shouldShow?: boolean; + + /** The ID of childReport */ childReportID?: string; + + /** Name of child report */ childReportName?: string; + + /** Type of child report */ childType?: string; + childOldestFourEmails?: string; childOldestFourAccountIDs?: string; childCommenterCount?: number; childLastVisibleActionCreated?: string; childVisibleActionCount?: number; + parentReportID?: string; + childManagerAccountID?: number; + + /** The status of the child report */ + childStatusNum?: ValueOf; + + /** Report action child status name */ + childStateNum?: ValueOf; + childLastReceiptTransactionIDs?: string; + childLastMoneyRequestComment?: string; timestamp?: number; reportActionTimestamp?: number; childMoneyRequestCount?: number; + isFirstItem?: boolean; + + /** Informations about attachments of report action */ + attachmentInfo?: (File & {source: string; uri: string}) | Record; + + /** Receipt tied to report action */ + receipt?: Receipt; /** ISO-formatted datetime */ lastModified?: string; @@ -112,6 +136,8 @@ type ReportActionBase = { errors?: OnyxCommon.Errors; isAttachment?: boolean; + childRecentReceiptTransactionIDs?: Record; + reportID?: string; }; type ReportAction = ReportActionBase & OriginalMessage; diff --git a/src/types/utils/EmptyObject.ts b/src/types/utils/EmptyObject.ts new file mode 100644 index 000000000000..9b9f3244a5f8 --- /dev/null +++ b/src/types/utils/EmptyObject.ts @@ -0,0 +1,15 @@ +import Falsy from './Falsy'; + +type EmptyObject = Record; + +// eslint-disable-next-line rulesdir/no-negated-variables +function isNotEmptyObject | Falsy>(arg: T | EmptyObject): arg is NonNullable { + return Object.keys(arg ?? {}).length > 0; +} + +function isEmptyObject(obj: T): boolean { + return Object.keys(obj ?? {}).length === 0; +} + +export {isNotEmptyObject, isEmptyObject}; +export type {EmptyObject}; diff --git a/src/types/utils/Falsy.ts b/src/types/utils/Falsy.ts new file mode 100644 index 000000000000..c1bd7527a223 --- /dev/null +++ b/src/types/utils/Falsy.ts @@ -0,0 +1,3 @@ +type Falsy = undefined | null | false; + +export default Falsy;