diff --git a/android/app/build.gradle b/android/app/build.gradle
index 62e30858e73c..0b2271d16716 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001045602
- versionName "1.4.56-2"
+ versionCode 1001045605
+ versionName "1.4.56-5"
}
flavorDimensions "default"
diff --git a/docs/articles/expensify-classic/expenses/Distance-Tracking.md b/docs/articles/expensify-classic/expenses/Distance-Tracking.md
deleted file mode 100644
index c0d8956f71ac..000000000000
--- a/docs/articles/expensify-classic/expenses/Distance-Tracking.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-title: Distance Tracking in Expensify
-description: Learn how distance tracking works in Expensify!
----
-
-# Overview
-
-Expensify provides a convenient feature for tracking your mileage-related expenses. You'll find all the essential information to begin logging your trips below.
-
-# How to Use Distance Tracking
-## Mobile App
-
-First, you’ll want to click the **+** in the top right corner.
-
-If you select **Manually Create**, you’ll be prompted to enter your mileage, select a rate, and code the expense before clicking **Save**.
-
- ![Click manually create or odometer to create a distance request.](https://help.expensify.com/assets/images/ExpensifyHelp_CreateExpense_Mobile.png){:width="100%"}
-
-If you select **Manually Create**:
- - Enter your mileage.
- - Select a rate.
- - Code the expense.
- - Click **Save**.
-
-![Enter your mileage, rate, code the expense, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_ManualDistance_Mobile.png){:width="100%"}
-
-If you select **Odometer**:
- - Enter your vehicle’s mileage reading before and after your trip.
- - Select your rate.
- - Code the expense.
- - Click **Save**.
-
-![Etner your mileage readings, your rate, code the expense, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_Odometer_Mobile.png){:width="100%"}
-
-The **Start GPS** option also exists on the mobile app. However, we’ve learned that most customers prefer to track their mileage after their trips (thus not needing to hit that start button!)
-
-We’ve temporarily paused the development of GPS mileage tracking in the mobile app, and we recommend you use one of the above options instead!
-
-
-## Web
-
-Navigate to the **Expenses** page, click **New Expense**, and review the two **Distance** options.
-
-![Select manually create or create from map to create a new distance request.](https://help.expensify.com/assets/images/ExpensifyHelp_CreateExpense.png){:width="100%"}
-
-If you select **Manually Create**:
- - Enter the number of miles for your trip.
- - Mileage rate is automatically selected based on your history, or manually select it if it's your first time.
- - Complete any other applicable coding fields.
- - Click **Save**.
-
-![Enter the number of miles, select your rate, code the expense, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_ManualDistance.png){:width="100%"}
-
-For **Create from Map** expenses:
- - Add your start and end location, and the distance will be calculated.
- - You can also click **Add Destination** for multiple stops.
- - Leave **Create Receipt** selected if you want a map receipt generated.
- - Click **Save**.
-
-![Enter your start and end locations, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_ManualDistanceMap.png){:width="100%"}
-
-Once you click **Save**, review the details from your map selection.
- - Select your rate.
- - Enter any other applicable coding.
- - Click **Save**.
-
-![Select your rate, code the expense, and click save.](https://help.expensify.com/assets/images/ExpensifyHelp_ManualDistanceConfirm.png){:width="100%"}
-
-# Mileage Tracking FAQs
-## **How can I change the rate of my mileage expenses?**
-You can change the rate by going to Settings > Workspaces > [Your Workspace] > Expenses > Distance > Add a Mileage Rate.
-If you submit mileage expenses on a group workspace, only workspace admins can do this.
-
-## **Do you plan to add the "Create from Map" option to the mobile app or "Odometer" option to web?**
-Not now, but if that changes, you'll be the first to know!
-
-## **Will you restart maintenance on the mobile app's GPS option anytime soon?**
-Not now, but if that changes, you'll be the first to know!
-
-## **Does Expensify automatically update IRS Mileage rates?**
- We never automatically update mileage rates in Expensify because different companies want the new rates to become effective on different dates.
diff --git a/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md b/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md
new file mode 100644
index 000000000000..e8b9ab0eac75
--- /dev/null
+++ b/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md
@@ -0,0 +1,66 @@
+---
+title: Track mileage expenses
+description: Add mileage-related expenses
+---
+
+
+
+You can track your mileage-related expenses by logging your trips in Expensify. You have a couple of different options for logging distance:
+
+- Web app:
+ - **Manually create**: Manually enter the number of miles for the trip
+ - **Create from map**: Automatically determine the trip distance based on the start and end location.
+- Mobile app:
+ - **Manually create**: Manually enter the miles for the trip and your mileage rate
+ - **Odometer**: Enter your odometer reading before and after the trip
+ - **Start GPS**: Currently under development and unavailable for use.
+
+{% include info.html %}
+When adding a distance expense, the rates available are determined by the rates set in your workspace rate settings. To update these rates or add a new rate, you must be a Workspace Admin.
+{% include end-info.html %}
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+
+1. Click the **Expenses** tab.
+2. Click **New Expense**.
+3. Select the expense type.
+ - **Manually create**:
+ - Enter the number of miles for the trip.
+ - Select your rate.
+ - If desired, select the category, add a description, or select a report to add the expense to.
+ - Click **Save**.
+ - **Create from map**:
+ - Add your start location as point A.
+ - Add your end location as point B.
+ - If applicable, click **Add Destination** to add additional stops.
+ - To generate a map receipt, leave the Create Receipt checkbox selected.
+ - Click **Save**.
+ - Select your rate.
+ - If desired, select the category, add a description, or select a report to add the expense to.
+ - Click **Save**.
+
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+
+1. Click the + icon in the top right corner.
+2. Under the Distance section, select the expense type.
+ - **Manually create**:
+ - Enter your mileage.
+ - Select your rate.
+ - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to.
+ - Click **Save**.
+ - **Odometer**:
+ - Enter your vehicle’s odometer reading before the trip.
+ - Enter your vehicle’s odometer reading after the trip.
+ - Select your rate.
+ - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to.
+ - Click **Save**.
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+
+
diff --git a/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md b/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md
new file mode 100644
index 000000000000..29fbc8b46323
--- /dev/null
+++ b/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md
@@ -0,0 +1,26 @@
+---
+title: Change member workspace roles
+description: Update a member's role for a workspace
+---
+
+
+To change the roles and permissions for members of your workspace,
+
+1. Hover over Settings, then click **Workspaces**.
+2. Click the **Group** tab on the left.
+3. Click the desired workspace name.
+4. Click the **Members** tab on the left.
+5. Click the Settings icon next to the desired member.
+6. Select a new role for the member.
+
+| | Employee | Auditor | Workspace Admin |
+|---------------------------|----------------------------------|---------|-----------------|
+| Submit reports | Yes | Yes | Yes |
+| Comment on reports | Yes | Yes | Yes |
+| Approve workspace reports | Only reports submitted to them | Yes | Yes |
+| Edit workspace settings | No | No | Yes |
+
+7. If your workspace uses Advanced Approvals, select an “Approves to.” This determines who the member’s reports must be approved by, if applicable. If “no one” is selected, then any one with the Auditor or Workspace Admin role can approve the member’s reports.
+8. Click **Save**.
+
+
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index a962c69f0bc6..37741d484d63 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.56.2
+ 1.4.56.5ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 9f20eb574abc..e228808076d0 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature????CFBundleVersion
- 1.4.56.2
+ 1.4.56.5
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 2319ff879a03..11f83011d47a 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString1.4.56CFBundleVersion
- 1.4.56.2
+ 1.4.56.5NSExtensionNSExtensionPointIdentifier
diff --git a/metro.config.js b/metro.config.js
index 2422d29aaacf..68ed72d52ba0 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -7,7 +7,7 @@ require('dotenv').config();
const defaultConfig = getDefaultConfig(__dirname);
const isE2ETesting = process.env.E2E_TESTING === 'true';
-const e2eSourceExts = ['e2e.js', 'e2e.ts'];
+const e2eSourceExts = ['e2e.js', 'e2e.ts', 'e2e.tsx'];
/**
* Metro configuration
diff --git a/package-lock.json b/package-lock.json
index 22c26e83b2e6..60823bc5091d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.56-2",
+ "version": "1.4.56-5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.56-2",
+ "version": "1.4.56-5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -73,6 +73,7 @@
"react-content-loader": "^7.0.0",
"react-dom": "18.1.0",
"react-error-boundary": "^4.0.11",
+ "react-fast-pdf": "^1.0.6",
"react-map-gl": "^7.1.3",
"react-native": "0.73.2",
"react-native-android-location-enabler": "^2.0.1",
@@ -25301,7 +25302,6 @@
},
"node_modules/dequal": {
"version": "2.0.3",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -39048,6 +39048,74 @@
"react": ">=16.13.1"
}
},
+ "node_modules/react-fast-pdf": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.6.tgz",
+ "integrity": "sha512-CdAnBSZaLCGLSEuiqWLzzXhV9Wvdf1VRixaXCrb3NFrXyeltahF7PY+u7eU6ynrWZGmNI6g0cMLPv0DQhJEeew==",
+ "dependencies": {
+ "react-pdf": "^7.7.0",
+ "react-window": "^1.8.10"
+ },
+ "engines": {
+ "node": "20.10.0",
+ "npm": "10.2.3"
+ },
+ "peerDependencies": {
+ "lodash": "4.x",
+ "prop-types": "15.x",
+ "react": "18.x",
+ "react-dom": "18.x"
+ }
+ },
+ "node_modules/react-fast-pdf/node_modules/pdfjs-dist": {
+ "version": "3.11.174",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
+ "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==",
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "canvas": "^2.11.2",
+ "path2d-polyfill": "^2.0.1"
+ }
+ },
+ "node_modules/react-fast-pdf/node_modules/react-pdf": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.1.tgz",
+ "integrity": "sha512-cbbf/PuRtGcPPw+HLhMI1f6NSka8OJgg+j/yPWTe95Owf0fK6gmVY7OXpTxMeh92O3T3K3EzfE0ML0eXPGwR5g==",
+ "dependencies": {
+ "clsx": "^2.0.0",
+ "dequal": "^2.0.3",
+ "make-cancellable-promise": "^1.3.1",
+ "make-event-props": "^1.6.0",
+ "merge-refs": "^1.2.1",
+ "pdfjs-dist": "3.11.174",
+ "prop-types": "^15.6.2",
+ "tiny-invariant": "^1.0.0",
+ "warning": "^4.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-fast-pdf/node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/react-freeze": {
"version": "1.0.3",
"license": "MIT",
@@ -40524,8 +40592,9 @@
}
},
"node_modules/react-window": {
- "version": "1.8.9",
- "license": "MIT",
+ "version": "1.8.10",
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz",
+ "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==",
"dependencies": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
diff --git a/package.json b/package.json
index f3d062313bf6..8395f2253591 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.56-2",
+ "version": "1.4.56-5",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -124,6 +124,7 @@
"react-content-loader": "^7.0.0",
"react-dom": "18.1.0",
"react-error-boundary": "^4.0.11",
+ "react-fast-pdf": "^1.0.6",
"react-map-gl": "^7.1.3",
"react-native": "0.73.2",
"react-native-android-location-enabler": "^2.0.1",
diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh
index 43ed481e2dc0..791f59d73330 100755
--- a/scripts/build-desktop.sh
+++ b/scripts/build-desktop.sh
@@ -13,8 +13,18 @@ else
ENV_FILE=".env"
fi
+if [[ -n "$GCP_GEOLOCATION_API_KEY" ]]; then
+ if grep -qE "^GCP_GEOLOCATION_API_KEY=" "$ENV_FILE"; then
+ # Replace the value for the existing key
+ sed -i "s|^GCP_GEOLOCATION_API_KEY=.*$|GCP_GEOLOCATION_API_KEY=$GCP_GEOLOCATION_API_KEY|g" "$ENV_FILE"
+ else
+ # Add the key-value pair to the config file
+ echo "GCP_GEOLOCATION_API_KEY=$GCP_GEOLOCATION_API_KEY" >> "$ENV_FILE"
+ fi
+fi
+
SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]}")
-source "$SCRIPTS_DIR/shellUtils.sh";
+source "$SCRIPTS_DIR/shellUtils.sh"
title "Bundling Desktop js Bundle Using Webpack"
info " • ELECTRON_ENV: $ELECTRON_ENV"
diff --git a/src/CONST.ts b/src/CONST.ts
index 12c254736cdb..3109b9ea90ca 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1851,13 +1851,6 @@ const CONST = {
MAX_INT_FOR_RANDOM_7_DIGIT_VALUE: 10000000,
IOS_KEYBOARD_SPACE_OFFSET: -30,
- PDF_PASSWORD_FORM: {
- // Constants for password-related error responses received from react-pdf.
- REACT_PDF_PASSWORD_RESPONSES: {
- NEED_PASSWORD: 1,
- INCORRECT_PASSWORD: 2,
- },
- },
API_REQUEST_TYPE: {
READ: 'read',
WRITE: 'write',
@@ -4150,6 +4143,8 @@ const CONST = {
},
},
},
+
+ MAX_TAX_RATE_DECIMAL_PLACES: 4,
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index c216d5ac288c..6302e0ee4683 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -179,7 +179,7 @@ const ROUTES = {
REPORT: 'r',
REPORT_WITH_ID: {
route: 'r/:reportID?/:reportActionID?',
- getRoute: (reportID: string) => `r/${reportID}` as const,
+ getRoute: (reportID: string, reportActionID?: string) => (reportActionID ? (`r/${reportID}/${reportActionID}` as const) : (`r/${reportID}` as const)),
},
REPORT_AVATAR: {
route: 'r/:reportID/avatar',
diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.tsx
similarity index 63%
rename from src/components/AddPaymentMethodMenu.js
rename to src/components/AddPaymentMethodMenu.tsx
index 803b7f2cdabe..ac9657694500 100644
--- a/src/components/AddPaymentMethodMenu.js
+++ b/src/components/AddPaymentMethodMenu.tsx
@@ -1,78 +1,75 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
+import type {RefObject} from 'react';
import React from 'react';
+import type {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import useLocalize from '@hooks/useLocalize';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
-import iouReportPropTypes from '@pages/iouReportPropTypes';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {AnchorPosition} from '@src/styles';
+import type {Report, Session} from '@src/types/onyx';
+import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
+import type {EmptyObject} from '@src/types/utils/EmptyObject';
import * as Expensicons from './Icon/Expensicons';
+import type {PaymentMethod} from './KYCWall/types';
import PopoverMenu from './PopoverMenu';
-import refPropTypes from './refPropTypes';
-const propTypes = {
+type AddPaymentMethodMenuOnyxProps = {
+ /** Session info for the currently logged-in user. */
+ session: OnyxEntry;
+};
+
+type AddPaymentMethodMenuProps = AddPaymentMethodMenuOnyxProps & {
/** Should the component be visible? */
- isVisible: PropTypes.bool.isRequired,
+ isVisible: boolean;
/** Callback to execute when the component closes. */
- onClose: PropTypes.func.isRequired,
+ onClose: () => void;
/** Callback to execute when the payment method is selected. */
- onItemSelected: PropTypes.func.isRequired,
+ onItemSelected: (paymentMethod: PaymentMethod) => void;
/** The IOU/Expense report we are paying */
- iouReport: iouReportPropTypes,
+ iouReport?: OnyxEntry | EmptyObject;
/** Anchor position for the AddPaymentMenu. */
- anchorPosition: PropTypes.shape({
- horizontal: PropTypes.number,
- vertical: PropTypes.number,
- }),
+ anchorPosition: AnchorPosition;
/** Where the popover should be positioned relative to the anchor points. */
- anchorAlignment: PropTypes.shape({
- horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
- vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
- }),
+ anchorAlignment?: AnchorAlignment;
/** Popover anchor ref */
- anchorRef: refPropTypes,
-
- /** Session info for the currently logged in user. */
- session: PropTypes.shape({
- /** Currently logged in user accountID */
- accountID: PropTypes.number,
- }),
+ anchorRef: RefObject;
/** Whether the personal bank account option should be shown */
- shouldShowPersonalBankAccountOption: PropTypes.bool,
+ shouldShowPersonalBankAccountOption?: boolean;
};
-const defaultProps = {
- iouReport: {},
- anchorPosition: {},
- anchorAlignment: {
+function AddPaymentMethodMenu({
+ isVisible,
+ onClose,
+ anchorPosition,
+ anchorAlignment = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
},
- anchorRef: () => {},
- session: {},
- shouldShowPersonalBankAccountOption: false,
-};
-
-function AddPaymentMethodMenu({isVisible, onClose, anchorPosition, anchorAlignment, anchorRef, iouReport, onItemSelected, session, shouldShowPersonalBankAccountOption}) {
+ anchorRef,
+ iouReport,
+ onItemSelected,
+ session,
+ shouldShowPersonalBankAccountOption = false,
+}: AddPaymentMethodMenuProps) {
const {translate} = useLocalize();
// Users can choose to pay with business bank account in case of Expense reports or in case of P2P IOU report
// which then starts a bottom up flow and creates a Collect workspace where the payer is an admin and payee is an employee.
+ const isIOUReport = ReportUtils.isIOUReport(iouReport ?? {});
const canUseBusinessBankAccount =
- ReportUtils.isExpenseReport(iouReport) ||
- (ReportUtils.isIOUReport(iouReport) && !ReportActionsUtils.hasRequestFromCurrentAccount(lodashGet(iouReport, 'reportID', 0), lodashGet(session, 'accountID', 0)));
+ ReportUtils.isExpenseReport(iouReport ?? {}) || (isIOUReport && !ReportActionsUtils.hasRequestFromCurrentAccount(iouReport?.reportID ?? '', session?.accountID ?? 0));
- const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || ReportUtils.isIOUReport(iouReport);
+ const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || isIOUReport;
return (
({
session: {
key: ONYXKEYS.SESSION,
},
diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx
index 48035dd884bd..e947c74f7c60 100644
--- a/src/components/AmountForm.tsx
+++ b/src/components/AmountForm.tsx
@@ -37,6 +37,9 @@ type AmountFormProps = {
/** Whether the currency symbol is pressable */
isCurrencyPressable?: boolean;
+
+ /** Custom max amount length. It defaults to CONST.IOU.AMOUNT_MAX_LENGTH */
+ amountMaxLength?: number;
} & Pick &
Pick;
@@ -53,7 +56,7 @@ const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView';
const NUM_PAD_VIEW_ID = 'numPadView';
function AmountForm(
- {value: amount, currency = CONST.CURRENCY.USD, extraDecimals = 0, errorText, onInputChange, onCurrencyButtonPress, isCurrencyPressable = true, ...rest}: AmountFormProps,
+ {value: amount, currency = CONST.CURRENCY.USD, extraDecimals = 0, amountMaxLength, errorText, onInputChange, onCurrencyButtonPress, isCurrencyPressable = true, ...rest}: AmountFormProps,
forwardedRef: ForwardedRef,
) {
const styles = useThemeStyles();
@@ -101,7 +104,7 @@ function AmountForm(
const newAmountWithoutSpaces = MoneyRequestUtils.stripSpacesFromAmount(newAmount);
// Use a shallow copy of selection to trigger setSelection
// More info: https://github.com/Expensify/App/issues/16385
- if (!MoneyRequestUtils.validateAmount(newAmountWithoutSpaces, decimals)) {
+ if (!MoneyRequestUtils.validateAmount(newAmountWithoutSpaces, decimals, amountMaxLength)) {
setSelection((prevSelection) => ({...prevSelection}));
return;
}
@@ -111,13 +114,13 @@ function AmountForm(
setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : currentAmount.length, strippedAmount.length));
onInputChange?.(strippedAmount);
},
- [currentAmount, decimals, onInputChange],
+ [amountMaxLength, currentAmount, decimals, onInputChange],
);
// Modifies the amount to match the decimals for changed currency.
useEffect(() => {
// If the changed currency supports decimals, we can return
- if (MoneyRequestUtils.validateAmount(currentAmount, decimals)) {
+ if (MoneyRequestUtils.validateAmount(currentAmount, decimals, amountMaxLength)) {
return;
}
diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
index d389ac4b92f0..da8e3694a7d2 100644
--- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
+++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
@@ -59,7 +59,6 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow
role={CONST.ROLE.BUTTON}
>
;
};
-type Attachment = {
- source: AvatarSource;
- isAuthTokenRequired: boolean;
- file: FileObject;
- isReceipt: boolean;
- hasBeenFlagged?: boolean;
- reportActionID?: string;
-};
-
type ImagePickerResponse = {
height: number;
name: string;
@@ -79,7 +71,7 @@ type ImagePickerResponse = {
width: number;
};
-type FileObject = File | ImagePickerResponse;
+type FileObject = Partial;
type ChildrenProps = {
displayFileInModal: (data: FileObject) => void;
@@ -181,7 +173,7 @@ function AttachmentModal({
const [isAuthTokenRequiredState, setIsAuthTokenRequiredState] = useState(isAuthTokenRequired);
const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(null);
const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null);
- const [sourceState, setSourceState] = useState(() => source);
+ const [sourceState, setSourceState] = useState(() => source);
const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE);
const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false);
const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1));
@@ -190,7 +182,7 @@ function AttachmentModal({
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid);
- const [file, setFile] = useState | undefined>(
+ const [file, setFile] = useState(
originalFileName
? {
name: originalFileName,
@@ -211,7 +203,7 @@ function AttachmentModal({
(attachment: Attachment) => {
setSourceState(attachment.source);
setFile(attachment.file);
- setIsAuthTokenRequiredState(attachment.isAuthTokenRequired);
+ setIsAuthTokenRequiredState(attachment.isAuthTokenRequired ?? false);
onCarouselAttachmentChange(attachment);
},
[onCarouselAttachmentChange],
@@ -222,7 +214,7 @@ function AttachmentModal({
*/
const getModalType = useCallback(
(sourceURL: string, fileObject: FileObject) =>
- sourceURL && (Str.isPDF(sourceURL) || (fileObject && Str.isPDF(fileObject.name || translate('attachmentView.unknownFilename'))))
+ sourceURL && (Str.isPDF(sourceURL) || (fileObject && Str.isPDF(fileObject.name ?? translate('attachmentView.unknownFilename'))))
? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE
: CONST.MODAL.MODAL_TYPE.CENTERED,
[translate],
@@ -292,14 +284,14 @@ function AttachmentModal({
}, [transaction, report]);
const isValidFile = useCallback((fileObject: FileObject) => {
- if (fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
+ if (fileObject.size !== undefined && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooLarge');
setAttachmentInvalidReason('attachmentPicker.sizeExceeded');
return false;
}
- if (fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
+ if (fileObject.size !== undefined && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooSmall');
setAttachmentInvalidReason('attachmentPicker.sizeNotMet');
@@ -352,7 +344,7 @@ function AttachmentModal({
setSourceState(inputSource);
setFile(updatedFile);
setModalType(inputModalType);
- } else {
+ } else if (fileObject.uri) {
const inputModalType = getModalType(fileObject.uri, fileObject);
setIsModalOpen(true);
setSourceState(fileObject.uri);
@@ -536,7 +528,6 @@ function AttachmentModal({
onNavigate={onNavigate}
onClose={closeModal}
source={source}
- onToggleKeyboard={updateConfirmButtonVisibility}
setDownloadButtonVisibility={setDownloadButtonVisibility}
/>
) : (
@@ -546,7 +537,6 @@ function AttachmentModal({
!shouldShowNotFoundPage && (
({
},
})(memo(AttachmentModal));
-export type {Attachment, FileObject};
+export type {FileObject};
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx
similarity index 71%
rename from src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js
rename to src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx
index f4cbffc0e1e4..839e05c419df 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx
@@ -1,19 +1,15 @@
-import PropTypes from 'prop-types';
import React from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
import {PixelRatio, View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-const propTypes = {
+type AttachmentCarouselCellRendererProps = {
/** Cell Container styles */
- style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+ style?: StyleProp;
};
-const defaultProps = {
- style: [],
-};
-
-function AttachmentCarouselCellRenderer(props) {
+function AttachmentCarouselCellRenderer(props: AttachmentCarouselCellRendererProps) {
const styles = useThemeStyles();
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true);
@@ -28,8 +24,6 @@ function AttachmentCarouselCellRenderer(props) {
);
}
-AttachmentCarouselCellRenderer.propTypes = propTypes;
-AttachmentCarouselCellRenderer.defaultProps = defaultProps;
AttachmentCarouselCellRenderer.displayName = 'AttachmentCarouselCellRenderer';
export default React.memo(AttachmentCarouselCellRenderer);
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselActions.js b/src/components/Attachments/AttachmentCarousel/CarouselActions.tsx
similarity index 73%
rename from src/components/Attachments/AttachmentCarousel/CarouselActions.js
rename to src/components/Attachments/AttachmentCarousel/CarouselActions.tsx
index cf5309222c4e..6138f07809c5 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselActions.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselActions.tsx
@@ -1,25 +1,22 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import {useEffect} from 'react';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import CONST from '@src/CONST';
-const propTypes = {
+type CarouselActionsProps = {
/** Callback to cycle through attachments */
- onCycleThroughAttachments: PropTypes.func.isRequired,
+ onCycleThroughAttachments: (deltaSlide: number) => void;
};
-function CarouselActions({onCycleThroughAttachments}) {
+function CarouselActions({onCycleThroughAttachments}: CarouselActionsProps) {
useEffect(() => {
const shortcutLeftConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT;
const unsubscribeLeftKey = KeyboardShortcut.subscribe(
shortcutLeftConfig.shortcutKey,
- (e) => {
- if (lodashGet(e, 'target.blur')) {
+ (event) => {
+ if (event?.target instanceof HTMLElement) {
// prevents focus from highlighting around the modal
- e.target.blur();
+ event.target.blur();
}
-
onCycleThroughAttachments(-1);
},
shortcutLeftConfig.descriptionKey,
@@ -29,12 +26,11 @@ function CarouselActions({onCycleThroughAttachments}) {
const shortcutRightConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT;
const unsubscribeRightKey = KeyboardShortcut.subscribe(
shortcutRightConfig.shortcutKey,
- (e) => {
- if (lodashGet(e, 'target.blur')) {
+ (event) => {
+ if (event?.target instanceof HTMLElement) {
// prevents focus from highlighting around the modal
- e.target.blur();
+ event.target.blur();
}
-
onCycleThroughAttachments(1);
},
shortcutRightConfig.descriptionKey,
@@ -50,6 +46,4 @@ function CarouselActions({onCycleThroughAttachments}) {
return null;
}
-CarouselActions.propTypes = propTypes;
-
export default CarouselActions;
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
similarity index 75%
rename from src/components/Attachments/AttachmentCarousel/CarouselButtons.js
rename to src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
index a2c5dadb101d..2037ebdab086 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
@@ -1,8 +1,6 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
-import _ from 'underscore';
-import * as AttachmentCarouselViewPropTypes from '@components/Attachments/propTypes';
+import type {Attachment} from '@components/Attachments/types';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import Tooltip from '@components/Tooltip';
@@ -11,36 +9,34 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-const propTypes = {
+type CarouselButtonsProps = {
/** Where the arrows should be visible */
- shouldShowArrows: PropTypes.bool.isRequired,
+ shouldShowArrows: boolean;
/** The current page index */
- page: PropTypes.number.isRequired,
+ page: number;
/** The attachments from the carousel */
- attachments: AttachmentCarouselViewPropTypes.attachmentsPropType.isRequired,
+ attachments: Attachment[];
/** Callback to go one page back */
- onBack: PropTypes.func.isRequired,
+ onBack: () => void;
+
/** Callback to go one page forward */
- onForward: PropTypes.func.isRequired,
+ onForward: () => void;
- autoHideArrow: PropTypes.func,
- cancelAutoHideArrow: PropTypes.func,
-};
+ /** Callback for autohiding carousel button arrows */
+ autoHideArrow?: () => void;
-const defaultProps = {
- autoHideArrow: () => {},
- cancelAutoHideArrow: () => {},
+ /** Callback for cancelling autohiding of carousel button arrows */
+ cancelAutoHideArrow?: () => void;
};
-function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}) {
+function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}: CarouselButtonsProps) {
const theme = useTheme();
const styles = useThemeStyles();
const isBackDisabled = page === 0;
- const isForwardDisabled = page === _.size(attachments) - 1;
-
+ const isForwardDisabled = page === attachments.length - 1;
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
@@ -80,8 +76,6 @@ function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward
) : null;
}
-CarouselButtons.propTypes = propTypes;
-CarouselButtons.defaultProps = defaultProps;
CarouselButtons.displayName = 'CarouselButtons';
export default CarouselButtons;
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
similarity index 65%
rename from src/components/Attachments/AttachmentCarousel/CarouselItem.js
rename to src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
index b2c9fed64467..4988110538fe 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
@@ -1,8 +1,8 @@
-import PropTypes from 'prop-types';
import React, {useContext, useState} from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import AttachmentView from '@components/Attachments/AttachmentView';
-import * as AttachmentsPropTypes from '@components/Attachments/propTypes';
+import type {Attachment} from '@components/Attachments/types';
import Button from '@components/Button';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
@@ -12,55 +12,27 @@ import useThemeStyles from '@hooks/useThemeStyles';
import ReportAttachmentsContext from '@pages/home/report/ReportAttachmentsContext';
import CONST from '@src/CONST';
-const propTypes = {
+type CarouselItemProps = {
/** Attachment required information such as the source and file name */
- item: PropTypes.shape({
- /** Report action ID of the attachment */
- reportActionID: PropTypes.string,
-
- /** Whether source URL requires authentication */
- isAuthTokenRequired: PropTypes.bool,
-
- /** URL to full-sized attachment or SVG function */
- source: AttachmentsPropTypes.attachmentSourcePropType.isRequired,
-
- /** Additional information about the attachment file */
- file: PropTypes.shape({
- /** File name of the attachment */
- name: PropTypes.string.isRequired,
- }).isRequired,
-
- /** Whether the attachment has been flagged */
- hasBeenFlagged: PropTypes.bool,
-
- /** The id of the transaction related to the attachment */
- transactionID: PropTypes.string,
-
- duration: PropTypes.number,
- }).isRequired,
+ item: Attachment;
/** onPress callback */
- onPress: PropTypes.func,
+ onPress?: () => void;
- isModalHovered: PropTypes.bool,
+ /** Whether attachment carousel modal is hovered over */
+ isModalHovered?: boolean;
/** Whether the attachment is currently being viewed in the carousel */
- isFocused: PropTypes.bool.isRequired,
-};
-
-const defaultProps = {
- onPress: undefined,
- isModalHovered: false,
+ isFocused: boolean;
};
-function CarouselItem({item, onPress, isFocused, isModalHovered}) {
+function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isAttachmentHidden} = useContext(ReportAttachmentsContext);
- // eslint-disable-next-line es/no-nullish-coalescing-operators
- const [isHidden, setIsHidden] = useState(() => isAttachmentHidden(item.reportActionID) ?? item.hasBeenFlagged);
+ const [isHidden, setIsHidden] = useState(() => (item.reportActionID ? isAttachmentHidden(item.reportActionID) : item.hasBeenFlagged));
- const renderButton = (style) => (
+ const renderButton = (style: StyleProp) => (
@@ -121,8 +94,6 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) {
);
}
-CarouselItem.propTypes = propTypes;
-CarouselItem.defaultProps = defaultProps;
CarouselItem.displayName = 'CarouselItem';
export default CarouselItem;
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
index 87cabecc5878..87a9108d5f2e 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
@@ -2,11 +2,12 @@ import type {ForwardedRef} from 'react';
import {createContext} from 'react';
import type PagerView from 'react-native-pager-view';
import type {SharedValue} from 'react-native-reanimated';
+import type {AttachmentSource} from '@components/Attachments/types';
/** The pager items array is used within the pager to render and navigate between the images */
type AttachmentCarouselPagerItems = {
/** The source of the image is used to identify each attachment/page in the pager */
- source: string;
+ source: AttachmentSource;
/** The index of the pager item determines the order of the images in the pager */
index: number;
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
index 33d9f20b5e57..b7ef9309eb10 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+import type {NativeSyntheticEvent} from 'react-native';
import {View} from 'react-native';
import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler';
import {createNativeWrapper} from 'react-native-gesture-handler';
@@ -7,6 +8,7 @@ import type {PagerViewProps} from 'react-native-pager-view';
import PagerView from 'react-native-pager-view';
import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated';
import CarouselItem from '@components/Attachments/AttachmentCarousel/CarouselItem';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import useThemeStyles from '@hooks/useThemeStyles';
import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
import usePageScrollHandler from './usePageScrollHandler';
@@ -20,22 +22,24 @@ type AttachmentCarouselPagerHandle = {
setPage: (selectedPage: number) => void;
};
-type Attachment = {
- source: string;
-};
-
type AttachmentCarouselPagerProps = {
/** The attachments to be rendered in the pager. */
items: Attachment[];
/** The source (URL) of the currently active attachment. */
- activeSource: string;
+ activeSource: AttachmentSource;
/** The index of the initial page to be rendered. */
initialPage: number;
/** A callback to be called when the page is changed. */
- onPageSelected: () => void;
+ onPageSelected: (
+ event: NativeSyntheticEvent<
+ Readonly<{
+ position: number;
+ }>
+ >,
+ ) => void;
/**
* A callback that can be used to toggle the attachment carousel arrows, when the scale of the image changes.
@@ -112,6 +116,12 @@ function AttachmentCarouselPager(
onRequestToggleArrows();
}, [isScrollEnabled.value, onRequestToggleArrows]);
+ const extractItemKey = useCallback(
+ (item: Attachment, index: number) =>
+ typeof item.source === 'string' || typeof item.source === 'number' ? `source-${item.source}` : `reportActionID-${item.reportActionID}` ?? `index-${index}`,
+ [],
+ );
+
const contextValue = useMemo(
() => ({
pagerItems,
@@ -146,14 +156,11 @@ function AttachmentCarouselPager(
const carouselItems = items.map((item, index) => (
@@ -179,3 +186,4 @@ function AttachmentCarouselPager(
AttachmentCarouselPager.displayName = 'AttachmentCarouselPager';
export default React.forwardRef(AttachmentCarouselPager);
+export type {AttachmentCarouselPagerHandle};
diff --git a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js
deleted file mode 100644
index 5aa665683162..000000000000
--- a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import PropTypes from 'prop-types';
-import transactionPropTypes from '@components/transactionPropTypes';
-import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
-import reportPropTypes from '@pages/reportPropTypes';
-
-const propTypes = {
- /** source is used to determine the starting index in the array of attachments */
- source: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-
- /** Callback to update the parent modal's state with a source and name from the attachments array */
- onNavigate: PropTypes.func,
-
- /** Function to change the download button Visibility */
- setDownloadButtonVisibility: PropTypes.func,
-
- /** Object of report actions for this report */
- reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
-
- /** The report currently being looked at */
- report: reportPropTypes.isRequired,
-
- /** The parent of `report` */
- parentReport: reportPropTypes,
-
- /** The report actions of the parent report */
- parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
-
- /** The transaction attached to the parent report action */
- transaction: transactionPropTypes,
-};
-
-const defaultProps = {
- source: '',
- reportActions: {},
- parentReport: {},
- parentReportActions: {},
- transaction: {},
- onNavigate: () => {},
- setDownloadButtonVisibility: () => {},
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts
similarity index 82%
rename from src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
rename to src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts
index 9524c5203110..342afa1d5366 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts
@@ -1,20 +1,19 @@
import {Parser as HtmlParser} from 'htmlparser2';
-import _ from 'underscore';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {Attachment} from '@components/Attachments/types';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import CONST from '@src/CONST';
+import type {ReportAction, ReportActions} from '@src/types/onyx';
/**
* Constructs the initial component state from report actions
- * @param {Object} parentReportAction
- * @param {Object} reportActions
- * @param {Object} transaction
- * @returns {Array}
*/
-function extractAttachmentsFromReport(parentReportAction, reportActions) {
- const actions = [parentReportAction, ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))];
- const attachments = [];
+function extractAttachmentsFromReport(parentReportAction?: OnyxEntry, reportActions?: OnyxEntry) {
+ const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))];
+ const attachments: Attachment[] = [];
+
// We handle duplicate image sources by considering the first instance as original. Selecting any duplicate
// and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position.
const uniqueSources = new Set();
@@ -30,7 +29,6 @@ function extractAttachmentsFromReport(parentReportAction, reportActions) {
uniqueSources.add(source);
const splittedUrl = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE].split('/');
attachments.unshift({
- reportActionID: null,
source: tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]),
isAuthTokenRequired: Boolean(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]),
file: {name: splittedUrl[splittedUrl.length - 1]},
@@ -73,14 +71,14 @@ function extractAttachmentsFromReport(parentReportAction, reportActions) {
},
});
- _.forEach(actions, (action, key) => {
+ actions.forEach((action, key) => {
if (!ReportActionsUtils.shouldReportActionBeVisible(action, key) || ReportActionsUtils.isMoneyRequestAction(action)) {
return;
}
- const decision = _.get(action, ['message', 0, 'moderationDecision', 'decision'], '');
+ const decision = action?.message?.[0].moderationDecision?.decision;
const hasBeenFlagged = decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN;
- const html = _.get(action, ['message', 0, 'html'], '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`);
+ const html = (action?.message?.[0].html ?? '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`);
htmlParser.write(html);
});
htmlParser.end();
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.tsx
similarity index 67%
rename from src/components/Attachments/AttachmentCarousel/index.native.js
rename to src/components/Attachments/AttachmentCarousel/index.native.tsx
index f02b6690ae8e..f6d63fc9307d 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx
@@ -1,63 +1,62 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Keyboard, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import BlockingView from '@components/BlockingViews/BlockingView';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import * as Illustrations from '@components/Icon/Illustrations';
-import withLocalize from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import ONYXKEYS from '@src/ONYXKEYS';
-import {defaultProps, propTypes} from './attachmentCarouselPropTypes';
import CarouselButtons from './CarouselButtons';
import extractAttachmentsFromReport from './extractAttachmentsFromReport';
+import type {AttachmentCarouselPagerHandle} from './Pager';
import AttachmentCarouselPager from './Pager';
+import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps} from './types';
import useCarouselArrows from './useCarouselArrows';
-function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, onClose}) {
+function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, onClose}: AttachmentCarouselProps) {
const styles = useThemeStyles();
- const pagerRef = useRef(null);
- const [page, setPage] = useState();
- const [attachments, setAttachments] = useState([]);
- const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
- const [activeSource, setActiveSource] = useState(source);
+ const {translate} = useLocalize();
+ const pagerRef = useRef(null);
+ const [page, setPage] = useState();
+ const [attachments, setAttachments] = useState([]);
+ const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
+ const [activeSource, setActiveSource] = useState(source);
- const compareImage = useCallback((attachment) => attachment.source === source, [source]);
+ const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]);
useEffect(() => {
- const parentReportAction = parentReportActions[report.parentReportActionID];
+ const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined;
const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions);
- const initialPage = _.findIndex(attachmentsFromReport, compareImage);
+ const initialPage = attachmentsFromReport.findIndex(compareImage);
// Dismiss the modal when deleting an attachment during its display in preview.
- if (initialPage === -1 && _.find(attachments, compareImage)) {
+ if (initialPage === -1 && attachments.find(compareImage)) {
Navigation.dismissModal();
} else {
setPage(initialPage);
setAttachments(attachmentsFromReport);
// Update the download button visibility in the parent modal
- setDownloadButtonVisibility(initialPage !== -1);
+ if (setDownloadButtonVisibility) {
+ setDownloadButtonVisibility(initialPage !== -1);
+ }
// Update the parent modal's state with the source and name from the mapped attachments
- if (!_.isUndefined(attachmentsFromReport[initialPage])) {
+ if (attachmentsFromReport[initialPage] !== undefined && onNavigate) {
onNavigate(attachmentsFromReport[initialPage]);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reportActions, compareImage]);
- /**
- * Updates the page state when the user navigates between attachments
- * @param {Object} item
- * @param {number} index
- */
+ /** Updates the page state when the user navigates between attachments */
const updatePage = useCallback(
- (newPageIndex) => {
+ (newPageIndex: number) => {
Keyboard.dismiss();
setShouldShowArrows(true);
@@ -66,7 +65,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
setPage(newPageIndex);
setActiveSource(item.source);
- onNavigate(item);
+ if (onNavigate) {
+ onNavigate(item);
+ }
},
[setShouldShowArrows, attachments, onNavigate],
);
@@ -76,10 +77,13 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
* @param {Number} deltaSlide
*/
const cycleThroughAttachments = useCallback(
- (deltaSlide) => {
+ (deltaSlide: number) => {
+ if (page === undefined) {
+ return;
+ }
const nextPageIndex = page + deltaSlide;
updatePage(nextPageIndex);
- pagerRef.current.setPage(nextPageIndex);
+ pagerRef.current?.setPage(nextPageIndex);
autoHideArrows();
},
@@ -91,7 +95,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
* @param {Boolean} showArrows if showArrows is passed, it will set the visibility to the passed value
*/
const toggleArrows = useCallback(
- (showArrows) => {
+ (showArrows?: boolean) => {
if (showArrows === undefined) {
setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows);
return;
@@ -148,23 +152,15 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
);
}
-AttachmentCarousel.propTypes = propTypes;
-AttachmentCarousel.defaultProps = defaultProps;
AttachmentCarousel.displayName = 'AttachmentCarousel';
-export default compose(
- withOnyx({
- reportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
- canEvict: false,
- },
- parentReport: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report ? report.parentReportID : '0'}`,
- },
- parentReportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`,
- canEvict: false,
- },
- }),
- withLocalize,
-)(AttachmentCarousel);
+export default withOnyx({
+ parentReportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`,
+ canEvict: false,
+ },
+ reportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+ canEvict: false,
+ },
+})(AttachmentCarousel);
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.tsx
similarity index 65%
rename from src/components/Attachments/AttachmentCarousel/index.js
rename to src/components/Attachments/AttachmentCarousel/index.tsx
index ef6a11f6e67c..f05abfd6a0de 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -1,25 +1,25 @@
+import isEqual from 'lodash/isEqual';
import React, {useCallback, useEffect, useRef, useState} from 'react';
+import type {ListRenderItemInfo} from 'react-native';
import {FlatList, Keyboard, PixelRatio, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import BlockingView from '@components/BlockingViews/BlockingView';
import * as Illustrations from '@components/Icon/Illustrations';
-import withLocalize from '@components/withLocalize';
-import withWindowDimensions from '@components/withWindowDimensions';
+import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import AttachmentCarouselCellRenderer from './AttachmentCarouselCellRenderer';
-import {defaultProps, propTypes} from './attachmentCarouselPropTypes';
import CarouselActions from './CarouselActions';
import CarouselButtons from './CarouselButtons';
import CarouselItem from './CarouselItem';
import extractAttachmentsFromReport from './extractAttachmentsFromReport';
+import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps, UpdatePageProps} from './types';
import useCarouselArrows from './useCarouselArrows';
const viewabilityConfig = {
@@ -28,79 +28,79 @@ const viewabilityConfig = {
itemVisiblePercentThreshold: 95,
};
-function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate}) {
+function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility}: AttachmentCarouselProps) {
const theme = useTheme();
+ const {translate} = useLocalize();
const styles = useThemeStyles();
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null);
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
const [containerWidth, setContainerWidth] = useState(0);
const [page, setPage] = useState(0);
- const [attachments, setAttachments] = useState([]);
- const [activeSource, setActiveSource] = useState(source);
- const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
+ const [attachments, setAttachments] = useState([]);
+ const [activeSource, setActiveSource] = useState(source);
+ const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
- const compareImage = useCallback((attachment) => attachment.source === source, [source]);
+ const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]);
useEffect(() => {
- const parentReportAction = parentReportActions[report.parentReportActionID];
- const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions);
+ const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined;
+ const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions ?? undefined);
- const initialPage = _.findIndex(attachmentsFromReport, compareImage);
-
- if (_.isEqual(attachments, attachmentsFromReport)) {
+ if (isEqual(attachments, attachmentsFromReport)) {
return;
}
+ const initialPage = attachmentsFromReport.findIndex(compareImage);
+
// Dismiss the modal when deleting an attachment during its display in preview.
- if (initialPage === -1 && _.find(attachments, compareImage)) {
+ if (initialPage === -1 && attachments.find(compareImage)) {
Navigation.dismissModal();
} else {
setPage(initialPage);
setAttachments(attachmentsFromReport);
// Update the download button visibility in the parent modal
- setDownloadButtonVisibility(initialPage !== -1);
+ if (setDownloadButtonVisibility) {
+ setDownloadButtonVisibility(initialPage !== -1);
+ }
// Update the parent modal's state with the source and name from the mapped attachments
- if (!_.isUndefined(attachmentsFromReport[initialPage])) {
+ if (attachmentsFromReport[initialPage] !== undefined && onNavigate) {
onNavigate(attachmentsFromReport[initialPage]);
}
}
- }, [attachments, reportActions, parentReportActions, compareImage, report.parentReportActionID, setDownloadButtonVisibility, onNavigate]);
+ }, [reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate]);
- /**
- * Updates the page state when the user navigates between attachments
- * @param {Object} item
- * @param {number} index
- */
+ /** Updates the page state when the user navigates between attachments */
const updatePage = useCallback(
- ({viewableItems}) => {
+ ({viewableItems}: UpdatePageProps) => {
Keyboard.dismiss();
// Since we can have only one item in view at a time, we can use the first item in the array
// to get the index of the current page
- const entry = _.first(viewableItems);
+ const entry = viewableItems[0];
if (!entry) {
setActiveSource(null);
return;
}
- setPage(entry.index);
- setActiveSource(entry.item.source);
+ if (entry.index !== null) {
+ setPage(entry.index);
+ setActiveSource(entry.item.source);
+ }
- onNavigate(entry.item);
+ if (onNavigate) {
+ onNavigate(entry.item);
+ }
},
[onNavigate],
);
- /**
- * Increments or decrements the index to get another selected item
- * @param {Number} deltaSlide
- */
+ /** Increments or decrements the index to get another selected item */
const cycleThroughAttachments = useCallback(
- (deltaSlide) => {
+ (deltaSlide: number) => {
const nextIndex = page + deltaSlide;
const nextItem = attachments[nextIndex];
@@ -113,14 +113,15 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[attachments, canUseTouchScreen, page],
);
- /**
- * Calculate items layout information to optimize scrolling performance
- * @param {*} data
- * @param {Number} index
- * @returns {{offset: Number, length: Number, index: Number}}
- */
+ const extractItemKey = useCallback(
+ (item: Attachment, index: number) =>
+ typeof item.source === 'string' || typeof item.source === 'number' ? `source-${item.source}` : `reportActionID-${item.reportActionID}` ?? `index-${index}`,
+ [],
+ );
+
+ /** Calculate items layout information to optimize scrolling performance */
const getItemLayout = useCallback(
- (_data, index) => ({
+ (data: ArrayLike | null | undefined, index: number) => ({
length: containerWidth,
offset: containerWidth * index,
index,
@@ -128,30 +129,17 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[containerWidth],
);
- /**
- * Defines how a single attachment should be rendered
- * @param {Object} item
- * @param {String} item.reportActionID
- * @param {Boolean} item.isAuthTokenRequired
- * @param {String} item.source
- * @param {Object} item.file
- * @param {String} item.file.name
- * @param {Boolean} item.hasBeenFlagged
- * @returns {JSX.Element}
- */
+ /** Defines how a single attachment should be rendered */
const renderItem = useCallback(
- ({item, index}) => (
+ ({item}: ListRenderItemInfo) => (
setShouldShowArrows((oldState) => !oldState) : undefined}
+ onPress={canUseTouchScreen ? () => setShouldShowArrows((oldState: boolean) => !oldState) : undefined}
isModalHovered={shouldShowArrows}
- index={index}
- activeIndex={page}
/>
),
- [activeSource, attachments.length, canUseTouchScreen, page, setShouldShowArrows, shouldShowArrows],
+ [activeSource, canUseTouchScreen, setShouldShowArrows, shouldShowArrows],
);
return (
@@ -184,7 +172,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
{containerWidth > 0 && (
item.source}
+ keyExtractor={extractItemKey}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={updatePage}
/>
@@ -220,24 +207,15 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
);
}
-AttachmentCarousel.propTypes = propTypes;
-AttachmentCarousel.defaultProps = defaultProps;
AttachmentCarousel.displayName = 'AttachmentCarousel';
-export default compose(
- withOnyx({
- reportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
- canEvict: false,
- },
- parentReport: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report ? report.parentReportID : '0'}`,
- },
- parentReportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`,
- canEvict: false,
- },
- }),
- withLocalize,
- withWindowDimensions,
-)(AttachmentCarousel);
+export default withOnyx({
+ parentReportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`,
+ canEvict: false,
+ },
+ reportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+ canEvict: false,
+ },
+})(AttachmentCarousel);
diff --git a/src/components/Attachments/AttachmentCarousel/types.ts b/src/components/Attachments/AttachmentCarousel/types.ts
new file mode 100644
index 000000000000..8ba3489a5fcf
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/types.ts
@@ -0,0 +1,35 @@
+import type {ViewToken} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
+import type {Report, ReportActions} from '@src/types/onyx';
+
+type UpdatePageProps = {
+ viewableItems: ViewToken[];
+};
+
+type AttachmentCaraouselOnyxProps = {
+ /** Object of report actions for this report */
+ reportActions: OnyxEntry;
+
+ /** The report actions of the parent report */
+ parentReportActions: OnyxEntry;
+};
+
+type AttachmentCarouselProps = AttachmentCaraouselOnyxProps & {
+ /** Source is used to determine the starting index in the array of attachments */
+ source: AttachmentSource;
+
+ /** Callback to update the parent modal's state with a source and name from the attachments array */
+ onNavigate?: (attachment: Attachment) => void;
+
+ /** Function to change the download button Visibility */
+ setDownloadButtonVisibility?: (isButtonVisible: boolean) => void;
+
+ /** The report currently being looked at */
+ report: Report;
+
+ /** A callback that is called when swipe-down-to-close gesture happens */
+ onClose: () => void;
+};
+
+export type {AttachmentCarouselProps, UpdatePageProps, AttachmentCaraouselOnyxProps};
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
similarity index 72%
rename from src/components/Attachments/AttachmentCarousel/useCarouselArrows.js
rename to src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
index 0c55c3ae519d..12ca3db4e2ff 100644
--- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
@@ -1,3 +1,4 @@
+import type {SetStateAction} from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import CONST from '@src/CONST';
@@ -5,12 +6,17 @@ import CONST from '@src/CONST';
function useCarouselArrows() {
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
const [shouldShowArrows, setShouldShowArrowsInternal] = useState(canUseTouchScreen);
- const autoHideArrowTimeout = useRef(null);
+ const autoHideArrowTimeout = useRef(null);
/**
* Cancels the automatic hiding of the arrows.
*/
- const cancelAutoHideArrows = useCallback(() => clearTimeout(autoHideArrowTimeout.current), []);
+ const cancelAutoHideArrows = useCallback(() => {
+ if (!autoHideArrowTimeout.current) {
+ return;
+ }
+ clearTimeout(autoHideArrowTimeout.current);
+ }, []);
/**
* Automatically hide the arrows if there is no interaction for 3 seconds.
@@ -27,7 +33,7 @@ function useCarouselArrows() {
}, [canUseTouchScreen, cancelAutoHideArrows]);
const setShouldShowArrows = useCallback(
- (show = true) => {
+ (show: SetStateAction = true) => {
setShouldShowArrowsInternal(show);
autoHideArrows();
},
@@ -39,7 +45,7 @@ function useCarouselArrows() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- return [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows];
+ return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows};
}
export default useCarouselArrows;
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx
old mode 100755
new mode 100644
similarity index 54%
rename from src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
rename to src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx
index 67f87b1733d3..c195c1e34554
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx
@@ -1,26 +1,31 @@
import React, {memo} from 'react';
import ImageView from '@components/ImageView';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import CONST from '@src/CONST';
-import {attachmentViewImageDefaultProps, attachmentViewImagePropTypes} from './propTypes';
+import type {AttachmentViewProps} from '..';
-const propTypes = {
- ...attachmentViewImagePropTypes,
- ...withLocalizePropTypes,
+type AttachmentViewImageProps = Pick & {
+ url: string;
+
+ loadComplete: boolean;
+
+ isImage: boolean;
+
+ /** Function for handle on error */
+ onError?: () => void;
};
-function AttachmentViewImage({url, file, isAuthTokenRequired, isFocused, loadComplete, onPress, onError, isImage, translate}) {
+function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, isImage}: AttachmentViewImageProps) {
+ const {translate} = useLocalize();
const styles = useThemeStyles();
const children = (
);
@@ -30,7 +35,8 @@ function AttachmentViewImage({url, file, isAuthTokenRequired, isFocused, loadCom
disabled={loadComplete}
style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
- accessibilityLabel={file.name || translate('attachmentView.unknownFilename')}
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ accessibilityLabel={file?.name || translate('attachmentView.unknownFilename')}
>
{children}
@@ -39,8 +45,6 @@ function AttachmentViewImage({url, file, isAuthTokenRequired, isFocused, loadCom
);
}
-AttachmentViewImage.propTypes = propTypes;
-AttachmentViewImage.defaultProps = attachmentViewImageDefaultProps;
AttachmentViewImage.displayName = 'AttachmentViewImage';
-export default compose(memo, withLocalize)(AttachmentViewImage);
+export default memo(AttachmentViewImage);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js
deleted file mode 100644
index f2a275fc9a21..000000000000
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/propTypes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import PropTypes from 'prop-types';
-import {attachmentViewDefaultProps, attachmentViewPropTypes} from '@components/Attachments/AttachmentView/propTypes';
-
-const attachmentViewImagePropTypes = {
- ...attachmentViewPropTypes,
-
- url: PropTypes.string.isRequired,
-
- loadComplete: PropTypes.bool.isRequired,
-
- isImage: PropTypes.bool.isRequired,
-};
-
-const attachmentViewImageDefaultProps = {
- ...attachmentViewDefaultProps,
-
- loadComplete: false,
- isImage: false,
-};
-
-export {attachmentViewImagePropTypes, attachmentViewImageDefaultProps};
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
similarity index 79%
rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js
rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
index 2f16b63aacc6..44aeb2a58b81 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
@@ -1,21 +1,8 @@
-import PropTypes from 'prop-types';
import React, {memo, useCallback, useContext, useEffect} from 'react';
+import type {GestureResponderEvent} from 'react-native';
import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext';
import PDFView from '@components/PDFView';
-import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes';
-
-const baseAttachmentViewPdfPropTypes = {
- ...attachmentViewPdfPropTypes,
-
- /** Triggered when the PDF's onScaleChanged event is triggered */
- onScaleChanged: PropTypes.func,
-};
-
-const baseAttachmentViewPdfDefaultProps = {
- ...attachmentViewPdfDefaultProps,
-
- onScaleChanged: undefined,
-};
+import type AttachmentViewPdfProps from './types';
function BaseAttachmentViewPdf({
file,
@@ -28,7 +15,7 @@ function BaseAttachmentViewPdf({
onLoadComplete,
errorLabelStyles,
style,
-}) {
+}: AttachmentViewPdfProps) {
const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext);
const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled;
@@ -46,7 +33,7 @@ function BaseAttachmentViewPdf({
* as well as call the onScaleChanged prop of the AttachmentViewPdf component if defined.
*/
const onScaleChanged = useCallback(
- (newScale) => {
+ (newScale: number) => {
if (onScaleChangedProp !== undefined) {
onScaleChangedProp(newScale);
}
@@ -66,13 +53,13 @@ function BaseAttachmentViewPdf({
* Otherwise it means that the PDF is currently zoomed in, therefore the onTap callback should be ignored
*/
const onPress = useCallback(
- (e) => {
+ (event?: GestureResponderEvent | KeyboardEvent) => {
if (onPressProp !== undefined) {
- onPressProp(e);
+ onPressProp(event);
}
- if (attachmentCarouselPagerContext !== null && isScrollEnabled.value) {
- attachmentCarouselPagerContext.onTap(e);
+ if (attachmentCarouselPagerContext !== null && isScrollEnabled?.value) {
+ attachmentCarouselPagerContext.onTap();
}
},
[attachmentCarouselPagerContext, isScrollEnabled, onPressProp],
@@ -80,10 +67,11 @@ function BaseAttachmentViewPdf({
return (
{
isPanGestureActive.value = false;
+ if (!isScrollEnabled) {
+ return;
+ }
isScrollEnabled.value = true;
});
@@ -93,7 +96,4 @@ function AttachmentViewPdf(props) {
);
}
-AttachmentViewPdf.propTypes = attachmentViewPdfPropTypes;
-AttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps;
-
export default memo(AttachmentViewPdf);
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx
similarity index 54%
rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js
rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx
index 103ff292760f..4ee60e9dfff5 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx
@@ -1,8 +1,8 @@
import React, {memo} from 'react';
import BaseAttachmentViewPdf from './BaseAttachmentViewPdf';
-import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes';
+import type AttachmentViewPdfProps from './types';
-function AttachmentViewPdf(props) {
+function AttachmentViewPdf(props: AttachmentViewPdfProps) {
return (
& {
+ encryptedSourceUrl: string;
+ onLoadComplete: (path: string) => void;
+
+ /** Additional style props */
+ style?: StyleProp;
+
+ /** Styles for the error label */
+ errorLabelStyles?: StyleProp;
+
+ /** Triggered when the PDF's onScaleChanged event is triggered */
+ onScaleChanged?: (scale: number) => void;
+};
+
+export default AttachmentViewPdfProps;
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.js b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx
similarity index 52%
rename from src/components/Attachments/AttachmentView/AttachmentViewVideo/index.js
rename to src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx
index 2b71e799beed..03e0c0252a66 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx
@@ -1,28 +1,17 @@
-import PropTypes from 'prop-types';
import React from 'react';
import VideoPlayer from '@components/VideoPlayer';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import type {AttachmentViewProps} from '..';
-const propTypes = {
+type AttachmentViewVideoProps = Pick & {
/** Video file source URL */
- source: PropTypes.string.isRequired,
+ source: string;
- /** Whether the video is currently being hovered over */
- isHovered: PropTypes.bool,
-
- shouldUseSharedVideoElement: PropTypes.bool,
-
- videoDuration: PropTypes.number,
-};
-
-const defaultProps = {
- isHovered: false,
- shouldUseSharedVideoElement: false,
- videoDuration: 0,
+ shouldUseSharedVideoElement?: boolean;
};
-function AttachmentViewVideo({source, isHovered, shouldUseSharedVideoElement, videoDuration}) {
+function AttachmentViewVideo({source, isHovered = false, shouldUseSharedVideoElement = false, duration = 0}: AttachmentViewVideoProps) {
const {isSmallScreen} = useWindowDimensions();
const styles = useThemeStyles();
@@ -31,14 +20,12 @@ function AttachmentViewVideo({source, isHovered, shouldUseSharedVideoElement, vi
url={source}
shouldUseSharedVideoElement={shouldUseSharedVideoElement && !isSmallScreen}
isVideoHovered={isHovered}
- videoDuration={videoDuration}
+ videoDuration={duration}
style={[styles.w100, styles.h100]}
/>
);
}
-AttachmentViewVideo.propTypes = propTypes;
-AttachmentViewVideo.defaultProps = defaultProps;
AttachmentViewVideo.displayName = 'AttachmentViewVideo';
export default React.memo(AttachmentViewVideo);
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.tsx
old mode 100755
new mode 100644
similarity index 66%
rename from src/components/Attachments/AttachmentView/index.js
rename to src/components/Attachments/AttachmentView/index.tsx
index 9fe37734e8ee..2685a5cef407
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -1,10 +1,10 @@
import Str from 'expensify-common/lib/str';
-import PropTypes from 'prop-types';
import React, {memo, useEffect, useState} from 'react';
+import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import {ActivityIndicator, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import * as AttachmentsPropTypes from '@components/Attachments/propTypes';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceipt from '@components/EReceipt';
import Icon from '@components/Icon';
@@ -13,74 +13,61 @@ import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
-import compose from '@libs/compose';
import * as TransactionUtils from '@libs/TransactionUtils';
+import type {ColorValue} from '@styles/utils/types';
import variables from '@styles/variables';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {Transaction} from '@src/types/onyx';
import AttachmentViewImage from './AttachmentViewImage';
import AttachmentViewPdf from './AttachmentViewPdf';
import AttachmentViewVideo from './AttachmentViewVideo';
-import {attachmentViewDefaultProps, attachmentViewPropTypes} from './propTypes';
-const propTypes = {
- ...attachmentViewPropTypes,
- ...withLocalizePropTypes,
+type AttachmentViewOnyxProps = {
+ transaction: OnyxEntry;
+};
- /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
- source: AttachmentsPropTypes.attachmentSourcePropType.isRequired,
+type AttachmentViewProps = AttachmentViewOnyxProps &
+ Attachment & {
+ /** Whether this view is the active screen */
+ isFocused?: boolean;
- /** Flag to show/hide download icon */
- shouldShowDownloadIcon: PropTypes.bool,
+ /** Function for handle on press */
+ onPress?: (e?: GestureResponderEvent | KeyboardEvent) => void;
- /** Flag to show the loading indicator */
- shouldShowLoadingSpinnerIcon: PropTypes.bool,
+ /** Whether this AttachmentView is shown as part of a AttachmentCarousel */
+ isUsedInCarousel?: boolean;
- /** Notify parent that the UI should be modified to accommodate keyboard */
- onToggleKeyboard: PropTypes.func,
+ isUsedInAttachmentModal?: boolean;
- /** Extra styles to pass to View wrapper */
- // eslint-disable-next-line react/forbid-prop-types
- containerStyles: PropTypes.arrayOf(PropTypes.object),
+ /** Flag to show/hide download icon */
+ shouldShowDownloadIcon?: boolean;
- /** Denotes whether it is a workspace avatar or not */
- isWorkspaceAvatar: PropTypes.bool,
+ /** Flag to show the loading indicator */
+ shouldShowLoadingSpinnerIcon?: boolean;
- /** Denotes whether it is an icon (ex: SVG) */
- maybeIcon: PropTypes.bool,
+ /** Notify parent that the UI should be modified to accommodate keyboard */
+ onToggleKeyboard?: (shouldFadeOut: boolean) => void;
- /** The id of the transaction related to the attachment */
- // eslint-disable-next-line react/no-unused-prop-types
- transactionID: PropTypes.string,
+ /** Extra styles to pass to View wrapper */
+ containerStyles?: StyleProp;
- /** The id of the report action related to the attachment */
- reportActionID: PropTypes.string,
+ /** Denotes whether it is a workspace avatar or not */
+ isWorkspaceAvatar?: boolean;
- isHovered: PropTypes.bool,
+ /** Denotes whether it is an icon (ex: SVG) */
+ maybeIcon?: boolean;
- optionalVideoDuration: PropTypes.number,
-};
+ fallbackSource?: AttachmentSource;
-const defaultProps = {
- ...attachmentViewDefaultProps,
- shouldShowDownloadIcon: false,
- shouldShowLoadingSpinnerIcon: false,
- onToggleKeyboard: () => {},
- containerStyles: [],
- isWorkspaceAvatar: false,
- maybeIcon: false,
- transactionID: '',
- reportActionID: '',
- isHovered: false,
- optionalVideoDuration: 0,
- fallbackSource: Expensicons.Gallery,
-};
+ isHovered?: boolean;
+ };
function AttachmentView({
source,
@@ -91,7 +78,6 @@ function AttachmentView({
shouldShowDownloadIcon,
containerStyles,
onToggleKeyboard,
- translate,
isFocused,
isUsedInCarousel,
isUsedInAttachmentModal,
@@ -101,21 +87,22 @@ function AttachmentView({
transaction,
reportActionID,
isHovered,
- optionalVideoDuration,
-}) {
+ duration,
+}: AttachmentViewProps) {
+ const {translate} = useLocalize();
const {updateCurrentlyPlayingURL} = usePlaybackContext();
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [loadComplete, setLoadComplete] = useState(false);
- const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file && Str.isVideo(file.name));
+ const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file?.name && Str.isVideo(file.name));
useEffect(() => {
if (!isFocused && !(file && isUsedInAttachmentModal)) {
return;
}
- updateCurrentlyPlayingURL(isVideo ? source : null);
- }, [isFocused, isVideo, source, updateCurrentlyPlayingURL, file, isUsedInAttachmentModal]);
+ updateCurrentlyPlayingURL(isVideo && typeof source === 'string' ? source : null);
+ }, [file, isFocused, isUsedInAttachmentModal, isVideo, source, updateCurrentlyPlayingURL]);
const [imageError, setImageError] = useState(false);
@@ -123,11 +110,11 @@ function AttachmentView({
// Handles case where source is a component (ex: SVG) or a number
// Number may represent a SVG or an image
- if ((maybeIcon && typeof source === 'number') || _.isFunction(source)) {
- let iconFillColor = '';
- let additionalStyles = [];
- if (isWorkspaceAvatar) {
- const defaultWorkspaceAvatarColor = StyleUtils.getDefaultWorkspaceAvatarColor(file.name);
+ if (typeof source === 'function' || (maybeIcon && typeof source === 'number')) {
+ let iconFillColor: ColorValue | undefined = '';
+ let additionalStyles: ViewStyle[] = [];
+ if (isWorkspaceAvatar && file) {
+ const defaultWorkspaceAvatarColor = StyleUtils.getDefaultWorkspaceAvatarColor(file.name ?? '');
iconFillColor = defaultWorkspaceAvatarColor.fill;
additionalStyles = [defaultWorkspaceAvatarColor];
}
@@ -143,7 +130,7 @@ function AttachmentView({
);
}
- if (TransactionUtils.hasEReceipt(transaction)) {
+ if (TransactionUtils.hasEReceipt(transaction) && transaction) {
return (
{
- const id = (transaction && transaction.transactionID) || reportActionID;
+ const onPDFLoadComplete = (path: string) => {
+ const id = (transaction && transaction.transactionID) ?? reportActionID;
if (path && id) {
CachedPDFPaths.add(id, path);
}
@@ -177,34 +164,31 @@ function AttachmentView({
return (
);
}
- if (TransactionUtils.isDistanceRequest(transaction)) {
+ if (TransactionUtils.isDistanceRequest(transaction) && transaction) {
return ;
}
// For this check we use both source and file.name since temporary file source is a blob
// both PDFs and images will appear as images when pasted into the text field.
// We also check for numeric source since this is how static images (used for preview) are represented in RN.
- const isImage = typeof source === 'number' || Str.isImage(source);
- if (isImage || (file && Str.isImage(file.name))) {
+ const isImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
+ if (isImage || (file?.name && Str.isImage(file.name))) {
if (imageError) {
// AttachmentViewImage can't handle icon fallbacks, so we need to handle it here
- if (typeof fallbackSource === 'number' || _.isFunction(fallbackSource)) {
+ if (typeof fallbackSource === 'number' || typeof fallbackSource === 'function') {
return (
{
@@ -234,19 +216,19 @@ function AttachmentView({
);
}
- if (isVideo) {
+ if ((isVideo ?? (file?.name && Str.isVideo(file.name))) && typeof source === 'string') {
return (
);
}
return (
-
+
- {file && file.name}
+ {file?.name}
{!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && (
@@ -279,16 +261,14 @@ function AttachmentView({
);
}
-AttachmentView.propTypes = propTypes;
-AttachmentView.defaultProps = defaultProps;
AttachmentView.displayName = 'AttachmentView';
-export default compose(
- memo,
- withLocalize,
- withOnyx({
+export default memo(
+ withOnyx({
transaction: {
key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
},
- }),
-)(AttachmentView);
+ })(AttachmentView),
+);
+
+export type {AttachmentViewProps};
diff --git a/src/components/Attachments/AttachmentView/propTypes.js b/src/components/Attachments/AttachmentView/propTypes.js
deleted file mode 100644
index 0a0d654912d3..000000000000
--- a/src/components/Attachments/AttachmentView/propTypes.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import PropTypes from 'prop-types';
-import * as AttachmentsPropTypes from '@components/Attachments/propTypes';
-
-const attachmentViewPropTypes = {
- /** Whether source url requires authentication */
- isAuthTokenRequired: PropTypes.bool,
-
- /** File object can be an instance of File or Object */
- file: AttachmentsPropTypes.attachmentFilePropType,
-
- /** Whether this view is the active screen */
- isFocused: PropTypes.bool,
-
- /** Whether this AttachmentView is shown as part of a AttachmentCarousel */
- isUsedInCarousel: PropTypes.bool,
-
- /** Function for handle on press */
- onPress: PropTypes.func,
-
- /** Handles scale changed event */
- onScaleChanged: PropTypes.func,
-};
-
-const attachmentViewDefaultProps = {
- isAuthTokenRequired: false,
- file: {
- name: '',
- },
- isFocused: false,
- isSingleElement: false,
- isUsedInCarousel: false,
- isUsedInAttachmentModal: false,
- onPress: undefined,
- onScaleChanged: () => {},
-};
-
-export {attachmentViewPropTypes, attachmentViewDefaultProps};
diff --git a/src/components/Attachments/propTypes.js b/src/components/Attachments/propTypes.js
deleted file mode 100644
index 13adc468ce64..000000000000
--- a/src/components/Attachments/propTypes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import PropTypes from 'prop-types';
-
-const attachmentSourcePropType = PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.number]);
-const attachmentFilePropType = PropTypes.shape({
- name: PropTypes.string.isRequired,
-});
-
-const attachmentPropType = PropTypes.shape({
- /** Whether source url requires authentication */
- isAuthTokenRequired: PropTypes.bool,
-
- /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
- source: attachmentSourcePropType.isRequired,
-
- /** File object can be an instance of File or Object */
- file: attachmentFilePropType.isRequired,
-});
-
-const attachmentsPropType = PropTypes.arrayOf(attachmentPropType);
-
-export {attachmentSourcePropType, attachmentFilePropType, attachmentPropType, attachmentsPropType};
diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts
new file mode 100644
index 000000000000..835482ca99d9
--- /dev/null
+++ b/src/components/Attachments/types.ts
@@ -0,0 +1,30 @@
+import type {FileObject} from '@components/AttachmentModal';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type AttachmentSource = string | IconAsset | number;
+
+type Attachment = {
+ /** Report action ID of the attachment */
+ reportActionID?: string;
+
+ /** Whether source url requires authentication */
+ isAuthTokenRequired?: boolean;
+
+ /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
+ source: AttachmentSource;
+
+ /** File object can be an instance of File or Object */
+ file?: FileObject;
+
+ /** Whether the attachment has been flagged */
+ hasBeenFlagged?: boolean;
+
+ /** The id of the transaction related to the attachment */
+ transactionID?: string;
+
+ isReceipt?: boolean;
+
+ duration?: number;
+};
+
+export type {AttachmentSource, Attachment};
diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx
index b80d6a138c9e..453e72dc761f 100644
--- a/src/components/ContextMenuItem.tsx
+++ b/src/components/ContextMenuItem.tsx
@@ -1,6 +1,6 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useImperativeHandle} from 'react';
-import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
+import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useThrottledButtonState from '@hooks/useThrottledButtonState';
@@ -46,6 +46,9 @@ type ContextMenuItemProps = {
wrapperStyle?: StyleProp;
shouldPreventDefaultFocusOnPress?: boolean;
+
+ /** The ref of mini context menu item */
+ buttonRef?: React.RefObject;
};
type ContextMenuItemHandle = {
@@ -66,6 +69,7 @@ function ContextMenuItem(
shouldLimitWidth = true,
wrapperStyle,
shouldPreventDefaultFocusOnPress = true,
+ buttonRef = {current: null},
}: ContextMenuItemProps,
ref: ForwardedRef,
) {
@@ -94,6 +98,7 @@ function ContextMenuItem(
return isMini ? (
{
const [emojiPopoverAnchorOrigin, setEmojiPopoverAnchorOrigin] = useState(DEFAULT_ANCHOR_ORIGIN);
const [activeID, setActiveID] = useState();
const emojiPopoverAnchorRef = useRef(null);
+ const emojiAnchorDimension = useRef({
+ width: 0,
+ height: 0,
+ });
const onModalHide = useRef(() => {});
const onEmojiSelected = useRef(() => {});
const activeEmoji = useRef();
@@ -76,7 +80,14 @@ const EmojiPicker = forwardRef((props, ref) => {
// eslint-disable-next-line es/no-optional-chaining
onWillShow?.();
setIsEmojiPickerVisible(true);
- setEmojiPopoverAnchorPosition(value);
+ setEmojiPopoverAnchorPosition({
+ horizontal: value.horizontal,
+ vertical: value.vertical,
+ });
+ emojiAnchorDimension.current = {
+ width: value.width,
+ height: value.height,
+ };
setEmojiPopoverAnchorOrigin(anchorOriginValue);
setActiveID(id);
});
@@ -155,7 +166,14 @@ const EmojiPicker = forwardRef((props, ref) => {
return;
}
calculateAnchorPosition(emojiPopoverAnchor.current, emojiPopoverAnchorOrigin).then((value) => {
- setEmojiPopoverAnchorPosition(value);
+ setEmojiPopoverAnchorPosition({
+ horizontal: value.horizontal,
+ vertical: value.vertical,
+ });
+ emojiAnchorDimension.current = {
+ width: value.width,
+ height: value.height,
+ };
});
});
return () => {
@@ -192,7 +210,9 @@ const EmojiPicker = forwardRef((props, ref) => {
anchorAlignment={emojiPopoverAnchorOrigin}
outerStyle={StyleUtils.getOuterModalStyle(windowHeight, props.viewportOffsetTop)}
innerContainerStyle={styles.popoverInnerContainer}
+ anchorDimensions={emojiAnchorDimension.current}
avoidKeyboard
+ shoudSwitchPositionIfOverflow
>
{
if (!onLoad) {
return;
}
+ if (isLoadedRef.current === true) {
+ return;
+ }
// We override `onLoad`, so both web and native have the same signature
const {width, height} = event.source;
+ isLoadedRef.current = true;
onLoad({nativeEvent: {width, height}});
},
[onLoad],
diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.e2e.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.e2e.tsx
new file mode 100644
index 000000000000..5df39ec02c89
--- /dev/null
+++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.e2e.tsx
@@ -0,0 +1,54 @@
+import React, {forwardRef, useMemo} from 'react';
+import type {FlatListProps, ScrollViewProps, ViewToken} from 'react-native';
+import {FlatList} from 'react-native';
+import type {ReportAction} from '@src/types/onyx';
+
+type BaseInvertedFlatListProps = FlatListProps & {
+ shouldEnableAutoScrollToTopThreshold?: boolean;
+};
+
+const AUTOSCROLL_TO_TOP_THRESHOLD = 128;
+
+let localViewableItems: ViewToken[];
+const getViewableItems = () => localViewableItems;
+
+function BaseInvertedFlatListE2e(props: BaseInvertedFlatListProps, ref: React.ForwardedRef>) {
+ const {shouldEnableAutoScrollToTopThreshold, ...rest} = props;
+
+ const handleViewableItemsChanged = useMemo(
+ () =>
+ ({viewableItems}: {viewableItems: ViewToken[]}) => {
+ localViewableItems = viewableItems;
+ },
+ [],
+ );
+
+ const maintainVisibleContentPosition = useMemo(() => {
+ const config: ScrollViewProps['maintainVisibleContentPosition'] = {
+ // This needs to be 1 to avoid using loading views as anchors.
+ minIndexForVisible: 1,
+ };
+
+ if (shouldEnableAutoScrollToTopThreshold) {
+ config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD;
+ }
+
+ return config;
+ }, [shouldEnableAutoScrollToTopThreshold]);
+
+ return (
+
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...rest}
+ ref={ref}
+ maintainVisibleContentPosition={maintainVisibleContentPosition}
+ inverted
+ onViewableItemsChanged={handleViewableItemsChanged}
+ />
+ );
+}
+
+BaseInvertedFlatListE2e.displayName = 'BaseInvertedFlatListE2e';
+
+export default forwardRef(BaseInvertedFlatListE2e);
+export {getViewableItems};
diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx
similarity index 100%
rename from src/components/InvertedFlatList/BaseInvertedFlatList.tsx
rename to src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 6835bcf3f5fc..110256ba166b 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -35,6 +35,7 @@ import MultipleAvatars from './MultipleAvatars';
import PressableWithSecondaryInteraction from './PressableWithSecondaryInteraction';
import RenderHTML from './RenderHTML';
import SelectCircle from './SelectCircle';
+import SubscriptAvatar from './SubscriptAvatar';
import Text from './Text';
type IconProps = {
@@ -154,6 +155,8 @@ type MenuItemBaseProps = {
/** Text that appears above the title */
label?: string;
+ isLabelHoverable?: boolean;
+
/** Label to be displayed on the right */
rightLabel?: string;
@@ -184,12 +187,18 @@ type MenuItemBaseProps = {
/** Prop to represent the size of the float right avatar images to be shown */
floatRightAvatarSize?: ValueOf;
+ /** Whether the secondary right avatar should show as a subscript */
+ shouldShowSubscriptRightAvatar?: boolean;
+
/** Affects avatar size */
viewMode?: ValueOf;
/** Used to truncate the text with an ellipsis after computing the text layout */
numberOfLinesTitle?: number;
+ /** Used to truncate the description with an ellipsis after computing the text layout */
+ numberOfLinesDescription?: number;
+
/** Whether we should use small avatar subscript sizing the for menu item */
isSmallAvatarSubscriptMenu?: boolean;
@@ -255,6 +264,7 @@ function MenuItem(
badgeStyle,
viewMode = CONST.OPTION_MODE.DEFAULT,
numberOfLinesTitle = 1,
+ numberOfLinesDescription = 2,
icon,
iconFill,
secondaryIcon,
@@ -280,6 +290,7 @@ function MenuItem(
subtitle,
shouldShowBasicTitle,
label,
+ isLabelHoverable = true,
rightLabel,
shouldShowSelectedState = false,
isSelected = false,
@@ -289,6 +300,7 @@ function MenuItem(
rightComponent,
floatRightAvatars = [],
floatRightAvatarSize,
+ shouldShowSubscriptRightAvatar = false,
avatarSize = CONST.AVATAR_SIZE.DEFAULT,
isSmallAvatarSubscriptMenu = false,
brickRoadIndicator,
@@ -404,240 +416,261 @@ function MenuItem(
};
return (
-
- {(isHovered) => (
- shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
- onPressOut={ControlSelection.unblock}
- onSecondaryInteraction={onSecondaryInteraction}
- style={({pressed}) =>
- [
- containerStyle,
- errorText ? styles.pb5 : {},
- combinedStyle,
- !interactive && styles.cursorDefault,
- StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true),
- !focused && (isHovered || pressed) && hoverAndPressStyle,
- ...(Array.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]),
- shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled,
- ] as StyleProp
- }
- disabledStyle={shouldUseDefaultCursorWhenDisabled && [styles.cursorDefault]}
- disabled={disabled || isExecuting}
- ref={ref}
- role={CONST.ROLE.MENUITEM}
- accessibilityLabel={title ? title.toString() : ''}
- accessible
- >
- {({pressed}) => (
- <>
-
- {!!label && (
-
- {label}
-
- )}
-
- {!!icon && Array.isArray(icon) && (
-
+
+ {!!label && !isLabelHoverable && (
+
+ {label}
+
+ )}
+
+ {(isHovered) => (
+ shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
+ onPressOut={ControlSelection.unblock}
+ onSecondaryInteraction={onSecondaryInteraction}
+ style={({pressed}) =>
+ [
+ containerStyle,
+ errorText ? styles.pb5 : {},
+ combinedStyle,
+ !interactive && styles.cursorDefault,
+ StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true),
+ !focused && (isHovered || pressed) && hoverAndPressStyle,
+ ...(Array.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]),
+ shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled,
+ ] as StyleProp
+ }
+ disabledStyle={shouldUseDefaultCursorWhenDisabled && [styles.cursorDefault]}
+ disabled={disabled || isExecuting}
+ ref={ref}
+ role={CONST.ROLE.MENUITEM}
+ accessibilityLabel={title ? title.toString() : ''}
+ accessible
+ >
+ {({pressed}) => (
+ <>
+
+ {!!label && isLabelHoverable && (
+
+
+ {label}
+
+
)}
- {!icon && shouldPutLeftPaddingWhenNoIcon && }
- {icon && !Array.isArray(icon) && (
-
- {typeof icon !== 'string' && iconType === CONST.ICON_TYPE_ICON && (
+
+ {!!icon && Array.isArray(icon) && (
+
+ )}
+ {!icon && shouldPutLeftPaddingWhenNoIcon && }
+ {icon && !Array.isArray(icon) && (
+
+ {typeof icon !== 'string' && iconType === CONST.ICON_TYPE_ICON && (
+
+ )}
+ {icon && iconType === CONST.ICON_TYPE_WORKSPACE && (
+
+ )}
+ {iconType === CONST.ICON_TYPE_AVATAR && (
+
+ )}
+
+ )}
+ {secondaryIcon && (
+
+
+ )}
+
+ {!!description && shouldShowDescriptionOnTop && (
+
+ {description}
+
)}
- {icon && iconType === CONST.ICON_TYPE_WORKSPACE && (
-
+
+ {!!title && (shouldRenderAsHTML || (shouldParseTitle && !!html.length)) && (
+
+
+
+ )}
+ {!shouldRenderAsHTML && !shouldParseTitle && !!title && (
+
+ {renderTitleContent()}
+
+ )}
+ {shouldShowTitleIcon && titleIcon && (
+
+
+
+ )}
+
+ {!!description && !shouldShowDescriptionOnTop && (
+
+ {description}
+
)}
- {iconType === CONST.ICON_TYPE_AVATAR && (
-
+ {error}
+
+ )}
+ {!!furtherDetails && (
+
+ {!!furtherDetailsIcon && (
+
+ )}
+
+ {furtherDetails}
+
+
+ )}
+
+
+
+
+ {badgeText && (
+
+ )}
+ {/* Since subtitle can be of type number, we should allow 0 to be shown */}
+ {(subtitle === 0 || subtitle) && (
+
+ {subtitle}
+
+ )}
+ {floatRightAvatars?.length > 0 && (
+
+ {shouldShowSubscriptRightAvatar ? (
+
+ ) : (
+
)}
)}
- {secondaryIcon && (
-
+ {!!brickRoadIndicator && (
+
)}
-
- {!!description && shouldShowDescriptionOnTop && (
-
- {description}
-
- )}
-
- {!!title && (shouldRenderAsHTML || (shouldParseTitle && !!html.length)) && (
-
-
-
- )}
- {!shouldRenderAsHTML && !shouldParseTitle && !!title && (
-
- {renderTitleContent()}
-
- )}
- {shouldShowTitleIcon && titleIcon && (
-
-
-
- )}
+ {!title && !!rightLabel && (
+
+ {rightLabel}
- {!!description && !shouldShowDescriptionOnTop && (
-
- {description}
-
- )}
- {!!error && (
-
- {error}
-
- )}
- {!!furtherDetails && (
-
- {!!furtherDetailsIcon && (
-
- )}
-
- {furtherDetails}
-
-
- )}
-
+ )}
+ {shouldShowRightIcon && (
+
+
+
+ )}
+ {shouldShowRightComponent && rightComponent}
+ {shouldShowSelectedState && }
-
-
- {badgeText && (
-
)}
- {/* Since subtitle can be of type number, we should allow 0 to be shown */}
- {(subtitle === 0 || subtitle) && (
-
- {subtitle}
-
- )}
- {floatRightAvatars?.length > 0 && (
-
-
-
- )}
- {!!brickRoadIndicator && (
-
-
-
- )}
- {!title && !!rightLabel && (
-
- {rightLabel}
-
- )}
- {shouldShowRightIcon && (
-
-
-
- )}
- {shouldShowRightComponent && rightComponent}
- {shouldShowSelectedState && }
-
- {!!errorText && (
-
- )}
- >
- )}
-
- )}
-
+ >
+ )}
+
+ )}
+
+
);
}
diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx
index 98bc47e41bbe..dedaba500a9c 100644
--- a/src/components/MultipleAvatars.tsx
+++ b/src/components/MultipleAvatars.tsx
@@ -101,8 +101,7 @@ function MultipleAvatars({
}),
[styles],
);
-
- const secondAvatarStyle = secondAvatarStyleProp ?? [StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)];
+ const secondAvatarStyle = secondAvatarStyleProp ?? [StyleUtils.getBackgroundAndBorderStyle(isHovered ? theme.activeComponentBG : theme.componentBG)];
let avatarContainerStyles = StyleUtils.getContainerStyles(size, isInReportAction);
const {singleAvatarStyle, secondAvatarStyles} = useMemo(() => avatarSizeToStylesMap[size as AvatarSizeToStyles] ?? avatarSizeToStylesMap.default, [size, avatarSizeToStylesMap]);
diff --git a/src/components/PDFView/WebPDFDocument.js b/src/components/PDFView/WebPDFDocument.js
deleted file mode 100644
index dd9d1e066b19..000000000000
--- a/src/components/PDFView/WebPDFDocument.js
+++ /dev/null
@@ -1,132 +0,0 @@
-import 'core-js/features/array/at';
-import PropTypes from 'prop-types';
-import React, {memo, useCallback} from 'react';
-import {Document} from 'react-pdf';
-import {VariableSizeList as List} from 'react-window';
-import _ from 'underscore';
-import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
-import Text from '@components/Text';
-import stylePropTypes from '@styles/stylePropTypes';
-import CONST from '@src/CONST';
-import PageRenderer from './WebPDFPageRenderer';
-
-const propTypes = {
- /** Index of the PDF page to be displayed passed by VariableSizeList */
- errorLabelStyles: stylePropTypes,
- /** Returns translated string for given locale and phrase */
- translate: PropTypes.func.isRequired,
- /** The source URL from which to load PDF file to be displayed */
- sourceURL: PropTypes.string.isRequired,
- /** Callback invoked when the PDF document is loaded successfully */
- onDocumentLoadSuccess: PropTypes.func.isRequired,
- /** Viewport info of all PDF pages */
- pageViewportsLength: PropTypes.number.isRequired,
- /** Sets attributes to list container */
- setListAttributes: PropTypes.func.isRequired,
- /** Indicates, whether the screen is of small width */
- isSmallScreenWidth: PropTypes.bool.isRequired,
- /** Height of PDF document container view */
- containerHeight: PropTypes.number.isRequired,
- /** Width of PDF document container view */
- containerWidth: PropTypes.number.isRequired,
- /** The number of pages of the PDF file to be rendered */
- numPages: PropTypes.number,
- /** Function that calculates the height of a page of the PDF document */
- calculatePageHeight: PropTypes.func.isRequired,
- /** Function that calculates the devicePixelRatio the page should be rendered with */
- getDevicePixelRatio: PropTypes.func.isRequired,
- /** The estimated height of a single PDF page for virtualized rendering purposes */
- estimatedItemSize: PropTypes.number.isRequired,
- /** The width of a page in the PDF file */
- pageWidth: PropTypes.number.isRequired,
- /** The style applied to the list component */
- listStyle: stylePropTypes,
- /** Function that should initiate that the user should be prompted for password to the PDF file */
- initiatePasswordChallenge: PropTypes.func.isRequired,
- /** Either:
- * - `string` - the password provided by the user to unlock the PDF file
- * - `undefined` if password isn't needed to view the PDF file
- * - `null` if the password is required but hasn't been provided yet */
- password: PropTypes.string,
-};
-
-const defaultProps = {
- errorLabelStyles: [],
- numPages: null,
- listStyle: undefined,
- password: undefined,
-};
-
-const WebPDFDocument = memo(
- ({
- errorLabelStyles,
- translate,
- sourceURL,
- onDocumentLoadSuccess,
- pageViewportsLength,
- setListAttributes,
- isSmallScreenWidth,
- containerHeight,
- containerWidth,
- numPages,
- calculatePageHeight,
- getDevicePixelRatio,
- estimatedItemSize,
- pageWidth,
- listStyle,
- initiatePasswordChallenge,
- password,
- }) => {
- const onPassword = useCallback(
- (callback, reason) => {
- if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.NEED_PASSWORD) {
- if (password) {
- callback(password);
- } else {
- initiatePasswordChallenge(reason);
- }
- } else if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.INCORRECT_PASSWORD) {
- initiatePasswordChallenge(reason);
- }
- },
- [password, initiatePasswordChallenge],
- );
-
- return (
- }
- error={{translate('attachmentView.failedToLoadPDF')}}
- file={sourceURL}
- options={{
- cMapUrl: 'cmaps/',
- cMapPacked: true,
- }}
- externalLinkTarget="_blank"
- onLoadSuccess={onDocumentLoadSuccess}
- onPassword={onPassword}
- >
- {!!pageViewportsLength && (
-
- {PageRenderer}
-
- )}
-
- );
- },
- (prevProps, nextProps) => _.isEqual(prevProps, nextProps),
-);
-
-WebPDFDocument.displayName = 'WebPDFDocument';
-WebPDFDocument.propTypes = propTypes;
-WebPDFDocument.defaultProps = defaultProps;
-
-export default WebPDFDocument;
diff --git a/src/components/PDFView/WebPDFPageRenderer.js b/src/components/PDFView/WebPDFPageRenderer.js
deleted file mode 100644
index 15af0bb88e39..000000000000
--- a/src/components/PDFView/WebPDFPageRenderer.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import PropTypes from 'prop-types';
-import React, {memo} from 'react';
-import {View} from 'react-native';
-import {Page} from 'react-pdf';
-import _ from 'underscore';
-import stylePropTypes from '@styles/stylePropTypes';
-import PDFViewConstants from './constants';
-
-const propTypes = {
- /** Index of the PDF page to be displayed passed by VariableSizeList */
- index: PropTypes.number.isRequired,
-
- /** Page extra data passed by VariableSizeList's data prop */
- data: PropTypes.shape({
- /** Width of a single page in the document */
- pageWidth: PropTypes.number.isRequired,
- /** Function that calculates the height of a page given its index */
- calculatePageHeight: PropTypes.func.isRequired,
- /** Function that calculates the pixel ratio for a page given its calculated width and height */
- getDevicePixelRatio: PropTypes.func.isRequired,
- /** The estimated height of a single page in the document */
- estimatedItemSize: PropTypes.number.isRequired,
- }).isRequired,
-
- /** Additional style props passed by VariableSizeList */
- style: stylePropTypes.isRequired,
-};
-
-const WebPDFPageRenderer = memo(
- ({index: pageIndex, data, style}) => {
- const {pageWidth, calculatePageHeight, getDevicePixelRatio, estimatedItemSize} = data;
-
- const pageHeight = calculatePageHeight(pageIndex);
- const devicePixelRatio = getDevicePixelRatio(pageWidth, pageHeight);
-
- return (
-
-
-
- );
- },
- (prevProps, nextProps) => _.isEqual(prevProps, nextProps),
-);
-
-WebPDFPageRenderer.displayName = 'WebPDFPageRenderer';
-WebPDFPageRenderer.propTypes = propTypes;
-
-export default WebPDFPageRenderer;
diff --git a/src/components/PDFView/constants.js b/src/components/PDFView/constants.js
deleted file mode 100644
index a45beddfbb68..000000000000
--- a/src/components/PDFView/constants.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Each page has a default border. The app should take this size into account
- * when calculates the page width and height.
- */
-const PAGE_BORDER = 9;
-
-/**
- * Pages should be more narrow than the container on large screens. The app should take this size into account
- * when calculates the page width.
- */
-const LARGE_SCREEN_SIDE_SPACING = 40;
-
-const REQUIRED_PASSWORD_MISSING = null;
-
-export default {PAGE_BORDER, LARGE_SCREEN_SIDE_SPACING, REQUIRED_PASSWORD_MISSING};
diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js
index 9706f8e06cc1..e69b52b74e95 100644
--- a/src/components/PDFView/index.js
+++ b/src/components/PDFView/index.js
@@ -1,53 +1,30 @@
import 'core-js/features/array/at';
-import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker';
import React, {Component} from 'react';
+import {PDFPreviewer} from 'react-fast-pdf';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import {pdfjs} from 'react-pdf';
import _ from 'underscore';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import Text from '@components/Text';
import withLocalize from '@components/withLocalize';
import withThemeStyles from '@components/withThemeStyles';
import withWindowDimensions from '@components/withWindowDimensions';
import compose from '@libs/compose';
-import Log from '@libs/Log';
import variables from '@styles/variables';
import * as CanvasSize from '@userActions/CanvasSize';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import PDFViewConstants from './constants';
import PDFPasswordForm from './PDFPasswordForm';
import * as pdfViewPropTypes from './pdfViewPropTypes';
-import PDFDocument from './WebPDFDocument';
class PDFView extends Component {
constructor(props) {
super(props);
this.state = {
- numPages: null,
- pageViewports: [],
- containerWidth: props.windowWidth,
- containerHeight: props.windowHeight,
- password: undefined,
- /** used to keep the PDFPasswordForm mounted (for it to maintain state) while password is being verified */
- isCheckingPassword: false,
- isPasswordInvalid: false,
isKeyboardOpen: false,
};
- this.onDocumentLoadSuccess = this.onDocumentLoadSuccess.bind(this);
- this.initiatePasswordChallenge = this.initiatePasswordChallenge.bind(this);
- this.attemptPDFLoad = this.attemptPDFLoad.bind(this);
this.toggleKeyboardOnSmallScreens = this.toggleKeyboardOnSmallScreens.bind(this);
- this.calculatePageHeight = this.calculatePageHeight.bind(this);
- this.calculatePageWidth = this.calculatePageWidth.bind(this);
- this.getDevicePixelRatio = _.memoize(this.getDevicePixelRatio.bind(this));
- this.setListAttributes = this.setListAttributes.bind(this);
-
- const workerURL = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'}));
- if (pdfjs.GlobalWorkerOptions.workerSrc !== workerURL) {
- pdfjs.GlobalWorkerOptions.workerSrc = workerURL;
- }
-
this.retrieveCanvasLimits();
}
@@ -69,134 +46,6 @@ class PDFView extends Component {
}
}
- /**
- * Upon successful document load, combine an array of page viewports,
- * set the number of pages on PDF,
- * hide/reset PDF password form, and notify parent component that
- * user input is no longer required.
- *
- * @param {Object} pdf - The PDF file instance
- * @param {Number} pdf.numPages - Number of pages of the PDF file
- * @param {Function} pdf.getPage - A method to get page by its number. It requires to have the context. It should be the pdf itself.
- * @memberof PDFView
- */
- onDocumentLoadSuccess(pdf) {
- const {numPages} = pdf;
-
- Promise.all(
- _.times(numPages, (index) => {
- const pageNumber = index + 1;
-
- return pdf.getPage(pageNumber).then((page) => page.getViewport({scale: 1}));
- }),
- ).then((pageViewports) => {
- this.setState({
- pageViewports,
- numPages,
- isPasswordInvalid: false,
- isCheckingPassword: false,
- });
- });
- }
-
- /**
- * Sets attributes to list container.
- * It unblocks a default scroll by keyboard of browsers.
- * @param {Object|undefined} ref
- */
- setListAttributes(ref) {
- if (!ref) {
- return;
- }
-
- // Useful for elements that should not be navigated to directly using the "Tab" key,
- // but need to have keyboard focus set to them.
- // eslint-disable-next-line no-param-reassign
- ref.tabIndex = -1;
- }
-
- /**
- * Calculate the devicePixelRatio the page should be rendered with
- * Each platform has a different default devicePixelRatio and different canvas limits, we need to verify that
- * with the default devicePixelRatio it will be able to diplay the pdf correctly, if not we must change the devicePixelRatio.
- * @param {Number} width of the page
- * @param {Number} height of the page
- * @returns {Number} devicePixelRatio for this page on this platform
- */
- getDevicePixelRatio(width, height) {
- const nbPixels = width * height;
- const ratioHeight = this.props.maxCanvasHeight / height;
- const ratioWidth = this.props.maxCanvasWidth / width;
- const ratioArea = Math.sqrt(this.props.maxCanvasArea / nbPixels);
- const ratio = Math.min(ratioHeight, ratioArea, ratioWidth);
-
- return ratio > window.devicePixelRatio ? undefined : ratio;
- }
-
- /**
- * Calculates a proper page height. The method should be called only when there are page viewports.
- * It is based on a ratio between the specific page viewport width and provided page width.
- * Also, the app should take into account the page borders.
- * @param {Number} pageIndex
- * @returns {Number}
- */
- calculatePageHeight(pageIndex) {
- if (this.state.pageViewports.length === 0 || _.some(this.state.pageViewports, (viewport) => !viewport)) {
- Log.warn('Dev error: calculatePageHeight() in PDFView called too early');
-
- return 0;
- }
-
- const pageViewport = this.state.pageViewports[pageIndex];
- const pageWidth = this.calculatePageWidth();
- const scale = pageWidth / pageViewport.width;
- const actualHeight = pageViewport.height * scale + PDFViewConstants.PAGE_BORDER * 2;
-
- return actualHeight;
- }
-
- /**
- * Calculates a proper page width.
- * It depends on a screen size. Also, the app should take into account the page borders.
- * @returns {Number}
- */
- calculatePageWidth() {
- const pdfContainerWidth = this.state.containerWidth;
- const pageWidthOnLargeScreen = Math.min(pdfContainerWidth - PDFViewConstants.LARGE_SCREEN_SIDE_SPACING * 2, variables.pdfPageMaxWidth);
- const pageWidth = this.props.isSmallScreenWidth ? this.state.containerWidth : pageWidthOnLargeScreen;
-
- return pageWidth + PDFViewConstants.PAGE_BORDER * 2;
- }
-
- /**
- * Initiate password challenge process. The WebPDFDocument
- * component calls this handler to indicate that a PDF requires a
- * password, or to indicate that a previously provided password was
- * invalid.
- *
- * The PasswordResponses constants used below were copied from react-pdf
- * because they're not exported in entry.webpack.
- *
- * @param {Number} reason Reason code for password request
- */
- initiatePasswordChallenge(reason) {
- if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.NEED_PASSWORD) {
- this.setState({password: PDFViewConstants.REQUIRED_PASSWORD_MISSING, isCheckingPassword: false});
- } else if (reason === CONST.PDF_PASSWORD_FORM.REACT_PDF_PASSWORD_RESPONSES.INCORRECT_PASSWORD) {
- this.setState({password: PDFViewConstants.REQUIRED_PASSWORD_MISSING, isPasswordInvalid: true, isCheckingPassword: false});
- }
- }
-
- /**
- * Send password to react-pdf via its callback so that it can attempt to load
- * the PDF.
- *
- * @param {String} password Password to send via callback to react-pdf
- */
- attemptPDFLoad(password) {
- this.setState({password, isCheckingPassword: true});
- }
-
/**
* On small screens notify parent that the keyboard has opened or closed.
*
@@ -229,59 +78,33 @@ class PDFView extends Component {
renderPDFView() {
const styles = this.props.themeStyles;
- const pageWidth = this.calculatePageWidth();
const outerContainerStyle = [styles.w100, styles.h100, styles.justifyContentCenter, styles.alignItemsCenter];
- const pdfContainerStyle = [styles.PDFView, styles.noSelect, this.props.style];
- // If we're requesting a password then we need to hide - but still render -
- // the PDF component.
- if (this.state.password === PDFViewConstants.REQUIRED_PASSWORD_MISSING || this.state.isCheckingPassword) {
- pdfContainerStyle.push(styles.invisible);
- }
-
- const estimatedItemSize = this.calculatePageHeight(0);
-
return (
-
- this.setState({containerWidth: width, containerHeight: height})}
- >
-
-
- {(this.state.password === PDFViewConstants.REQUIRED_PASSWORD_MISSING || this.state.isCheckingPassword) && (
- this.setState({isPasswordInvalid: false})}
- isPasswordInvalid={this.state.isPasswordInvalid}
- onPasswordFieldFocused={this.toggleKeyboardOnSmallScreens}
- />
- )}
+
+ }
+ ErrorComponent={{this.props.translate('attachmentView.failedToLoadPDF')}}
+ renderPasswordForm={({isPasswordInvalid, onSubmit, onPasswordChange}) => (
+
+ )}
+ />
);
}
@@ -302,6 +125,7 @@ class PDFView extends Component {
);
}
}
+
PDFView.propTypes = pdfViewPropTypes.propTypes;
PDFView.defaultProps = pdfViewPropTypes.defaultProps;
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 8f54de5182f8..1fd1c8ef5a3b 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -203,11 +203,17 @@ function PopoverMenu({
title={item.text}
shouldCheckActionAllowedOnPress={false}
description={item.description}
+ numberOfLinesDescription={item.numberOfLinesDescription}
onPress={() => selectItem(menuIndex)}
focused={focusedIndex === menuIndex}
displayInDefaultIconColor={item.displayInDefaultIconColor}
shouldShowRightIcon={item.shouldShowRightIcon}
shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon}
+ label={item.label}
+ isLabelHoverable={item.isLabelHoverable}
+ floatRightAvatars={item.floatRightAvatars}
+ floatRightAvatarSize={item.floatRightAvatarSize}
+ shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar}
disabled={item.disabled}
/>
))}
diff --git a/src/components/PopoverWithMeasuredContent.tsx b/src/components/PopoverWithMeasuredContent.tsx
index deda6dbd217a..af6f91a64026 100644
--- a/src/components/PopoverWithMeasuredContent.tsx
+++ b/src/components/PopoverWithMeasuredContent.tsx
@@ -6,7 +6,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import PopoverWithMeasuredContentUtils from '@libs/PopoverWithMeasuredContentUtils';
import CONST from '@src/CONST';
-import type {AnchorPosition} from '@src/styles';
+import type {AnchorDimensions, AnchorPosition} from '@src/styles';
import Popover from './Popover';
import type {PopoverProps} from './Popover/types';
import type {WindowDimensionsProps} from './withWindowDimensions/types';
@@ -15,6 +15,12 @@ type PopoverWithMeasuredContentProps = Omit
diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx
index 61a13d271e7d..936bd23b530d 100644
--- a/src/components/TaxPicker.tsx
+++ b/src/components/TaxPicker.tsx
@@ -37,7 +37,7 @@ function TaxPicker({selectedTaxRate = '', taxRates, insets, onSubmit}: TaxPicker
const shouldShowTextInput = !isTaxRatesCountBelowThreshold;
- const getTaxName = useCallback((key: string) => taxRates?.taxes[key].name, [taxRates?.taxes]);
+ const getTaxName = useCallback((key: string) => taxRates?.taxes[key]?.name, [taxRates?.taxes]);
const selectedOptions = useMemo(() => {
if (!selectedTaxRate) {
diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.tsx
similarity index 80%
rename from src/components/TimePicker/TimePicker.js
rename to src/components/TimePicker/TimePicker.tsx
index 4d4520fedeea..17cd93db432b 100644
--- a/src/components/TimePicker/TimePicker.js
+++ b/src/components/TimePicker/TimePicker.tsx
@@ -1,12 +1,12 @@
-import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import type {ForwardedRef} from 'react';
import {View} from 'react-native';
-import _ from 'underscore';
+import type {GestureResponderEvent, NativeSyntheticEvent} from 'react-native';
+import type {TextInput} from 'react-native-gesture-handler';
import AmountTextInput from '@components/AmountTextInput';
import BigNumberPad from '@components/BigNumberPad';
import Button from '@components/Button';
import FormHelpMessage from '@components/FormHelpMessage';
-import refPropTypes from '@components/refPropTypes';
import Text from '@components/Text';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
@@ -19,24 +19,17 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import CONST from '@src/CONST';
import setCursorPosition from './setCursorPosition';
-const propTypes = {
- /** Refs forwarded to the TextInputWithCurrencySymbol */
- forwardedRef: refPropTypes,
+type MinuteHourRefs = {hourRef: TextInput | null; minuteRef: TextInput | null};
+type TimePickerProps = {
/** Default value for the inputs */
- defaultValue: PropTypes.string,
+ defaultValue?: string;
/** Callback to call when the Save button is pressed */
- onSubmit: PropTypes.func.isRequired,
+ onSubmit: (timeString: string) => void;
/** Callback to call when the input changes */
- onInputChange: PropTypes.func,
-};
-
-const defaultProps = {
- forwardedRef: null,
- onInputChange: () => {},
- defaultValue: '',
+ onInputChange?: (timeString: string) => void;
};
const AMOUNT_VIEW_ID = 'amountView';
@@ -45,14 +38,14 @@ const NUM_PAD_VIEW_ID = 'numPadView';
/**
* Replace the sub-string of the given string with the provided value
- * @param {String} originalString - the string that will be modified
- * @param {String} newSubstring - the replacement string
- * @param {Number} from - the start index of the sub-string to replace
- * @param {Number} to - the end index of the sub-string to replace
+ * @param originalString - the string that will be modified
+ * @param newSubstring - the replacement string
+ * @param from - the start index of the sub-string to replace
+ * @param to - the end index of the sub-string to replace
*
- * @returns {String} - the modified string with the range (from, to) replaced with the provided value
+ * @returns - the modified string with the range (from, to) replaced with the provided value
*/
-function insertAtPosition(originalString, newSubstring, from, to) {
+function insertAtPosition(originalString: string, newSubstring: string, from: number, to: number): string {
// Check for invalid positions
if (from < 0 || to < 0 || from > originalString.length || to > originalString.length) {
return originalString;
@@ -72,13 +65,13 @@ function insertAtPosition(originalString, newSubstring, from, to) {
/**
* Replace the sub-string of the given string with zeros
- * @param {String} originalString - the string that will be modified
- * @param {Number} from - the start index of the sub-string to replace
- * @param {Number} to - the end index of the sub-string to replace
+ * @param originalString - the string that will be modified
+ * @param from - the start index of the sub-string to replace
+ * @param to - the end index of the sub-string to replace
*
- * @returns {String} - the modified string with the range (from, to) replaced with zeros
+ * @returns - the modified string with the range (from, to) replaced with zeros
*/
-function replaceRangeWithZeros(originalString, from, to) {
+function replaceRangeWithZeros(originalString: string, from: number, to: number): string {
const normalizedFrom = Math.max(from, 0);
const normalizedTo = Math.min(to, 2);
const replacement = '0'.repeat(normalizedTo - normalizedFrom);
@@ -87,12 +80,13 @@ function replaceRangeWithZeros(originalString, from, to) {
/**
* Clear the value under selection of an input (either hours or minutes) by replacing it with zeros
- * @param {String} value - current value of the input
- * @param {Object} selection - current selection of the input
- * @param {Function} setValue - the function that modifies the value of the input
- * @param {Function} setSelection - the function that modifies the selection of the input
+ *
+ * @param value - current value of the input
+ * @param selection - current selection of the input
+ * @param setValue - the function that modifies the value of the input
+ * @param setSelection - the function that modifies the selection of the input
*/
-function clearSelectedValue(value, selection, setValue, setSelection) {
+function clearSelectedValue(value: string, selection: {start: number; end: number}, setValue: (value: string) => void, setSelection: (value: {start: number; end: number}) => void) {
let newValue;
let newCursorPosition;
@@ -109,7 +103,7 @@ function clearSelectedValue(value, selection, setValue, setSelection) {
setSelection({start: newCursorPosition, end: newCursorPosition});
}
-function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
+function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}}: TimePickerProps, ref: ForwardedRef) {
const {numberFormat, translate} = useLocalize();
const {isExtraSmallScreenHeight} = useWindowDimensions();
const styles = useThemeStyles();
@@ -125,8 +119,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
const [amPmValue, setAmPmValue] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).period);
const lastPressedKey = useRef('');
- const hourInputRef = useRef(null);
- const minuteInputRef = useRef(null);
+ const hourInputRef = useRef(null);
+ const minuteInputRef = useRef(null);
const {inputCallbackRef} = useAutoFocusInput();
@@ -134,7 +128,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
const focusHourInputOnLastCharacter = useCallback(() => setCursorPosition(2, hourInputRef, setSelectionHour), []);
const validate = useCallback(
- (time) => {
+ (time: string) => {
const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({timeString: time || `${hours}:${minutes} ${amPmValue}`, dateTimeString: defaultValue});
setError(!isValid);
return isValid;
@@ -154,10 +148,10 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
// This function receive value from hour input and validate it
// The valid format is HH(from 00 to 12). If the user input 9, it will be 09. If user try to change 09 to 19 it would skip the first character
- const handleHourChange = (text) => {
+ const handleHourChange = (text: string) => {
// Replace spaces with 0 to implement the following digit removal by pressing space
const trimmedText = text.replace(/ /g, '0');
- if (_.isEmpty(trimmedText)) {
+ if (!trimmedText) {
resetHours();
return;
}
@@ -179,13 +173,13 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
// To support the forward-removal using Delete key
newHour = `0${firstDigit}`;
newSelection = 1;
- } else if (firstDigit <= 1) {
+ } else if (Number(firstDigit) <= 1) {
/*
The first entered digit is 0 or 1.
If the first digit is 0, we can safely append the second digit.
If the first digit is 1, we must check the second digit to ensure it is not greater than 2, amd replace it with 0 otherwise.
*/
- newHour = `${firstDigit}${firstDigit === '1' && secondDigit > 2 ? 0 : secondDigit}`;
+ newHour = `${firstDigit}${firstDigit === '1' && Number(secondDigit) > 2 ? 0 : secondDigit}`;
newSelection = 1;
} else {
// The first entered digit is 2-9. We should replace the whole value by prepending 0 to the entered digit.
@@ -210,7 +204,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
// There is an active selection of the second digit
newHour = trimmedText.substring(0, 2).padEnd(2, '0');
newSelection = trimmedText.length === 1 ? 1 : 2;
- } else if (trimmedText.length === 1 && trimmedText <= 1) {
+ } else if (trimmedText.length === 1 && Number(trimmedText) <= 1) {
/*
The trimmed text is either 0 or 1.
We are either replacing hours with a single digit, or removing the last digit.
@@ -224,10 +218,11 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
newSelection = 2;
}
- if (newHour > 24) {
+ const newHourNumber = Number(newHour);
+ if (newHourNumber > 24) {
newHour = hours;
- } else if (newHour > 12) {
- newHour = String(newHour - 12).padStart(2, '0');
+ } else if (newHourNumber > 12) {
+ newHour = String(newHourNumber - 12).padStart(2, '0');
setAmPmValue(CONST.TIME_PERIOD.PM);
}
@@ -242,10 +237,10 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
This function receives value from the minutes input and validates it.
The valid format is MM(from 00 to 59). If the user enters 9, it will be prepended to 09. If the user tries to change 09 to 99, it would skip the character
*/
- const handleMinutesChange = (text) => {
+ const handleMinutesChange = (text: string) => {
// Replace spaces with 0 to implement the following digit removal by pressing space
const trimmedText = text.replace(/ /g, '0');
- if (_.isEmpty(trimmedText)) {
+ if (!trimmedText) {
resetMinutes();
return;
}
@@ -265,7 +260,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
// To support the forward-removal using Delete key
newMinute = `0${firstDigit}`;
newSelection = 1;
- } else if (firstDigit <= 5) {
+ } else if (Number(firstDigit) <= 5) {
// The first entered digit is 0-5, we can safely append the second digit.
newMinute = `${firstDigit}${trimmedText[2] || 0}`;
newSelection = 1;
@@ -292,7 +287,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
// There is an active selection of the second digit
newMinute = trimmedText.substring(0, 2).padEnd(2, '0');
newSelection = trimmedText.length === 1 ? 1 : 2;
- } else if (trimmedText.length === 1 && trimmedText <= 5) {
+ } else if (trimmedText.length === 1 && Number(trimmedText) <= 5) {
/*
The trimmed text is from 0 to 5.
We are either replacing minutes with a single digit, or removing the last digit.
@@ -306,7 +301,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
newSelection = 2;
}
- if (newMinute > 59) {
+ if (Number(newMinute) > 59) {
newMinute = minutes;
}
@@ -317,15 +312,13 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
/**
* Update amount with number or Backspace pressed for BigNumberPad.
* Validate new amount with decimal number regex up to 6 digits and 2 decimal digit to enable Next button
- *
- * @param {String} key
*/
const updateAmountNumberPad = useCallback(
- (key) => {
- const isHourFocused = hourInputRef.current.isFocused();
- const isMinuteFocused = minuteInputRef.current.isFocused();
+ (key: string) => {
+ const isHourFocused = hourInputRef.current?.isFocused();
+ const isMinuteFocused = minuteInputRef.current?.isFocused();
if (!isHourFocused && !isMinuteFocused) {
- minuteInputRef.current.focus();
+ minuteInputRef.current?.focus();
}
if (key === '.') {
@@ -370,13 +363,11 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
);
const arrowLeftCallback = useCallback(
- (e) => {
- const isMinuteFocused = minuteInputRef.current.isFocused();
+ (e?: GestureResponderEvent | KeyboardEvent) => {
+ const isMinuteFocused = minuteInputRef.current?.isFocused();
if (isMinuteFocused && selectionMinute.start === 0) {
- if (e) {
- // Check e to be truthy to avoid crashing on Android (e is undefined there)
- e.preventDefault();
- }
+ // Check e to be truthy to avoid crashing on Android (e is undefined there)
+ e?.preventDefault();
focusHourInputOnLastCharacter();
}
},
@@ -384,14 +375,12 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
[selectionHour, selectionMinute],
);
const arrowRightCallback = useCallback(
- (e) => {
- const isHourFocused = hourInputRef.current.isFocused();
+ (e?: GestureResponderEvent | KeyboardEvent) => {
+ const isHourFocused = hourInputRef.current?.isFocused();
if (isHourFocused && selectionHour.start === 2) {
- if (e) {
- // Check e to be truthy to avoid crashing on Android (e is undefined there)
- e.preventDefault();
- }
+ // Check e to be truthy to avoid crashing on Android (e is undefined there)
+ e?.preventDefault();
focusMinuteInputOnFirstCharacter();
}
},
@@ -403,8 +392,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT, arrowRightCallback, arrowConfig);
const handleFocusOnBackspace = useCallback(
- (e) => {
- if (selectionMinute.start !== 0 || selectionMinute.end !== 0 || e.key !== 'Backspace') {
+ (e: NativeSyntheticEvent) => {
+ if (selectionMinute.start !== 0 || selectionMinute.end !== 0 || e.nativeEvent.key !== 'Backspace') {
return;
}
e.preventDefault();
@@ -422,7 +411,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
}
return (
@@ -457,15 +446,14 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
lastPressedKey.current = e.nativeEvent.key;
}}
onChangeAmount={handleHourChange}
- role={CONST.ACCESSIBILITY_ROLE.TEXT}
- ref={(ref) => {
- if (typeof forwardedRef === 'function') {
- forwardedRef({refHour: ref, minuteRef: minuteInputRef.current});
- } else if (forwardedRef && _.has(forwardedRef, 'current')) {
+ ref={(textInputRef) => {
+ if (typeof ref === 'function') {
+ ref({hourRef: textInputRef as TextInput | null, minuteRef: minuteInputRef.current});
+ } else if (ref && 'current' in ref) {
// eslint-disable-next-line no-param-reassign
- forwardedRef.current = {hourRef: ref, minuteRef: minuteInputRef.current};
+ ref.current = {hourRef: textInputRef as TextInput | null, minuteRef: minuteInputRef.current};
}
- hourInputRef.current = ref;
+ hourInputRef.current = textInputRef as TextInput | null;
}}
onSelectionChange={(e) => {
setSelectionHour(e.nativeEvent.selection);
@@ -473,7 +461,6 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
style={styles.timePickerInput}
touchableInputWrapperStyle={styles.timePickerHeight100}
selection={selectionHour}
- showSoftInputOnFocus={false}
/>
{CONST.COLON} {
- if (typeof forwardedRef === 'function') {
- forwardedRef({refHour: hourInputRef.current, minuteRef: ref});
- } else if (forwardedRef && _.has(forwardedRef, 'current')) {
+ ref={(textInputRef) => {
+ if (typeof ref === 'function') {
+ ref({hourRef: hourInputRef.current, minuteRef: textInputRef as TextInput | null});
+ } else if (ref && 'current' in ref) {
// eslint-disable-next-line no-param-reassign
- minuteInputRef.current = {hourRef: hourInputRef.current, minuteInputRef: ref};
+ ref.current = {hourRef: hourInputRef.current, minuteRef: textInputRef as TextInput | null};
}
- minuteInputRef.current = ref;
- inputCallbackRef(ref);
+ minuteInputRef.current = textInputRef as TextInput | null;
+ inputCallbackRef(textInputRef as TextInput | null);
}}
onSelectionChange={(e) => {
setSelectionMinute(e.nativeEvent.selection);
@@ -501,7 +487,6 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
style={styles.timePickerInput}
touchableInputWrapperStyle={styles.timePickerHeight100}
selection={selectionMinute}
- showSoftInputOnFocus={false}
/>
@@ -520,7 +505,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
/>