diff --git a/.env.example b/.env.example
index 601813eeab98..944da2aa9296 100644
--- a/.env.example
+++ b/.env.example
@@ -26,7 +26,5 @@ EXPENSIFY_ACCOUNT_ID_QA=-1
EXPENSIFY_ACCOUNT_ID_QA_TRAVIS=-1
EXPENSIFY_ACCOUNT_ID_RECEIPTS=-1
EXPENSIFY_ACCOUNT_ID_REWARDS=-1
-EXPENSIFY_ACCOUNT_ID_SAASTR=-1
-EXPENSIFY_ACCOUNT_ID_SBE=-1
EXPENSIFY_ACCOUNT_ID_STUDENT_AMBASSADOR=-1
EXPENSIFY_ACCOUNT_ID_SVFG=-1
diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml
index 6bdf500912c0..ffaa55c0b3be 100644
--- a/.github/actions/composite/setupNode/action.yml
+++ b/.github/actions/composite/setupNode/action.yml
@@ -24,6 +24,31 @@ runs:
path: desktop/node_modules
key: ${{ runner.os }}-desktop-node-modules-${{ hashFiles('desktop/package-lock.json') }}
+ - name: Check if patch files changed
+ id: patchCheck
+ shell: bash
+ run: |
+ set -e
+ if [[ `git diff main --name-only | grep \.patch` != null ]]; then
+ echo 'CHANGES_IN_PATCH_FILES=true' >> "$GITHUB_OUTPUT"
+ else
+ echo 'CHANGES_IN_PATCH_FILES=false' >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Patch root project node packages
+ shell: bash
+ if: |
+ steps.patchCheck.outputs.CHANGES_IN_PATCH_FILES == 'true' &&
+ steps.cache-node-modules.outputs.cache-hit == 'true'
+ run: npx patch-package
+
+ - name: Patch node packages for desktop submodule
+ shell: bash
+ if: |
+ steps.patchCheck.outputs.CHANGES_IN_PATCH_FILES == 'true' &&
+ steps.cache-desktop-node-modules.outputs.cache-hit == 'true'
+ run: cd desktop && npx patch-package
+
- name: Install root project node packages
if: steps.cache-node-modules.outputs.cache-hit != 'true'
uses: nick-fields/retry@v2
diff --git a/android/app/build.gradle b/android/app/build.gradle
index be63a2a37f82..294d2d334ffd 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -22,7 +22,7 @@ react {
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
- // debuggableVariants = ["liteDebug", "prodDebug"]
+ debuggableVariants = ["developmentDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
@@ -90,8 +90,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001035620
- versionName "1.3.56-20"
+ versionCode 1001035705
+ versionName "1.3.57-5"
}
flavorDimensions "default"
diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md
index 414cb9d49ef1..0d6774792c45 100644
--- a/contributingGuides/TS_STYLE.md
+++ b/contributingGuides/TS_STYLE.md
@@ -23,6 +23,7 @@
- [1.16 Reusable Types](#reusable-types)
- [1.17 `.tsx`](#tsx)
- [1.18 No inline prop types](#no-inline-prop-types)
+ - [1.19 Satisfies operator](#satisfies-operator)
- [Exception to Rules](#exception-to-rules)
- [Communication Items](#communication-items)
- [Migration Guidelines](#migration-guidelines)
@@ -101,7 +102,7 @@ type Foo = {
-- [1.2](#d-ts-extension) **`d.ts` Extension**: Do not use `d.ts` file extension even when a file contains only type declarations. Only exception is the `global.d.ts` file in which third party packages can be modified using module augmentation. Refer to the [Communication Items](#communication-items) section to learn more about module augmentation.
+- [1.2](#d-ts-extension) **`d.ts` Extension**: Do not use `d.ts` file extension even when a file contains only type declarations. Only exceptions are `src/types/global.d.ts` and `src/types/modules/*.d.ts` files in which third party packages can be modified using module augmentation. Refer to the [Communication Items](#communication-items) section to learn more about module augmentation.
> Why? Type errors in `d.ts` files are not checked by TypeScript [^1].
@@ -358,7 +359,7 @@ type Foo = {
-- [1.15](#file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files.
+- [1.15](#file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. Do not use [`satisfies` operator](#satisfies-operator) for platform-specific implementations, always define shared types that complies with all variants.
> Why? To encourage consistent API across platform-specific implementations. If you're migrating module that doesn't have a default implement (i.e. `index.ts`, e.g. `getPlatform`), refer to [Migration Guidelines](#migration-guidelines) for further information.
@@ -458,6 +459,34 @@ type Foo = {
}
```
+
+
+- [1.19](#satisfies-operator) **Satisfies Operator**: Use the `satisfies` operator when you need to validate that the structure of an expression matches a specific type, without affecting the resulting type of the expression.
+
+ > Why? TypeScript developers often want to ensure that an expression aligns with a certain type, but they also want to retain the most specific type possible for inference purposes. The `satisfies` operator assists in doing both.
+
+ ```ts
+ // BAD
+ const sizingStyles = {
+ w50: {
+ width: '50%',
+ },
+ mw100: {
+ maxWidth: '100%',
+ },
+ } as const;
+
+ // GOOD
+ const sizingStyles = {
+ w50: {
+ width: '50%',
+ },
+ mw100: {
+ maxWidth: '100%',
+ },
+ } satisfies Record;
+ ```
+
## Exception to Rules
Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. The internal engineer assigned to the PR should be the one that approves each exception, however all discussion regarding granting exceptions should happen in the public channel instead of the GitHub PR page so that the TS migration team can access them easily.
@@ -472,9 +501,11 @@ This rule will apply until the migration is done. After the migration, discussio
- I think types definitions in a third party library is incomplete or incorrect
-When the library indeed contains incorrect or missing type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `/src/global.d.ts`.
+When the library indeed contains incorrect or missing type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `/src/types/modules/*.d.ts`, each library as a separate file.
```ts
+// external-library-name.d.ts
+
declare module "external-library-name" {
interface LibraryComponentProps {
// Add or modify typings
diff --git a/docs/Card-Rev-Share-for-Approved-Partners.md b/docs/Card-Rev-Share-for-Approved-Partners.md
new file mode 100644
index 000000000000..9b5647a004d3
--- /dev/null
+++ b/docs/Card-Rev-Share-for-Approved-Partners.md
@@ -0,0 +1,17 @@
+---
+title: Expensify Card revenue share for ExpensifyApproved! partners
+description: Earn money when your clients adopt the Expensify Card
+---
+
+
+# About
+Start making more with us! We're thrilled to announce a new incentive for our US-based ExpensifyApproved! partner accountants. You can now earn additional income for your firm every time your client uses their Expensify Card. We're offering 0.5% of the total Expensify Card spend of your clients in cashback returned to your firm. The more your clients spend, the more cashback your firm receives!
+ This program is currently only available to US-based ExpensifyApproved! partner accountants.
+
+# How-to
+To benefit from this program, all you need to do is ensure that you are listed as a domain admin on your client's Expensify account. If you're not currently a domain admin, your client can follow the instructions outlined in [our help article](https://community.expensify.com/discussion/5749/how-to-add-and-remove-domain-admins#:~:text=Domain%20Admins%20have%20total%20control,a%20member%20of%20the%20domain.) to assign you this role.
+# FAQ
+- What if my firm is not permitted to accept revenue share from our clients?
+ We understand that different firms may have different policies. If your firm is unable to accept this revenue share, you can pass the revenue share back to your client to give them an additional 0.5% of cash back using your own internal payment tools.
+- What if my firm does not wish to participate in the program?
+ Please reach out to your assigned partner manager at new.expensify.com to inform them you would not like to accept the revenue share nor do you want to pass the revenue share to your clients.
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
index 07f9f23bdbbf..39d62bb0ea9c 100644
--- a/docs/_layouts/default.html
+++ b/docs/_layouts/default.html
@@ -2,7 +2,7 @@
-
+
Expensify Help
@@ -13,12 +13,17 @@
+
+
+
{% seo %}
-
+
+
+
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index a5f25ac8cd7a..384c96a5712b 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 1.3.56
+ 1.3.57CFBundleSignature????CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.56.20
+ 1.3.57.5ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index f16cc3e67a1f..90502c109aab 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 1.3.56
+ 1.3.57CFBundleSignature????CFBundleVersion
- 1.3.56.20
+ 1.3.57.5
diff --git a/package-lock.json b/package-lock.json
index 7f2c28c67ad7..1d6b5ce003ba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.56-20",
+ "version": "1.3.57-5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.56-20",
+ "version": "1.3.57-5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -45,12 +45,11 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
"fbjs": "^3.0.2",
"htmlparser2": "^7.2.0",
+ "idb-keyval": "^6.2.1",
"jest-when": "^3.5.2",
- "localforage": "^1.10.0",
- "localforage-removeitems": "^1.4.0",
"lodash": "4.17.21",
"lottie-react-native": "^5.1.6",
"mapbox-gl": "^2.15.0",
@@ -86,7 +85,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.61",
+ "react-native-onyx": "1.0.63",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.6.2",
"react-native-performance": "^4.0.0",
@@ -29544,8 +29543,8 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
- "integrity": "sha512-nNYAweSE5bwjKyFTi9tz+p1z+gxkytCnIa8M11vnseV60ZzJespcwB/2SbWkdaAL5wpvcgHLlFTTGbPUwIiTvw==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
+ "integrity": "sha512-uhcd3sV276dFdrJCvk+KFWjMQ6rMKUNrNcBiZNB58S9NECCVioRuqFJXgfy90DcQz2CELHzZATdciXQ+fJ0Qhw==",
"license": "MIT",
"dependencies": {
"classnames": "2.3.1",
@@ -32145,6 +32144,11 @@
"node": ">= 6"
}
},
+ "node_modules/idb-keyval": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
+ "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"funding": [
@@ -36259,12 +36263,6 @@
"lie": "3.1.1"
}
},
- "node_modules/localforage-removeitems": {
- "version": "1.4.0",
- "dependencies": {
- "localforage": ">=1.4.0"
- }
- },
"node_modules/locate-path": {
"version": "6.0.0",
"license": "MIT",
@@ -43117,9 +43115,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "1.0.61",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.61.tgz",
- "integrity": "sha512-8lESU3qczhhWzyJSViZsgREyCZP+evLcrfZ9xmH5ys0PXWVpxUymxZ6zfTWbWlAkCQCFpYSikk2fXcpC6clF0g==",
+ "version": "1.0.63",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.63.tgz",
+ "integrity": "sha512-GJc4vlhx/+vnM+xRZqT7aq/BEYMAFcPxFF5TW5OKS7j5Ba/SKMmooZB5zAutsbVq5tfh+Cfh3L2O4rNRXNjKEg==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
@@ -43130,17 +43128,13 @@
"npm": "8.11.0"
},
"peerDependencies": {
- "localforage": "^1.10.0",
- "localforage-removeitems": "^1.4.0",
+ "idb-keyval": "^6.2.1",
"react": ">=18.1.0",
"react-native-performance": "^4.0.0",
"react-native-quick-sqlite": "^8.0.0-beta.2"
},
"peerDependenciesMeta": {
- "localforage": {
- "optional": true
- },
- "localforage-removeitems": {
+ "idb-keyval": {
"optional": true
},
"react-native-performance": {
@@ -70819,9 +70813,9 @@
}
},
"expensify-common": {
- "version": "git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
- "integrity": "sha512-nNYAweSE5bwjKyFTi9tz+p1z+gxkytCnIa8M11vnseV60ZzJespcwB/2SbWkdaAL5wpvcgHLlFTTGbPUwIiTvw==",
- "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
+ "version": "git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
+ "integrity": "sha512-uhcd3sV276dFdrJCvk+KFWjMQ6rMKUNrNcBiZNB58S9NECCVioRuqFJXgfy90DcQz2CELHzZATdciXQ+fJ0Qhw==",
+ "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
"requires": {
"classnames": "2.3.1",
"clipboard": "2.0.4",
@@ -72588,6 +72582,11 @@
"postcss": "^7.0.14"
}
},
+ "idb-keyval": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
+ "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="
+ },
"ieee754": {
"version": "1.2.1"
},
@@ -75244,12 +75243,6 @@
"lie": "3.1.1"
}
},
- "localforage-removeitems": {
- "version": "1.4.0",
- "requires": {
- "localforage": ">=1.4.0"
- }
- },
"locate-path": {
"version": "6.0.0",
"requires": {
@@ -80094,9 +80087,9 @@
}
},
"react-native-onyx": {
- "version": "1.0.61",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.61.tgz",
- "integrity": "sha512-8lESU3qczhhWzyJSViZsgREyCZP+evLcrfZ9xmH5ys0PXWVpxUymxZ6zfTWbWlAkCQCFpYSikk2fXcpC6clF0g==",
+ "version": "1.0.63",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.63.tgz",
+ "integrity": "sha512-GJc4vlhx/+vnM+xRZqT7aq/BEYMAFcPxFF5TW5OKS7j5Ba/SKMmooZB5zAutsbVq5tfh+Cfh3L2O4rNRXNjKEg==",
"requires": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
diff --git a/package.json b/package.json
index 6d6c62c0e0b9..eeb52419e1a0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.56-20",
+ "version": "1.3.57-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.",
@@ -85,12 +85,11 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
"fbjs": "^3.0.2",
"htmlparser2": "^7.2.0",
+ "idb-keyval": "^6.2.1",
"jest-when": "^3.5.2",
- "localforage": "^1.10.0",
- "localforage-removeitems": "^1.4.0",
"lodash": "4.17.21",
"lottie-react-native": "^5.1.6",
"mapbox-gl": "^2.15.0",
@@ -126,7 +125,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.61",
+ "react-native-onyx": "1.0.63",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.6.2",
"react-native-performance": "^4.0.0",
diff --git a/src/CONFIG.js b/src/CONFIG.ts
similarity index 62%
rename from src/CONFIG.js
rename to src/CONFIG.ts
index 4f9eab573a9e..e08b771d4b34 100644
--- a/src/CONFIG.js
+++ b/src/CONFIG.ts
@@ -1,25 +1,24 @@
-import get from 'lodash/get';
import {Platform} from 'react-native';
-import Config from 'react-native-config';
-import getPlatform from './libs/getPlatform/index';
+import Config, {NativeConfig} from 'react-native-config';
+import getPlatform from './libs/getPlatform';
import * as Url from './libs/Url';
import CONST from './CONST';
// react-native-config doesn't trim whitespace on iOS for some reason so we
-// add a trim() call to lodashGet here to prevent headaches
-const lodashGet = (config, key, defaultValue) => get(config, key, defaultValue).trim();
+// add a trim() call to prevent headaches
+const get = (config: NativeConfig, key: string, defaultValue: string): string => (config?.[key] ?? defaultValue).trim();
// Set default values to contributor friendly values to make development work out of the box without an .env file
-const ENVIRONMENT = lodashGet(Config, 'ENVIRONMENT', CONST.ENVIRONMENT.DEV);
-const newExpensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com/'));
-const expensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'EXPENSIFY_URL', 'https://www.expensify.com/'));
-const stagingExpensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'STAGING_EXPENSIFY_URL', 'https://staging.expensify.com/'));
-const stagingSecureExpensifyUrl = Url.addTrailingForwardSlash(lodashGet(Config, 'STAGING_SECURE_EXPENSIFY_URL', 'https://staging-secure.expensify.com/'));
-const ngrokURL = Url.addTrailingForwardSlash(lodashGet(Config, 'NGROK_URL', ''));
-const secureNgrokURL = Url.addTrailingForwardSlash(lodashGet(Config, 'SECURE_NGROK_URL', ''));
-const secureExpensifyUrl = Url.addTrailingForwardSlash(lodashGet(Config, 'SECURE_EXPENSIFY_URL', 'https://secure.expensify.com/'));
-const useNgrok = lodashGet(Config, 'USE_NGROK', 'false') === 'true';
-const useWebProxy = lodashGet(Config, 'USE_WEB_PROXY', 'true') === 'true';
+const ENVIRONMENT = get(Config, 'ENVIRONMENT', CONST.ENVIRONMENT.DEV);
+const newExpensifyURL = Url.addTrailingForwardSlash(get(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com/'));
+const expensifyURL = Url.addTrailingForwardSlash(get(Config, 'EXPENSIFY_URL', 'https://www.expensify.com/'));
+const stagingExpensifyURL = Url.addTrailingForwardSlash(get(Config, 'STAGING_EXPENSIFY_URL', 'https://staging.expensify.com/'));
+const stagingSecureExpensifyUrl = Url.addTrailingForwardSlash(get(Config, 'STAGING_SECURE_EXPENSIFY_URL', 'https://staging-secure.expensify.com/'));
+const ngrokURL = Url.addTrailingForwardSlash(get(Config, 'NGROK_URL', ''));
+const secureNgrokURL = Url.addTrailingForwardSlash(get(Config, 'SECURE_NGROK_URL', ''));
+const secureExpensifyUrl = Url.addTrailingForwardSlash(get(Config, 'SECURE_EXPENSIFY_URL', 'https://secure.expensify.com/'));
+const useNgrok = get(Config, 'USE_NGROK', 'false') === 'true';
+const useWebProxy = get(Config, 'USE_WEB_PROXY', 'true') === 'true';
const expensifyComWithProxy = getPlatform() === 'web' && useWebProxy ? '/' : expensifyURL;
// Throw errors on dev if config variables are not set correctly
@@ -58,8 +57,8 @@ export default {
DEFAULT_SECURE_API_ROOT: secureURLRoot,
STAGING_API_ROOT: stagingExpensifyURL,
STAGING_SECURE_API_ROOT: stagingSecureExpensifyUrl,
- PARTNER_NAME: lodashGet(Config, 'EXPENSIFY_PARTNER_NAME', 'chat-expensify-com'),
- PARTNER_PASSWORD: lodashGet(Config, 'EXPENSIFY_PARTNER_PASSWORD', 'e21965746fd75f82bb66'),
+ PARTNER_NAME: get(Config, 'EXPENSIFY_PARTNER_NAME', 'chat-expensify-com'),
+ PARTNER_PASSWORD: get(Config, 'EXPENSIFY_PARTNER_PASSWORD', 'e21965746fd75f82bb66'),
EXPENSIFY_CASH_REFERER: 'ecash',
CONCIERGE_URL_PATHNAME: 'concierge/',
DEVPORTAL_URL_PATHNAME: '_devportal/',
@@ -69,8 +68,8 @@ export default {
IS_IN_STAGING: ENVIRONMENT === CONST.ENVIRONMENT.STAGING,
IS_USING_LOCAL_WEB: useNgrok || expensifyURLRoot.includes('dev'),
PUSHER: {
- APP_KEY: lodashGet(Config, 'PUSHER_APP_KEY', '268df511a204fbb60884'),
- SUFFIX: lodashGet(Config, 'PUSHER_DEV_SUFFIX', ''),
+ APP_KEY: get(Config, 'PUSHER_APP_KEY', '268df511a204fbb60884'),
+ SUFFIX: get(Config, 'PUSHER_DEV_SUFFIX', ''),
CLUSTER: 'mt1',
},
SITE_TITLE: 'New Expensify',
@@ -78,11 +77,11 @@ export default {
DEFAULT: '/favicon.png',
UNREAD: '/favicon-unread.png',
},
- CAPTURE_METRICS: lodashGet(Config, 'CAPTURE_METRICS', 'false') === 'true',
- ONYX_METRICS: lodashGet(Config, 'ONYX_METRICS', 'false') === 'true',
- DEV_PORT: process.env.PORT || 8082,
- E2E_TESTING: lodashGet(Config, 'E2E_TESTING', 'false') === 'true',
- SEND_CRASH_REPORTS: lodashGet(Config, 'SEND_CRASH_REPORTS', 'false') === 'true',
+ CAPTURE_METRICS: get(Config, 'CAPTURE_METRICS', 'false') === 'true',
+ ONYX_METRICS: get(Config, 'ONYX_METRICS', 'false') === 'true',
+ DEV_PORT: process.env.PORT ?? 8082,
+ E2E_TESTING: get(Config, 'E2E_TESTING', 'false') === 'true',
+ SEND_CRASH_REPORTS: get(Config, 'SEND_CRASH_REPORTS', 'false') === 'true',
IS_USING_WEB_PROXY: getPlatform() === 'web' && useWebProxy,
APPLE_SIGN_IN: {
SERVICE_ID: 'com.chat.expensify.chat.AppleSignIn',
@@ -92,4 +91,4 @@ export default {
WEB_CLIENT_ID: '921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com',
IOS_CLIENT_ID: '921154746561-s3uqn2oe4m85tufi6mqflbfbuajrm2i3.apps.googleusercontent.com',
},
-};
+} as const;
diff --git a/src/CONST.js b/src/CONST.ts
similarity index 96%
rename from src/CONST.js
rename to src/CONST.ts
index 878d3f858dfb..864d078ab04b 100755
--- a/src/CONST.js
+++ b/src/CONST.ts
@@ -1,4 +1,4 @@
-import lodashGet from 'lodash/get';
+/* eslint-disable @typescript-eslint/naming-convention */
import Config from 'react-native-config';
import * as KeyCommand from 'react-native-key-command';
import * as Url from './libs/Url';
@@ -6,24 +6,24 @@ import SCREENS from './SCREENS';
const CLOUDFRONT_DOMAIN = 'cloudfront.net';
const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`;
-const ACTIVE_EXPENSIFY_URL = Url.addTrailingForwardSlash(lodashGet(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com'));
+const ACTIVE_EXPENSIFY_URL = Url.addTrailingForwardSlash(Config?.NEW_EXPENSIFY_URL ?? 'https://new.expensify.com');
const USE_EXPENSIFY_URL = 'https://use.expensify.com';
const PLATFORM_OS_MACOS = 'Mac OS';
const PLATFORM_IOS = 'iOS';
const ANDROID_PACKAGE_NAME = 'com.expensify.chat';
const CURRENT_YEAR = new Date().getFullYear();
-const PULL_REQUEST_NUMBER = lodashGet(Config, 'PULL_REQUEST_NUMBER', '');
-
-const keyModifierControl = lodashGet(KeyCommand, 'constants.keyModifierControl', 'keyModifierControl');
-const keyModifierCommand = lodashGet(KeyCommand, 'constants.keyModifierCommand', 'keyModifierCommand');
-const keyModifierShiftControl = lodashGet(KeyCommand, 'constants.keyModifierShiftControl', 'keyModifierShiftControl');
-const keyModifierShiftCommand = lodashGet(KeyCommand, 'constants.keyModifierShiftCommand', 'keyModifierShiftCommand');
-const keyInputEscape = lodashGet(KeyCommand, 'constants.keyInputEscape', 'keyInputEscape');
-const keyInputEnter = lodashGet(KeyCommand, 'constants.keyInputEnter', 'keyInputEnter');
-const keyInputUpArrow = lodashGet(KeyCommand, 'constants.keyInputUpArrow', 'keyInputUpArrow');
-const keyInputDownArrow = lodashGet(KeyCommand, 'constants.keyInputDownArrow', 'keyInputDownArrow');
-const keyInputLeftArrow = lodashGet(KeyCommand, 'constants.keyInputLeftArrow', 'keyInputLeftArrow');
-const keyInputRightArrow = lodashGet(KeyCommand, 'constants.keyInputRightArrow', 'keyInputRightArrow');
+const PULL_REQUEST_NUMBER = Config?.PULL_REQUEST_NUMBER ?? '';
+
+const keyModifierControl = KeyCommand?.constants?.keyModifierControl ?? 'keyModifierControl';
+const keyModifierCommand = KeyCommand?.constants?.keyModifierCommand ?? 'keyModifierCommand';
+const keyModifierShiftControl = KeyCommand?.constants?.keyModifierShiftControl ?? 'keyModifierShiftControl';
+const keyModifierShiftCommand = KeyCommand?.constants?.keyModifierShiftCommand ?? 'keyModifierShiftCommand';
+const keyInputEscape = KeyCommand?.constants?.keyInputEscape ?? 'keyInputEscape';
+const keyInputEnter = KeyCommand?.constants?.keyInputEnter ?? 'keyInputEnter';
+const keyInputUpArrow = KeyCommand?.constants?.keyInputUpArrow ?? 'keyInputUpArrow';
+const keyInputDownArrow = KeyCommand?.constants?.keyInputDownArrow ?? 'keyInputDownArrow';
+const keyInputLeftArrow = KeyCommand?.constants?.keyInputLeftArrow ?? 'keyInputLeftArrow';
+const keyInputRightArrow = KeyCommand?.constants?.keyInputRightArrow ?? 'keyInputRightArrow';
// describes if a shortcut key can cause navigation
const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT';
@@ -586,6 +586,7 @@ const CONST = {
MUTE: 'mute',
DAILY: 'daily',
ALWAYS: 'always',
+ HIDDEN: 'hidden',
},
// Options for which room members can post
WRITE_CAPABILITIES: {
@@ -817,8 +818,10 @@ const CONST = {
},
FILE_TYPE_REGEX: {
- IMAGE: /\.(jpg|jpeg|png|webp|avif|gif|tiff|wbmp|ico|jng|bmp|heic|svg|svg2)$/,
- VIDEO: /\.(3gp|h261|h263|h264|m4s|jpgv|jpm|jpgm|mp4|mp4v|mpg4|mpeg|mpg|ogv|ogg|mov|qt|webm|flv|mkv|wmv|wav|avi|movie|f4v|avchd|mp2|mpe|mpv|m4v|swf)$/,
+ // Image MimeTypes allowed by iOS photos app.
+ IMAGE: /\.(jpg|jpeg|png|webp|gif|tiff|bmp|heic|heif)$/,
+ // Video MimeTypes allowed by iOS photos app.
+ VIDEO: /\.(mov|mp4)$/,
},
IOS_CAMERAROLL_ACCESS_ERROR: 'Access to photo library was denied',
ADD_PAYMENT_MENU_POSITION_Y: 226,
@@ -881,24 +884,22 @@ const CONST = {
},
ACCOUNT_ID: {
- ACCOUNTING: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_ACCOUNTING', 9645353)),
- ADMIN: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_ADMIN', -1)),
- BILLS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_BILLS', 1371)),
- CHRONOS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_CHRONOS', 10027416)),
- CONCIERGE: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_CONCIERGE', 8392101)),
- CONTRIBUTORS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_CONTRIBUTORS', 9675014)),
- FIRST_RESPONDER: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_FIRST_RESPONDER', 9375152)),
- HELP: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_HELP', -1)),
- INTEGRATION_TESTING_CREDS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_INTEGRATION_TESTING_CREDS', -1)),
- PAYROLL: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_PAYROLL', 9679724)),
- QA: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_QA', 3126513)),
- QA_TRAVIS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_QA_TRAVIS', 8595733)),
- RECEIPTS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_RECEIPTS', -1)),
- REWARDS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_REWARDS', 11023767)), // rewards@expensify.com
- SAASTR: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_SAASTR', 15252830)),
- SBE: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_SBE', 15305309)),
- STUDENT_AMBASSADOR: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_STUDENT_AMBASSADOR', 10476956)),
- SVFG: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_SVFG', 2012843)),
+ ACCOUNTING: Number(Config?.EXPENSIFY_ACCOUNT_ID_ACCOUNTING ?? 9645353),
+ ADMIN: Number(Config?.EXPENSIFY_ACCOUNT_ID_ADMIN ?? -1),
+ BILLS: Number(Config?.EXPENSIFY_ACCOUNT_ID_BILLS ?? 1371),
+ CHRONOS: Number(Config?.EXPENSIFY_ACCOUNT_ID_CHRONOS ?? 10027416),
+ CONCIERGE: Number(Config?.EXPENSIFY_ACCOUNT_ID_CONCIERGE ?? 8392101),
+ CONTRIBUTORS: Number(Config?.EXPENSIFY_ACCOUNT_ID_CONTRIBUTORS ?? 9675014),
+ FIRST_RESPONDER: Number(Config?.EXPENSIFY_ACCOUNT_ID_FIRST_RESPONDER ?? 9375152),
+ HELP: Number(Config?.EXPENSIFY_ACCOUNT_ID_HELP ?? -1),
+ INTEGRATION_TESTING_CREDS: Number(Config?.EXPENSIFY_ACCOUNT_ID_INTEGRATION_TESTING_CREDS ?? -1),
+ PAYROLL: Number(Config?.EXPENSIFY_ACCOUNT_ID_PAYROLL ?? 9679724),
+ QA: Number(Config?.EXPENSIFY_ACCOUNT_ID_QA ?? 3126513),
+ QA_TRAVIS: Number(Config?.EXPENSIFY_ACCOUNT_ID_QA_TRAVIS ?? 8595733),
+ RECEIPTS: Number(Config?.EXPENSIFY_ACCOUNT_ID_RECEIPTS ?? -1),
+ REWARDS: Number(Config?.EXPENSIFY_ACCOUNT_ID_REWARDS ?? 11023767), // rewards@expensify.com
+ STUDENT_AMBASSADOR: Number(Config?.EXPENSIFY_ACCOUNT_ID_STUDENT_AMBASSADOR ?? 10476956),
+ SVFG: Number(Config?.EXPENSIFY_ACCOUNT_ID_SVFG ?? 2012843),
},
ENVIRONMENT: {
@@ -1200,6 +1201,8 @@ const CONST = {
TIME_STARTS_01: /^01:\d{2} [AP]M$/,
TIME_FORMAT: /^\d{2}:\d{2} [AP]M$/,
DATE_TIME_FORMAT: /^\d{2}-\d{2} \d{2}:\d{2} [AP]M$/,
+ ATTACHMENT_ROUTE: /\/r\/(\d*)\/attachment/,
+ ILLEGAL_FILENAME_CHARACTERS: /\/|<|>|\*|"|:|\?|\\|\|/g,
},
PRONOUNS: {
@@ -1233,8 +1236,6 @@ const CONST = {
this.EMAIL.QA,
this.EMAIL.QA_TRAVIS,
this.EMAIL.RECEIPTS,
- this.EMAIL.SAASTR,
- this.EMAIL.SBE,
this.EMAIL.STUDENT_AMBASSADOR,
this.EMAIL.SVFG,
];
@@ -1255,8 +1256,6 @@ const CONST = {
this.ACCOUNT_ID.QA_TRAVIS,
this.ACCOUNT_ID.RECEIPTS,
this.ACCOUNT_ID.REWARDS,
- this.ACCOUNT_ID.SAASTR,
- this.ACCOUNT_ID.SBE,
this.ACCOUNT_ID.STUDENT_AMBASSADOR,
this.ACCOUNT_ID.SVFG,
];
@@ -2600,10 +2599,11 @@ const CONST = {
NAVIGATE: 'NAVIGATE',
},
},
+
DEMO_PAGES: {
SAASTR: 'SaaStrDemoSetup',
SBE: 'SbeDemoSetup',
},
-};
+} as const;
export default CONST;
diff --git a/src/ROUTES.js b/src/ROUTES.js
index ef5cf62d40bf..bf1beaecb3c3 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -132,7 +132,10 @@ export default {
DETAILS: 'details',
getDetailsRoute: (login) => `details?login=${encodeURIComponent(login)}`,
PROFILE: 'a/:accountID',
- getProfileRoute: (accountID) => `a/${accountID}`,
+ getProfileRoute: (accountID, backTo = '') => {
+ const backToParam = backTo ? `?backTo=${encodeURIComponent(backTo)}` : '';
+ return `a/${accountID}${backToParam}`;
+ },
REPORT_PARTICIPANTS: 'r/:reportID/participants',
getReportParticipantsRoute: (reportID) => `r/${reportID}/participants`,
REPORT_WITH_ID_DETAILS: 'r/:reportID/details',
diff --git a/src/SCREENS.js b/src/SCREENS.ts
similarity index 98%
rename from src/SCREENS.js
rename to src/SCREENS.ts
index f69911a45284..bcb3a02cebb4 100644
--- a/src/SCREENS.js
+++ b/src/SCREENS.ts
@@ -15,4 +15,4 @@ export default {
},
SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop',
SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop',
-};
+} as const;
diff --git a/src/components/AmountTextInput.js b/src/components/AmountTextInput.js
index 8472ef271be0..98166cabd944 100644
--- a/src/components/AmountTextInput.js
+++ b/src/components/AmountTextInput.js
@@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
import TextInput from './TextInput';
import styles from '../styles/styles';
import CONST from '../CONST';
+import refPropTypes from './refPropTypes';
const propTypes = {
/** Formatted amount in local currency */
formattedAmount: PropTypes.string.isRequired,
/** A ref to forward to amount text input */
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+ forwardedRef: refPropTypes,
/** Function to call when amount in text input is changed */
onChangeAmount: PropTypes.func.isRequired,
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js
index 636a041cbb83..9779963dfc4a 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/index.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js
@@ -8,6 +8,7 @@ import PagerView from 'react-native-pager-view';
import _ from 'underscore';
import styles from '../../../../styles/styles';
import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
+import refPropTypes from '../../../refPropTypes';
const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));
@@ -50,7 +51,7 @@ const pagerPropTypes = {
onSwipeSuccess: PropTypes.func,
onSwipeDown: PropTypes.func,
onPinchGestureChange: PropTypes.func,
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+ forwardedRef: refPropTypes,
containerWidth: PropTypes.number.isRequired,
containerHeight: PropTypes.number.isRequired,
};
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
index dc329a9fd3fd..bf777f41945e 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
@@ -1,4 +1,4 @@
-import React, {memo, useCallback, useContext} from 'react';
+import React, {memo, useCallback, useContext, useEffect} from 'react';
import styles from '../../../../styles/styles';
import {attachmentViewPdfPropTypes, attachmentViewPdfDefaultProps} from './propTypes';
import PDFView from '../../../PDFView';
@@ -7,6 +7,11 @@ import AttachmentCarouselPagerContext from '../../AttachmentCarousel/Pager/Attac
function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, isUsedInCarousel, onPress, onScaleChanged: onScaleChangedProp, onToggleKeyboard, onLoadComplete}) {
const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext);
+ useEffect(() => {
+ attachmentCarouselPagerContext.onPinchGestureChange(false);
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted
+ }, []);
+
const onScaleChanged = useCallback(
(scale) => {
onScaleChangedProp();
@@ -15,6 +20,8 @@ function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, isUsedInCarouse
if (isUsedInCarousel) {
const shouldPagerScroll = scale === 1;
+ attachmentCarouselPagerContext.onPinchGestureChange(!shouldPagerScroll);
+
if (attachmentCarouselPagerContext.shouldPagerScroll.value === shouldPagerScroll) return;
attachmentCarouselPagerContext.shouldPagerScroll.value = shouldPagerScroll;
diff --git a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js b/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js
index 6ff330d839c6..16040991a3d8 100644
--- a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js
+++ b/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js
@@ -27,8 +27,13 @@ const propTypes = {
/** create accessibility label for each item */
accessibilityLabelExtractor: PropTypes.func.isRequired,
+
+ /** Meaures the parent container's position and dimensions. */
+ measureParentContainer: PropTypes.func,
};
-const defaultProps = {};
+const defaultProps = {
+ measureParentContainer: () => {},
+};
export {propTypes, defaultProps};
diff --git a/src/components/AutoCompleteSuggestions/index.js b/src/components/AutoCompleteSuggestions/index.js
index 9e1951d9a1d5..b37fcd7181d9 100644
--- a/src/components/AutoCompleteSuggestions/index.js
+++ b/src/components/AutoCompleteSuggestions/index.js
@@ -1,7 +1,11 @@
import React from 'react';
+import {View} from 'react-native';
+import ReactDOM from 'react-dom';
import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions';
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
import {propTypes} from './autoCompleteSuggestionsPropTypes';
+import * as StyleUtils from '../../styles/StyleUtils';
+import useWindowDimensions from '../../hooks/useWindowDimensions';
/**
* On the mobile-web platform, when long-pressing on auto-complete suggestions,
@@ -10,8 +14,14 @@ import {propTypes} from './autoCompleteSuggestionsPropTypes';
* On the native platform, tapping on auto-complete suggestions will not blur the main input.
*/
-function AutoCompleteSuggestions(props) {
+function AutoCompleteSuggestions({parentContainerRef, ...props}) {
const containerRef = React.useRef(null);
+ const {windowHeight, windowWidth} = useWindowDimensions();
+ const [{width, left, bottom}, setContainerState] = React.useState({
+ width: 0,
+ left: 0,
+ bottom: 0,
+ });
React.useEffect(() => {
const container = containerRef.current;
if (!container) {
@@ -26,13 +36,26 @@ function AutoCompleteSuggestions(props) {
return () => (container.onpointerdown = null);
}, []);
- return (
+ React.useEffect(() => {
+ if (!parentContainerRef || !parentContainerRef.current) {
+ return;
+ }
+ parentContainerRef.current.measureInWindow((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w}));
+ }, [parentContainerRef, windowHeight, windowWidth]);
+
+ const componentToRender = (
);
+
+ if (!width) {
+ return componentToRender;
+ }
+
+ return ReactDOM.createPortal({componentToRender}, document.querySelector('body'));
}
AutoCompleteSuggestions.propTypes = propTypes;
diff --git a/src/components/AutoCompleteSuggestions/index.native.js b/src/components/AutoCompleteSuggestions/index.native.js
index 22af774bd4fc..514cec6cd844 100644
--- a/src/components/AutoCompleteSuggestions/index.native.js
+++ b/src/components/AutoCompleteSuggestions/index.native.js
@@ -2,7 +2,7 @@ import React from 'react';
import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions';
import {propTypes} from './autoCompleteSuggestionsPropTypes';
-function AutoCompleteSuggestions(props) {
+function AutoCompleteSuggestions({parentContainerRef, ...props}) {
// eslint-disable-next-line react/jsx-props-no-spreading
return ;
}
diff --git a/src/components/Button/index.js b/src/components/Button/index.js
index a850a43d2fb0..bfde528a4750 100644
--- a/src/components/Button/index.js
+++ b/src/components/Button/index.js
@@ -15,6 +15,7 @@ import * as Expensicons from '../Icon/Expensicons';
import withNavigationFocus from '../withNavigationFocus';
import validateSubmitShortcut from './validateSubmitShortcut';
import PressableWithFeedback from '../Pressable/PressableWithFeedback';
+import refPropTypes from '../refPropTypes';
const propTypes = {
/** The text for the button label */
@@ -118,8 +119,7 @@ const propTypes = {
accessibilityLabel: PropTypes.string,
/** A ref to forward the button */
- // eslint-disable-next-line react/forbid-prop-types
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.object})]),
+ forwardedRef: refPropTypes,
};
const defaultProps = {
diff --git a/src/components/Checkbox.js b/src/components/Checkbox.js
index 86b6e05d5ed7..1bb5824f612a 100644
--- a/src/components/Checkbox.js
+++ b/src/components/Checkbox.js
@@ -9,6 +9,7 @@ import * as Expensicons from './Icon/Expensicons';
import * as StyleUtils from '../styles/StyleUtils';
import CONST from '../CONST';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
+import refPropTypes from './refPropTypes';
const propTypes = {
/** Whether checkbox is checked */
@@ -45,7 +46,7 @@ const propTypes = {
caretSize: PropTypes.number,
/** A ref to forward to the Pressable */
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+ forwardedRef: refPropTypes,
/** An accessibility label for the checkbox */
accessibilityLabel: PropTypes.string.isRequired,
diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js
index dc9b5ba4ac67..cbd22cc39dfd 100755
--- a/src/components/Composer/index.js
+++ b/src/components/Composer/index.js
@@ -279,7 +279,14 @@ function Composer({
}
if (textInput.current !== event.target) {
- return;
+ // To make sure the composer does not capture paste events from other inputs, we check where the event originated
+ // If it did originate in another input, we return early to prevent the composer from handling the paste
+ const isTargetInput = event.target.nodeName === 'INPUT' || event.target.nodeName === 'TEXTAREA' || event.target.contentEditable === 'true';
+ if (isTargetInput) {
+ return;
+ }
+
+ textInput.current.focus();
}
event.preventDefault();
diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js
index 6d1435dca796..684296d20b11 100644
--- a/src/components/CountryPicker/index.js
+++ b/src/components/CountryPicker/index.js
@@ -7,6 +7,7 @@ import MenuItemWithTopDescription from '../MenuItemWithTopDescription';
import useLocalize from '../../hooks/useLocalize';
import CountrySelectorModal from './CountrySelectorModal';
import FormHelpMessage from '../FormHelpMessage';
+import refPropTypes from '../refPropTypes';
const propTypes = {
/** Form Error description */
@@ -19,7 +20,7 @@ const propTypes = {
onInputChange: PropTypes.func,
/** A ref to forward to MenuItemWithTopDescription */
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+ forwardedRef: refPropTypes,
};
const defaultProps = {
diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js
index 9dbd9dc3fc29..40d91ff03267 100644
--- a/src/components/EmojiPicker/EmojiPicker.js
+++ b/src/components/EmojiPicker/EmojiPicker.js
@@ -27,8 +27,8 @@ const EmojiPicker = forwardRef((props, ref) => {
horizontal: 0,
vertical: 0,
});
- const [reportAction, setReportAction] = useState({});
const [emojiPopoverAnchorOrigin, setEmojiPopoverAnchorOrigin] = useState(DEFAULT_ANCHOR_ORIGIN);
+ const [activeID, setActiveID] = useState();
const emojiPopoverAnchor = useRef(null);
const onModalHide = useRef(() => {});
const onEmojiSelected = useRef(() => {});
@@ -42,9 +42,9 @@ const EmojiPicker = forwardRef((props, ref) => {
* @param {Element} emojiPopoverAnchorValue - Element to which Popover is anchored
* @param {Object} [anchorOrigin=DEFAULT_ANCHOR_ORIGIN] - Anchor origin for Popover
* @param {Function} [onWillShow=() => {}] - Run a callback when Popover will show
- * @param {Object} reportActionValue - ReportAction for EmojiPicker
+ * @param {String} id - Unique id for EmojiPicker
*/
- const showEmojiPicker = (onModalHideValue, onEmojiSelectedValue, emojiPopoverAnchorValue, anchorOrigin, onWillShow = () => {}, reportActionValue) => {
+ const showEmojiPicker = (onModalHideValue, onEmojiSelectedValue, emojiPopoverAnchorValue, anchorOrigin, onWillShow = () => {}, id) => {
onModalHide.current = onModalHideValue;
onEmojiSelected.current = onEmojiSelectedValue;
emojiPopoverAnchor.current = emojiPopoverAnchorValue;
@@ -60,7 +60,7 @@ const EmojiPicker = forwardRef((props, ref) => {
setIsEmojiPickerVisible(true);
setEmojiPopoverAnchorPosition(value);
setEmojiPopoverAnchorOrigin(anchorOriginValue);
- setReportAction(reportActionValue);
+ setActiveID(id);
});
};
@@ -107,16 +107,16 @@ const EmojiPicker = forwardRef((props, ref) => {
};
/**
- * Whether Context Menu is active for the Report Action.
+ * Whether emoji picker is active for the given id.
*
- * @param {Number|String} actionID
+ * @param {String} id
* @return {Boolean}
*/
- const isActiveReportAction = (actionID) => Boolean(actionID) && reportAction.reportActionID === actionID;
+ const isActive = (id) => Boolean(id) && id === activeID;
const resetEmojiPopoverAnchor = () => (emojiPopoverAnchor.current = null);
- useImperativeHandle(ref, () => ({showEmojiPicker, isActiveReportAction, hideEmojiPicker, isEmojiPickerVisible, resetEmojiPopoverAnchor}));
+ useImperativeHandle(ref, () => ({showEmojiPicker, isActive, hideEmojiPicker, isEmojiPickerVisible, resetEmojiPopoverAnchor}));
useEffect(() => {
const emojiPopoverDimensionListener = Dimensions.addEventListener('change', () => {
diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js
index c78e9fdd285a..cbfc3517117c 100644
--- a/src/components/EmojiPicker/EmojiPickerButton.js
+++ b/src/components/EmojiPicker/EmojiPickerButton.js
@@ -17,12 +17,8 @@ const propTypes = {
/** Id to use for the emoji picker button */
nativeID: PropTypes.string,
- /**
- * ReportAction for EmojiPicker.
- */
- reportAction: PropTypes.shape({
- reportActionID: PropTypes.string,
- }),
+ /** Unique id for emoji picker */
+ emojiPickerID: PropTypes.string,
...withLocalizePropTypes,
};
@@ -30,7 +26,7 @@ const propTypes = {
const defaultProps = {
isDisabled: false,
nativeID: '',
- reportAction: {},
+ emojiPickerID: '',
};
function EmojiPickerButton(props) {
@@ -46,7 +42,7 @@ function EmojiPickerButton(props) {
disabled={props.isDisabled}
onPress={() => {
if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) {
- EmojiPickerAction.showEmojiPicker(props.onModalHide, props.onEmojiSelected, emojiPopoverAnchor.current, undefined, () => {}, props.reportAction);
+ EmojiPickerAction.showEmojiPicker(props.onModalHide, props.onEmojiSelected, emojiPopoverAnchor.current, undefined, () => {}, props.emojiPickerID);
} else {
EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker();
}
diff --git a/src/components/EmojiSuggestions.js b/src/components/EmojiSuggestions.js
index 76eb90f2295a..b06b0cc63eb8 100644
--- a/src/components/EmojiSuggestions.js
+++ b/src/components/EmojiSuggestions.js
@@ -45,9 +45,15 @@ const propTypes = {
/** Stores user's preferred skin tone */
preferredSkinToneIndex: PropTypes.number.isRequired,
+
+ /** Meaures the parent container's position and dimensions. */
+ measureParentContainer: PropTypes.func,
};
-const defaultProps = {highlightedEmojiIndex: 0};
+const defaultProps = {
+ highlightedEmojiIndex: 0,
+ measureParentContainer: () => {},
+};
/**
* Create unique keys for each emoji item
@@ -98,6 +104,7 @@ function EmojiSuggestions(props) {
isSuggestionPickerLarge={props.isEmojiPickerLarge}
shouldIncludeReportRecipientLocalTimeHeight={props.shouldIncludeReportRecipientLocalTimeHeight}
accessibilityLabelExtractor={keyExtractor}
+ measureParentContainer={props.measureParentContainer}
/>
);
}
diff --git a/src/components/ExceededCommentLength.js b/src/components/ExceededCommentLength.js
index c403aa63c172..2aa50779e10f 100644
--- a/src/components/ExceededCommentLength.js
+++ b/src/components/ExceededCommentLength.js
@@ -1,19 +1,29 @@
import React, {useEffect, useState, useMemo} from 'react';
import PropTypes from 'prop-types';
import {debounce} from 'lodash';
+import {withOnyx} from 'react-native-onyx';
import CONST from '../CONST';
import * as ReportUtils from '../libs/ReportUtils';
import Text from './Text';
import styles from '../styles/styles';
+import ONYXKEYS from '../ONYXKEYS';
const propTypes = {
+ /** Report ID to get the comment from (used in withOnyx) */
+ // eslint-disable-next-line react/no-unused-prop-types
+ reportID: PropTypes.string.isRequired,
+
/** Text Comment */
- comment: PropTypes.string.isRequired,
+ comment: PropTypes.string,
/** Update UI on parent when comment length is exceeded */
onExceededMaxCommentLength: PropTypes.func.isRequired,
};
+const defaultProps = {
+ comment: '',
+};
+
function ExceededCommentLength(props) {
const [commentLength, setCommentLength] = useState(0);
const updateCommentLength = useMemo(
@@ -38,5 +48,11 @@ function ExceededCommentLength(props) {
}
ExceededCommentLength.propTypes = propTypes;
-
-export default ExceededCommentLength;
+ExceededCommentLength.defaultProps = defaultProps;
+ExceededCommentLength.displayName = 'ExceededCommentLength';
+
+export default withOnyx({
+ comment: {
+ key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`,
+ },
+})(ExceededCommentLength);
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
index 643785ab09d1..bfe39459ed74 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
@@ -60,7 +60,17 @@ function ImageRenderer(props) {
const route = ROUTES.getReportAttachmentRoute(report.reportID, source);
Navigation.navigate(route);
}}
- onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
+ onLongPress={(event) =>
+ showContextMenuForReport(
+ // Imitate the web event for native renderers
+ {nativeEvent: {...(event.nativeEvent || {}), target: {tagName: 'IMG'}}},
+ anchor,
+ report.reportID,
+ action,
+ checkIfContextMenuActive,
+ ReportUtils.isArchivedRoom(report),
+ )
+ }
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
accessibilityLabel={props.translate('accessibilityHints.viewAttachment')}
>
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js
index 57e3f5ba9bf1..fa38a6fcc23d 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js
@@ -34,7 +34,7 @@ const BasePreRenderer = forwardRef((props, ref) => {
diff --git a/src/components/IllustratedHeaderPageLayout.js b/src/components/IllustratedHeaderPageLayout.js
index 7fc340426d69..d1403bd4029e 100644
--- a/src/components/IllustratedHeaderPageLayout.js
+++ b/src/components/IllustratedHeaderPageLayout.js
@@ -11,6 +11,7 @@ import themeColors from '../styles/themes/default';
import * as StyleUtils from '../styles/StyleUtils';
import useWindowDimensions from '../hooks/useWindowDimensions';
import FixedFooter from './FixedFooter';
+import useNetwork from '../hooks/useNetwork';
const propTypes = {
...headerWithBackButtonPropTypes,
@@ -35,6 +36,7 @@ const defaultProps = {
function IllustratedHeaderPageLayout({backgroundColor, children, illustration, footer, ...propsToPassToHeader}) {
const {windowHeight} = useWindowDimensions();
+ const {isOffline} = useNetwork();
return (
-
+ {},
+ style: {},
};
-class ImageView extends PureComponent {
- constructor(props) {
- super(props);
-
- this.state = {
- isLoading: true,
- imageWidth: 0,
- imageHeight: 0,
- interactionPromise: undefined,
- };
-
- // Use the default double click interval from the ImageZoom library
- // https://github.com/ascoders/react-native-image-zoom/blob/master/src/image-zoom/image-zoom.type.ts#L79
- this.doubleClickInterval = 175;
- this.imageZoomScale = 1;
- this.lastClickTime = 0;
- this.amountOfTouches = 0;
-
- // PanResponder used to capture how many touches are active on the attachment image
- this.panResponder = PanResponder.create({
- onStartShouldSetPanResponder: this.updatePanResponderTouches.bind(this),
- });
-
- this.configureImageZoom = this.configureImageZoom.bind(this);
- this.imageLoadingStart = this.imageLoadingStart.bind(this);
- }
+// Use the default double click interval from the ImageZoom library
+// https://github.com/ascoders/react-native-image-zoom/blob/master/src/image-zoom/image-zoom.type.ts#L79
+const DOUBLE_CLICK_INTERVAL = 175;
- componentDidUpdate(prevProps) {
- if (this.props.url === prevProps.url) {
- return;
- }
+function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style}) {
+ const {windowWidth, windowHeight} = useWindowDimensions();
- this.imageLoadingStart();
+ const [isLoading, setIsLoading] = useState(true);
+ const [imageDimensions, setImageDimensions] = useState({
+ width: 0,
+ height: 0,
+ });
+ const [containerHeight, setContainerHeight] = useState(null);
- if (this.interactionPromise) {
- this.state.interactionPromise.cancel();
- }
- }
-
- componentWillUnmount() {
- if (!this.state.interactionPromise) {
- return;
- }
- this.state.interactionPromise.cancel();
- }
+ const imageZoomScale = useRef(1);
+ const lastClickTime = useRef(0);
+ const numberOfTouches = useRef(0);
+ const zoom = useRef(null);
/**
* Updates the amount of active touches on the PanResponder on our ImageZoom overlay View
@@ -86,14 +61,58 @@ class ImageView extends PureComponent {
* @param {GestureState} gestureState
* @returns {Boolean}
*/
- updatePanResponderTouches(e, gestureState) {
+ const updatePanResponderTouches = (e, gestureState) => {
if (_.isNumber(gestureState.numberActiveTouches)) {
- this.amountOfTouches = gestureState.numberActiveTouches;
+ numberOfTouches.current = gestureState.numberActiveTouches;
}
// We don't need to set the panResponder since all we care about is checking the gestureState, so return false
return false;
- }
+ };
+
+ // PanResponder used to capture how many touches are active on the attachment image
+ const panResponder = useRef(
+ PanResponder.create({
+ onStartShouldSetPanResponder: updatePanResponderTouches,
+ }),
+ ).current;
+
+ /**
+ * When the url changes and the image must load again,
+ * this resets the zoom to ensure the next image loads with the correct dimensions.
+ */
+ const resetImageZoom = () => {
+ if (imageZoomScale.current !== 1) {
+ imageZoomScale.current = 1;
+ }
+
+ if (zoom.current) {
+ zoom.current.centerOn({
+ x: 0,
+ y: 0,
+ scale: 1,
+ duration: 0,
+ });
+ }
+ };
+
+ const imageLoadingStart = () => {
+ if (isLoading) {
+ return;
+ }
+
+ resetImageZoom();
+ setImageDimensions({
+ width: 0,
+ height: 0,
+ });
+ setIsLoading(true);
+ };
+
+ useEffect(() => {
+ imageLoadingStart();
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- this effect only needs to run when the url changes
+ }, [url]);
/**
* The `ImageZoom` component requires image dimensions which
@@ -102,148 +121,126 @@ class ImageView extends PureComponent {
*
* @param {Object} nativeEvent
*/
- configureImageZoom({nativeEvent}) {
- let imageWidth = nativeEvent.width;
- let imageHeight = nativeEvent.height;
- const containerWidth = Math.round(this.props.windowWidth);
- const containerHeight = Math.round(this.state.containerHeight ? this.state.containerHeight : this.props.windowHeight);
+ const configureImageZoom = ({nativeEvent}) => {
+ let imageZoomWidth = nativeEvent.width;
+ let imageZoomHeight = nativeEvent.height;
+ const roundedContainerWidth = Math.round(windowWidth);
+ const roundedContainerHeight = Math.round(containerHeight || windowHeight);
- const aspectRatio = Math.min(containerHeight / imageHeight, containerWidth / imageWidth);
+ const aspectRatio = Math.min(roundedContainerHeight / imageZoomHeight, roundedContainerWidth / imageZoomWidth);
- imageHeight *= aspectRatio;
- imageWidth *= aspectRatio;
+ imageZoomHeight *= aspectRatio;
+ imageZoomWidth *= aspectRatio;
// Resize the image to max dimensions possible on the Native platforms to prevent crashes on Android. To keep the same behavior, apply to IOS as well.
const maxDimensionsScale = 11;
- imageWidth = Math.min(imageWidth, containerWidth * maxDimensionsScale);
- imageHeight = Math.min(imageHeight, containerHeight * maxDimensionsScale);
- this.setState({imageHeight, imageWidth, isLoading: false});
- }
+ imageZoomWidth = Math.min(imageZoomWidth, roundedContainerWidth * maxDimensionsScale);
+ imageZoomHeight = Math.min(imageZoomHeight, roundedContainerHeight * maxDimensionsScale);
- /**
- * When the url changes and the image must load again,
- * this resets the zoom to ensure the next image loads with the correct dimensions.
- */
- resetImageZoom() {
- if (this.imageZoomScale !== 1) {
- this.imageZoomScale = 1;
+ setImageDimensions({
+ height: imageZoomHeight,
+ width: imageZoomWidth,
+ });
+ setIsLoading(false);
+ };
+
+ const configurePanResponder = () => {
+ const currentTimestamp = new Date().getTime();
+ const isDoubleClick = currentTimestamp - lastClickTime.current <= DOUBLE_CLICK_INTERVAL;
+ lastClickTime.current = currentTimestamp;
+
+ // Let ImageZoom handle the event if the tap is more than one touchPoint or if we are zoomed in
+ if (numberOfTouches.current === 2 || imageZoomScale.current !== 1) {
+ return true;
}
- if (this.zoom) {
- this.zoom.centerOn({
+ // When we have a double click and the zoom scale is 1 then programmatically zoom the image
+ // but let the tap fall through to the parent so we can register a swipe down to dismiss
+ if (isDoubleClick) {
+ zoom.current.centerOn({
x: 0,
y: 0,
- scale: 1,
- duration: 0,
+ scale: 2,
+ duration: 100,
});
- }
- }
- imageLoadingStart() {
- if (this.state.isLoading) {
- return;
+ // onMove will be called after the zoom animation.
+ // So it's possible to zoom and swipe and stuck in between the images.
+ // Sending scale just when we actually trigger the animation makes this nearly impossible.
+ // you should be really fast to catch in between state updates.
+ // And this lucky case will be fixed by migration to UI thread only code
+ // with gesture handler and reanimated.
+ onScaleChanged(2);
}
- this.resetImageZoom();
- this.setState({imageHeight: 0, imageWidth: 0, isLoading: true});
- }
-
- render() {
- // Default windowHeight accounts for the modal header height
- const windowHeight = this.props.windowHeight - variables.contentHeaderHeight;
- const hasImageDimensions = this.state.imageWidth !== 0 && this.state.imageHeight !== 0;
- const shouldShowLoadingIndicator = this.state.isLoading || !hasImageDimensions;
-
- // Zoom view should be loaded only after measuring actual image dimensions, otherwise it causes blurred images on Android
- return (
- {
- const layout = event.nativeEvent.layout;
- this.setState({
- containerHeight: layout.height,
- });
- }}
- >
- {Boolean(this.state.containerHeight) && (
- (this.zoom = el)}
- onClick={() => this.props.onPress()}
- cropWidth={this.props.windowWidth}
- cropHeight={windowHeight}
- imageWidth={this.state.imageWidth}
- imageHeight={this.state.imageHeight}
- onStartShouldSetPanResponder={() => {
- const isDoubleClick = new Date().getTime() - this.lastClickTime <= this.doubleClickInterval;
- this.lastClickTime = new Date().getTime();
-
- // Let ImageZoom handle the event if the tap is more than one touchPoint or if we are zoomed in
- if (this.amountOfTouches === 2 || this.imageZoomScale !== 1) {
- return true;
- }
-
- // When we have a double click and the zoom scale is 1 then programmatically zoom the image
- // but let the tap fall through to the parent so we can register a swipe down to dismiss
- if (isDoubleClick) {
- this.zoom.centerOn({
- x: 0,
- y: 0,
- scale: 2,
- duration: 100,
- });
-
- // onMove will be called after the zoom animation.
- // So it's possible to zoom and swipe and stuck in between the images.
- // Sending scale just when we actually trigger the animation makes this nearly impossible.
- // you should be really fast to catch in between state updates.
- // And this lucky case will be fixed by migration to UI thread only code
- // with gesture handler and reanimated.
- this.props.onScaleChanged(2);
- }
-
- // We must be either swiping down or double tapping since we are at zoom scale 1
- return false;
- }}
- onMove={({scale}) => {
- this.props.onScaleChanged(scale);
- this.imageZoomScale = scale;
- }}
- >
-
- {/**
- Create an invisible view on top of the image so we can capture and set the amount of touches before
- the ImageZoom's PanResponder does. Children will be triggered first, so this needs to be inside the
- ImageZoom to work
- */}
-
-
- )}
- {shouldShowLoadingIndicator && }
-
- );
- }
+
+ // We must be either swiping down or double tapping since we are at zoom scale 1
+ return false;
+ };
+
+ // Default windowHeight accounts for the modal header height
+ const calculatedWindowHeight = windowHeight - variables.contentHeaderHeight;
+ const hasImageDimensions = imageDimensions.width !== 0 && imageDimensions.height !== 0;
+ const shouldShowLoadingIndicator = isLoading || !hasImageDimensions;
+
+ // Zoom view should be loaded only after measuring actual image dimensions, otherwise it causes blurred images on Android
+ return (
+ {
+ const layout = event.nativeEvent.layout;
+ setContainerHeight(layout.height);
+ }}
+ >
+ {Boolean(containerHeight) && (
+ {
+ onScaleChanged(scale);
+ imageZoomScale.current = scale;
+ }}
+ >
+
+ {/**
+ Create an invisible view on top of the image so we can capture and set the amount of touches before
+ the ImageZoom's PanResponder does. Children will be triggered first, so this needs to be inside the
+ ImageZoom to work
+ */}
+
+
+ )}
+ {shouldShowLoadingIndicator && }
+
+ );
}
ImageView.propTypes = propTypes;
ImageView.defaultProps = defaultProps;
+ImageView.displayName = 'ImageView';
-export default withWindowDimensions(ImageView);
+export default ImageView;
diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js
index a3fbb5e41378..fb081bc8690c 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.js
+++ b/src/components/LHNOptionsList/OptionRowLHN.js
@@ -2,9 +2,11 @@ import _ from 'underscore';
import React, {useState, useRef} from 'react';
import PropTypes from 'prop-types';
import {View, StyleSheet} from 'react-native';
+import lodashGet from 'lodash/get';
import * as optionRowStyles from '../../styles/optionRowStyles';
import styles from '../../styles/styles';
import * as StyleUtils from '../../styles/StyleUtils';
+import DateUtils from '../../libs/DateUtils';
import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
import MultipleAvatars from '../MultipleAvatars';
@@ -22,12 +24,17 @@ import * as ContextMenuActions from '../../pages/home/report/ContextMenu/Context
import * as OptionsListUtils from '../../libs/OptionsListUtils';
import * as ReportUtils from '../../libs/ReportUtils';
import useLocalize from '../../hooks/useLocalize';
+import Permissions from '../../libs/Permissions';
+import Tooltip from '../Tooltip';
const propTypes = {
/** Style for hovered state */
// eslint-disable-next-line react/forbid-prop-types
hoverStyle: PropTypes.object,
+ /** List of betas available to current user */
+ betas: PropTypes.arrayOf(PropTypes.string),
+
/** The ID of the report that the option is for */
reportID: PropTypes.string.isRequired,
@@ -54,6 +61,7 @@ const defaultProps = {
style: null,
optionItem: null,
isFocused: false,
+ betas: [],
};
function OptionRowLHN(props) {
@@ -68,8 +76,8 @@ function OptionRowLHN(props) {
return null;
}
- const isMuted = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE;
- if (isMuted && !props.isFocused && !optionItem.isPinned) {
+ const isHidden = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
+ if (isHidden && !props.isFocused && !optionItem.isPinned) {
return null;
}
@@ -124,6 +132,13 @@ function OptionRowLHN(props) {
);
};
+ const emojiCode = lodashGet(optionItem, 'status.emojiCode', '');
+ const statusText = lodashGet(optionItem, 'status.text', '');
+ const statusClearAfterDate = lodashGet(optionItem, 'status.clearAfter', '');
+ const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate);
+ const statusContent = formattedDate ? `${statusText} (${formattedDate})` : statusText;
+ const isStatusVisible = Permissions.canUseCustomStatus(props.betas) && !!emojiCode && ReportUtils.isOneOnOneChat(optionItem);
+
return (
+ {isStatusVisible && (
+
+ {emojiCode}
+
+ )}
{optionItem.alternateText ? (
login: personalData.login,
displayName: personalData.displayName,
firstName: personalData.firstName,
+ status: personalData.status,
avatar: UserUtils.getAvatar(personalData.avatar, personalData.accountID),
};
return finalPersonalDetails;
diff --git a/src/components/MentionSuggestions.js b/src/components/MentionSuggestions.js
index 1480d98d7899..4b0129635269 100644
--- a/src/components/MentionSuggestions.js
+++ b/src/components/MentionSuggestions.js
@@ -42,10 +42,14 @@ const propTypes = {
/** Show that we should include ReportRecipientLocalTime view height */
shouldIncludeReportRecipientLocalTimeHeight: PropTypes.bool.isRequired,
+
+ /** Meaures the parent container's position and dimensions. */
+ measureParentContainer: PropTypes.func,
};
const defaultProps = {
highlightedMentionIndex: 0,
+ measureParentContainer: () => {},
};
/**
@@ -122,6 +126,7 @@ function MentionSuggestions(props) {
isSuggestionPickerLarge={props.isMentionPickerLarge}
shouldIncludeReportRecipientLocalTimeHeight={props.shouldIncludeReportRecipientLocalTimeHeight}
accessibilityLabelExtractor={keyExtractor}
+ measureParentContainer={props.measureParentContainer}
/>
);
}
diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js
index 06778266d113..ab9d420f949c 100644
--- a/src/components/Modal/BaseModal.js
+++ b/src/components/Modal/BaseModal.js
@@ -1,15 +1,17 @@
-import React, {PureComponent} from 'react';
+import React, {forwardRef, useCallback, useEffect, useMemo} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import ReactNativeModal from 'react-native-modal';
-import {SafeAreaInsetsContext} from 'react-native-safe-area-context';
+import {useSafeAreaInsets} from 'react-native-safe-area-context';
import styles from '../../styles/styles';
+import * as Modal from '../../libs/actions/Modal';
import * as StyleUtils from '../../styles/StyleUtils';
import themeColors from '../../styles/themes/default';
import {propTypes as modalPropTypes, defaultProps as modalDefaultProps} from './modalPropTypes';
-import * as Modal from '../../libs/actions/Modal';
import getModalStyles from '../../styles/getModalStyles';
+import useWindowDimensions from '../../hooks/useWindowDimensions';
import variables from '../../styles/variables';
+import CONST from '../../CONST';
import ComposerFocusManager from '../../libs/ComposerFocusManager';
const propTypes = {
@@ -24,173 +26,190 @@ const defaultProps = {
forwardedRef: () => {},
};
-class BaseModal extends PureComponent {
- constructor(props) {
- super(props);
-
- this.hideModal = this.hideModal.bind(this);
- }
-
- componentDidMount() {
- if (!this.props.isVisible) {
- return;
- }
-
- Modal.willAlertModalBecomeVisible(true);
-
- // To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu
- Modal.setCloseModal(this.props.onClose);
- }
-
- componentDidUpdate(prevProps) {
- if (prevProps.isVisible === this.props.isVisible) {
- return;
- }
-
- Modal.willAlertModalBecomeVisible(this.props.isVisible);
- Modal.setCloseModal(this.props.isVisible ? this.props.onClose : null);
- }
-
- componentWillUnmount() {
- // Only trigger onClose and setModalVisibility if the modal is unmounting while visible.
- if (this.props.isVisible) {
- this.hideModal(true);
- Modal.willAlertModalBecomeVisible(false);
- }
-
- // To prevent closing any modal already unmounted when this modal still remains as visible state
- Modal.setCloseModal(null);
- }
+function BaseModal({
+ isVisible,
+ onClose,
+ shouldSetModalVisibility,
+ onModalHide,
+ type,
+ popoverAnchorPosition,
+ innerContainerStyle,
+ outerStyle,
+ onModalShow,
+ propagateSwipe,
+ fullscreen,
+ animationIn,
+ animationOut,
+ useNativeDriver,
+ hideModalContentWhileAnimating,
+ animationInTiming,
+ animationOutTiming,
+ statusBarTranslucent,
+ onLayout,
+ avoidKeyboard,
+ forwardedRef,
+ children,
+}) {
+ const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions();
+
+ const safeAreaInsets = useSafeAreaInsets();
/**
* Hides modal
* @param {Boolean} [callHideCallback=true] Should we call the onModalHide callback
*/
- hideModal(callHideCallback = true) {
- if (this.props.shouldSetModalVisibility) {
- Modal.setModalVisibility(false);
- }
- if (callHideCallback) {
- this.props.onModalHide();
+ const hideModal = useCallback(
+ (callHideCallback = true) => {
+ if (shouldSetModalVisibility) {
+ Modal.setModalVisibility(false);
+ }
+ if (callHideCallback) {
+ onModalHide();
+ }
+ Modal.onModalDidClose();
+ if (!fullscreen) {
+ ComposerFocusManager.setReadyToFocus();
+ }
+ },
+ [shouldSetModalVisibility, onModalHide, fullscreen],
+ );
+
+ useEffect(() => {
+ Modal.willAlertModalBecomeVisible(isVisible);
+
+ // To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu
+ Modal.setCloseModal(isVisible ? onClose : null);
+ }, [isVisible, onClose]);
+
+ useEffect(
+ () => () => {
+ // Only trigger onClose and setModalVisibility if the modal is unmounting while visible.
+ if (isVisible) {
+ hideModal(true);
+ Modal.willAlertModalBecomeVisible(false);
+ }
+
+ // To prevent closing any modal already unmounted when this modal still remains as visible state
+ Modal.setCloseModal(null);
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+
+ const handleShowModal = () => {
+ if (shouldSetModalVisibility) {
+ Modal.setModalVisibility(true);
}
- Modal.onModalDidClose();
- if (!this.props.fullscreen) {
- ComposerFocusManager.setReadyToFocus();
+ onModalShow();
+ };
+
+ const handleBackdropPress = (e) => {
+ if (e && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) {
+ return;
}
- }
-
- render() {
- const {
- modalStyle,
- modalContainerStyle,
- swipeDirection,
- animationIn,
- animationOut,
- shouldAddTopSafeAreaMargin,
- shouldAddBottomSafeAreaMargin,
- shouldAddTopSafeAreaPadding,
- shouldAddBottomSafeAreaPadding,
- hideBackdrop,
- } = getModalStyles(
- this.props.type,
- {
- windowWidth: this.props.windowWidth,
- windowHeight: this.props.windowHeight,
- isSmallScreenWidth: this.props.isSmallScreenWidth,
- },
- this.props.popoverAnchorPosition,
- this.props.innerContainerStyle,
- this.props.outerStyle,
- );
- return (
- {
- if (e && e.key === 'Enter') {
- return;
- }
- this.props.onClose();
- }}
- // Note: Escape key on web/desktop will trigger onBackButtonPress callback
- // eslint-disable-next-line react/jsx-props-no-multi-spaces
- onBackButtonPress={this.props.onClose}
- onModalWillShow={() => {
- ComposerFocusManager.resetReadyToFocus();
- }}
- onModalShow={() => {
- if (this.props.shouldSetModalVisibility) {
- Modal.setModalVisibility(true);
- }
- this.props.onModalShow();
- }}
- propagateSwipe={this.props.propagateSwipe}
- onModalHide={this.hideModal}
- onDismiss={() => ComposerFocusManager.setReadyToFocus()}
- onSwipeComplete={this.props.onClose}
- swipeDirection={swipeDirection}
- isVisible={this.props.isVisible}
- backdropColor={themeColors.overlay}
- backdropOpacity={hideBackdrop ? 0 : variables.overlayOpacity}
- backdropTransitionOutTiming={0}
- hasBackdrop={this.props.fullscreen}
- coverScreen={this.props.fullscreen}
- style={modalStyle}
- deviceHeight={this.props.windowHeight}
- deviceWidth={this.props.windowWidth}
- animationIn={this.props.animationIn || animationIn}
- animationOut={this.props.animationOut || animationOut}
- useNativeDriver={this.props.useNativeDriver}
- hideModalContentWhileAnimating={this.props.hideModalContentWhileAnimating}
- animationInTiming={this.props.animationInTiming}
- animationOutTiming={this.props.animationOutTiming}
- statusBarTranslucent={this.props.statusBarTranslucent}
- onLayout={this.props.onLayout}
- avoidKeyboard={this.props.avoidKeyboard}
+ onClose();
+ };
+
+ const handleDismissModal = () => {
+ ComposerFocusManager.setReadyToFocus();
+ };
+
+ const {
+ modalStyle,
+ modalContainerStyle,
+ swipeDirection,
+ animationIn: modalStyleAnimationIn,
+ animationOut: modalStyleAnimationOut,
+ shouldAddTopSafeAreaMargin,
+ shouldAddBottomSafeAreaMargin,
+ shouldAddTopSafeAreaPadding,
+ shouldAddBottomSafeAreaPadding,
+ hideBackdrop,
+ } = useMemo(
+ () =>
+ getModalStyles(
+ type,
+ {
+ windowWidth,
+ windowHeight,
+ isSmallScreenWidth,
+ },
+ popoverAnchorPosition,
+ innerContainerStyle,
+ outerStyle,
+ ),
+ [innerContainerStyle, isSmallScreenWidth, outerStyle, popoverAnchorPosition, type, windowHeight, windowWidth],
+ );
+
+ const {
+ paddingTop: safeAreaPaddingTop,
+ paddingBottom: safeAreaPaddingBottom,
+ paddingLeft: safeAreaPaddingLeft,
+ paddingRight: safeAreaPaddingRight,
+ } = StyleUtils.getSafeAreaPadding(safeAreaInsets);
+
+ const modalPaddingStyles = StyleUtils.getModalPaddingStyles({
+ safeAreaPaddingTop,
+ safeAreaPaddingBottom,
+ safeAreaPaddingLeft,
+ safeAreaPaddingRight,
+ shouldAddBottomSafeAreaMargin,
+ shouldAddTopSafeAreaMargin,
+ shouldAddBottomSafeAreaPadding,
+ shouldAddTopSafeAreaPadding,
+ modalContainerStyleMarginTop: modalContainerStyle.marginTop,
+ modalContainerStyleMarginBottom: modalContainerStyle.marginBottom,
+ modalContainerStylePaddingTop: modalContainerStyle.paddingTop,
+ modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom,
+ insets: safeAreaInsets,
+ });
+
+ return (
+
+
-
- {(insets) => {
- const {
- paddingTop: safeAreaPaddingTop,
- paddingBottom: safeAreaPaddingBottom,
- paddingLeft: safeAreaPaddingLeft,
- paddingRight: safeAreaPaddingRight,
- } = StyleUtils.getSafeAreaPadding(insets);
-
- const modalPaddingStyles = StyleUtils.getModalPaddingStyles({
- safeAreaPaddingTop,
- safeAreaPaddingBottom,
- safeAreaPaddingLeft,
- safeAreaPaddingRight,
- shouldAddBottomSafeAreaMargin,
- shouldAddTopSafeAreaMargin,
- shouldAddBottomSafeAreaPadding,
- shouldAddTopSafeAreaPadding,
- modalContainerStyleMarginTop: modalContainerStyle.marginTop,
- modalContainerStyleMarginBottom: modalContainerStyle.marginBottom,
- modalContainerStylePaddingTop: modalContainerStyle.paddingTop,
- modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom,
- insets,
- });
-
- return (
-
- {this.props.children}
-
- );
- }}
-
-
- );
- }
+ {children}
+
+
+ );
}
BaseModal.propTypes = propTypes;
BaseModal.defaultProps = defaultProps;
+BaseModal.displayName = 'BaseModal';
-export default React.forwardRef((props, ref) => (
+export default forwardRef((props, ref) => (
{
if (option.accountID) {
- Navigation.navigate(ROUTES.getProfileRoute(option.accountID));
+ const activeRoute = Navigation.getActiveRoute().replace(/\?.*/, '');
+
+ Navigation.navigate(ROUTES.getProfileRoute(option.accountID, activeRoute));
} else if (option.reportID) {
Navigation.navigate(ROUTES.getReportDetailsRoute(option.reportID));
}
@@ -369,7 +371,7 @@ function MoneyRequestConfirmationList(props) {
disabled={didConfirm || props.isReadOnly}
/>
{!showAllFields && (
-
+
);
})}
-
+ {!props.shouldBlockReactions && (
+
+ )}
)
);
diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js
index c8a18da2ac03..5e5ee4829786 100644
--- a/src/components/ReportActionItem/MoneyRequestAction.js
+++ b/src/components/ReportActionItem/MoneyRequestAction.js
@@ -17,12 +17,12 @@ import styles from '../../styles/styles';
import * as IOUUtils from '../../libs/IOUUtils';
import * as ReportUtils from '../../libs/ReportUtils';
import * as Report from '../../libs/actions/Report';
-import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
import refPropTypes from '../refPropTypes';
import RenderHTML from '../RenderHTML';
import * as PersonalDetailsUtils from '../../libs/PersonalDetailsUtils';
import reportPropTypes from '../../pages/reportPropTypes';
+import useLocalize from '../../hooks/useLocalize';
const propTypes = {
/** All the data of the action */
@@ -58,17 +58,9 @@ const propTypes = {
network: networkPropTypes.isRequired,
- /** Session info for the currently logged in user. */
- session: PropTypes.shape({
- /** Currently logged in user email */
- email: PropTypes.string,
- }),
-
/** Styles to be assigned to Container */
// eslint-disable-next-line react/forbid-prop-types
style: PropTypes.arrayOf(PropTypes.object),
-
- ...withLocalizePropTypes,
};
const defaultProps = {
@@ -78,77 +70,73 @@ const defaultProps = {
iouReport: {},
reportActions: {},
isHovered: false,
- session: {
- email: null,
- },
style: [],
};
-function MoneyRequestAction(props) {
- const isSplitBillAction = lodashGet(props.action, 'originalMessage.type', '') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT;
+function MoneyRequestAction({
+ action,
+ chatReportID,
+ requestReportID,
+ isMostRecentIOUReportAction,
+ contextMenuAnchor,
+ checkIfContextMenuActive,
+ chatReport,
+ iouReport,
+ reportActions,
+ isHovered,
+ network,
+ style,
+}) {
+ const {translate} = useLocalize();
+ const isSplitBillAction = lodashGet(action, 'originalMessage.type', '') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT;
const onMoneyRequestPreviewPressed = () => {
if (isSplitBillAction) {
- const reportActionID = lodashGet(props.action, 'reportActionID', '0');
- Navigation.navigate(ROUTES.getSplitBillDetailsRoute(props.chatReportID, reportActionID));
+ const reportActionID = lodashGet(action, 'reportActionID', '0');
+ Navigation.navigate(ROUTES.getSplitBillDetailsRoute(chatReportID, reportActionID));
return;
}
// If the childReportID is not present, we need to create a new thread
- const childReportID = lodashGet(props.action, 'childReportID', '0');
- if (childReportID === '0') {
- const participantAccountIDs = _.uniq([props.session.accountID, Number(props.action.actorAccountID)]);
- const thread = ReportUtils.buildOptimisticChatReport(
- participantAccountIDs,
- ReportUtils.getTransactionReportName(props.action),
- '',
- lodashGet(props.iouReport, 'policyID', CONST.POLICY.OWNER_EMAIL_FAKE),
- CONST.POLICY.OWNER_ACCOUNT_ID_FAKE,
- false,
- '',
- undefined,
- undefined,
- CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
- props.action.reportActionID,
- props.requestReportID,
- );
-
+ const childReportID = lodashGet(action, 'childReportID', 0);
+ if (!childReportID) {
+ const thread = ReportUtils.buildTransactionThread(action);
const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs);
- Report.openReport(thread.reportID, userLogins, thread, props.action.reportActionID);
+ Report.openReport(thread.reportID, userLogins, thread, action.reportActionID);
Navigation.navigate(ROUTES.getReportRoute(thread.reportID));
- } else {
- Report.openReport(childReportID);
- Navigation.navigate(ROUTES.getReportRoute(childReportID));
+ return;
}
+ Report.openReport(childReportID);
+ Navigation.navigate(ROUTES.getReportRoute(childReportID));
};
let shouldShowPendingConversionMessage = false;
- const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(props.action);
+ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action);
if (
- !_.isEmpty(props.iouReport) &&
- !_.isEmpty(props.reportActions) &&
- props.chatReport.hasOutstandingIOU &&
- props.isMostRecentIOUReportAction &&
- props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD &&
- props.network.isOffline
+ !_.isEmpty(iouReport) &&
+ !_.isEmpty(reportActions) &&
+ chatReport.hasOutstandingIOU &&
+ isMostRecentIOUReportAction &&
+ action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD &&
+ network.isOffline
) {
- shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(props.iouReport);
+ shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport);
}
return isDeletedParentAction ? (
- ${props.translate('parentReportAction.deletedRequest')}`} />
+ ${translate('parentReportAction.deletedRequest')}`} />
) : (
);
}
@@ -158,7 +146,6 @@ MoneyRequestAction.defaultProps = defaultProps;
MoneyRequestAction.displayName = 'MoneyRequestAction';
export default compose(
- withLocalize,
withOnyx({
chatReport: {
key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`,
@@ -170,9 +157,6 @@ export default compose(
key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`,
canEvict: false,
},
- session: {
- key: ONYXKEYS.SESSION,
- },
}),
withNetwork(),
)(MoneyRequestAction);
diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js
index a22eaf65412e..306adf6bdc39 100644
--- a/src/components/ReportActionItem/MoneyRequestView.js
+++ b/src/components/ReportActionItem/MoneyRequestView.js
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import reportPropTypes from '../../pages/reportPropTypes';
import ONYXKEYS from '../../ONYXKEYS';
import ROUTES from '../../ROUTES';
-import * as Policy from '../../libs/actions/Policy';
import Navigation from '../../libs/Navigation/Navigation';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '../withCurrentUserPersonalDetails';
import compose from '../../libs/compose';
@@ -36,21 +35,6 @@ const propTypes = {
/** The expense report or iou report (only will have a value if this is a transaction thread) */
parentReport: iouReportPropTypes,
- /** The policy object for the current route */
- policy: PropTypes.shape({
- /** The name of the policy */
- name: PropTypes.string,
-
- /** The URL for the policy avatar */
- avatar: PropTypes.string,
- }),
-
- /** Session info for the currently logged in user. */
- session: PropTypes.shape({
- /** Currently logged in user email */
- email: PropTypes.string,
- }),
-
/** The transaction associated with the transactionThread */
transaction: transactionPropTypes,
@@ -62,10 +46,6 @@ const propTypes = {
const defaultProps = {
parentReport: {},
- policy: null,
- session: {
- email: null,
- },
transaction: {
amount: 0,
currency: CONST.CURRENCY.USD,
@@ -73,7 +53,7 @@ const defaultProps = {
},
};
-function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, policy, session, transaction}) {
+function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, transaction}) {
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
@@ -89,9 +69,7 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, polic
const formattedTransactionAmount = transactionAmount && transactionCurrency && CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency);
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
- const isAdmin = Policy.isAdminOfFreePolicy([policy]) && ReportUtils.isExpenseReport(moneyRequestReport);
- const isRequestor = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === parentReportAction.actorAccountID;
- const canEdit = !isSettled && (isAdmin || isRequestor);
+ const canEdit = ReportUtils.canEditMoneyRequest(parentReportAction);
let description = `${translate('iou.amount')} • ${translate('iou.cash')}`;
if (isSettled) {
diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js
index c7244093396e..046b64e9e5c0 100644
--- a/src/components/SelectionList/BaseSelectionList.js
+++ b/src/components/SelectionList/BaseSelectionList.js
@@ -22,6 +22,7 @@ import Button from '../Button';
import useLocalize from '../../hooks/useLocalize';
import Log from '../../libs/Log';
import OptionsListSkeletonView from '../OptionsListSkeletonView';
+import useActiveElement from '../../hooks/useActiveElement';
const propTypes = {
...keyboardStatePropTypes,
@@ -49,6 +50,7 @@ function BaseSelectionList({
onConfirm,
showScrollIndicator = false,
showLoadingPlaceholder = false,
+ showConfirmButton = false,
isKeyboardShown = false,
}) {
const {translate} = useLocalize();
@@ -58,7 +60,7 @@ function BaseSelectionList({
const focusTimeoutRef = useRef(null);
const shouldShowTextInput = Boolean(textInputLabel);
const shouldShowSelectAll = Boolean(onSelectAll);
- const shouldShowConfirmButton = Boolean(onConfirm);
+ const activeElement = useActiveElement();
/**
* Iterates through the sections and items inside each section, and builds 3 arrays along the way:
@@ -133,17 +135,8 @@ function BaseSelectionList({
};
}, [canSelectMultiple, sections]);
- const [focusedIndex, setFocusedIndex] = useState(() => {
- const defaultIndex = 0;
-
- const indexOfInitiallyFocusedOption = _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey);
-
- if (indexOfInitiallyFocusedOption >= 0) {
- return indexOfInitiallyFocusedOption;
- }
-
- return defaultIndex;
- });
+ // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member
+ const [focusedIndex, setFocusedIndex] = useState(() => _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey));
/**
* Scrolls to the desired item index in the section list
@@ -171,7 +164,7 @@ function BaseSelectionList({
}
}
- listRef.current.scrollToLocation({sectionIndex: adjustedSectionIndex, itemIndex, animated});
+ listRef.current.scrollToLocation({sectionIndex: adjustedSectionIndex, itemIndex, animated, viewOffset: variables.contentHeaderHeight});
};
const selectRow = (item, index) => {
@@ -185,6 +178,11 @@ function BaseSelectionList({
// If the list has multiple sections (e.g. Workspace Invite list), we focus the first one after all the selected (selected items are always at the top)
const selectedOptionsCount = item.isSelected ? flattenedSections.selectedOptions.length - 1 : flattenedSections.selectedOptions.length + 1;
setFocusedIndex(selectedOptionsCount);
+
+ if (!item.isSelected) {
+ // If we're selecting an item, scroll to it's position at the top, so we can see it
+ scrollToIndex(Math.max(selectedOptionsCount - 1, 0), true);
+ }
}
}
@@ -286,10 +284,18 @@ function BaseSelectionList({
};
}, [shouldDelayFocus, shouldShowTextInput]);
- /** Selects row when pressing enter */
+ /** Selects row when pressing Enter */
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, {
captureOnInputs: true,
shouldBubble: () => !flattenedSections.allOptions[focusedIndex],
+ isActive: !activeElement,
+ });
+
+ /** Calls confirm action when pressing CTRL (CMD) + Enter */
+ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, {
+ captureOnInputs: true,
+ shouldBubble: () => !flattenedSections.allOptions[focusedIndex],
+ isActive: Boolean(onConfirm),
});
return (
@@ -339,11 +345,13 @@ function BaseSelectionList({
accessibilityLabel={translate('workspace.people.selectAll')}
accessibilityRole="button"
accessibilityState={{checked: flattenedSections.allSelected}}
+ disabled={flattenedSections.allOptions.length === flattenedSections.disabledOptionsIndexes.length}
>
{translate('workspace.people.selectAll')}
@@ -378,7 +386,7 @@ function BaseSelectionList({
/>
>
)}
- {shouldShowConfirmButton && (
+ {showConfirmButton && (
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js
index 2d8bd5f35655..e2c50b288fe5 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.js
+++ b/src/pages/home/report/ReportActionItemMessageEdit.js
@@ -131,7 +131,7 @@ function ReportActionItemMessageEdit(props) {
return () => {
// Skip if this is not the focused message so the other edit composer stays focused.
// In small screen devices, when EmojiPicker is shown, the current edit message will lose focus, we need to check this case as well.
- if (!isFocusedRef.current && !EmojiPickerAction.isActiveReportAction(props.action.reportActionID)) {
+ if (!isFocusedRef.current && !EmojiPickerAction.isActive(props.action.reportActionID)) {
return;
}
@@ -382,7 +382,7 @@ function ReportActionItemMessageEdit(props) {
}}
onEmojiSelected={addEmojiToTextBox}
nativeID={emojiButtonID}
- reportAction={props.action}
+ emojiPickerID={props.action.reportActionID}
/>
diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js
index 2af66779309e..1d89d76e960f 100644
--- a/src/pages/home/report/ReportActionItemParentAction.js
+++ b/src/pages/home/report/ReportActionItemParentAction.js
@@ -53,6 +53,7 @@ function ReportActionItemParentAction(props) {
}
return (
!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(props.action.delegateAccountID ? props.action.delegateAccountID : actorAccountID),
diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js
index 343f2fb87333..8d92c09b7a6e 100644
--- a/src/pages/home/report/ReportFooter.js
+++ b/src/pages/home/report/ReportFooter.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import {View, Keyboard} from 'react-native';
import CONST from '../../../CONST';
-import ReportActionCompose from './ReportActionCompose';
+import ReportActionCompose from './ReportActionCompose/ReportActionCompose';
import AnonymousReportFooter from '../../../components/AnonymousReportFooter';
import SwipeableView from '../../../components/SwipeableView';
import OfflineIndicator from '../../../components/OfflineIndicator';
diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.js b/src/pages/home/sidebar/AvatarWithOptionalStatus.js
new file mode 100644
index 000000000000..adf9cdda5cd0
--- /dev/null
+++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.js
@@ -0,0 +1,65 @@
+/* eslint-disable rulesdir/onyx-props-must-have-default */
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import PressableWithoutFeedback from '../../../components/Pressable/PressableWithoutFeedback';
+import Text from '../../../components/Text';
+import PressableAvatarWithIndicator from './PressableAvatarWithIndicator';
+import Navigation from '../../../libs/Navigation/Navigation';
+import useLocalize from '../../../hooks/useLocalize';
+import styles from '../../../styles/styles';
+import ROUTES from '../../../ROUTES';
+import CONST from '../../../CONST';
+
+const propTypes = {
+ /** Whether the create menu is open or not */
+ isCreateMenuOpen: PropTypes.bool,
+
+ /** Emoji status */
+ emojiStatus: PropTypes.string,
+};
+
+const defaultProps = {
+ isCreateMenuOpen: false,
+ emojiStatus: '',
+};
+
+function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) {
+ const {translate} = useLocalize();
+
+ const showStatusPage = useCallback(() => {
+ if (isCreateMenuOpen) {
+ // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon
+ return;
+ }
+
+ Navigation.setShouldPopAllStateOnUP();
+ Navigation.navigate(ROUTES.SETTINGS_STATUS);
+ }, [isCreateMenuOpen]);
+
+ return (
+
+
+
+
+ {emojiStatus}
+
+
+
+
+
+ );
+}
+
+AvatarWithOptionalStatus.propTypes = propTypes;
+AvatarWithOptionalStatus.defaultProps = defaultProps;
+AvatarWithOptionalStatus.displayName = 'AvatarWithOptionalStatus';
+export default AvatarWithOptionalStatus;
diff --git a/src/pages/home/sidebar/PressableAvatarWithIndicator.js b/src/pages/home/sidebar/PressableAvatarWithIndicator.js
new file mode 100644
index 000000000000..ef6e663ce705
--- /dev/null
+++ b/src/pages/home/sidebar/PressableAvatarWithIndicator.js
@@ -0,0 +1,64 @@
+/* eslint-disable rulesdir/onyx-props-must-have-default */
+import lodashGet from 'lodash/get';
+import React, {useCallback} from 'react';
+import PropTypes from 'prop-types';
+import withCurrentUserPersonalDetails from '../../../components/withCurrentUserPersonalDetails';
+import PressableWithoutFeedback from '../../../components/Pressable/PressableWithoutFeedback';
+import AvatarWithIndicator from '../../../components/AvatarWithIndicator';
+import OfflineWithFeedback from '../../../components/OfflineWithFeedback';
+import Navigation from '../../../libs/Navigation/Navigation';
+import * as UserUtils from '../../../libs/UserUtils';
+import useLocalize from '../../../hooks/useLocalize';
+import ROUTES from '../../../ROUTES';
+import CONST from '../../../CONST';
+import personalDetailsPropType from '../../personalDetailsPropType';
+
+const propTypes = {
+ /** Whether the create menu is open or not */
+ isCreateMenuOpen: PropTypes.bool,
+
+ /** The personal details of the person who is logged in */
+ currentUserPersonalDetails: personalDetailsPropType,
+};
+
+const defaultProps = {
+ isCreateMenuOpen: false,
+ currentUserPersonalDetails: {
+ pendingFields: {avatar: ''},
+ accountID: '',
+ avatar: '',
+ },
+};
+
+function PressableAvatarWithIndicator({isCreateMenuOpen, currentUserPersonalDetails}) {
+ const {translate} = useLocalize();
+
+ const showSettingsPage = useCallback(() => {
+ if (isCreateMenuOpen) {
+ // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon
+ return;
+ }
+
+ Navigation.navigate(ROUTES.SETTINGS);
+ }, [isCreateMenuOpen]);
+
+ return (
+
+
+
+
+
+ );
+}
+
+PressableAvatarWithIndicator.propTypes = propTypes;
+PressableAvatarWithIndicator.defaultProps = defaultProps;
+PressableAvatarWithIndicator.displayName = 'PressableAvatarWithIndicator';
+export default withCurrentUserPersonalDetails(PressableAvatarWithIndicator);
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index af858c15cdc6..b81bd5df647a 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -14,16 +14,13 @@ import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';
import Icon from '../../../components/Icon';
import * as Expensicons from '../../../components/Icon/Expensicons';
-import AvatarWithIndicator from '../../../components/AvatarWithIndicator';
import Tooltip from '../../../components/Tooltip';
import CONST from '../../../CONST';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import * as App from '../../../libs/actions/App';
-import withCurrentUserPersonalDetails from '../../../components/withCurrentUserPersonalDetails';
import withWindowDimensions from '../../../components/withWindowDimensions';
import LHNOptionsList from '../../../components/LHNOptionsList/LHNOptionsList';
import SidebarUtils from '../../../libs/SidebarUtils';
-import OfflineWithFeedback from '../../../components/OfflineWithFeedback';
import Header from '../../../components/Header';
import defaultTheme from '../../../styles/themes/default';
import OptionsListSkeletonView from '../../../components/OptionsListSkeletonView';
@@ -31,14 +28,12 @@ import variables from '../../../styles/variables';
import LogoComponent from '../../../../assets/images/expensify-wordmark.svg';
import PressableWithoutFeedback from '../../../components/Pressable/PressableWithoutFeedback';
import * as Session from '../../../libs/actions/Session';
-import Button from '../../../components/Button';
-import * as UserUtils from '../../../libs/UserUtils';
import KeyboardShortcut from '../../../libs/KeyboardShortcut';
import onyxSubscribe from '../../../libs/onyxSubscribe';
-import personalDetailsPropType from '../../personalDetailsPropType';
import * as ReportActionContextMenu from '../report/ContextMenu/ReportActionContextMenu';
import withCurrentReportID from '../../../components/withCurrentReportID';
import OptionRowLHNData from '../../../components/LHNOptionsList/OptionRowLHNData';
+import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus';
const basePropTypes = {
/** Toggles the navigation menu open and closed */
@@ -58,8 +53,6 @@ const propTypes = {
isLoading: PropTypes.bool.isRequired,
- currentUserPersonalDetails: personalDetailsPropType,
-
priorityMode: PropTypes.oneOf(_.values(CONST.PRIORITY_MODE)),
/** The top most report id */
@@ -75,9 +68,6 @@ const propTypes = {
};
const defaultProps = {
- currentUserPersonalDetails: {
- avatar: '',
- },
priorityMode: CONST.PRIORITY_MODE.DEFAULT,
currentReportID: '',
report: {},
@@ -88,7 +78,6 @@ class SidebarLinks extends React.PureComponent {
super(props);
this.showSearchPage = this.showSearchPage.bind(this);
- this.showSettingsPage = this.showSettingsPage.bind(this);
this.showReportPage = this.showReportPage.bind(this);
if (this.props.isSmallScreenWidth) {
@@ -147,15 +136,6 @@ class SidebarLinks extends React.PureComponent {
Navigation.navigate(ROUTES.SEARCH);
}
- showSettingsPage() {
- if (this.props.isCreateMenuOpen) {
- // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon
- return;
- }
-
- Navigation.navigate(ROUTES.SETTINGS);
- }
-
/**
* Show Report page with selected report id
*
@@ -204,29 +184,7 @@ class SidebarLinks extends React.PureComponent {
-
- {Session.isAnonymousUser() ? (
-
-
- ) : (
-
-
-
- )}
-
+
{this.props.isLoading ? (
<>
@@ -258,7 +216,6 @@ SidebarLinks.propTypes = propTypes;
SidebarLinks.defaultProps = defaultProps;
export default compose(
withLocalize,
- withCurrentUserPersonalDetails,
withWindowDimensions,
withCurrentReportID,
withOnyx({
diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
index 698a72a205cc..3d54306b6248 100644
--- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
+++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
@@ -3,8 +3,6 @@ import {View} from 'react-native';
import styles from '../../../../styles/styles';
import SidebarLinksData from '../SidebarLinksData';
import ScreenWrapper from '../../../../components/ScreenWrapper';
-import Navigation from '../../../../libs/Navigation/Navigation';
-import ROUTES from '../../../../ROUTES';
import Timing from '../../../../libs/actions/Timing';
import CONST from '../../../../CONST';
import Performance from '../../../../libs/Performance';
@@ -17,13 +15,6 @@ const propTypes = {
...windowDimensionsPropTypes,
};
-/**
- * Function called when avatar is clicked
- */
-const navigateToSettings = () => {
- Navigation.navigate(ROUTES.SETTINGS);
-};
-
/**
* Function called when a pinned chat is selected.
*/
@@ -50,7 +41,6 @@ function BaseSidebarScreen(props) {
diff --git a/src/pages/home/sidebar/SignInButton.js b/src/pages/home/sidebar/SignInButton.js
new file mode 100644
index 000000000000..24b90d778445
--- /dev/null
+++ b/src/pages/home/sidebar/SignInButton.js
@@ -0,0 +1,33 @@
+/* eslint-disable rulesdir/onyx-props-must-have-default */
+import React from 'react';
+import {View} from 'react-native';
+import PressableWithoutFeedback from '../../../components/Pressable/PressableWithoutFeedback';
+import Button from '../../../components/Button';
+import styles from '../../../styles/styles';
+import * as Session from '../../../libs/actions/Session';
+import useLocalize from '../../../hooks/useLocalize';
+import CONST from '../../../CONST';
+
+function SignInButton() {
+ const {translate} = useLocalize();
+
+ return (
+
+
+
+
+
+ );
+}
+
+SignInButton.displayName = 'SignInButton';
+export default SignInButton;
diff --git a/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.js b/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.js
new file mode 100644
index 000000000000..2599ac6b6942
--- /dev/null
+++ b/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.js
@@ -0,0 +1,63 @@
+/* eslint-disable rulesdir/onyx-props-must-have-default */
+import React from 'react';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
+import lodashGet from 'lodash/get';
+import withCurrentUserPersonalDetails from '../../../components/withCurrentUserPersonalDetails';
+import personalDetailsPropType from '../../personalDetailsPropType';
+import PressableAvatarWithIndicator from './PressableAvatarWithIndicator';
+import AvatarWithOptionalStatus from './AvatarWithOptionalStatus';
+import SignInButton from './SignInButton';
+import * as Session from '../../../libs/actions/Session';
+import Permissions from '../../../libs/Permissions';
+import compose from '../../../libs/compose';
+import ONYXKEYS from '../../../ONYXKEYS';
+
+const propTypes = {
+ /** The personal details of the person who is logged in */
+ currentUserPersonalDetails: personalDetailsPropType,
+
+ /** Whether the create menu is open or not */
+ isCreateMenuOpen: PropTypes.bool,
+
+ /** Beta features list */
+ betas: PropTypes.arrayOf(PropTypes.string),
+};
+
+const defaultProps = {
+ betas: [],
+ isCreateMenuOpen: false,
+ currentUserPersonalDetails: {
+ status: {emojiCode: ''},
+ },
+};
+
+function SignInOrAvatarWithOptionalStatus({currentUserPersonalDetails, isCreateMenuOpen, betas}) {
+ const statusEmojiCode = lodashGet(currentUserPersonalDetails, 'status.emojiCode', '');
+ const emojiStatus = Permissions.canUseCustomStatus(betas) ? statusEmojiCode : '';
+
+ if (Session.isAnonymousUser()) {
+ return ;
+ }
+ if (emojiStatus) {
+ return (
+
+ );
+ }
+ return ;
+}
+
+SignInOrAvatarWithOptionalStatus.propTypes = propTypes;
+SignInOrAvatarWithOptionalStatus.defaultProps = defaultProps;
+SignInOrAvatarWithOptionalStatus.displayName = 'SignInOrAvatarWithOptionalStatus';
+export default compose(
+ withCurrentUserPersonalDetails,
+ withOnyx({
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
+ }),
+)(SignInOrAvatarWithOptionalStatus);
diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js
index f9dc03c66f90..7b2d278681aa 100644
--- a/src/pages/iou/IOUCurrencySelection.js
+++ b/src/pages/iou/IOUCurrencySelection.js
@@ -24,6 +24,24 @@ const greenCheckmark = {src: Expensicons.Checkmark, color: themeColors.success};
* IOU Currency selection for selecting currency
*/
const propTypes = {
+ /** Route from navigation */
+ route: PropTypes.shape({
+ /** Params from the route */
+ params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
+ iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
+ reportID: PropTypes.string,
+
+ /** Currently selected currency */
+ currency: PropTypes.string,
+
+ /** Route to navigate back after selecting a currency */
+ backTo: PropTypes.string,
+ }),
+ }).isRequired,
+
// The currency list constant object from Onyx
currencyList: PropTypes.objectOf(
PropTypes.shape({
@@ -40,6 +58,7 @@ const propTypes = {
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** Currency of the request */
currency: PropTypes.string,
}),
diff --git a/src/pages/iou/MoneyRequestDatePage.js b/src/pages/iou/MoneyRequestDatePage.js
index 3fcd3aba263c..e06794af51b7 100644
--- a/src/pages/iou/MoneyRequestDatePage.js
+++ b/src/pages/iou/MoneyRequestDatePage.js
@@ -19,10 +19,17 @@ const propTypes = {
/** Onyx Props */
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** ID (iouType + reportID) of the request */
id: PropTypes.string,
+
+ /** Amount of the request */
amount: PropTypes.number,
+
+ /** Description of the request */
comment: PropTypes.string,
created: PropTypes.string,
+
+ /** List of the participants */
participants: PropTypes.arrayOf(optionPropTypes),
receiptPath: PropTypes.string,
}),
@@ -31,6 +38,12 @@ const propTypes = {
route: PropTypes.shape({
/** Params from the route */
params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
+ iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
+ reportID: PropTypes.string,
+
/** Which field we are editing */
field: PropTypes.string,
diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js
index 2e6e459f1d96..7a9f666c5843 100644
--- a/src/pages/iou/MoneyRequestDescriptionPage.js
+++ b/src/pages/iou/MoneyRequestDescriptionPage.js
@@ -21,9 +21,16 @@ const propTypes = {
/** Onyx Props */
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** ID (iouType + reportID) of the request */
id: PropTypes.string,
+
+ /** Amount of the request */
amount: PropTypes.number,
+
+ /** Description of the request */
comment: PropTypes.string,
+
+ /** List of the participants */
participants: PropTypes.arrayOf(optionPropTypes),
receiptPath: PropTypes.string,
}),
@@ -32,6 +39,12 @@ const propTypes = {
route: PropTypes.shape({
/** Params from the route */
params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
+ iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
+ reportID: PropTypes.string,
+
/** Which field we are editing */
field: PropTypes.string,
diff --git a/src/pages/iou/MoneyRequestMerchantPage.js b/src/pages/iou/MoneyRequestMerchantPage.js
index e2d04288353f..21c7254d61a0 100644
--- a/src/pages/iou/MoneyRequestMerchantPage.js
+++ b/src/pages/iou/MoneyRequestMerchantPage.js
@@ -21,11 +21,18 @@ const propTypes = {
/** Onyx Props */
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** ID (iouType + reportID) of the request */
id: PropTypes.string,
+
+ /** Amount of the request */
amount: PropTypes.number,
+
+ /** Description of the request */
comment: PropTypes.string,
created: PropTypes.string,
merchant: PropTypes.string,
+
+ /** List of the participants */
participants: PropTypes.arrayOf(optionPropTypes),
receiptPath: PropTypes.string,
}),
@@ -34,6 +41,12 @@ const propTypes = {
route: PropTypes.shape({
/** Params from the route */
params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
+ iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
+ reportID: PropTypes.string,
+
/** Which field we are editing */
field: PropTypes.string,
diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js
index 4dd688950fbd..f70412d9734e 100644
--- a/src/pages/iou/MoneyRequestSelectorPage.js
+++ b/src/pages/iou/MoneyRequestSelectorPage.js
@@ -25,17 +25,28 @@ import NewRequestAmountPage from './steps/NewRequestAmountPage';
const propTypes = {
/** React Navigation route */
route: PropTypes.shape({
+ /** Params from the route */
params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
reportID: PropTypes.string,
}),
- }),
+ }).isRequired,
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** ID (iouType + reportID) of the request */
id: PropTypes.string,
+
+ /** Amount of the request */
amount: PropTypes.number,
+
+ /** Currency of the request */
currency: PropTypes.string,
+
+ /** List of the participants */
participants: PropTypes.arrayOf(participantPropTypes),
}),
@@ -44,12 +55,6 @@ const propTypes = {
};
const defaultProps = {
- route: {
- params: {
- iouType: '',
- reportID: '',
- },
- },
iou: {
id: '',
amount: 0,
diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js
index 392e96887f8b..f37ea18842ad 100644
--- a/src/pages/iou/ReceiptSelector/index.js
+++ b/src/pages/iou/ReceiptSelector/index.js
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import * as IOU from '../../../libs/actions/IOU';
import reportPropTypes from '../../reportPropTypes';
+import participantPropTypes from '../../../components/participantPropTypes';
import CONST from '../../../CONST';
import ReceiptUpload from '../../../../assets/images/receipt-upload.svg';
import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback';
@@ -32,27 +33,31 @@ const propTypes = {
/** The report on which the request is initiated on */
report: reportPropTypes,
+ /** React Navigation route */
route: PropTypes.shape({
+ /** Params from the route */
params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
reportID: PropTypes.string,
}),
- }),
+ }).isRequired,
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** ID (iouType + reportID) of the request */
id: PropTypes.string,
+
+ /** Amount of the request */
amount: PropTypes.number,
+
+ /** Currency of the request */
currency: PropTypes.string,
- participants: PropTypes.arrayOf(
- PropTypes.shape({
- accountID: PropTypes.number,
- login: PropTypes.string,
- isPolicyExpenseChat: PropTypes.bool,
- isOwnPolicyExpenseChat: PropTypes.bool,
- selected: PropTypes.bool,
- }),
- ),
+
+ /** List of the participants */
+ participants: PropTypes.arrayOf(participantPropTypes),
}),
};
@@ -63,12 +68,6 @@ const defaultProps = {
attachmentInvalidReason: '',
},
report: {},
- route: {
- params: {
- iouType: '',
- reportID: '',
- },
- },
iou: {
id: '',
amount: 0,
diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js
index 730e5d1e1dfb..727cbb05d395 100644
--- a/src/pages/iou/ReceiptSelector/index.native.js
+++ b/src/pages/iou/ReceiptSelector/index.native.js
@@ -23,35 +23,40 @@ import Log from '../../../libs/Log';
import participantPropTypes from '../../../components/participantPropTypes';
const propTypes = {
- /** Route params */
+ /** React Navigation route */
route: PropTypes.shape({
+ /** Params from the route */
params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
reportID: PropTypes.string,
}),
- }),
+ }).isRequired,
/** The report on which the request is initiated on */
report: reportPropTypes,
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** ID (iouType + reportID) of the request */
id: PropTypes.string,
+
+ /** Amount of the request */
amount: PropTypes.number,
+
+ /** Description of the request */
comment: PropTypes.string,
created: PropTypes.string,
merchant: PropTypes.string,
+
+ /** List of the participants */
participants: PropTypes.arrayOf(participantPropTypes),
}),
};
const defaultProps = {
- route: {
- params: {
- iouType: '',
- reportID: '',
- },
- },
report: {},
iou: {
id: '',
diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js
index 99f7e982cb10..8aece1c11d7d 100644
--- a/src/pages/iou/steps/MoneyRequestAmountForm.js
+++ b/src/pages/iou/steps/MoneyRequestAmountForm.js
@@ -12,6 +12,7 @@ import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
import TextInputWithCurrencySymbol from '../../../components/TextInputWithCurrencySymbol';
import useLocalize from '../../../hooks/useLocalize';
import CONST from '../../../CONST';
+import refPropTypes from '../../../components/refPropTypes';
const propTypes = {
/** IOU amount saved in Onyx */
@@ -24,7 +25,7 @@ const propTypes = {
isEditing: PropTypes.bool,
/** Refs forwarded to the TextInputWithCurrencySymbol */
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+ forwardedRef: refPropTypes,
/** Fired when back button pressed, navigates to currency selection page */
onCurrencyButtonPress: PropTypes.func.isRequired,
@@ -121,8 +122,9 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
return;
}
setCurrentAmount((prevAmount) => {
- setSelection((prevSelection) => getNewSelection(prevSelection, prevAmount.length, newAmountWithoutSpaces.length));
- return MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces);
+ const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces);
+ setSelection((prevSelection) => getNewSelection(prevSelection, prevAmount.length, strippedAmount.length));
+ return strippedAmount;
});
};
diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js
index c22a81cd7299..06d8f4c8b69d 100644
--- a/src/pages/iou/steps/MoneyRequestConfirmPage.js
+++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js
@@ -20,29 +20,43 @@ import ONYXKEYS from '../../../ONYXKEYS';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../../components/withCurrentUserPersonalDetails';
import reportPropTypes from '../../reportPropTypes';
import personalDetailsPropType from '../../personalDetailsPropType';
+import participantPropTypes from '../../../components/participantPropTypes';
import * as FileUtils from '../../../libs/fileDownload/FileUtils';
import * as Policy from '../../../libs/actions/Policy';
const propTypes = {
+ /** React Navigation route */
+ route: PropTypes.shape({
+ /** Params from the route */
+ params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
+ iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
+ reportID: PropTypes.string,
+ }),
+ }).isRequired,
+
report: reportPropTypes,
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** ID (iouType + reportID) of the request */
id: PropTypes.string,
+
+ /** Amount of the request */
amount: PropTypes.number,
+
+ /** Description of the request */
comment: PropTypes.string,
created: PropTypes.string,
+
+ /** Currency of the request */
currency: PropTypes.string,
merchant: PropTypes.string,
- participants: PropTypes.arrayOf(
- PropTypes.shape({
- accountID: PropTypes.number,
- login: PropTypes.string,
- isPolicyExpenseChat: PropTypes.bool,
- isOwnPolicyExpenseChat: PropTypes.bool,
- selected: PropTypes.bool,
- }),
- ),
+
+ /** List of the participants */
+ participants: PropTypes.arrayOf(participantPropTypes),
receiptPath: PropTypes.string,
}),
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
index d6d069dc0329..8ff6ce822991 100644
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
@@ -16,21 +16,31 @@ import compose from '../../../../libs/compose';
import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities';
import HeaderWithBackButton from '../../../../components/HeaderWithBackButton';
import * as IOU from '../../../../libs/actions/IOU';
+import participantPropTypes from '../../../../components/participantPropTypes';
const propTypes = {
+ /** React Navigation route */
+ route: PropTypes.shape({
+ /** Params from the route */
+ params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
+ iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
+ reportID: PropTypes.string,
+ }),
+ }).isRequired,
+
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** ID (iouType + reportID) of the request */
id: PropTypes.string,
+
+ /** Amount of the request */
amount: PropTypes.number,
- participants: PropTypes.arrayOf(
- PropTypes.shape({
- accountID: PropTypes.number,
- login: PropTypes.string,
- isPolicyExpenseChat: PropTypes.bool,
- isOwnPolicyExpenseChat: PropTypes.bool,
- selected: PropTypes.bool,
- }),
- ),
+
+ /** List of the participants */
+ participants: PropTypes.arrayOf(participantPropTypes),
}),
...withLocalizePropTypes,
diff --git a/src/pages/iou/steps/NewRequestAmountPage.js b/src/pages/iou/steps/NewRequestAmountPage.js
index e0199d72b01a..7a6fcbfeb10b 100644
--- a/src/pages/iou/steps/NewRequestAmountPage.js
+++ b/src/pages/iou/steps/NewRequestAmountPage.js
@@ -12,6 +12,7 @@ import * as ReportUtils from '../../../libs/ReportUtils';
import * as CurrencyUtils from '../../../libs/CurrencyUtils';
import CONST from '../../../CONST';
import reportPropTypes from '../../reportPropTypes';
+import participantPropTypes from '../../../components/participantPropTypes';
import * as IOU from '../../../libs/actions/IOU';
import useLocalize from '../../../hooks/useLocalize';
import MoneyRequestAmountForm from './MoneyRequestAmountForm';
@@ -22,42 +23,43 @@ import HeaderWithBackButton from '../../../components/HeaderWithBackButton';
import ScreenWrapper from '../../../components/ScreenWrapper';
const propTypes = {
+ /** React Navigation route */
route: PropTypes.shape({
+ /** Params from the route */
params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
reportID: PropTypes.string,
+
+ /** Selected currency from IOUCurrencySelection */
+ currency: PropTypes.string,
}),
- }),
+ }).isRequired,
/** The report on which the request is initiated on */
report: reportPropTypes,
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: PropTypes.shape({
+ /** ID (iouType + reportID) of the request */
id: PropTypes.string,
+
+ /** Amount of the request */
amount: PropTypes.number,
+
+ /** Currency of the request */
currency: PropTypes.string,
merchant: PropTypes.string,
created: PropTypes.string,
- participants: PropTypes.arrayOf(
- PropTypes.shape({
- accountID: PropTypes.number,
- login: PropTypes.string,
- isPolicyExpenseChat: PropTypes.bool,
- isOwnPolicyExpenseChat: PropTypes.bool,
- selected: PropTypes.bool,
- }),
- ),
+
+ /** List of the participants */
+ participants: PropTypes.arrayOf(participantPropTypes),
}),
};
const defaultProps = {
- route: {
- params: {
- iouType: '',
- reportID: '',
- },
- },
report: {},
iou: {
id: '',
diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js
index 783c69a08ed9..a67e7cbc122e 100755
--- a/src/pages/settings/InitialSettingsPage.js
+++ b/src/pages/settings/InitialSettingsPage.js
@@ -41,6 +41,8 @@ import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'
import * as CurrencyUtils from '../../libs/CurrencyUtils';
import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback';
import useLocalize from '../../hooks/useLocalize';
+import useSingleExecution from '../../hooks/useSingleExecution';
+import useWaitForNavigation from '../../hooks/useWaitForNavigation';
const propTypes = {
/* Onyx Props */
@@ -125,6 +127,8 @@ const defaultProps = {
};
function InitialSettingsPage(props) {
+ const {isExecuting, singleExecution} = useSingleExecution();
+ const waitForNavigate = useWaitForNavigation();
const popoverAnchor = useRef(null);
const {translate} = useLocalize();
@@ -186,16 +190,16 @@ function InitialSettingsPage(props) {
{
translationKey: 'common.shareCode',
icon: Expensicons.QrCode,
- action: () => {
+ action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_SHARE_CODE);
- },
+ }),
},
{
translationKey: 'common.workspaces',
icon: Expensicons.Building,
- action: () => {
+ action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
- },
+ }),
floatRightAvatars: policiesAvatars,
shouldStackHorizontally: true,
avatarSize: CONST.AVATAR_SIZE.SMALLER,
@@ -204,31 +208,31 @@ function InitialSettingsPage(props) {
{
translationKey: 'common.profile',
icon: Expensicons.Profile,
- action: () => {
+ action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_PROFILE);
- },
+ }),
brickRoadIndicator: profileBrickRoadIndicator,
},
{
translationKey: 'common.preferences',
icon: Expensicons.Gear,
- action: () => {
+ action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_PREFERENCES);
- },
+ }),
},
{
translationKey: 'initialSettingsPage.security',
icon: Expensicons.Lock,
- action: () => {
+ action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_SECURITY);
- },
+ }),
},
{
translationKey: 'common.wallet',
icon: Expensicons.Wallet,
- action: () => {
+ action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_WALLET);
- },
+ }),
brickRoadIndicator:
PaymentMethods.hasPaymentMethodError(props.bankAccountList, paymentCardList) || !_.isEmpty(props.userWallet.errors) || !_.isEmpty(props.walletTerms.errors)
? 'error'
@@ -247,9 +251,9 @@ function InitialSettingsPage(props) {
{
translationKey: 'initialSettingsPage.about',
icon: Expensicons.Info,
- action: () => {
+ action: waitForNavigate(() => {
Navigation.navigate(ROUTES.SETTINGS_ABOUT);
- },
+ }),
},
{
translationKey: 'initialSettingsPage.signOut',
@@ -270,6 +274,7 @@ function InitialSettingsPage(props) {
props.userWallet.errors,
props.walletTerms.errors,
signOut,
+ waitForNavigate,
]);
const getMenuItems = useMemo(() => {
@@ -292,7 +297,8 @@ function InitialSettingsPage(props) {
title={keyTitle}
icon={item.icon}
iconType={item.iconType}
- onPress={item.action}
+ disabled={isExecuting}
+ onPress={singleExecution(item.action)}
iconStyles={item.iconStyles}
shouldShowRightIcon
iconRight={item.iconRight}
@@ -312,7 +318,7 @@ function InitialSettingsPage(props) {
})}
>
);
- }, [getDefaultMenuItems, props.betas, props.userWallet.currentBalance, translate]);
+ }, [getDefaultMenuItems, props.betas, props.userWallet.currentBalance, translate, isExecuting, singleExecution]);
// On the very first sign in or after clearing storage these
// details will not be present on the first render so we'll just
diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.js b/src/pages/settings/Profile/CustomStatus/StatusPage.js
index d896fc2fed63..0d7e1c09454d 100644
--- a/src/pages/settings/Profile/CustomStatus/StatusPage.js
+++ b/src/pages/settings/Profile/CustomStatus/StatusPage.js
@@ -37,6 +37,12 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
const customStatus = draftEmojiCode ? `${draftEmojiCode} ${draftText}` : `${currentUserEmojiCode || ''} ${currentUserStatusText || ''}`;
const hasDraftStatus = !!draftEmojiCode || !!draftText;
+ const clearStatus = () => {
+ User.clearCustomStatus();
+ User.clearDraftCustomStatus();
+ };
+
+ const navigateBackToSettingsPage = useCallback(() => Navigation.goBack(ROUTES.SETTINGS_PROFILE, false, true), []);
const updateStatus = useCallback(() => {
const endOfDay = moment().endOf('day').format('YYYY-MM-DD HH:mm:ss');
User.updateCustomStatus({text: defaultText, emojiCode: defaultEmoji, clearAfter: endOfDay});
@@ -44,12 +50,6 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
User.clearDraftCustomStatus();
Navigation.goBack(ROUTES.SETTINGS_PROFILE);
}, [defaultText, defaultEmoji]);
-
- const clearStatus = () => {
- User.clearCustomStatus();
- User.clearDraftCustomStatus();
- };
-
const footerComponent = useMemo(
() =>
hasDraftStatus ? (
@@ -67,7 +67,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
return (
Navigation.goBack(ROUTES.SETTINGS_PROFILE)}
+ onBackButtonPress={navigateBackToSettingsPage}
backgroundColor={themeColors.midtone}
image={MobileBackgroundImage}
footer={footerComponent}
diff --git a/src/pages/settings/Profile/DisplayNamePage.js b/src/pages/settings/Profile/DisplayNamePage.js
index 31ec955ed0c6..d93eeed2689c 100644
--- a/src/pages/settings/Profile/DisplayNamePage.js
+++ b/src/pages/settings/Profile/DisplayNamePage.js
@@ -92,7 +92,6 @@ function DisplayNamePage(props) {
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
defaultValue={lodashGet(currentUserDetails, 'firstName', '')}
maxLength={CONST.DISPLAY_NAME.MAX_LENGTH}
- autoCapitalize="words"
spellCheck={false}
/>
@@ -105,7 +104,6 @@ function DisplayNamePage(props) {
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
defaultValue={lodashGet(currentUserDetails, 'lastName', '')}
maxLength={CONST.DISPLAY_NAME.MAX_LENGTH}
- autoCapitalize="words"
spellCheck={false}
/>
diff --git a/src/pages/settings/Profile/PronounsPage.js b/src/pages/settings/Profile/PronounsPage.js
index 50a231523834..ce460bc30ff4 100644
--- a/src/pages/settings/Profile/PronounsPage.js
+++ b/src/pages/settings/Profile/PronounsPage.js
@@ -1,6 +1,6 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
-import React, {useState, useEffect, useMemo, useCallback} from 'react';
+import React, {useState, useEffect, useCallback} from 'react';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails';
import ScreenWrapper from '../../../components/ScreenWrapper';
import HeaderWithBackButton from '../../../components/HeaderWithBackButton';
@@ -78,13 +78,7 @@ function PronounsPage(props) {
* Pronouns list filtered by searchValue needed for the OptionsSelector.
* Empty array if the searchValue is empty.
*/
- const filteredPronounsList = useMemo(() => {
- const searchedValue = searchValue.trim();
- if (searchedValue.length === 0) {
- return [];
- }
- return _.filter(pronounsList, (pronous) => pronous.text.toLowerCase().indexOf(searchedValue.toLowerCase()) >= 0);
- }, [pronounsList, searchValue]);
+ const filteredPronounsList = _.filter(pronounsList, (pronous) => pronous.text.toLowerCase().indexOf(searchValue.trim().toLowerCase()) >= 0);
const headerMessage = searchValue.trim() && !filteredPronounsList.length ? props.translate('common.noResultsFound') : '';
@@ -104,16 +98,20 @@ function PronounsPage(props) {
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_PROFILE)}
/>
{props.translate('pronounsPage.isShownOnProfile')}
-
+ {/* Only render pronouns if list was loaded (not filtered list), otherwise initially focused item will be empty */}
+ {pronounsList.length > 0 && (
+
+ )}
);
}
diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.js
index 7bdf1afd498a..d0a50acdeb17 100644
--- a/src/pages/settings/Profile/TimezoneSelectPage.js
+++ b/src/pages/settings/Profile/TimezoneSelectPage.js
@@ -78,6 +78,8 @@ function TimezoneSelectPage(props) {
onSelectRow={saveSelectedTimezone}
sections={[{data: timezoneOptions, indexOffset: 0, isDisabled: timezone.automatic}]}
initiallyFocusedOptionKey={_.get(_.filter(timezoneOptions, (tz) => tz.text === timezone.selected)[0], 'keyForList')}
+ shouldDelayFocus
+ showScrollIndicator
/>
);
diff --git a/src/pages/settings/Report/ReportSettingsPage.js b/src/pages/settings/Report/ReportSettingsPage.js
index b21c4b971dca..c2bd69028a44 100644
--- a/src/pages/settings/Report/ReportSettingsPage.js
+++ b/src/pages/settings/Report/ReportSettingsPage.js
@@ -65,7 +65,10 @@ function ReportSettingsPage(props) {
const shouldDisableSettings = _.isEmpty(report) || ReportUtils.shouldDisableSettings(report) || ReportUtils.isArchivedRoom(report);
const shouldShowRoomName = !ReportUtils.isPolicyExpenseChat(report) && !ReportUtils.isChatThread(report);
- const notificationPreference = translate(`notificationPreferencesPage.notificationPreferences.${report.notificationPreference}`);
+ const notificationPreference =
+ report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN
+ ? translate(`notificationPreferencesPage.notificationPreferences.${report.notificationPreference}`)
+ : '';
const writeCapability = ReportUtils.isAdminRoom(report) ? CONST.REPORT.WRITE_CAPABILITIES.ADMINS : report.writeCapability || CONST.REPORT.WRITE_CAPABILITIES.ALL;
const writeCapabilityText = translate(`writeCapabilityPage.writeCapability.${writeCapability}`);
@@ -79,12 +82,14 @@ function ReportSettingsPage(props) {
onBackButtonPress={() => Navigation.goBack(ROUTES.getReportDetailsRoute(report.reportID))}
/>
- Navigation.navigate(ROUTES.getReportSettingsNotificationPreferencesRoute(report.reportID))}
- />
+ {report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && (
+ Navigation.navigate(ROUTES.getReportSettingsNotificationPreferencesRoute(report.reportID))}
+ />
+ )}
{shouldShowRoomName && (
;
+ }
+
return (
nameOnCardRef.current && nameOnCardRef.current.focus()}
@@ -194,4 +204,7 @@ export default withOnyx({
formData: {
key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
},
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
})(DebitCardPage);
diff --git a/src/pages/signin/LoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js
similarity index 81%
rename from src/pages/signin/LoginForm.js
rename to src/pages/signin/LoginForm/BaseLoginForm.js
index b3a154763067..ccf67844e7f6 100644
--- a/src/pages/signin/LoginForm.js
+++ b/src/pages/signin/LoginForm/BaseLoginForm.js
@@ -1,43 +1,46 @@
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import _ from 'underscore';
import Str from 'expensify-common/lib/str';
import {parsePhoneNumber} from 'awesome-phonenumber';
-import styles from '../../styles/styles';
-import Text from '../../components/Text';
-import * as Session from '../../libs/actions/Session';
-import ONYXKEYS from '../../ONYXKEYS';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions';
-import compose from '../../libs/compose';
-import canFocusInputOnScreenFocus from '../../libs/canFocusInputOnScreenFocus';
-import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
-import TextInput from '../../components/TextInput';
-import * as ValidationUtils from '../../libs/ValidationUtils';
-import * as LoginUtils from '../../libs/LoginUtils';
-import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '../../components/withToggleVisibilityView';
-import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton';
-import {withNetwork} from '../../components/OnyxProvider';
-import networkPropTypes from '../../components/networkPropTypes';
-import * as ErrorUtils from '../../libs/ErrorUtils';
-import DotIndicatorMessage from '../../components/DotIndicatorMessage';
-import * as CloseAccount from '../../libs/actions/CloseAccount';
-import CONST from '../../CONST';
-import CONFIG from '../../CONFIG';
-import AppleSignIn from '../../components/SignInButtons/AppleSignIn';
-import GoogleSignIn from '../../components/SignInButtons/GoogleSignIn';
-import isInputAutoFilled from '../../libs/isInputAutoFilled';
-import * as PolicyUtils from '../../libs/PolicyUtils';
-import Log from '../../libs/Log';
-import withNavigationFocus, {withNavigationFocusPropTypes} from '../../components/withNavigationFocus';
-import usePrevious from '../../hooks/usePrevious';
-import * as MemoryOnlyKeys from '../../libs/actions/MemoryOnlyKeys/MemoryOnlyKeys';
+import styles from '../../../styles/styles';
+import Text from '../../../components/Text';
+import * as Session from '../../../libs/actions/Session';
+import ONYXKEYS from '../../../ONYXKEYS';
+import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
+import compose from '../../../libs/compose';
+import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus';
+import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
+import TextInput from '../../../components/TextInput';
+import * as ValidationUtils from '../../../libs/ValidationUtils';
+import * as LoginUtils from '../../../libs/LoginUtils';
+import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '../../../components/withToggleVisibilityView';
+import FormAlertWithSubmitButton from '../../../components/FormAlertWithSubmitButton';
+import {withNetwork} from '../../../components/OnyxProvider';
+import networkPropTypes from '../../../components/networkPropTypes';
+import * as ErrorUtils from '../../../libs/ErrorUtils';
+import DotIndicatorMessage from '../../../components/DotIndicatorMessage';
+import * as CloseAccount from '../../../libs/actions/CloseAccount';
+import CONST from '../../../CONST';
+import CONFIG from '../../../CONFIG';
+import AppleSignIn from '../../../components/SignInButtons/AppleSignIn';
+import GoogleSignIn from '../../../components/SignInButtons/GoogleSignIn';
+import isInputAutoFilled from '../../../libs/isInputAutoFilled';
+import * as PolicyUtils from '../../../libs/PolicyUtils';
+import Log from '../../../libs/Log';
+import withNavigationFocus, {withNavigationFocusPropTypes} from '../../../components/withNavigationFocus';
+import usePrevious from '../../../hooks/usePrevious';
+import * as MemoryOnlyKeys from '../../../libs/actions/MemoryOnlyKeys/MemoryOnlyKeys';
const propTypes = {
/** Should we dismiss the keyboard when transitioning away from the page? */
blurOnSubmit: PropTypes.bool,
+ /** A reference so we can expose if the form input is focused */
+ innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+
/* Onyx Props */
/** The details about the account that the user is signing in with */
@@ -73,6 +76,7 @@ const defaultProps = {
account: {},
closeAccount: {},
blurOnSubmit: false,
+ innerRef: () => {},
};
function LoginForm(props) {
@@ -177,6 +181,12 @@ function LoginForm(props) {
input.current.focus();
}, [props.blurOnSubmit, props.isVisible, prevIsVisible]);
+ useImperativeHandle(props.innerRef, () => ({
+ isInputFocused() {
+ return input.current && input.current.isFocused();
+ },
+ }));
+
const formErrorText = useMemo(() => (formError ? translate(formError) : ''), [formError, translate]);
const serverErrorText = useMemo(() => ErrorUtils.getLatestErrorMessage(props.account), [props.account]);
const hasError = !_.isEmpty(serverErrorText);
@@ -272,4 +282,12 @@ export default compose(
withLocalize,
withToggleVisibilityView,
withNetwork(),
-)(LoginForm);
+)(
+ forwardRef((props, ref) => (
+
+ )),
+);
diff --git a/src/pages/signin/LoginForm/index.js b/src/pages/signin/LoginForm/index.js
new file mode 100644
index 000000000000..b9dfbb8dfbb5
--- /dev/null
+++ b/src/pages/signin/LoginForm/index.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import BaseLoginForm from './BaseLoginForm';
+
+const propTypes = {
+ /** Function used to scroll to the top of the page */
+ scrollPageToTop: PropTypes.func,
+};
+const defaultProps = {
+ scrollPageToTop: undefined,
+};
+
+function LoginForm(props) {
+ return (
+
+ );
+}
+
+LoginForm.displayName = 'LoginForm';
+LoginForm.propTypes = propTypes;
+LoginForm.defaultProps = defaultProps;
+
+export default LoginForm;
diff --git a/src/pages/signin/LoginForm/index.native.js b/src/pages/signin/LoginForm/index.native.js
new file mode 100644
index 000000000000..dc55ad68e53b
--- /dev/null
+++ b/src/pages/signin/LoginForm/index.native.js
@@ -0,0 +1,48 @@
+import React, {useEffect, useRef} from 'react';
+import PropTypes from 'prop-types';
+import BaseLoginForm from './BaseLoginForm';
+import AppStateMonitor from '../../../libs/AppStateMonitor';
+
+const propTypes = {
+ /** Function used to scroll to the top of the page */
+ scrollPageToTop: PropTypes.func,
+};
+const defaultProps = {
+ scrollPageToTop: undefined,
+};
+
+function LoginForm(props) {
+ const loginFormRef = useRef();
+ const {scrollPageToTop} = props;
+
+ useEffect(() => {
+ if (!scrollPageToTop) {
+ return;
+ }
+
+ const unsubscribeToBecameActiveListener = AppStateMonitor.addBecameActiveListener(() => {
+ const isInputFocused = loginFormRef.current && loginFormRef.current.isInputFocused();
+ if (!isInputFocused) {
+ return;
+ }
+
+ scrollPageToTop();
+ });
+
+ return unsubscribeToBecameActiveListener;
+ }, [scrollPageToTop]);
+
+ return (
+ (loginFormRef.current = ref)}
+ />
+ );
+}
+
+LoginForm.displayName = 'LoginForm';
+LoginForm.propTypes = propTypes;
+LoginForm.defaultProps = defaultProps;
+
+export default LoginForm;
diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js
index f1a8cf3b910e..21a92bce41c0 100644
--- a/src/pages/signin/SignInPage.js
+++ b/src/pages/signin/SignInPage.js
@@ -1,4 +1,4 @@
-import React, {useEffect} from 'react';
+import React, {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
@@ -95,6 +95,7 @@ function SignInPage({credentials, account, isInModal, demoInfo}) {
const {isSmallScreenWidth} = useWindowDimensions();
const shouldShowSmallScreen = isSmallScreenWidth || isInModal;
const safeAreaInsets = useSafeAreaInsets();
+ const signInPageLayoutRef = useRef();
useEffect(() => Performance.measureTTI(), []);
useEffect(() => {
@@ -161,6 +162,7 @@ function SignInPage({credentials, account, isInModal, demoInfo}) {
welcomeText={welcomeText}
shouldShowWelcomeHeader={shouldShowWelcomeHeader || !isSmallScreenWidth || !isInModal}
shouldShowWelcomeText={shouldShowWelcomeText}
+ ref={signInPageLayoutRef}
isInModal={isInModal}
customHeadline={customHeadline}
>
@@ -169,6 +171,7 @@ function SignInPage({credentials, account, isInModal, demoInfo}) {
{shouldShowValidateCodeForm && }
{shouldShowUnlinkLoginForm && }
diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js
index 8031d0096bc6..13c8c1d9ab07 100644
--- a/src/pages/signin/SignInPageLayout/index.js
+++ b/src/pages/signin/SignInPageLayout/index.js
@@ -1,4 +1,4 @@
-import React, {useRef, useEffect} from 'react';
+import React, {forwardRef, useRef, useEffect, useImperativeHandle} from 'react';
import {View, ScrollView} from 'react-native';
import {withSafeAreaInsets} from 'react-native-safe-area-context';
import PropTypes from 'prop-types';
@@ -35,6 +35,9 @@ const propTypes = {
/** Whether to show welcome header on a particular page */
shouldShowWelcomeHeader: PropTypes.bool.isRequired,
+ /** A reference so we can expose scrollPageToTop */
+ innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+
/** Whether or not the sign in page is being rendered in the RHP modal */
isInModal: PropTypes.bool.isRequired,
@@ -46,6 +49,7 @@ const propTypes = {
};
const defaultProps = {
+ innerRef: () => {},
customHeadline: '',
};
@@ -71,6 +75,10 @@ function SignInPageLayout(props) {
scrollViewRef.current.scrollTo({y: 0, animated});
};
+ useImperativeHandle(props.innerRef, () => ({
+ scrollPageToTop,
+ }));
+
useEffect(() => {
if (prevPreferredLocale !== props.preferredLocale) {
return;
@@ -160,4 +168,16 @@ SignInPageLayout.propTypes = propTypes;
SignInPageLayout.displayName = 'SignInPageLayout';
SignInPageLayout.defaultProps = defaultProps;
-export default compose(withWindowDimensions, withSafeAreaInsets, withLocalize)(SignInPageLayout);
+export default compose(
+ withWindowDimensions,
+ withSafeAreaInsets,
+ withLocalize,
+)(
+ forwardRef((props, ref) => (
+
+ )),
+);
diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js
index 37f9d8815324..e7dfc6472abd 100644
--- a/src/pages/tasks/TaskAssigneeSelectorModal.js
+++ b/src/pages/tasks/TaskAssigneeSelectorModal.js
@@ -87,6 +87,8 @@ function TaskAssigneeSelectorModal(props) {
[],
CONST.EXPENSIFY_EMAILS,
false,
+ true,
+ false,
);
setHeaderMessage(OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), searchValue));
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
index b22421167478..6db3a20a3e4a 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -225,7 +225,10 @@ function WorkspaceInvitePage(props) {
onChangeText={setSearchTerm}
headerMessage={headerMessage}
onSelectRow={toggleOption}
+ onConfirm={inviteUser}
showScrollIndicator
+ shouldDelayFocus
+ showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)}
/>
toggleAllUsers(data)}
onDismissError={dismissError}
showLoadingPlaceholder={!OptionsListUtils.isPersonalDetailsReady(props.personalDetails) || _.isEmpty(props.policyMembers)}
- initiallyFocusedOptionKey={lodashGet(
- _.find(data, (item) => !item.isDisabled),
- 'keyForList',
- undefined,
- )}
+ shouldDelayFocus
+ showScrollIndicator
/>
diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js
index 425c8ebea691..9dcf954e84fd 100644
--- a/src/styles/StyleUtils.js
+++ b/src/styles/StyleUtils.js
@@ -1050,6 +1050,17 @@ function getAutoCompleteSuggestionItemStyle(highlightedEmojiIndex, rowHeight, ho
];
}
+/**
+ * Gets the correct position for the base auto complete suggestion container
+ *
+ * @param {Object} parentContainerLayout
+ * @returns {Object}
+ */
+
+function getBaseAutoCompleteSuggestionContainerStyle({left, bottom, width}) {
+ return {position: 'fixed', bottom, left, width};
+}
+
/**
* Gets the correct position for auto complete suggestion container
*
@@ -1061,7 +1072,7 @@ function getAutoCompleteSuggestionContainerStyle(itemsHeight, shouldIncludeRepor
'worklet';
const optionalPadding = shouldIncludeReportRecipientLocalTimeHeight ? CONST.RECIPIENT_LOCAL_TIME_HEIGHT : 0;
- const padding = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING - optionalPadding;
+ const padding = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + optionalPadding;
const borderWidth = 2;
const height = itemsHeight + 2 * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING + borderWidth;
@@ -1367,6 +1378,7 @@ export {
getReportWelcomeBackgroundImageStyle,
getReportWelcomeTopMarginStyle,
getReportWelcomeContainerStyle,
+ getBaseAutoCompleteSuggestionContainerStyle,
getAutoCompleteSuggestionItemStyle,
getAutoCompleteSuggestionContainerStyle,
getColoredBackgroundStyle,
diff --git a/src/styles/ThemeStylesContext.js b/src/styles/ThemeStylesContext.ts
similarity index 100%
rename from src/styles/ThemeStylesContext.js
rename to src/styles/ThemeStylesContext.ts
diff --git a/src/styles/fontFamily/index.native.js b/src/styles/fontFamily/index.native.ts
similarity index 86%
rename from src/styles/fontFamily/index.native.js
rename to src/styles/fontFamily/index.native.ts
index 369d1f66f8f1..0d6c253eb3a6 100644
--- a/src/styles/fontFamily/index.native.js
+++ b/src/styles/fontFamily/index.native.ts
@@ -1,6 +1,7 @@
import bold from './bold';
+import FontFamilyStyles from './types';
-const fontFamily = {
+const fontFamily: FontFamilyStyles = {
EXP_NEUE_ITALIC: 'ExpensifyNeue-Italic',
EXP_NEUE_BOLD: bold,
EXP_NEUE: 'ExpensifyNeue-Regular',
diff --git a/src/styles/fontFamily/index.js b/src/styles/fontFamily/index.ts
similarity index 91%
rename from src/styles/fontFamily/index.js
rename to src/styles/fontFamily/index.ts
index 899ef20772de..57d08ce28771 100644
--- a/src/styles/fontFamily/index.js
+++ b/src/styles/fontFamily/index.ts
@@ -1,8 +1,9 @@
import bold from './bold';
+import FontFamilyStyles from './types';
// In windows and ubuntu, we need some extra system fonts for emojis to work properly
// otherwise few of them will appear as black and white
-const fontFamily = {
+const fontFamily: FontFamilyStyles = {
EXP_NEUE_ITALIC: 'ExpensifyNeue-Italic, Segoe UI Emoji, Noto Color Emoji',
EXP_NEUE_BOLD: bold,
EXP_NEUE: 'ExpensifyNeue-Regular, Segoe UI Emoji, Noto Color Emoji',
diff --git a/src/styles/fontFamily/types.ts b/src/styles/fontFamily/types.ts
new file mode 100644
index 000000000000..4c9a121e80d7
--- /dev/null
+++ b/src/styles/fontFamily/types.ts
@@ -0,0 +1,15 @@
+type FontFamilyKeys =
+ | 'EXP_NEUE_ITALIC'
+ | 'EXP_NEUE_BOLD'
+ | 'EXP_NEUE'
+ | 'EXP_NEW_KANSAS_MEDIUM'
+ | 'EXP_NEW_KANSAS_MEDIUM_ITALIC'
+ | 'SYSTEM'
+ | 'MONOSPACE'
+ | 'MONOSPACE_ITALIC'
+ | 'MONOSPACE_BOLD'
+ | 'MONOSPACE_BOLD_ITALIC';
+
+type FontFamilyStyles = Record;
+
+export default FontFamilyStyles;
diff --git a/src/styles/getNavigationModalCardStyles/getBaseNavigationModalCardStyles.js b/src/styles/getNavigationModalCardStyles/getBaseNavigationModalCardStyles.ts
similarity index 51%
rename from src/styles/getNavigationModalCardStyles/getBaseNavigationModalCardStyles.js
rename to src/styles/getNavigationModalCardStyles/getBaseNavigationModalCardStyles.ts
index bddb639655a0..083fbda58b70 100644
--- a/src/styles/getNavigationModalCardStyles/getBaseNavigationModalCardStyles.js
+++ b/src/styles/getNavigationModalCardStyles/getBaseNavigationModalCardStyles.ts
@@ -1,6 +1,7 @@
import variables from '../variables';
+import GetNavigationModalCardStyles from './types';
-export default ({isSmallScreenWidth}) => ({
+const getBaseNavigationModalCardStyles: GetNavigationModalCardStyles = ({isSmallScreenWidth}) => ({
position: 'absolute',
top: 0,
right: 0,
@@ -8,3 +9,5 @@ export default ({isSmallScreenWidth}) => ({
backgroundColor: 'transparent',
height: '100%',
});
+
+export default getBaseNavigationModalCardStyles;
diff --git a/src/styles/getNavigationModalCardStyles/index.js b/src/styles/getNavigationModalCardStyles/index.ts
similarity index 100%
rename from src/styles/getNavigationModalCardStyles/index.js
rename to src/styles/getNavigationModalCardStyles/index.ts
diff --git a/src/styles/getNavigationModalCardStyles/index.website.js b/src/styles/getNavigationModalCardStyles/index.website.ts
similarity index 71%
rename from src/styles/getNavigationModalCardStyles/index.website.js
rename to src/styles/getNavigationModalCardStyles/index.website.ts
index f5668f955111..0c47536fc57c 100644
--- a/src/styles/getNavigationModalCardStyles/index.website.js
+++ b/src/styles/getNavigationModalCardStyles/index.website.ts
@@ -1,6 +1,7 @@
import getBaseNavigationModalCardStyles from './getBaseNavigationModalCardStyles';
+import GetNavigationModalCardStyles from './types';
-export default ({isSmallScreenWidth}) => ({
+const getNavigationModalCardStyles: GetNavigationModalCardStyles = ({isSmallScreenWidth}) => ({
...getBaseNavigationModalCardStyles({isSmallScreenWidth}),
// position: fixed is set instead of position absolute to workaround Safari known issues of updating heights in DOM.
@@ -10,3 +11,5 @@ export default ({isSmallScreenWidth}) => ({
// https://github.com/Expensify/App/issues/20709
position: 'fixed',
});
+
+export default getNavigationModalCardStyles;
diff --git a/src/styles/getNavigationModalCardStyles/types.ts b/src/styles/getNavigationModalCardStyles/types.ts
new file mode 100644
index 000000000000..504b659c87b7
--- /dev/null
+++ b/src/styles/getNavigationModalCardStyles/types.ts
@@ -0,0 +1,9 @@
+import {CSSProperties} from 'react';
+import {ViewStyle} from 'react-native';
+import {Merge} from 'type-fest';
+
+type GetNavigationModalCardStylesParams = {isSmallScreenWidth: number};
+
+type GetNavigationModalCardStyles = (params: GetNavigationModalCardStylesParams) => Merge>;
+
+export default GetNavigationModalCardStyles;
diff --git a/src/styles/italic/index.android.js b/src/styles/italic/index.android.js
deleted file mode 100644
index 92f6d65241bb..000000000000
--- a/src/styles/italic/index.android.js
+++ /dev/null
@@ -1,3 +0,0 @@
-const italic = 'normal';
-
-export default italic;
diff --git a/src/styles/italic/index.android.ts b/src/styles/italic/index.android.ts
new file mode 100644
index 000000000000..bbd9deb8cf8d
--- /dev/null
+++ b/src/styles/italic/index.android.ts
@@ -0,0 +1,5 @@
+import ItalicStyles from './types';
+
+const italic: ItalicStyles = 'normal';
+
+export default italic;
diff --git a/src/styles/italic/index.js b/src/styles/italic/index.js
deleted file mode 100644
index 8e8433c7cc05..000000000000
--- a/src/styles/italic/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-const italic = 'italic';
-
-export default italic;
diff --git a/src/styles/italic/index.ts b/src/styles/italic/index.ts
new file mode 100644
index 000000000000..02d6c46423f6
--- /dev/null
+++ b/src/styles/italic/index.ts
@@ -0,0 +1,5 @@
+import ItalicStyles from './types';
+
+const italic: ItalicStyles = 'italic';
+
+export default italic;
diff --git a/src/styles/italic/types.ts b/src/styles/italic/types.ts
new file mode 100644
index 000000000000..61e0328e52b6
--- /dev/null
+++ b/src/styles/italic/types.ts
@@ -0,0 +1,6 @@
+import {CSSProperties} from 'react';
+import {TextStyle} from 'react-native';
+
+type ItalicStyles = (TextStyle | CSSProperties)['fontStyle'];
+
+export default ItalicStyles;
diff --git a/src/styles/optionAlternateTextPlatformStyles/index.ios.js b/src/styles/optionAlternateTextPlatformStyles/index.ios.js
deleted file mode 100644
index 0f506d6675a8..000000000000
--- a/src/styles/optionAlternateTextPlatformStyles/index.ios.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- paddingTop: 1,
-};
diff --git a/src/styles/optionAlternateTextPlatformStyles/index.ios.ts b/src/styles/optionAlternateTextPlatformStyles/index.ios.ts
new file mode 100644
index 000000000000..14b024757fb8
--- /dev/null
+++ b/src/styles/optionAlternateTextPlatformStyles/index.ios.ts
@@ -0,0 +1,7 @@
+import OptionAlternateTextPlatformStyles from './types';
+
+const optionAlternateTextPlatformStyles: OptionAlternateTextPlatformStyles = {
+ paddingTop: 1,
+};
+
+export default optionAlternateTextPlatformStyles;
diff --git a/src/styles/optionAlternateTextPlatformStyles/index.js b/src/styles/optionAlternateTextPlatformStyles/index.js
deleted file mode 100644
index ff8b4c56321a..000000000000
--- a/src/styles/optionAlternateTextPlatformStyles/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/src/styles/optionAlternateTextPlatformStyles/index.ts b/src/styles/optionAlternateTextPlatformStyles/index.ts
new file mode 100644
index 000000000000..e3232b810e06
--- /dev/null
+++ b/src/styles/optionAlternateTextPlatformStyles/index.ts
@@ -0,0 +1,5 @@
+import OptionAlternateTextPlatformStyles from './types';
+
+const optionAlternateTextPlatformStyles: OptionAlternateTextPlatformStyles = {};
+
+export default optionAlternateTextPlatformStyles;
diff --git a/src/styles/optionAlternateTextPlatformStyles/types.ts b/src/styles/optionAlternateTextPlatformStyles/types.ts
new file mode 100644
index 000000000000..b2e8e4745fff
--- /dev/null
+++ b/src/styles/optionAlternateTextPlatformStyles/types.ts
@@ -0,0 +1,5 @@
+import {TextStyle} from 'react-native';
+
+type OptionAlternateTextPlatformStyles = Partial>;
+
+export default OptionAlternateTextPlatformStyles;
diff --git a/src/styles/optionRowStyles/index.native.js b/src/styles/optionRowStyles/index.native.ts
similarity index 76%
rename from src/styles/optionRowStyles/index.native.js
rename to src/styles/optionRowStyles/index.native.ts
index b95382777f7e..11371509ce73 100644
--- a/src/styles/optionRowStyles/index.native.js
+++ b/src/styles/optionRowStyles/index.native.ts
@@ -1,3 +1,4 @@
+import OptionRowStyles from './types';
import styles from '../styles';
/**
@@ -7,7 +8,7 @@ import styles from '../styles';
* https://github.com/Expensify/App/issues/14148
*/
-const compactContentContainerStyles = styles.alignItemsCenter;
+const compactContentContainerStyles: OptionRowStyles = styles.alignItemsCenter;
export {
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/styles/optionRowStyles/index.js b/src/styles/optionRowStyles/index.ts
similarity index 53%
rename from src/styles/optionRowStyles/index.js
rename to src/styles/optionRowStyles/index.ts
index 2bef2a0cd094..fbeca3c702d9 100644
--- a/src/styles/optionRowStyles/index.js
+++ b/src/styles/optionRowStyles/index.ts
@@ -1,6 +1,7 @@
+import OptionRowStyles from './types';
import styles from '../styles';
-const compactContentContainerStyles = styles.alignItemsBaseline;
+const compactContentContainerStyles: OptionRowStyles = styles.alignItemsBaseline;
export {
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/styles/optionRowStyles/types.ts b/src/styles/optionRowStyles/types.ts
new file mode 100644
index 000000000000..c08174470701
--- /dev/null
+++ b/src/styles/optionRowStyles/types.ts
@@ -0,0 +1,6 @@
+import {CSSProperties} from 'react';
+import {ViewStyle} from 'react-native';
+
+type OptionRowStyles = CSSProperties | ViewStyle;
+
+export default OptionRowStyles;
diff --git a/src/styles/overflowXHidden/index.js b/src/styles/overflowXHidden/index.js
deleted file mode 100644
index 6cdd34a05eb0..000000000000
--- a/src/styles/overflowXHidden/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- overflowX: 'hidden',
-};
diff --git a/src/styles/overflowXHidden/index.native.js b/src/styles/overflowXHidden/index.native.js
deleted file mode 100644
index ff8b4c56321a..000000000000
--- a/src/styles/overflowXHidden/index.native.js
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/src/styles/overflowXHidden/index.native.ts b/src/styles/overflowXHidden/index.native.ts
new file mode 100644
index 000000000000..3a2f61893d94
--- /dev/null
+++ b/src/styles/overflowXHidden/index.native.ts
@@ -0,0 +1,5 @@
+import OverflowXHiddenStyles from './types';
+
+const overflowXHidden: OverflowXHiddenStyles = {};
+
+export default overflowXHidden;
diff --git a/src/styles/overflowXHidden/index.ts b/src/styles/overflowXHidden/index.ts
new file mode 100644
index 000000000000..6807be275be9
--- /dev/null
+++ b/src/styles/overflowXHidden/index.ts
@@ -0,0 +1,7 @@
+import OverflowXHiddenStyles from './types';
+
+const overflowXHidden: OverflowXHiddenStyles = {
+ overflowX: 'hidden',
+};
+
+export default overflowXHidden;
diff --git a/src/styles/overflowXHidden/types.ts b/src/styles/overflowXHidden/types.ts
new file mode 100644
index 000000000000..7ac572f0e651
--- /dev/null
+++ b/src/styles/overflowXHidden/types.ts
@@ -0,0 +1,5 @@
+import {CSSProperties} from 'react';
+
+type OverflowXHiddenStyles = Partial>;
+
+export default OverflowXHiddenStyles;
diff --git a/src/styles/pointerEventsAuto/index.js b/src/styles/pointerEventsAuto/index.js
deleted file mode 100644
index add748e52fe5..000000000000
--- a/src/styles/pointerEventsAuto/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- pointerEvents: 'auto',
-};
diff --git a/src/styles/pointerEventsAuto/index.native.js b/src/styles/pointerEventsAuto/index.native.js
deleted file mode 100644
index ff8b4c56321a..000000000000
--- a/src/styles/pointerEventsAuto/index.native.js
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/src/styles/pointerEventsAuto/index.native.ts b/src/styles/pointerEventsAuto/index.native.ts
new file mode 100644
index 000000000000..cbefa643d1e0
--- /dev/null
+++ b/src/styles/pointerEventsAuto/index.native.ts
@@ -0,0 +1,5 @@
+import PointerEventsAutoStyles from './types';
+
+const pointerEventsAuto: PointerEventsAutoStyles = {};
+
+export default pointerEventsAuto;
diff --git a/src/styles/pointerEventsAuto/index.ts b/src/styles/pointerEventsAuto/index.ts
new file mode 100644
index 000000000000..8abda90caafe
--- /dev/null
+++ b/src/styles/pointerEventsAuto/index.ts
@@ -0,0 +1,7 @@
+import PointerEventsAutoStyles from './types';
+
+const pointerEventsAuto: PointerEventsAutoStyles = {
+ pointerEvents: 'auto',
+};
+
+export default pointerEventsAuto;
diff --git a/src/styles/pointerEventsAuto/types.ts b/src/styles/pointerEventsAuto/types.ts
new file mode 100644
index 000000000000..7c9f0164c936
--- /dev/null
+++ b/src/styles/pointerEventsAuto/types.ts
@@ -0,0 +1,5 @@
+import {CSSProperties} from 'react';
+
+type PointerEventsAutoStyles = Partial>;
+
+export default PointerEventsAutoStyles;
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 011c24b03b90..45b3ba316e31 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -3645,7 +3645,7 @@ const styles = {
height: 40,
marginLeft: 12,
alignItems: 'center',
- overflowY: 'hidden',
+ overflow: 'hidden',
},
googlePillButtonContainer: {
@@ -3810,6 +3810,27 @@ const styles = {
transform: [{rotate: '90deg'}],
},
+ emojiStatusLHN: {
+ fontSize: 22,
+ },
+ sidebarStatusAvatarContainer: {
+ height: 44,
+ width: 84,
+ backgroundColor: themeColors.componentBG,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderRadius: 42,
+ paddingHorizontal: 2,
+ marginVertical: -2,
+ marginRight: -2,
+ },
+ sidebarStatusAvatar: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+
moneyRequestViewImage: {
...spacing.mh5,
...spacing.mv3,
@@ -3851,7 +3872,6 @@ const styles = {
...flex.flex1,
borderRadius: variables.componentBorderRadiusLarge,
},
-
userReportStatusEmoji: {
fontSize: variables.fontSizeNormal,
marginRight: 4,
diff --git a/src/styles/utilities/flex.js b/src/styles/utilities/flex.ts
similarity index 96%
rename from src/styles/utilities/flex.js
rename to src/styles/utilities/flex.ts
index aef7de698519..6c7541a3ef46 100644
--- a/src/styles/utilities/flex.js
+++ b/src/styles/utilities/flex.ts
@@ -1,3 +1,5 @@
+import {ViewStyle} from 'react-native';
+
/**
* Flex layout utility styles with Bootstrap inspired naming.
*
@@ -134,4 +136,4 @@ export default {
flexBasis0: {
flexBasis: 0,
},
-};
+} satisfies Record;
diff --git a/src/styles/utilities/overflow.js b/src/styles/utilities/overflow.js
index c190abfa912b..430525f99e65 100644
--- a/src/styles/utilities/overflow.js
+++ b/src/styles/utilities/overflow.js
@@ -18,8 +18,8 @@ export default {
overflow: 'scroll',
},
- overscrollBehaviorNone: {
- overscrollBehavior: 'none',
+ overscrollBehaviorXNone: {
+ overscrollBehaviorX: 'none',
},
overflowAuto,
diff --git a/src/styles/utilities/overflowAuto/index.js b/src/styles/utilities/overflowAuto/index.js
deleted file mode 100644
index 358f781077d7..000000000000
--- a/src/styles/utilities/overflowAuto/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- overflow: 'auto',
-};
diff --git a/src/styles/utilities/overflowAuto/index.native.js b/src/styles/utilities/overflowAuto/index.native.js
deleted file mode 100644
index 2075ce53e3e1..000000000000
--- a/src/styles/utilities/overflowAuto/index.native.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// Overflow auto doesn't exist in react-native so we'll default to overflow: visible
-export default {
- overflow: 'visible',
-};
diff --git a/src/styles/utilities/overflowAuto/index.native.ts b/src/styles/utilities/overflowAuto/index.native.ts
new file mode 100644
index 000000000000..34ee18db1d0a
--- /dev/null
+++ b/src/styles/utilities/overflowAuto/index.native.ts
@@ -0,0 +1,8 @@
+import OverflowAutoStyles from './types';
+
+// Overflow auto doesn't exist in react-native so we'll default to overflow: visible
+const overflowAuto: OverflowAutoStyles = {
+ overflow: 'visible',
+};
+
+export default overflowAuto;
diff --git a/src/styles/utilities/overflowAuto/index.ts b/src/styles/utilities/overflowAuto/index.ts
new file mode 100644
index 000000000000..0eb19068738f
--- /dev/null
+++ b/src/styles/utilities/overflowAuto/index.ts
@@ -0,0 +1,7 @@
+import OverflowAutoStyles from './types';
+
+const overflowAuto: OverflowAutoStyles = {
+ overflow: 'auto',
+};
+
+export default overflowAuto;
diff --git a/src/styles/utilities/overflowAuto/types.ts b/src/styles/utilities/overflowAuto/types.ts
new file mode 100644
index 000000000000..faba7c2cbdb8
--- /dev/null
+++ b/src/styles/utilities/overflowAuto/types.ts
@@ -0,0 +1,5 @@
+import {CSSProperties} from 'react';
+
+type OverflowAutoStyles = Pick;
+
+export default OverflowAutoStyles;
diff --git a/src/styles/utilities/userSelect/index.native.js b/src/styles/utilities/userSelect/index.native.ts
similarity index 52%
rename from src/styles/utilities/userSelect/index.native.js
rename to src/styles/utilities/userSelect/index.native.ts
index 0d1dfe97aa1d..0d1a34ef2473 100644
--- a/src/styles/utilities/userSelect/index.native.js
+++ b/src/styles/utilities/userSelect/index.native.ts
@@ -1,4 +1,6 @@
-export default {
+import UserSelectStyles from './types';
+
+const userSelect: UserSelectStyles = {
userSelectText: {
userSelect: 'text',
},
@@ -6,3 +8,5 @@ export default {
userSelect: 'none',
},
};
+
+export default userSelect;
diff --git a/src/styles/utilities/userSelect/index.js b/src/styles/utilities/userSelect/index.ts
similarity index 63%
rename from src/styles/utilities/userSelect/index.js
rename to src/styles/utilities/userSelect/index.ts
index c6b71170cf14..6b9f26131b5e 100644
--- a/src/styles/utilities/userSelect/index.js
+++ b/src/styles/utilities/userSelect/index.ts
@@ -1,4 +1,6 @@
-export default {
+import UserSelectStyles from './types';
+
+const userSelect: UserSelectStyles = {
userSelectText: {
userSelect: 'text',
WebkitUserSelect: 'text',
@@ -8,3 +10,5 @@ export default {
WebkitUserSelect: 'none',
},
};
+
+export default userSelect;
diff --git a/src/styles/utilities/userSelect/types.ts b/src/styles/utilities/userSelect/types.ts
new file mode 100644
index 000000000000..67a8c9c7b9b6
--- /dev/null
+++ b/src/styles/utilities/userSelect/types.ts
@@ -0,0 +1,5 @@
+import {CSSProperties} from 'react';
+
+type UserSelectStyles = Record<'userSelectText' | 'userSelectNone', Partial>>;
+
+export default UserSelectStyles;
diff --git a/src/styles/utilities/writingDirection.js b/src/styles/utilities/writingDirection.ts
similarity index 80%
rename from src/styles/utilities/writingDirection.js
rename to src/styles/utilities/writingDirection.ts
index d9c630c86912..1d9a32122373 100644
--- a/src/styles/utilities/writingDirection.js
+++ b/src/styles/utilities/writingDirection.ts
@@ -1,3 +1,4 @@
+import {TextStyle} from 'react-native';
/**
* Writing direction utility styles.
* Note: writingDirection isn't supported on Android. Unicode controls are being used for Android
@@ -10,4 +11,4 @@ export default {
ltr: {
writingDirection: 'ltr',
},
-};
+} satisfies Record;
diff --git a/src/global.d.ts b/src/types/global.d.ts
similarity index 60%
rename from src/global.d.ts
rename to src/types/global.d.ts
index 0dc745d5e0ea..1910b5a994b8 100644
--- a/src/global.d.ts
+++ b/src/types/global.d.ts
@@ -1,13 +1,13 @@
-import {OnyxKey, OnyxCollectionKey, OnyxValues} from './ONYXKEYS';
-
declare module '*.png' {
const value: import('react-native').ImageSourcePropType;
export default value;
}
+
declare module '*.jpg' {
const value: import('react-native').ImageSourcePropType;
export default value;
}
+
declare module '*.svg' {
import React from 'react';
import {SvgProps} from 'react-native-svg';
@@ -17,12 +17,3 @@ declare module '*.svg' {
}
declare module 'react-native-device-info/jest/react-native-device-info-mock';
-
-declare module 'react-native-onyx' {
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
- interface CustomTypeOptions {
- keys: OnyxKey;
- collectionKeys: OnyxCollectionKey;
- values: OnyxValues;
- }
-}
diff --git a/src/types/modules/react-native-key-command.d.ts b/src/types/modules/react-native-key-command.d.ts
new file mode 100644
index 000000000000..f93204891e84
--- /dev/null
+++ b/src/types/modules/react-native-key-command.d.ts
@@ -0,0 +1,30 @@
+declare module 'react-native-key-command' {
+ declare const constants = {
+ keyInputDownArrow: 'keyInputDownArrow',
+ keyInputEscape: 'keyInputEscape',
+ keyInputLeftArrow: 'keyInputLeftArrow',
+ keyInputRightArrow: 'keyInputRightArrow',
+ keyInputUpArrow: 'keyInputUpArrow',
+ keyInputEnter: 'keyInputEnter',
+ keyModifierCapsLock: 'keyModifierCapsLock',
+ keyModifierCommand: 'keyModifierCommand',
+ keyModifierControl: 'keyModifierControl',
+ keyModifierControlCommand: 'keyModifierControlCommand',
+ keyModifierControlOption: 'keyModifierControlOption',
+ keyModifierControlOptionCommand: 'keyModifierControlOptionCommand',
+ keyModifierNumericPad: 'keyModifierNumericPad',
+ keyModifierOption: 'keyModifierOption',
+ keyModifierOptionCommand: 'keyModifierOptionCommand',
+ keyModifierShift: 'keyModifierShift',
+ keyModifierShiftCommand: 'keyModifierShiftCommand',
+ keyModifierShiftControl: 'keyModifierShiftControl',
+ keyModifierAlternate: 'keyModifierAlternate',
+ } as const;
+
+ type KeyCommand = {input: string; modifierFlags: string};
+
+ declare function addListener(keyCommand: KeyCommand, callback: (keycommandEvent: KeyCommand, event: Event) => void): () => void;
+
+ // eslint-disable-next-line import/prefer-default-export
+ export {constants, addListener};
+}
diff --git a/src/types/modules/react-native-onyx.d.ts b/src/types/modules/react-native-onyx.d.ts
new file mode 100644
index 000000000000..4979f8fd0dbb
--- /dev/null
+++ b/src/types/modules/react-native-onyx.d.ts
@@ -0,0 +1,10 @@
+import {OnyxKey, OnyxCollectionKey, OnyxValues} from '../../ONYXKEYS';
+
+declare module 'react-native-onyx' {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface CustomTypeOptions {
+ keys: OnyxKey;
+ collectionKeys: OnyxCollectionKey;
+ values: OnyxValues;
+ }
+}
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index d52489c36da9..8ed25cb286b0 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -1,5 +1,3 @@
-// TODO: Remove this after CONST.ts is migrated to TS
-/* eslint-disable @typescript-eslint/no-duplicate-type-constituents */
import {ValueOf} from 'type-fest';
import CONST from '../../CONST';
diff --git a/src/types/onyx/WalletTransfer.ts b/src/types/onyx/WalletTransfer.ts
index c5d92250f91d..3dd28729ba96 100644
--- a/src/types/onyx/WalletTransfer.ts
+++ b/src/types/onyx/WalletTransfer.ts
@@ -9,8 +9,6 @@ type WalletTransfer = {
selectedAccountType?: string;
/** Type to filter the payment Method list */
- // TODO: Remove this after CONST.ts is migrated to TS
- // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
filterPaymentMethodType?: typeof CONST.PAYMENT_METHODS.DEBIT_CARD | typeof CONST.PAYMENT_METHODS.BANK_ACCOUNT;
/** Whether the success screen is shown to user. */
diff --git a/tests/unit/FileUtilsTest.js b/tests/unit/FileUtilsTest.js
index 5b6ad7fe3ad3..34dc0dfcf129 100644
--- a/tests/unit/FileUtilsTest.js
+++ b/tests/unit/FileUtilsTest.js
@@ -1,3 +1,4 @@
+import CONST from '../../src/CONST';
import DateUtils from '../../src/libs/DateUtils';
import * as FileUtils from '../../src/libs/fileDownload/FileUtils';
@@ -26,13 +27,13 @@ describe('FileUtils', () => {
it('should append current time to the end of the file name', () => {
const actualFileName = FileUtils.appendTimeToFileName('image.jpg');
const expectedFileName = `image-${DateUtils.getDBTime()}.jpg`;
- expect(actualFileName).toEqual(expectedFileName);
+ expect(actualFileName).toEqual(expectedFileName.replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_'));
});
it('should append current time to the end of the file name without extension', () => {
const actualFileName = FileUtils.appendTimeToFileName('image');
const expectedFileName = `image-${DateUtils.getDBTime()}`;
- expect(actualFileName).toEqual(expectedFileName);
+ expect(actualFileName).toEqual(expectedFileName.replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_'));
});
});
});