From 115c045f5ac3906b954b17e39177c187aaeb9c7c Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 15 May 2024 20:20:51 -0700 Subject: [PATCH 001/420] Allow distance splits on start page --- src/pages/iou/request/IOURequestStartPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 1bbf0d02a941..573c44beaf96 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -103,7 +103,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate) && iouType !== CONST.IOU.TYPE.SPLIT; + const shouldDisplayDistanceRequest = (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate); // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the exoense const isAllowedToCreateRequest = isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || PolicyUtils.canSendInvoice(allPolicies); From 3ee8a7aa5401ca541a7a0d3b501b1f931830679b Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 16 May 2024 07:37:19 -0700 Subject: [PATCH 002/420] Set split share when distanceAmount updates --- src/components/MoneyRequestConfirmationList.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 5ffd9beda6fe..5517b3b5f543 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -306,10 +306,11 @@ function MoneyRequestConfirmationList({ const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; + const distanceRequestAmount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); const formattedAmount = isDistanceRequestWithPendingRoute ? '' : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0) : iouAmount, + shouldCalculateDistanceAmount ? distanceRequestAmount : iouAmount, isDistanceRequest ? currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); @@ -365,9 +366,15 @@ function MoneyRequestConfirmationList({ return; } - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); + const amount = distanceRequestAmount; IOU.setMoneyRequestAmount(transactionID, amount, currency ?? ''); - }, [shouldCalculateDistanceAmount, distance, rate, unit, transactionID, currency]); + + // If it's a split request among individuals, set the split shares + const participantAccountIDs: number[] = selectedParticipantsProp.map((participant) => participant.accountID ?? -1); + if (isTypeSplit && !isPolicyExpenseChat && amount && transaction?.currency) { + IOU.setSplitShares(transaction, amount, currency, participantAccountIDs); + } + }, [shouldCalculateDistanceAmount, distanceRequestAmount, transactionID, currency, isTypeSplit, isPolicyExpenseChat, selectedParticipantsProp, transaction]); // Calculate and set tax amount in transaction draft useEffect(() => { From 813f8ca9c0dd58bf15120631e816ca9129f216b8 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 16 May 2024 08:30:45 -0700 Subject: [PATCH 003/420] WIP split distance global create set split data --- src/libs/actions/IOU.ts | 211 ++++++++++-------- .../step/IOURequestStepConfirmation.tsx | 51 +++-- 2 files changed, 148 insertions(+), 114 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 2464dbff7dbd..2d1b9115e8d1 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2191,99 +2191,6 @@ function getTrackExpenseInformation( }; } -/** Requests money based on a distance (e.g. mileage from a map) */ -function createDistanceRequest( - report: OnyxEntry, - participant: Participant, - comment: string, - created: string, - category: string | undefined, - tag: string | undefined, - taxCode: string | undefined, - taxAmount: number | undefined, - amount: number, - currency: string, - merchant: string, - billable: boolean | undefined, - validWaypoints: WaypointCollection, - policy?: OnyxEntry, - policyTagList?: OnyxEntry, - policyCategories?: OnyxEntry, - customUnitRateID?: string, -) { - // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function - const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report?.chatReportID) : report; - const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; - const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); - - const optimisticReceipt: Receipt = { - source: ReceiptGeneric as ReceiptSource, - state: CONST.IOU.RECEIPT_STATE.OPEN, - }; - const { - iouReport, - chatReport, - transaction, - iouAction, - createdChatReportActionID, - createdIOUReportActionID, - reportPreviewAction, - transactionThreadReportID, - createdReportActionIDForThread, - payerEmail, - onyxData, - } = getMoneyRequestInformation( - currentChatReport, - participant, - comment, - amount, - currency, - currentCreated, - merchant, - optimisticReceipt, - undefined, - category, - tag, - taxCode, - taxAmount, - billable, - policy, - policyTagList, - policyCategories, - userAccountID, - currentUserEmail, - moneyRequestReportID, - ); - - const activeReportID = isMoneyRequestReport ? report?.reportID ?? '' : chatReport.reportID; - const parameters: CreateDistanceRequestParams = { - comment, - iouReportID: iouReport.reportID, - chatReportID: chatReport.reportID, - transactionID: transaction.transactionID, - reportActionID: iouAction.reportActionID, - createdChatReportActionID, - createdIOUReportActionID, - reportPreviewReportActionID: reportPreviewAction.reportActionID, - waypoints: JSON.stringify(validWaypoints), - created: currentCreated, - category, - tag, - taxCode, - taxAmount, - billable, - transactionThreadReportID, - createdReportActionIDForThread, - payerEmail, - customUnitRateID, - }; - - API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData); - Navigation.dismissModal(activeReportID); - Report.notifyNewAction(activeReportID, userAccountID); -} - /** * Compute the diff amount when we update the transaction */ @@ -4807,6 +4714,124 @@ function setDraftSplitTransaction(transactionID: string, transactionChanges: Tra Onyx.merge(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, updatedTransaction); } +/** Requests money based on a distance (e.g. mileage from a map) */ +function createDistanceRequest( + report: OnyxEntry, + participants: Participant[], + comment: string, + created: string, + category: string | undefined, + tag: string | undefined, + taxCode: string | undefined, + taxAmount: number | undefined, + amount: number, + currency: string, + merchant: string, + billable: boolean | undefined, + validWaypoints: WaypointCollection, + policy?: OnyxEntry, + policyTagList?: OnyxEntry, + policyCategories?: OnyxEntry, + customUnitRateID?: string, + currentUserLogin = '', + currentUserAccountID = -1, + splitShares: SplitShares = {}, +) { + // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function + const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); + const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report?.chatReportID) : report; + const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; + const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); + + const optimisticReceipt: Receipt = { + source: ReceiptGeneric as ReceiptSource, + state: CONST.IOU.RECEIPT_STATE.OPEN, + }; + + if (participants.length > 1) { + const {splitData, splits, onyxData} = createSplitsAndOnyxData( + participants, + currentUserLogin ?? '', + currentUserAccountID, + amount, + comment, + currency, + merchant, + currentCreated, + category ?? '', + tag ?? '', + splitShares, + '', + billable, + CONST.IOU.REQUEST_TYPE.DISTANCE, + ); + + } + + const participant = participants[0] ?? {}; + const { + iouReport, + chatReport, + transaction, + iouAction, + createdChatReportActionID, + createdIOUReportActionID, + reportPreviewAction, + transactionThreadReportID, + createdReportActionIDForThread, + payerEmail, + onyxData, + } = getMoneyRequestInformation( + currentChatReport, + participant, + comment, + amount, + currency, + currentCreated, + merchant, + optimisticReceipt, + undefined, + category, + tag, + taxCode, + taxAmount, + billable, + policy, + policyTagList, + policyCategories, + userAccountID, + currentUserEmail, + moneyRequestReportID, + ); + + const activeReportID = isMoneyRequestReport ? report?.reportID ?? '' : chatReport.reportID; + const parameters: CreateDistanceRequestParams = { + comment, + iouReportID: iouReport.reportID, + chatReportID: chatReport.reportID, + transactionID: transaction.transactionID, + reportActionID: iouAction.reportActionID, + createdChatReportActionID, + createdIOUReportActionID, + reportPreviewReportActionID: reportPreviewAction.reportActionID, + waypoints: JSON.stringify(validWaypoints), + created: currentCreated, + category, + tag, + taxCode, + taxAmount, + billable, + transactionThreadReportID, + createdReportActionIDForThread, + payerEmail, + customUnitRateID, + }; + + API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData); + Navigation.dismissModal(activeReportID); + Report.notifyNewAction(activeReportID, userAccountID); +} + function editRegularMoneyRequest( transactionID: string, transactionThreadReportID: string, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 458df99a5f6c..0384eb6f8179 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -289,13 +289,14 @@ function IOURequestStepConfirmation({ ); const createDistanceRequest = useCallback( - (selectedParticipants: Participant[], trimmedComment: string, customUnitRateID: string) => { + (selectedParticipants: Participant[], trimmedComment: string) => { if (!transaction) { return; } + const customUnitRateID = TransactionUtils.getRateID(transaction) ?? ''; IOU.createDistanceRequest( report, - selectedParticipants[0], + selectedParticipants, trimmedComment, transaction.created, transaction.category, @@ -311,9 +312,13 @@ function IOURequestStepConfirmation({ policyTags, policyCategories, customUnitRateID, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + transaction.splitShares, + // splitPayerAccountIDs: transaction.splitPayerAccountIDs, ); }, - [policy, policyCategories, policyTags, report, transaction, transactionTaxCode, transactionTaxAmount], + [policy, policyCategories, policyTags, report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails], ); const createTransaction = useCallback( @@ -383,22 +388,27 @@ function IOURequestStepConfirmation({ // If the split expense is created from the global create menu, we also navigate the user to the group report if (iouType === CONST.IOU.TYPE.SPLIT) { if (currentUserPersonalDetails.login && !!transaction) { - IOU.splitBillAndOpenReport({ - participants: splitParticipants, - currentUserLogin: currentUserPersonalDetails.login, - currentUserAccountID: currentUserPersonalDetails.accountID, - amount: transaction.amount, - comment: trimmedComment, - currency: transaction.currency, - merchant: transaction.merchant, - created: transaction.created, - category: transaction.category, - tag: transaction.tag, - billable: !!transaction.billable, - iouRequestType: transaction.iouRequestType, - splitShares: transaction.splitShares, - splitPayerAccountIDs: transaction.splitPayerAccountIDs, - }); + if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { + createDistanceRequest(splitParticipants, trimmedComment); + } else { + IOU.splitBillAndOpenReport({ + participants: splitParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + amount: transaction.amount, + comment: trimmedComment, + currency: transaction.currency, + merchant: transaction.merchant, + created: transaction.created, + category: transaction.category, + tag: transaction.tag, + billable: !!transaction.billable, + iouRequestType: transaction.iouRequestType, + splitShares: transaction.splitShares, + splitPayerAccountIDs: transaction.splitPayerAccountIDs, + }); + } + } return; } @@ -469,8 +479,7 @@ function IOURequestStepConfirmation({ } if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE && !IOUUtils.isMovingTransactionFromTrackExpense(action)) { - const customUnitRateID = TransactionUtils.getRateID(transaction) ?? ''; - createDistanceRequest(selectedParticipants, trimmedComment, customUnitRateID); + createDistanceRequest(selectedParticipants, trimmedComment); return; } From cc66e8083d25166c7a6865d5fb3566cddc56f771 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 16 May 2024 08:49:08 -0700 Subject: [PATCH 004/420] WIP set up CreateDistanceRequestParams for split --- src/libs/actions/IOU.ts | 142 +++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 59 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 2d1b9115e8d1..c93e2d0322bb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4748,6 +4748,8 @@ function createDistanceRequest( state: CONST.IOU.RECEIPT_STATE.OPEN, }; + let parameters: CreateDistanceRequestParams; + if (participants.length > 1) { const {splitData, splits, onyxData} = createSplitsAndOnyxData( participants, @@ -4766,66 +4768,88 @@ function createDistanceRequest( CONST.IOU.REQUEST_TYPE.DISTANCE, ); - } - - const participant = participants[0] ?? {}; - const { - iouReport, - chatReport, - transaction, - iouAction, - createdChatReportActionID, - createdIOUReportActionID, - reportPreviewAction, - transactionThreadReportID, - createdReportActionIDForThread, - payerEmail, - onyxData, - } = getMoneyRequestInformation( - currentChatReport, - participant, - comment, - amount, - currency, - currentCreated, - merchant, - optimisticReceipt, - undefined, - category, - tag, - taxCode, - taxAmount, - billable, - policy, - policyTagList, - policyCategories, - userAccountID, - currentUserEmail, - moneyRequestReportID, - ); + parameters = { + comment, + iouReportID: iouReport.reportID, + chatReportID: splitData.chatReportID, + transactionID: splitData.transactionID, + reportActionID: splitData.reportActionID, + createdChatReportActionID, + createdIOUReportActionID: splitData.createdReportActionID, + reportPreviewReportActionID: reportPreviewAction.reportActionID, + waypoints: JSON.stringify(validWaypoints), + created: currentCreated, + category, + tag, + taxCode, + taxAmount, + billable, + transactionThreadReportID, + createdReportActionIDForThread, + customUnitRateID, + splits: JSON.stringify(splits), + chatType: splitData.chatType, + }; + } else { + const participant = participants[0] ?? {}; + const { + iouReport, + chatReport, + transaction, + iouAction, + createdChatReportActionID, + createdIOUReportActionID, + reportPreviewAction, + transactionThreadReportID, + createdReportActionIDForThread, + payerEmail, + onyxData, + } = getMoneyRequestInformation( + currentChatReport, + participant, + comment, + amount, + currency, + currentCreated, + merchant, + optimisticReceipt, + undefined, + category, + tag, + taxCode, + taxAmount, + billable, + policy, + policyTagList, + policyCategories, + userAccountID, + currentUserEmail, + moneyRequestReportID, + ); - const activeReportID = isMoneyRequestReport ? report?.reportID ?? '' : chatReport.reportID; - const parameters: CreateDistanceRequestParams = { - comment, - iouReportID: iouReport.reportID, - chatReportID: chatReport.reportID, - transactionID: transaction.transactionID, - reportActionID: iouAction.reportActionID, - createdChatReportActionID, - createdIOUReportActionID, - reportPreviewReportActionID: reportPreviewAction.reportActionID, - waypoints: JSON.stringify(validWaypoints), - created: currentCreated, - category, - tag, - taxCode, - taxAmount, - billable, - transactionThreadReportID, - createdReportActionIDForThread, - payerEmail, - customUnitRateID, - }; + const activeReportID = isMoneyRequestReport ? report?.reportID ?? '' : chatReport.reportID; + parameters = { + comment, + iouReportID: iouReport.reportID, + chatReportID: chatReport.reportID, + transactionID: transaction.transactionID, + reportActionID: iouAction.reportActionID, + createdChatReportActionID, + createdIOUReportActionID, + reportPreviewReportActionID: reportPreviewAction.reportActionID, + waypoints: JSON.stringify(validWaypoints), + created: currentCreated, + category, + tag, + taxCode, + taxAmount, + billable, + transactionThreadReportID, + createdReportActionIDForThread, + payerEmail, + customUnitRateID, + }; + } API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData); Navigation.dismissModal(activeReportID); From a9e267ca7f65b81a2e2ec2d091288f6cd2a55575 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 16 May 2024 08:49:15 -0700 Subject: [PATCH 005/420] Prettier --- src/components/MoneyRequestConfirmationList.tsx | 5 +---- src/pages/iou/request/IOURequestStartPage.tsx | 2 +- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 5 ++--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 5517b3b5f543..a7f476cde9be 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -309,10 +309,7 @@ function MoneyRequestConfirmationList({ const distanceRequestAmount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); const formattedAmount = isDistanceRequestWithPendingRoute ? '' - : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? distanceRequestAmount : iouAmount, - isDistanceRequest ? currency : iouCurrencyCode, - ); + : CurrencyUtils.convertToDisplayString(shouldCalculateDistanceAmount ? distanceRequestAmount : iouAmount, isDistanceRequest ? currency : iouCurrencyCode); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); const taxRateTitle = TransactionUtils.getTaxName(policy, transaction); diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 573c44beaf96..aa089f7008f5 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -103,7 +103,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate); + const shouldDisplayDistanceRequest = !!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the exoense const isAllowedToCreateRequest = isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || PolicyUtils.canSendInvoice(allPolicies); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 0384eb6f8179..e9874e0a0c77 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -312,10 +312,10 @@ function IOURequestStepConfirmation({ policyTags, policyCategories, customUnitRateID, - currentUserPersonalDetails.login, + currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, transaction.splitShares, - // splitPayerAccountIDs: transaction.splitPayerAccountIDs, + // splitPayerAccountIDs: transaction.splitPayerAccountIDs, ); }, [policy, policyCategories, policyTags, report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails], @@ -408,7 +408,6 @@ function IOURequestStepConfirmation({ splitPayerAccountIDs: transaction.splitPayerAccountIDs, }); } - } return; } From c9a7962248074e2a92e73b60abbf616bbd1b0030 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 16 May 2024 12:51:23 -0700 Subject: [PATCH 006/420] Carefully fix distance split params --- .../API/parameters/CreateDistanceRequestParams.ts | 2 ++ src/libs/actions/IOU.ts | 15 +++++++++------ .../request/step/IOURequestStepConfirmation.tsx | 1 - 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/libs/API/parameters/CreateDistanceRequestParams.ts b/src/libs/API/parameters/CreateDistanceRequestParams.ts index 68469899af06..c8f9f4a80538 100644 --- a/src/libs/API/parameters/CreateDistanceRequestParams.ts +++ b/src/libs/API/parameters/CreateDistanceRequestParams.ts @@ -18,6 +18,8 @@ type CreateDistanceRequestParams = { createdReportActionIDForThread: string; payerEmail: string; customUnitRateID?: string; + splits?: string; + chatType?: string; }; export default CreateDistanceRequestParams; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c93e2d0322bb..a98141d01fff 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4768,15 +4768,17 @@ function createDistanceRequest( CONST.IOU.REQUEST_TYPE.DISTANCE, ); + // Splits don't use the IOU report param. The split transaction isn't linked to a report shown in the UI, it's linked to a special default reportID of -2. + // Therefore, anything related to the IOU report is irrelevant. parameters = { comment, - iouReportID: iouReport.reportID, + iouReportID: '', chatReportID: splitData.chatReportID, transactionID: splitData.transactionID, reportActionID: splitData.reportActionID, - createdChatReportActionID, - createdIOUReportActionID: splitData.createdReportActionID, - reportPreviewReportActionID: reportPreviewAction.reportActionID, + createdChatReportActionID: splitData.createdReportActionID ?? '', + createdIOUReportActionID: '', + reportPreviewReportActionID: '', // No report preview needed, since the split transaction report is not shown in the UI waypoints: JSON.stringify(validWaypoints), created: currentCreated, category, @@ -4784,9 +4786,10 @@ function createDistanceRequest( taxCode, taxAmount, billable, - transactionThreadReportID, - createdReportActionIDForThread, + transactionThreadReportID: '', + createdReportActionIDForThread: '', customUnitRateID, + payerEmail: '', splits: JSON.stringify(splits), chatType: splitData.chatType, }; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index e9874e0a0c77..45a331fa0402 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -315,7 +315,6 @@ function IOURequestStepConfirmation({ currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, transaction.splitShares, - // splitPayerAccountIDs: transaction.splitPayerAccountIDs, ); }, [policy, policyCategories, policyTags, report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails], From 87010b18e844c68656e4f4f51894d5c58068363b Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 16 May 2024 12:58:29 -0700 Subject: [PATCH 007/420] Set onyxData from split or money request data --- src/libs/actions/IOU.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index a98141d01fff..c2f05bb348ad 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4739,6 +4739,7 @@ function createDistanceRequest( ) { // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); + const activeReportID = isMoneyRequestReport ? report?.reportID ?? '' : chatReport.reportID; const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report?.chatReportID) : report; const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); @@ -4749,9 +4750,9 @@ function createDistanceRequest( }; let parameters: CreateDistanceRequestParams; - + let onyxData: OnyxData; if (participants.length > 1) { - const {splitData, splits, onyxData} = createSplitsAndOnyxData( + const {splitData, splits, onyxData: splitOnyxData} = createSplitsAndOnyxData( participants, currentUserLogin ?? '', currentUserAccountID, @@ -4767,6 +4768,7 @@ function createDistanceRequest( billable, CONST.IOU.REQUEST_TYPE.DISTANCE, ); + onyxData = splitOnyxData; // Splits don't use the IOU report param. The split transaction isn't linked to a report shown in the UI, it's linked to a special default reportID of -2. // Therefore, anything related to the IOU report is irrelevant. @@ -4806,7 +4808,7 @@ function createDistanceRequest( transactionThreadReportID, createdReportActionIDForThread, payerEmail, - onyxData, + onyxData: moneyRequestOnyxData, } = getMoneyRequestInformation( currentChatReport, participant, @@ -4829,8 +4831,8 @@ function createDistanceRequest( currentUserEmail, moneyRequestReportID, ); + onyxData = moneyRequestOnyxData; - const activeReportID = isMoneyRequestReport ? report?.reportID ?? '' : chatReport.reportID; parameters = { comment, iouReportID: iouReport.reportID, From d7c54247046fca71a8faa1c0e276a00d1c738f10 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 16 May 2024 14:07:35 -0700 Subject: [PATCH 008/420] Make distance split navigate for report or global create --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c2f05bb348ad..4eec736647e2 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4739,7 +4739,6 @@ function createDistanceRequest( ) { // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const activeReportID = isMoneyRequestReport ? report?.reportID ?? '' : chatReport.reportID; const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report?.chatReportID) : report; const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); @@ -4857,6 +4856,7 @@ function createDistanceRequest( } API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData); + const activeReportID = isMoneyRequestReport ? report?.reportID ?? '' : parameters.chatReportID; Navigation.dismissModal(activeReportID); Report.notifyNewAction(activeReportID, userAccountID); } From 8ba2bd1db5da235a9929593dfad1c9809398fe01 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 16 May 2024 14:08:15 -0700 Subject: [PATCH 009/420] Simplify creating distance for split or regular request --- .../step/IOURequestStepConfirmation.tsx | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 45a331fa0402..60da290310ae 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -340,6 +340,11 @@ function IOURequestStepConfirmation({ formHasBeenSubmitted.current = true; + if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE && !IOUUtils.isMovingTransactionFromTrackExpense(action)) { + createDistanceRequest((iouType === CONST.IOU.TYPE.SPLIT) ? splitParticipants : selectedParticipants, trimmedComment); + return; + } + // If we have a receipt let's start the split expense by creating only the action, the transaction, and the group DM if needed if (iouType === CONST.IOU.TYPE.SPLIT && receiptFile) { if (currentUserPersonalDetails.login && !!transaction) { @@ -387,26 +392,22 @@ function IOURequestStepConfirmation({ // If the split expense is created from the global create menu, we also navigate the user to the group report if (iouType === CONST.IOU.TYPE.SPLIT) { if (currentUserPersonalDetails.login && !!transaction) { - if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { - createDistanceRequest(splitParticipants, trimmedComment); - } else { - IOU.splitBillAndOpenReport({ - participants: splitParticipants, - currentUserLogin: currentUserPersonalDetails.login, - currentUserAccountID: currentUserPersonalDetails.accountID, - amount: transaction.amount, - comment: trimmedComment, - currency: transaction.currency, - merchant: transaction.merchant, - created: transaction.created, - category: transaction.category, - tag: transaction.tag, - billable: !!transaction.billable, - iouRequestType: transaction.iouRequestType, - splitShares: transaction.splitShares, - splitPayerAccountIDs: transaction.splitPayerAccountIDs, - }); - } + IOU.splitBillAndOpenReport({ + participants: splitParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + amount: transaction.amount, + comment: trimmedComment, + currency: transaction.currency, + merchant: transaction.merchant, + created: transaction.created, + category: transaction.category, + tag: transaction.tag, + billable: !!transaction.billable, + iouRequestType: transaction.iouRequestType, + splitShares: transaction.splitShares, + splitPayerAccountIDs: transaction.splitPayerAccountIDs, + }); } return; } @@ -476,10 +477,6 @@ function IOURequestStepConfirmation({ return; } - if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE && !IOUUtils.isMovingTransactionFromTrackExpense(action)) { - createDistanceRequest(selectedParticipants, trimmedComment); - return; - } requestMoney(selectedParticipants, trimmedComment); }, From 4591003df992d7ad70f6852a1d9d02d61db5ce76 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 16 May 2024 14:47:22 -0700 Subject: [PATCH 010/420] Make customUnitRateID basically required, fix tsc --- .../parameters/CreateDistanceRequestParams.ts | 20 +++++++++---------- src/libs/actions/IOU.ts | 18 ++++++----------- .../request/step/IOURequestStepDistance.tsx | 7 ++++++- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/libs/API/parameters/CreateDistanceRequestParams.ts b/src/libs/API/parameters/CreateDistanceRequestParams.ts index c8f9f4a80538..07dd594d7356 100644 --- a/src/libs/API/parameters/CreateDistanceRequestParams.ts +++ b/src/libs/API/parameters/CreateDistanceRequestParams.ts @@ -1,23 +1,23 @@ type CreateDistanceRequestParams = { - comment: string; - iouReportID: string; - chatReportID: string; transactionID: string; - reportActionID: string; + chatReportID: string; createdChatReportActionID: string; - createdIOUReportActionID: string; - reportPreviewReportActionID: string; + reportActionID: string; waypoints: string; + customUnitRateID: string; + comment: string; created: string; + iouReportID?: string; + createdIOUReportActionID?: string; + reportPreviewReportActionID?: string; category?: string; tag?: string; taxCode?: string; taxAmount?: number; billable?: boolean; - transactionThreadReportID: string; - createdReportActionIDForThread: string; - payerEmail: string; - customUnitRateID?: string; + transactionThreadReportID?: string; + createdReportActionIDForThread?: string; + payerEmail?: string; splits?: string; chatType?: string; }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4eec736647e2..0fe9347205ef 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4732,7 +4732,7 @@ function createDistanceRequest( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, - customUnitRateID?: string, + customUnitRateID = '', currentUserLogin = '', currentUserAccountID = -1, splitShares: SplitShares = {}, @@ -4770,27 +4770,21 @@ function createDistanceRequest( onyxData = splitOnyxData; // Splits don't use the IOU report param. The split transaction isn't linked to a report shown in the UI, it's linked to a special default reportID of -2. - // Therefore, anything related to the IOU report is irrelevant. + // Therefore, any params related to the IOU report are irrelevant and omitted below. parameters = { - comment, - iouReportID: '', - chatReportID: splitData.chatReportID, transactionID: splitData.transactionID, - reportActionID: splitData.reportActionID, + chatReportID: splitData.chatReportID, createdChatReportActionID: splitData.createdReportActionID ?? '', - createdIOUReportActionID: '', - reportPreviewReportActionID: '', // No report preview needed, since the split transaction report is not shown in the UI + reportActionID: splitData.reportActionID, waypoints: JSON.stringify(validWaypoints), + customUnitRateID, + comment, created: currentCreated, category, tag, taxCode, taxAmount, billable, - transactionThreadReportID: '', - createdReportActionIDForThread: '', - customUnitRateID, - payerEmail: '', splits: JSON.stringify(splits), chatType: splitData.chatType, }; diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 0d5972cffcb9..a15ca9809d5b 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -37,6 +37,7 @@ import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; type IOURequestStepDistanceOnyxProps = { /** backup version of the original transaction */ @@ -278,7 +279,7 @@ function IOURequestStepDistance({ IOU.createDistanceRequest( report, - participants[0], + participants, '', transaction?.created ?? '', '', @@ -290,6 +291,10 @@ function IOURequestStepDistance({ translate('iou.fieldPending'), false, TransactionUtils.getValidWaypoints(waypoints, true), + undefined, + undefined, + undefined, + DistanceRequestUtils.getCustomUnitRateID(report.reportID), ); return; } From 944c6ed14a28604782026d8da2ea7e02bb60065c Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 10 Jun 2024 17:03:43 +0700 Subject: [PATCH 011/420] fix Hide video playback controls on auto-playing videos --- src/components/FeatureTrainingModal.tsx | 1 + .../VideoPlayer/BaseVideoPlayer.tsx | 29 +++++++++++++++++-- src/components/VideoPlayer/types.ts | 1 + 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 88099b6d078b..587e48605828 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -135,6 +135,7 @@ function FeatureTrainingModal({ videoPlayerStyle={[styles.onboardingVideoPlayer, {aspectRatio}]} onVideoLoaded={setAspectRatio} controlsStatus={CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW} + canToggleControlOnTap shouldUseControlsBottomMargin={false} shouldPlay isLooping diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index bc7c32729c5a..d193ea88e54e 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -5,6 +5,7 @@ import type {MutableRefObject} from 'react'; import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; import {View} from 'react-native'; +import {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Hoverable from '@components/Hoverable'; @@ -48,6 +49,7 @@ function BaseVideoPlayer({ // eslint-disable-next-line @typescript-eslint/no-unused-vars isVideoHovered = false, isPreview, + canToggleControlOnTap = false, }: VideoPlayerProps) { const styles = useThemeStyles(); const { @@ -73,6 +75,11 @@ function BaseVideoPlayer({ const [sourceURL] = useState(VideoUtils.addSkipTimeTagToURL(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url), 0.001)); const [isPopoverVisible, setIsPopoverVisible] = useState(false); const [popoverAnchorPosition, setPopoverAnchorPosition] = useState({horizontal: 0, vertical: 0}); + const [controlStatusState, setControlStatusState] = useState(controlsStatus); + const controlsOpacity = useSharedValue(1); + const controlsAnimatedStyle = useAnimatedStyle(() => ({ + opacity: controlsOpacity.value, + })); const videoPlayerRef = useRef(null); const videoPlayerElementParentRef = useRef(null); @@ -96,6 +103,21 @@ function BaseVideoPlayer({ } }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumber]); + const toggleControlStatusState = useCallback(() => { + if (!canToggleControlOnTap) { + return; + } + if (controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { + setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE); + } else { + setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); + controlsOpacity.value = 1; + setTimeout(() => { + controlsOpacity.value = withTiming(0, {duration: 500}, () => setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); + }, 2000); + } + }, [canToggleControlOnTap, controlStatusState]); + const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { videoPopoverMenuPlayerRef.current = videoPlayerRef.current; videoPlayerRef.current?.getStatusAsync().then((status) => { @@ -311,6 +333,7 @@ function BaseVideoPlayer({ return; } togglePlayCurrentVideo(); + toggleControlStatusState(); }} style={[styles.flex1, styles.noSelect]} > @@ -367,7 +390,7 @@ function BaseVideoPlayer({ {((isLoading && !isOffline) || isBuffering) && } {isLoading && !isBuffering && } - {controlsStatus !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && ( + {controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && ( )} diff --git a/src/components/VideoPlayer/types.ts b/src/components/VideoPlayer/types.ts index 3dd3884534ec..7aaa06126122 100644 --- a/src/components/VideoPlayer/types.ts +++ b/src/components/VideoPlayer/types.ts @@ -30,6 +30,7 @@ type VideoPlayerProps = { controlsStatus?: ValueOf; shouldPlay?: boolean; isPreview?: boolean; + canToggleControlOnTap?: boolean; }; export type {VideoPlayerProps, VideoWithOnFullScreenUpdate}; From fe167aa523065af8dacc9de2131dbc4338b70857 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 10 Jun 2024 17:13:20 +0700 Subject: [PATCH 012/420] fix lint --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index d193ea88e54e..560d17067e99 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -116,7 +116,7 @@ function BaseVideoPlayer({ controlsOpacity.value = withTiming(0, {duration: 500}, () => setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); }, 2000); } - }, [canToggleControlOnTap, controlStatusState]); + }, [canToggleControlOnTap, controlStatusState, controlsOpacity]); const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { videoPopoverMenuPlayerRef.current = videoPlayerRef.current; From 9caa203f83ce7de986b3b5e0cd53c7a7dc0a4a97 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 10 Jun 2024 17:41:03 +0700 Subject: [PATCH 013/420] fix update controlsStatus --- src/components/FeatureTrainingModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 587e48605828..5825a6fddd58 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -134,7 +134,7 @@ function FeatureTrainingModal({ url={videoURL} videoPlayerStyle={[styles.onboardingVideoPlayer, {aspectRatio}]} onVideoLoaded={setAspectRatio} - controlsStatus={CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW} + controlsStatus={CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE} canToggleControlOnTap shouldUseControlsBottomMargin={false} shouldPlay From 8a7993d11fa8feb5bd67df8d441ef12c8f27ca22 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 11 Jun 2024 09:50:34 +0700 Subject: [PATCH 014/420] fix use runOnJS --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 560d17067e99..be5a842b6b29 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -5,7 +5,7 @@ import type {MutableRefObject} from 'react'; import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; import {View} from 'react-native'; -import {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import {runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Hoverable from '@components/Hoverable'; @@ -113,7 +113,7 @@ function BaseVideoPlayer({ setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); controlsOpacity.value = 1; setTimeout(() => { - controlsOpacity.value = withTiming(0, {duration: 500}, () => setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); + controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); }, 2000); } }, [canToggleControlOnTap, controlStatusState, controlsOpacity]); From 336172b25ce7a6098553a6cd388f8e776418a402 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 20 Jun 2024 14:36:30 -0700 Subject: [PATCH 015/420] Remove accidentally committed file --- src/libs/actions/creatDistance.ts | 91 ------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 src/libs/actions/creatDistance.ts diff --git a/src/libs/actions/creatDistance.ts b/src/libs/actions/creatDistance.ts deleted file mode 100644 index 42263b01b366..000000000000 --- a/src/libs/actions/creatDistance.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** Requests money based on a distance (e.g. mileage from a map) */ -function createDistanceRequest( - report: OnyxEntry, - participant: Participant, - comment: string, - created: string, - category: string | undefined, - tag: string | undefined, - taxCode: string | undefined, - taxAmount: number | undefined, - amount: number, - currency: string, - merchant: string, - billable: boolean | undefined, - validWaypoints: WaypointCollection, - policy?: OnyxEntry, - policyTagList?: OnyxEntry, - policyCategories?: OnyxEntry, - customUnitRateID?: string, -) { - // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function - const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const currentChatReport = isMoneyRequestReport ? getReportOrDraftReport(report?.chatReportID) : report; - const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; - - const optimisticReceipt: Receipt = { - source: ReceiptGeneric as ReceiptSource, - state: CONST.IOU.RECEIPT_STATE.OPEN, - }; - const { - iouReport, - chatReport, - transaction, - iouAction, - createdChatReportActionID, - createdIOUReportActionID, - reportPreviewAction, - transactionThreadReportID, - createdReportActionIDForThread, - payerEmail, - onyxData, - } = getMoneyRequestInformation( - currentChatReport, - participant, - comment, - amount, - currency, - created, - merchant, - optimisticReceipt, - undefined, - category, - tag, - taxCode, - taxAmount, - billable, - policy, - policyTagList, - policyCategories, - userAccountID, - currentUserEmail, - moneyRequestReportID, - ); - - const activeReportID = isMoneyRequestReport ? report?.reportID ?? '-1' : chatReport.reportID; - const parameters: CreateDistanceRequestParams = { - comment, - iouReportID: iouReport.reportID, - chatReportID: chatReport.reportID, - transactionID: transaction.transactionID, - reportActionID: iouAction.reportActionID, - createdChatReportActionID, - createdIOUReportActionID, - reportPreviewReportActionID: reportPreviewAction.reportActionID, - waypoints: JSON.stringify(validWaypoints), - created, - category, - tag, - taxCode, - taxAmount, - billable, - transactionThreadReportID, - createdReportActionIDForThread, - payerEmail, - customUnitRateID, - }; - - API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData); - Navigation.dismissModal(activeReportID); - Report.notifyNewAction(activeReportID, userAccountID); -} \ No newline at end of file From cc1ee1fe7f44e2035f256536bc2569f51cf4885e Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 24 Jun 2024 22:34:02 +0700 Subject: [PATCH 016/420] feature: align tooltip by anchorAlignment on web --- src/components/MenuItem.tsx | 9 +-- src/components/PopoverMenu.tsx | 1 - .../BaseGenericTooltip/index.native.tsx | 3 - .../Tooltip/BaseGenericTooltip/index.tsx | 10 +++- .../Tooltip/BaseGenericTooltip/types.ts | 4 +- src/components/Tooltip/GenericTooltip.tsx | 7 ++- src/components/Tooltip/types.ts | 9 +-- .../FloatingActionButtonAndPopover.tsx | 7 ++- .../generators/TooltipStyleUtils/index.ts | 58 ++++++++++++------- .../generators/TooltipStyleUtils/types.ts | 2 + src/types/utils/AnchorAlignment.ts | 9 +++ 11 files changed, 78 insertions(+), 41 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index fa58b5cd5f5f..2667eca0833c 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -19,6 +19,7 @@ import variables from '@styles/variables'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; +import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; import type IconAsset from '@src/types/utils/IconAsset'; import Avatar from './Avatar'; import Badge from './Badge'; @@ -290,8 +291,8 @@ type MenuItemBaseProps = { /** Whether to show the tooltip */ shouldRenderTooltip?: boolean; - /** Whether to align the tooltip left */ - shouldForceRenderingTooltipLeft?: boolean; + /** Anchor alignment of the tooltip */ + tooltipAnchorAlignment?: TooltipAnchorAlignment; /** Additional styles for tooltip wrapper */ tooltipWrapperStyle?: StyleProp; @@ -383,7 +384,7 @@ function MenuItem( onBlur, avatarID, shouldRenderTooltip = false, - shouldForceRenderingTooltipLeft = false, + tooltipAnchorAlignment, tooltipWrapperStyle = {}, renderTooltipContent, }: MenuItemProps, @@ -490,7 +491,7 @@ function MenuItem( )} diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index e5a9b873dbc0..c4ce198cf5ae 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -25,7 +25,6 @@ function BaseGenericTooltip({ maxWidth = 0, renderTooltipContent, shouldForceRenderingBelow = false, - shouldForceRenderingLeft = false, wrapperStyle = {}, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning @@ -66,7 +65,6 @@ function BaseGenericTooltip({ manualShiftHorizontal: shiftHorizontal, manualShiftVertical: shiftVertical, shouldForceRenderingBelow, - shouldForceRenderingLeft, wrapperStyle, }), [ @@ -83,7 +81,6 @@ function BaseGenericTooltip({ shiftHorizontal, shiftVertical, shouldForceRenderingBelow, - shouldForceRenderingLeft, wrapperStyle, ], ); diff --git a/src/components/Tooltip/BaseGenericTooltip/index.tsx b/src/components/Tooltip/BaseGenericTooltip/index.tsx index bb02e17f07d9..e41e4eeea26f 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import {Animated, View} from 'react-native'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; +import CONST from '@src/CONST'; import textRef from '@src/types/utils/textRef'; import viewRef from '@src/types/utils/viewRef'; import type {BaseGenericTooltipProps} from './types'; @@ -27,7 +28,10 @@ function BaseGenericTooltip({ renderTooltipContent, shouldForceRenderingBelow = false, wrapperStyle = {}, - shouldForceRenderingLeft = false, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning // as a width of 0 will cause the content to be rendered of a width of 0, @@ -63,7 +67,7 @@ function BaseGenericTooltip({ manualShiftHorizontal: shiftHorizontal, manualShiftVertical: shiftVertical, shouldForceRenderingBelow, - shouldForceRenderingLeft, + anchorAlignment, wrapperStyle, }), [ @@ -80,7 +84,7 @@ function BaseGenericTooltip({ shiftHorizontal, shiftVertical, shouldForceRenderingBelow, - shouldForceRenderingLeft, + anchorAlignment, wrapperStyle, ], ); diff --git a/src/components/Tooltip/BaseGenericTooltip/types.ts b/src/components/Tooltip/BaseGenericTooltip/types.ts index 662905fc1ec6..35624e54d78f 100644 --- a/src/components/Tooltip/BaseGenericTooltip/types.ts +++ b/src/components/Tooltip/BaseGenericTooltip/types.ts @@ -1,5 +1,5 @@ import type {Animated} from 'react-native'; -import type TooltipProps from '@components/Tooltip/types'; +import type {SharedTooltipProps} from '@components/Tooltip/types'; type BaseGenericTooltipProps = { /** Window width */ @@ -27,7 +27,7 @@ type BaseGenericTooltipProps = { /** Any additional amount to manually adjust the vertical position of the tooltip. A positive value shifts the tooltip down, and a negative value shifts it up. */ shiftVertical?: number; -} & Pick; +} & Pick; // eslint-disable-next-line import/prefer-default-export export type {BaseGenericTooltipProps}; diff --git a/src/components/Tooltip/GenericTooltip.tsx b/src/components/Tooltip/GenericTooltip.tsx index 2b48fa91141f..4be9c1422380 100644 --- a/src/components/Tooltip/GenericTooltip.tsx +++ b/src/components/Tooltip/GenericTooltip.tsx @@ -29,7 +29,10 @@ function GenericTooltip({ shiftVertical = 0, shouldForceRenderingBelow = false, wrapperStyle = {}, - shouldForceRenderingLeft = false, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, shouldForceAnimate = false, }: GenericTooltipProps) { const {preferredLocale} = useLocalize(); @@ -164,7 +167,7 @@ function GenericTooltip({ key={[text, ...renderTooltipContentKey, preferredLocale].join('-')} shouldForceRenderingBelow={shouldForceRenderingBelow} wrapperStyle={wrapperStyle} - shouldForceRenderingLeft={shouldForceRenderingLeft} + anchorAlignment={anchorAlignment} /> )} diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts index cf2218abf5b3..aba8567b2125 100644 --- a/src/components/Tooltip/types.ts +++ b/src/components/Tooltip/types.ts @@ -1,6 +1,7 @@ import type {ReactNode} from 'react'; import type React from 'react'; import type {LayoutRectangle, StyleProp, ViewStyle} from 'react-native'; +import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; type SharedTooltipProps = { @@ -27,8 +28,8 @@ type SharedTooltipProps = { /** Unique key of renderTooltipContent to rerender the tooltip when one of the key changes */ renderTooltipContentKey?: string[]; - /** Whether to left align the tooltip relative to wrapped component */ - shouldForceRenderingLeft?: boolean; + /** The anchor alignment of the tooltip */ + anchorAlignment?: TooltipAnchorAlignment; /** Whether to display tooltip below the wrapped component */ shouldForceRenderingBelow?: boolean; @@ -64,7 +65,7 @@ type TooltipProps = ChildrenProps & shouldHandleScroll?: boolean; }; -type EducationalTooltipProps = ChildrenProps & TooltipProps; +type EducationalTooltipProps = ChildrenProps & SharedTooltipProps; type TooltipExtendedProps = (EducationalTooltipProps | TooltipProps) & { /** Whether the actual Tooltip should be rendered. If false, it's just going to return the children */ @@ -72,4 +73,4 @@ type TooltipExtendedProps = (EducationalTooltipProps | TooltipProps) & { }; export default TooltipProps; -export type {EducationalTooltipProps, GenericTooltipProps, TooltipExtendedProps}; +export type {EducationalTooltipProps, GenericTooltipProps, SharedTooltipProps, TooltipExtendedProps}; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index ff5cfa05b57b..3fe4fa6c2aa6 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -471,8 +471,11 @@ function FloatingActionButtonAndPopover( numberOfLinesDescription: 1, onSelected: () => interceptAnonymousUser(() => navigateToQuickAction()), shouldShowSubscriptRightAvatar: ReportUtils.isPolicyExpenseChat(quickActionReport), - shouldRenderTooltip: quickAction?.isFirstQuickAction, - shouldForceRenderingTooltipLeft: true, + shouldRenderTooltip: true, + tooltipAnchorAlignment: { + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + }, renderTooltipContent: renderQuickActionTooltip, tooltipWrapperStyle: styles.quickActionTooltipWrapper, }, diff --git a/src/styles/utils/generators/TooltipStyleUtils/index.ts b/src/styles/utils/generators/TooltipStyleUtils/index.ts index 846848ab25bd..8e98c5b23218 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/index.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/index.ts @@ -11,6 +11,7 @@ import spacing from '@styles/utils/spacing'; // eslint-disable-next-line no-restricted-imports import titleBarHeight from '@styles/utils/titleBarHeight'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import type {GetTooltipStylesStyleUtil} from './types'; /** This defines the proximity with the edge of the window in which tooltips should not be displayed. @@ -120,7 +121,7 @@ function isOverlappingAtTop(tooltip: View | HTMLDivElement, xOffset: number, yOf * @param [manualShiftVertical] - Any additional amount to manually shift the tooltip up or down. * A positive value shifts it down, and a negative value shifts it up. * @param [shouldForceRenderingBelow] - Should display tooltip below the wrapped component. - * @param [shouldForceRenderingLeft] - Align the tooltip left relative to the wrapped component instead of horizontally align center. + * @param [anchorAlignment] - Align tooltip anchor horizontally and vertically. * @param [wrapperStyle] - Any additional styles for the root wrapper. */ const createTooltipStyleUtils: StyleUtilGenerator = ({theme, styles}) => ({ @@ -138,7 +139,10 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( manualShiftHorizontal = 0, manualShiftVertical = 0, shouldForceRenderingBelow = false, - shouldForceRenderingLeft = false, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, wrapperStyle = {}, }) => { const customWrapperStyle = StyleSheet.flatten(wrapperStyle); @@ -171,7 +175,8 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( shouldShowBelow = shouldForceRenderingBelow || yOffset - tooltipHeight - POINTER_HEIGHT < GUTTER_WIDTH + titleBarHeight || - !!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight)); + !!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight)) || + anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP; // When the tooltip size is ready, we can start animating the scale. scale = currentSize; @@ -202,22 +207,6 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( : // We need to shift the tooltip up above the component. So shift the tooltip up (-) by... yOffset - (tooltipHeight + POINTER_HEIGHT) + manualShiftVertical; - // Next, we'll position it horizontally. - // we will use xOffset to position the tooltip relative to the Wrapped Component - // To shift the tooltip right, we'll give `left` a positive value. - // To shift the tooltip left, we'll give `left` a negative value. - // - // So we'll: - // 1a) Horizontally align left: No need for shifting. - // 1b) Horizontally align center: - // - Shift the tooltip right (+) to the center of the component, - // so the left edge lines up with the component center. - // - Shift it left (-) to by half the tooltip's width, - // so the tooltip's center lines up with the center of the wrapped component. - // 2) Add the horizontal shift (left or right) computed above to keep it out of the gutters. - // 3) Lastly, add the manual horizontal shift passed in as a parameter. - rootWrapperLeft = xOffset + (shouldForceRenderingLeft ? 0 : tooltipTargetWidth / 2 - tooltipWidth / 2) + horizontalShift + manualShiftHorizontal; - // By default, the pointer's top-left will align with the top-left of the tooltip wrapper. // // To align it vertically, we'll: @@ -228,6 +217,22 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( // so that the bottom of the pointer lines up with the top of the tooltip pointerWrapperTop = shouldShowBelow ? -POINTER_HEIGHT : tooltipHeight; + // Horizontal tooltip position: + // we will use xOffset to position the tooltip relative to the Wrapped Component + // To shift the tooltip right, we'll give `left` a positive value. + // To shift the tooltip left, we'll give `left` a negative value. + // + // So we'll: + // 1) Add the horizontal shift (left or right) computed above to keep it out of the gutters. + // 2) Add the manual horizontal shift passed in as a parameter. + // 3a) Horizontally align left: No need for shifting. + // 3b) Horizontally align center: + // - Shift the tooltip right (+) to the center of the component, + // so the left edge lines up with the component center. + // - Shift it left (-) to by half the tooltip's width, + // so the tooltip's center lines up with the center of the wrapped component. + + // Horizontal pointer position: // 1) Left align: Shift the pointer to the right (+) by half the pointer's width, // so the left edge of the pointer does not overlap with the wrapper's border radius. // 2) Center align: @@ -237,7 +242,20 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( // so the pointer's center lines up with the tooltipWidth's center. // - Remove the wrapper's horizontalShift to maintain the pointer // at the center of the hovered component. - pointerWrapperLeft = shouldForceRenderingLeft ? POINTER_WIDTH / 2 : horizontalShiftPointer + (tooltipWidth / 2 - POINTER_WIDTH / 2); + rootWrapperLeft = xOffset + horizontalShift + manualShiftHorizontal; + switch (anchorAlignment.horizontal) { + case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT: + pointerWrapperLeft = POINTER_WIDTH / 2; + break; + case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT: + pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth - POINTER_WIDTH * 1.5); + rootWrapperLeft += tooltipTargetWidth - tooltipWidth; + break; + case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER: + default: + pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth / 2 - POINTER_WIDTH / 2); + rootWrapperLeft += tooltipTargetWidth / 2 - tooltipWidth / 2; + } pointerAdditionalStyle = shouldShowBelow ? styles.flipUpsideDown : {}; } diff --git a/src/styles/utils/generators/TooltipStyleUtils/types.ts b/src/styles/utils/generators/TooltipStyleUtils/types.ts index 1907309e1bf5..7965ec151485 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/types.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/types.ts @@ -1,4 +1,5 @@ import type {Animated, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; type TooltipStyles = { animationStyle: ViewStyle; @@ -24,6 +25,7 @@ type TooltipParams = { shouldForceRenderingBelow?: boolean; shouldForceRenderingLeft?: boolean; wrapperStyle: StyleProp; + anchorAlignment?: TooltipAnchorAlignment; }; type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; diff --git a/src/types/utils/AnchorAlignment.ts b/src/types/utils/AnchorAlignment.ts index 899e3d9e277b..5ed043d36b18 100644 --- a/src/types/utils/AnchorAlignment.ts +++ b/src/types/utils/AnchorAlignment.ts @@ -9,4 +9,13 @@ type AnchorAlignment = { vertical: ValueOf; }; +type TooltipAnchorAlignment = { + /** The horizontal anchor alignment of the tooltip */ + horizontal: ValueOf; + + /** The vertical anchor alignment of the tooltip */ + vertical: Exclude, typeof CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.CENTER>; +}; + +export type {TooltipAnchorAlignment}; export default AnchorAlignment; From f6c3290613c3c6cf321b648ac9c0e8bdedacce47 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 24 Jun 2024 23:42:35 +0700 Subject: [PATCH 017/420] make anchorAlignment work on native --- src/components/MenuItem.tsx | 12 +- src/components/PopoverMenu.tsx | 3 + .../BaseGenericTooltip/index.native.tsx | 7 + .../FloatingActionButtonAndPopover.tsx | 4 +- .../computeHorizontalShift/index.native.ts | 5 + .../computeHorizontalShift/index.ts | 42 ++++ .../computeHorizontalShift/types.ts | 3 + .../TooltipStyleUtils/index.native.ts | 179 ------------------ .../generators/TooltipStyleUtils/index.ts | 122 ++++-------- .../isOverlappingAtTop/index.native.ts | 5 + .../isOverlappingAtTop/index.ts | 47 +++++ .../isOverlappingAtTop/types.ts | 5 + .../tooltipPlatformStyles/index.native.ts | 7 + .../tooltipPlatformStyles/index.ts | 7 + .../generators/TooltipStyleUtils/types.ts | 33 ---- 15 files changed, 182 insertions(+), 299 deletions(-) create mode 100644 src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts delete mode 100644 src/styles/utils/generators/TooltipStyleUtils/index.native.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.native.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/types.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.native.ts create mode 100644 src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.ts delete mode 100644 src/styles/utils/generators/TooltipStyleUtils/types.ts diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 2667eca0833c..b2834bac5deb 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -297,6 +297,12 @@ type MenuItemBaseProps = { /** Additional styles for tooltip wrapper */ tooltipWrapperStyle?: StyleProp; + /** Any additional amount to manually adjust the horizontal position of the tooltip */ + tooltipShiftHorizontal?: number; + + /** Any additional amount to manually adjust the vertical position of the tooltip */ + tooltipShiftVertical?: number; + /** Render custom content inside the tooltip. */ renderTooltipContent?: () => ReactNode; }; @@ -386,6 +392,8 @@ function MenuItem( shouldRenderTooltip = false, tooltipAnchorAlignment, tooltipWrapperStyle = {}, + tooltipShiftHorizontal = 0, + tooltipShiftVertical = 0, renderTooltipContent, }: MenuItemProps, ref: PressableRef, @@ -494,8 +502,8 @@ function MenuItem( anchorAlignment={tooltipAnchorAlignment} renderTooltipContent={renderTooltipContent} wrapperStyle={tooltipWrapperStyle} - shiftHorizontal={styles.popoverMenuItem.paddingHorizontal} - shiftVertical={styles.popoverMenuItem.paddingVertical / 2} + shiftHorizontal={tooltipShiftHorizontal} + shiftVertical={tooltipShiftVertical} > diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index b9cff08d020a..cb280d5b3aa5 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -244,6 +244,9 @@ function PopoverMenu({ success={item.success} containerStyle={item.containerStyle} shouldRenderTooltip={item.shouldRenderTooltip} + tooltipAnchorAlignment={item.tooltipAnchorAlignment} + tooltipShiftHorizontal={item.tooltipShiftHorizontal} + tooltipShiftVertical={item.tooltipShiftVertical} tooltipWrapperStyle={item.tooltipWrapperStyle} renderTooltipContent={item.renderTooltipContent} /> diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index c4ce198cf5ae..2dafbecf84d0 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -4,6 +4,7 @@ import {Animated, View} from 'react-native'; import type {Text as RNText, View as RNView} from 'react-native'; import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; +import CONST from '@src/CONST'; import type {BaseGenericTooltipProps} from './types'; // Props will change frequently. @@ -25,6 +26,10 @@ function BaseGenericTooltip({ maxWidth = 0, renderTooltipContent, shouldForceRenderingBelow = false, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, wrapperStyle = {}, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning @@ -65,6 +70,7 @@ function BaseGenericTooltip({ manualShiftHorizontal: shiftHorizontal, manualShiftVertical: shiftVertical, shouldForceRenderingBelow, + anchorAlignment, wrapperStyle, }), [ @@ -81,6 +87,7 @@ function BaseGenericTooltip({ shiftHorizontal, shiftVertical, shouldForceRenderingBelow, + anchorAlignment, wrapperStyle, ], ); diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 3fe4fa6c2aa6..3d5f712623af 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -471,11 +471,13 @@ function FloatingActionButtonAndPopover( numberOfLinesDescription: 1, onSelected: () => interceptAnonymousUser(() => navigateToQuickAction()), shouldShowSubscriptRightAvatar: ReportUtils.isPolicyExpenseChat(quickActionReport), - shouldRenderTooltip: true, + shouldRenderTooltip: quickAction.isFirstQuickAction, tooltipAnchorAlignment: { vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, }, + tooltipShiftHorizontal: styles.popoverMenuItem.paddingHorizontal, + tooltipShiftVertical: styles.popoverMenuItem.paddingVertical / 2, renderTooltipContent: renderQuickActionTooltip, tooltipWrapperStyle: styles.quickActionTooltipWrapper, }, diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts new file mode 100644 index 000000000000..61c10170a9b7 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.native.ts @@ -0,0 +1,5 @@ +import type ComputeHorizontalShift from './types'; + +const computeHorizontalShift: ComputeHorizontalShift = () => 0; + +export default computeHorizontalShift; diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts new file mode 100644 index 000000000000..339ddf306197 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts @@ -0,0 +1,42 @@ +import roundToNearestMultipleOfFour from '@libs/roundToNearestMultipleOfFour'; +import variables from '@styles/variables'; +import type ComputeHorizontalShift from './types'; + +/** This defines the proximity with the edge of the window in which tooltips should not be displayed. + * If a tooltip is too close to the edge of the screen, we'll shift it towards the center. */ +const GUTTER_WIDTH = variables.gutterWidth; + +/** + * Compute the amount the tooltip needs to be horizontally shifted in order to keep it from displaying in the gutters. + * + * @param windowWidth - The width of the window. + * @param xOffset - The distance between the left edge of the window + * and the left edge of the wrapped component. + * @param componentWidth - The width of the wrapped component. + * @param tooltipWidth - The width of the tooltip itself. + * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. + * A positive value shifts it to the right, + * and a negative value shifts it to the left. + */ +const computeHorizontalShift: ComputeHorizontalShift = (windowWidth, xOffset, componentWidth, tooltipWidth, manualShiftHorizontal) => { + // First find the left and right edges of the tooltip (by default, it is centered on the component). + const componentCenter = xOffset + componentWidth / 2 + manualShiftHorizontal; + const tooltipLeftEdge = componentCenter - tooltipWidth / 2; + const tooltipRightEdge = componentCenter + tooltipWidth / 2; + + if (tooltipLeftEdge < GUTTER_WIDTH) { + // Tooltip is in left gutter, shift right by a multiple of four. + return roundToNearestMultipleOfFour(GUTTER_WIDTH - tooltipLeftEdge); + } + + if (tooltipRightEdge > windowWidth - GUTTER_WIDTH) { + // Tooltip is in right gutter, shift left by a multiple of four. + return roundToNearestMultipleOfFour(windowWidth - GUTTER_WIDTH - tooltipRightEdge); + } + + // Tooltip is not in the gutter, so no need to shift it horizontally + return 0; +}; + +export {GUTTER_WIDTH}; +export default computeHorizontalShift; diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts new file mode 100644 index 000000000000..983155e811aa --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts @@ -0,0 +1,3 @@ +type ComputeHorizontalShift = (windowWidth: number, xOffset: number, componentWidth: number, tooltipWidth: number, manualShiftHorizontal: number) => number; + +export default ComputeHorizontalShift; diff --git a/src/styles/utils/generators/TooltipStyleUtils/index.native.ts b/src/styles/utils/generators/TooltipStyleUtils/index.native.ts deleted file mode 100644 index fa4264f45b1c..000000000000 --- a/src/styles/utils/generators/TooltipStyleUtils/index.native.ts +++ /dev/null @@ -1,179 +0,0 @@ -import {Animated, StyleSheet} from 'react-native'; -import FontUtils from '@styles/utils/FontUtils'; -// eslint-disable-next-line no-restricted-imports -import type StyleUtilGenerator from '@styles/utils/generators/types'; -// eslint-disable-next-line no-restricted-imports -import positioning from '@styles/utils/positioning'; -// eslint-disable-next-line no-restricted-imports -import spacing from '@styles/utils/spacing'; -import variables from '@styles/variables'; -import type {GetTooltipStylesStyleUtil} from './types'; - -/** The height of a tooltip pointer */ -const POINTER_HEIGHT = 4; - -/** The width of a tooltip pointer */ -const POINTER_WIDTH = 12; - -/** - * Generate styles for the tooltip component. - * - * @param tooltip - The reference to the tooltip's root element - * @param currentSize - The current size of the tooltip used in the scaling animation. - * @param windowWidth - The width of the window. - * @param xOffset - The distance between the left edge of the wrapped component - * and the left edge of the parent component. - * @param yOffset - The distance between the top edge of the wrapped component - * and the top edge of the parent component. - * @param tooltipTargetWidth - The width of the tooltip's target - * @param tooltipTargetHeight - The height of the tooltip's target - * @param maxWidth - The tooltip's max width. - * @param tooltipContentWidth - The tooltip's inner content measured width. - * @param tooltipWrapperHeight - The tooltip's wrapper measured height. - * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. - * A positive value shifts it to the right, - * and a negative value shifts it to the left. - * @param [manualShiftVertical] - Any additional amount to manually shift the tooltip up or down. - * A positive value shifts it down, and a negative value shifts it up. - * @param [shouldForceRenderingBelow] - Should display tooltip below the wrapped component. - * @param [shouldForceRenderingLeft] - Align the tooltip left relative to the wrapped component instead of horizontally align center. - * @param [wrapperStyle] - Any additional styles for the root wrapper. - */ -const createTooltipStyleUtils: StyleUtilGenerator = ({theme, styles}) => ({ - getTooltipStyles: ({ - currentSize, - xOffset, - yOffset, - tooltipTargetWidth, - maxWidth, - tooltipContentWidth, - tooltipWrapperHeight, - manualShiftHorizontal = 0, - manualShiftVertical = 0, - shouldForceRenderingLeft = false, - wrapperStyle = {}, - }) => { - const customWrapperStyle = StyleSheet.flatten(wrapperStyle); - const tooltipVerticalPadding = spacing.pv1; - - // We calculate tooltip width based on the tooltip's content width - // so the tooltip wrapper is just big enough to fit content and prevent white space. - // NOTE: Add 1 to the tooltipWidth to prevent truncated text in Safari - const tooltipWidth = tooltipContentWidth && tooltipContentWidth + spacing.ph2.paddingHorizontal * 2 + 1; - const tooltipHeight = tooltipWrapperHeight; - - const isTooltipSizeReady = tooltipWidth !== undefined && tooltipHeight !== undefined; - - // Set the scale to 1 to be able to measure the tooltip size correctly when it's not ready yet. - let scale = new Animated.Value(1); - let rootWrapperTop = 0; - let rootWrapperLeft = 0; - let pointerWrapperTop = 0; - let pointerWrapperLeft = 0; - let opacity = 0; - - if (isTooltipSizeReady) { - // When the tooltip size is ready, we can start animating the scale. - scale = currentSize; - - // Because it uses absolute positioning, the top-left corner of the tooltip is aligned - // with the top-left corner of the wrapped component by default. - // we will use yOffset to position the tooltip relative to the Wrapped Component - // So we need to shift the tooltip vertically and horizontally to position it correctly. - // - // First, we'll position it vertically. - // To shift the tooltip down, we'll give `top` a positive value. - // To shift the tooltip up, we'll give `top` a negative value. - rootWrapperTop = yOffset - (tooltipHeight + POINTER_HEIGHT) + manualShiftVertical; - - // Next, we'll position it horizontally. - // we will use xOffset to position the tooltip relative to the Wrapped Component - // To shift the tooltip right, we'll give `left` a positive value. - // To shift the tooltip left, we'll give `left` a negative value. - // - // So we'll: - // 1a) Horizontally align left: No need for shifting. - // 1b) Horizontally align center: - // - Shift the tooltip right (+) to the center of the component, - // so the left edge lines up with the component center. - // - Shift it left (-) to by half the tooltip's width, - // so the tooltip's center lines up with the center of the wrapped component. - // 2) Add the manual horizontal shift passed in as a parameter. - rootWrapperLeft = xOffset + (shouldForceRenderingLeft ? 0 : tooltipTargetWidth / 2 - tooltipWidth / 2) + manualShiftHorizontal; - - // By default, the pointer's top-left will align with the top-left of the tooltip wrapper. - // - // To align it vertically, the pointer up (-) by the pointer's height - // so that the bottom of the pointer lines up with the top of the tooltip - pointerWrapperTop = tooltipHeight; - - // To align it horizontally, we'll: - // 1) Left align: Shift the pointer to the right (+) by half the pointer's width, - // so the left edge of the pointer does not overlap with the wrapper's border radius. - // 2) Center align: - // - Shift the pointer to the right (+) by the half the tooltipWidth's width, - // so the left edge of the pointer lines up with the tooltipWidth's center. - // - To the left (-) by half the pointer's width, - // so the pointer's center lines up with the tooltipWidth's center. - pointerWrapperLeft = shouldForceRenderingLeft ? POINTER_WIDTH / 2 : tooltipWidth / 2 - POINTER_WIDTH / 2; - - // React Native's measure() is asynchronous, we temporarily hide the tooltip until its bound is calculated - opacity = 100; - } - - return { - animationStyle: { - // remember Transform causes a new Local cordinate system - // https://drafts.csswg.org/css-transforms-1/#transform-rendering - // so Position fixed children will be relative to this new Local cordinate system - transform: [{scale}], - }, - rootWrapperStyle: { - ...positioning.pAbsolute, - backgroundColor: theme.heading, - borderRadius: variables.componentBorderRadiusSmall, - ...tooltipVerticalPadding, - ...spacing.ph2, - zIndex: variables.tooltipzIndex, - width: tooltipWidth, - maxWidth, - top: rootWrapperTop, - left: rootWrapperLeft, - opacity, - ...customWrapperStyle, - - // We are adding this to prevent the tooltip text from being selected and copied on CTRL + A. - ...styles.userSelectNone, - ...styles.pointerEventsNone, - }, - textStyle: { - color: theme.textReversed, - fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, - fontSize: variables.fontSizeSmall, - overflow: 'hidden', - lineHeight: variables.lineHeightSmall, - textAlign: 'center', - }, - pointerWrapperStyle: { - ...positioning.pAbsolute, - top: pointerWrapperTop, - left: pointerWrapperLeft, - opacity, - }, - pointerStyle: { - width: 0, - height: 0, - backgroundColor: theme.transparent, - borderStyle: 'solid', - borderLeftWidth: POINTER_WIDTH / 2, - borderRightWidth: POINTER_WIDTH / 2, - borderTopWidth: POINTER_HEIGHT, - borderLeftColor: theme.transparent, - borderRightColor: theme.transparent, - borderTopColor: customWrapperStyle.backgroundColor ?? theme.heading, - }, - }; - }, -}); - -export default createTooltipStyleUtils; diff --git a/src/styles/utils/generators/TooltipStyleUtils/index.ts b/src/styles/utils/generators/TooltipStyleUtils/index.ts index 8e98c5b23218..588b054b7157 100644 --- a/src/styles/utils/generators/TooltipStyleUtils/index.ts +++ b/src/styles/utils/generators/TooltipStyleUtils/index.ts @@ -1,22 +1,18 @@ -import type {View} from 'react-native'; +import type {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import {Animated, StyleSheet} from 'react-native'; -import roundToNearestMultipleOfFour from '@libs/roundToNearestMultipleOfFour'; import FontUtils from '@styles/utils/FontUtils'; // eslint-disable-next-line no-restricted-imports import type StyleUtilGenerator from '@styles/utils/generators/types'; // eslint-disable-next-line no-restricted-imports -import positioning from '@styles/utils/positioning'; -// eslint-disable-next-line no-restricted-imports import spacing from '@styles/utils/spacing'; // eslint-disable-next-line no-restricted-imports import titleBarHeight from '@styles/utils/titleBarHeight'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {GetTooltipStylesStyleUtil} from './types'; - -/** This defines the proximity with the edge of the window in which tooltips should not be displayed. - * If a tooltip is too close to the edge of the screen, we'll shift it towards the center. */ -const GUTTER_WIDTH = variables.gutterWidth; +import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; +import computeHorizontalShift, {GUTTER_WIDTH} from './computeHorizontalShift'; +import isOverlappingAtTop from './isOverlappingAtTop'; +import tooltipPlatformStyle from './tooltipPlatformStyles'; /** The height of a tooltip pointer */ const POINTER_HEIGHT = 4; @@ -24,81 +20,33 @@ const POINTER_HEIGHT = 4; /** The width of a tooltip pointer */ const POINTER_WIDTH = 12; -/** - * Compute the amount the tooltip needs to be horizontally shifted in order to keep it from displaying in the gutters. - * - * @param windowWidth - The width of the window. - * @param xOffset - The distance between the left edge of the window - * and the left edge of the wrapped component. - * @param componentWidth - The width of the wrapped component. - * @param tooltipWidth - The width of the tooltip itself. - * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right. - * A positive value shifts it to the right, - * and a negative value shifts it to the left. - */ -function computeHorizontalShift(windowWidth: number, xOffset: number, componentWidth: number, tooltipWidth: number, manualShiftHorizontal: number): number { - // First find the left and right edges of the tooltip (by default, it is centered on the component). - const componentCenter = xOffset + componentWidth / 2 + manualShiftHorizontal; - const tooltipLeftEdge = componentCenter - tooltipWidth / 2; - const tooltipRightEdge = componentCenter + tooltipWidth / 2; - - if (tooltipLeftEdge < GUTTER_WIDTH) { - // Tooltip is in left gutter, shift right by a multiple of four. - return roundToNearestMultipleOfFour(GUTTER_WIDTH - tooltipLeftEdge); - } +type TooltipStyles = { + animationStyle: ViewStyle; + rootWrapperStyle: ViewStyle; + textStyle: TextStyle; + pointerWrapperStyle: ViewStyle; + pointerStyle: ViewStyle; +}; - if (tooltipRightEdge > windowWidth - GUTTER_WIDTH) { - // Tooltip is in right gutter, shift left by a multiple of four. - return roundToNearestMultipleOfFour(windowWidth - GUTTER_WIDTH - tooltipRightEdge); - } +type TooltipParams = { + tooltip: View | HTMLDivElement | null; + currentSize: Animated.Value; + windowWidth: number; + xOffset: number; + yOffset: number; + tooltipTargetWidth: number; + tooltipTargetHeight: number; + maxWidth: number; + tooltipContentWidth?: number; + tooltipWrapperHeight?: number; + manualShiftHorizontal?: number; + manualShiftVertical?: number; + shouldForceRenderingBelow?: boolean; + wrapperStyle: StyleProp; + anchorAlignment?: TooltipAnchorAlignment; +}; - // Tooltip is not in the gutter, so no need to shift it horizontally - return 0; -} - -/** - * Determines if there is an overlapping element at the top of a given coordinate. - * (targetCenterX, y) - * | - * v - * _ _ _ _ _ - * | | - * | | - * | | - * | | - * |_ _ _ _ _| - * - * @param tooltip - The reference to the tooltip's root element - * @param xOffset - The distance between the left edge of the window - * and the left edge of the wrapped component. - * @param yOffset - The distance between the top edge of the window - * and the top edge of the wrapped component. - * @param tooltipTargetWidth - The width of the tooltip's target - * @param tooltipTargetHeight - The height of the tooltip's target - */ -function isOverlappingAtTop(tooltip: View | HTMLDivElement, xOffset: number, yOffset: number, tooltipTargetWidth: number, tooltipTargetHeight: number) { - if (typeof document.elementFromPoint !== 'function') { - return false; - } - - // Use the x center position of the target to prevent wrong element returned by elementFromPoint - // in case the target has a border radius or is a multiline text. - const targetCenterX = xOffset + tooltipTargetWidth / 2; - const elementAtTargetCenterX = document.elementFromPoint(targetCenterX, yOffset); - - // Ensure it's not the already rendered element of this very tooltip, so the tooltip doesn't try to "avoid" itself - if (!elementAtTargetCenterX || ('contains' in tooltip && tooltip.contains(elementAtTargetCenterX))) { - return false; - } - - const rectAtTargetCenterX = elementAtTargetCenterX.getBoundingClientRect(); - - // Ensure it's not overlapping with another element by checking if the yOffset is greater than the top of the element - // and less than the bottom of the element. Also ensure the tooltip target is not completely inside the elementAtTargetCenterX by vertical direction - const isOverlappingAtTargetCenterX = yOffset > rectAtTargetCenterX.top && yOffset < rectAtTargetCenterX.bottom && yOffset + tooltipTargetHeight > rectAtTargetCenterX.bottom; - - return isOverlappingAtTargetCenterX; -} +type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; /** * Generate styles for the tooltip component. @@ -166,6 +114,7 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( let pointerWrapperTop = 0; let pointerWrapperLeft = 0; let pointerAdditionalStyle = {}; + let opacity = 0; if (isTooltipSizeReady) { // Determine if the tooltip should display below the wrapped component. @@ -258,6 +207,9 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( } pointerAdditionalStyle = shouldShowBelow ? styles.flipUpsideDown : {}; + + // React Native's measure() is asynchronous, we temporarily hide the tooltip until its bound is calculated + opacity = 100; } return { @@ -268,7 +220,7 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( transform: [{scale}], }, rootWrapperStyle: { - ...positioning.pFixed, + ...tooltipPlatformStyle, backgroundColor: theme.heading, borderRadius: variables.componentBorderRadiusSmall, ...tooltipVerticalPadding, @@ -278,6 +230,7 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( maxWidth, top: rootWrapperTop, left: rootWrapperLeft, + opacity, ...customWrapperStyle, // We are adding this to prevent the tooltip text from being selected and copied on CTRL + A. @@ -293,9 +246,10 @@ const createTooltipStyleUtils: StyleUtilGenerator = ( textAlign: 'center', }, pointerWrapperStyle: { - ...positioning.pFixed, + ...tooltipPlatformStyle, top: pointerWrapperTop, left: pointerWrapperLeft, + opacity, }, pointerStyle: { width: 0, diff --git a/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.native.ts b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.native.ts new file mode 100644 index 000000000000..fa80f4471870 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.native.ts @@ -0,0 +1,5 @@ +import type IsOverlappingAtTop from './types'; + +const isOverlappingAtTop: IsOverlappingAtTop = () => false; + +export default isOverlappingAtTop; diff --git a/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.ts b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.ts new file mode 100644 index 000000000000..081d1a0a693e --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.ts @@ -0,0 +1,47 @@ +import type IsOverlappingAtTop from './types'; + +/** + * Determines if there is an overlapping element at the top of a given coordinate. + * (targetCenterX, y) + * | + * v + * _ _ _ _ _ + * | | + * | | + * | | + * | | + * |_ _ _ _ _| + * + * @param tooltip - The reference to the tooltip's root element + * @param xOffset - The distance between the left edge of the window + * and the left edge of the wrapped component. + * @param yOffset - The distance between the top edge of the window + * and the top edge of the wrapped component. + * @param tooltipTargetWidth - The width of the tooltip's target + * @param tooltipTargetHeight - The height of the tooltip's target + */ +const isOverlappingAtTop: IsOverlappingAtTop = (tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight) => { + if (typeof document.elementFromPoint !== 'function') { + return false; + } + + // Use the x center position of the target to prevent wrong element returned by elementFromPoint + // in case the target has a border radius or is a multiline text. + const targetCenterX = xOffset + tooltipTargetWidth / 2; + const elementAtTargetCenterX = document.elementFromPoint(targetCenterX, yOffset); + + // Ensure it's not the already rendered element of this very tooltip, so the tooltip doesn't try to "avoid" itself + if (!elementAtTargetCenterX || ('contains' in tooltip && tooltip.contains(elementAtTargetCenterX))) { + return false; + } + + const rectAtTargetCenterX = elementAtTargetCenterX.getBoundingClientRect(); + + // Ensure it's not overlapping with another element by checking if the yOffset is greater than the top of the element + // and less than the bottom of the element. Also ensure the tooltip target is not completely inside the elementAtTargetCenterX by vertical direction + const isOverlappingAtTargetCenterX = yOffset > rectAtTargetCenterX.top && yOffset < rectAtTargetCenterX.bottom && yOffset + tooltipTargetHeight > rectAtTargetCenterX.bottom; + + return isOverlappingAtTargetCenterX; +}; + +export default isOverlappingAtTop; diff --git a/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/types.ts b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/types.ts new file mode 100644 index 000000000000..bdd8ff346a86 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/types.ts @@ -0,0 +1,5 @@ +import type {View} from 'react-native'; + +type IsOverlappingAtTop = (tooltip: View | HTMLDivElement, xOffset: number, yOffset: number, tooltipTargetWidth: number, tooltipTargetHeight: number) => boolean; + +export default IsOverlappingAtTop; diff --git a/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.native.ts b/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.native.ts new file mode 100644 index 000000000000..17cc7200b20d --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.native.ts @@ -0,0 +1,7 @@ +import type {ViewStyle} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import positioning from '@styles/utils/positioning'; + +const tooltipPlatformStyle: ViewStyle = positioning.pAbsolute; + +export default tooltipPlatformStyle; diff --git a/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.ts b/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.ts new file mode 100644 index 000000000000..fd49d03b9413 --- /dev/null +++ b/src/styles/utils/generators/TooltipStyleUtils/tooltipPlatformStyles/index.ts @@ -0,0 +1,7 @@ +import type {ViewStyle} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import positioning from '@styles/utils/positioning'; + +const tooltipPlatformStyle: ViewStyle = positioning.pFixed; + +export default tooltipPlatformStyle; diff --git a/src/styles/utils/generators/TooltipStyleUtils/types.ts b/src/styles/utils/generators/TooltipStyleUtils/types.ts deleted file mode 100644 index 7965ec151485..000000000000 --- a/src/styles/utils/generators/TooltipStyleUtils/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type {Animated, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; -import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; - -type TooltipStyles = { - animationStyle: ViewStyle; - rootWrapperStyle: ViewStyle; - textStyle: TextStyle; - pointerWrapperStyle: ViewStyle; - pointerStyle: ViewStyle; -}; - -type TooltipParams = { - tooltip: View | HTMLDivElement | null; - currentSize: Animated.Value; - windowWidth: number; - xOffset: number; - yOffset: number; - tooltipTargetWidth: number; - tooltipTargetHeight: number; - maxWidth: number; - tooltipContentWidth?: number; - tooltipWrapperHeight?: number; - manualShiftHorizontal?: number; - manualShiftVertical?: number; - shouldForceRenderingBelow?: boolean; - shouldForceRenderingLeft?: boolean; - wrapperStyle: StyleProp; - anchorAlignment?: TooltipAnchorAlignment; -}; - -type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; - -export type {TooltipStyles, TooltipParams, GetTooltipStylesStyleUtil}; From cf99a4cb0a664878cccf1e3efe9447c0664a78e9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 26 Jun 2024 16:01:07 +0700 Subject: [PATCH 018/420] fix auto hide controls after playing video 2s --- .../VideoPlayer/BaseVideoPlayer.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index f99d1e36635c..5ab176f20c48 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -1,8 +1,9 @@ /* eslint-disable no-underscore-dangle */ import type {AVPlaybackStatus, VideoFullscreenUpdateEvent} from 'expo-av'; import {ResizeMode, Video, VideoFullscreenUpdate} from 'expo-av'; +import {debounce} from 'lodash'; import type {MutableRefObject} from 'react'; -import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; import {View} from 'react-native'; import {runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; @@ -17,6 +18,7 @@ import {useVideoPopoverMenuContext} from '@components/VideoPlayerContexts/VideoP import {useVolumeContext} from '@components/VideoPlayerContexts/VolumeContext'; import VideoPopoverMenu from '@components/VideoPopoverMenu'; import useNetwork from '@hooks/useNetwork'; +import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -69,6 +71,7 @@ function BaseVideoPlayer({ const [duration, setDuration] = useState(videoDuration * 1000); const [position, setPosition] = useState(0); const [isPlaying, setIsPlaying] = useState(false); + const prevIsPlaying = usePrevious(isPlaying); const [isLoading, setIsLoading] = useState(true); const [isBuffering, setIsBuffering] = useState(true); // we add "#t=0.001" at the end of the URL to skip first milisecond of the video and always be able to show proper video preview when video is paused at the beginning @@ -112,11 +115,21 @@ function BaseVideoPlayer({ } else { setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); controlsOpacity.value = 1; - setTimeout(() => { - controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); - }, 2000); } - }, [canToggleControlOnTap, controlStatusState, controlsOpacity]); + }, [canToggleControlOnTap, controlStatusState]); + + const hideControlWithDelay = useCallback(() => { + setTimeout(() => { + controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); + }, 2000); + }, [controlsOpacity]); + const debouncedHideControlWithDelay = useMemo(() => debounce(hideControlWithDelay, 2000, {leading: true}), [hideControlWithDelay]); + + useEffect(() => { + if (!prevIsPlaying && isPlaying && controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { + debouncedHideControlWithDelay(); + } + }, [isPlaying, prevIsPlaying, debouncedHideControlWithDelay, controlStatusState]); const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { videoPopoverMenuPlayerRef.current = videoPlayerRef.current; From 964a0d2644012881acfde0b545f114703aeb7064 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 26 Jun 2024 16:02:49 +0700 Subject: [PATCH 019/420] fix missing condition --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 5ab176f20c48..84267c3b1ab4 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -126,10 +126,10 @@ function BaseVideoPlayer({ const debouncedHideControlWithDelay = useMemo(() => debounce(hideControlWithDelay, 2000, {leading: true}), [hideControlWithDelay]); useEffect(() => { - if (!prevIsPlaying && isPlaying && controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { + if (canToggleControlOnTap && !prevIsPlaying && isPlaying && controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { debouncedHideControlWithDelay(); } - }, [isPlaying, prevIsPlaying, debouncedHideControlWithDelay, controlStatusState]); + }, [isPlaying, prevIsPlaying, debouncedHideControlWithDelay, controlStatusState, canToggleControlOnTap]); const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { videoPopoverMenuPlayerRef.current = videoPlayerRef.current; From 5daecf088096729488d9e1279876451379a3647f Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 26 Jun 2024 16:26:14 +0700 Subject: [PATCH 020/420] fix lint --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 84267c3b1ab4..16fdde4e1513 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -106,17 +106,16 @@ function BaseVideoPlayer({ } }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumber]); - const toggleControlStatusState = useCallback(() => { + const showControl = useCallback(() => { if (!canToggleControlOnTap) { return; } if (controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { - setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE); - } else { - setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); - controlsOpacity.value = 1; + return; } - }, [canToggleControlOnTap, controlStatusState]); + setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); + controlsOpacity.value = 1; + }, [canToggleControlOnTap, controlStatusState, controlsOpacity]); const hideControlWithDelay = useCallback(() => { setTimeout(() => { @@ -346,7 +345,7 @@ function BaseVideoPlayer({ return; } togglePlayCurrentVideo(); - toggleControlStatusState(); + showControl(); }} style={[styles.flex1, styles.noSelect]} > From cfe687ae95c80ad815e14dc65fc8c00b29339b67 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 26 Jun 2024 16:28:00 +0700 Subject: [PATCH 021/420] fix lint --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 16fdde4e1513..3a7582359e44 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -125,9 +125,10 @@ function BaseVideoPlayer({ const debouncedHideControlWithDelay = useMemo(() => debounce(hideControlWithDelay, 2000, {leading: true}), [hideControlWithDelay]); useEffect(() => { - if (canToggleControlOnTap && !prevIsPlaying && isPlaying && controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { - debouncedHideControlWithDelay(); + if (!canToggleControlOnTap || prevIsPlaying || !isPlaying || controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { + return; } + debouncedHideControlWithDelay(); }, [isPlaying, prevIsPlaying, debouncedHideControlWithDelay, controlStatusState, canToggleControlOnTap]); const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { From f1f9bead59220a0ca4fed0dd5a6de973ee87b509 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 27 Jun 2024 15:38:24 +0700 Subject: [PATCH 022/420] fix do not play video on tap --- .../VideoPlayer/BaseVideoPlayer.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 3a7582359e44..10c707489a01 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -18,7 +18,7 @@ import {useVideoPopoverMenuContext} from '@components/VideoPlayerContexts/VideoP import {useVolumeContext} from '@components/VideoPlayerContexts/VolumeContext'; import VideoPopoverMenu from '@components/VideoPopoverMenu'; import useNetwork from '@hooks/useNetwork'; -import usePrevious from '@hooks/usePrevious'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -71,7 +71,6 @@ function BaseVideoPlayer({ const [duration, setDuration] = useState(videoDuration * 1000); const [position, setPosition] = useState(0); const [isPlaying, setIsPlaying] = useState(false); - const prevIsPlaying = usePrevious(isPlaying); const [isLoading, setIsLoading] = useState(true); const [isBuffering, setIsBuffering] = useState(true); // we add "#t=0.001" at the end of the URL to skip first milisecond of the video and always be able to show proper video preview when video is paused at the beginning @@ -94,6 +93,7 @@ function BaseVideoPlayer({ const videoStateRef = useRef(null); const {updateVolume} = useVolumeContext(); const {videoPopoverMenuPlayerRef, setCurrentPlaybackSpeed} = useVideoPopoverMenuContext(); + const {isSmallScreenWidth} = useResponsiveLayout(); const togglePlayCurrentVideo = useCallback(() => { videoResumeTryNumber.current = 0; @@ -117,19 +117,23 @@ function BaseVideoPlayer({ controlsOpacity.value = 1; }, [canToggleControlOnTap, controlStatusState, controlsOpacity]); - const hideControlWithDelay = useCallback(() => { - setTimeout(() => { - controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); - }, 2000); - }, [controlsOpacity]); - const debouncedHideControlWithDelay = useMemo(() => debounce(hideControlWithDelay, 2000, {leading: true}), [hideControlWithDelay]); + const hideControl = useCallback( + () => (controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE))), + [controlsOpacity], + ); + const debouncedHideControl = useMemo(() => debounce(hideControl, 2000), [hideControl]); useEffect(() => { - if (!canToggleControlOnTap || prevIsPlaying || !isPlaying || controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { + if (!canToggleControlOnTap || controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { + return; + } + if (!isPlaying) { + debouncedHideControl.cancel(); return; } - debouncedHideControlWithDelay(); - }, [isPlaying, prevIsPlaying, debouncedHideControlWithDelay, controlStatusState, canToggleControlOnTap]); + + debouncedHideControl(); + }, [isPlaying, debouncedHideControl, controlStatusState, canToggleControlOnTap]); const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { videoPopoverMenuPlayerRef.current = videoPlayerRef.current; @@ -345,7 +349,9 @@ function BaseVideoPlayer({ if (isFullScreenRef.current) { return; } - togglePlayCurrentVideo(); + if (!isSmallScreenWidth) { + togglePlayCurrentVideo(); + } showControl(); }} style={[styles.flex1, styles.noSelect]} From 88915134fba1bcd0552b041eb33cfa3ebbdd4926 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 28 Jun 2024 16:14:45 -0700 Subject: [PATCH 023/420] Fix creating workspace distance split and other cleanups --- src/libs/actions/IOU.ts | 3 ++- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 6 ++++-- src/pages/iou/request/step/IOURequestStepDistance.tsx | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 222b24771c2f..1b46c51da568 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4880,6 +4880,7 @@ function createDistanceRequest( currentUserLogin = '', currentUserAccountID = -1, splitShares: SplitShares = {}, + iouType: ValueOf = CONST.IOU.TYPE.SUBMIT, ) { // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); @@ -4893,7 +4894,7 @@ function createDistanceRequest( let parameters: CreateDistanceRequestParams; let onyxData: OnyxData; - if (participants.length > 1) { + if (iouType === CONST.IOU.TYPE.SPLIT) { const {splitData, splits, onyxData: splitOnyxData} = createSplitsAndOnyxData( participants, currentUserLogin ?? '', diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 2a8998a17bf3..d09894bd8bb0 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -296,7 +296,6 @@ function IOURequestStepConfirmation({ if (!transaction) { return; } - const customUnitRateID = TransactionUtils.getRateID(transaction) ?? ''; IOU.createDistanceRequest( report, selectedParticipants, @@ -318,9 +317,10 @@ function IOURequestStepConfirmation({ currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, transaction.splitShares, + iouType, ); }, - [policy, policyCategories, policyTags, report, transaction, transactionTaxCode, transactionTaxAmount, customUnitRateID, currentUserPersonalDetails], + [policy, policyCategories, policyTags, report, transaction, transactionTaxCode, transactionTaxAmount, customUnitRateID, currentUserPersonalDetails, iouType], ); const createTransaction = useCallback( @@ -514,6 +514,8 @@ function IOURequestStepConfirmation({ policyCategories, transactionTaxAmount, transactionTaxCode, + action, + requestType, ], ); diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 259390b97c28..7b1f5f7ef66f 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -295,6 +295,10 @@ function IOURequestStepDistance({ undefined, undefined, DistanceRequestUtils.getCustomUnitRateID(report.reportID), + currentUserPersonalDetails.login ?? '', + currentUserPersonalDetails.accountID, + transaction?.splitShares, + iouType, ); return; } From 29a01f343e7a791751f061ae49701dc1928e1f26 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Mon, 1 Jul 2024 19:16:05 -0700 Subject: [PATCH 024/420] Build optimistic receipt for distance split, and move it inside optimistic transaction function --- src/libs/TransactionUtils.ts | 4 ++-- src/libs/actions/IOU.ts | 19 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index b28e5b782965..23ff5d78008b 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -147,8 +147,8 @@ function buildOptimisticTransaction( merchant: merchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, created: created || DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - receipt, - filename, + receipt: receipt?.source ? {source: receipt.source, state: receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY} : {}, + filename: receipt?.source ?? receipt?.name ?? filename, category, tag, taxCode, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 1b46c51da568..dee07ba59733 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1955,14 +1955,8 @@ function getMoneyRequestInformation( } else { iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency); } - // STEP 3: Build optimistic receipt and transaction - const receiptObject: Receipt = {}; - let filename; - if (receipt?.source) { - receiptObject.source = receipt.source; - receiptObject.state = receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY; - filename = receipt.name; - } + + // STEP 3: Build an optimistic transaction with the receipt const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${existingTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( @@ -1974,8 +1968,8 @@ function getMoneyRequestInformation( '', '', merchant, - receiptObject, - filename, + receipt, + '', existingTransactionID, category, tag, @@ -3758,6 +3752,9 @@ function createSplitsAndOnyxData( const {splitChatReport, existingSplitChatReport} = getOrCreateOptimisticSplitChatReport(existingSplitChatReportID, participants, participantAccountIDs, currentUserAccountID); const isOwnPolicyExpenseChat = !!splitChatReport.isOwnPolicyExpenseChat; + // Pass an open receipt so the distance expense will show a map with the route optimistically + const receipt: Receipt|undefined = (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) ? {source: ReceiptGeneric as ReceiptSource, state: CONST.IOU.RECEIPT_STATE.OPEN} : undefined; + const splitTransaction = TransactionUtils.buildOptimisticTransaction( amount, currency, @@ -3767,7 +3764,7 @@ function createSplitsAndOnyxData( '', '', merchant || Localize.translateLocal('iou.expense'), - undefined, + receipt, undefined, undefined, category, From dc2d0dad89c161a0a7068790a5097b5c42298364 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 2 Jul 2024 08:37:21 -0700 Subject: [PATCH 025/420] Use existing split chat reportID for distance if present --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index dee07ba59733..93accc320928 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4904,7 +4904,7 @@ function createDistanceRequest( category ?? '', tag ?? '', splitShares, - '', + report?.reportID ?? '', billable, CONST.IOU.REQUEST_TYPE.DISTANCE, ); From a981882edd5cdafbcad5b85a69723ec0de862719 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 2 Jul 2024 16:01:14 -0700 Subject: [PATCH 026/420] Prevent using an existingSplitChatReport with undefined id --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 93accc320928..ef6d4d314888 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3676,7 +3676,7 @@ function trackExpense( function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string, participants: Participant[], participantAccountIDs: number[], currentUserAccountID: number) { // The existing chat report could be passed as reportID or exist on the sole "participant" (in this case a report option) - const existingChatReportID = existingSplitChatReportID || participants[0].reportID; + const existingChatReportID = existingSplitChatReportID ?? participants[0].reportID ?? ''; // Check if the report is available locally if we do have one let existingSplitChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`]; From 41dec6aa4c202ef8d8e91ebc04528168c1afaca2 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 2 Jul 2024 18:37:25 -0700 Subject: [PATCH 027/420] Set optimistic data marking split as a distance transaction --- src/libs/actions/IOU.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ef6d4d314888..35b94d067c3c 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3755,7 +3755,7 @@ function createSplitsAndOnyxData( // Pass an open receipt so the distance expense will show a map with the route optimistically const receipt: Receipt|undefined = (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) ? {source: ReceiptGeneric as ReceiptSource, state: CONST.IOU.RECEIPT_STATE.OPEN} : undefined; - const splitTransaction = TransactionUtils.buildOptimisticTransaction( + let splitTransaction = TransactionUtils.buildOptimisticTransaction( amount, currency, CONST.REPORT.SPLIT_REPORTID, @@ -3774,6 +3774,13 @@ function createSplitsAndOnyxData( billable, ); + // Important data is set on the draft distance transaction, such as the iouRequestType marking it as a distance request, so merge it into the optimistic split transaction + const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; + if (isDistanceRequest) { + splitTransaction = fastMerge(existingTransaction, splitTransaction, false); + } + // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const splitCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); const splitIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( From 1b10e4026f4b213737cedc0dc9aabce9d7055cfd Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 2 Jul 2024 18:46:22 -0700 Subject: [PATCH 028/420] Set waypoints pending so optimistic route shows --- src/libs/actions/IOU.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 35b94d067c3c..9ca7167e9544 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3755,6 +3755,8 @@ function createSplitsAndOnyxData( // Pass an open receipt so the distance expense will show a map with the route optimistically const receipt: Receipt|undefined = (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) ? {source: ReceiptGeneric as ReceiptSource, state: CONST.IOU.RECEIPT_STATE.OPEN} : undefined; + const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; let splitTransaction = TransactionUtils.buildOptimisticTransaction( amount, currency, @@ -3772,11 +3774,10 @@ function createSplitsAndOnyxData( taxCode, taxAmount, billable, + isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, ); // Important data is set on the draft distance transaction, such as the iouRequestType marking it as a distance request, so merge it into the optimistic split transaction - const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; - const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; if (isDistanceRequest) { splitTransaction = fastMerge(existingTransaction, splitTransaction, false); } From 801707f6467711d1788d17501c5d5f9f2b7739f4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 3 Jul 2024 16:56:21 +0700 Subject: [PATCH 029/420] fix reset the debounce once click on video --- .../VideoPlayer/BaseVideoPlayer.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 10c707489a01..80bf7472d1e3 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -106,17 +106,6 @@ function BaseVideoPlayer({ } }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumber]); - const showControl = useCallback(() => { - if (!canToggleControlOnTap) { - return; - } - if (controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { - return; - } - setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); - controlsOpacity.value = 1; - }, [canToggleControlOnTap, controlStatusState, controlsOpacity]); - const hideControl = useCallback( () => (controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE))), [controlsOpacity], @@ -135,6 +124,18 @@ function BaseVideoPlayer({ debouncedHideControl(); }, [isPlaying, debouncedHideControl, controlStatusState, canToggleControlOnTap]); + const showControl = useCallback(() => { + if (!canToggleControlOnTap) { + return; + } + if (controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { + debouncedHideControl(); + return; + } + setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); + controlsOpacity.value = 1; + }, [canToggleControlOnTap, controlStatusState, controlsOpacity, debouncedHideControl]); + const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { videoPopoverMenuPlayerRef.current = videoPlayerRef.current; videoPlayerRef.current?.getStatusAsync().then((status) => { From cfd72f13feaaf1b66d7347f26a00a03ead656b49 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 3 Jul 2024 17:18:36 +0700 Subject: [PATCH 030/420] apply tooltip to lhn row --- .../LHNOptionsList/OptionRowLHN.tsx | 348 ++++++++++-------- .../BaseEducationalTooltip.tsx | 22 +- 2 files changed, 197 insertions(+), 173 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 7703b804611a..fe625bc6a52d 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -14,6 +14,7 @@ import PressableWithSecondaryInteraction from '@components/PressableWithSecondar import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; +import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -56,6 +57,16 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti }, []), ); + const renderGBRTooltip = useCallback( + () => ( + + {translate('quickAction.tooltip.title')} + {translate('quickAction.tooltip.subtitle')} + + ), + [styles.quickActionTooltipTitle, styles.quickActionTooltipSubtitle, translate], + ); + const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const sidebarInnerRowStyle = StyleSheet.flatten( isInFocusMode @@ -140,176 +151,189 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti shouldShowErrorMessages={false} needsOffscreenAlphaCompositing > - - {(hovered) => ( - { - Performance.markStart(CONST.TIMING.OPEN_REPORT); + + + + {(hovered) => ( + { + Performance.markStart(CONST.TIMING.OPEN_REPORT); - event?.preventDefault(); - // Enable Composer to focus on clicking the same chat after opening the context menu. - ReportActionComposeFocusManager.focus(); - onSelectRow(optionItem, popoverAnchor); - }} - onMouseDown={(event) => { - // Allow composer blur on right click - if (!event) { - return; - } + event?.preventDefault(); + // Enable Composer to focus on clicking the same chat after opening the context menu. + ReportActionComposeFocusManager.focus(); + onSelectRow(optionItem, popoverAnchor); + }} + onMouseDown={(event) => { + // Allow composer blur on right click + if (!event) { + return; + } - // Prevent composer blur on left click - event.preventDefault(); - }} - testID={optionItem.reportID} - onSecondaryInteraction={(event) => { - showPopover(event); - // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time - if (DomUtils.getActiveElement()) { - (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); - } - }} - withoutFocusOnSecondaryInteraction - activeOpacity={0.8} - style={[ - styles.flexRow, - styles.alignItemsCenter, - styles.justifyContentBetween, - styles.sidebarLink, - styles.sidebarLinkInnerLHN, - StyleUtils.getBackgroundColorStyle(theme.sidebar), - isFocused ? styles.sidebarLinkActive : null, - (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, - ]} - role={CONST.ROLE.BUTTON} - accessibilityLabel={translate('accessibilityHints.navigatesToChat')} - onLayout={onLayout} - needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} - > - - - {!!optionItem.icons?.length && - (optionItem.shouldShowSubscript ? ( - - ) : ( - - ))} - - - - {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( - - )} - {isStatusVisible && ( - - {emojiCode} - + // Prevent composer blur on left click + event.preventDefault(); + }} + testID={optionItem.reportID} + onSecondaryInteraction={(event) => { + showPopover(event); + // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time + if (DomUtils.getActiveElement()) { + (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); + } + }} + withoutFocusOnSecondaryInteraction + activeOpacity={0.8} + style={[ + styles.flexRow, + styles.alignItemsCenter, + styles.justifyContentBetween, + styles.sidebarLink, + styles.sidebarLinkInnerLHN, + StyleUtils.getBackgroundColorStyle(theme.sidebar), + isFocused ? styles.sidebarLinkActive : null, + (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, + ]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('accessibilityHints.navigatesToChat')} + onLayout={onLayout} + needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} + > + + + {!!optionItem.icons?.length && + (optionItem.shouldShowSubscript ? ( + + ) : ( + + ))} + + + + {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( + + )} + {isStatusVisible && ( + + {emojiCode} + + )} + + {optionItem.alternateText ? ( + + {parseHtmlToText(optionItem.alternateText)} + + ) : null} + + {optionItem?.descriptiveText ? ( + + {optionItem.descriptiveText} + + ) : null} + {hasBrickError && ( + + + )} - {optionItem.alternateText ? ( - - {parseHtmlToText(optionItem.alternateText)} - - ) : null} - - {optionItem?.descriptiveText ? ( - - {optionItem.descriptiveText} - - ) : null} - {hasBrickError && ( - - - - )} - - - - {shouldShowGreenDotIndicator && ( - - - )} - {hasDraftComment && optionItem.isAllowedToComment && ( - - - )} - {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( - - + {shouldShowGreenDotIndicator && ( + + + + )} + {hasDraftComment && optionItem.isAllowedToComment && ( + + + + )} + {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( + + + + )} - )} - - - )} - + + )} + + + ); } diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 1398d74bbd67..f440f672ee04 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -22,17 +22,17 @@ function BaseEducationalTooltip({children, ...props}: TooltipProps) { [], ); - // Automatically hide tooltip after 5 seconds - useEffect(() => { - if (!hideTooltipRef.current) { - return; - } - - const intervalID = setInterval(hideTooltipRef.current, 5000); - return () => { - clearInterval(intervalID); - }; - }, []); + // // Automatically hide tooltip after 5 seconds + // useEffect(() => { + // if (!hideTooltipRef.current) { + // return; + // } + + // const intervalID = setInterval(hideTooltipRef.current, 5000); + // return () => { + // clearInterval(intervalID); + // }; + // }, []); return ( Date: Wed, 3 Jul 2024 11:26:47 -0700 Subject: [PATCH 031/420] Clear pending fields after split --- src/libs/actions/IOU.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9ca7167e9544..8c80a6f68500 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3876,7 +3876,7 @@ function createSplitsAndOnyxData( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`, - value: {pendingAction: null}, + value: {pendingAction: null, pendingFields: null}, }, ]; @@ -3895,6 +3895,8 @@ function createSplitsAndOnyxData( key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`, value: { errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), + pendingAction: null, + pendingFields: null, }, }, ]; @@ -3971,6 +3973,7 @@ function createSplitsAndOnyxData( // or, if the split is being made from the workspace chat, then the oneOnOneChatReport is the same as the splitChatReport // in this case existingSplitChatReport will belong to the policy expense chat and we won't be // entering code that creates optimistic personal details + console.log('Ndebug participant data', {participant, hasMultipleParticipants, existingSplitChatReportID, isOwnPolicyExpenseChat, accountID, currentUserAccountID}); if ((!hasMultipleParticipants && !existingSplitChatReportID) || isOwnPolicyExpenseChat) { oneOnOneChatReport = splitChatReport; shouldCreateOptimisticPersonalDetails = !existingSplitChatReport && !personalDetailExists; @@ -3978,6 +3981,7 @@ function createSplitsAndOnyxData( const existingChatReport = ReportUtils.getChatByParticipants([accountID, currentUserAccountID]); isNewOneOnOneChatReport = !existingChatReport; shouldCreateOptimisticPersonalDetails = isNewOneOnOneChatReport && !personalDetailExists; + console.log('Ndebug existingChatReport', existingChatReport); oneOnOneChatReport = existingChatReport ?? ReportUtils.buildOptimisticChatReport([accountID, currentUserAccountID]); } From 8c6175967d6b728d37df581963972427d276b979 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 3 Jul 2024 11:28:00 -0700 Subject: [PATCH 032/420] Show optimistic distance route when the request failed --- src/components/ReportActionItem/ReportActionItemImage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 1251be83994b..a20654947d4a 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -20,6 +20,7 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; type ReportActionItemImageProps = { /** thumbnail URI for the image */ @@ -75,7 +76,8 @@ function ReportActionItemImage({ const {translate} = useLocalize(); const isDistanceRequest = !!transaction && TransactionUtils.isDistanceRequest(transaction); const hasPendingWaypoints = transaction && TransactionUtils.isFetchingWaypointsFromServer(transaction); - const showMapAsImage = isDistanceRequest && hasPendingWaypoints; + const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields); + const showMapAsImage = isDistanceRequest && (hasErrors || hasPendingWaypoints); if (showMapAsImage) { return ( From 2fc87597d6b2a3bf93e32d18594e6223d919ee57 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 5 Jul 2024 18:49:46 +0700 Subject: [PATCH 033/420] fix do not hide control if popover is opening --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 80bf7472d1e3..fd6976eabffc 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -116,13 +116,13 @@ function BaseVideoPlayer({ if (!canToggleControlOnTap || controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { return; } - if (!isPlaying) { + if (!isPlaying || isPopoverVisible) { debouncedHideControl.cancel(); return; } debouncedHideControl(); - }, [isPlaying, debouncedHideControl, controlStatusState, canToggleControlOnTap]); + }, [isPlaying, debouncedHideControl, controlStatusState, canToggleControlOnTap, isPopoverVisible]); const showControl = useCallback(() => { if (!canToggleControlOnTap) { From c1a5e67ef3d15b6704a9664aba775e5c0518d8d5 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sun, 7 Jul 2024 15:28:38 +0300 Subject: [PATCH 034/420] add build approval chain --- src/libs/PolicyUtils.ts | 43 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 4c071317907b..d3b42ed2c1ee 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -7,7 +7,7 @@ import type {SelectorType} from '@components/SelectionScreen'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx'; +import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagList, PolicyTags, Report, TaxRate} from '@src/types/onyx'; import type {ConnectionLastSync, Connections, CustomUnit, NetSuiteConnection, PolicyFeatureName, Rate, Tenant} from '@src/types/onyx/Policy'; import type PolicyEmployee from '@src/types/onyx/PolicyEmployee'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -383,6 +383,10 @@ function getApprovalWorkflow(policy: OnyxEntry): ValueOf): boolean { + return policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.ADVANCED; +} + function getDefaultApprover(policy: OnyxEntry): string { return policy?.approver ?? policy?.owner ?? ''; } @@ -407,6 +411,42 @@ function getSubmitToAccountID(policy: OnyxEntry, employeeAccountID: numb return getAccountIDsByLogins([employee.submitsTo ?? defaultApprover])[0]; } +function getSubmitToEmail(policy: OnyxEntry, employeeAccountID: number): string { + const submitToAccountID = getSubmitToAccountID(policy, employeeAccountID); + return getLoginsByAccountIDs([submitToAccountID])[0] ?? ''; +} + +function getForwardsToAccount(policy: OnyxEntry, employeeEmail: string, reportTotal: number): string { + if (!hasAdvancedApprovalEnabled(policy)) { + return ''; + } + + const employee = policy?.employeeList?.[employeeEmail]; + if (!employee) { + return ''; + } + + const positiveReportTotal = Math.abs(reportTotal); + if (employee.approvalLimit && positiveReportTotal > employee.approvalLimit) { + return employee.overLimitForwardsTo ?? ''; + } + + return employee.forwardsTo ?? ''; +} + +function buildApprovalChain(policy: OnyxEntry, employeeAccountID: number, reportTotal: number): string[] { + const approvalChain: string[] = []; + + let nextApproverEmail = getSubmitToEmail(policy, employeeAccountID); + + while (!approvalChain.includes(nextApproverEmail)) { + approvalChain.push(nextApproverEmail); + nextApproverEmail = getForwardsToAccount(policy, nextApproverEmail, reportTotal); + } + + return approvalChain.filter((approverEmail) => !!approverEmail); +} + function getPersonalPolicy() { return Object.values(allPolicies ?? {}).find((policy) => policy?.type === CONST.POLICY.TYPE.PERSONAL); } @@ -682,6 +722,7 @@ export { getIntegrationLastSuccessfulDate, getCurrentConnectionName, getCustomersOrJobsLabelNetSuite, + buildApprovalChain, }; export type {MemberEmailsToAccountIDs}; From a8e29ef11478d045bea7621792604a118706007c Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sun, 7 Jul 2024 15:43:40 +0300 Subject: [PATCH 035/420] fix lint --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d3b42ed2c1ee..21b8764060bd 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -7,7 +7,7 @@ import type {SelectorType} from '@components/SelectionScreen'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagList, PolicyTags, Report, TaxRate} from '@src/types/onyx'; +import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx'; import type {ConnectionLastSync, Connections, CustomUnit, NetSuiteConnection, PolicyFeatureName, Rate, Tenant} from '@src/types/onyx/Policy'; import type PolicyEmployee from '@src/types/onyx/PolicyEmployee'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; From a9afa160f1e14136f44f3c273e9c33ba6fe34353 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sun, 7 Jul 2024 16:27:23 +0300 Subject: [PATCH 036/420] rename to get --- src/libs/PolicyUtils.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 21b8764060bd..6c966a89d3b9 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -430,20 +430,17 @@ function getForwardsToAccount(policy: OnyxEntry, employeeEmail: string, if (employee.approvalLimit && positiveReportTotal > employee.approvalLimit) { return employee.overLimitForwardsTo ?? ''; } - return employee.forwardsTo ?? ''; } -function buildApprovalChain(policy: OnyxEntry, employeeAccountID: number, reportTotal: number): string[] { +function getApprovalChain(policy: OnyxEntry, employeeAccountID: number, reportTotal: number): string[] { const approvalChain: string[] = []; - let nextApproverEmail = getSubmitToEmail(policy, employeeAccountID); while (!approvalChain.includes(nextApproverEmail)) { approvalChain.push(nextApproverEmail); nextApproverEmail = getForwardsToAccount(policy, nextApproverEmail, reportTotal); } - return approvalChain.filter((approverEmail) => !!approverEmail); } @@ -722,7 +719,8 @@ export { getIntegrationLastSuccessfulDate, getCurrentConnectionName, getCustomersOrJobsLabelNetSuite, - buildApprovalChain, + getApprovalChain, + hasAdvancedApprovalEnabled, }; export type {MemberEmailsToAccountIDs}; From a7adbbaaff2ba70c13b5e454e7d019e15a523ba8 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sun, 7 Jul 2024 16:27:35 +0300 Subject: [PATCH 037/420] add next steps for multiple approvers --- src/libs/NextStepUtils.ts | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 0ac2878b6857..6c30f4462ccc 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -270,6 +270,62 @@ function buildNextStep(report: OnyxEntry, predictedNextStatus: ValueOf Date: Mon, 8 Jul 2024 11:34:26 +0300 Subject: [PATCH 038/420] Update src/libs/PolicyUtils.ts Co-authored-by: Alex Beaman --- src/libs/PolicyUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 6c966a89d3b9..7252e3a8344d 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -427,8 +427,8 @@ function getForwardsToAccount(policy: OnyxEntry, employeeEmail: string, } const positiveReportTotal = Math.abs(reportTotal); - if (employee.approvalLimit && positiveReportTotal > employee.approvalLimit) { - return employee.overLimitForwardsTo ?? ''; + if (employee.approvalLimit && employee.overLimitForwardsTo && positiveReportTotal > employee.approvalLimit) { + return employee.overLimitForwardsTo; } return employee.forwardsTo ?? ''; } From 5500e9c181a89c4fff2a56a4353a0c3541e37d4b Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 8 Jul 2024 11:36:48 +0300 Subject: [PATCH 039/420] Update src/libs/PolicyUtils.ts Co-authored-by: Alex Beaman --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 7252e3a8344d..68b9b22008c3 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -437,7 +437,7 @@ function getApprovalChain(policy: OnyxEntry, employeeAccountID: number, const approvalChain: string[] = []; let nextApproverEmail = getSubmitToEmail(policy, employeeAccountID); - while (!approvalChain.includes(nextApproverEmail)) { + while (nextApproverEmail && !approvalChain.includes(nextApproverEmail)) { approvalChain.push(nextApproverEmail); nextApproverEmail = getForwardsToAccount(policy, nextApproverEmail, reportTotal); } From a4fdf8e47fbde24fbfb16aab38624b2b9202d284 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 8 Jul 2024 11:37:05 +0300 Subject: [PATCH 040/420] Update src/libs/PolicyUtils.ts --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 68b9b22008c3..23cb8a39bd6e 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -441,7 +441,7 @@ function getApprovalChain(policy: OnyxEntry, employeeAccountID: number, approvalChain.push(nextApproverEmail); nextApproverEmail = getForwardsToAccount(policy, nextApproverEmail, reportTotal); } - return approvalChain.filter((approverEmail) => !!approverEmail); + return approvalChain; } function getPersonalPolicy() { From ebecd03526adeb110ecbecae152ab90a8f62c94f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 8 Jul 2024 16:00:01 +0200 Subject: [PATCH 041/420] Implement Pay as business functionality --- src/components/MoneyReportHeader.tsx | 4 +- .../ReportActionItem/ReportPreview.tsx | 16 ++++- src/components/SettlementButton.tsx | 58 +++++++++++++------ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/API/parameters/PayInvoiceParams.ts | 1 + src/libs/ReportUtils.ts | 43 ++++++++++++-- src/libs/actions/IOU.ts | 48 +++++++++++---- src/libs/actions/Policy/Policy.ts | 2 +- .../home/report/ReportActionItemSingle.tsx | 36 +++++++----- 10 files changed, 157 insertions(+), 53 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 80ad2890afaa..1a6fa5326234 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -137,7 +137,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount; const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout); - const confirmPayment = (type?: PaymentMethodType | undefined) => { + const confirmPayment = (type?: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type || !chatReport) { return; } @@ -146,7 +146,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) { setIsHoldMenuVisible(true); } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) { - IOU.payInvoice(type, chatReport, moneyRequestReport); + IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, moneyRequestReport, true); } diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index d986af8f5cf3..9693b982ec4a 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -138,6 +138,7 @@ function ReportPreview({ const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); + const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); const isApproved = ReportUtils.isReportApproved(iouReport, action); @@ -177,7 +178,7 @@ function ReportPreview({ [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled], ); - const confirmPayment = (type: PaymentMethodType | undefined) => { + const confirmPayment = (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type) { return; } @@ -187,7 +188,7 @@ function ReportPreview({ setIsHoldMenuVisible(true); } else if (chatReport && iouReport) { if (ReportUtils.isInvoiceReport(iouReport)) { - IOU.payInvoice(type, chatReport, iouReport); + IOU.payInvoice(type, chatReport, iouReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, iouReport); } @@ -246,7 +247,16 @@ function ReportPreview({ if (isScanning) { return translate('common.receipt'); } - let payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + + let payerOrApproverName; + if (isPolicyExpenseChat) { + payerOrApproverName = ReportUtils.getPolicyName(chatReport); + } else if (isInvoiceRoom) { + payerOrApproverName = ReportUtils.getInvoicePayerName(chatReport); + } else { + payerOrApproverName = ReportUtils.getDisplayNameForParticipant(managerID, true); + } + if (isApproved) { return translate('iou.managerApproved', {manager: payerOrApproverName}); } diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index 7a7e4e584363..3cda457fb219 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -5,11 +5,13 @@ import {useOnyx, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import * as IOU from '@userActions/IOU'; +import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -41,7 +43,7 @@ type SettlementButtonOnyxProps = { type SettlementButtonProps = SettlementButtonOnyxProps & { /** Callback to execute when this button is pressed. Receives a single payment type argument. */ - onPress: (paymentType?: PaymentMethodType) => void; + onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean) => void; /** The route to redirect if user does not have a payment method setup */ enablePaymentsRoute: EnablePaymentsRoute; @@ -143,6 +145,9 @@ function SettlementButton({ }: SettlementButtonProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + + const primaryPolicy = useMemo(() => PolicyActions.getPrimaryPolicy(activePolicyID), [activePolicyID]); const session = useSession(); // The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here. @@ -199,20 +204,39 @@ function SettlementButton({ } if (isInvoiceReport) { - buttonOptions.push({ - text: translate('iou.settlePersonal', {formattedAmount}), - icon: Expensicons.User, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - backButtonText: translate('iou.individual'), - subMenuItems: [ - { - text: translate('iou.payElsewhere', {formattedAmount: ''}), - icon: Expensicons.Cash, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), - }, - ], - }); + if (ReportUtils.isIndividualInvoiceRoom(chatReport)) { + buttonOptions.push({ + text: translate('iou.settlePersonal', {formattedAmount}), + icon: Expensicons.User, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.individual'), + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), + }, + ], + }); + } + + if (PolicyUtils.isPolicyAdmin(primaryPolicy) && PolicyUtils.isPaidGroupPolicy(primaryPolicy)) { + buttonOptions.push({ + text: translate('iou.settleBusiness', {formattedAmount}), + icon: Expensicons.Building, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.business'), + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, true), + }, + ], + }); + } } if (shouldShowApproveButton) { @@ -226,7 +250,7 @@ function SettlementButton({ return buttonOptions; // We don't want to reorder the options when the preferred payment method changes while the button is still visible // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currency, formattedAmount, iouReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); + }, [currency, formattedAmount, iouReport, chatReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => { if (policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) { @@ -259,7 +283,7 @@ function SettlementButton({ return ( onPress(paymentType)} enablePaymentsRoute={enablePaymentsRoute} addBankAccountRoute={addBankAccountRoute} addDebitCardRoute={addDebitCardRoute} diff --git a/src/languages/en.ts b/src/languages/en.ts index e3e080f26201..94dbf7d33e04 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -705,6 +705,7 @@ export default { settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', individual: 'Individual', + business: 'Business', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as an individual` : `Pay as an individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pay ${formattedAmount}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 9e104ca9b1bb..1a4632f2a1a7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -698,6 +698,7 @@ export default { settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', individual: 'Individual', + business: 'Empresa', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pago ${formattedAmount} como individuo` : `Pago individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pagar ${formattedAmount}`, diff --git a/src/libs/API/parameters/PayInvoiceParams.ts b/src/libs/API/parameters/PayInvoiceParams.ts index 4c6633749adb..a6b9746d87bc 100644 --- a/src/libs/API/parameters/PayInvoiceParams.ts +++ b/src/libs/API/parameters/PayInvoiceParams.ts @@ -4,6 +4,7 @@ type PayInvoiceParams = { reportID: string; reportActionID: string; paymentMethodType: PaymentMethodType; + payAsBusiness: boolean; }; export default PayInvoiceParams; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 342e2439ed66..1e1f72adb37d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -902,11 +902,20 @@ function isTripRoom(report: OnyxEntry): boolean { return isChatReport(report) && getChatType(report) === CONST.REPORT.CHAT_TYPE.TRIP_ROOM; } +function isIndividualInvoiceRoom(report: OnyxEntry): boolean { + return isInvoiceRoom(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; +} + function isCurrentUserInvoiceReceiver(report: OnyxEntry): boolean { if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { return currentUserAccountID === report.invoiceReceiver.accountID; } + if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS) { + const policy = PolicyUtils.getPolicy(report.invoiceReceiver.policyID); + return PolicyUtils.isPolicyAdmin(policy); + } + return false; } @@ -2050,9 +2059,15 @@ function getIcons( if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { icons.push(...getIconsForParticipants([report?.invoiceReceiver.accountID], personalDetails)); } else { - const receiverPolicy = getPolicy(report?.invoiceReceiver?.policyID); + const receiverPolicyID = report?.invoiceReceiver?.policyID; + const receiverPolicy = getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(report, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicyID, + }); } } } @@ -2124,10 +2139,16 @@ function getIcons( return icons; } - const receiverPolicy = getPolicy(invoiceRoomReport?.invoiceReceiver?.policyID); + const receiverPolicyID = invoiceRoomReport?.invoiceReceiver?.policyID; + const receiverPolicy = getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(invoiceRoomReport, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicyID, + }); } return icons; @@ -2592,7 +2613,17 @@ function getMoneyRequestReportName(report: OnyxEntry, policy?: OnyxEntry const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency); - let payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; + + let payerOrApproverName; + if (isExpenseReport(report)) { + payerOrApproverName = getPolicyName(report, false, policy); + } else if (isInvoiceReport(report)) { + const chatReport = getReportOrDraftReport(report?.chatReportID); + payerOrApproverName = getInvoicePayerName(chatReport); + } else { + payerOrApproverName = getDisplayNameForParticipant(report?.managerID) ?? ''; + } + const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerOrApproverName, amount: formattedAmount, @@ -5522,6 +5553,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec isChatThread(report) || isTaskReport(report) || isMoneyRequestReport(report) || + isInvoiceReport(report) || isChatRoom(report) || isPolicyExpenseChat(report) || (isGroupChat(report) && !shouldIncludeGroupChats) @@ -7313,6 +7345,7 @@ export { getChatUsedForOnboarding, findPolicyExpenseChatByPolicyID, hasOnlyNonReimbursableTransactions, + isIndividualInvoiceRoom, }; export type { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 48c70021cacc..42381d9008a7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -291,6 +291,12 @@ Onyx.connect({ }, }); +let primaryPolicyID: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + callback: (value) => (primaryPolicyID = value), +}); + /** * Get the report or draft report given a reportID */ @@ -5938,13 +5944,22 @@ function getSendMoneyParams( } function getPayMoneyRequestParams( - chatReport: OnyxTypes.Report, + initialChatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, recipient: Participant, paymentMethodType: PaymentMethodType, full: boolean, + payAsBusiness?: boolean, ): PayMoneyRequestData { const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport); + let chatReport = initialChatReport; + + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { + const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', primaryPolicyID); + if (existingB2BInvoiceRoom) { + chatReport = existingB2BInvoiceRoom; + } + } let total = (iouReport.total ?? 0) - (iouReport.nonReimbursableTotal ?? 0); if (ReportUtils.hasHeldExpenses(iouReport.reportID) && !full && !!iouReport.unheldTotal) { @@ -5977,19 +5992,27 @@ function getPayMoneyRequestParams( optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithExpensify: paymentMethodType === CONST.IOU.PAYMENT_TYPE.VBBA}); } + const optimisticChatReport = { + ...chatReport, + lastReadTime: DateUtils.getDBTime(), + lastVisibleActionCreated: optimisticIOUReportAction.created, + hasOutstandingChildRequest: false, + iouReportID: null, + lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), + lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticIOUReportAction), + }; + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { + optimisticChatReport.invoiceReceiver = { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, + policyID: primaryPolicyID, + }; + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, - value: { - ...chatReport, - lastReadTime: DateUtils.getDBTime(), - lastVisibleActionCreated: optimisticIOUReportAction.created, - hasOutstandingChildRequest: false, - iouReportID: null, - lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), - lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticIOUReportAction), - }, + value: optimisticChatReport, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -6611,19 +6634,20 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R Navigation.dismissModalWithReport(chatReport); } -function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report) { +function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report, payAsBusiness = false) { const recipient = {accountID: invoiceReport.ownerAccountID}; const { optimisticData, successData, failureData, params: {reportActionID}, - } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true); + } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true, payAsBusiness); const params: PayInvoiceParams = { reportID: invoiceReport.reportID, reportActionID, paymentMethodType, + payAsBusiness, }; API.write(WRITE_COMMANDS.PAY_INVOICE, params, {optimisticData, successData, failureData}); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index acd42b6202c7..0111c2e406c6 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -190,7 +190,7 @@ function getPolicy(policyID: string | undefined): OnyxEntry { */ function getPrimaryPolicy(activePolicyID?: OnyxEntry): Policy | undefined { const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); - const primaryPolicy: Policy | null | undefined = allPolicies?.[activePolicyID ?? '-1']; + const primaryPolicy: Policy | null | undefined = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]; return primaryPolicy ?? activeAdminWorkspaces[0]; } diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 7b0db3e0d844..53527e85b215 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -19,6 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; import {getReportActionMessage} from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -112,21 +113,30 @@ function ReportActionItemSingle({ let secondaryAvatar: Icon; const primaryDisplayName = displayName; if (displayAllActors) { - // The ownerAccountID and actorAccountID can be the same if a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice - const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? actorAccountID : ownerAccountID; - const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; - const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); + if (ReportUtils.isInvoiceRoom(report) && !ReportUtils.isIndividualInvoiceRoom(report)) { + const secondaryPolicyID = report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : '-1'; + const secondaryPolicy = PolicyUtils.getPolicy(secondaryPolicyID); + const secondaryPolicyAvatar = secondaryPolicy?.avatarURL ?? ReportUtils.getDefaultWorkspaceAvatar(secondaryPolicy?.name); - if (!isInvoiceReport) { - displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; - } + secondaryAvatar = { + source: secondaryPolicyAvatar, + type: CONST.ICON_TYPE_WORKSPACE, + name: secondaryPolicy?.name, + id: secondaryPolicyID, + }; + } else { + // The ownerAccountID and actorAccountID can be the same if a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice + const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? actorAccountID : ownerAccountID; + const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; + const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); - secondaryAvatar = { - source: secondaryUserAvatar, - type: CONST.ICON_TYPE_AVATAR, - name: secondaryDisplayName ?? '', - id: secondaryAccountId, - }; + secondaryAvatar = { + source: secondaryUserAvatar, + type: CONST.ICON_TYPE_AVATAR, + name: secondaryDisplayName ?? '', + id: secondaryAccountId, + }; + } } else if (!isWorkspaceActor) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const avatarIconIndex = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1; From 85c45ddc9e65aaecb97ec9f90b5efb0fa4e337c8 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 9 Jul 2024 15:13:29 +0200 Subject: [PATCH 042/420] Fix workspace details display --- src/ROUTES.ts | 4 ++-- src/components/RoomHeaderAvatars.tsx | 4 ++-- src/libs/Navigation/types.ts | 1 + src/pages/ReportAvatar.tsx | 5 +++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d548297cb854..9e83c59e5e4b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -240,8 +240,8 @@ const ROUTES = { }, }, REPORT_AVATAR: { - route: 'r/:reportID/avatar', - getRoute: (reportID: string) => `r/${reportID}/avatar` as const, + route: 'r/:reportID/avatar/:policyID', + getRoute: (reportID: string, policyID: string) => `r/${reportID}/avatar/${policyID}` as const, }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index ac3b9c4d1396..0c14588afc30 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -17,8 +17,8 @@ type RoomHeaderAvatarsProps = { function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) { const navigateToAvatarPage = (icon: Icon) => { - if (icon.type === CONST.ICON_TYPE_WORKSPACE) { - Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID)); + if (icon.type === CONST.ICON_TYPE_WORKSPACE && icon.id) { + Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID, icon.id.toString())); return; } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a60316fb7768..a8241ce20a21 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1087,6 +1087,7 @@ type AuthScreensParamList = CentralPaneScreensParamList & }; [SCREENS.REPORT_AVATAR]: { reportID: string; + policyID: string; }; [SCREENS.NOT_FOUND]: undefined; [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams; diff --git a/src/pages/ReportAvatar.tsx b/src/pages/ReportAvatar.tsx index 5ec34e3b71dc..560c4dccdb9c 100644 --- a/src/pages/ReportAvatar.tsx +++ b/src/pages/ReportAvatar.tsx @@ -20,8 +20,9 @@ type ReportAvatarOnyxProps = { type ReportAvatarProps = ReportAvatarOnyxProps & StackScreenProps; -function ReportAvatar({report = {} as Report, policies, isLoadingApp = true}: ReportAvatarProps) { - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`]; +function ReportAvatar({report = {} as Report, route, policies, isLoadingApp = true}: ReportAvatarProps) { + const policyID = route.params.policyID; + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const policyName = ReportUtils.getPolicyName(report, false, policy); const avatarURL = ReportUtils.getWorkspaceAvatar(report); From 5188d489fe7738e1ad7366ef9ee5216d8c7b1a31 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 9 Jul 2024 10:59:42 -0700 Subject: [PATCH 043/420] Only allow global create distance for non-splits --- src/pages/iou/request/IOURequestStartPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index f320b013f2b3..b1428da962ec 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -101,7 +101,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = !!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = !!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.SPLIT); const navigateBack = () => { Navigation.closeRHPFlow(); From 948e1f7f0724a6789f2d59e6fff7d4be0692454d Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:25:44 +0200 Subject: [PATCH 044/420] fix hold/unhold logic --- src/libs/ReportUtils.ts | 5 +++-- src/pages/ReportDetailsPage.tsx | 17 +++++++++++------ .../report/ContextMenu/ContextMenuActions.tsx | 6 ++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fb3bebd75274..e8d5697de64b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2823,12 +2823,13 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry) const isApprover = isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && currentUserPersonalDetails?.accountID === moneyRequestReport?.managerID; const isOnHold = TransactionUtils.isOnHold(transaction); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); + const isClosed = isClosedReport(moneyRequestReport); const canModifyStatus = !isTrackExpenseMoneyReport && (isPolicyAdmin || isActionOwner || isApprover); const isDeletedParentAction = isEmptyObject(parentReportAction) || ReportActionsUtils.isDeletedAction(parentReportAction); - const canHoldOrUnholdRequest = !isRequestSettled && !isApproved && !isDeletedParentAction; - const canHoldRequest = canHoldOrUnholdRequest && !isOnHold && (isRequestHoldCreator || (!isRequestIOU && canModifyStatus)) && !isScanning && !!transaction?.reimbursable; + const canHoldOrUnholdRequest = !isRequestSettled && !isApproved && !isDeletedParentAction && !isClosed; + const canHoldRequest = canHoldOrUnholdRequest && !isOnHold && (isRequestIOU || canModifyStatus) && !isScanning && !!transaction?.reimbursable; const canUnholdRequest = !!(canHoldOrUnholdRequest && isOnHold && (isRequestHoldCreator || (!isRequestIOU && canModifyStatus))) && !!transaction?.reimbursable; return {canHoldRequest, canUnholdRequest}; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index a585f9c94d67..6726e5d36540 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -171,7 +171,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD // 2. MoneyReport case if (caseID === CASES.MONEY_REPORT) { if (!reportActions || !transactionThreadReport?.parentReportActionID) { - return null; + return undefined; } return reportActions.find((action) => action.reportActionID === transactionThreadReport.parentReportActionID); } @@ -512,9 +512,14 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID ?? '' : ''; - const canHoldUnholdReportAction = ReportUtils.canHoldUnholdReportAction(parentReportAction); - const shouldShowHoldAction = - caseID !== CASES.MONEY_REPORT && (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) && !ReportUtils.isArchivedRoom(parentReport); + let holdReportAction: OnyxTypes.ReportAction | undefined; + if (caseID == CASES.MONEY_REQUEST) { + holdReportAction = parentReportAction; + } else if (caseID == CASES.MONEY_REPORT) { + holdReportAction = requestParentReportAction; + } + const canHoldUnholdReportAction = ReportUtils.canHoldUnholdReportAction(holdReportAction); + const shouldShowHoldAction = (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) && !ReportUtils.isArchivedRoom(parentReport); const canJoin = ReportUtils.canJoinChat(report, parentReportAction, policy); @@ -526,7 +531,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD } if (isExpenseReport && shouldShowHoldAction) { - result.push(PromotedActions.hold({isTextHold: canHoldUnholdReportAction.canHoldRequest, reportAction: parentReportAction})); + result.push(PromotedActions.hold({isTextHold: canHoldUnholdReportAction.canHoldRequest, reportAction: holdReportAction})); } if (report) { @@ -536,7 +541,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD result.push(PromotedActions.share(report)); return result; - }, [report, parentReportAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest]); + }, [report, holdReportAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest]); const nameSectionExpenseIOU = ( diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 218c382fd776..42bd26ac6dbd 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -260,8 +260,7 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'iou.unholdExpense', icon: Expensicons.Stopwatch, - shouldShow: (type, reportAction) => - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canEditReportAction(reportAction) && ReportUtils.canHoldUnholdReportAction(reportAction).canUnholdRequest, + shouldShow: (type, reportAction) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canHoldUnholdReportAction(reportAction).canUnholdRequest, onPress: (closePopover, {reportAction}) => { if (closePopover) { hideContextMenu(false, () => ReportUtils.changeMoneyRequestHoldStatus(reportAction)); @@ -277,8 +276,7 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'iou.hold', icon: Expensicons.Stopwatch, - shouldShow: (type, reportAction) => - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canEditReportAction(reportAction) && ReportUtils.canHoldUnholdReportAction(reportAction).canHoldRequest, + shouldShow: (type, reportAction) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canHoldUnholdReportAction(reportAction).canHoldRequest, onPress: (closePopover, {reportAction}) => { if (closePopover) { hideContextMenu(false, () => ReportUtils.changeMoneyRequestHoldStatus(reportAction)); From ff2f2583432b57898d71db63370fa7af25d52f89 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 10 Jul 2024 09:20:33 -0700 Subject: [PATCH 045/420] Fix missed replacement of changed variable --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7239c702c884..188743492468 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2010,7 +2010,7 @@ function getMoneyRequestInformation( undefined, false, false, - receiptObject, + optimisticTransaction.receipt, false, undefined, linkedTrackedExpenseReportAction?.childReportID, From e2809f44f7c3bb9bfaec6e68dc33ed99d268c506 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 10 Jul 2024 09:26:16 -0700 Subject: [PATCH 046/420] Prettify --- src/components/MoneyRequestConfirmationList.tsx | 5 +---- src/components/ReportActionItem/ReportActionItemImage.tsx | 2 +- src/libs/actions/IOU.ts | 8 ++++++-- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 2 +- src/pages/iou/request/step/IOURequestStepDistance.tsx | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index b8175dd2ed3a..af8959f9278e 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -262,10 +262,7 @@ function MoneyRequestConfirmationList({ const distanceRequestAmount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); const formattedAmount = isDistanceRequestWithPendingRoute ? '' - : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? distanceRequestAmount : iouAmount, - isDistanceRequest ? currency : iouCurrencyCode, - ); + : CurrencyUtils.convertToDisplayString(shouldCalculateDistanceAmount ? distanceRequestAmount : iouAmount, isDistanceRequest ? currency : iouCurrencyCode); const previousTransactionAmount = usePrevious(transaction?.amount); const previousTransactionCurrency = usePrevious(transaction?.currency); diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index a20654947d4a..4f58b193eeb2 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -20,7 +20,7 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; type ReportActionItemImageProps = { /** thumbnail URI for the image */ diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 188743492468..e0d8fcafe34e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3777,7 +3777,7 @@ function createSplitsAndOnyxData( const isOwnPolicyExpenseChat = !!splitChatReport.isOwnPolicyExpenseChat; // Pass an open receipt so the distance expense will show a map with the route optimistically - const receipt: Receipt|undefined = (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) ? {source: ReceiptGeneric as ReceiptSource, state: CONST.IOU.RECEIPT_STATE.OPEN} : undefined; + const receipt: Receipt | undefined = iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE ? {source: ReceiptGeneric as ReceiptSource, state: CONST.IOU.RECEIPT_STATE.OPEN} : undefined; const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; @@ -4931,7 +4931,11 @@ function createDistanceRequest( let parameters: CreateDistanceRequestParams; let onyxData: OnyxData; if (iouType === CONST.IOU.TYPE.SPLIT) { - const {splitData, splits, onyxData: splitOnyxData} = createSplitsAndOnyxData( + const { + splitData, + splits, + onyxData: splitOnyxData, + } = createSplitsAndOnyxData( participants, currentUserLogin ?? '', currentUserAccountID, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index c02754c4eadb..9b77894cf561 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -344,7 +344,7 @@ function IOURequestStepConfirmation({ formHasBeenSubmitted.current = true; if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE && !IOUUtils.isMovingTransactionFromTrackExpense(action)) { - createDistanceRequest((iouType === CONST.IOU.TYPE.SPLIT) ? splitParticipants : selectedParticipants, trimmedComment); + createDistanceRequest(iouType === CONST.IOU.TYPE.SPLIT ? splitParticipants : selectedParticipants, trimmedComment); return; } diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index a48d6454c002..b3aead35f53c 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -17,6 +17,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -37,7 +38,6 @@ import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -import DistanceRequestUtils from '@libs/DistanceRequestUtils'; type IOURequestStepDistanceOnyxProps = { /** backup version of the original transaction */ From 0c865389313e33905d07171bd999023f79cc716d Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 10 Jul 2024 09:40:38 -0700 Subject: [PATCH 047/420] Remove leftover debug console logs --- src/libs/actions/IOU.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index e0d8fcafe34e..89495ef38523 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3997,7 +3997,6 @@ function createSplitsAndOnyxData( // or, if the split is being made from the workspace chat, then the oneOnOneChatReport is the same as the splitChatReport // in this case existingSplitChatReport will belong to the policy expense chat and we won't be // entering code that creates optimistic personal details - console.log('Ndebug participant data', {participant, hasMultipleParticipants, existingSplitChatReportID, isOwnPolicyExpenseChat, accountID, currentUserAccountID}); if ((!hasMultipleParticipants && !existingSplitChatReportID) || isOwnPolicyExpenseChat) { oneOnOneChatReport = splitChatReport; shouldCreateOptimisticPersonalDetails = !existingSplitChatReport && !personalDetailExists; @@ -4005,7 +4004,6 @@ function createSplitsAndOnyxData( const existingChatReport = ReportUtils.getChatByParticipants([accountID, currentUserAccountID]); isNewOneOnOneChatReport = !existingChatReport; shouldCreateOptimisticPersonalDetails = isNewOneOnOneChatReport && !personalDetailExists; - console.log('Ndebug existingChatReport', existingChatReport); oneOnOneChatReport = existingChatReport ?? ReportUtils.buildOptimisticChatReport([accountID, currentUserAccountID]); } From e43ffe0e7617aaa49566c647d4026e9f0e6ffbf2 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 11 Jul 2024 10:51:34 +0200 Subject: [PATCH 048/420] fix lint errors --- src/pages/ReportDetailsPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 6726e5d36540..d324800baa7f 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -513,9 +513,9 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD : ''; let holdReportAction: OnyxTypes.ReportAction | undefined; - if (caseID == CASES.MONEY_REQUEST) { + if (caseID === CASES.MONEY_REQUEST) { holdReportAction = parentReportAction; - } else if (caseID == CASES.MONEY_REPORT) { + } else if (caseID === CASES.MONEY_REPORT) { holdReportAction = requestParentReportAction; } const canHoldUnholdReportAction = ReportUtils.canHoldUnholdReportAction(holdReportAction); From 8353e39d0ca7c997091d224ecc9585fa0d6b6a9b Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 12 Jul 2024 11:27:06 +0700 Subject: [PATCH 049/420] add copy and icon --- .../LHNOptionsList/OptionRowLHN.tsx | 25 +++++++++++++++---- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 2b45cecbc477..4fd5a03d7afb 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -59,12 +59,26 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const renderGBRTooltip = useCallback( () => ( - - {translate('quickAction.tooltip.title')} - {translate('quickAction.tooltip.subtitle')} - + + + {translate('sidebarScreen.tooltip')} + ), - [styles.quickActionTooltipTitle, styles.quickActionTooltipSubtitle, translate], + [ + styles.alignItemsCenter, + styles.flexRow, + styles.justifyContentCenter, + styles.flexWrap, + styles.textAlignCenter, + styles.gap1, + styles.quickActionTooltipSubtitle, + theme.tooltipHighlightText, + translate, + ], ); const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; @@ -159,6 +173,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, }} shiftHorizontal={-20} + shiftVertical={-50} wrapperStyle={styles.quickActionTooltipWrapper} > diff --git a/src/languages/en.ts b/src/languages/en.ts index c7b5125d02fa..98b2245a276c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -604,6 +604,7 @@ export default { listOfChatMessages: 'List of chat messages', listOfChats: 'List of chats', saveTheWorld: 'Save the world', + tooltip: 'Get started here!', }, allSettingsScreen: { subscriptions: 'Subscriptions', diff --git a/src/languages/es.ts b/src/languages/es.ts index 075903d0f324..43021e936cda 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -597,6 +597,7 @@ export default { listOfChatMessages: 'Lista de mensajes del chat', listOfChats: 'lista de chats', saveTheWorld: 'Salvar el mundo', + tooltip: '¡Comienza aquí!', }, allSettingsScreen: { subscriptions: 'Suscripciones', From e26e63c636e7f2846d1233c97af6f27e3cb78a26 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jul 2024 15:06:30 +0700 Subject: [PATCH 050/420] hide tooltip when navigate --- .../LHNOptionsList/OptionRowLHN.tsx | 5 +-- .../BaseEducationalTooltip.tsx | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 4fd5a03d7afb..f46343276400 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -44,6 +44,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`); + const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); @@ -166,14 +167,14 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti needsOffscreenAlphaCompositing > diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 0cd134d1e63c..0874369c8935 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -1,4 +1,5 @@ -import React, {memo, useEffect, useRef} from 'react'; +import {useNavigation} from '@react-navigation/native'; +import React, {memo, useCallback, useEffect, useRef} from 'react'; import type {LayoutEvent} from 'react-native'; import GenericTooltip from '@components/Tooltip/GenericTooltip'; import type TooltipProps from '@components/Tooltip/types'; @@ -9,26 +10,28 @@ import getBounds from './getBounds'; * This tooltip would show immediately without user's interaction and hide after 5 seconds. */ function BaseEducationalTooltip({children, ...props}: TooltipProps) { + const navigation = useNavigation(); const hideTooltipRef = useRef<() => void>(); - useEffect( - () => () => { - if (!hideTooltipRef.current) { - return; - } + const triggerHideTooltip = useCallback(() => { + if (!hideTooltipRef.current) { + return; + } - hideTooltipRef.current(); - }, - [], - ); + hideTooltipRef.current(); + }, []); + + useEffect(() => { + const unsubscribeBlur = navigation.addListener('blur', triggerHideTooltip); + return () => { + unsubscribeBlur(); + triggerHideTooltip(); + }; + }, [navigation, triggerHideTooltip]); // // Automatically hide tooltip after 5 seconds // useEffect(() => { - // if (!hideTooltipRef.current) { - // return; - // } - - // const intervalID = setInterval(hideTooltipRef.current, 5000); + // const intervalID = setInterval(triggerHideTooltip, 5000); // return () => { // clearInterval(intervalID); // }; From da051d6f2b2b9102bfef501ec59bbe6cce2e6e54 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jul 2024 16:04:36 +0700 Subject: [PATCH 051/420] fix test & perf --- .../Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 0874369c8935..832b533c0392 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -24,7 +24,7 @@ function BaseEducationalTooltip({children, ...props}: TooltipProps) { useEffect(() => { const unsubscribeBlur = navigation.addListener('blur', triggerHideTooltip); return () => { - unsubscribeBlur(); + unsubscribeBlur?.(); triggerHideTooltip(); }; }, [navigation, triggerHideTooltip]); From c8829de06e6e4407bcd07553a764f95322f5de41 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jul 2024 16:09:39 +0700 Subject: [PATCH 052/420] revert: apply tooltip to lhn row --- .../LHNOptionsList/OptionRowLHN.tsx | 364 ++++++++---------- 1 file changed, 162 insertions(+), 202 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index f46343276400..431a12d00106 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -14,7 +14,6 @@ import PressableWithSecondaryInteraction from '@components/PressableWithSecondar import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; -import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -44,7 +43,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`); - const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); @@ -58,30 +56,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti }, []), ); - const renderGBRTooltip = useCallback( - () => ( - - - {translate('sidebarScreen.tooltip')} - - ), - [ - styles.alignItemsCenter, - styles.flexRow, - styles.justifyContentCenter, - styles.flexWrap, - styles.textAlignCenter, - styles.gap1, - styles.quickActionTooltipSubtitle, - theme.tooltipHighlightText, - translate, - ], - ); - const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const sidebarInnerRowStyle = StyleSheet.flatten( isInFocusMode @@ -166,190 +140,176 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti shouldShowErrorMessages={false} needsOffscreenAlphaCompositing > - - - - {(hovered) => ( - { - Performance.markStart(CONST.TIMING.OPEN_REPORT); + + {(hovered) => ( + { + Performance.markStart(CONST.TIMING.OPEN_REPORT); - event?.preventDefault(); - // Enable Composer to focus on clicking the same chat after opening the context menu. - ReportActionComposeFocusManager.focus(); - onSelectRow(optionItem, popoverAnchor); - }} - onMouseDown={(event) => { - // Allow composer blur on right click - if (!event) { - return; - } + event?.preventDefault(); + // Enable Composer to focus on clicking the same chat after opening the context menu. + ReportActionComposeFocusManager.focus(); + onSelectRow(optionItem, popoverAnchor); + }} + onMouseDown={(event) => { + // Allow composer blur on right click + if (!event) { + return; + } - // Prevent composer blur on left click - event.preventDefault(); - }} - testID={optionItem.reportID} - onSecondaryInteraction={(event) => { - showPopover(event); - // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time - if (DomUtils.getActiveElement()) { - (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); - } - }} - withoutFocusOnSecondaryInteraction - activeOpacity={0.8} - style={[ - styles.flexRow, - styles.alignItemsCenter, - styles.justifyContentBetween, - styles.sidebarLink, - styles.sidebarLinkInnerLHN, - StyleUtils.getBackgroundColorStyle(theme.sidebar), - isFocused ? styles.sidebarLinkActive : null, - (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, - ]} - role={CONST.ROLE.BUTTON} - accessibilityLabel={translate('accessibilityHints.navigatesToChat')} - onLayout={onLayout} - needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} - > - - - {!!optionItem.icons?.length && - (optionItem.shouldShowSubscript ? ( - - ) : ( - - ))} - - - - {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( - - )} - {isStatusVisible && ( - - {emojiCode} - - )} - - {optionItem.alternateText ? ( - - {Parser.htmlToText(optionItem.alternateText)} - - ) : null} - - {optionItem?.descriptiveText ? ( - - {optionItem.descriptiveText} - - ) : null} - {hasBrickError && ( - - - + // Prevent composer blur on left click + event.preventDefault(); + }} + testID={optionItem.reportID} + onSecondaryInteraction={(event) => { + showPopover(event); + // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time + if (DomUtils.getActiveElement()) { + (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); + } + }} + withoutFocusOnSecondaryInteraction + activeOpacity={0.8} + style={[ + styles.flexRow, + styles.alignItemsCenter, + styles.justifyContentBetween, + styles.sidebarLink, + styles.sidebarLinkInnerLHN, + StyleUtils.getBackgroundColorStyle(theme.sidebar), + isFocused ? styles.sidebarLinkActive : null, + (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, + ]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('accessibilityHints.navigatesToChat')} + onLayout={onLayout} + needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} + > + + + {!!optionItem.icons?.length && + (optionItem.shouldShowSubscript ? ( + + ) : ( + + ))} + + + + {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( + + )} + {isStatusVisible && ( + + {emojiCode} + )} + {optionItem.alternateText ? ( + + {Parser.htmlToText(optionItem.alternateText)} + + ) : null} + + {optionItem?.descriptiveText ? ( + + {optionItem.descriptiveText} + + ) : null} + {hasBrickError && ( + + + + )} + + + + {shouldShowGreenDotIndicator && ( + + + )} + {hasDraftComment && optionItem.isAllowedToComment && ( - {shouldShowGreenDotIndicator && ( - - - - )} - {hasDraftComment && optionItem.isAllowedToComment && ( - - - - )} - {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( - - - - )} + + + )} + {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( + + - - )} - - - + )} + + + )} + ); } From 5de4ac771b0374226c8d5c16885819bec87e48e2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jul 2024 16:27:45 +0700 Subject: [PATCH 053/420] implement tooltip ui --- src/languages/en.ts | 4 + src/languages/es.ts | 4 + .../ReportActionCompose.tsx | 246 +++++++++++------- src/styles/index.ts | 6 + 4 files changed, 160 insertions(+), 100 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 07af25321d8e..f7bf2ae12a56 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -506,6 +506,10 @@ export default { emoji: 'Emoji', collapse: 'Collapse', expand: 'Expand', + tooltip: { + title: 'Get started!', + subtitle: ' Submit your first expense', + }, }, reportActionContextMenu: { copyToClipboard: 'Copy to clipboard', diff --git a/src/languages/es.ts b/src/languages/es.ts index 97938aa48dfd..7f146c586b8f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -497,6 +497,10 @@ export default { emoji: 'Emoji', collapse: 'Colapsar', expand: 'Expandir', + tooltip: { + title: 'Get started!', + subtitle: ' Submit your first expense', + }, }, reportActionContextMenu: { copyToClipboard: 'Copiar al portapapeles', diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 9fede8068e64..d6dcff6a5831 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -10,16 +10,21 @@ import type {FileObject} from '@components/AttachmentModal'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; import type {Mention} from '@components/MentionSuggestions'; import OfflineIndicator from '@components/OfflineIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxProvider'; +import Text from '@components/Text'; +import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useDebounce from '@hooks/useDebounce'; import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; @@ -118,6 +123,7 @@ function ReportActionCompose({ onComposerFocus, onComposerBlur, }: ReportActionComposeProps) { + const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); @@ -377,6 +383,34 @@ function ReportActionCompose({ return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM; }, [styles]); + const renderWorkspaceChatTooltip = useCallback( + () => ( + + + + {translate('reportActionCompose.tooltip.title')} + {translate('reportActionCompose.tooltip.subtitle')} + + + ), + [ + styles.alignItemsCenter, + styles.flexRow, + styles.justifyContentCenter, + styles.flexWrap, + styles.textAlignCenter, + styles.gap1, + styles.quickActionTooltipTitle, + styles.quickActionTooltipSubtitle, + theme.tooltipHighlightText, + translate, + ], + ); + return ( @@ -388,110 +422,122 @@ function ReportActionCompose({ style={isComposerFullSize ? styles.chatItemFullComposeRow : {}} contentContainerStyle={isComposerFullSize ? styles.flex1 : {}} > - - setIsAttachmentPreviewActive(true)} - onModalHide={onAttachmentPreviewClose} + - {({displayFileInModal}) => ( - <> - { - isNextModalWillOpenRef.current = false; - restoreKeyboardState(); - }} - onMenuClosed={restoreKeyboardState} - onAddActionPressed={onAddActionPressed} - onItemSelected={onItemSelected} - actionButtonRef={actionButtonRef} - /> - { - if (value.length === 0 && isComposerFullSize) { - Report.setIsComposerFullSize(reportID, false); - } - validateCommentMaxLength(value, {reportID}); - }} - /> - { - if (isAttachmentPreviewActive) { - return; - } - const data = event.dataTransfer?.items[0]; - displayFileInModal(data as unknown as FileObject); - }} - /> - + setIsAttachmentPreviewActive(true)} + onModalHide={onAttachmentPreviewClose} + > + {({displayFileInModal}) => ( + <> + { + isNextModalWillOpenRef.current = false; + restoreKeyboardState(); + }} + onMenuClosed={restoreKeyboardState} + onAddActionPressed={onAddActionPressed} + onItemSelected={onItemSelected} + actionButtonRef={actionButtonRef} + /> + { + if (value.length === 0 && isComposerFullSize) { + Report.setIsComposerFullSize(reportID, false); + } + validateCommentMaxLength(value, {reportID}); + }} + /> + { + if (isAttachmentPreviewActive) { + return; + } + const data = event.dataTransfer?.items[0]; + displayFileInModal(data as unknown as FileObject); + }} + /> + + )} + + {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( + composerRef.current?.replaceSelectionWithText(...args)} + emojiPickerID={report?.reportID} + shiftVertical={emojiShiftVertical} + /> )} - - {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( - composerRef.current?.replaceSelectionWithText(...args)} - emojiPickerID={report?.reportID} - shiftVertical={emojiShiftVertical} + - )} - - + + ...wordBreak.breakWord, }, + reportActionComposeTooltipWrapper: { + backgroundColor: theme.tooltipHighlightBG, + paddingVertical: 8, + borderRadius: variables.componentBorderRadiusMedium, + }, + quickActionTooltipWrapper: { backgroundColor: theme.tooltipHighlightBG, }, From 80504a8728130196d01e785c2bb3a57ebf34daee Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jul 2024 00:40:06 +0700 Subject: [PATCH 054/420] remove canToggleControlOnTap and fix lint --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 17 ++++++----------- src/components/VideoPlayer/types.ts | 1 - 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index cfd234c1f9f5..b93d4e13c78c 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -51,7 +51,6 @@ function BaseVideoPlayer({ // eslint-disable-next-line @typescript-eslint/no-unused-vars isVideoHovered = false, isPreview, - canToggleControlOnTap = false, }: VideoPlayerProps) { const styles = useThemeStyles(); const { @@ -106,14 +105,13 @@ function BaseVideoPlayer({ } }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumber]); - const hideControl = useCallback( - () => (controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE))), - [controlsOpacity], - ); + const hideControl = useCallback(() => { + controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); + }, [controlsOpacity]); const debouncedHideControl = useMemo(() => debounce(hideControl, 2000), [hideControl]); useEffect(() => { - if (!canToggleControlOnTap || controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { + if (controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { return; } if (!isPlaying || isPopoverVisible) { @@ -122,19 +120,16 @@ function BaseVideoPlayer({ } debouncedHideControl(); - }, [isPlaying, debouncedHideControl, controlStatusState, canToggleControlOnTap, isPopoverVisible]); + }, [isPlaying, debouncedHideControl, controlStatusState, isPopoverVisible]); const showControl = useCallback(() => { - if (!canToggleControlOnTap) { - return; - } if (controlStatusState === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW) { debouncedHideControl(); return; } setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); controlsOpacity.value = 1; - }, [canToggleControlOnTap, controlStatusState, controlsOpacity, debouncedHideControl]); + }, [controlStatusState, controlsOpacity, debouncedHideControl]); const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { videoPopoverMenuPlayerRef.current = videoPlayerRef.current; diff --git a/src/components/VideoPlayer/types.ts b/src/components/VideoPlayer/types.ts index 7aaa06126122..3dd3884534ec 100644 --- a/src/components/VideoPlayer/types.ts +++ b/src/components/VideoPlayer/types.ts @@ -30,7 +30,6 @@ type VideoPlayerProps = { controlsStatus?: ValueOf; shouldPlay?: boolean; isPreview?: boolean; - canToggleControlOnTap?: boolean; }; export type {VideoPlayerProps, VideoWithOnFullScreenUpdate}; From 1cb1e811582846ab9d8219960692f0bbd0816a5d Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jul 2024 00:44:48 +0700 Subject: [PATCH 055/420] fix type --- src/components/FeatureTrainingModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 7df83b423c70..68f9cafe12bb 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -139,7 +139,6 @@ function FeatureTrainingModal({ videoPlayerStyle={[styles.onboardingVideoPlayer, {aspectRatio}]} onVideoLoaded={setAspectRatio} controlsStatus={CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE} - canToggleControlOnTap shouldUseControlsBottomMargin={false} shouldPlay isLooping From f73f558d87d4dcf19ba415183303f6147896d007 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Jul 2024 01:05:55 +0700 Subject: [PATCH 056/420] fix lint --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index b93d4e13c78c..89a4753b9f7f 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -106,6 +106,7 @@ function BaseVideoPlayer({ }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumber]); const hideControl = useCallback(() => { + // eslint-disable-next-line react-compiler/react-compiler controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); }, [controlsOpacity]); const debouncedHideControl = useMemo(() => debounce(hideControl, 2000), [hideControl]); From d189427780e856e8118067cce5962b8661538223 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 17 Jul 2024 11:52:23 +0200 Subject: [PATCH 057/420] partial removal of selection debounce --- .../SelectionList/BaseSelectionList.tsx | 37 ++----------------- src/libs/actions/Task.ts | 3 ++ 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 6cc6b403dc7a..41d6fdc5388c 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -282,7 +282,7 @@ function BaseSelectionList( }, [onChangeText]); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - const debouncedOnSelectRow = useCallback(lodashDebounce(onSelectRow, 200), [onSelectRow]); + // const debouncedOnSelectRow = useCallback(lodashDebounce(onSelectRow, 200), [onSelectRow]); /** * Logic to run when a row is selected, either with click/press or keyboard hotkeys. @@ -309,11 +309,7 @@ function BaseSelectionList( } } - if (shouldDebounceRowSelect) { - debouncedOnSelectRow(item); - } else { - onSelectRow(item); - } + onSelectRow(item); if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && innerTextInputRef.current) { innerTextInputRef.current.focus(); @@ -338,11 +334,6 @@ function BaseSelectionList( selectRow(focusedOption); }; - // This debounce happens on the trailing edge because on repeated enter presses, rapid component state update cancels the existing debounce and the redundant - // enter presses runs the debounced function again. - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - const debouncedSelectFocusedOption = useCallback(lodashDebounce(selectFocusedOption, 100), [selectFocusedOption]); - /** * This function is used to compute the layout of any given item in our list. * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. @@ -507,26 +498,6 @@ function BaseSelectionList( [scrollToIndex, setFocusedIndex], ); - useEffect(() => { - if (!(shouldDebounceRowSelect && debouncedOnSelectRow.cancel)) { - return; - } - - return () => { - debouncedOnSelectRow.cancel(); - }; - }, [debouncedOnSelectRow, shouldDebounceRowSelect]); - - useEffect(() => { - if (!(shouldDebounceRowSelect && debouncedSelectFocusedOption.cancel)) { - return; - } - - return () => { - debouncedSelectFocusedOption.cancel(); - }; - }, [debouncedSelectFocusedOption, shouldDebounceRowSelect]); - /** Function to focus text input */ const focusTextInput = useCallback(() => { if (!innerTextInputRef.current) { @@ -621,7 +592,7 @@ function BaseSelectionList( useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect}), [scrollAndHighlightItem, clearInputAfterSelect]); /** Selects row when pressing Enter */ - useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, shouldDebounceRowSelect ? debouncedSelectFocusedOption : selectFocusedOption, { + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { captureOnInputs: true, shouldBubble: !flattenedSections.allOptions[focusedIndex], shouldStopPropagation, @@ -681,7 +652,7 @@ function BaseSelectionList( selectTextOnFocus spellCheck={false} iconLeft={textInputIconLeft} - onSubmitEditing={shouldDebounceRowSelect ? debouncedSelectFocusedOption : selectFocusedOption} + onSubmitEditing={selectFocusedOption} blurOnSubmit={!!flattenedSections.allOptions.length} isLoading={isLoadingNewOptions} testID="selection-list-text-input" diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 8dcb6128b143..e8c85fada74d 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -777,6 +777,7 @@ function setAssigneeValue( if (!isCurrentUser) { // Check for the chatReport by participants IDs if (!report) { + // here report = ReportUtils.getChatByParticipants([assigneeAccountID, currentUserAccountID]); } // If chat report is still not found we need to build new optimistic chat report @@ -784,6 +785,8 @@ function setAssigneeValue( report = setNewOptimisticAssignee(assigneeEmail, assigneeAccountID).assigneeReport; } + // here + // The optimistic field may not exist in the existing report and it can be overridden by the optimistic field of previous report data when merging the assignee chat report // Therefore, we should add these optimistic fields here to prevent incorrect merging, which could lead to the creation of duplicate actions for an existing report setAssigneeChatReport({ From 72b4f6f144eaf7d371ae7cea98c8dbd3c16d8079 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 17 Jul 2024 11:52:49 +0200 Subject: [PATCH 058/420] cleanup --- src/components/SelectionList/BaseSelectionList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 41d6fdc5388c..3ea3ba4365cd 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -1,5 +1,4 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import lodashDebounce from 'lodash/debounce'; import isEmpty from 'lodash/isEmpty'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; From 5739398ddc644337b009e7ab99198fe5fe703218 Mon Sep 17 00:00:00 2001 From: BrtqKr Date: Wed, 17 Jul 2024 13:34:56 +0200 Subject: [PATCH 059/420] apply useSingleExecution --- src/components/SelectionList/BaseSelectionList.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 3ea3ba4365cd..cb8c4dfc45e9 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -20,6 +20,7 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; +import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import getSectionsWithIndexOffset from '@libs/getSectionsWithIndexOffset'; import Log from '@libs/Log'; @@ -115,6 +116,7 @@ function BaseSelectionList( const [currentPage, setCurrentPage] = useState(1); const isTextInputFocusedRef = useRef(false); const isEmptyList = sections.length === 0; + const {singleExecution} = useSingleExecution(); const incrementPage = () => setCurrentPage((prev) => prev + 1); @@ -442,7 +444,7 @@ function BaseSelectionList( canSelectMultiple={canSelectMultiple} onLongPressRow={onLongPressRow} isMobileSelectionModeActive={isMobileSelectionModeActive} - onSelectRow={() => selectRow(item)} + onSelectRow={singleExecution(() => selectRow(item))} onCheckboxPress={handleOnCheckboxPress()} onDismissError={() => onDismissError?.(item)} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} From 9396b08d9acee62b8128826940ffe1303d0bc1be Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 19 Jul 2024 00:04:24 +0700 Subject: [PATCH 060/420] fix: drop user to main chat screen after onboarding --- src/components/OnboardingWelcomeVideo.tsx | 13 +++++++++++++ .../Navigators/OnboardingModalNavigator.tsx | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/OnboardingWelcomeVideo.tsx b/src/components/OnboardingWelcomeVideo.tsx index 47444d133166..b2776b541919 100644 --- a/src/components/OnboardingWelcomeVideo.tsx +++ b/src/components/OnboardingWelcomeVideo.tsx @@ -1,10 +1,17 @@ import React from 'react'; +import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import FeatureTrainingModal from './FeatureTrainingModal'; function OnboardingWelcomeVideo() { const {translate} = useLocalize(); + const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED); + const {isSmallScreenWidth} = useWindowDimensions(); return ( { + if (onboardingPurposeSelected !== CONST.ONBOARDING_CHOICES.EMPLOYER || !isSmallScreenWidth) { + return; + } + Navigation.goBack(ROUTES.HOME, true, true); + }} /> ); } diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 29a2205b2e37..1b36a739f17d 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -14,9 +14,9 @@ import OnboardingRefManager from '@libs/OnboardingRefManager'; import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails'; import OnboardingPurpose from '@pages/OnboardingPurpose'; import OnboardingWork from '@pages/OnboardingWork'; -import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import Overlay from './Overlay'; @@ -42,8 +42,8 @@ function OnboardingModalNavigator() { Navigation.isNavigationReady().then(() => { // Need to go back to previous route and then redirect to Concierge, // otherwise going back on Concierge will go to onboarding and then redirected to Concierge again - Navigation.goBack(); - Report.navigateToConciergeChat(); + Navigation.setShouldPopAllStateOnUP(true); + Navigation.goBack(ROUTES.HOME, true, true); }); }, [hasCompletedGuidedSetupFlow]); From 93e3d4549d375cac711606fc5a537187b47c1868 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:31:46 +0100 Subject: [PATCH 061/420] Support image pasting --- ...+0.73.4+018+Add-onPaste-to-TextInput.patch | 458 ++++++++++++++++++ ...-native+0.73.4+019+iOS-Image-Pasting.patch | 70 +++ src/components/Composer/index.native.tsx | 12 + src/components/Composer/types.ts | 3 +- .../{index.native.ts => index.android.ts} | 12 +- src/libs/Clipboard/index.ios.ts | 36 ++ src/libs/Clipboard/index.ts | 5 +- src/libs/Clipboard/types.ts | 5 +- 8 files changed, 597 insertions(+), 4 deletions(-) create mode 100644 patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch create mode 100644 patches/react-native+0.73.4+019+iOS-Image-Pasting.patch rename src/libs/Clipboard/{index.native.ts => index.android.ts} (57%) create mode 100644 src/libs/Clipboard/index.ios.ts diff --git a/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch b/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch new file mode 100644 index 000000000000..53acbd03e82e --- /dev/null +++ b/patches/react-native+0.73.4+018+Add-onPaste-to-TextInput.patch @@ -0,0 +1,458 @@ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +index 55b770d..7677336 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +@@ -464,6 +464,11 @@ export type NativeProps = $ReadOnly<{| + |}>, + >, + ++ /** ++ * Callback that is called when the clipboard content is pasted. ++ */ ++ onPaste?: ?DirectEventHandler<$ReadOnly<{|target: Int32|}>>, ++ + /** + * The string that will be rendered before text input has been entered. + */ +@@ -668,6 +673,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + topScroll: { + registrationName: 'onScroll', + }, ++ topPaste: { ++ registrationName: 'onPaste', ++ }, + }, + validAttributes: { + maxFontSizeMultiplier: true, +@@ -719,6 +727,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + textBreakStrategy: true, + onScroll: true, + onContentSizeChange: true, ++ onPaste: true, + disableFullscreenUI: true, + includeFontPadding: true, + fontWeight: true, +diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +index 88d3cc8..615c11f 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +@@ -97,6 +97,9 @@ const RCTTextInputViewConfig = { + topChangeSync: { + registrationName: 'onChangeSync', + }, ++ topPaste: { ++ registrationName: 'onPaste', ++ }, + }, + validAttributes: { + fontSize: true, +@@ -162,6 +165,7 @@ const RCTTextInputViewConfig = { + onSelectionChange: true, + onContentSizeChange: true, + onScroll: true, ++ onPaste: true, + onChangeSync: true, + onKeyPressSync: true, + onTextInput: true, +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +index 2c0c099..e5fbc79 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +@@ -804,6 +804,11 @@ export interface TextInputProps + | ((e: NativeSyntheticEvent) => void) + | undefined; + ++ /** ++ * Callback that is called when the clipboard content is pasted. ++ */ ++ onPaste?: ((e: NativeSyntheticEvent) => void) | undefined; ++ + /** + * The string that will be rendered before text input has been entered + */ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +index 9adbfe9..69ac611 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +@@ -796,6 +796,11 @@ export type Props = $ReadOnly<{| + */ + onScroll?: ?(e: ScrollEvent) => mixed, + ++ /** ++ * Callback that is called when the clipboard content is pasted. ++ */ ++ onPaste?: ?(e: TargetEvent) => mixed, ++ + /** + * The string that will be rendered before text input has been entered. + */ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +index 481938f..3f92f75 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +@@ -838,6 +838,11 @@ export type Props = $ReadOnly<{| + */ + onScroll?: ?(e: ScrollEvent) => mixed, + ++ /** ++ * Callback that is called when the clipboard content is pasted. ++ */ ++ onPaste?: ?(e: TargetEvent) => mixed, ++ + /** + * The string that will be rendered before text input has been entered. + */ +diff --git a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +index 582b49c..43c6c7d 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +@@ -167,6 +167,7 @@ - (void)paste:(id)sender + { + _textWasPasted = YES; + [super paste:sender]; ++ [_textInputDelegateAdapter didPaste]; + } + + // Turn off scroll animation to fix flaky scrolling. +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +index 7187177..aaa6a17 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +@@ -36,6 +36,7 @@ NS_ASSUME_NONNULL_BEGIN + - (void)textInputDidChange; + + - (void)textInputDidChangeSelection; ++- (void)textInputDidPaste; + + @optional + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +index f1c32e6..2b91f03 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +@@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN + + - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; + - (void)selectedTextRangeWasSet; ++- (void)didPaste; + + @end + +@@ -30,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN + - (instancetype)initWithTextView:(UITextView *)backedTextInputView; + + - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; ++- (void)didPaste; + + @end + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +index 9dca6a5..6814c57 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +@@ -147,6 +147,11 @@ - (void)selectedTextRangeWasSet + [self textFieldProbablyDidChangeSelection]; + } + ++- (void)didPaste ++{ ++ [_backedTextInputView.textInputDelegate textInputDidPaste]; ++} ++ + #pragma mark - Generalization + + - (void)textFieldProbablyDidChangeSelection +@@ -290,6 +295,11 @@ - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)tex + _previousSelectedTextRange = textRange; + } + ++- (void)didPaste ++{ ++ [_backedTextInputView.textInputDelegate textInputDidPaste]; ++} ++ + #pragma mark - Generalization + + - (void)textViewProbablyDidChangeSelection +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h +index 209947d..5092dbd 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h +@@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN + @property (nonatomic, copy, nullable) RCTDirectEventBlock onChangeSync; + @property (nonatomic, copy, nullable) RCTDirectEventBlock onTextInput; + @property (nonatomic, copy, nullable) RCTDirectEventBlock onScroll; ++@property (nonatomic, copy, nullable) RCTDirectEventBlock onPaste; + + @property (nonatomic, assign) NSInteger mostRecentEventCount; + @property (nonatomic, assign, readonly) NSInteger nativeEventCount; +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +index b0d71dc..f64dd69 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +@@ -562,6 +562,14 @@ - (void)textInputDidChangeSelection + }); + } + ++- (void)textInputDidPaste ++{ ++ if (!_onPaste) { ++ return; ++ } ++ _onPaste(@{@"target" : self.reactTag}); ++} ++ + - (void)updateLocalData + { + [self enforceTextAttributesIfNeeded]; +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +index a19b555..8146c0d 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +@@ -66,6 +66,7 @@ @implementation RCTBaseTextInputViewManager { + RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) ++RCT_EXPORT_VIEW_PROPERTY(onPaste, RCTDirectEventBlock) + + RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +index 4d0afd9..10009ff 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +@@ -205,6 +205,7 @@ - (void)paste:(id)sender + { + _textWasPasted = YES; + [super paste:sender]; ++ [_textInputDelegateAdapter didPaste]; + } + + #pragma mark - Layout +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +index 7ce04da..97c502d 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +@@ -426,6 +426,13 @@ - (void)textInputDidChangeSelection + } + } + ++- (void)textInputDidPaste ++{ ++ if (_eventEmitter) { ++ static_cast(*_eventEmitter).onPaste(); ++ } ++} ++ + #pragma mark - RCTBackedTextInputDelegate (UIScrollViewDelegate) + + - (void)scrollViewDidScroll:(UIScrollView *)scrollView +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java +new file mode 100644 +index 0000000..7eacc51 +--- /dev/null ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java +@@ -0,0 +1,17 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++package com.facebook.react.views.textinput; ++ ++/** ++ * Implement this interface to be informed of paste event in the ++ * ReactTextEdit This is used by the ReactTextInputManager to forward events ++ * from the EditText to JS ++ */ ++interface PasteWatcher { ++ public void onPaste(); ++} +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +index 081f2b8..98c0f0d 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +@@ -110,6 +110,7 @@ public class ReactEditText extends AppCompatEditText { + private @Nullable SelectionWatcher mSelectionWatcher; + private @Nullable ContentSizeWatcher mContentSizeWatcher; + private @Nullable ScrollWatcher mScrollWatcher; ++ private @Nullable PasteWatcher mPasteWatcher; + private InternalKeyListener mKeyListener; + private boolean mDetectScrollMovement = false; + private boolean mOnKeyPress = false; +@@ -153,6 +154,7 @@ public class ReactEditText extends AppCompatEditText { + mKeyListener = new InternalKeyListener(); + } + mScrollWatcher = null; ++ mPasteWatcher = null; + mTextAttributes = new TextAttributes(); + + applyTextAttributes(); +@@ -307,10 +309,15 @@ public class ReactEditText extends AppCompatEditText { + */ + @Override + public boolean onTextContextMenuItem(int id) { +- if (id == android.R.id.paste) { ++ if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + id = android.R.id.pasteAsPlainText; +- } else { ++ boolean actionPerformed = super.onTextContextMenuItem(id); ++ if (mPasteWatcher != null) { ++ mPasteWatcher.onPaste(); ++ } ++ return actionPerformed; ++ } else if (id == android.R.id.paste) { + ClipboardManager clipboard = + (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData previousClipData = clipboard.getPrimaryClip(); +@@ -389,6 +396,10 @@ public class ReactEditText extends AppCompatEditText { + mScrollWatcher = scrollWatcher; + } + ++ public void setPasteWatcher(PasteWatcher pasteWatcher) { ++ mPasteWatcher = pasteWatcher; ++ } ++ + /** + * Attempt to set a selection or fail silently. Intentionally meant to handle bad inputs. + * EventCounter is the same one used as with text. +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +index 8496a7d..deb54d4 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +@@ -273,6 +273,9 @@ public class ReactTextInputManager extends BaseViewManager { ++ ++ private static final String EVENT_NAME = "topPaste"; ++ ++ @Deprecated ++ public ReactTextInputPasteEvent(int viewId) { ++ this(ViewUtil.NO_SURFACE_ID, viewId); ++ } ++ ++ public ReactTextInputPasteEvent(int surfaceId, int viewId) { ++ super(surfaceId, viewId); ++ } ++ ++ @Override ++ public String getEventName() { ++ return EVENT_NAME; ++ } ++ ++ @Override ++ public boolean canCoalesce() { ++ return false; ++ } ++ ++ @Nullable ++ @Override ++ protected WritableMap getEventData() { ++ WritableMap eventData = Arguments.createMap(); ++ return eventData; ++ } ++} +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp +index 497569a..c6adbc3 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp +@@ -193,6 +193,10 @@ void TextInputEventEmitter::onScroll( + }); + } + ++void TextInputEventEmitter::onPaste() const { ++ dispatchEvent("onPaste"); ++} ++ + void TextInputEventEmitter::dispatchTextInputEvent( + const std::string& name, + const TextInputMetrics& textInputMetrics, +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h +index 0ab2b18..6373012 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h +@@ -47,6 +47,7 @@ class TextInputEventEmitter : public ViewEventEmitter { + void onKeyPress(const KeyPressMetrics& keyPressMetrics) const; + void onKeyPressSync(const KeyPressMetrics& keyPressMetrics) const; + void onScroll(const TextInputMetrics& textInputMetrics) const; ++ void onPaste() const; + + private: + void dispatchTextInputEvent( diff --git a/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch b/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch new file mode 100644 index 000000000000..3b681679c470 --- /dev/null +++ b/patches/react-native+0.73.4+019+iOS-Image-Pasting.patch @@ -0,0 +1,70 @@ +diff --git a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +index 43c6c7d..5904762 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +@@ -13,6 +13,8 @@ + #import + #import + ++#import ++ + @implementation RCTUITextView { + UILabel *_placeholderView; + UITextView *_detachedTextView; +@@ -166,7 +168,9 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO + - (void)paste:(id)sender + { + _textWasPasted = YES; +- [super paste:sender]; ++ if (![UIPasteboard generalPasteboard].hasImages) { ++ [super paste:sender]; ++ } + [_textInputDelegateAdapter didPaste]; + } + +@@ -259,6 +263,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender + return NO; + } + ++ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { ++ return YES; ++ } ++ + return [super canPerformAction:action withSender:sender]; + } + +diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +index 10009ff..273bc0a 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +@@ -12,6 +12,8 @@ + #import + #import + ++#import ++ + @implementation RCTUITextField { + RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; + NSDictionary *_defaultTextAttributes; +@@ -139,6 +141,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender + return NO; + } + ++ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { ++ return YES; ++ } ++ + return [super canPerformAction:action withSender:sender]; + } + +@@ -204,7 +210,9 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO + - (void)paste:(id)sender + { + _textWasPasted = YES; +- [super paste:sender]; ++ if (![UIPasteboard generalPasteboard].hasImages) { ++ [super paste:sender]; ++ } + [_textInputDelegateAdapter didPaste]; + } + diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index c5f2e07eef80..13290a03bd5e 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -10,6 +10,7 @@ import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Clipboard from '@libs/Clipboard'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import * as EmojiUtils from '@libs/EmojiUtils'; import type {ComposerProps} from './types'; @@ -21,6 +22,7 @@ function Composer( { shouldClear = false, onClear = () => {}, + onPasteFile = () => {}, isDisabled = false, maxLines, isComposerFullSize = false, @@ -64,6 +66,15 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); + const pasteImage = useCallback(() => { + Clipboard.getImage().then((image) => { + if (!image) { + return; + } + onPasteFile(image); + }); + }, [onPasteFile]); + useEffect(() => { if (!shouldClear) { return; @@ -92,6 +103,7 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} + onPaste={pasteImage} onBlur={(e) => { if (!isFocused) { // eslint-disable-next-line react-compiler/react-compiler diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 9c7a5a215c1c..543649c2cca0 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,4 +1,5 @@ import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; +import {FileObject} from '@components/AttachmentModal'; type TextSelection = { start: number; @@ -31,7 +32,7 @@ type ComposerProps = TextInputProps & { onChangeText?: (numberOfLines: string) => void; /** Callback method to handle pasting a file */ - onPasteFile?: (file: File) => void; + onPasteFile?: (file: FileObject) => void; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types diff --git a/src/libs/Clipboard/index.native.ts b/src/libs/Clipboard/index.android.ts similarity index 57% rename from src/libs/Clipboard/index.native.ts rename to src/libs/Clipboard/index.android.ts index 2c345327a106..bb3cb22ab66f 100644 --- a/src/libs/Clipboard/index.native.ts +++ b/src/libs/Clipboard/index.android.ts @@ -1,5 +1,5 @@ import Clipboard from '@react-native-clipboard/clipboard'; -import type {CanSetHtml, SetHtml, SetString} from './types'; +import type {CanSetHtml, GetImage, SetHtml, SetString} from './types'; /** * Sets a string on the Clipboard object via @react-native-clipboard/clipboard @@ -12,8 +12,18 @@ const setString: SetString = (text) => { const canSetHtml: CanSetHtml = () => false; const setHtml: SetHtml = () => {}; +const getImage: GetImage = () => { + return Clipboard.getImage().then((imageb64) => { + if (!imageb64) { + return undefined; + } + return {uri: imageb64, name: 'image.png', type: 'image/png'}; + }); +}; + export default { setString, canSetHtml, setHtml, + getImage, }; diff --git a/src/libs/Clipboard/index.ios.ts b/src/libs/Clipboard/index.ios.ts new file mode 100644 index 000000000000..c214bdfc76e6 --- /dev/null +++ b/src/libs/Clipboard/index.ios.ts @@ -0,0 +1,36 @@ +import Clipboard from '@react-native-clipboard/clipboard'; +import type {CanSetHtml, GetImage, SetHtml, SetString} from './types'; + +/** + * Sets a string on the Clipboard object via @react-native-clipboard/clipboard + */ +const setString: SetString = (text) => { + Clipboard.setString(text); +}; + +// We don't want to set HTML on native platforms so noop them. +const canSetHtml: CanSetHtml = () => false; +const setHtml: SetHtml = () => {}; + +const getImage: GetImage = () => { + return Clipboard.hasImage() + .then((hasImage) => { + if (!hasImage) { + return undefined; + } + return Clipboard.getImagePNG(); + }) + .then((imageb64) => { + if (!imageb64) { + return undefined; + } + return {uri: imageb64, name: 'image.png', type: 'image/png'}; + }); +}; + +export default { + setString, + canSetHtml, + setHtml, + getImage, +}; diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index 130aad270b92..a03e406ec9c0 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -1,7 +1,7 @@ import Clipboard from '@react-native-clipboard/clipboard'; import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; -import type {CanSetHtml, SetHtml, SetString} from './types'; +import type {CanSetHtml, GetImage, SetHtml, SetString} from './types'; type ComposerSelection = { start: number; @@ -136,8 +136,11 @@ const setString: SetString = (text) => { Clipboard.setString(text); }; +const getImage: GetImage = () => Promise.reject('getImage not supported on web'); + export default { setString, canSetHtml, setHtml, + getImage, }; diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts index 1d899144a2ba..e54cf60df635 100644 --- a/src/libs/Clipboard/types.ts +++ b/src/libs/Clipboard/types.ts @@ -1,5 +1,8 @@ +import {FileObject} from '@components/AttachmentModal'; + type SetString = (text: string) => void; type SetHtml = (html: string, text: string) => void; type CanSetHtml = (() => (...args: ClipboardItems) => Promise) | (() => boolean); +type GetImage = () => Promise; -export type {SetString, CanSetHtml, SetHtml}; +export type {SetString, CanSetHtml, SetHtml, GetImage}; From 1384fe1b59316c3e61d401f10da26969180c247d Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:07:12 +0200 Subject: [PATCH 062/420] fix isPolicyAdmin not being properly implemented in ReportUtils check --- src/libs/ReportUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e8d5697de64b..35a008cdeb45 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2821,11 +2821,12 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry) typeof currentUserPersonalDetails?.accountID === 'number' && parentReportAction.actorAccountID === currentUserPersonalDetails?.accountID; const isApprover = isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && currentUserPersonalDetails?.accountID === moneyRequestReport?.managerID; + const isAdmin = isPolicyAdmin(moneyRequestReport.policyID ?? '-1', allPolicies); const isOnHold = TransactionUtils.isOnHold(transaction); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); const isClosed = isClosedReport(moneyRequestReport); - const canModifyStatus = !isTrackExpenseMoneyReport && (isPolicyAdmin || isActionOwner || isApprover); + const canModifyStatus = !isTrackExpenseMoneyReport && (isAdmin || isActionOwner || isApprover); const isDeletedParentAction = isEmptyObject(parentReportAction) || ReportActionsUtils.isDeletedAction(parentReportAction); const canHoldOrUnholdRequest = !isRequestSettled && !isApproved && !isDeletedParentAction && !isClosed; From ade4976c6cb38c724ea228e0d78728fda3f09700 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:13:53 +0200 Subject: [PATCH 063/420] fix prettier --- src/libs/ReportUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f92cfadb229e..914c4a2082bf 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2829,7 +2829,9 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry) const canHoldOrUnholdRequest = !isRequestSettled && !isApproved && !isDeletedParentAction && !isClosed; const canHoldRequest = canHoldOrUnholdRequest && !isOnHold && (isRequestIOU || canModifyStatus) && !isScanning && !!transaction?.reimbursable; - const canUnholdRequest = !!(canHoldOrUnholdRequest && isOnHold && !TransactionUtils.isDuplicate(transaction.transactionID, true) && (isRequestHoldCreator || (!isRequestIOU && canModifyStatus))) && !!transaction?.reimbursable; + const canUnholdRequest = + !!(canHoldOrUnholdRequest && isOnHold && !TransactionUtils.isDuplicate(transaction.transactionID, true) && (isRequestHoldCreator || (!isRequestIOU && canModifyStatus))) && + !!transaction?.reimbursable; return {canHoldRequest, canUnholdRequest}; } From c8dce56a27ca755c0b8d41b94020aaa6a1da61e7 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:25:24 +0200 Subject: [PATCH 064/420] fix lint errors after merge --- src/pages/ReportDetailsPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 6dc207cf743c..cea6358fe7cb 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -183,8 +183,6 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD return report; }, [caseID, parentReport, report]); - const moneyRequestAction = transactionThreadReportID ? requestParentReportAction : parentReportAction; - const canModifyTask = Task.canModifyTask(report, session?.accountID ?? -1); const shouldShowTaskDeleteButton = isTaskReport && @@ -525,8 +523,9 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD } else if (caseID === CASES.MONEY_REPORT) { holdReportAction = requestParentReportAction; } + const canHoldUnholdReportAction = ReportUtils.canHoldUnholdReportAction(holdReportAction); - const shouldShowHoldAction = (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) && !ReportUtils.isArchivedRoom(parentReport); + const shouldShowHoldAction = (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) && !ReportUtils.isArchivedRoom(transactionThreadReportID ? report : parentReport, parentReportNameValuePairs); const canJoin = ReportUtils.canJoinChat(report, parentReportAction, policy); From c4289b496243c65c9cceff7795c75433927e75e3 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:26:30 +0200 Subject: [PATCH 065/420] clean up code --- src/pages/ReportDetailsPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index cea6358fe7cb..73a2011c3d12 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -525,7 +525,9 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD } const canHoldUnholdReportAction = ReportUtils.canHoldUnholdReportAction(holdReportAction); - const shouldShowHoldAction = (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) && !ReportUtils.isArchivedRoom(transactionThreadReportID ? report : parentReport, parentReportNameValuePairs); + const shouldShowHoldAction = + (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) && + !ReportUtils.isArchivedRoom(transactionThreadReportID ? report : parentReport, parentReportNameValuePairs); const canJoin = ReportUtils.canJoinChat(report, parentReportAction, policy); From a6dee6d29503dfed581092387ed56b1d97a6beb6 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:24:40 +0100 Subject: [PATCH 066/420] lint + ts --- src/components/Composer/types.ts | 2 +- src/libs/Clipboard/index.android.ts | 5 ++--- src/libs/Clipboard/index.ios.ts | 5 ++--- src/libs/Clipboard/index.ts | 2 +- src/libs/Clipboard/types.ts | 2 +- src/stories/Composer.stories.tsx | 5 +++-- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 543649c2cca0..a2d062021205 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,5 +1,5 @@ import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; -import {FileObject} from '@components/AttachmentModal'; +import type {FileObject} from '@components/AttachmentModal'; type TextSelection = { start: number; diff --git a/src/libs/Clipboard/index.android.ts b/src/libs/Clipboard/index.android.ts index bb3cb22ab66f..6e412441b194 100644 --- a/src/libs/Clipboard/index.android.ts +++ b/src/libs/Clipboard/index.android.ts @@ -12,14 +12,13 @@ const setString: SetString = (text) => { const canSetHtml: CanSetHtml = () => false; const setHtml: SetHtml = () => {}; -const getImage: GetImage = () => { - return Clipboard.getImage().then((imageb64) => { +const getImage: GetImage = () => + Clipboard.getImage().then((imageb64) => { if (!imageb64) { return undefined; } return {uri: imageb64, name: 'image.png', type: 'image/png'}; }); -}; export default { setString, diff --git a/src/libs/Clipboard/index.ios.ts b/src/libs/Clipboard/index.ios.ts index c214bdfc76e6..4b2ba52b9672 100644 --- a/src/libs/Clipboard/index.ios.ts +++ b/src/libs/Clipboard/index.ios.ts @@ -12,8 +12,8 @@ const setString: SetString = (text) => { const canSetHtml: CanSetHtml = () => false; const setHtml: SetHtml = () => {}; -const getImage: GetImage = () => { - return Clipboard.hasImage() +const getImage: GetImage = () => + Clipboard.hasImage() .then((hasImage) => { if (!hasImage) { return undefined; @@ -26,7 +26,6 @@ const getImage: GetImage = () => { } return {uri: imageb64, name: 'image.png', type: 'image/png'}; }); -}; export default { setString, diff --git a/src/libs/Clipboard/index.ts b/src/libs/Clipboard/index.ts index a03e406ec9c0..fb0f00e6eb84 100644 --- a/src/libs/Clipboard/index.ts +++ b/src/libs/Clipboard/index.ts @@ -136,7 +136,7 @@ const setString: SetString = (text) => { Clipboard.setString(text); }; -const getImage: GetImage = () => Promise.reject('getImage not supported on web'); +const getImage: GetImage = () => Promise.reject(new Error('getImage not supported on web')); export default { setString, diff --git a/src/libs/Clipboard/types.ts b/src/libs/Clipboard/types.ts index e54cf60df635..c04cf952f1f7 100644 --- a/src/libs/Clipboard/types.ts +++ b/src/libs/Clipboard/types.ts @@ -1,4 +1,4 @@ -import {FileObject} from '@components/AttachmentModal'; +import type {FileObject} from '@components/AttachmentModal'; type SetString = (text: string) => void; type SetHtml = (html: string, text: string) => void; diff --git a/src/stories/Composer.stories.tsx b/src/stories/Composer.stories.tsx index 805f2b4c7448..a92dc0e789a0 100644 --- a/src/stories/Composer.stories.tsx +++ b/src/stories/Composer.stories.tsx @@ -3,6 +3,7 @@ import type {Meta} from '@storybook/react'; import {ExpensiMark} from 'expensify-common'; import React, {useState} from 'react'; import {Image, View} from 'react-native'; +import type {FileObject} from '@components/AttachmentModal'; import Composer from '@components/Composer'; import type {ComposerProps} from '@components/Composer/types'; import RenderHTML from '@components/RenderHTML'; @@ -29,7 +30,7 @@ const parser = new ExpensiMark(); function Default(props: ComposerProps) { const StyleUtils = useStyleUtils(); - const [pastedFile, setPastedFile] = useState(null); + const [pastedFile, setPastedFile] = useState(null); const [comment, setComment] = useState(props.defaultValue); const renderedHTML = parser.replace(comment ?? ''); @@ -53,7 +54,7 @@ function Default(props: ComposerProps) { Rendered Comment {!!renderedHTML && } - {!!pastedFile && ( + {!!pastedFile && pastedFile instanceof File && ( Date: Mon, 22 Jul 2024 09:24:53 +0200 Subject: [PATCH 067/420] cleanup --- .../CurrencySelectionList/index.tsx | 2 +- .../MoneyRequestConfirmationList.tsx | 2 +- src/components/Search/index.tsx | 2 +- .../SelectionList/BaseSelectionList.tsx | 13 ++++++++----- src/components/SelectionList/types.ts | 19 ++++++++++--------- src/libs/actions/Task.ts | 3 --- src/pages/ChatFinderPage/index.tsx | 2 +- src/pages/NewChatPage.tsx | 2 +- .../BusinessTypeSelectorModal.tsx | 2 +- .../ReportParticipantRoleSelectionPage.tsx | 2 +- src/pages/ReportParticipantsPage.tsx | 2 +- .../MoneyRequestParticipantsSelector.tsx | 2 +- .../step/IOURequestStepDistanceRate.tsx | 2 +- .../request/step/IOURequestStepSendFrom.tsx | 2 +- .../request/step/IOURequestStepSplitPayer.tsx | 2 +- .../ShareLogList/BaseShareLogList.tsx | 2 +- .../settings/Preferences/LanguagePage.tsx | 2 +- .../settings/Preferences/PriorityModePage.tsx | 2 +- src/pages/settings/Preferences/ThemePage.tsx | 2 +- .../PersonalDetails/CountrySelectionPage.tsx | 2 +- .../PersonalDetails/StateSelectionPage.tsx | 2 +- src/pages/settings/Profile/PronounsPage.tsx | 2 +- .../settings/Profile/TimezoneSelectPage.tsx | 2 +- .../Report/NotificationPreferencePage.tsx | 2 +- src/pages/settings/Report/VisibilityPage.tsx | 2 +- .../settings/Report/WriteCapabilityPage.tsx | 2 +- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 2 +- .../TaskShareDestinationSelectorModal.tsx | 2 +- src/pages/workspace/WorkspaceMembersPage.tsx | 2 +- .../NetSuiteCustomListSelectorModal.tsx | 2 +- .../advanced/QuickbooksAccountSelectPage.tsx | 2 +- .../QuickbooksInvoiceAccountSelectPage.tsx | 2 +- ...ompanyCardExpenseAccountSelectCardPage.tsx | 2 +- ...oksCompanyCardExpenseAccountSelectPage.tsx | 2 +- .../export/QuickbooksExportDateSelectPage.tsx | 2 +- ...ickbooksExportInvoiceAccountSelectPage.tsx | 2 +- ...NonReimbursableDefaultVendorSelectPage.tsx | 2 +- ...oksOutOfPocketExpenseAccountSelectPage.tsx | 2 +- ...ooksOutOfPocketExpenseEntitySelectPage.tsx | 2 +- ...ooksPreferredExporterConfigurationPage.tsx | 2 +- ...roMapTrackingCategoryConfigurationPage.tsx | 2 +- .../XeroOrganizationConfigurationPage.tsx | 2 +- .../workspace/card/issueNew/LimitTypeStep.tsx | 2 +- ...rkspaceMemberDetailsRoleSelectionModal.tsx | 2 +- .../ReportFieldsListValuesPage.tsx | 1 - .../workspace/tags/WorkspaceTagsPage.tsx | 2 +- ...orkspaceAutoReportingMonthlyOffsetPage.tsx | 2 +- .../WorkspaceWorkflowsApproverPage.tsx | 2 +- .../workflows/WorkspaceWorkflowsPayerPage.tsx | 2 +- 49 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index bfe4578afd0f..b2f48e2b0f9a 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -48,7 +48,7 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, textInputValue={searchValue} onChangeText={setSearchValue} onSelectRow={onSelect} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect headerMessage={headerMessage} initiallyFocusedOptionKey={initiallySelectedCurrencyCode} showScrollIndicator diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index ae8cbe4298f2..047b341eac90 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -877,7 +877,7 @@ function MoneyRequestConfirmationList({ sections={sections} ListItem={UserListItem} onSelectRow={navigateToReportOrUserDetail} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect canSelectMultiple={false} shouldPreventDefaultFocusOnSelectRow footerContent={footerContent} diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 78992496f031..73dfde10439d 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -214,7 +214,7 @@ function Search({query, policyIDs, sortBy, sortOrder, isMobileSelectionModeActiv ListItem={ListItem} onSelectRow={openReport} getItemHeight={getItemHeightMemoized} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph8, styles.pv3, styles.pb5]} containerStyle={[styles.pv0]} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index cb8c4dfc45e9..72776554024c 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -40,7 +40,7 @@ function BaseSelectionList( shouldUseUserSkeletonView, canSelectMultiple = false, onSelectRow, - shouldDebounceRowSelect = false, + shouldSingleExecuteRowSelect = false, onCheckboxPress, onSelectAll, onDismissError, @@ -282,9 +282,6 @@ function BaseSelectionList( onChangeText?.(''); }, [onChangeText]); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - // const debouncedOnSelectRow = useCallback(lodashDebounce(onSelectRow, 200), [onSelectRow]); - /** * Logic to run when a row is selected, either with click/press or keyboard hotkeys. * @@ -444,7 +441,13 @@ function BaseSelectionList( canSelectMultiple={canSelectMultiple} onLongPressRow={onLongPressRow} isMobileSelectionModeActive={isMobileSelectionModeActive} - onSelectRow={singleExecution(() => selectRow(item))} + onSelectRow={() => { + if (shouldSingleExecuteRowSelect) { + singleExecution(() => selectRow(item)); + } else { + selectRow(item); + } + }} onCheckboxPress={handleOnCheckboxPress()} onDismissError={() => onDismissError?.(item)} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 723e48699b3c..a31388322edc 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,12 +1,12 @@ -import type {MutableRefObject, ReactElement, ReactNode} from 'react'; -import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native'; -import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; +import type { MutableRefObject, ReactElement, ReactNode } from 'react'; +import type { GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle } from 'react-native'; +import type { BrickRoad } from '@libs/WorkspacesSettingsUtils'; // eslint-disable-next-line no-restricted-imports import type CursorStyles from '@styles/utils/cursor/types'; import type CONST from '@src/CONST'; -import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {SearchAccountDetails, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; -import type {ReceiptErrors} from '@src/types/onyx/Transaction'; +import type { Errors, Icon, PendingAction } from '@src/types/onyx/OnyxCommon'; +import type { SearchAccountDetails, SearchReport, SearchTransaction } from '@src/types/onyx/SearchResults'; +import type { ReceiptErrors } from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type IconAsset from '@src/types/utils/IconAsset'; import type InviteMemberListItem from './InviteMemberListItem'; @@ -16,6 +16,7 @@ import type TransactionListItem from './Search/TransactionListItem'; import type TableListItem from './TableListItem'; import type UserListItem from './UserListItem'; + type TRightHandSideComponent = { /** Component to display on the right side */ rightHandSideComponent?: ((item: TItem) => ReactElement | null | undefined) | ReactElement | null; @@ -302,8 +303,8 @@ type BaseSelectionListProps = Partial & { /** Callback to fire when a row is pressed */ onSelectRow: (item: TItem) => void; - /** Whether to debounce `onRowSelect` */ - shouldDebounceRowSelect?: boolean; + /** Whether to single execution `onRowSelect` - workaround for unintentional multiple navigation calls https://github.com/Expensify/App/issues/44443 */ + shouldSingleExecuteRowSelect?: boolean; /** Optional callback function triggered upon pressing a checkbox. If undefined and the list displays checkboxes, checkbox interactions are managed by onSelectRow, allowing for pressing anywhere on the list. */ onCheckboxPress?: (item: TItem) => void; @@ -532,4 +533,4 @@ export type { TransactionListItemType, UserListItemProps, ValidListItem, -}; +}; \ No newline at end of file diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index e8c85fada74d..8dcb6128b143 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -777,7 +777,6 @@ function setAssigneeValue( if (!isCurrentUser) { // Check for the chatReport by participants IDs if (!report) { - // here report = ReportUtils.getChatByParticipants([assigneeAccountID, currentUserAccountID]); } // If chat report is still not found we need to build new optimistic chat report @@ -785,8 +784,6 @@ function setAssigneeValue( report = setNewOptimisticAssignee(assigneeEmail, assigneeAccountID).assigneeReport; } - // here - // The optimistic field may not exist in the existing report and it can be overridden by the optimistic field of previous report data when merging the assignee chat report // Therefore, we should add these optimistic fields here to prevent incorrect merging, which could lead to the creation of duplicate actions for an existing report setAssigneeChatReport({ diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx index cb3d0b1eafd9..1ae1a54dcafd 100644 --- a/src/pages/ChatFinderPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -188,7 +188,7 @@ function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPa headerMessage={headerMessage} onLayout={setPerformanceTimersEnd} onSelectRow={selectReport} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect showLoadingPlaceholder={!areOptionsInitialized || !isScreenTransitionEnd} footerContent={!isDismissed && ChatFinderPageFooterInstance} isLoadingNewOptions={!!isSearchingForReports} diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 17baddd39251..33c345c12ea3 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -337,7 +337,7 @@ function NewChatPage({isGroupChat}: NewChatPageProps) { textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')} headerMessage={headerMessage} onSelectRow={createChat} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect onConfirm={(e, option) => (selectedOptions.length > 0 ? createGroup() : createChat(option))} rightHandSideComponent={itemRightSideComponent} footerContent={footerContent} diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx index a3fc3737dbf2..45db53a23286 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx @@ -65,7 +65,7 @@ function BusinessTypeSelectorModal({isVisible, currentBusinessType, onBusinessTy sections={[{data: incorporationTypes}]} initiallyFocusedOptionKey={currentBusinessType} onSelectRow={onBusinessTypeSelected} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect shouldStopPropagation shouldUseDynamicMaxToRenderPerBatch ListItem={RadioListItem} diff --git a/src/pages/ReportParticipantRoleSelectionPage.tsx b/src/pages/ReportParticipantRoleSelectionPage.tsx index d6f3eb9e4603..d2fb5ca365a0 100644 --- a/src/pages/ReportParticipantRoleSelectionPage.tsx +++ b/src/pages/ReportParticipantRoleSelectionPage.tsx @@ -68,7 +68,7 @@ function ReportParticipantRoleSelectionPage({report, route}: ReportParticipantRo sections={[{data: items}]} ListItem={RadioListItem} onSelectRow={changeRole} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={items.find((item) => item.isSelected)?.keyForList} /> diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 15dd063ec6ee..5eebf8c9a4d8 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -353,7 +353,7 @@ function ReportParticipantsPage({report, personalDetails, session}: ReportPartic ListItem={TableListItem} headerContent={headerContent} onSelectRow={openMemberDetails} - shouldDebounceRowSelect={!(isGroupChat && isCurrentUserAdmin)} + shouldSingleExecuteRowSelect={!(isGroupChat && isCurrentUserAdmin)} onCheckboxPress={(item) => toggleUser(item.accountID)} onSelectAll={() => toggleAllUsers(participants)} showScrollIndicator diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 5f10b403f5e9..019326851437 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -421,7 +421,7 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF onChangeText={setSearchTerm} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} onSelectRow={onSelectRow} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect footerContent={footerContent} headerMessage={header} showLoadingPlaceholder={!areOptionsInitialized || !didScreenTransitionEnd} diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 4f70d3e4fee9..c68c0606d62c 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -102,7 +102,7 @@ function IOURequestStepDistanceRate({ sections={[{data: sections}]} ListItem={RadioListItem} onSelectRow={({value}) => selectDistanceRate(value ?? '')} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={initiallyFocusedOption} /> diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index 74a1370fb00c..7ff4124d0871 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -90,7 +90,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte diff --git a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx index 35adfb9fb815..8236a780d970 100644 --- a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx +++ b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx @@ -87,7 +87,7 @@ function IOURequestStepSplitPayer({ sections={sections} ListItem={UserListItem} onSelectRow={setSplitPayer} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect showLoadingPlaceholder={!didScreenTransitionEnd} /> diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index fca23d1e1cb9..d51da52d7f38 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -105,7 +105,7 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { ListItem={UserListItem} sections={didScreenTransitionEnd ? sections : CONST.EMPTY_ARRAY} onSelectRow={attachLogToReport} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect onChangeText={setSearchValue} textInputValue={searchValue} headerMessage={searchOptions.headerMessage} diff --git a/src/pages/settings/Preferences/LanguagePage.tsx b/src/pages/settings/Preferences/LanguagePage.tsx index f5c2cb4097af..dbc1440a5e67 100644 --- a/src/pages/settings/Preferences/LanguagePage.tsx +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -31,7 +31,7 @@ function LanguagePage() { sections={[{data: localesToLanguages}]} ListItem={RadioListItem} onSelectRow={(language) => App.setLocaleAndNavigate(language.value)} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={localesToLanguages.find((locale) => locale.isSelected)?.keyForList} /> diff --git a/src/pages/settings/Preferences/PriorityModePage.tsx b/src/pages/settings/Preferences/PriorityModePage.tsx index aef727e448f6..57f565e7dca7 100644 --- a/src/pages/settings/Preferences/PriorityModePage.tsx +++ b/src/pages/settings/Preferences/PriorityModePage.tsx @@ -58,7 +58,7 @@ function PriorityModePage() { sections={[{data: priorityModes}]} ListItem={RadioListItem} onSelectRow={updateMode} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={priorityModes.find((mode) => mode.isSelected)?.keyForList} /> diff --git a/src/pages/settings/Preferences/ThemePage.tsx b/src/pages/settings/Preferences/ThemePage.tsx index c46e0f2abaaa..bd14c076bfb3 100644 --- a/src/pages/settings/Preferences/ThemePage.tsx +++ b/src/pages/settings/Preferences/ThemePage.tsx @@ -49,7 +49,7 @@ function ThemePage({preferredTheme}: ThemePageProps) { sections={[{data: localesToThemes}]} ListItem={RadioListItem} onSelectRow={(theme) => User.updateTheme(theme.value)} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={localesToThemes.find((theme) => theme.isSelected)?.keyForList} /> diff --git a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx index 249698e1916e..080d53dd8573 100644 --- a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx @@ -80,7 +80,7 @@ function CountrySelectionPage({route, navigation}: CountrySelectionPageProps) { sections={[{data: searchResults}]} ListItem={RadioListItem} onSelectRow={selectCountry} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect onChangeText={setSearchValue} initiallyFocusedOptionKey={currentCountry} shouldUseDynamicMaxToRenderPerBatch diff --git a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx index 2fad9452bd38..c04e631bd13f 100644 --- a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx @@ -105,7 +105,7 @@ function StateSelectionPage() { diff --git a/src/pages/settings/Profile/TimezoneSelectPage.tsx b/src/pages/settings/Profile/TimezoneSelectPage.tsx index fdc10b72ad43..326db5481d37 100644 --- a/src/pages/settings/Profile/TimezoneSelectPage.tsx +++ b/src/pages/settings/Profile/TimezoneSelectPage.tsx @@ -72,7 +72,7 @@ function TimezoneSelectPage({currentUserPersonalDetails}: TimezoneSelectPageProp textInputValue={timezoneInputText} onChangeText={filterShownTimezones} onSelectRow={saveSelectedTimezone} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect sections={[{data: timezoneOptions, isDisabled: timezone.automatic}]} initiallyFocusedOptionKey={timezoneOptions.find((tz) => tz.text === timezone.selected)?.keyForList} showScrollIndicator diff --git a/src/pages/settings/Report/NotificationPreferencePage.tsx b/src/pages/settings/Report/NotificationPreferencePage.tsx index 9e50644cce99..ff3cf90382c0 100644 --- a/src/pages/settings/Report/NotificationPreferencePage.tsx +++ b/src/pages/settings/Report/NotificationPreferencePage.tsx @@ -47,7 +47,7 @@ function NotificationPreferencePage({report}: NotificationPreferencePageProps) { onSelectRow={(option) => report && ReportActions.updateNotificationPreference(report.reportID, report.notificationPreference, option.value, true, undefined, undefined, report) } - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={notificationPreferenceOptions.find((locale) => locale.isSelected)?.keyForList} /> diff --git a/src/pages/settings/Report/VisibilityPage.tsx b/src/pages/settings/Report/VisibilityPage.tsx index 76ba076138c8..6cdb8a8d828f 100644 --- a/src/pages/settings/Report/VisibilityPage.tsx +++ b/src/pages/settings/Report/VisibilityPage.tsx @@ -75,7 +75,7 @@ function VisibilityPage({report}: VisibilityProps) { } changeVisibility(option.value); }} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={visibilityOptions.find((visibility) => visibility.isSelected)?.keyForList} ListItem={RadioListItem} /> diff --git a/src/pages/settings/Report/WriteCapabilityPage.tsx b/src/pages/settings/Report/WriteCapabilityPage.tsx index 6f8ae3d81a55..19e8c10b698b 100644 --- a/src/pages/settings/Report/WriteCapabilityPage.tsx +++ b/src/pages/settings/Report/WriteCapabilityPage.tsx @@ -55,7 +55,7 @@ function WriteCapabilityPage({report, policy}: WriteCapabilityPageProps) { sections={[{data: writeCapabilityOptions}]} ListItem={RadioListItem} onSelectRow={(option) => report && ReportActions.updateWriteCapabilityAndNavigate(report, option.value)} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={writeCapabilityOptions.find((locale) => locale.isSelected)?.keyForList} /> diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 98a12a321be1..47b1bc80ca96 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -220,7 +220,7 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro sections={areOptionsInitialized ? sections : []} ListItem={UserListItem} onSelectRow={selectReport} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect onChangeText={setSearchValue} textInputValue={searchValue} headerMessage={headerMessage} diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index c486e5b294c3..661149db5fcc 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -108,7 +108,7 @@ function TaskShareDestinationSelectorModal() { ListItem={UserListItem} sections={areOptionsInitialized ? options.sections : []} onSelectRow={selectReportHandler} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect onChangeText={setSearchValue} textInputValue={searchValue} headerMessage={options.headerMessage} diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index adbf5a664c82..1dee97d42eb8 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -579,7 +579,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, headerMessage={getHeaderMessage()} headerContent={!isSmallScreenWidth && getHeaderContent()} onSelectRow={openMemberDetails} - shouldDebounceRowSelect={!isPolicyAdmin} + shouldSingleExecuteRowSelect={!isPolicyAdmin} onCheckboxPress={(item) => toggleUser(item.accountID)} onSelectAll={() => toggleAllUsers(data)} onDismissError={dismissError} diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx index 3eeebbafc8a3..e8f0d9e8315f 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx @@ -98,7 +98,7 @@ function NetSuiteCustomListSelectorModal({isVisible, currentCustomListValue, onC ListItem={RadioListItem} isRowMultilineSupported initiallyFocusedOptionKey={currentCustomListValue} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect shouldStopPropagation shouldUseDynamicMaxToRenderPerBatch /> diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx index a02c0b76809f..bed84acfb7ce 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx @@ -92,7 +92,7 @@ function QuickbooksAccountSelectPage({policy}: WithPolicyConnectionsProps) { ListItem={RadioListItem} headerContent={listHeaderComponent} onSelectRow={saveSelection} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={initiallyFocusedOptionKey} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx index 0459f61b88d6..69acda4e1ba6 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx @@ -93,7 +93,7 @@ function QuickbooksInvoiceAccountSelectPage({policy}: WithPolicyConnectionsProps ListItem={RadioListItem} headerContent={listHeaderComponent} onSelectRow={updateAccount} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={initiallyFocusedOptionKey} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx index b1af64cb2f4b..c36f7df6b245 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx @@ -100,7 +100,7 @@ function QuickbooksCompanyCardExpenseAccountSelectCardPage({policy}: WithPolicyC sections={sections} ListItem={RadioListItem} onSelectRow={selectExportCompanyCard} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={sections[0].data.find((option) => option.isSelected)?.keyForList} footerContent={ isLocationEnabled && {translate('workspace.qbo.companyCardsLocationEnabledDescription')} diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx index 3c44888d782d..8bee7d206180 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx @@ -97,7 +97,7 @@ function QuickbooksCompanyCardExpenseAccountSelectPage({policy}: WithPolicyConne sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportAccount} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx index 64e55edeb862..89fbd6a96b33 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx @@ -58,7 +58,7 @@ function QuickbooksExportDateSelectPage({policy}: WithPolicyConnectionsProps) { sections={[{data}]} ListItem={RadioListItem} onSelectRow={selectExportDate} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx index b95e70fe11dd..e2b285fe3d41 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx @@ -78,7 +78,7 @@ function QuickbooksExportInvoiceAccountSelectPage({policy}: WithPolicyConnection sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportInvoice} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx index 3c9e7c085578..d57da414b57b 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx @@ -75,7 +75,7 @@ function QuickbooksNonReimbursableDefaultVendorSelectPage({policy}: WithPolicyCo sections={sections} ListItem={RadioListItem} onSelectRow={selectVendor} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={sections[0]?.data.find((mode) => mode.isSelected)?.keyForList} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx index 50b44640642b..bc4523a034f5 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx @@ -116,7 +116,7 @@ function QuickbooksOutOfPocketExpenseAccountSelectPage({policy}: WithPolicyConne sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportAccount} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx index 4843c192991f..b0d8afa6d53b 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx @@ -120,7 +120,7 @@ function QuickbooksOutOfPocketExpenseEntitySelectPage({policy}: WithPolicyConnec sections={sections} ListItem={RadioListItem} onSelectRow={selectExportEntity} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} footerContent={