From 0843b661532c7e8e88fd105473ede37e4af848fd Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 25 Aug 2024 06:29:51 +0530 Subject: [PATCH 001/185] fix: Dupe detect - Tax field does not show the Default label on the confirmation page. Signed-off-by: krishna2323 --- src/libs/TransactionUtils/index.ts | 25 ++++++++++++++++--- .../ReviewDescription.tsx | 3 ++- src/types/onyx/ReviewDuplicates.ts | 4 +++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index e5bd5d9b0753..986435041fa6 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -959,9 +959,27 @@ function compareDuplicateTransactionFields(transactionID: string): {keep: Partia ]; } + const getTransactionCommentWithoutWaypointKeys = (firstTransaction: OnyxEntry) => { + // Remove keyForList from each waypoint + const updatedWaypoints = Object.fromEntries(Object.entries(firstTransaction?.waypoints ?? {}).map(([key, waypoint]) => [key, (({keyForList, ...rest}) => rest)(waypoint)])); + + // Create the new object + const updatedData = { + ...firstTransaction, + waypoints: updatedWaypoints, + }; + + return updatedData; + }; + // Helper function to check if all comments are equal function areAllCommentsEqual(items: Array>, firstTransaction: OnyxEntry) { - return items.every((item) => lodashIsEqual(item?.comment, firstTransaction?.comment)); + return items.every((item) => { + if (item?.comment?.waypoints) { + return lodashIsEqual(getTransactionCommentWithoutWaypointKeys(item.comment), getTransactionCommentWithoutWaypointKeys(firstTransaction?.comment)); + } + return lodashIsEqual(item?.comment, firstTransaction?.comment); + }); } // Helper function to check if all fields are equal for a given key @@ -986,10 +1004,11 @@ function compareDuplicateTransactionFields(transactionID: string): {keep: Partia if (fieldName === 'description') { const allCommentsAreEqual = areAllCommentsEqual(transactions, firstTransaction); - const allCommentsAreEmpty = isFirstTransactionCommentEmptyObject && transactions.every((item) => item?.comment === undefined); + const allCommentsAreEmpty = isFirstTransactionCommentEmptyObject && transactions.every((item) => !item?.comment); if (allCommentsAreEqual || allCommentsAreEmpty) { keep[fieldName] = firstTransaction?.comment?.comment ?? firstTransaction?.comment; + keep.comment = firstTransaction?.comment; } else { processChanges(fieldName, transactions, keys); } @@ -1027,7 +1046,7 @@ function buildNewTransactionAfterReviewingDuplicates(reviewDuplicateTransaction: ...restReviewDuplicateTransaction, modifiedMerchant: reviewDuplicateTransaction?.merchant, merchant: reviewDuplicateTransaction?.merchant, - comment: {comment: reviewDuplicateTransaction?.description}, + comment: {...reviewDuplicateTransaction?.comment, comment: reviewDuplicateTransaction?.description}, }; } diff --git a/src/pages/TransactionDuplicate/ReviewDescription.tsx b/src/pages/TransactionDuplicate/ReviewDescription.tsx index e6229afe48ac..be7bb7ca4b18 100644 --- a/src/pages/TransactionDuplicate/ReviewDescription.tsx +++ b/src/pages/TransactionDuplicate/ReviewDescription.tsx @@ -33,7 +33,8 @@ function ReviewDescription() { ); const setDescription = (data: FieldItemType<'description'>) => { if (data.value !== undefined) { - setReviewDuplicatesKey({description: data.value}); + const comment = compareResult.change.description?.find((d) => d?.comment === data.value); + setReviewDuplicatesKey({description: data.value, comment}); } navigateToNextScreen(); }; diff --git a/src/types/onyx/ReviewDuplicates.ts b/src/types/onyx/ReviewDuplicates.ts index 0682ed0a7f7c..e8944a9f56b0 100644 --- a/src/types/onyx/ReviewDuplicates.ts +++ b/src/types/onyx/ReviewDuplicates.ts @@ -1,3 +1,5 @@ +import type {Comment} from './Transaction'; + /** * Model of review duplicates request */ @@ -26,6 +28,8 @@ type ReviewDuplicates = { /** Description which user want to keep */ description: string; + /** Description which user want to keep */ + comment: Comment; /** Whether the transaction is reimbursable */ reimbursable: boolean; From ccfc7d86c09920aacf195b711a1b5c7cf43ab68a Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 11 Oct 2024 20:21:48 +0530 Subject: [PATCH 002/185] revert changes. Signed-off-by: krishna2323 --- src/libs/TransactionUtils/index.ts | 25 +++---------------- .../ReviewDescription.tsx | 3 +-- src/types/onyx/ReviewDuplicates.ts | 4 --- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 986435041fa6..e5bd5d9b0753 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -959,27 +959,9 @@ function compareDuplicateTransactionFields(transactionID: string): {keep: Partia ]; } - const getTransactionCommentWithoutWaypointKeys = (firstTransaction: OnyxEntry) => { - // Remove keyForList from each waypoint - const updatedWaypoints = Object.fromEntries(Object.entries(firstTransaction?.waypoints ?? {}).map(([key, waypoint]) => [key, (({keyForList, ...rest}) => rest)(waypoint)])); - - // Create the new object - const updatedData = { - ...firstTransaction, - waypoints: updatedWaypoints, - }; - - return updatedData; - }; - // Helper function to check if all comments are equal function areAllCommentsEqual(items: Array>, firstTransaction: OnyxEntry) { - return items.every((item) => { - if (item?.comment?.waypoints) { - return lodashIsEqual(getTransactionCommentWithoutWaypointKeys(item.comment), getTransactionCommentWithoutWaypointKeys(firstTransaction?.comment)); - } - return lodashIsEqual(item?.comment, firstTransaction?.comment); - }); + return items.every((item) => lodashIsEqual(item?.comment, firstTransaction?.comment)); } // Helper function to check if all fields are equal for a given key @@ -1004,11 +986,10 @@ function compareDuplicateTransactionFields(transactionID: string): {keep: Partia if (fieldName === 'description') { const allCommentsAreEqual = areAllCommentsEqual(transactions, firstTransaction); - const allCommentsAreEmpty = isFirstTransactionCommentEmptyObject && transactions.every((item) => !item?.comment); + const allCommentsAreEmpty = isFirstTransactionCommentEmptyObject && transactions.every((item) => item?.comment === undefined); if (allCommentsAreEqual || allCommentsAreEmpty) { keep[fieldName] = firstTransaction?.comment?.comment ?? firstTransaction?.comment; - keep.comment = firstTransaction?.comment; } else { processChanges(fieldName, transactions, keys); } @@ -1046,7 +1027,7 @@ function buildNewTransactionAfterReviewingDuplicates(reviewDuplicateTransaction: ...restReviewDuplicateTransaction, modifiedMerchant: reviewDuplicateTransaction?.merchant, merchant: reviewDuplicateTransaction?.merchant, - comment: {...reviewDuplicateTransaction?.comment, comment: reviewDuplicateTransaction?.description}, + comment: {comment: reviewDuplicateTransaction?.description}, }; } diff --git a/src/pages/TransactionDuplicate/ReviewDescription.tsx b/src/pages/TransactionDuplicate/ReviewDescription.tsx index be7bb7ca4b18..e6229afe48ac 100644 --- a/src/pages/TransactionDuplicate/ReviewDescription.tsx +++ b/src/pages/TransactionDuplicate/ReviewDescription.tsx @@ -33,8 +33,7 @@ function ReviewDescription() { ); const setDescription = (data: FieldItemType<'description'>) => { if (data.value !== undefined) { - const comment = compareResult.change.description?.find((d) => d?.comment === data.value); - setReviewDuplicatesKey({description: data.value, comment}); + setReviewDuplicatesKey({description: data.value}); } navigateToNextScreen(); }; diff --git a/src/types/onyx/ReviewDuplicates.ts b/src/types/onyx/ReviewDuplicates.ts index e8944a9f56b0..0682ed0a7f7c 100644 --- a/src/types/onyx/ReviewDuplicates.ts +++ b/src/types/onyx/ReviewDuplicates.ts @@ -1,5 +1,3 @@ -import type {Comment} from './Transaction'; - /** * Model of review duplicates request */ @@ -28,8 +26,6 @@ type ReviewDuplicates = { /** Description which user want to keep */ description: string; - /** Description which user want to keep */ - comment: Comment; /** Whether the transaction is reimbursable */ reimbursable: boolean; From 76ab756af59c94d9f9ff35bcb74a26e0ccb80330 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 17 Oct 2024 19:07:11 +0530 Subject: [PATCH 003/185] update main branch. Signed-off-by: krishna2323 --- src/libs/TransactionUtils/index.ts | 3 ++- src/pages/TransactionDuplicate/ReviewDescription.tsx | 4 +++- src/types/onyx/ReviewDuplicates.ts | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index bd0db668fe2c..1630d74835cb 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1045,6 +1045,7 @@ function compareDuplicateTransactionFields(transactionID: string): {keep: Partia const allCommentsAreEmpty = isFirstTransactionCommentEmptyObject && transactions.every((item) => getDescription(item) === ''); if (allCommentsAreEqual || allCommentsAreEmpty) { keep[fieldName] = firstTransaction?.comment?.comment ?? firstTransaction?.comment; + keep.comment = firstTransaction?.comment; } else { processChanges(fieldName, transactions, keys); } @@ -1082,7 +1083,7 @@ function buildNewTransactionAfterReviewingDuplicates(reviewDuplicateTransaction: ...restReviewDuplicateTransaction, modifiedMerchant: reviewDuplicateTransaction?.merchant, merchant: reviewDuplicateTransaction?.merchant, - comment: {comment: reviewDuplicateTransaction?.description}, + comment: {...reviewDuplicateTransaction?.comment, comment: reviewDuplicateTransaction?.description}, }; } diff --git a/src/pages/TransactionDuplicate/ReviewDescription.tsx b/src/pages/TransactionDuplicate/ReviewDescription.tsx index 3d74d8cc36e1..108a39d654ce 100644 --- a/src/pages/TransactionDuplicate/ReviewDescription.tsx +++ b/src/pages/TransactionDuplicate/ReviewDescription.tsx @@ -38,7 +38,9 @@ function ReviewDescription() { ); const setDescription = (data: FieldItemType<'description'>) => { if (data.value !== undefined) { - setReviewDuplicatesKey({description: data.value}); + // setReviewDuplicatesKey({description: data.value}); + const comment = compareResult.change.description?.find((d) => d?.comment === data.value); + setReviewDuplicatesKey({comment, description: data.value}); } navigateToNextScreen(); }; diff --git a/src/types/onyx/ReviewDuplicates.ts b/src/types/onyx/ReviewDuplicates.ts index 0682ed0a7f7c..f436b50b1679 100644 --- a/src/types/onyx/ReviewDuplicates.ts +++ b/src/types/onyx/ReviewDuplicates.ts @@ -1,3 +1,5 @@ +import type {Comment} from './Transaction'; + /** * Model of review duplicates request */ @@ -26,6 +28,9 @@ type ReviewDuplicates = { /** Description which user want to keep */ description: string; + /** Description which user want to keep */ + comment: Comment; + /** Whether the transaction is reimbursable */ reimbursable: boolean; From dd9c2d426f05a146d6f89bd35089eafc8aa7a4de Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 29 Oct 2024 13:58:47 +0800 Subject: [PATCH 004/185] fix wrong backTo --- src/ROUTES.ts | 6 +++++- src/libs/Navigation/types.ts | 3 ++- src/libs/actions/BankAccounts.ts | 2 +- src/pages/settings/Wallet/PaymentMethodList.tsx | 2 +- src/pages/settings/Wallet/VerifyAccountPage.tsx | 8 ++++---- src/pages/settings/Wallet/WalletPage/WalletPage.tsx | 4 +++- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2e895537eaac..09f8fad58ff0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -156,7 +156,11 @@ const ROUTES = { SETTINGS_ABOUT: 'settings/about', SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', - SETTINGS_WALLET_VERIFY_ACCOUNT: {route: 'settings/wallet/verify', getRoute: (backTo?: string) => getUrlWithBackToParam('settings/wallet/verify', backTo)}, + SETTINGS_WALLET_VERIFY_ACCOUNT: { + route: 'settings/wallet/verify', + getRoute: (backTo?: string, forwardTo?: string) => + getUrlWithBackToParam(forwardTo ? `settings/wallet/verify?forwardTo=${encodeURIComponent(forwardTo)}` : 'settings/wallet/verify', backTo), + }, SETTINGS_WALLET_DOMAINCARD: { route: 'settings/wallet/card/:cardID?', getRoute: (cardID: string) => `settings/wallet/card/${cardID}` as const, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3de07f2c801f..138e723345a3 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -108,7 +108,8 @@ type SettingsNavigatorParamList = { backTo?: Routes; }; [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: { - backTo: Routes; + backTo?: Routes; + forwardTo?: Routes; }; [SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6679a6e4b9ea..4795c1524d0e 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -79,7 +79,7 @@ function openPersonalBankAccountSetupView(exitReportID?: string, isUserValidated Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {exitReportID}); } if (!isUserValidated) { - Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_ADD_BANK_ACCOUNT)); + Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_WALLET, ROUTES.SETTINGS_ADD_BANK_ACCOUNT)); } Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT); }); diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 23d0b5ab6550..9525c54f84f1 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -347,7 +347,7 @@ function PaymentMethodList({ const onPressItem = useCallback(() => { if (!isUserValidated) { - Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_ADD_BANK_ACCOUNT)); + Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_WALLET, ROUTES.SETTINGS_ADD_BANK_ACCOUNT)); return; } onPress(); diff --git a/src/pages/settings/Wallet/VerifyAccountPage.tsx b/src/pages/settings/Wallet/VerifyAccountPage.tsx index a3b51ef0de17..3275cea40495 100644 --- a/src/pages/settings/Wallet/VerifyAccountPage.tsx +++ b/src/pages/settings/Wallet/VerifyAccountPage.tsx @@ -8,7 +8,6 @@ import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as User from '@userActions/User'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; type VerifyAccountPageProps = StackScreenProps; @@ -23,6 +22,7 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) { const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(true); + const navigateForwardTo = route.params?.forwardTo; const navigateBackTo = route?.params?.backTo; useEffect(() => () => User.clearUnvalidatedNewContactMethodAction(), []); @@ -49,8 +49,8 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) { return; } - Navigation.navigate(navigateBackTo); - }, [isUserValidated, navigateBackTo]); + Navigation.navigate(navigateForwardTo); + }, [isUserValidated, navigateForwardTo]); useEffect(() => { if (isValidateCodeActionModalVisible) { @@ -58,7 +58,7 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) { } if (!isUserValidated && navigateBackTo) { - Navigation.navigate(ROUTES.SETTINGS_WALLET); + Navigation.goBack(navigateBackTo); } else if (!navigateBackTo) { Navigation.goBack(); } diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 7b9366370349..3e3030c86d4f 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -500,7 +500,9 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { ref={buttonRef as ForwardedRef} onPress={() => { if (!isUserValidated) { - Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_ENABLE_PAYMENTS)); + Navigation.navigate( + ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_WALLET, ROUTES.SETTINGS_ENABLE_PAYMENTS), + ); return; } Navigation.navigate(ROUTES.SETTINGS_ENABLE_PAYMENTS); From 774851deb0e68c408e3f51c893f2f0ec05cc51b8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 29 Oct 2024 14:35:01 +0800 Subject: [PATCH 005/185] fix wrong variable being checked --- src/pages/settings/Wallet/VerifyAccountPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/VerifyAccountPage.tsx b/src/pages/settings/Wallet/VerifyAccountPage.tsx index 3275cea40495..302f73f09144 100644 --- a/src/pages/settings/Wallet/VerifyAccountPage.tsx +++ b/src/pages/settings/Wallet/VerifyAccountPage.tsx @@ -45,7 +45,7 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) { setIsValidateCodeActionModalVisible(false); - if (!navigateBackTo) { + if (!navigateForwardTo) { return; } From 059316a87b0b811b8c381d40fbca30d86ec807ae Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 1 Nov 2024 15:47:22 -0600 Subject: [PATCH 006/185] add approve and pay bulk actions --- src/CONST.ts | 2 ++ src/components/Search/SearchPageHeader.tsx | 40 ++++++++++++++++++++++ src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ 4 files changed, 46 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index f01c8a72eb8e..4771c21f0f49 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5849,6 +5849,8 @@ const CONST = { }, BULK_ACTION_TYPES: { EXPORT: 'export', + APPROVE: 'approve', + PAY: 'pay', HOLD: 'hold', UNHOLD: 'unhold', DELETE: 'delete', diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 9fb19ff16288..a044339a2849 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -171,6 +171,46 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { const options: Array> = []; + const shouldShowApproveOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.APPROVE); + + if (shouldShowApproveOption) { + options.push({ + icon: Expensicons.ThumbsUp, + text: translate('search.bulkActions.approve'), + value: CONST.SEARCH.BULK_ACTION_TYPES.APPROVE, + shouldCloseModalOnSelect: true, + onSelected: () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + const reportIDList = selectedReports?.filter((report) => !!report) ?? []; + SearchActions.approveMoneyRequestOnSearch(hash, reportIDList); + }, + }); + } + + const shouldShowPayOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.PAY); + + if (shouldShowPayOption) { + options.push({ + icon: Expensicons.MoneyBag, + text: translate('search.bulkActions.pay'), + value: CONST.SEARCH.BULK_ACTION_TYPES.PAY, + shouldCloseModalOnSelect: true, + onSelected: () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + const reportIDList = selectedReports?.filter((report) => !!report) ?? []; + // SearchActions.payMoneyRequestOnSearch(hash, reportIDList); + }, + }); + } + options.push({ icon: Expensicons.Download, text: translate('common.download'), diff --git a/src/languages/en.ts b/src/languages/en.ts index 03bbaf8ca8ab..a59913f1a3d1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4335,6 +4335,8 @@ const translations = { savedSearchesMenuItemTitle: 'Saved', groupedExpenses: 'grouped expenses', bulkActions: { + approve: 'Approve', + pay: 'Pay', delete: 'Delete', hold: 'Hold', unhold: 'Unhold', diff --git a/src/languages/es.ts b/src/languages/es.ts index 96921bd40388..f32868353517 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4382,6 +4382,8 @@ const translations = { deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?', groupedExpenses: 'gastos agrupados', bulkActions: { + approve: 'Aprobar', + pay: 'Pagar', delete: 'Eliminar', hold: 'Bloquear', unhold: 'Desbloquear', From 087a4d31ec5b56b91be4014500bbf11dce494219 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 1 Nov 2024 15:57:39 -0600 Subject: [PATCH 007/185] add data to selected transactions; --- src/components/Search/SearchPageHeader.tsx | 2 +- src/components/Search/index.tsx | 17 ++++++++++++++++- src/components/Search/types.ts | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index a044339a2849..78d23bd182a7 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -184,7 +184,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { setIsOfflineModalVisible(true); return; } - + console.log('over here', {selectedTransactions, selectedReports}) const reportIDList = selectedReports?.filter((report) => !!report) ?? []; SearchActions.approveMoneyRequestOnSearch(hash, reportIDList); }, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 9238488361b0..e7caafefdd61 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -83,7 +83,18 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact return transactions; } - return {...selectedTransactions, [item.keyForList]: {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}}; + return { + ...selectedTransactions, + [item.keyForList]: { + isSelected: true, + canDelete: item.canDelete, + canHold: item.canHold, + canUnhold: item.canUnhold, + action: item.action, + reportID: item.reportID, + policyID: item.policyID, + }, + }; } function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchProps) { @@ -228,6 +239,8 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr canUnhold: transaction.canUnhold, isSelected: selectedTransactions[transaction.transactionID].isSelected, canDelete: transaction.canDelete, + reportID: transaction.reportID, + policyID: transaction.policyID, }; }); } else { @@ -245,6 +258,8 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr canUnhold: transaction.canUnhold, isSelected: selectedTransactions[transaction.transactionID].isSelected, canDelete: transaction.canDelete, + reportID: transaction.reportID, + policyID: transaction.policyID, }; }); }); diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index d5be896c1c50..ebbd82768d98 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -19,6 +19,12 @@ type SelectedTransactionInfo = { /** The action that can be performed for the transaction */ action: string; + + /** The reportID of the transaction */ + reportID: string; + + /** The policyID tied to the report the transaction is reported on */ + policyID: string; }; /** Model of selected results */ From 45e3944655a7835744d9601898d0699a4665942d Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 1 Nov 2024 16:16:33 -0600 Subject: [PATCH 008/185] get reportIDs --- src/components/Search/SearchPageHeader.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 78d23bd182a7..afb34cdea0a7 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -170,7 +170,6 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { } const options: Array> = []; - const shouldShowApproveOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.APPROVE); if (shouldShowApproveOption) { @@ -184,8 +183,10 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { setIsOfflineModalVisible(true); return; } - console.log('over here', {selectedTransactions, selectedReports}) - const reportIDList = selectedReports?.filter((report) => !!report) ?? []; + + const reportIDList = !selectedReports.length + ? Object.values(selectedTransactions).map((transaction) => transaction.reportID) + : selectedReports?.filter((report) => !!report) ?? []; SearchActions.approveMoneyRequestOnSearch(hash, reportIDList); }, }); From 533f91e070f0cdc8513734143c373abde2fc8c05 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 4 Nov 2024 15:24:21 -0700 Subject: [PATCH 009/185] fix logic to show button --- src/components/Search/SearchContext.tsx | 10 +++++----- src/components/Search/SearchPageHeader.tsx | 13 ++++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index f3206868d556..9a668a0ba44f 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useContext, useMemo, useState} from 'react'; import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; +import {isMoneyRequestReport} from '@libs/ReportUtils'; import * as SearchUIUtils from '@libs/SearchUIUtils'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type {SearchContext, SelectedTransactions} from './types'; @@ -22,13 +23,12 @@ const Context = React.createContext(defaultSearchContext); function getReportsFromSelectedTransactions(data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[], selectedTransactions: SelectedTransactions) { return (data ?? []) .filter( - (item) => - !SearchUIUtils.isTransactionListItemType(item) && - !SearchUIUtils.isReportActionListItemType(item) && - item.reportID && + (item): item is ReportListItemType => + SearchUIUtils.isReportListItemType(item) && + isMoneyRequestReport(item) && item?.transactions?.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected), ) - .map((item) => item.reportID); + .map((item) => ({reportID: item.reportID, action: item.action})); } function SearchContextProvider({children}: ChildrenProps) { diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index afb34cdea0a7..5f72d47d5b12 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -170,8 +170,11 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { } const options: Array> = []; - const shouldShowApproveOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.APPROVE); - + const shouldShowApproveOption = + !isOffline && + (selectedReports.length + ? selectedReports.every((report) => report.action === CONST.SEARCH.ACTION_TYPES.APPROVE) + : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.APPROVE)); if (shouldShowApproveOption) { options.push({ icon: Expensicons.ThumbsUp, @@ -186,7 +189,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { const reportIDList = !selectedReports.length ? Object.values(selectedTransactions).map((transaction) => transaction.reportID) - : selectedReports?.filter((report) => !!report) ?? []; + : selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []; SearchActions.approveMoneyRequestOnSearch(hash, reportIDList); }, }); @@ -206,7 +209,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { return; } - const reportIDList = selectedReports?.filter((report) => !!report) ?? []; + const reportIDList = selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []; // SearchActions.payMoneyRequestOnSearch(hash, reportIDList); }, }); @@ -223,7 +226,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { return; } - const reportIDList = selectedReports?.filter((report) => !!report) ?? []; + const reportIDList = selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []; SearchActions.exportSearchItemsToCSV( {query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']}, () => { From 42f53104b99484347e6445ce51c5a268bcd3427c Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 4 Nov 2024 16:33:13 -0700 Subject: [PATCH 010/185] handle bulk pay --- src/components/Search/SearchContext.tsx | 3 ++- src/components/Search/SearchPageHeader.tsx | 26 ++++++++++++++---- src/components/Search/types.ts | 27 ++++++++++++++++--- .../PayMoneyRequestOnSearchParams.ts | 5 ++-- src/libs/actions/Search.ts | 10 +++---- 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 9a668a0ba44f..7be80589ac50 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useContext, useMemo, useState} from 'react'; import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import {isMoneyRequestReport} from '@libs/ReportUtils'; import * as SearchUIUtils from '@libs/SearchUIUtils'; +import CONST from '@src/CONST'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type {SearchContext, SelectedTransactions} from './types'; @@ -28,7 +29,7 @@ function getReportsFromSelectedTransactions(data: TransactionListItemType[] | Re isMoneyRequestReport(item) && item?.transactions?.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected), ) - .map((item) => ({reportID: item.reportID, action: item.action})); + .map((item) => ({reportID: item.reportID, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, total: item.total ?? 0, policyID: item.policyID ?? ''})); } function SearchContextProvider({children}: ChildrenProps) { diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 5f72d47d5b12..9508e3285cc2 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -36,7 +36,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; import {useSearchContext} from './SearchContext'; import SearchButton from './SearchRouter/SearchButton'; import SearchRouterInput from './SearchRouter/SearchRouterInput'; -import type {SearchQueryJSON} from './types'; +import type {PaymentData, SearchQueryJSON} from './types'; type HeaderWrapperProps = Pick & { text: string; @@ -132,6 +132,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { const [currencyList = {}] = useOnyx(ONYXKEYS.CURRENCY_LIST); const [policyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); const [policyTagsLists] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); + const [lastPaymentMethods = {}] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD); const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); @@ -175,6 +176,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { (selectedReports.length ? selectedReports.every((report) => report.action === CONST.SEARCH.ACTION_TYPES.APPROVE) : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.APPROVE)); + if (shouldShowApproveOption) { options.push({ icon: Expensicons.ThumbsUp, @@ -195,7 +197,13 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { }); } - const shouldShowPayOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.PAY); + const shouldShowPayOption = + !isOffline && + (selectedReports.length + ? selectedReports.every((report) => report.action === CONST.SEARCH.ACTION_TYPES.PAY && report.policyID && lastPaymentMethods[report.policyID]) + : selectedTransactionsKeys.every( + (id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.PAY && selectedTransactions[id].policyID && lastPaymentMethods[selectedTransactions[id].policyID], + )); if (shouldShowPayOption) { options.push({ @@ -208,9 +216,17 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { setIsOfflineModalVisible(true); return; } - - const reportIDList = selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []; - // SearchActions.payMoneyRequestOnSearch(hash, reportIDList); + const paymentData = ( + selectedReports.length + ? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentMethod: lastPaymentMethods[report.policyID]})) + : Object.values(selectedTransactions).map((transaction) => ({ + reportID: transaction.reportID, + amount: transaction.amount, + paymentMethod: lastPaymentMethods[transaction.policyID] + })) + ) as PaymentData[]; + + SearchActions.payMoneyRequestOnSearch(hash, paymentData); }, }); } diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index ebbd82768d98..9b97ac446d5b 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -1,7 +1,7 @@ import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils'; import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import type CONST from '@src/CONST'; -import type {SearchDataTypes, SearchReport} from '@src/types/onyx/SearchResults'; +import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; /** Model of the selected transaction */ type SelectedTransactionInfo = { @@ -18,18 +18,36 @@ type SelectedTransactionInfo = { canUnhold: boolean; /** The action that can be performed for the transaction */ - action: string; + action: ValueOf; /** The reportID of the transaction */ reportID: string; /** The policyID tied to the report the transaction is reported on */ policyID: string; + + /** The transaction amount */ + amount: number; }; -/** Model of selected results */ +/** Model of selected transactons */ type SelectedTransactions = Record; +/** Model of selected reports */ +type SelectedReports = { + reportID: string; + policyID: string; + action: ValueOf; + total: number; +}; + +/** Model of payment data used by Search bulk actions */ +type PaymentData = { + reportID: string; + amount: number; + paymentMethod: ValueOf; +}; + type SortOrder = ValueOf; type SearchColumnType = ValueOf; type ExpenseSearchStatus = ValueOf; @@ -41,7 +59,7 @@ type SearchStatus = ExpenseSearchStatus | InvoiceSearchStatus | TripSearchStatus type SearchContext = { currentSearchHash: number; selectedTransactions: SelectedTransactions; - selectedReports: Array; + selectedReports: SelectedReports[]; setCurrentSearchHash: (hash: number) => void; setSelectedTransactions: (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => void; clearSelectedTransactions: (hash?: number) => void; @@ -118,4 +136,5 @@ export type { ChatSearchStatus, SearchAutocompleteResult, AutocompleteRange, + PaymentData, }; diff --git a/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts b/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts index 10174f0baa37..ee51a1961448 100644 --- a/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts +++ b/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts @@ -1,11 +1,10 @@ type PayMoneyRequestOnSearchParams = { hash: number; - paymentType: string; /** * Stringified JSON object with type of following structure: - * Array<{reportID: string, amount: number}> + * Array<{reportID: string, amount: number, paymentMethod: string}> */ - reportsAndAmounts: string; + paymentData: string; }; export default PayMoneyRequestOnSearchParams; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 556fe76df4d1..dca82a66fd9c 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -2,7 +2,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {FormOnyxValues} from '@components/Form/types'; -import type {SearchQueryJSON} from '@components/Search/types'; +import type {PaymentData, SearchQueryJSON} from '@components/Search/types'; import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import * as API from '@libs/API'; import type {ExportSearchItemsToCSVParams} from '@libs/API/parameters'; @@ -35,11 +35,11 @@ Onyx.connect({ }); function handleActionButtonPress(hash: number, item: TransactionListItemType | ReportListItemType, goToItem: () => void) { - const lastPolicyPaymentMethod = item.policyID ? lastPaymentMethod?.[item.policyID] : null; + const lastPolicyPaymentMethod = item.policyID ? (lastPaymentMethod?.[item.policyID] as ValueOf) : null; const amount = isReportListItemType(item) ? item.total ?? 0 : item.formattedTotal; switch (item.action) { case CONST.SEARCH.ACTION_TYPES.PAY: - return lastPolicyPaymentMethod ? payMoneyRequestOnSearch(hash, lastPolicyPaymentMethod, {[item.reportID]: amount}) : goToItem(); + return lastPolicyPaymentMethod ? payMoneyRequestOnSearch(hash, [{reportID: item.reportID, amount, paymentMethod: lastPolicyPaymentMethod}]) : goToItem(); case CONST.SEARCH.ACTION_TYPES.APPROVE: return approveMoneyRequestOnSearch(hash, [item.reportID]); default: @@ -194,10 +194,10 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[]) { API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData}); } -function payMoneyRequestOnSearch(hash: number, paymentType: string, reportsAndAmounts: Record) { +function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[]) { const {optimisticData, finallyData} = getOnyxLoadingData(hash); - API.write(WRITE_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH, {hash, paymentType, reportsAndAmounts: JSON.stringify(reportsAndAmounts)}, {optimisticData, finallyData}); + API.write(WRITE_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH, {hash, paymentData: JSON.stringify(paymentData)}, {optimisticData, finallyData}); } function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { From 543d7486746ede0bc41776b6647d4b099eeddb6a Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 4 Nov 2024 16:35:49 -0700 Subject: [PATCH 011/185] fix lint --- src/components/Search/SearchPageHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 9508e3285cc2..74735b3d7bbc 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -222,7 +222,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { : Object.values(selectedTransactions).map((transaction) => ({ reportID: transaction.reportID, amount: transaction.amount, - paymentMethod: lastPaymentMethods[transaction.policyID] + paymentMethod: lastPaymentMethods[transaction.policyID], })) ) as PaymentData[]; From 6970f3edddbfef11ae0f2efb06dac7eb23ccef6e Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 4 Nov 2024 16:48:21 -0700 Subject: [PATCH 012/185] fix ts --- src/components/Search/index.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index e7caafefdd61..ce9eab45a9d7 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -49,7 +49,19 @@ const searchHeaderHeight = 54; const sortableSearchStatuses: SearchStatus[] = [CONST.SEARCH.STATUS.EXPENSE.ALL]; function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [string, SelectedTransactionInfo] { - return [item.keyForList, {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}]; + return [ + item.keyForList, + { + isSelected: true, + canDelete: item.canDelete, + canHold: item.canHold, + canUnhold: item.canUnhold, + action: item.action, + reportID: item.reportID, + policyID: item.policyID, + amount: item.modifiedAmount ?? item.amount, + }, + ]; } function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean, shouldAnimateInHighlight: boolean) { @@ -93,6 +105,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact action: item.action, reportID: item.reportID, policyID: item.policyID, + amount: item.modifiedAmount ?? item.amount, }, }; } @@ -241,6 +254,7 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr canDelete: transaction.canDelete, reportID: transaction.reportID, policyID: transaction.policyID, + amount: transaction.modifiedAmount ?? transaction.amount, }; }); } else { @@ -260,6 +274,7 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr canDelete: transaction.canDelete, reportID: transaction.reportID, policyID: transaction.policyID, + amount: transaction.modifiedAmount ?? transaction.amount, }; }); }); From 700bfc8813ec242cf8a1d9dfcaa756e56af6ab6e Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 4 Nov 2024 16:50:33 -0700 Subject: [PATCH 013/185] update dependency array --- src/components/Search/SearchPageHeader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 74735b3d7bbc..dc3efae4bdbc 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -341,6 +341,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { activeWorkspaceID, selectedReports, styles.textWrap, + lastPaymentMethods, ]); if (shouldUseNarrowLayout) { From 51eea863651c5506e80f13e1a9bd70e00fbf4060 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 5 Nov 2024 12:05:23 +0100 Subject: [PATCH 014/185] Initial work on updating reanimated --- package-lock.json | 9 +- package.json | 2 +- .../AttachmentCarousel/Pager/index.tsx | 7 +- .../useCarouselContextEvents.ts | 9 +- .../AttachmentViewPdf/index.android.tsx | 24 +-- .../BaseAutoCompleteSuggestions.tsx | 31 ++-- .../AvatarCropModal/AvatarCropModal.tsx | 149 ++++++++---------- src/components/FloatingActionButton.tsx | 17 +- src/components/OpacityView.tsx | 9 +- src/components/SpacerView.tsx | 11 +- 10 files changed, 128 insertions(+), 140 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2f927adf13f..cf21c105a4ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,7 +104,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.15.1", + "react-native-reanimated": "^3.16.1", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", @@ -35626,9 +35626,10 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.15.1.tgz", - "integrity": "sha512-DbBeUUExtJ1x1nfE94I8qgDgWjq5ztM3IO/+XFO+agOkPeVpBs5cRnxHfJKrjqJ2MgwhJOUDmtHxo+tDsoeitg==", + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.1.tgz", + "integrity": "sha512-Wnbo7toHZ6kPLAD8JWKoKCTfNoqYOMW5vUEP76Rr4RBmJCrdXj6oauYP0aZnZq8NCbiP5bwwu7+RECcWtoetnQ==", + "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", diff --git a/package.json b/package.json index 9620386b5560..6b22e123c174 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.15.1", + "react-native-reanimated": "^3.16.1", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 6fe32050b6d5..68a12c4cd76e 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -70,14 +70,13 @@ function AttachmentCarouselPager( const pageScrollHandler = usePageScrollHandler((e) => { 'worklet'; - // eslint-disable-next-line react-compiler/react-compiler - activePage.value = e.position; - isPagerScrolling.value = e.offset !== 0; + activePage.set(e.position); + isPagerScrolling.set(e.offset !== 0); }, []); useEffect(() => { setActivePageIndex(initialPage); - activePage.value = initialPage; + activePage.set(initialPage); }, [activePage, initialPage]); /** The `pagerItems` object that passed down to the context. Later used to detect current page, whether it's a single image gallery etc. */ diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts index 1c54d7841347..3311f6476194 100644 --- a/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts +++ b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts @@ -35,12 +35,11 @@ function useCarouselContextEvents(setShouldShowArrows: (show?: SetStateAction { - if (!isScrollEnabled.value) { + if (!isScrollEnabled.get()) { return; } onRequestToggleArrows(); - }, [isScrollEnabled.value, onRequestToggleArrows]); + }, [isScrollEnabled, onRequestToggleArrows]); return {handleTap, handleScaleChange, scale, isScrollEnabled}; } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx index c6e7984b793f..c756345664cc 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx @@ -32,32 +32,32 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) { const Pan = Gesture.Pan() .manualActivation(true) .onTouchesMove((evt) => { - if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled && scale.value === 1) { - const translateX = Math.abs((evt.allTouches.at(0)?.absoluteX ?? 0) - offsetX.value); - const translateY = Math.abs((evt.allTouches.at(0)?.absoluteY ?? 0) - offsetY.value); - const allowEnablingScroll = !isPanGestureActive.value || isScrollEnabled.value; + if (offsetX.get() !== 0 && offsetY.get() !== 0 && isScrollEnabled && scale.get() === 1) { + const translateX = Math.abs((evt.allTouches.at(0)?.absoluteX ?? 0) - offsetX.get()); + const translateY = Math.abs((evt.allTouches.at(0)?.absoluteY ?? 0) - offsetY.get()); + const allowEnablingScroll = !isPanGestureActive.get() || isScrollEnabled.get(); // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (translateX > translateY && translateX > SCROLL_THRESHOLD && allowEnablingScroll) { // eslint-disable-next-line react-compiler/react-compiler - isScrollEnabled.value = true; + isScrollEnabled.set(true); } else if (translateY > SCROLL_THRESHOLD) { - isScrollEnabled.value = false; + isScrollEnabled.set(false); } } - isPanGestureActive.value = true; - offsetX.value = evt.allTouches.at(0)?.absoluteX ?? 0; - offsetY.value = evt.allTouches.at(0)?.absoluteY ?? 0; + isPanGestureActive.set(true); + offsetX.set(evt.allTouches.at(0)?.absoluteX ?? 0); + offsetY.set(evt.allTouches.at(0)?.absoluteY ?? 0); }) .onTouchesUp(() => { - isPanGestureActive.value = false; + isPanGestureActive.set(false); if (!isScrollEnabled) { return; } - isScrollEnabled.value = scale.value === 1; + isScrollEnabled.set(scale.get() === 1); }); const Content = useMemo( @@ -69,7 +69,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) { // The react-native-pdf's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, // even though we're not pinching/zooming // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored. - scale.value = Math.round(newScale * 1e2) / 1e2; + scale.set(Math.round(newScale * 1e2) / 1e2); }} /> ), diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 2d22a2560bb0..abc221ed646a 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -50,24 +50,27 @@ function BaseAutoCompleteSuggestions({ const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; const animatedStyles = useAnimatedStyle(() => ({ - opacity: fadeInOpacity.value, - ...StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value), + opacity: fadeInOpacity.get(), + ...StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.get()), })); useEffect(() => { if (measuredHeightOfSuggestionRows === prevRowHeightRef.current) { - // eslint-disable-next-line react-compiler/react-compiler - fadeInOpacity.value = withTiming(1, { - duration: 70, - easing: Easing.inOut(Easing.ease), - }); - rowHeight.value = measuredHeightOfSuggestionRows; + fadeInOpacity.set( + withTiming(1, { + duration: 70, + easing: Easing.inOut(Easing.ease), + }), + ); + rowHeight.set(measuredHeightOfSuggestionRows); } else { - fadeInOpacity.value = 1; - rowHeight.value = withTiming(measuredHeightOfSuggestionRows, { - duration: 100, - easing: Easing.bezier(0.25, 0.1, 0.25, 1), - }); + fadeInOpacity.set(1); + rowHeight.set( + withTiming(measuredHeightOfSuggestionRows, { + duration: 100, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }), + ); } prevRowHeightRef.current = measuredHeightOfSuggestionRows; @@ -103,7 +106,7 @@ function BaseAutoCompleteSuggestions({ renderItem={renderItem} keyExtractor={keyExtractor} removeClippedSubviews={false} - showsVerticalScrollIndicator={innerHeight > rowHeight.value} + showsVerticalScrollIndicator={innerHeight > rowHeight.get()} extraData={[highlightedSuggestionIndex, renderSuggestionMenuItem]} /> diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index dca0d08d11d5..a3c4cdca78bf 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -97,16 +97,16 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose // Changes the modal state values to initial const resetState = useCallback(() => { - originalImageWidth.value = CONST.AVATAR_CROP_MODAL.INITIAL_SIZE; - originalImageHeight.value = CONST.AVATAR_CROP_MODAL.INITIAL_SIZE; - translateY.value = 0; - translateX.value = 0; - scale.value = CONST.AVATAR_CROP_MODAL.MIN_SCALE; - rotation.value = 0; - translateSlider.value = 0; - prevMaxOffsetX.value = 0; - prevMaxOffsetY.value = 0; - isLoading.value = false; + originalImageWidth.set(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + originalImageHeight.set(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + translateY.set(0); + translateX.set(0); + scale.set(CONST.AVATAR_CROP_MODAL.MIN_SCALE); + rotation.set(0); + translateSlider.set(0); + prevMaxOffsetX.set(0); + prevMaxOffsetY.set(0); + isLoading.set(false); setImageContainerSize(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); setSliderContainerSize(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); setIsImageContainerInitialized(false); @@ -123,12 +123,11 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose ImageSize.getSize(imageUri).then(({width, height, rotation: orginalRotation}) => { // On Android devices ImageSize library returns also rotation parameter. if (orginalRotation === 90 || orginalRotation === 270) { - // eslint-disable-next-line react-compiler/react-compiler - originalImageHeight.value = width; - originalImageWidth.value = height; + originalImageHeight.set(width); + originalImageWidth.set(height); } else { - originalImageHeight.value = height; - originalImageWidth.value = width; + originalImageHeight.set(height); + originalImageWidth.set(width); } setIsImageInitialized(true); @@ -136,8 +135,8 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose // Because the reanimated library has some internal optimizations, // sometimes when the modal is hidden styles of the image and slider might not be updated. // To trigger the update we need to slightly change the following values: - translateSlider.value += 0.01; - rotation.value += 360; + translateSlider.set((value) => value + 0.01); + rotation.set((value) => value + 360); }); }, [imageUri, originalImageHeight, originalImageWidth, rotation, translateSlider]); @@ -150,15 +149,15 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose * Returns current image size taking into account scale and rotation. */ const getDisplayedImageSize = useWorkletCallback(() => { - let height = imageContainerSize * scale.value; - let width = imageContainerSize * scale.value; + let height = imageContainerSize * scale.get(); + let width = imageContainerSize * scale.get(); // Since the smaller side will be always equal to the imageContainerSize multiplied by scale, // another side can be calculated with aspect ratio. - if (originalImageWidth.value > originalImageHeight.value) { - width *= originalImageWidth.value / originalImageHeight.value; + if (originalImageWidth.get() > originalImageHeight.get()) { + width *= originalImageWidth.get() / originalImageHeight.get(); } else { - height *= originalImageHeight.value / originalImageWidth.value; + height *= originalImageHeight.get() / originalImageWidth.get(); } return {height, width}; @@ -172,10 +171,10 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose const {height, width} = getDisplayedImageSize(); const maxOffsetX = (width - imageContainerSize) / 2; const maxOffsetY = (height - imageContainerSize) / 2; - translateX.value = clamp(offsetX, [maxOffsetX * -1, maxOffsetX]); - translateY.value = clamp(offsetY, [maxOffsetY * -1, maxOffsetY]); - prevMaxOffsetX.value = maxOffsetX; - prevMaxOffsetY.value = maxOffsetY; + translateX.set(clamp(offsetX, [maxOffsetX * -1, maxOffsetX])); + translateY.set(clamp(offsetY, [maxOffsetY * -1, maxOffsetY])); + prevMaxOffsetX.set(maxOffsetX); + prevMaxOffsetY.set(maxOffsetY); }, [imageContainerSize, scale, clamp], ); @@ -190,8 +189,8 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose * and updates image's offset. */ const panGesture = Gesture.Pan().onChange((event) => { - const newX = translateX.value + event.changeX; - const newY = translateY.value + event.changeY; + const newX = translateX.get() + event.changeX; + const newY = translateY.get() + event.changeY; updateImageOffset(newX, newY); }); @@ -200,7 +199,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose // when the browser window is resized. useEffect(() => { // If no panning has happened and the value is 0, do an early return. - if (!prevMaxOffsetX.value && !prevMaxOffsetY.value) { + if (!prevMaxOffsetX.get() && !prevMaxOffsetY.get()) { return; } const {height, width} = getDisplayedImageSize(); @@ -210,14 +209,14 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose // Since interpolation is expensive, we only want to do it if // image has been panned across X or Y axis by the user. if (prevMaxOffsetX) { - translateX.value = interpolate(translateX.value, [prevMaxOffsetX.value * -1, prevMaxOffsetX.value], [maxOffsetX * -1, maxOffsetX]); + translateX.set(interpolate(translateX.get(), [prevMaxOffsetX.get() * -1, prevMaxOffsetX.get()], [maxOffsetX * -1, maxOffsetX])); } if (prevMaxOffsetY) { - translateY.value = interpolate(translateY.value, [prevMaxOffsetY.value * -1, prevMaxOffsetY.value], [maxOffsetY * -1, maxOffsetY]); + translateY.set(interpolate(translateY.get(), [prevMaxOffsetY.get() * -1, prevMaxOffsetY.get()], [maxOffsetY * -1, maxOffsetY])); } - prevMaxOffsetX.value = maxOffsetX; - prevMaxOffsetY.value = maxOffsetY; + prevMaxOffsetX.set(maxOffsetX); + prevMaxOffsetY.set(maxOffsetY); }, [getDisplayedImageSize, imageContainerSize, prevMaxOffsetX, prevMaxOffsetY, translateX, translateY]); /** @@ -228,65 +227,72 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose onBegin: () => { 'worklet'; - isPressableEnabled.value = false; + isPressableEnabled.set(false); }, onChange: (event: GestureUpdateEvent) => { 'worklet'; - const newSliderValue = clamp(translateSlider.value + event.changeX, [0, sliderContainerSize]); + const newSliderValue = clamp(translateSlider.get() + event.changeX, [0, sliderContainerSize]); const newScale = newScaleValue(newSliderValue, sliderContainerSize); - const differential = newScale / scale.value; + const differential = newScale / scale.get(); - scale.value = newScale; - translateSlider.value = newSliderValue; + scale.set(newScale); + translateSlider.set(newSliderValue); - const newX = translateX.value * differential; - const newY = translateY.value * differential; + const newX = translateX.get() * differential; + const newY = translateY.get() * differential; updateImageOffset(newX, newY); }, onFinalize: () => { 'worklet'; - isPressableEnabled.value = true; + isPressableEnabled.set(true); }, }; // This effect is needed to prevent the incorrect position of // the slider's knob when the window's layout changes useEffect(() => { - translateSlider.value = interpolate(scale.value, [CONST.AVATAR_CROP_MODAL.MIN_SCALE, CONST.AVATAR_CROP_MODAL.MAX_SCALE], [0, sliderContainerSize]); - }, [scale.value, sliderContainerSize, translateSlider]); + translateSlider.set(interpolate(scale.get(), [CONST.AVATAR_CROP_MODAL.MIN_SCALE, CONST.AVATAR_CROP_MODAL.MAX_SCALE], [0, sliderContainerSize])); + }, [scale, sliderContainerSize, translateSlider]); // Rotates the image by changing the rotation value by 90 degrees // and updating the position so the image remains in the same place after rotation const rotateImage = useCallback(() => { - rotation.value -= 90; + rotation.set((value) => value - 90); + + const oldX = translateX.get(); + const oldY = translateY.get(); + const oldHeight = originalImageHeight.get(); + const oldWidth = originalImageWidth.get(); // Rotating 2d coordinates by applying the formula (x, y) → (-y, x). - [translateX.value, translateY.value] = [translateY.value, translateX.value * -1]; + translateX.set(oldY); + translateY.set(oldX * -1); // Since we rotated the image by 90 degrees, now width becomes height and vice versa. - [originalImageHeight.value, originalImageWidth.value] = [originalImageWidth.value, originalImageHeight.value]; - }, [originalImageHeight.value, originalImageWidth.value, rotation, translateX.value, translateY.value]); + originalImageHeight.set(oldWidth); + originalImageWidth.set(oldHeight); + }, [originalImageHeight, originalImageWidth, rotation, translateX, translateY]); // Crops an image that was provided in the imageUri prop, using the current position/scale // then calls onSave and onClose callbacks const cropAndSaveImage = useCallback(() => { - if (isLoading.value) { + if (isLoading.get()) { return; } - isLoading.value = true; - const smallerSize = Math.min(originalImageHeight.value, originalImageWidth.value); - const size = smallerSize / scale.value; - const imageCenterX = originalImageWidth.value / 2; - const imageCenterY = originalImageHeight.value / 2; + isLoading.set(true); + const smallerSize = Math.min(originalImageHeight.get(), originalImageWidth.get()); + const size = smallerSize / scale.get(); + const imageCenterX = originalImageWidth.get() / 2; + const imageCenterY = originalImageHeight.get() / 2; const apothem = size / 2; // apothem for squares is equals to half of it size // Since the translate value is only a distance from the image center, we are able to calculate // the originX and the originY - start coordinates of cropping view. - const originX = imageCenterX - apothem - (translateX.value / imageContainerSize / scale.value) * smallerSize; - const originY = imageCenterY - apothem - (translateY.value / imageContainerSize / scale.value) * smallerSize; + const originX = imageCenterX - apothem - (translateX.get() / imageContainerSize / scale.get()) * smallerSize; + const originY = imageCenterY - apothem - (translateY.get() / imageContainerSize / scale.get()) * smallerSize; const crop = { height: size, @@ -301,29 +307,15 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose const name = isSvg ? 'fileName.png' : imageName; const type = isSvg ? 'image/png' : imageType; - cropOrRotateImage(imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name, type}) + cropOrRotateImage(imageUri, [{rotate: rotation.get() % 360}, {crop}], {compress: 1, name, type}) .then((newImage) => { onClose?.(); onSave?.(newImage); }) .catch(() => { - isLoading.value = false; + isLoading.set(false); }); - }, [ - imageUri, - imageName, - imageType, - onClose, - onSave, - originalImageHeight.value, - originalImageWidth.value, - scale.value, - translateX.value, - imageContainerSize, - translateY.value, - rotation.value, - isLoading, - ]); + }, [isLoading, originalImageHeight, originalImageWidth, scale, translateX, imageContainerSize, translateY, imageType, imageName, imageUri, rotation, onClose, onSave]); const sliderOnPress = (locationX: number) => { // We are using the worklet directive here and running on the UI thread to ensure the Reanimated @@ -331,17 +323,16 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose 'worklet'; - if (!locationX || !isPressableEnabled.value) { + if (!locationX || !isPressableEnabled.get()) { return; } const newSliderValue = clamp(locationX, [0, sliderContainerSize]); const newScale = newScaleValue(newSliderValue, sliderContainerSize); - // eslint-disable-next-line react-compiler/react-compiler - translateSlider.value = newSliderValue; - const differential = newScale / scale.value; - scale.value = newScale; - const newX = translateX.value * differential; - const newY = translateY.value * differential; + translateSlider.set(newSliderValue); + const differential = newScale / scale.get(); + scale.set(newScale); + const newX = translateX.get() * differential; + const newY = translateY.get() * differential; updateImageOffset(newX, newY); }; diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index ecf72f89134b..3c831301db8b 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -60,18 +60,19 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo const buttonRef = ref; useEffect(() => { - // eslint-disable-next-line react-compiler/react-compiler - sharedValue.value = withTiming(isActive ? 1 : 0, { - duration: 340, - easing: Easing.inOut(Easing.ease), - }); + sharedValue.set( + withTiming(isActive ? 1 : 0, { + duration: 340, + easing: Easing.inOut(Easing.ease), + }), + ); }, [isActive, sharedValue]); const animatedStyle = useAnimatedStyle(() => { - const backgroundColor = interpolateColor(sharedValue.value, [0, 1], [success, buttonDefaultBG]); + const backgroundColor = interpolateColor(sharedValue.get(), [0, 1], [success, buttonDefaultBG]); return { - transform: [{rotate: `${sharedValue.value * 135}deg`}], + transform: [{rotate: `${sharedValue.get() * 135}deg`}], backgroundColor, borderRadius, }; @@ -79,7 +80,7 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo const animatedProps = useAnimatedProps( () => { - const fill = interpolateColor(sharedValue.value, [0, 1], [textLight, textDark]); + const fill = interpolateColor(sharedValue.get(), [0, 1], [textLight, textDark]); return { fill, diff --git a/src/components/OpacityView.tsx b/src/components/OpacityView.tsx index f4884fd3c0f8..6c7aa26d05ba 100644 --- a/src/components/OpacityView.tsx +++ b/src/components/OpacityView.tsx @@ -44,16 +44,11 @@ function OpacityView({ }: OpacityViewProps) { const opacity = useSharedValue(1); const opacityStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, + opacity: opacity.get(), })); React.useEffect(() => { - if (shouldDim) { - // eslint-disable-next-line react-compiler/react-compiler - opacity.value = withTiming(dimmingValue, {duration: dimAnimationDuration}); - } else { - opacity.value = withTiming(1, {duration: dimAnimationDuration}); - } + opacity.set(withTiming(shouldDim ? dimmingValue : 1, {duration: dimAnimationDuration})); }, [shouldDim, dimmingValue, opacity, dimAnimationDuration]); return ( diff --git a/src/components/SpacerView.tsx b/src/components/SpacerView.tsx index bb762da1226b..3ca874e8f260 100644 --- a/src/components/SpacerView.tsx +++ b/src/components/SpacerView.tsx @@ -22,9 +22,9 @@ function SpacerView({shouldShow, style}: SpacerViewProps) { const prevShouldShow = usePrevious(shouldShow); const duration = CONST.ANIMATED_TRANSITION; const animatedStyles = useAnimatedStyle(() => ({ - borderBottomWidth: withTiming(borderBottomWidth.value, {duration}), - marginTop: withTiming(marginVertical.value, {duration}), - marginBottom: withTiming(marginVertical.value, {duration}), + borderBottomWidth: withTiming(borderBottomWidth.get(), {duration}), + marginTop: withTiming(marginVertical.get(), {duration}), + marginBottom: withTiming(marginVertical.get(), {duration}), })); React.useEffect(() => { @@ -35,9 +35,8 @@ function SpacerView({shouldShow, style}: SpacerViewProps) { marginVertical: shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_MARGIN_VERTICAL : CONST.HORIZONTAL_SPACER.HIDDEN_MARGIN_VERTICAL, borderBottomWidth: shouldShow ? CONST.HORIZONTAL_SPACER.DEFAULT_BORDER_BOTTOM_WIDTH : CONST.HORIZONTAL_SPACER.HIDDEN_BORDER_BOTTOM_WIDTH, }; - // eslint-disable-next-line react-compiler/react-compiler - marginVertical.value = values.marginVertical; - borderBottomWidth.value = values.borderBottomWidth; + marginVertical.set(values.marginVertical); + borderBottomWidth.set(values.borderBottomWidth); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we only need to trigger when shouldShow prop is changed }, [shouldShow, prevShouldShow]); From f473d06bf18cc97b09e64821e716d6441fc29211 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 5 Nov 2024 12:51:05 +0100 Subject: [PATCH 015/185] Fix patches --- ...ive-reanimated+3.15.1+003+fixNullViewTag.patch | 15 --------------- ...native-reanimated+3.16.1+001+hybrid-app.patch} | 4 ++-- ...imated+3.16.1+002+dontWhitelistTextProp.patch} | 0 3 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 patches/react-native-reanimated+3.15.1+003+fixNullViewTag.patch rename patches/{react-native-reanimated+3.15.1+001+hybrid-app.patch => react-native-reanimated+3.16.1+001+hybrid-app.patch} (91%) rename patches/{react-native-reanimated+3.15.1+002+dontWhitelistTextProp.patch => react-native-reanimated+3.16.1+002+dontWhitelistTextProp.patch} (100%) diff --git a/patches/react-native-reanimated+3.15.1+003+fixNullViewTag.patch b/patches/react-native-reanimated+3.15.1+003+fixNullViewTag.patch deleted file mode 100644 index ca982c6f8036..000000000000 --- a/patches/react-native-reanimated+3.15.1+003+fixNullViewTag.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx -index 577b4a7..c60f0f8 100644 ---- a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx -+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx -@@ -481,7 +481,9 @@ export function createAnimatedComponent( - ? (ref as HTMLElement) - : findNodeHandle(ref as Component); - -- this._componentViewTag = tag as number; -+ if (tag !== null) { -+ this._componentViewTag = tag as number; -+ } - - const { layout, entering, exiting, sharedTransitionTag } = this.props; - if ( diff --git a/patches/react-native-reanimated+3.15.1+001+hybrid-app.patch b/patches/react-native-reanimated+3.16.1+001+hybrid-app.patch similarity index 91% rename from patches/react-native-reanimated+3.15.1+001+hybrid-app.patch rename to patches/react-native-reanimated+3.16.1+001+hybrid-app.patch index 3b40360d5860..835df1f034a9 100644 --- a/patches/react-native-reanimated+3.15.1+001+hybrid-app.patch +++ b/patches/react-native-reanimated+3.16.1+001+hybrid-app.patch @@ -1,9 +1,9 @@ diff --git a/node_modules/react-native-reanimated/scripts/reanimated_utils.rb b/node_modules/react-native-reanimated/scripts/reanimated_utils.rb -index af0935f..ccd2a9e 100644 +index 9fc7b15..e453d84 100644 --- a/node_modules/react-native-reanimated/scripts/reanimated_utils.rb +++ b/node_modules/react-native-reanimated/scripts/reanimated_utils.rb @@ -17,7 +17,11 @@ def find_config() - :react_native_common_dir => nil, + :react_native_reanimated_dir_from_pods_root => nil, } - react_native_node_modules_dir = File.join(File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native/package.json')"`), '..') diff --git a/patches/react-native-reanimated+3.15.1+002+dontWhitelistTextProp.patch b/patches/react-native-reanimated+3.16.1+002+dontWhitelistTextProp.patch similarity index 100% rename from patches/react-native-reanimated+3.15.1+002+dontWhitelistTextProp.patch rename to patches/react-native-reanimated+3.16.1+002+dontWhitelistTextProp.patch From 4ac36b19b9d92c8a16679beb819e572f06f75354 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 5 Nov 2024 12:52:08 +0100 Subject: [PATCH 016/185] Improve performance of rotateImage --- .../AvatarCropModal/AvatarCropModal.tsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index a3c4cdca78bf..44ae528c425a 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -260,20 +260,17 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose // Rotates the image by changing the rotation value by 90 degrees // and updating the position so the image remains in the same place after rotation const rotateImage = useCallback(() => { - rotation.set((value) => value - 90); + runOnUI(() => { + rotation.set((value) => value - 90); - const oldX = translateX.get(); - const oldY = translateY.get(); - const oldHeight = originalImageHeight.get(); - const oldWidth = originalImageWidth.get(); + const oldTranslateX = translateX.get(); + translateX.set(translateY.get()); + translateY.set(oldTranslateX * -1); - // Rotating 2d coordinates by applying the formula (x, y) → (-y, x). - translateX.set(oldY); - translateY.set(oldX * -1); - - // Since we rotated the image by 90 degrees, now width becomes height and vice versa. - originalImageHeight.set(oldWidth); - originalImageWidth.set(oldHeight); + const oldOriginalImageHeight = originalImageHeight.get(); + originalImageHeight.set(originalImageWidth.get()); + originalImageWidth.set(oldOriginalImageHeight); + })(); }, [originalImageHeight, originalImageWidth, rotation, translateX, translateY]); // Crops an image that was provided in the imageUri prop, using the current position/scale From 0667cf722d040636d7b1b1570a105e98cae15381 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 5 Nov 2024 13:26:05 +0100 Subject: [PATCH 017/185] Continue updating reanimated usage --- .../AvatarCropModal/AvatarCropModal.tsx | 26 ++- .../CustomStatusBarAndBackground/index.tsx | 14 +- src/components/MultiGestureCanvas/index.tsx | 84 +++++----- .../MultiGestureCanvas/usePanGesture.ts | 158 ++++++++++-------- 4 files changed, 157 insertions(+), 125 deletions(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 44ae528c425a..3ff9ccc4e3f8 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -4,7 +4,7 @@ import type {LayoutChangeEvent} from 'react-native'; import {Gesture, GestureHandlerRootView} from 'react-native-gesture-handler'; import type {GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import ImageSize from 'react-native-image-size'; -import {interpolate, runOnUI, useSharedValue, useWorkletCallback} from 'react-native-reanimated'; +import {interpolate, runOnUI, useSharedValue} from 'react-native-reanimated'; import Button from '@components/Button'; import HeaderGap from '@components/HeaderGap'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -143,12 +143,18 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose /** * Validates that value is within the provided mix/max range. */ - const clamp = useWorkletCallback((value: number, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []); + const clamp = useCallback((value: number, [min, max]: [number, number]) => { + 'worklet'; + + return interpolate(value, [min, max], [min, max], 'clamp'); + }, []); /** * Returns current image size taking into account scale and rotation. */ - const getDisplayedImageSize = useWorkletCallback(() => { + const getDisplayedImageSize = useCallback(() => { + 'worklet'; + let height = imageContainerSize * scale.get(); let width = imageContainerSize * scale.get(); @@ -161,13 +167,15 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose } return {height, width}; - }, [imageContainerSize, scale]); + }, [imageContainerSize, originalImageHeight, originalImageWidth, scale]); /** * Validates the offset to prevent overflow, and updates the image offset. */ - const updateImageOffset = useWorkletCallback( + const updateImageOffset = useCallback( (offsetX: number, offsetY: number) => { + 'worklet'; + const {height, width} = getDisplayedImageSize(); const maxOffsetX = (width - imageContainerSize) / 2; const maxOffsetY = (height - imageContainerSize) / 2; @@ -176,13 +184,15 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose prevMaxOffsetX.set(maxOffsetX); prevMaxOffsetY.set(maxOffsetY); }, - [imageContainerSize, scale, clamp], + [getDisplayedImageSize, imageContainerSize, translateX, clamp, translateY, prevMaxOffsetX, prevMaxOffsetY], ); - const newScaleValue = useWorkletCallback((newSliderValue: number, containerSize: number) => { + const newScaleValue = useCallback((newSliderValue: number, containerSize: number) => { + 'worklet'; + const {MAX_SCALE, MIN_SCALE} = CONST.AVATAR_CROP_MODAL; return (newSliderValue / containerSize) * (MAX_SCALE - MIN_SCALE) + MIN_SCALE; - }); + }, []); /** * Calculates new x & y image translate value on image panning diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index ec52f07d211c..ac1fc77dff96 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -44,14 +44,14 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack const statusBarAnimation = useSharedValue(0); useAnimatedReaction( - () => statusBarAnimation.value, + () => statusBarAnimation.get(), (current, previous) => { // Do not run if either of the animated value is null // or previous animated value is greater than or equal to the current one if (previous === null || current === null || current <= previous) { return; } - const backgroundColor = interpolateColor(statusBarAnimation.value, [0, 1], [prevStatusBarBackgroundColor.value, statusBarBackgroundColor.value]); + const backgroundColor = interpolateColor(statusBarAnimation.get(), [0, 1], [prevStatusBarBackgroundColor.get(), statusBarBackgroundColor.get()]); runOnJS(updateStatusBarAppearance)({backgroundColor}); }, ); @@ -92,8 +92,8 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack currentScreenBackgroundColor = backgroundColorFromRoute || pageTheme.backgroundColor; } - prevStatusBarBackgroundColor.value = statusBarBackgroundColor.value; - statusBarBackgroundColor.value = currentScreenBackgroundColor; + prevStatusBarBackgroundColor.set(statusBarBackgroundColor.get()); + statusBarBackgroundColor.set(currentScreenBackgroundColor); const callUpdateStatusBarAppearance = () => { updateStatusBarAppearance({statusBarStyle: newStatusBarStyle}); @@ -101,8 +101,8 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack }; const callUpdateStatusBarBackgroundColor = () => { - statusBarAnimation.value = 0; - statusBarAnimation.value = withDelay(300, withTiming(1)); + statusBarAnimation.set(0); + statusBarAnimation.set(withDelay(300, withTiming(1))); }; // Don't update the status bar style if it's the same as the current one, to prevent flashing. @@ -121,7 +121,7 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack callUpdateStatusBarAppearance(); } - if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.value !== theme.appBG) { + if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.get() !== theme.appBG) { callUpdateStatusBarBackgroundColor(); } }, diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index ff9566839d59..fda3ee9bea60 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -1,12 +1,12 @@ import type {ForwardedRef} from 'react'; -import React, {useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import type {GestureType} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import type {GestureRef} from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture'; import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; -import Animated, {cancelAnimation, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import Animated, {cancelAnimation, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -92,7 +92,7 @@ function MultiGestureCanvas({ // Adding together zoom scale and the initial scale to fit the content into the canvas // Using the minimum content scale, so that the image is not bigger than the canvas // and not smaller than needed to fit - const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); + const totalScale = useDerivedValue(() => zoomScale.get() * minContentScale, [minContentScale]); const panTranslateX = useSharedValue(0); const panTranslateY = useSharedValue(0); @@ -110,44 +110,50 @@ function MultiGestureCanvas({ /** * Stops any currently running decay animation from panning */ - const stopAnimation = useWorkletCallback(() => { + const stopAnimation = useCallback(() => { + 'worklet'; + cancelAnimation(offsetX); cancelAnimation(offsetY); - }); + }, [offsetX, offsetY]); /** * Resets the canvas to the initial state and animates back smoothly */ - const reset = useWorkletCallback((animated: boolean, callback?: () => void) => { - stopAnimation(); - - // eslint-disable-next-line react-compiler/react-compiler - offsetX.value = 0; - offsetY.value = 0; - pinchScale.value = 1; - - if (animated) { - panTranslateX.value = withSpring(0, SPRING_CONFIG); - panTranslateY.value = withSpring(0, SPRING_CONFIG); - pinchTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchTranslateY.value = withSpring(0, SPRING_CONFIG); - zoomScale.value = withSpring(1, SPRING_CONFIG, callback); - - return; - } - - panTranslateX.value = 0; - panTranslateY.value = 0; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - zoomScale.value = 1; - - if (callback === undefined) { - return; - } - - callback(); - }); + const reset = useCallback( + (animated: boolean, callback?: () => void) => { + 'worklet'; + + stopAnimation(); + + offsetX.set(0); + offsetY.set(0); + pinchScale.set(1); + + if (animated) { + panTranslateX.set(withSpring(0, SPRING_CONFIG)); + panTranslateY.set(withSpring(0, SPRING_CONFIG)); + pinchTranslateX.set(withSpring(0, SPRING_CONFIG)); + pinchTranslateY.set(withSpring(0, SPRING_CONFIG)); + zoomScale.set(withSpring(1, SPRING_CONFIG, callback)); + + return; + } + + panTranslateX.set(0); + panTranslateY.set(0); + pinchTranslateX.set(0); + pinchTranslateY.set(0); + zoomScale.set(1); + + if (callback === undefined) { + return; + } + + callback(); + }, + [offsetX, offsetY, panTranslateX, panTranslateY, pinchScale, pinchTranslateX, pinchTranslateY, stopAnimation, zoomScale], + ); const {singleTapGesture: baseSingleTapGesture, doubleTapGesture} = useTapGestures({ canvasSize, @@ -164,6 +170,7 @@ function MultiGestureCanvas({ onTap, shouldDisableTransformationGestures, }); + // eslint-disable-next-line react-compiler/react-compiler const singleTapGesture = baseSingleTapGesture.requireExternalGestureToFail(doubleTapGesture, panGestureRef); const panGestureSimultaneousList = useMemo( @@ -186,6 +193,7 @@ function MultiGestureCanvas({ onSwipeDown, }) .simultaneousWithExternalGesture(...panGestureSimultaneousList) + // eslint-disable-next-line react-compiler/react-compiler .withRef(panGestureRef); const pinchGesture = usePinchGesture({ @@ -217,8 +225,8 @@ function MultiGestureCanvas({ // Animate the x and y position of the content within the canvas based on all of the gestures const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + panTranslateX.value + offsetX.value; - const y = pinchTranslateY.value + panTranslateY.value + offsetY.value; + const x = pinchTranslateX.get() + panTranslateX.get() + offsetX.get(); + const y = pinchTranslateY.get() + panTranslateY.get() + offsetY.get(); return { transform: [ @@ -228,7 +236,7 @@ function MultiGestureCanvas({ { translateY: y, }, - {scale: totalScale.value}, + {scale: totalScale.get()}, ], // Hide the image if the size is not ready yet opacity: contentSizeProp?.width ? 1 : 0, diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index b31e310055ae..bd29a18cc46c 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -1,8 +1,9 @@ /* eslint-disable no-param-reassign */ +import {useCallback} from 'react'; import {Dimensions} from 'react-native'; import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; +import {runOnJS, useDerivedValue, useSharedValue, withDecay, withSpring} from 'react-native-reanimated'; import * as Browser from '@libs/Browser'; import {SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; @@ -47,8 +48,8 @@ const usePanGesture = ({ onSwipeDown, }: UsePanGestureProps): PanGesture => { // The content size after fitting it to the canvas and zooming - const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); - const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.get(), [contentSize.width]); + const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.get(), [contentSize.height]); // Used to track previous touch position for the "swipe down to close" gesture const previousTouch = useSharedValue<{x: number; y: number} | null>(null); @@ -61,140 +62,153 @@ const usePanGesture = ({ const isMobileBrowser = Browser.isMobile(); // Disable "swipe down to close" gesture when content is bigger than the canvas - const enableSwipeDownToClose = useDerivedValue(() => canvasSize.height < zoomedContentHeight.value, [canvasSize.height]); + const enableSwipeDownToClose = useDerivedValue(() => canvasSize.height < zoomedContentHeight.get(), [canvasSize.height]); // Calculates bounds of the scaled content // Can we pan left/right/up/down // Can be used to limit gesture or implementing tension effect - const getBounds = useWorkletCallback(() => { + const getBounds = useCallback(() => { + 'worklet'; + let horizontalBoundary = 0; let verticalBoundary = 0; - if (canvasSize.width < zoomedContentWidth.value) { - horizontalBoundary = Math.abs(canvasSize.width - zoomedContentWidth.value) / 2; + if (canvasSize.width < zoomedContentWidth.get()) { + horizontalBoundary = Math.abs(canvasSize.width - zoomedContentWidth.get()) / 2; } - if (canvasSize.height < zoomedContentHeight.value) { - verticalBoundary = Math.abs(zoomedContentHeight.value - canvasSize.height) / 2; + if (canvasSize.height < zoomedContentHeight.get()) { + verticalBoundary = Math.abs(zoomedContentHeight.get() - canvasSize.height) / 2; } const horizontalBoundaries = {min: -horizontalBoundary, max: horizontalBoundary}; const verticalBoundaries = {min: -verticalBoundary, max: verticalBoundary}; const clampedOffset = { - x: MultiGestureCanvasUtils.clamp(offsetX.value, horizontalBoundaries.min, horizontalBoundaries.max), - y: MultiGestureCanvasUtils.clamp(offsetY.value, verticalBoundaries.min, verticalBoundaries.max), + x: MultiGestureCanvasUtils.clamp(offsetX.get(), horizontalBoundaries.min, horizontalBoundaries.max), + y: MultiGestureCanvasUtils.clamp(offsetY.get(), verticalBoundaries.min, verticalBoundaries.max), }; // If the horizontal/vertical offset is the same after clamping to the min/max boundaries, the content is within the boundaries - const isInHoriztontalBoundary = clampedOffset.x === offsetX.value; - const isInVerticalBoundary = clampedOffset.y === offsetY.value; + const isInHorizontalBoundary = clampedOffset.x === offsetX.get(); + const isInVerticalBoundary = clampedOffset.y === offsetY.get(); return { horizontalBoundaries, verticalBoundaries, clampedOffset, - isInHoriztontalBoundary, + isInHorizontalBoundary, isInVerticalBoundary, }; - }, [canvasSize.width, canvasSize.height]); + }, [canvasSize.width, canvasSize.height, zoomedContentWidth, zoomedContentHeight, offsetX, offsetY]); // We want to smoothly decay/end the gesture by phasing out the pan animation // In case the content is outside of the boundaries of the canvas, // we need to move the content back into the boundaries - const finishPanGesture = useWorkletCallback(() => { + const finishPanGesture = useCallback(() => { + 'worklet'; + // If the content is centered within the canvas, we don't need to run any animations - if (offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { + if (offsetX.get() === 0 && offsetY.get() === 0 && panTranslateX.get() === 0 && panTranslateY.get() === 0) { return; } - const {clampedOffset, isInHoriztontalBoundary, isInVerticalBoundary, horizontalBoundaries, verticalBoundaries} = getBounds(); + const {clampedOffset, isInHorizontalBoundary, isInVerticalBoundary, horizontalBoundaries, verticalBoundaries} = getBounds(); // If the content is within the horizontal/vertical boundaries of the canvas, we can smoothly phase out the animation // If not, we need to snap back to the boundaries - if (isInHoriztontalBoundary) { + if (isInHorizontalBoundary) { // If the (absolute) velocity is 0, we don't need to run an animation - if (Math.abs(panVelocityX.value) !== 0) { + if (Math.abs(panVelocityX.get()) !== 0) { // Phase out the pan animation // eslint-disable-next-line react-compiler/react-compiler - offsetX.value = withDecay({ - velocity: panVelocityX.value, - clamp: [horizontalBoundaries.min, horizontalBoundaries.max], - deceleration: PAN_DECAY_DECELARATION, - rubberBandEffect: false, - }); + offsetX.set( + withDecay({ + velocity: panVelocityX.get(), + clamp: [horizontalBoundaries.min, horizontalBoundaries.max], + deceleration: PAN_DECAY_DECELARATION, + rubberBandEffect: false, + }), + ); } } else { // Animated back to the boundary - offsetX.value = withSpring(clampedOffset.x, SPRING_CONFIG); + offsetX.set(withSpring(clampedOffset.x, SPRING_CONFIG)); } if (isInVerticalBoundary) { // If the (absolute) velocity is 0, we don't need to run an animation - if (Math.abs(panVelocityY.value) !== 0) { + if (Math.abs(panVelocityY.get()) !== 0) { // Phase out the pan animation - offsetY.value = withDecay({ - velocity: panVelocityY.value, - clamp: [verticalBoundaries.min, verticalBoundaries.max], - deceleration: PAN_DECAY_DECELARATION, - }); + offsetY.set( + withDecay({ + velocity: panVelocityY.get(), + clamp: [verticalBoundaries.min, verticalBoundaries.max], + deceleration: PAN_DECAY_DECELARATION, + }), + ); } } else { - const finalTranslateY = offsetY.value + panVelocityY.value * 0.2; - - if (finalTranslateY > SNAP_POINT && zoomScale.value <= 1) { - offsetY.value = withSpring(SNAP_POINT_HIDDEN, SPRING_CONFIG, () => { - isSwipingDownToClose.value = false; - - if (onSwipeDown) { - runOnJS(onSwipeDown)(); - } - }); + const finalTranslateY = offsetY.get() + panVelocityY.get() * 0.2; + + if (finalTranslateY > SNAP_POINT && zoomScale.get() <= 1) { + offsetY.set( + withSpring(SNAP_POINT_HIDDEN, SPRING_CONFIG, () => { + isSwipingDownToClose.set(false); + + if (onSwipeDown) { + runOnJS(onSwipeDown)(); + } + }), + ); } else { // Animated back to the boundary - offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG, () => { - isSwipingDownToClose.value = false; - }); + offsetY.set( + withSpring(clampedOffset.y, SPRING_CONFIG, () => { + isSwipingDownToClose.set(false); + }), + ); } } // Reset velocity variables after we finished the pan gesture - panVelocityX.value = 0; - panVelocityY.value = 0; - }); + panVelocityX.set(0); + panVelocityY.set(0); + }, [getBounds, isSwipingDownToClose, offsetX, offsetY, onSwipeDown, panTranslateX, panTranslateY, panVelocityX, panVelocityY, zoomScale]); const panGesture = Gesture.Pan() .manualActivation(true) .averageTouches(true) .onTouchesUp(() => { - previousTouch.value = null; + previousTouch.set(null); }) .onTouchesMove((evt, state) => { // We only allow panning when the content is zoomed in - if (zoomScale.value > 1 && !shouldDisableTransformationGestures.value) { + if (zoomScale.get() > 1 && !shouldDisableTransformationGestures.get()) { state.activate(); } // TODO: this needs tuning to work properly - if (!shouldDisableTransformationGestures.value && zoomScale.value === 1 && previousTouch.value !== null) { - const velocityX = Math.abs((evt.allTouches.at(0)?.x ?? 0) - previousTouch.value.x); - const velocityY = (evt.allTouches.at(0)?.y ?? 0) - previousTouch.value.y; + const previousTouchValue = previousTouch.get(); + if (!shouldDisableTransformationGestures.get() && zoomScale.get() === 1 && previousTouchValue !== null) { + const velocityX = Math.abs((evt.allTouches.at(0)?.x ?? 0) - previousTouchValue.x); + const velocityY = (evt.allTouches.at(0)?.y ?? 0) - previousTouchValue.y; if (Math.abs(velocityY) > velocityX && velocityY > 20) { state.activate(); - isSwipingDownToClose.value = true; - previousTouch.value = null; + isSwipingDownToClose.set(true); + previousTouch.set(null); return; } } - if (previousTouch.value === null) { - previousTouch.value = { + if (previousTouchValue === null) { + previousTouch.set({ x: evt.allTouches.at(0)?.x ?? 0, y: evt.allTouches.at(0)?.y ?? 0, - }; + }); } }) .onStart(() => { @@ -207,31 +221,31 @@ const usePanGesture = ({ return; } - panVelocityX.value = evt.velocityX; - panVelocityY.value = evt.velocityY; + panVelocityX.set(evt.velocityX); + panVelocityY.set(evt.velocityY); - if (!isSwipingDownToClose.value) { - if (!isMobileBrowser || (isMobileBrowser && zoomScale.value !== 1)) { - panTranslateX.value += evt.changeX; + if (!isSwipingDownToClose.get()) { + if (!isMobileBrowser || (isMobileBrowser && zoomScale.get() !== 1)) { + panTranslateX.set((value) => value + evt.changeX); } } - if (enableSwipeDownToClose.value || isSwipingDownToClose.value) { - panTranslateY.value += evt.changeY; + if (enableSwipeDownToClose.get() || isSwipingDownToClose.get()) { + panTranslateY.set((value) => value + evt.changeY); } }) .onEnd(() => { // Add pan translation to total offset and reset gesture variables - offsetX.value += panTranslateX.value; - offsetY.value += panTranslateY.value; + offsetX.set((value) => value + panTranslateX.get()); + offsetY.set((value) => value + panTranslateY.get()); // Reset pan gesture variables - panTranslateX.value = 0; - panTranslateY.value = 0; - previousTouch.value = null; + panTranslateX.set(0); + panTranslateY.set(0); + previousTouch.set(null); // If we are swiping (in the pager), we don't want to return to boundaries - if (shouldDisableTransformationGestures.value) { + if (shouldDisableTransformationGestures.get()) { return; } From 73c358f73962f392493c082b880579523c4b0ff7 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 5 Nov 2024 14:40:45 +0100 Subject: [PATCH 018/185] Finish migrating to new reanimated --- .../MultiGestureCanvas/usePinchGesture.ts | 88 ++++++++++--------- .../MultiGestureCanvas/useTapGestures.ts | 23 +++-- .../ReportActionItem/ReportPreview.tsx | 19 ++-- .../AnimatedSettlementButton.tsx | 44 +++++----- .../SplashScreenHider/index.native.tsx | 31 ++++--- .../VideoPlayer/BaseVideoPlayer.tsx | 7 +- .../VideoPlayerControls/ProgressBar/index.tsx | 11 ++- .../VolumeButton/index.tsx | 15 ++-- .../VideoPlayerContexts/VolumeContext.tsx | 7 +- src/hooks/useAnimatedHighlightStyle/index.ts | 35 ++++---- src/pages/Search/SearchPageBottomTab.tsx | 15 ++-- .../report/AnimatedEmptyStateBackground.tsx | 9 +- .../home/report/FloatingMessageCounter.tsx | 6 +- .../ComposerWithSuggestions.tsx | 13 ++- .../ReportActionCompose.tsx | 7 +- .../report/ReportActionItemMessageEdit.tsx | 14 ++- .../step/IOURequestStepScan/index.native.tsx | 13 ++- .../BackgroundImage/index.ios.tsx | 13 +-- 18 files changed, 182 insertions(+), 188 deletions(-) diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts index 46a5e28e5732..7c517ad2f30b 100644 --- a/src/components/MultiGestureCanvas/usePinchGesture.ts +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -1,8 +1,8 @@ /* eslint-disable no-param-reassign */ -import {useEffect, useState} from 'react'; +import {useCallback, useEffect, useState} from 'react'; import type {PinchGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useAnimatedReaction, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {runOnJS, useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated'; import {SPRING_CONFIG, ZOOM_RANGE_BOUNCE_FACTORS} from './constants'; import type {MultiGestureCanvasVariables} from './types'; @@ -61,16 +61,16 @@ const usePinchGesture = ({ return; } - runOnJS(onScaleChanged)(zoomScale.value); + runOnJS(onScaleChanged)(zoomScale.get()); }; // Update the total (pinch) translation based on the regular pinch + bounce useAnimatedReaction( - () => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value], + () => [pinchTranslateX.get(), pinchTranslateY.get(), pinchBounceTranslateX.get(), pinchBounceTranslateY.get()], ([translateX, translateY, bounceX, bounceY]) => { // eslint-disable-next-line react-compiler/react-compiler - totalPinchTranslateX.value = translateX + bounceX; - totalPinchTranslateY.value = translateY + bounceY; + totalPinchTranslateX.set(translateX + bounceX); + totalPinchTranslateY.set(translateY + bounceY); }, ); @@ -78,12 +78,16 @@ const usePinchGesture = ({ * Calculates the adjusted focal point of the pinch gesture, * based on the canvas size and the current offset */ - const getAdjustedFocal = useWorkletCallback( - (focalX: number, focalY: number) => ({ - x: focalX - (canvasSize.width / 2 + offsetX.value), - y: focalY - (canvasSize.height / 2 + offsetY.value), - }), - [canvasSize.width, canvasSize.height], + const getAdjustedFocal = useCallback( + (focalX: number, focalY: number) => { + 'worklet'; + + return { + x: focalX - (canvasSize.width / 2 + offsetX.get()), + y: focalY - (canvasSize.height / 2 + offsetY.get()), + }; + }, + [canvasSize.width, canvasSize.height, offsetX, offsetY], ); // The pinch gesture is disabled when we release one of the fingers @@ -101,7 +105,7 @@ const usePinchGesture = ({ // The first argument is not used, but must be defined .onTouchesDown((_evt, state) => { // We don't want to activate pinch gesture when we are swiping in the pager - if (!shouldDisableTransformationGestures.value) { + if (!shouldDisableTransformationGestures.get()) { return; } @@ -112,8 +116,8 @@ const usePinchGesture = ({ // Set the origin focal point of the pinch gesture at the start of the gesture const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - pinchOrigin.x.value = adjustedFocal.x; - pinchOrigin.y.value = adjustedFocal.y; + pinchOrigin.x.set(adjustedFocal.x); + pinchOrigin.y.set(adjustedFocal.y); }) .onChange((evt) => { // Disable the pinch gesture if one finger is released, @@ -123,58 +127,58 @@ const usePinchGesture = ({ return; } - const newZoomScale = pinchScale.value * evt.scale; - + const newZoomScale = pinchScale.get() * evt.scale; + const zoomScaleValue = zoomScale.get(); // Limit the zoom scale to zoom range including bounce range - if (zoomScale.value >= zoomRange.min * ZOOM_RANGE_BOUNCE_FACTORS.min && zoomScale.value <= zoomRange.max * ZOOM_RANGE_BOUNCE_FACTORS.max) { - zoomScale.value = newZoomScale; - currentPinchScale.value = evt.scale; + if (zoomScaleValue >= zoomRange.min * ZOOM_RANGE_BOUNCE_FACTORS.min && zoomScaleValue <= zoomRange.max * ZOOM_RANGE_BOUNCE_FACTORS.max) { + zoomScale.set(newZoomScale); + currentPinchScale.set(evt.scale); triggerScaleChangedEvent(); } // Calculate new pinch translation const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + currentPinchScale.value * pinchOrigin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + currentPinchScale.value * pinchOrigin.y.value * -1; + const newPinchTranslateX = adjustedFocal.x + currentPinchScale.get() * pinchOrigin.x.get() * -1; + const newPinchTranslateY = adjustedFocal.y + currentPinchScale.get() * pinchOrigin.y.get() * -1; // If the zoom scale is within the zoom range, we perform the regular pinch translation // Otherwise it means that we are "overzoomed" or "underzoomed", so we need to bounce back - if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { - pinchTranslateX.value = newPinchTranslateX; - pinchTranslateY.value = newPinchTranslateY; + if (zoomScaleValue >= zoomRange.min && zoomScaleValue <= zoomRange.max) { + pinchTranslateX.set(newPinchTranslateX); + pinchTranslateY.set(newPinchTranslateY); } else { // Store x and y translation that is produced while bouncing // so we can revert the bounce once pinch gesture is released - pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; - pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; + pinchBounceTranslateX.set(newPinchTranslateX - pinchTranslateX.get()); + pinchBounceTranslateY.set(newPinchTranslateY - pinchTranslateY.get()); } }) .onEnd(() => { // Add pinch translation to total offset and reset gesture variables - offsetX.value += pinchTranslateX.value; - offsetY.value += pinchTranslateY.value; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - currentPinchScale.value = 1; + offsetX.set((value) => value + pinchTranslateX.get()); + offsetY.set((value) => value + pinchTranslateY.get()); + pinchTranslateX.set(0); + pinchTranslateY.set(0); + currentPinchScale.set(1); // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation - if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + if (pinchBounceTranslateX.get() !== 0 || pinchBounceTranslateY.get() !== 0) { + pinchBounceTranslateX.set(withSpring(0, SPRING_CONFIG)); + pinchBounceTranslateY.set(withSpring(0, SPRING_CONFIG)); } - if (zoomScale.value < zoomRange.min) { + if (zoomScale.get() < zoomRange.min) { // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum - pinchScale.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangedEvent); - } else if (zoomScale.value > zoomRange.max) { + pinchScale.set(zoomRange.min); + zoomScale.set(withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangedEvent)); + } else if (zoomScale.get() > zoomRange.max) { // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum - pinchScale.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangedEvent); + pinchScale.set(zoomRange.max); + zoomScale.set(withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangedEvent)); } else { // Otherwise, we just update the pinch scale offset - pinchScale.value = zoomScale.value; + pinchScale.set(zoomScale.get()); triggerScaleChangedEvent(); } }); diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index e4bb02bd5d34..a918310d2862 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -1,8 +1,8 @@ /* eslint-disable no-param-reassign */ -import {useMemo} from 'react'; +import {useCallback, useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; -import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {runOnJS, withSpring} from 'react-native-reanimated'; import {DOUBLE_TAP_SCALE, SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -46,7 +46,7 @@ const useTapGestures = ({ // On double tap the content should be zoomed to fill, but at least zoomed by DOUBLE_TAP_SCALE const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); - const zoomToCoordinates = useWorkletCallback( + const zoomToCoordinates = useCallback( (focalX: number, focalY: number, callback: () => void) => { 'worklet'; @@ -111,19 +111,18 @@ const useTapGestures = ({ offsetAfterZooming.y = 0; } - // eslint-disable-next-line react-compiler/react-compiler - offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG); - offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG, callback); - pinchScale.value = doubleTapScale; + offsetX.set(withSpring(offsetAfterZooming.x, SPRING_CONFIG)); + offsetY.set(withSpring(offsetAfterZooming.y, SPRING_CONFIG)); + zoomScale.set(withSpring(doubleTapScale, SPRING_CONFIG, callback)); + pinchScale.set(doubleTapScale); }, - [scaledContentWidth, scaledContentHeight, canvasSize, doubleTapScale], + [stopAnimation, canvasSize.width, canvasSize.height, scaledContentWidth, scaledContentHeight, doubleTapScale, offsetX, offsetY, zoomScale, pinchScale], ); const doubleTapGesture = Gesture.Tap() // The first argument is not used, but must be defined .onTouchesDown((_evt, state) => { - if (!shouldDisableTransformationGestures.value) { + if (!shouldDisableTransformationGestures.get()) { return; } @@ -137,13 +136,13 @@ const useTapGestures = ({ 'worklet'; if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); + runOnJS(onScaleChanged)(zoomScale.get()); } }; // If the content is already zoomed, we want to reset the zoom, // otherwise we want to zoom in - if (zoomScale.value > 1) { + if (zoomScale.get() > 1) { reset(true, triggerScaleChangedEvent); } else { zoomToCoordinates(evt.x, evt.y, triggerScaleChangedEvent); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 9067f1abb11a..684e922e2dc6 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -135,7 +135,7 @@ function ReportPreview({ const iouSettled = ReportUtils.isSettled(iouReportID) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; const previewMessageOpacity = useSharedValue(1); const previewMessageStyle = useAnimatedStyle(() => ({ - opacity: previewMessageOpacity.value, + opacity: previewMessageOpacity.get(), })); const checkMarkScale = useSharedValue(iouSettled ? 1 : 0); @@ -425,11 +425,11 @@ function ReportPreview({ return; } - // eslint-disable-next-line react-compiler/react-compiler - previewMessageOpacity.value = withTiming(0.75, {duration: CONST.ANIMATION_PAID_DURATION / 2}, () => { - // eslint-disable-next-line react-compiler/react-compiler - previewMessageOpacity.value = withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION / 2}); - }); + previewMessageOpacity.set( + withTiming(0.75, {duration: CONST.ANIMATION_PAID_DURATION / 2}, () => { + previewMessageOpacity.set(withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION / 2})); + }), + ); // We only want to animate the text when the text changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [previewMessage, previewMessageOpacity]); @@ -439,12 +439,7 @@ function ReportPreview({ return; } - if (isPaidAnimationRunning) { - // eslint-disable-next-line react-compiler/react-compiler - checkMarkScale.value = withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION})); - } else { - checkMarkScale.value = 1; - } + checkMarkScale.set(isPaidAnimationRunning ? withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION})) : 1); }, [isPaidAnimationRunning, iouSettled, checkMarkScale]); return ( diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 5de528d741a2..65c2fd2f493b 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -23,20 +23,20 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is const height = useSharedValue(variables.componentSizeNormal); const buttonMarginTop = useSharedValue(styles.expenseAndReportPreviewTextButtonContainer.gap); const buttonStyles = useAnimatedStyle(() => ({ - transform: [{scale: buttonScale.value}], - opacity: buttonOpacity.value, + transform: [{scale: buttonScale.get()}], + opacity: buttonOpacity.get(), })); const paymentCompleteTextStyles = useAnimatedStyle(() => ({ - transform: [{scale: paymentCompleteTextScale.value}], - opacity: paymentCompleteTextOpacity.value, + transform: [{scale: paymentCompleteTextScale.get()}], + opacity: paymentCompleteTextOpacity.get(), position: 'absolute', alignSelf: 'center', })); const containerStyles = useAnimatedStyle(() => ({ - height: height.value, + height: height.get(), justifyContent: 'center', overflow: 'hidden', - marginTop: buttonMarginTop.value, + marginTop: buttonMarginTop.get(), })); const buttonDisabledStyle = isPaidAnimationRunning ? { @@ -46,13 +46,12 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is : undefined; const resetAnimation = useCallback(() => { - // eslint-disable-next-line react-compiler/react-compiler - buttonScale.value = 1; - buttonOpacity.value = 1; - paymentCompleteTextScale.value = 0; - paymentCompleteTextOpacity.value = 1; - height.value = variables.componentSizeNormal; - buttonMarginTop.value = styles.expenseAndReportPreviewTextButtonContainer.gap; + buttonScale.set(1); + buttonOpacity.set(1); + paymentCompleteTextScale.set(0); + paymentCompleteTextOpacity.set(1); + height.set(variables.componentSizeNormal); + buttonMarginTop.set(styles.expenseAndReportPreviewTextButtonContainer.gap); }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]); useEffect(() => { @@ -60,19 +59,20 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is resetAnimation(); return; } - // eslint-disable-next-line react-compiler/react-compiler - buttonScale.value = withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}); - buttonOpacity.value = withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}); - paymentCompleteTextScale.value = withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION}); + buttonScale.set(withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); + buttonOpacity.set(withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); + paymentCompleteTextScale.set(withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION})); // Wait for the above animation + 1s delay before hiding the component const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY; - height.value = withDelay( - totalDelay, - withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()), + height.set( + withDelay( + totalDelay, + withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()), + ), ); - buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); - paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); + buttonMarginTop.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}))); + paymentCompleteTextOpacity.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}))); }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, buttonMarginTop, resetAnimation]); return ( diff --git a/src/components/SplashScreenHider/index.native.tsx b/src/components/SplashScreenHider/index.native.tsx index 7c579519c926..d41770fb7f52 100644 --- a/src/components/SplashScreenHider/index.native.tsx +++ b/src/components/SplashScreenHider/index.native.tsx @@ -17,10 +17,10 @@ function SplashScreenHider({onHide = () => {}}: SplashScreenHiderProps): SplashS const scale = useSharedValue(1); const opacityStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, + opacity: opacity.get(), })); const scaleStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}], + transform: [{scale: scale.get()}], })); const hideHasBeenCalled = useRef(false); @@ -34,19 +34,22 @@ function SplashScreenHider({onHide = () => {}}: SplashScreenHiderProps): SplashS hideHasBeenCalled.current = true; BootSplash.hide().then(() => { - // eslint-disable-next-line react-compiler/react-compiler - scale.value = withTiming(0, { - duration: 200, - easing: Easing.back(2), - }); + scale.set( + withTiming(0, { + duration: 200, + easing: Easing.back(2), + }), + ); - opacity.value = withTiming( - 0, - { - duration: 250, - easing: Easing.out(Easing.ease), - }, - () => runOnJS(onHide)(), + opacity.set( + withTiming( + 0, + { + duration: 250, + easing: Easing.out(Easing.ease), + }, + () => runOnJS(onHide)(), + ), ); }); }, [opacity, scale, onHide]); diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 012537b75108..7fd26a177b0c 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -78,7 +78,7 @@ function BaseVideoPlayer({ const [controlStatusState, setControlStatusState] = useState(controlsStatus); const controlsOpacity = useSharedValue(1); const controlsAnimatedStyle = useAnimatedStyle(() => ({ - opacity: controlsOpacity.value, + opacity: controlsOpacity.get(), })); const videoPlayerRef = useRef(null); @@ -106,8 +106,7 @@ function BaseVideoPlayer({ }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumberRef]); 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.set(withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE))); }, [controlsOpacity]); const debouncedHideControl = useMemo(() => debounce(hideControl, 1500), [hideControl]); @@ -144,7 +143,7 @@ function BaseVideoPlayer({ return; } setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); - controlsOpacity.value = 1; + controlsOpacity.set(1); }, [controlStatusState, controlsOpacity, hideControl]); const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { diff --git a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx index 5d1ea0d85d0b..69273c1444e8 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx +++ b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx @@ -30,13 +30,12 @@ function ProgressBar({duration, position, seekPosition}: ProgressBarProps) { const wasVideoPlayingOnCheck = useSharedValue(false); const onCheckVideoPlaying = (isPlaying: boolean) => { - // eslint-disable-next-line react-compiler/react-compiler - wasVideoPlayingOnCheck.value = isPlaying; + wasVideoPlayingOnCheck.set(isPlaying); }; const progressBarInteraction = (event: GestureUpdateEvent | GestureStateChangeEvent) => { const progress = getProgress(event.x, sliderWidth); - progressWidth.value = progress; + progressWidth.set(progress); runOnJS(seekPosition)((progress * duration) / 100); }; @@ -56,7 +55,7 @@ function ProgressBar({duration, position, seekPosition}: ProgressBarProps) { }) .onFinalize(() => { runOnJS(setIsSliderPressed)(false); - if (!wasVideoPlayingOnCheck.value) { + if (!wasVideoPlayingOnCheck.get()) { return; } runOnJS(playVideo)(); @@ -66,10 +65,10 @@ function ProgressBar({duration, position, seekPosition}: ProgressBarProps) { if (isSliderPressed) { return; } - progressWidth.value = getProgress(position, duration); + progressWidth.set(getProgress(position, duration)); }, [duration, isSliderPressed, position, progressWidth]); - const progressBarStyle: ViewStyle = useAnimatedStyle(() => ({width: `${progressWidth.value}%`})); + const progressBarStyle: ViewStyle = useAnimatedStyle(() => ({width: `${progressWidth.get()}%`})); return ( diff --git a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx index 751bd1b1df26..a6e44607ea62 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx +++ b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx @@ -35,7 +35,7 @@ function VolumeButton({style, small = false}: VolumeButtonProps) { const {translate} = useLocalize(); const {updateVolume, volume} = useVolumeContext(); const [sliderHeight, setSliderHeight] = useState(1); - const [volumeIcon, setVolumeIcon] = useState({icon: getVolumeIcon(volume.value)}); + const [volumeIcon, setVolumeIcon] = useState({icon: getVolumeIcon(volume.get())}); const [isSliderBeingUsed, setIsSliderBeingUsed] = useState(false); const onSliderLayout = useCallback((event: LayoutChangeEvent) => { @@ -45,8 +45,7 @@ function VolumeButton({style, small = false}: VolumeButtonProps) { const changeVolumeOnPan = useCallback( (event: GestureStateChangeEvent | GestureUpdateEvent) => { const val = NumberUtils.roundToTwoDecimalPlaces(1 - event.y / sliderHeight); - // eslint-disable-next-line react-compiler/react-compiler - volume.value = NumberUtils.clamp(val, 0, 1); + volume.set(NumberUtils.clamp(val, 0, 1)); }, [sliderHeight, volume], ); @@ -63,15 +62,15 @@ function VolumeButton({style, small = false}: VolumeButtonProps) { runOnJS(setIsSliderBeingUsed)(false); }); - const progressBarStyle = useAnimatedStyle(() => ({height: `${volume.value * 100}%`})); + const progressBarStyle = useAnimatedStyle(() => ({height: `${volume.get() * 100}%`})); const updateIcon = useCallback((vol: number) => { setVolumeIcon({icon: getVolumeIcon(vol)}); }, []); useDerivedValue(() => { - runOnJS(updateVolume)(volume.value); - runOnJS(updateIcon)(volume.value); + runOnJS(updateVolume)(volume.get()); + runOnJS(updateIcon)(volume.get()); }, [volume]); return ( @@ -95,8 +94,8 @@ function VolumeButton({style, small = false}: VolumeButtonProps) { )} updateVolume(volume.value === 0 ? 1 : 0)} + tooltipText={volume.get() === 0 ? translate('videoPlayer.unmute') : translate('videoPlayer.mute')} + onPress={() => updateVolume(volume.get() === 0 ? 1 : 0)} src={volumeIcon.icon} small={small} shouldForceRenderingTooltipBelow diff --git a/src/components/VideoPlayerContexts/VolumeContext.tsx b/src/components/VideoPlayerContexts/VolumeContext.tsx index f22b524848de..1f7b3bf66551 100644 --- a/src/components/VideoPlayerContexts/VolumeContext.tsx +++ b/src/components/VideoPlayerContexts/VolumeContext.tsx @@ -16,8 +16,7 @@ function VolumeContextProvider({children}: ChildrenProps) { return; } currentVideoPlayerRef.current.setStatusAsync({volume: newVolume, isMuted: newVolume === 0}); - // eslint-disable-next-line react-compiler/react-compiler - volume.value = newVolume; + volume.set(newVolume); }, [currentVideoPlayerRef, volume], ); @@ -28,8 +27,8 @@ function VolumeContextProvider({children}: ChildrenProps) { if (!originalParent) { return; } - updateVolume(volume.value); - }, [originalParent, updateVolume, volume.value]); + updateVolume(volume.get()); + }, [originalParent, updateVolume, volume]); const contextValue = useMemo(() => ({updateVolume, volume}), [updateVolume, volume]); return {children}; diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts index e17f30fc60bf..4c9099c7f1ba 100644 --- a/src/hooks/useAnimatedHighlightStyle/index.ts +++ b/src/hooks/useAnimatedHighlightStyle/index.ts @@ -66,9 +66,9 @@ export default function useAnimatedHighlightStyle({ const theme = useTheme(); const highlightBackgroundStyle = useAnimatedStyle(() => ({ - backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], [backgroundColor ?? theme.appBG, highlightColor ?? theme.border]), - height: height ? interpolate(nonRepeatableProgress.value, [0, 1], [0, height]) : 'auto', - opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), + backgroundColor: interpolateColor(repeatableProgress.get(), [0, 1], [backgroundColor ?? theme.appBG, highlightColor ?? theme.border]), + height: height ? interpolate(nonRepeatableProgress.get(), [0, 1], [0, height]) : 'auto', + opacity: interpolate(nonRepeatableProgress.get(), [0, 1], [0, 1]), borderRadius, })); @@ -90,19 +90,22 @@ export default function useAnimatedHighlightStyle({ setStartHighlight(false); InteractionManager.runAfterInteractions(() => { runOnJS(() => { - nonRepeatableProgress.value = withDelay( - itemEnterDelay, - withTiming(1, {duration: itemEnterDuration, easing: Easing.inOut(Easing.ease)}, (finished) => { - if (!finished) { - return; - } - - // eslint-disable-next-line react-compiler/react-compiler - repeatableProgress.value = withSequence( - withDelay(highlightStartDelay, withTiming(1, {duration: highlightStartDuration, easing: Easing.inOut(Easing.ease)})), - withDelay(highlightEndDelay, withTiming(0, {duration: highlightEndDuration, easing: Easing.inOut(Easing.ease)})), - ); - }), + nonRepeatableProgress.set( + withDelay( + itemEnterDelay, + withTiming(1, {duration: itemEnterDuration, easing: Easing.inOut(Easing.ease)}, (finished) => { + if (!finished) { + return; + } + + repeatableProgress.set( + withSequence( + withDelay(highlightStartDelay, withTiming(1, {duration: highlightStartDuration, easing: Easing.inOut(Easing.ease)})), + withDelay(highlightEndDelay, withTiming(0, {duration: highlightEndDuration, easing: Easing.inOut(Easing.ease)})), + ), + ); + }), + ), ); })(); }); diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 38efb8eb929f..a34e8a47e0cb 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -37,7 +37,7 @@ function SearchPageBottomTab() { const scrollOffset = useSharedValue(0); const topBarOffset = useSharedValue(variables.searchHeaderHeight); const topBarAnimatedStyle = useAnimatedStyle(() => ({ - top: topBarOffset.value, + top: topBarOffset.get(), })); const scrollHandler = useAnimatedScrollHandler({ @@ -47,15 +47,14 @@ function SearchPageBottomTab() { return; } const currentOffset = contentOffset.y; - const isScrollingDown = currentOffset > scrollOffset.value; - const distanceScrolled = currentOffset - scrollOffset.value; + const isScrollingDown = currentOffset > scrollOffset.get(); + const distanceScrolled = currentOffset - scrollOffset.get(); if (isScrollingDown && contentOffset.y > TOO_CLOSE_TO_TOP_DISTANCE) { - // eslint-disable-next-line react-compiler/react-compiler - topBarOffset.value = clamp(topBarOffset.value - distanceScrolled, variables.minimalTopBarOffset, variables.searchHeaderHeight); + topBarOffset.set(clamp(topBarOffset.get() - distanceScrolled, variables.minimalTopBarOffset, variables.searchHeaderHeight)); } else if (!isScrollingDown && distanceScrolled < 0 && contentOffset.y + layoutMeasurement.height < contentSize.height - TOO_CLOSE_TO_BOTTOM_DISTANCE) { - topBarOffset.value = withTiming(variables.searchHeaderHeight, {duration: ANIMATION_DURATION_IN_MS}); + topBarOffset.set(withTiming(variables.searchHeaderHeight, {duration: ANIMATION_DURATION_IN_MS})); } - scrollOffset.value = currentOffset; + scrollOffset.set(currentOffset); }, }); @@ -113,7 +112,7 @@ function SearchPageBottomTab() { { - topBarOffset.value = withTiming(variables.searchHeaderHeight, {duration: ANIMATION_DURATION_IN_MS}); + topBarOffset.set(withTiming(variables.searchHeaderHeight, {duration: ANIMATION_DURATION_IN_MS})); }} /> diff --git a/src/pages/home/report/AnimatedEmptyStateBackground.tsx b/src/pages/home/report/AnimatedEmptyStateBackground.tsx index d277862ebf2c..56ae27de5ea1 100644 --- a/src/pages/home/report/AnimatedEmptyStateBackground.tsx +++ b/src/pages/home/report/AnimatedEmptyStateBackground.tsx @@ -35,13 +35,12 @@ function AnimatedEmptyStateBackground() { * We use x and y gyroscope velocity and add it to position offset to move background based on device movements. * Position the phone was in while entering the screen is the initial position for background image. */ - const {x, y} = animatedSensor.sensor.value; + const {x, y} = animatedSensor.sensor.get(); // The x vs y here seems wrong but is the way to make it feel right to the user - // eslint-disable-next-line react-compiler/react-compiler - xOffset.value = clamp(xOffset.value + y * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_X, IMAGE_OFFSET_X); - yOffset.value = clamp(yOffset.value - x * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_Y, IMAGE_OFFSET_Y); + xOffset.set((value) => clamp(value + y * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_X, IMAGE_OFFSET_X)); + yOffset.set((value) => clamp(value - x * CONST.ANIMATION_GYROSCOPE_VALUE, -IMAGE_OFFSET_Y, IMAGE_OFFSET_Y)); return { - transform: [{translateX: withSpring(xOffset.value)}, {translateY: withSpring(yOffset.value, {overshootClamping: true})}, {scale: 1.15}], + transform: [{translateX: withSpring(xOffset.get())}, {translateY: withSpring(yOffset.get(), {overshootClamping: true})}, {scale: 1.15}], }; }, [isReducedMotionEnabled]); diff --git a/src/pages/home/report/FloatingMessageCounter.tsx b/src/pages/home/report/FloatingMessageCounter.tsx index 0d92946e3d66..a196704d2119 100644 --- a/src/pages/home/report/FloatingMessageCounter.tsx +++ b/src/pages/home/report/FloatingMessageCounter.tsx @@ -30,13 +30,13 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating const show = useCallback(() => { 'worklet'; - translateY.value = withSpring(MARKER_ACTIVE_TRANSLATE_Y); + translateY.set(withSpring(MARKER_ACTIVE_TRANSLATE_Y)); }, [translateY]); const hide = useCallback(() => { 'worklet'; - translateY.value = withSpring(MARKER_INACTIVE_TRANSLATE_Y); + translateY.set(withSpring(MARKER_INACTIVE_TRANSLATE_Y)); }, [translateY]); useEffect(() => { @@ -49,7 +49,7 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating const wrapperStyle = useAnimatedStyle(() => ({ ...styles.floatingMessageCounterWrapper, - transform: [{translateY: translateY.value}], + transform: [{translateY: translateY.get()}], })); return ( diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 1cb70fe6c926..9042c30440f7 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -709,20 +709,19 @@ function ComposerWithSuggestions( useEffect(() => { // We use the tag to store the native ID of the text input. Later, we use it in onSelectionChange to pick up the proper text input data. + tag.set(findNodeHandle(textInputRef.current) ?? -1); + }, [tag]); - tag.value = findNodeHandle(textInputRef.current) ?? -1; - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); useFocusedInputHandler( { onSelectionChange: (event) => { 'worklet'; - if (event.target === tag.value) { - cursorPositionValue.value = { + if (event.target === tag.get()) { + cursorPositionValue.set({ x: event.selection.end.x, y: event.selection.end.y, - }; + }); } }, }, @@ -731,7 +730,7 @@ function ComposerWithSuggestions( const measureParentContainerAndReportCursor = useCallback( (callback: MeasureParentContainerAndCursorCallback) => { const {scrollValue} = getScrollPosition({mobileInputScrollPosition, textInputRef}); - const {x: xPosition, y: yPosition} = getCursorPosition({positionOnMobile: cursorPositionValue.value, positionOnWeb: selection}); + const {x: xPosition, y: yPosition} = getCursorPosition({positionOnMobile: cursorPositionValue.get(), positionOnWeb: selection}); measureParentContainer((x, y, width, height) => { callback({ x, diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 23b059f2fda2..4673475063c8 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -342,7 +342,7 @@ function ReportActionCompose({ const handleSendMessage = useCallback(() => { 'worklet'; - const clearComposer = composerRefShared.value.clear; + const clearComposer = composerRefShared.get().clear; if (!clearComposer) { throw new Error('The composerRefShared.clear function is not set yet. This should never happen, and indicates a developer error.'); } @@ -468,10 +468,9 @@ function ReportActionCompose({ { composerRef.current = ref ?? undefined; - // eslint-disable-next-line react-compiler/react-compiler - composerRefShared.value = { + composerRefShared.set({ clear: ref?.clear, - }; + }); }} suggestionsRef={suggestionsRef} isNextModalWillOpenRef={isNextModalWillOpenRef} diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index fd2dc2d44d4b..8bcf5a462d38 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -417,7 +417,7 @@ function ReportActionItemMessageEdit( const measureParentContainerAndReportCursor = useCallback( (callback: MeasureParentContainerAndCursorCallback) => { const {scrollValue} = getScrollPosition({mobileInputScrollPosition, textInputRef}); - const {x: xPosition, y: yPosition} = getCursorPosition({positionOnMobile: cursorPositionValue.value, positionOnWeb: selection}); + const {x: xPosition, y: yPosition} = getCursorPosition({positionOnMobile: cursorPositionValue.get(), positionOnWeb: selection}); measureContainer((x, y, width, height) => { callback({ x, @@ -429,25 +429,23 @@ function ReportActionItemMessageEdit( }); }); }, - [cursorPositionValue.value, measureContainer, selection], + [cursorPositionValue, measureContainer, selection], ); useEffect(() => { // We use the tag to store the native ID of the text input. Later, we use it in onSelectionChange to pick up the proper text input data. - - // eslint-disable-next-line react-compiler/react-compiler - tag.value = findNodeHandle(textInputRef.current) ?? -1; + tag.set(findNodeHandle(textInputRef.current) ?? -1); }, [tag]); useFocusedInputHandler( { onSelectionChange: (event) => { 'worklet'; - if (event.target === tag.value) { - cursorPositionValue.value = { + if (event.target === tag.get()) { + cursorPositionValue.set({ x: event.selection.end.x, y: event.selection.end.y, - }; + }); } }, }, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index d3f0c9cb496d..61c856c856cf 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -120,8 +120,8 @@ function IOURequestStepScan({ const focusIndicatorPosition = useSharedValue({x: 0, y: 0}); const cameraFocusIndicatorAnimatedStyle = useAnimatedStyle(() => ({ - opacity: focusIndicatorOpacity.value, - transform: [{translateX: focusIndicatorPosition.value.x}, {translateY: focusIndicatorPosition.value.y}, {scale: focusIndicatorScale.value}], + opacity: focusIndicatorOpacity.get(), + transform: [{translateX: focusIndicatorPosition.get().x}, {translateY: focusIndicatorPosition.get().y}, {scale: focusIndicatorScale.get()}], })); const focusCamera = (point: Point) => { @@ -143,11 +143,10 @@ function IOURequestStepScan({ .onStart((ev: {x: number; y: number}) => { const point = {x: ev.x, y: ev.y}; - // eslint-disable-next-line react-compiler/react-compiler - focusIndicatorOpacity.value = withSequence(withTiming(0.8, {duration: 250}), withDelay(1000, withTiming(0, {duration: 250}))); - focusIndicatorScale.value = 2; - focusIndicatorScale.value = withSpring(1, {damping: 10, stiffness: 200}); - focusIndicatorPosition.value = point; + focusIndicatorOpacity.set(withSequence(withTiming(0.8, {duration: 250}), withDelay(1000, withTiming(0, {duration: 250})))); + focusIndicatorScale.set(2); + focusIndicatorScale.set(withSpring(1, {damping: 10, stiffness: 200})); + focusIndicatorPosition.set(point); runOnJS(focusCamera)(point); }); diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx index 75e0be7e5f7c..fc28dc08fb15 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx @@ -20,14 +20,15 @@ function BackgroundImage({width, transitionDuration, isSmallScreen = false}: Bac const isAnonymous = isAnonymousUser(); const opacity = useSharedValue(0); - const animatedStyle = useAnimatedStyle(() => ({opacity: opacity.value})); + const animatedStyle = useAnimatedStyle(() => ({opacity: opacity.get()})); // This sets the opacity animation for the background image once it has loaded. function setOpacityAnimation() { - // eslint-disable-next-line react-compiler/react-compiler - opacity.value = withTiming(1, { - duration: CONST.MICROSECONDS_PER_MS, - easing: Easing.ease, - }); + opacity.set( + withTiming(1, { + duration: CONST.MICROSECONDS_PER_MS, + easing: Easing.ease, + }), + ); } useEffect(() => { From 70253160c8bc659506327794b52ee853bfab2a08 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 5 Nov 2024 15:03:51 +0100 Subject: [PATCH 019/185] Refactor LoadingBar to use setter methods for animated values --- src/components/LoadingBar.tsx | 75 +++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/components/LoadingBar.tsx b/src/components/LoadingBar.tsx index 163ffe2aa66b..5650c3fe38ac 100644 --- a/src/components/LoadingBar.tsx +++ b/src/components/LoadingBar.tsx @@ -17,44 +17,49 @@ function LoadingBar({shouldShow}: LoadingBarProps) { useEffect(() => { if (shouldShow) { - // eslint-disable-next-line react-compiler/react-compiler - isVisible.value = true; - left.value = 0; - width.value = 0; - opacity.value = withTiming(1, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION}); - left.value = withDelay( - CONST.ANIMATED_PROGRESS_BAR_DELAY, - withRepeat( - withSequence( - withTiming(0, {duration: 0}), - withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), - withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + isVisible.set(true); + left.set(0); + width.set(0); + opacity.set(withTiming(1, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION})); + left.set( + withDelay( + CONST.ANIMATED_PROGRESS_BAR_DELAY, + withRepeat( + withSequence( + withTiming(0, {duration: 0}), + withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + ), + -1, + false, ), - -1, - false, ), ); - width.value = withDelay( - CONST.ANIMATED_PROGRESS_BAR_DELAY, - withRepeat( - withSequence( - withTiming(0, {duration: 0}), - withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), - withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + width.set( + withDelay( + CONST.ANIMATED_PROGRESS_BAR_DELAY, + withRepeat( + withSequence( + withTiming(0, {duration: 0}), + withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + ), + -1, + false, ), - -1, - false, ), ); - } else if (isVisible.value) { - opacity.value = withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION}, () => { - runOnJS(() => { - isVisible.value = false; - cancelAnimation(left); - cancelAnimation(width); - }); - }); + } else if (isVisible.get()) { + opacity.set( + withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION}, () => { + runOnJS(() => { + isVisible.set(false); + cancelAnimation(left); + cancelAnimation(width); + }); + }), + ); } // we want to update only when shouldShow changes // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps @@ -62,20 +67,20 @@ function LoadingBar({shouldShow}: LoadingBarProps) { const animatedIndicatorStyle = useAnimatedStyle(() => { return { - left: `${left.value}%`, - width: `${width.value}%`, + left: `${left.get()}%`, + width: `${width.get()}%`, }; }); const animatedContainerStyle = useAnimatedStyle(() => { return { - opacity: opacity.value, + opacity: opacity.get(), }; }); return ( - {isVisible.value ? : null} + {isVisible.get() ? : null} ); } From 166923b0a96fd200a493d916204cab488c0c86a2 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 5 Nov 2024 14:17:09 -0700 Subject: [PATCH 020/185] update param name --- src/components/Search/SearchPageHeader.tsx | 4 ++-- src/components/Search/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index dc3efae4bdbc..05ae72654daf 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -218,11 +218,11 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { } const paymentData = ( selectedReports.length - ? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentMethod: lastPaymentMethods[report.policyID]})) + ? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentType: lastPaymentMethods[report.policyID]})) : Object.values(selectedTransactions).map((transaction) => ({ reportID: transaction.reportID, amount: transaction.amount, - paymentMethod: lastPaymentMethods[transaction.policyID], + paymentType: lastPaymentMethods[transaction.policyID], })) ) as PaymentData[]; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 9b97ac446d5b..d88a4e3dd3be 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -45,7 +45,7 @@ type SelectedReports = { type PaymentData = { reportID: string; amount: number; - paymentMethod: ValueOf; + paymentType: ValueOf; }; type SortOrder = ValueOf; From 8277ee5023ae528d5180a0033266139a3ed568ea Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 5 Nov 2024 15:36:12 -0700 Subject: [PATCH 021/185] fix ts --- src/libs/actions/Search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index dca82a66fd9c..f343d0d6b849 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -39,7 +39,7 @@ function handleActionButtonPress(hash: number, item: TransactionListItemType | R const amount = isReportListItemType(item) ? item.total ?? 0 : item.formattedTotal; switch (item.action) { case CONST.SEARCH.ACTION_TYPES.PAY: - return lastPolicyPaymentMethod ? payMoneyRequestOnSearch(hash, [{reportID: item.reportID, amount, paymentMethod: lastPolicyPaymentMethod}]) : goToItem(); + return lastPolicyPaymentMethod ? payMoneyRequestOnSearch(hash, [{reportID: item.reportID, amount, paymentType: lastPolicyPaymentMethod}]) : goToItem(); case CONST.SEARCH.ACTION_TYPES.APPROVE: return approveMoneyRequestOnSearch(hash, [item.reportID]); default: From 0d3a83fcfba17c3209a62df4a6acaee3a13456b3 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 5 Nov 2024 15:50:05 -0700 Subject: [PATCH 022/185] update docs --- src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts b/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts index ee51a1961448..a12c0fdc32f2 100644 --- a/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts +++ b/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts @@ -2,7 +2,7 @@ type PayMoneyRequestOnSearchParams = { hash: number; /** * Stringified JSON object with type of following structure: - * Array<{reportID: string, amount: number, paymentMethod: string}> + * Array<{reportID: string, amount: number, paymentType: string}> */ paymentData: string; }; From 317183449f69eb0b1f6486e8e4b14c0a713f6ddd Mon Sep 17 00:00:00 2001 From: layacat Date: Wed, 6 Nov 2024 10:31:09 +0700 Subject: [PATCH 023/185] fix: 50485 mWeb/Chrome - IOU- Continue and green button not working in scan page --- .../new-expensify/expenses-&-payments/Create-an-expense.md | 5 +++++ src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md index cf6a13f9d5ac..5b43223ae3c3 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md @@ -40,6 +40,11 @@ You can create an expense to request payment from an employer’s workspace or f You can also forward receipts to receipts@expensify.com using an email address that is your primary or secondary email address. SmartScan will automatically pull all of the details from the receipt and add it to your expenses. {% include end-info.html %} +{% include info.html %} +If the Continue button doesn't open the camera on your mobile web browser, +it might be because you denied camera permission. Learn how to enable camera permission in your browser [here](https://support.google.com/chrome/answer/2693767). +{% include end-info.html %} + # Manually add an expense {% include selector.html values="desktop, mobile" %} diff --git a/src/languages/en.ts b/src/languages/en.ts index 21f220b747f2..5a56363ad3da 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -779,7 +779,7 @@ const translations = { dragReceiptAfterEmail: ' or choose a file to upload below.', chooseReceipt: 'Choose a receipt to upload or forward a receipt to ', takePhoto: 'Take a photo', - cameraAccess: 'Camera access is required to take pictures of receipts.', + cameraAccess: "Camera access still hasn't been granted, please follow these instructions", cameraErrorTitle: 'Camera error', cameraErrorMessage: 'An error occurred while taking a photo. Please try again.', locationAccessTitle: 'Allow location access', diff --git a/src/languages/es.ts b/src/languages/es.ts index ac06741f467e..a1ef94209c88 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -773,7 +773,7 @@ const translations = { dragReceiptAfterEmail: ' o elije un archivo para subir a continuación.', chooseReceipt: 'Elige un recibo para subir o reenvía un recibo a ', takePhoto: 'Haz una foto', - cameraAccess: 'Se requiere acceso a la cámara para hacer fotos de los recibos.', + cameraAccess: 'Si aún no se ha concedido el acceso a la cámara, siga estas instrucciones', cameraErrorTitle: 'Error en la cámara', locationAccessTitle: 'Permitir acceso a la ubicación', locationAccessMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', From 2d8e017514c80c23f255d32a5511379b6bcd2491 Mon Sep 17 00:00:00 2001 From: layacat Date: Wed, 6 Nov 2024 18:39:05 +0700 Subject: [PATCH 024/185] Fix show only when denied --- src/languages/en.ts | 4 ++- src/languages/es.ts | 4 ++- .../request/step/IOURequestStepScan/index.tsx | 28 +++++++++++++------ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 5a56363ad3da..065470c1eba8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -779,7 +779,9 @@ const translations = { dragReceiptAfterEmail: ' or choose a file to upload below.', chooseReceipt: 'Choose a receipt to upload or forward a receipt to ', takePhoto: 'Take a photo', - cameraAccess: "Camera access still hasn't been granted, please follow these instructions", + cameraAccess: 'Camera access is required to take pictures of receipts.', + deniedCameraAccess: "Camera access still hasn't been granted, please follow ", + deniedCameraAccessInstructions: 'these instructions', cameraErrorTitle: 'Camera error', cameraErrorMessage: 'An error occurred while taking a photo. Please try again.', locationAccessTitle: 'Allow location access', diff --git a/src/languages/es.ts b/src/languages/es.ts index a1ef94209c88..d8e2ccf206e4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -773,7 +773,9 @@ const translations = { dragReceiptAfterEmail: ' o elije un archivo para subir a continuación.', chooseReceipt: 'Elige un recibo para subir o reenvía un recibo a ', takePhoto: 'Haz una foto', - cameraAccess: 'Si aún no se ha concedido el acceso a la cámara, siga estas instrucciones', + cameraAccess: 'Se requiere acceso a la cámara para hacer fotos de los recibos.', + deniedCameraAccess: 'Si aún no se ha concedido el acceso a la cámara, siga ', + deniedCameraAccessInstructions: 'estas instrucciones', cameraErrorTitle: 'Error en la cámara', locationAccessTitle: 'Permitir acceso a la ubicación', locationAccessMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 4ed956e5ce7e..e3a6fc1f8cb5 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -20,6 +20,7 @@ import LocationPermissionModal from '@components/LocationPermissionModal'; import PDFThumbnail from '@components/PDFThumbnail'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; +import TextLink from '@components/TextLink'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; @@ -629,14 +630,25 @@ function IOURequestStepScan({ additionalStyles={[styles.pb5]} /> {translate('receipt.takePhoto')} - {translate('receipt.cameraAccess')} -