diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index bac7ab34920d..3884f88e4ff2 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -157,14 +157,7 @@ jobs: ruby-version: '2.7' bundler-cache: true - - uses: actions/cache@v3 - id: cache-pods - with: - path: ios/Pods - key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} - - name: Install cocoapods - if: steps.cache-pods.outputs.cache-hit != 'true' uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 with: timeout_minutes: 10 diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index b75ee2a402e4..cee2ad4c3b83 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -140,14 +140,7 @@ jobs: ruby-version: '2.7' bundler-cache: true - - uses: actions/cache@v3 - id: cache-pods - with: - path: ios/Pods - key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} - - name: Install cocoapods - if: steps.cache-pods.outputs.cache-hit != 'true' uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 with: timeout_minutes: 10 diff --git a/android/app/build.gradle b/android/app/build.gradle index 7c860d4ac340..1630538e4b8e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -106,8 +106,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001032706 - versionName "1.3.27-6" + versionCode 1001032802 + versionName "1.3.28-2" } splits { diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md index b8aac1e38baa..01f145dafbc6 100644 --- a/contributingGuides/FORMS.md +++ b/contributingGuides/FORMS.md @@ -114,10 +114,10 @@ Here's an example for a form that has two inputs, `routingNumber` and `accountNu function validate(values) { const errors = {}; if (!values.routingNumber) { - errors.routingNumber = props.translate(CONST.ERRORS.ROUTING_NUMBER); + errors.routingNumber = CONST.ERRORS.ROUTING_NUMBER; } if (!values.accountNumber) { - errors.accountNumber = props.translate(CONST.ERRORS.ACCOUNT_NUMBER); + errors.accountNumber = CONST.ERRORS.ACCOUNT_NUMBER; } return errors; } @@ -130,15 +130,15 @@ function validate(values) { let errors = {}; if (!ValidationUtils.isValidDisplayName(values.firstName)) { - errors = ErrorUtils.addErrorMessage(errors, 'firstName', props.translate('personalDetails.error.hasInvalidCharacter')); + errors = ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter'); } if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_FIRST_NAMES)) { - errors = ErrorUtils.addErrorMessage(errors, 'firstName', props.translate('personalDetails.error.containsReservedWord')); + errors = ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord'); } if (!ValidationUtils.isValidDisplayName(values.lastName)) { - errors.lastName = props.translate('personalDetails.error.hasInvalidCharacter'); + errors.lastName = 'personalDetails.error.hasInvalidCharacter'; } return errors; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a73349d4f9d5..f3a960f1a98c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.27 + 1.3.28 CFBundleSignature ???? CFBundleURLTypes @@ -32,7 +32,7 @@ CFBundleVersion - 1.3.27.6 + 1.3.28.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 98de86749f65..a2b4783ca4dd 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.27 + 1.3.28 CFBundleSignature ???? CFBundleVersion - 1.3.27.6 + 1.3.28.2 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6cb4235c58c8..048eeca5f76f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -662,6 +662,8 @@ PODS: - Firebase/Performance (= 8.8.0) - React-Core - RNFBApp + - RNFS (2.20.0): + - React-Core - RNGestureHandler (2.9.0): - React-Core - RNLocalize (2.2.6): @@ -809,6 +811,7 @@ DEPENDENCIES: - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)" - "RNFBPerf (from `../node_modules/@react-native-firebase/perf`)" + - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNLocalize (from `../node_modules/react-native-localize`) - RNPermissions (from `../node_modules/react-native-permissions`) @@ -999,6 +1002,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-firebase/crashlytics" RNFBPerf: :path: "../node_modules/@react-native-firebase/perf" + RNFS: + :path: "../node_modules/react-native-fs" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNLocalize: @@ -1120,6 +1125,7 @@ SPEC CHECKSUMS: RNFBApp: 729c0666395b1953198dc4a1ec6deb8fbe1c302e RNFBCrashlytics: 2061ca863e8e2fa1aae9b12477d7dfa8e88ca0f9 RNFBPerf: 389914cda4000fe0d996a752532a591132cbf3f9 + RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 RNPermissions: dcdb7b99796bbeda6975a6e79ad519c41b251b1c diff --git a/jest/setup.js b/jest/setup.js index 228f3a22f33b..f03c53540359 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -25,3 +25,9 @@ jest.spyOn(console, 'debug').mockImplementation((...params) => { // eslint-disable-next-line no-console console.log('DEBUG', ...params); }); + +// This mock is required for mocking file systems when running tests +jest.mock('react-native-fs', () => ({ + unlink: jest.fn(() => new Promise((res) => res())), + CachesDirectoryPath: jest.fn(), +})); diff --git a/package-lock.json b/package-lock.json index 369dd6f6e1b0..7d460c18d812 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.27-6", + "version": "1.3.28-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.27-6", + "version": "1.3.28-2", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -40,9 +40,8 @@ "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a1e97c5e236ae7bc10623da6db9847bbd91863ab", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#743108fd2428945c5a75baa2a027b5a4ab407b5b", "fbjs": "^3.0.2", - "html-entities": "^1.3.1", "htmlparser2": "^7.2.0", "jest-when": "^3.5.2", "localforage": "^1.10.0", @@ -68,6 +67,7 @@ "react-native-device-info": "^10.3.0", "react-native-document-picker": "^8.0.0", "react-native-fast-image": "^8.6.3", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "2.9.0", "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#6f436a06a3018cb49750bb110b89df75f6a865d5", "react-native-haptic-feedback": "^1.13.0", @@ -5335,11 +5335,6 @@ } } }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/html-entities": { - "version": "2.3.3", - "dev": true, - "license": "MIT" - }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { "version": "0.7.4", "dev": true, @@ -24054,13 +24049,13 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a1e97c5e236ae7bc10623da6db9847bbd91863ab", - "integrity": "sha512-24qOK8Sjsu0RgC0WGxPGzp/MFgHZubojgOJZqPJLa2PDDkpoli3BSFaUrtWsEOKOmP+HUYPdB7SYwjSew/0tmQ==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#743108fd2428945c5a75baa2a027b5a4ab407b5b", + "integrity": "sha512-RwY3fJGVDTanD9+shESDDHT4YCjQ9/hNtfOiWuoKNsTuJm5fkS/FPyh8kHj+gGm+6DzTtMPuRBsPDSKCFT8fXA==", "license": "MIT", "dependencies": { "classnames": "2.3.1", "clipboard": "2.0.4", - "html-entities": "^1.3.1", + "html-entities": "^2.3.3", "jquery": "3.6.0", "localforage": "^1.10.0", "lodash": "4.17.21", @@ -25878,8 +25873,9 @@ } }, "node_modules/html-entities": { - "version": "1.4.0", - "license": "MIT" + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz", + "integrity": "sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg==" }, "node_modules/html-escaper": { "version": "2.0.2", @@ -36206,6 +36202,24 @@ "react-native": ">0.62.0" } }, + "node_modules/react-native-fs": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz", + "integrity": "sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ==", + "dependencies": { + "base-64": "^0.1.0", + "utf8": "^3.0.0" + }, + "peerDependencies": { + "react-native": "*", + "react-native-windows": "*" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, "node_modules/react-native-gesture-handler": { "version": "2.9.0", "license": "MIT", @@ -41385,6 +41399,11 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -42229,11 +42248,6 @@ "dev": true, "license": "MIT" }, - "node_modules/webpack-dev-server/node_modules/html-entities": { - "version": "2.3.3", - "dev": true, - "license": "MIT" - }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.0.1", "dev": true, @@ -42407,11 +42421,6 @@ "strip-ansi": "^6.0.0" } }, - "node_modules/webpack-hot-middleware/node_modules/html-entities": { - "version": "2.3.3", - "dev": true, - "license": "MIT" - }, "node_modules/webpack-log": { "version": "2.0.0", "dev": true, @@ -46530,10 +46539,6 @@ "source-map": "^0.7.3" }, "dependencies": { - "html-entities": { - "version": "2.3.3", - "dev": true - }, "source-map": { "version": "0.7.4", "dev": true @@ -59286,13 +59291,13 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#a1e97c5e236ae7bc10623da6db9847bbd91863ab", - "integrity": "sha512-24qOK8Sjsu0RgC0WGxPGzp/MFgHZubojgOJZqPJLa2PDDkpoli3BSFaUrtWsEOKOmP+HUYPdB7SYwjSew/0tmQ==", - "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#a1e97c5e236ae7bc10623da6db9847bbd91863ab", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#743108fd2428945c5a75baa2a027b5a4ab407b5b", + "integrity": "sha512-RwY3fJGVDTanD9+shESDDHT4YCjQ9/hNtfOiWuoKNsTuJm5fkS/FPyh8kHj+gGm+6DzTtMPuRBsPDSKCFT8fXA==", + "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#743108fd2428945c5a75baa2a027b5a4ab407b5b", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", - "html-entities": "^1.3.1", + "html-entities": "^2.3.3", "jquery": "3.6.0", "localforage": "^1.10.0", "lodash": "4.17.21", @@ -60493,7 +60498,9 @@ } }, "html-entities": { - "version": "1.4.0" + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz", + "integrity": "sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg==" }, "html-escaper": { "version": "2.0.2" @@ -67500,6 +67507,15 @@ "dev": true, "requires": {} }, + "react-native-fs": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz", + "integrity": "sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ==", + "requires": { + "base-64": "^0.1.0", + "utf8": "^3.0.0" + } + }, "react-native-gesture-handler": { "version": "2.9.0", "requires": { @@ -70847,6 +70863,11 @@ "version": "1.2.0", "requires": {} }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -71472,10 +71493,6 @@ "version": "2.0.19", "dev": true }, - "html-entities": { - "version": "2.3.3", - "dev": true - }, "ipaddr.js": { "version": "2.0.1", "dev": true @@ -71585,12 +71602,6 @@ "ansi-html-community": "0.0.8", "html-entities": "^2.1.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "html-entities": { - "version": "2.3.3", - "dev": true - } } }, "webpack-log": { diff --git a/package.json b/package.json index 6c9d9b946898..a7b899d37204 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.27-6", + "version": "1.3.28-2", "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.", @@ -76,9 +76,8 @@ "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a1e97c5e236ae7bc10623da6db9847bbd91863ab", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#743108fd2428945c5a75baa2a027b5a4ab407b5b", "fbjs": "^3.0.2", - "html-entities": "^1.3.1", "htmlparser2": "^7.2.0", "jest-when": "^3.5.2", "localforage": "^1.10.0", @@ -104,6 +103,7 @@ "react-native-device-info": "^10.3.0", "react-native-document-picker": "^8.0.0", "react-native-fast-image": "^8.6.3", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "2.9.0", "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#6f436a06a3018cb49750bb110b89df75f6a865d5", "react-native-haptic-feedback": "^1.13.0", diff --git a/src/components/AttachmentView.js b/src/components/AttachmentView.js index e5d4fd1bf299..731e0c0554bb 100755 --- a/src/components/AttachmentView.js +++ b/src/components/AttachmentView.js @@ -1,5 +1,5 @@ import React, {memo, useState} from 'react'; -import {View, ActivityIndicator, Pressable} from 'react-native'; +import {View, ActivityIndicator} from 'react-native'; import _ from 'underscore'; import PropTypes from 'prop-types'; import Str from 'expensify-common/lib/str'; @@ -15,6 +15,7 @@ import Tooltip from './Tooltip'; import themeColors from '../styles/themes/default'; import variables from '../styles/variables'; import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL'; +import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; const propTypes = { /** Whether source url requires authentication */ @@ -93,13 +94,15 @@ function AttachmentView(props) { /> ); return props.onPress ? ( - {children} - + ) : ( children ); @@ -117,13 +120,15 @@ function AttachmentView(props) { /> ); return props.onPress ? ( - {children} - + ) : ( children ); diff --git a/src/components/BaseMiniContextMenuItem.js b/src/components/BaseMiniContextMenuItem.js index 67430902a947..0393b94620d2 100644 --- a/src/components/BaseMiniContextMenuItem.js +++ b/src/components/BaseMiniContextMenuItem.js @@ -1,4 +1,4 @@ -import {Pressable, View} from 'react-native'; +import {View} from 'react-native'; import React from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; @@ -7,6 +7,7 @@ import * as StyleUtils from '../styles/StyleUtils'; import getButtonState from '../libs/getButtonState'; import variables from '../styles/variables'; import Tooltip from './Tooltip'; +import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; const propTypes = { /** @@ -49,9 +50,8 @@ const defaultProps = { function BaseMiniContextMenuItem(props) { return ( - [ @@ -64,7 +64,7 @@ function BaseMiniContextMenuItem(props) { {_.isFunction(props.children) ? props.children(pressableState) : props.children} )} - + ); } diff --git a/src/components/DotIndicatorMessage.js b/src/components/DotIndicatorMessage.js index 197c111c0ee3..ac550f34de3f 100644 --- a/src/components/DotIndicatorMessage.js +++ b/src/components/DotIndicatorMessage.js @@ -17,7 +17,7 @@ const propTypes = { * timestamp: 'message', * } */ - messages: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.array, PropTypes.string])), + messages: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))])), // The type of message, 'error' shows a red dot, 'success' shows a green dot type: PropTypes.oneOf(['error', 'success']).isRequired, diff --git a/src/components/Form.js b/src/components/Form.js index afc50d24763c..9191979ac671 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -96,7 +96,7 @@ function Form(props) { const inputRefs = useRef({}); const touchedInputs = useRef({}); - const {validate, translate, onSubmit, children} = props; + const {validate, onSubmit, children} = props; /** * @param {Object} values - An object containing the value of each inputID, e.g. {inputID1: value1, inputID2: value2} @@ -127,7 +127,7 @@ function Form(props) { } // Add a validation error here because it is a string value that contains HTML characters - validationErrors[inputID] = translate('common.error.invalidCharacter'); + validationErrors[inputID] = 'common.error.invalidCharacter'; }); if (!_.isObject(validationErrors)) { @@ -142,7 +142,7 @@ function Form(props) { return touchedInputErrors; }, - [errors, touchedInputs, props.formID, validate, translate], + [errors, touchedInputs, props.formID, validate], ); useEffect(() => { diff --git a/src/components/FormHelpMessage.js b/src/components/FormHelpMessage.js index 24d78267766b..df8befe5af30 100644 --- a/src/components/FormHelpMessage.js +++ b/src/components/FormHelpMessage.js @@ -12,7 +12,7 @@ import * as Localize from '../libs/Localize'; const propTypes = { /** Error or hint text. Ignored when children is not empty */ - message: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** Children to render next to dot indicator */ children: PropTypes.node, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index c56e529e1a94..a77c425c0ed2 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -9,6 +9,7 @@ import htmlRendererPropTypes from './htmlRendererPropTypes'; import withCurrentUserPersonalDetails from '../../withCurrentUserPersonalDetails'; import personalDetailsPropType from '../../../pages/personalDetailsPropType'; import * as StyleUtils from '../../../styles/StyleUtils'; +import TextLink from '../../TextLink'; const propTypes = { ...htmlRendererPropTypes, @@ -30,22 +31,23 @@ function MentionUserRenderer(props) { const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']); // We need to remove the leading @ from data as it is not part of the login - const loginWhithoutLeadingAt = props.tnode.data.slice(1); + const loginWithoutLeadingAt = props.tnode.data.slice(1); - const isOurMention = loginWhithoutLeadingAt === props.currentUserPersonalDetails.login; + const isOurMention = loginWithoutLeadingAt === props.currentUserPersonalDetails.login; return ( - - + showUserDetails(loginWhithoutLeadingAt)} + onPress={() => showUserDetails(loginWithoutLeadingAt)} > - + ); diff --git a/src/components/Pressable/PressableWithFeedback.js b/src/components/Pressable/PressableWithFeedback.js index 4516a57a85d3..94f10a380bb9 100644 --- a/src/components/Pressable/PressableWithFeedback.js +++ b/src/components/Pressable/PressableWithFeedback.js @@ -78,7 +78,7 @@ const PressableWithFeedback = forwardRef((props, ref) => { ...(state.focused ? StyleUtils.parseStyleAsArray(props.focusStyle, state) : []), ]} > - {props.children} + {_.isFunction(props.children) ? props.children(state) : props.children} )} diff --git a/src/components/PressableWithDelayToggle.js b/src/components/PressableWithDelayToggle.js index 57f561ec91ac..f29345ef8d95 100644 --- a/src/components/PressableWithDelayToggle.js +++ b/src/components/PressableWithDelayToggle.js @@ -98,9 +98,10 @@ function PressableWithDelayToggle(props) { <> {props.isDelayButtonStateComplete && props.textChecked ? props.textChecked : props.text} +   - [styles.emojiReactionBubble, styles.userSelectNone, StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, false, props.isContextMenu)]} onPress={Session.checkIfActionIsAllowed(onPress)} // Prevent text input blur when Add reaction is clicked onMouseDown={(e) => e.preventDefault()} + accessibilityLabel={props.translate('emojiReactions.addReactionTooltip')} + accessibilityRole="button" + // disable dimming + pressDimmingValue={1} > {({hovered, pressed}) => ( <> @@ -91,7 +96,7 @@ function AddReactionBubble(props) { )} - + ); } diff --git a/src/components/ReportActionItem/RenameAction.js b/src/components/ReportActionItem/RenameAction.js index 369f4da10b39..5e81abd0917b 100644 --- a/src/components/ReportActionItem/RenameAction.js +++ b/src/components/ReportActionItem/RenameAction.js @@ -5,16 +5,22 @@ import Text from '../Text'; import styles from '../../styles/styles'; import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; +import compose from '../../libs/compose'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '../withCurrentUserPersonalDetails'; const propTypes = { /** All the data of the action */ action: PropTypes.shape(reportActionPropTypes).isRequired, ...withLocalizePropTypes, + ...withCurrentUserPersonalDetailsPropTypes, }; function RenameAction(props) { - const displayName = lodashGet(props.action, ['message', 0, 'text']); + const currentUserAccountID = lodashGet(props.currentUserPersonalDetails, 'accountID', ''); + const userDisplayName = lodashGet(props.action, ['person', 0, 'text']); + const actorAccountID = lodashGet(props.action, 'actorAccountID', ''); + const displayName = actorAccountID === currentUserAccountID ? `${props.translate('common.you')}` : `${userDisplayName}`; const oldName = lodashGet(props.action, 'originalMessage.oldName', ''); const newName = lodashGet(props.action, 'originalMessage.newName', ''); @@ -29,4 +35,4 @@ function RenameAction(props) { RenameAction.propTypes = propTypes; RenameAction.displayName = 'RenameAction'; -export default withLocalize(RenameAction); +export default compose(withLocalize, withCurrentUserPersonalDetails)(RenameAction); diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 2f4af1ae2e92..de233b8d96d6 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -12,7 +12,7 @@ const propTypes = { disabled: PropTypes.bool, /** Error text to show */ - errorText: PropTypes.string, + errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), ...withLocalizePropTypes, diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index 45720ceb8c47..2e278bab5d69 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -17,7 +17,7 @@ const propTypes = { placeholder: PropTypes.string, /** Error text to display */ - errorText: PropTypes.string, + errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** Icon to display in right side of text input */ icon: PropTypes.func, diff --git a/src/components/TextLink.js b/src/components/TextLink.js index 07690600cb0d..f498fbe16bbd 100644 --- a/src/components/TextLink.js +++ b/src/components/TextLink.js @@ -31,6 +31,7 @@ const defaultProps = { }; function TextLink(props) { + const rest = _.omit(props, _.keys(propTypes)); const additionalStyles = _.isArray(props.style) ? props.style : [props.style]; /** @@ -64,6 +65,8 @@ function TextLink(props) { onPress={openLink} onMouseDown={props.onMouseDown} onKeyDown={openLinkIfEnterKeyPressed} + // eslint-disable-next-line react/jsx-props-no-spreading + {...rest} > {props.children} diff --git a/src/components/UserDetailsTooltip/index.js b/src/components/UserDetailsTooltip/index.js index 493b40a0a5e1..5dd2fa1c5785 100644 --- a/src/components/UserDetailsTooltip/index.js +++ b/src/components/UserDetailsTooltip/index.js @@ -9,6 +9,7 @@ import Tooltip from '../Tooltip'; import {propTypes, defaultProps} from './userDetailsTooltipPropTypes'; import styles from '../../styles/styles'; import ONYXKEYS from '../../ONYXKEYS'; +import * as UserUtils from '../../libs/UserUtils'; function UserDetailsTooltip(props) { const userDetails = lodashGet(props.personalDetailsList, props.accountID, props.fallbackUserDetails); @@ -18,7 +19,7 @@ function UserDetailsTooltip(props) { diff --git a/src/components/withKeyboardState.js b/src/components/withKeyboardState.js index c5a72f752003..667e8865a0e3 100755 --- a/src/components/withKeyboardState.js +++ b/src/components/withKeyboardState.js @@ -68,4 +68,4 @@ export default function withKeyboardState(WrappedComponent) { return WithKeyboardState; } -export {KeyboardStateProvider, keyboardStatePropTypes}; +export {KeyboardStateProvider, keyboardStatePropTypes, KeyboardStateContext}; diff --git a/src/components/withLocalize.js b/src/components/withLocalize.js index 4cbdda876231..def7110c1b40 100755 --- a/src/components/withLocalize.js +++ b/src/components/withLocalize.js @@ -179,4 +179,4 @@ export default function withLocalize(WrappedComponent) { return WithLocalize; } -export {withLocalizePropTypes, Provider as LocaleContextProvider}; +export {withLocalizePropTypes, Provider as LocaleContextProvider, LocaleContext}; diff --git a/src/hooks/useKeyboardState.js b/src/hooks/useKeyboardState.js new file mode 100644 index 000000000000..8b57fb60f2b6 --- /dev/null +++ b/src/hooks/useKeyboardState.js @@ -0,0 +1,11 @@ +import {useContext} from 'react'; +import {KeyboardStateContext} from '../components/withKeyboardState'; + +/** + * Hook for getting current state of keyboard + * whether or not the keyboard is open + * @returns {Object} + */ +export default function useKeyboardState() { + return useContext(KeyboardStateContext); +} diff --git a/src/hooks/useLocalize.js b/src/hooks/useLocalize.js new file mode 100644 index 000000000000..9ad5048729bd --- /dev/null +++ b/src/hooks/useLocalize.js @@ -0,0 +1,6 @@ +import {useContext} from 'react'; +import {LocaleContext} from '../components/withLocalize'; + +export default function useLocalize() { + return useContext(LocaleContext); +} diff --git a/src/languages/en.js b/src/languages/en.js index ed205f6576a0..8cde382c49c8 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1244,6 +1244,7 @@ export default { pleaseEnterRoomName: 'Please enter a room name', pleaseSelectWorkspace: 'Please select a workspace', renamedRoomAction: ({oldName, newName}) => ` renamed this room from ${oldName} to ${newName}`, + roomRenamedTo: ({newName}) => `Room renamed to ${newName}`, social: 'social', selectAWorkspace: 'Select a workspace', growlMessageOnRenameError: 'Unable to rename policy room, please check your connection and try again.', diff --git a/src/languages/es.js b/src/languages/es.js index e2a48a37f5f9..791ec5946c4d 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1250,6 +1250,7 @@ export default { pleaseEnterRoomName: 'Por favor, escribe el nombre de una sala', pleaseSelectWorkspace: 'Por favor, selecciona un espacio de trabajo', renamedRoomAction: ({oldName, newName}) => ` cambió el nombre de la sala de ${oldName} a ${newName}`, + roomRenamedTo: ({newName}) => `Sala renombrada a ${newName}`, social: 'social', selectAWorkspace: 'Seleccionar un espacio de trabajo', growlMessageOnRenameError: 'No se ha podido cambiar el nombre del espacio de trabajo, por favor, comprueba tu conexión e inténtalo de nuevo.', diff --git a/src/libs/ErrorUtils.js b/src/libs/ErrorUtils.js index 03a26d8fa14c..fdeae7809f58 100644 --- a/src/libs/ErrorUtils.js +++ b/src/libs/ErrorUtils.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import CONST from '../CONST'; import DateUtils from './DateUtils'; +import * as Localize from './Localize'; /** * @param {Object} response @@ -91,18 +92,23 @@ function getLatestErrorField(onyxData, fieldName) { * Method used to generate error message for given inputID * @param {Object} errors - An object containing current errors in the form * @param {String} inputID - * @param {String} message - Message to assign to the inputID errors + * @param {String|Array} message - Message to assign to the inputID errors * */ function addErrorMessage(errors, inputID, message) { - const errorList = errors; if (!message || !inputID) { return; } + + const errorList = errors; + const translatedMessage = Localize.translateIfPhraseKey(message); + if (_.isEmpty(errorList[inputID])) { - errorList[inputID] = message; + errorList[inputID] = [translatedMessage, {isTranslated: true}]; + } else if (_.isString(errorList[inputID])) { + errorList[inputID] = [`${errorList[inputID]}\n${translatedMessage}`, {isTranslated: true}]; } else { - errorList[inputID] = `${errorList[inputID]}\n${message}`; + errorList[inputID][0] = `${errorList[inputID][0]}\n${translatedMessage}`; } } diff --git a/src/libs/Localize/index.js b/src/libs/Localize/index.js index acbedc4b586f..9878873377b8 100644 --- a/src/libs/Localize/index.js +++ b/src/libs/Localize/index.js @@ -98,9 +98,20 @@ function translateLocal(phrase, variables) { * @returns {String} */ function translateIfPhraseKey(message) { + if (_.isEmpty(message)) { + return ''; + } + try { // check if error message has a variable parameter const [phrase, variables] = _.isArray(message) ? message : [message]; + + // This condition checks if the error is already translated. For example, if there are multiple errors per input, we handle translation in ErrorUtils.addErrorMessage due to the inability to concatenate error keys. + + if (variables && variables.isTranslated) { + return phrase; + } + return translateLocal(phrase, variables); } catch (error) { return message; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c4119cc26fc1..498c519661f2 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -726,6 +726,16 @@ function getIcons(report, personalDetails, defaultIcon = null, isPayer = false) return [actorIcon]; } + if (isTaskReport(report)) { + const ownerEmail = report.ownerEmail || ''; + const ownerIcon = { + source: UserUtils.getAvatar(lodashGet(personalDetails, [ownerEmail, 'avatar']), ownerEmail), + name: ownerEmail, + type: CONST.ICON_TYPE_AVATAR, + }; + + return [ownerIcon]; + } if (isDomainRoom(report)) { result.source = Expensicons.DomainRoomAvatar; return [result]; diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 87e8b97d0bd0..7da1017f2d9e 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -256,7 +256,6 @@ function getOptionData(reportID) { result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participants || []); result.hasOutstandingIOU = report.hasOutstandingIOU; result.parentReportID = report.parentReportID || null; - const parentReport = result.parentReportID ? allReports[`${ONYXKEYS.COLLECTION.REPORT}${result.parentReportID}`] : null; const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; const subtitle = ReportUtils.getChatRoomSubtitle(report); @@ -300,7 +299,13 @@ function getOptionData(reportID) { } if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport) && !result.isArchivedRoom) { - result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + const lastAction = visibleReportActionItems[report.reportID]; + if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + const newName = lodashGet(lastAction, 'originalMessage.newName', ''); + result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName}); + } else { + result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); + } } else { if (!lastMessageText) { // Here we get the beginning of chat history message and append the display name for each user, adding pronouns if there are any. @@ -340,7 +345,7 @@ function getOptionData(reportID) { result.subtitle = subtitle; result.participantsList = participantPersonalDetailList; - result.icons = ReportUtils.getIcons(result.isTaskReport ? parentReport : report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.login), true); + result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.login), true); result.searchText = OptionsListUtils.getSearchText(report, reportName, participantPersonalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); result.displayNamesWithTooltips = displayNamesWithTooltips; return result; diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index 7cefd11ec799..4778669840a8 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -5,7 +5,6 @@ import {parsePhoneNumber} from 'awesome-phonenumber'; import CONST from '../CONST'; import * as CardUtils from './CardUtils'; import * as LoginUtils from './LoginUtils'; -import * as Localize from './Localize'; /** * Implements the Luhn Algorithm, a checksum formula used to validate credit card @@ -225,22 +224,22 @@ function meetsMaximumAgeRequirement(date) { * @param {String} date * @param {Number} minimumAge * @param {Number} maximumAge - * @returns {String} + * @returns {String|Array} */ function getAgeRequirementError(date, minimumAge, maximumAge) { const recentDate = moment().startOf('day').subtract(minimumAge, 'years'); const longAgoDate = moment().startOf('day').subtract(maximumAge, 'years'); const testDate = moment(date); if (!testDate.isValid()) { - return Localize.translateLocal('common.error.dateInvalid'); + return 'common.error.dateInvalid'; } if (testDate.isBetween(longAgoDate, recentDate, undefined, '[]')) { return ''; } if (testDate.isSameOrAfter(recentDate)) { - return Localize.translateLocal('privatePersonalDetails.error.dateShouldBeBefore', {dateString: recentDate.format(CONST.DATE.MOMENT_FORMAT_STRING)}); + return ['privatePersonalDetails.error.dateShouldBeBefore', {dateString: recentDate.format(CONST.DATE.MOMENT_FORMAT_STRING)}]; } - return Localize.translateLocal('privatePersonalDetails.error.dateShouldBeAfter', {dateString: longAgoDate.format(CONST.DATE.MOMENT_FORMAT_STRING)}); + return ['privatePersonalDetails.error.dateShouldBeAfter', {dateString: longAgoDate.format(CONST.DATE.MOMENT_FORMAT_STRING)}]; } /** diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index b362436def5c..9c7dde0c7aa4 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -408,6 +408,48 @@ function updateAvatar(file) { }, ]; + const accountID = lodashGet(personalDetails, [currentUserEmail, 'accountID'], ''); + if (accountID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [accountID]: { + avatar: file.uri, + errorFields: { + avatar: null, + }, + pendingFields: { + avatar: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [accountID]: { + pendingFields: { + avatar: null, + }, + }, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [accountID]: { + avatar: personalDetails[currentUserEmail].avatar, + pendingFields: { + avatar: null, + }, + }, + }, + }); + } + API.write('UpdateUserAvatar', {file}, {optimisticData, successData, failureData}); } @@ -418,34 +460,52 @@ function deleteAvatar() { // We want to use the old dot avatar here as this affects both platforms. const defaultAvatar = UserUtils.getDefaultAvatarURL(currentUserEmail); - API.write( - 'DeleteUserAvatar', - {}, + const optimisticData = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS, - value: { - [currentUserEmail]: { - avatar: defaultAvatar, - }, - }, + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + avatar: defaultAvatar, }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS, - value: { - [currentUserEmail]: { - avatar: personalDetails[currentUserEmail].avatar, - }, - }, + }, + }, + ]; + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [currentUserEmail]: { + avatar: personalDetails[currentUserEmail].avatar, }, - ], + }, }, - ); + ]; + + const accountID = lodashGet(personalDetails, [currentUserEmail, 'accountID'], ''); + if (accountID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [personalDetails[currentUserEmail].accountID]: { + avatar: defaultAvatar, + }, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [personalDetails[currentUserEmail].accountID]: { + avatar: personalDetails[currentUserEmail].avatar, + }, + }, + }); + } + + API.write('DeleteUserAvatar', {}, {optimisticData, failureData}); } /** diff --git a/src/libs/actions/Session/clearCache/index.js b/src/libs/actions/Session/clearCache/index.js new file mode 100644 index 000000000000..9ccd0193cfbd --- /dev/null +++ b/src/libs/actions/Session/clearCache/index.js @@ -0,0 +1,5 @@ +function clearStorage() { + return new Promise((res) => res()); +} + +export default clearStorage; diff --git a/src/libs/actions/Session/clearCache/index.native.js b/src/libs/actions/Session/clearCache/index.native.js new file mode 100644 index 000000000000..3bd647dbf8fb --- /dev/null +++ b/src/libs/actions/Session/clearCache/index.native.js @@ -0,0 +1,8 @@ +import {CachesDirectoryPath, unlink} from 'react-native-fs'; + +function clearStorage() { + // `unlink` is used to delete the caches directory + return unlink(CachesDirectoryPath); +} + +export default clearStorage; diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index b53d7811ef8d..f59cf5972c9e 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {Linking} from 'react-native'; +import clearCache from './clearCache'; import ONYXKEYS from '../../../ONYXKEYS'; import redirectToSignIn from '../SignInRedirect'; import CONFIG from '../../../CONFIG'; @@ -72,7 +73,9 @@ function signOut() { partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, shouldRetry: false, }); - + clearCache().then(() => { + Log.info('Cleared all chache data', true, {}, true); + }); Timing.clearData(); } diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index b90a22ceac8a..12d98c664e7e 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -360,7 +360,7 @@ function addNewContactMethodAndNavigate(contactMethod, password) { ]; API.write('AddNewContactMethod', {partnerUserID: contactMethod, password}, {optimisticData, successData, failureData}); - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); } /** diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index 22dd50e2ab4a..ac3d2ed43ea7 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -123,47 +123,47 @@ class AdditionalDetailsStep extends React.Component { const errors = {}; if (_.isEmpty(values[INPUT_IDS.LEGAL_FIRST_NAME])) { - errors[INPUT_IDS.LEGAL_FIRST_NAME] = this.props.translate(this.errorTranslationKeys.legalFirstName); + errors[INPUT_IDS.LEGAL_FIRST_NAME] = this.errorTranslationKeys.legalFirstName; } if (_.isEmpty(values[INPUT_IDS.LEGAL_LAST_NAME])) { - errors[INPUT_IDS.LEGAL_LAST_NAME] = this.props.translate(this.errorTranslationKeys.legalLastName); + errors[INPUT_IDS.LEGAL_LAST_NAME] = this.errorTranslationKeys.legalLastName; } if (!ValidationUtils.isValidPastDate(values[INPUT_IDS.DOB]) || !ValidationUtils.meetsMaximumAgeRequirement(values[INPUT_IDS.DOB])) { - ErrorUtils.addErrorMessage(errors, INPUT_IDS.DOB, this.props.translate(this.errorTranslationKeys.dob)); + ErrorUtils.addErrorMessage(errors, INPUT_IDS.DOB, this.errorTranslationKeys.dob); } else if (!ValidationUtils.meetsMinimumAgeRequirement(values[INPUT_IDS.DOB])) { - ErrorUtils.addErrorMessage(errors, INPUT_IDS.DOB, this.props.translate(this.errorTranslationKeys.age)); + ErrorUtils.addErrorMessage(errors, INPUT_IDS.DOB, this.errorTranslationKeys.age); } if (!ValidationUtils.isValidAddress(values[INPUT_IDS.ADDRESS.street]) || _.isEmpty(values[INPUT_IDS.ADDRESS.street])) { - errors[INPUT_IDS.ADDRESS.street] = this.props.translate('bankAccount.error.addressStreet'); + errors[INPUT_IDS.ADDRESS.street] = 'bankAccount.error.addressStreet'; } if (_.isEmpty(values[INPUT_IDS.ADDRESS.city])) { - errors[INPUT_IDS.ADDRESS.city] = this.props.translate('bankAccount.error.addressCity'); + errors[INPUT_IDS.ADDRESS.city] = 'bankAccount.error.addressCity'; } if (_.isEmpty(values[INPUT_IDS.ADDRESS.state])) { - errors[INPUT_IDS.ADDRESS.state] = this.props.translate('bankAccount.error.addressState'); + errors[INPUT_IDS.ADDRESS.state] = 'bankAccount.error.addressState'; } if (!ValidationUtils.isValidZipCode(values[INPUT_IDS.ADDRESS.zipCode])) { - errors[INPUT_IDS.ADDRESS.zipCode] = this.props.translate('bankAccount.error.zipCode'); + errors[INPUT_IDS.ADDRESS.zipCode] = 'bankAccount.error.zipCode'; } if (!ValidationUtils.isValidUSPhone(values[INPUT_IDS.PHONE_NUMBER], true)) { - errors[INPUT_IDS.PHONE_NUMBER] = this.props.translate(this.errorTranslationKeys.phoneNumber); + errors[INPUT_IDS.PHONE_NUMBER] = this.errorTranslationKeys.phoneNumber; } // this.props.walletAdditionalDetails stores errors returned by the server. If the server returns an SSN error // then the user needs to provide the full 9 digit SSN. if (this.props.walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.SSN) { if (!ValidationUtils.isValidSSNFullNine(values[INPUT_IDS.SSN])) { - errors[INPUT_IDS.SSN] = this.props.translate(this.errorTranslationKeys.ssnFull9); + errors[INPUT_IDS.SSN] = this.errorTranslationKeys.ssnFull9; } } else if (!ValidationUtils.isValidSSNLastFour(values[INPUT_IDS.SSN])) { - errors[INPUT_IDS.SSN] = this.props.translate(this.errorTranslationKeys.ssn); + errors[INPUT_IDS.SSN] = this.errorTranslationKeys.ssn; } return errors; diff --git a/src/pages/ReimbursementAccount/ACHContractStep.js b/src/pages/ReimbursementAccount/ACHContractStep.js index c0d0f2c1af85..2e0979b00253 100644 --- a/src/pages/ReimbursementAccount/ACHContractStep.js +++ b/src/pages/ReimbursementAccount/ACHContractStep.js @@ -51,38 +51,38 @@ function ACHContractStep(props) { _.each(requiredFields, (inputKey) => { if (!ValidationUtils.isRequiredFulfilled(values[`beneficialOwner_${ownerKey}_${inputKey}`])) { const errorKey = errorKeys[inputKey] || inputKey; - errors[`beneficialOwner_${ownerKey}_${inputKey}`] = props.translate(`bankAccount.error.${errorKey}`); + errors[`beneficialOwner_${ownerKey}_${inputKey}`] = `bankAccount.error.${errorKey}`; } }); if (values[`beneficialOwner_${ownerKey}_dob`]) { if (!ValidationUtils.meetsMinimumAgeRequirement(values[`beneficialOwner_${ownerKey}_dob`])) { - errors[`beneficialOwner_${ownerKey}_dob`] = props.translate('bankAccount.error.age'); + errors[`beneficialOwner_${ownerKey}_dob`] = 'bankAccount.error.age'; } else if (!ValidationUtils.meetsMaximumAgeRequirement(values[`beneficialOwner_${ownerKey}_dob`])) { - errors[`beneficialOwner_${ownerKey}_dob`] = props.translate('bankAccount.error.dob'); + errors[`beneficialOwner_${ownerKey}_dob`] = 'bankAccount.error.dob'; } } if (values[`beneficialOwner_${ownerKey}_ssnLast4`] && !ValidationUtils.isValidSSNLastFour(values[`beneficialOwner_${ownerKey}_ssnLast4`])) { - errors[`beneficialOwner_${ownerKey}_ssnLast4`] = props.translate('bankAccount.error.ssnLast4'); + errors[`beneficialOwner_${ownerKey}_ssnLast4`] = 'bankAccount.error.ssnLast4'; } if (values[`beneficialOwner_${ownerKey}_street`] && !ValidationUtils.isValidAddress(values[`beneficialOwner_${ownerKey}_street`])) { - errors[`beneficialOwner_${ownerKey}_street`] = props.translate('bankAccount.error.addressStreet'); + errors[`beneficialOwner_${ownerKey}_street`] = 'bankAccount.error.addressStreet'; } if (values[`beneficialOwner_${ownerKey}_zipCode`] && !ValidationUtils.isValidZipCode(values[`beneficialOwner_${ownerKey}_zipCode`])) { - errors[`beneficialOwner_${ownerKey}_zipCode`] = props.translate('bankAccount.error.zipCode'); + errors[`beneficialOwner_${ownerKey}_zipCode`] = 'bankAccount.error.zipCode'; } }); } if (!ValidationUtils.isRequiredFulfilled(values.acceptTermsAndConditions)) { - errors.acceptTermsAndConditions = props.translate('common.error.acceptTerms'); + errors.acceptTermsAndConditions = 'common.error.acceptTerms'; } if (!ValidationUtils.isRequiredFulfilled(values.certifyTrueInformation)) { - errors.certifyTrueInformation = props.translate('beneficialOwnersStep.error.certify'); + errors.certifyTrueInformation = 'beneficialOwnersStep.error.certify'; } return errors; diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js index f8eeb6628495..5e8f46be00e5 100644 --- a/src/pages/ReimbursementAccount/BankAccountManualStep.js +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -41,13 +41,13 @@ class BankAccountManualStep extends React.Component { !values.accountNumber || (!CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) && !CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER.test(values.accountNumber.trim())) ) { - errorFields.accountNumber = this.props.translate('bankAccount.error.accountNumber'); + errorFields.accountNumber = 'bankAccount.error.accountNumber'; } if (!routingNumber || !CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(routingNumber) || !ValidationUtils.isValidRoutingNumber(routingNumber)) { - errorFields.routingNumber = this.props.translate('bankAccount.error.routingNumber'); + errorFields.routingNumber = 'bankAccount.error.routingNumber'; } if (!values.acceptTerms) { - errorFields.acceptTerms = this.props.translate('common.error.acceptTerms'); + errorFields.acceptTerms = 'common.error.acceptTerms'; } return errorFields; diff --git a/src/pages/ReimbursementAccount/BankAccountPlaidStep.js b/src/pages/ReimbursementAccount/BankAccountPlaidStep.js index 3af673084976..707828436d91 100644 --- a/src/pages/ReimbursementAccount/BankAccountPlaidStep.js +++ b/src/pages/ReimbursementAccount/BankAccountPlaidStep.js @@ -49,7 +49,7 @@ class BankAccountPlaidStep extends React.Component { validate(values) { const errorFields = {}; if (!values.acceptTerms) { - errorFields.acceptTerms = this.props.translate('common.error.acceptTerms'); + errorFields.acceptTerms = 'common.error.acceptTerms'; } return errorFields; diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 1265469abd26..e44d0562b58e 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -83,53 +83,53 @@ class CompanyStep extends React.Component { const errors = {}; if (!values.companyName) { - errors.companyName = this.props.translate('bankAccount.error.companyName'); + errors.companyName = 'bankAccount.error.companyName'; } if (!values.addressStreet || !ValidationUtils.isValidAddress(values.addressStreet)) { - errors.addressStreet = this.props.translate('bankAccount.error.addressStreet'); + errors.addressStreet = 'bankAccount.error.addressStreet'; } if (!values.addressZipCode || !ValidationUtils.isValidZipCode(values.addressZipCode)) { - errors.addressZipCode = this.props.translate('bankAccount.error.zipCode'); + errors.addressZipCode = 'bankAccount.error.zipCode'; } if (!values.addressCity) { - errors.addressCity = this.props.translate('bankAccount.error.addressCity'); + errors.addressCity = 'bankAccount.error.addressCity'; } if (!values.addressState) { - errors.addressState = this.props.translate('bankAccount.error.addressState'); + errors.addressState = 'bankAccount.error.addressState'; } if (!values.companyPhone || !ValidationUtils.isValidUSPhone(values.companyPhone, true)) { - errors.companyPhone = this.props.translate('bankAccount.error.phoneNumber'); + errors.companyPhone = 'bankAccount.error.phoneNumber'; } if (!values.website || !ValidationUtils.isValidWebsite(values.website)) { - errors.website = this.props.translate('bankAccount.error.website'); + errors.website = 'bankAccount.error.website'; } if (!values.companyTaxID || !ValidationUtils.isValidTaxID(values.companyTaxID)) { - errors.companyTaxID = this.props.translate('bankAccount.error.taxID'); + errors.companyTaxID = 'bankAccount.error.taxID'; } if (!values.incorporationType) { - errors.incorporationType = this.props.translate('bankAccount.error.companyType'); + errors.incorporationType = 'bankAccount.error.companyType'; } if (!values.incorporationDate || !ValidationUtils.isValidDate(values.incorporationDate)) { - errors.incorporationDate = this.props.translate('common.error.dateInvalid'); + errors.incorporationDate = 'common.error.dateInvalid'; } else if (!values.incorporationDate || !ValidationUtils.isValidPastDate(values.incorporationDate)) { - errors.incorporationDate = this.props.translate('bankAccount.error.incorporationDateFuture'); + errors.incorporationDate = 'bankAccount.error.incorporationDateFuture'; } if (!values.incorporationState) { - errors.incorporationState = this.props.translate('bankAccount.error.incorporationState'); + errors.incorporationState = 'bankAccount.error.incorporationState'; } if (!values.hasNoConnectionToCannabis) { - errors.hasNoConnectionToCannabis = this.props.translate('bankAccount.error.restrictedBusiness'); + errors.hasNoConnectionToCannabis = 'bankAccount.error.restrictedBusiness'; } return errors; diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js index b69067550848..c6496db73adc 100644 --- a/src/pages/ReimbursementAccount/RequestorStep.js +++ b/src/pages/ReimbursementAccount/RequestorStep.js @@ -41,51 +41,51 @@ class RequestorStep extends React.Component { const errors = {}; if (!ValidationUtils.isRequiredFulfilled(values.firstName)) { - errors.firstName = this.props.translate('bankAccount.error.firstName'); + errors.firstName = 'bankAccount.error.firstName'; } if (!ValidationUtils.isRequiredFulfilled(values.lastName)) { - errors.lastName = this.props.translate('bankAccount.error.lastName'); + errors.lastName = 'bankAccount.error.lastName'; } if (!ValidationUtils.isRequiredFulfilled(values.dob)) { - errors.dob = this.props.translate('bankAccount.error.dob'); + errors.dob = 'bankAccount.error.dob'; } if (values.dob) { if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) { - errors.dob = this.props.translate('bankAccount.error.age'); + errors.dob = 'bankAccount.error.age'; } else if (!ValidationUtils.meetsMaximumAgeRequirement(values.dob)) { - errors.dob = this.props.translate('bankAccount.error.dob'); + errors.dob = 'bankAccount.error.dob'; } } if (!ValidationUtils.isRequiredFulfilled(values.ssnLast4) || !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) { - errors.ssnLast4 = this.props.translate('bankAccount.error.ssnLast4'); + errors.ssnLast4 = 'bankAccount.error.ssnLast4'; } if (!ValidationUtils.isRequiredFulfilled(values.requestorAddressStreet)) { - errors.requestorAddressStreet = this.props.translate('bankAccount.error.address'); + errors.requestorAddressStreet = 'bankAccount.error.address'; } if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) { - errors.requestorAddressStreet = this.props.translate('bankAccount.error.addressStreet'); + errors.requestorAddressStreet = 'bankAccount.error.addressStreet'; } if (!ValidationUtils.isRequiredFulfilled(values.requestorAddressCity)) { - errors.requestorAddressCity = this.props.translate('bankAccount.error.addressCity'); + errors.requestorAddressCity = 'bankAccount.error.addressCity'; } if (!ValidationUtils.isRequiredFulfilled(values.requestorAddressState)) { - errors.requestorAddressState = this.props.translate('bankAccount.error.addressState'); + errors.requestorAddressState = 'bankAccount.error.addressState'; } if (!ValidationUtils.isRequiredFulfilled(values.requestorAddressZipCode) || !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) { - errors.requestorAddressZipCode = this.props.translate('bankAccount.error.zipCode'); + errors.requestorAddressZipCode = 'bankAccount.error.zipCode'; } if (!ValidationUtils.isRequiredFulfilled(values.isControllingOfficer)) { - errors.isControllingOfficer = this.props.translate('requestorStep.isControllingOfficerError'); + errors.isControllingOfficer = 'requestorStep.isControllingOfficerError'; } return errors; diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js index e8dfeae77be7..d07f23d50843 100644 --- a/src/pages/ReimbursementAccount/ValidationStep.js +++ b/src/pages/ReimbursementAccount/ValidationStep.js @@ -71,7 +71,7 @@ class ValidationStep extends React.Component { if (ValidationUtils.isRequiredFulfilled(filteredValue)) { return; } - errors[key] = this.props.translate('common.error.invalidAmount'); + errors[key] = 'common.error.invalidAmount'; }); return errors; diff --git a/src/pages/ReportWelcomeMessagePage.js b/src/pages/ReportWelcomeMessagePage.js index cf68568e38e3..33e36ff44ba1 100644 --- a/src/pages/ReportWelcomeMessagePage.js +++ b/src/pages/ReportWelcomeMessagePage.js @@ -48,7 +48,7 @@ function ReportWelcomeMessagePage(props) { return ( { + onEntryTransitionEnd={() => { if (!welcomeMessageInputRef.current) { return; } @@ -72,7 +72,7 @@ function ReportWelcomeMessagePage(props) { multiline numberOfLines={10} maxLength={CONST.MAX_COMMENT_LENGTH} - ref={welcomeMessageInputRef} + ref={(el) => (welcomeMessageInputRef.current = el)} value={welcomeMessage} onChangeText={handleWelcomeMessageChange} autoCapitalize="none" diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 1b56c40da024..79278452b8a1 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -79,7 +79,7 @@ function HeaderView(props) { const isChatRoom = ReportUtils.isChatRoom(props.report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); const isTaskReport = ReportUtils.isTaskReport(props.report); - const reportHeaderData = (isTaskReport || !isThread) && props.report.parentReportID ? props.parentReport : props.report; + const reportHeaderData = !isTaskReport && !isThread && props.report.parentReportID ? props.parentReport : props.report; const title = ReportUtils.getReportName(reportHeaderData); const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData); const isConcierge = participants.length === 1 && _.contains(participants, CONST.EMAIL.CONCIERGE); diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index d6242d3f2886..c5b092487d70 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -1,6 +1,5 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ import lodashGet from 'lodash/get'; -import React from 'react'; +import React, {useState, useRef, useMemo, useEffect, useCallback} from 'react'; import {InteractionManager, Keyboard, View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; @@ -15,7 +14,6 @@ import * as Report from '../../../libs/actions/Report'; import * as ReportScrollManager from '../../../libs/ReportScrollManager'; import openReportActionComposeViewWhenClosingMessageEdit from '../../../libs/openReportActionComposeViewWhenClosingMessageEdit'; import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager'; -import compose from '../../../libs/compose'; import EmojiPickerButton from '../../../components/EmojiPicker/EmojiPickerButton'; import Icon from '../../../components/Icon'; import * as Expensicons from '../../../components/Icon/Expensicons'; @@ -26,15 +24,15 @@ import * as EmojiUtils from '../../../libs/EmojiUtils'; import reportPropTypes from '../../reportPropTypes'; import ExceededCommentLength from '../../../components/ExceededCommentLength'; import CONST from '../../../CONST'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; -import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; -import withKeyboardState, {keyboardStatePropTypes} from '../../../components/withKeyboardState'; import refPropTypes from '../../../components/refPropTypes'; import * as ComposerUtils from '../../../libs/ComposerUtils'; import * as ComposerActions from '../../../libs/actions/Composer'; import * as User from '../../../libs/actions/User'; import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback'; import Hoverable from '../../../components/Hoverable'; +import useLocalize from '../../../hooks/useLocalize'; +import useKeyboardState from '../../../hooks/useKeyboardState'; +import useWindowDimensions from '../../../hooks/useWindowDimensions'; const propTypes = { /** All the data of the action */ @@ -61,10 +59,6 @@ const propTypes = { /** Stores user's preferred skin tone */ preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, - ...keyboardStatePropTypes, }; const defaultProps = { @@ -74,330 +68,307 @@ const defaultProps = { preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, }; -class ReportActionItemMessageEdit extends React.Component { - constructor(props) { - super(props); - this.updateDraft = this.updateDraft.bind(this); - this.deleteDraft = this.deleteDraft.bind(this); - this.debouncedSaveDraft = _.debounce(this.debouncedSaveDraft.bind(this), 1000); - this.publishDraft = this.publishDraft.bind(this); - this.triggerSaveOrCancel = this.triggerSaveOrCancel.bind(this); - this.onSelectionChange = this.onSelectionChange.bind(this); - this.addEmojiToTextBox = this.addEmojiToTextBox.bind(this); - this.setExceededMaxCommentLength = this.setExceededMaxCommentLength.bind(this); - this.saveButtonID = 'saveButton'; - this.cancelButtonID = 'cancelButton'; - this.emojiButtonID = 'emojiButton'; - this.messageEditInput = 'messageEditInput'; - - let draftMessage; - if (this.props.draftMessage === this.props.action.message[0].html) { +// native ids +const saveButtonID = 'saveButton'; +const cancelButtonID = 'cancelButton'; +const emojiButtonID = 'emojiButton'; +const messageEditInput = 'messageEditInput'; + +function ReportActionItemMessageEdit(props) { + const {translate} = useLocalize(); + const {isKeyboardShown} = useKeyboardState(); + const {isSmallScreenWidth} = useWindowDimensions(); + + const [draft, setDraft] = useState(() => { + if (props.draftMessage === props.action.message[0].html) { // We only convert the report action message to markdown if the draft message is unchanged. const parser = new ExpensiMark(); - draftMessage = parser.htmlToMarkdown(this.props.draftMessage).trim(); - } else { - // We need to decode saved draft message because it's escaped before saving. - draftMessage = Str.htmlDecode(this.props.draftMessage); + return parser.htmlToMarkdown(props.draftMessage).trim(); } - - this.state = { - draft: draftMessage, - selection: { - start: 0, - end: 0, - }, - isFocused: false, - hasExceededMaxCommentLength: false, - }; - } - - componentDidMount() { + // We need to decode saved draft message because it's escaped before saving. + return Str.htmlDecode(props.draftMessage); + }); + const [selection, setSelection] = useState({start: 0, end: 0}); + const [isFocused, setIsFocused] = useState(false); + const [hasExceededMaxCommentLength, setHasExceededMaxCommentLength] = useState(false); + + const textInputRef = useRef(null); + const isFocusedRef = useRef(false); + + useEffect(() => { + // required for keeping last state of isFocused variable + isFocusedRef.current = isFocused; + }, [isFocused]); + + useEffect(() => { // For mobile Safari, updating the selection prop on an unfocused input will cause it to automatically gain focus // and subsequent programmatic focus shifts (e.g., modal focus trap) to show the blue frame (:focus-visible style), // so we need to ensure that it is only updated after focus. - this.setState((prevState) => ({ - selection: { - start: prevState.draft.length, - end: prevState.draft.length, - }, - })); - } - - componentWillUnmount() { - // Skip if this is not the focused message so the other edit composer stays focused. - if (!this.state.isFocused) { - return; - } + setDraft((prevDraft) => { + setSelection({ + start: prevDraft.length, + end: prevDraft.length, + }); + return prevDraft; + }); - // Show the main composer when the focused message is deleted from another client - // to prevent the main composer stays hidden until we swtich to another chat. - ComposerActions.setShouldShowComposeInput(true); - } + return () => { + // Skip if this is not the focused message so the other edit composer stays focused + if (!isFocusedRef.current) { + return; + } - /** - * Update Selection on change cursor position. - * - * @param {Event} e - */ - onSelectionChange(e) { - this.setState({selection: e.nativeEvent.selection}); - } + // Show the main composer when the focused message is deleted from another client + // to prevent the main composer stays hidden until we swtich to another chat. + ComposerActions.setShouldShowComposeInput(true); + }; + }, []); /** - * Updates the composer when the comment length is exceeded - * Shows red borders and prevents the comment from being sent - * - * @param {Boolean} hasExceededMaxCommentLength + * Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft + * allows one to navigate somewhere else and come back to the comment and still have it in edit mode. + * @param {String} newDraft */ - setExceededMaxCommentLength(hasExceededMaxCommentLength) { - this.setState({hasExceededMaxCommentLength}); - } + const debouncedSaveDraft = useMemo( + () => + _.debounce((newDraft) => { + Report.saveReportActionDraft(props.reportID, props.action.reportActionID, newDraft); + }, 1000), + [props.reportID, props.action.reportActionID], + ); /** * Update the value of the draft in Onyx * - * @param {String} draft + * @param {String} newDraftInput */ - updateDraft(draft) { - const {text: newDraft = '', emojis = []} = EmojiUtils.replaceEmojis(draft, this.props.isSmallScreenWidth, this.props.preferredSkinTone); + const updateDraft = useCallback( + (newDraftInput) => { + const {text: newDraft = '', emojis = []} = EmojiUtils.replaceEmojis(newDraftInput, isSmallScreenWidth, props.preferredSkinTone); - if (!_.isEmpty(emojis)) { - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(emojis)); - } - - this.setState((prevState) => { - const newState = {draft: newDraft}; - if (draft !== newDraft) { - const remainder = prevState.draft.slice(prevState.selection.end).length; - newState.selection = { - start: newDraft.length - remainder, - end: newDraft.length - remainder, - }; + if (!_.isEmpty(emojis)) { + User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(emojis)); } - return newState; - }); + setDraft((prevDraft) => { + if (newDraftInput !== newDraft) { + setSelection((prevSelection) => { + const remainder = prevDraft.slice(prevSelection.end).length; + return { + start: newDraft.length - remainder, + end: newDraft.length - remainder, + }; + }); + } + return newDraft; + }); - // This component is rendered only when draft is set to a non-empty string. In order to prevent component - // unmount when user deletes content of textarea, we set previous message instead of empty string. - if (newDraft.trim().length > 0) { - // We want to escape the draft message to differentiate the HTML from the report action and the HTML the user drafted. - this.debouncedSaveDraft(_.escape(newDraft)); - } else { - this.debouncedSaveDraft(this.props.action.message[0].html); - } - } + // This component is rendered only when draft is set to a non-empty string. In order to prevent component + // unmount when user deletes content of textarea, we set previous message instead of empty string. + if (newDraft.trim().length > 0) { + // We want to escape the draft message to differentiate the HTML from the report action and the HTML the user drafted. + debouncedSaveDraft(_.escape(newDraft)); + } else { + debouncedSaveDraft(props.action.message[0].html); + } + }, + [props.action.message, debouncedSaveDraft, isSmallScreenWidth, props.preferredSkinTone], + ); /** * Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content. */ - deleteDraft() { - this.debouncedSaveDraft.cancel(); - Report.saveReportActionDraft(this.props.reportID, this.props.action.reportActionID, ''); + const deleteDraft = useCallback(() => { + debouncedSaveDraft.cancel(); + Report.saveReportActionDraft(props.reportID, props.action.reportActionID, ''); ComposerActions.setShouldShowComposeInput(true); ReportActionComposeFocusManager.focus(); // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report. - if (this.props.index === 0) { + if (props.index === 0) { const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => { - ReportScrollManager.scrollToIndex({animated: true, index: this.props.index}, false); + ReportScrollManager.scrollToIndex({animated: true, index: props.index}, false); keyboardDidHideListener.remove(); }); } - } - - /** - * Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft - * allows one to navigate somewhere else and come back to the comment and still have it in edit mode. - * @param {String} newDraft - */ - debouncedSaveDraft(newDraft) { - Report.saveReportActionDraft(this.props.reportID, this.props.action.reportActionID, newDraft); - } + }, [props.action.reportActionID, debouncedSaveDraft, props.index, props.reportID]); /** * Save the draft of the comment to be the new comment message. This will take the comment out of "edit mode" with * the new content. */ - publishDraft() { + const publishDraft = useCallback(() => { // Do nothing if draft exceed the character limit - if (ReportUtils.getCommentLength(this.state.draft) > CONST.MAX_COMMENT_LENGTH) { + if (ReportUtils.getCommentLength(draft) > CONST.MAX_COMMENT_LENGTH) { return; } // To prevent re-mount after user saves edit before debounce duration (example: within 1 second), we cancel // debounce here. - this.debouncedSaveDraft.cancel(); + debouncedSaveDraft.cancel(); - const trimmedNewDraft = this.state.draft.trim(); + const trimmedNewDraft = draft.trim(); // When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting. if (!trimmedNewDraft) { - ReportActionContextMenu.showDeleteModal(this.props.reportID, this.props.action, false, this.deleteDraft, () => - InteractionManager.runAfterInteractions(() => this.textInput.focus()), - ); + ReportActionContextMenu.showDeleteModal(props.reportID, props.action, false, deleteDraft, () => InteractionManager.runAfterInteractions(() => textInputRef.current.focus())); return; } - Report.editReportComment(this.props.reportID, this.props.action, trimmedNewDraft); - this.deleteDraft(); - } + Report.editReportComment(props.reportID, props.action, trimmedNewDraft); + deleteDraft(); + }, [props.action, debouncedSaveDraft, deleteDraft, draft, props.reportID]); /** * @param {String} emoji */ - addEmojiToTextBox(emoji) { - this.setState((prevState) => ({ - selection: { - start: prevState.selection.start + emoji.length, - end: prevState.selection.start + emoji.length, - }, + const addEmojiToTextBox = (emoji) => { + setSelection((prevSelection) => ({ + start: prevSelection.start + emoji.length, + end: prevSelection.start + emoji.length, })); - this.updateDraft(ComposerUtils.insertText(this.state.draft, this.state.selection, emoji)); - } + updateDraft(ComposerUtils.insertText(draft, selection, emoji)); + }; /** * Key event handlers that short cut to saving/canceling. * * @param {Event} e */ - triggerSaveOrCancel(e) { - if (!e || ComposerUtils.canSkipTriggerHotkeys(this.props.isSmallScreenWidth, this.props.isKeyboardShown)) { - return; - } - if (e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !e.shiftKey) { - e.preventDefault(); - this.publishDraft(); - } else if (e.key === CONST.KEYBOARD_SHORTCUTS.ESCAPE.shortcutKey) { - e.preventDefault(); - this.deleteDraft(); - } - } - - render() { - const hasExceededMaxCommentLength = this.state.hasExceededMaxCommentLength; - return ( - <> - - - - - {(hovered) => ( - - - - )} - - - - - - { - this.textInput = el; - this.props.forwardedRef.current = el; - }} - nativeID={this.messageEditInput} - onChangeText={this.updateDraft} // Debounced saveDraftComment - onKeyPress={this.triggerSaveOrCancel} - value={this.state.draft} - maxLines={this.props.isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES} // This is the same that slack has - style={[styles.textInputCompose, styles.flex1, styles.bgTransparent]} - onFocus={() => { - this.setState({isFocused: true}); - ReportScrollManager.scrollToIndex({animated: true, index: this.props.index}, true); - ComposerActions.setShouldShowComposeInput(false); - }} - onBlur={(event) => { - this.setState({isFocused: false}); - const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id'); - - // Return to prevent re-render when save/cancel button is pressed which cancels the onPress event by re-rendering - if (_.contains([this.saveButtonID, this.cancelButtonID, this.emojiButtonID], relatedTargetId)) { - return; - } - - if (this.messageEditInput === relatedTargetId) { - return; - } - openReportActionComposeViewWhenClosingMessageEdit(); - }} - selection={this.state.selection} - onSelectionChange={this.onSelectionChange} - /> - - - InteractionManager.runAfterInteractions(() => this.textInput.focus())} - onEmojiSelected={this.addEmojiToTextBox} - nativeID={this.emojiButtonID} - /> - - - - + const triggerSaveOrCancel = useCallback( + (e) => { + if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { + return; + } + if (e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !e.shiftKey) { + e.preventDefault(); + publishDraft(); + } else if (e.key === CONST.KEYBOARD_SHORTCUTS.ESCAPE.shortcutKey) { + e.preventDefault(); + deleteDraft(); + } + }, + [deleteDraft, isKeyboardShown, isSmallScreenWidth, publishDraft], + ); + + return ( + <> + + + + + {(hovered) => ( - - + )} + + + + + + { + textInputRef.current = el; + // eslint-disable-next-line no-param-reassign + props.forwardedRef.current = el; + }} + nativeID={messageEditInput} + onChangeText={updateDraft} // Debounced saveDraftComment + onKeyPress={triggerSaveOrCancel} + value={draft} + maxLines={isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES} // This is the same that slack has + style={[styles.textInputCompose, styles.flex1, styles.bgTransparent]} + onFocus={() => { + setIsFocused(true); + ReportScrollManager.scrollToIndex({animated: true, index: props.index}, true); + ComposerActions.setShouldShowComposeInput(false); + }} + onBlur={(event) => { + setIsFocused(false); + const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id'); + + // Return to prevent re-render when save/cancel button is pressed which cancels the onPress event by re-rendering + if (_.contains([saveButtonID, cancelButtonID, emojiButtonID], relatedTargetId)) { + return; + } + + if (messageEditInput === relatedTargetId) { + return; + } + openReportActionComposeViewWhenClosingMessageEdit(); + }} + selection={selection} + onSelectionChange={(e) => setSelection(e.nativeEvent.selection)} + /> + + + InteractionManager.runAfterInteractions(() => textInputRef.current.focus())} + onEmojiSelected={addEmojiToTextBox} + nativeID={emojiButtonID} + /> + + + + + + + + - - - ); - } + + setHasExceededMaxCommentLength(hasExceeded)} + /> + + ); } ReportActionItemMessageEdit.propTypes = propTypes; ReportActionItemMessageEdit.defaultProps = defaultProps; -export default compose( - withLocalize, - withWindowDimensions, - withKeyboardState, -)( - React.forwardRef((props, ref) => ( - - )), -); +ReportActionItemMessageEdit.displayName = 'ReportActionItemMessageEdit'; + +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/pages/settings/PasswordPage.js b/src/pages/settings/PasswordPage.js index f987cabb3c65..ab352851c76b 100755 --- a/src/pages/settings/PasswordPage.js +++ b/src/pages/settings/PasswordPage.js @@ -78,10 +78,7 @@ class PasswordPage extends Component { * @returns {String} */ getErrorText(field) { - if (this.state.errors[field]) { - return this.props.translate(this.errorKeysMap[field]); - } - return ''; + return this.state.errors[field] ? this.errorKeysMap[field] : ''; } /** diff --git a/src/pages/settings/Payments/AddDebitCardPage.js b/src/pages/settings/Payments/AddDebitCardPage.js index 7c174c58dbc2..77b7089fd2ef 100644 --- a/src/pages/settings/Payments/AddDebitCardPage.js +++ b/src/pages/settings/Payments/AddDebitCardPage.js @@ -74,39 +74,39 @@ class DebitCardPage extends Component { const errors = {}; if (!values.nameOnCard || !ValidationUtils.isValidCardName(values.nameOnCard)) { - errors.nameOnCard = this.props.translate('addDebitCardPage.error.invalidName'); + errors.nameOnCard = 'addDebitCardPage.error.invalidName'; } if (!values.cardNumber || !ValidationUtils.isValidDebitCard(values.cardNumber.replace(/ /g, ''))) { - errors.cardNumber = this.props.translate('addDebitCardPage.error.debitCardNumber'); + errors.cardNumber = 'addDebitCardPage.error.debitCardNumber'; } if (!values.expirationDate || !ValidationUtils.isValidExpirationDate(values.expirationDate)) { - errors.expirationDate = this.props.translate('addDebitCardPage.error.expirationDate'); + errors.expirationDate = 'addDebitCardPage.error.expirationDate'; } if (!values.securityCode || !ValidationUtils.isValidSecurityCode(values.securityCode)) { - errors.securityCode = this.props.translate('addDebitCardPage.error.securityCode'); + errors.securityCode = 'addDebitCardPage.error.securityCode'; } if (!values.addressStreet || !ValidationUtils.isValidAddress(values.addressStreet)) { - errors.addressStreet = this.props.translate('addDebitCardPage.error.addressStreet'); + errors.addressStreet = 'addDebitCardPage.error.addressStreet'; } if (!values.addressZipCode || !ValidationUtils.isValidZipCode(values.addressZipCode)) { - errors.addressZipCode = this.props.translate('addDebitCardPage.error.addressZipCode'); + errors.addressZipCode = 'addDebitCardPage.error.addressZipCode'; } if (!values.addressState || !values.addressState) { - errors.addressState = this.props.translate('addDebitCardPage.error.addressState'); + errors.addressState = 'addDebitCardPage.error.addressState'; } if (!Permissions.canUsePasswordlessLogins(this.props.betas) && (!values.password || _.isEmpty(values.password.trim()))) { - errors.password = this.props.translate('addDebitCardPage.error.password'); + errors.password = 'addDebitCardPage.error.password'; } if (!values.acceptTerms) { - errors.acceptTerms = this.props.translate('common.error.acceptTerms'); + errors.acceptTerms = 'common.error.acceptTerms'; } return errors; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js index 3100af19c4e9..383327ecd941 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js @@ -118,7 +118,7 @@ function ContactMethodsPage(props) { /> - + {props.translate('contacts.helpTextBeforeEmail')} { - const errors = {}; - const minimumAge = CONST.DATE_BIRTH.MIN_AGE; - const maximumAge = CONST.DATE_BIRTH.MAX_AGE; + const validate = useCallback((values) => { + const errors = {}; + const minimumAge = CONST.DATE_BIRTH.MIN_AGE; + const maximumAge = CONST.DATE_BIRTH.MAX_AGE; - if (!values.dob || !ValidationUtils.isValidDate(values.dob)) { - errors.dob = translate('common.error.fieldRequired'); - } - const dateError = ValidationUtils.getAgeRequirementError(values.dob, minimumAge, maximumAge); - if (dateError) { - errors.dob = dateError; - } + if (!values.dob || !ValidationUtils.isValidDate(values.dob)) { + errors.dob = 'common.error.fieldRequired'; + } + const dateError = ValidationUtils.getAgeRequirementError(values.dob, minimumAge, maximumAge); + if (dateError) { + errors.dob = dateError; + } - return errors; - }, - [translate], - ); + return errors; + }, []); return ( diff --git a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js index 41139e74bcb7..d743aa0cd983 100644 --- a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js +++ b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js @@ -44,28 +44,24 @@ const updateLegalName = (values) => { function LegalNamePage(props) { const legalFirstName = lodashGet(props.privatePersonalDetails, 'legalFirstName', ''); const legalLastName = lodashGet(props.privatePersonalDetails, 'legalLastName', ''); - const translate = props.translate; - const validate = useCallback( - (values) => { - const errors = {}; + const validate = useCallback((values) => { + const errors = {}; - if (!ValidationUtils.isValidLegalName(values.legalFirstName)) { - errors.legalFirstName = translate('privatePersonalDetails.error.hasInvalidCharacter'); - } else if (_.isEmpty(values.legalFirstName)) { - errors.legalFirstName = translate('common.error.fieldRequired'); - } + if (!ValidationUtils.isValidLegalName(values.legalFirstName)) { + errors.legalFirstName = 'privatePersonalDetails.error.hasInvalidCharacter'; + } else if (_.isEmpty(values.legalFirstName)) { + errors.legalFirstName = 'common.error.fieldRequired'; + } - if (!ValidationUtils.isValidLegalName(values.legalLastName)) { - errors.legalLastName = translate('privatePersonalDetails.error.hasInvalidCharacter'); - } else if (_.isEmpty(values.legalLastName)) { - errors.legalLastName = translate('common.error.fieldRequired'); - } + if (!ValidationUtils.isValidLegalName(values.legalLastName)) { + errors.legalLastName = 'privatePersonalDetails.error.hasInvalidCharacter'; + } else if (_.isEmpty(values.legalLastName)) { + errors.legalLastName = 'common.error.fieldRequired'; + } - return errors; - }, - [translate], - ); + return errors; + }, []); return ( diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index b4bf1d0c5566..011a65d4cb76 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -48,21 +48,21 @@ function RoomNamePage(props) { if (!values.roomName || values.roomName === CONST.POLICY.ROOM_PREFIX) { // We error if the user doesn't enter a room name or left blank - ErrorUtils.addErrorMessage(errors, 'roomName', translate('newRoomPage.pleaseEnterRoomName')); + ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.pleaseEnterRoomName'); } else if (!ValidationUtils.isValidRoomName(values.roomName)) { // We error if the room name has invalid characters - ErrorUtils.addErrorMessage(errors, 'roomName', translate('newRoomPage.roomNameInvalidError')); + ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomNameInvalidError'); } else if (ValidationUtils.isReservedRoomName(values.roomName)) { // Certain names are reserved for default rooms and should not be used for policy rooms. - ErrorUtils.addErrorMessage(errors, 'roomName', translate('newRoomPage.roomNameReservedError', {reservedName: values.roomName})); + ErrorUtils.addErrorMessage(errors, 'roomName', ['newRoomPage.roomNameReservedError', {reservedName: values.roomName}]); } else if (ValidationUtils.isExistingRoomName(values.roomName, reports, report.policyID)) { // The room name can't be set to one that already exists on the policy - ErrorUtils.addErrorMessage(errors, 'roomName', translate('newRoomPage.roomAlreadyExistsError')); + ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomAlreadyExistsError'); } return errors; }, - [report, reports, translate], + [report, reports], ); return ( diff --git a/src/pages/settings/Security/CloseAccountPage.js b/src/pages/settings/Security/CloseAccountPage.js index c5189727a3b7..3789804acc73 100644 --- a/src/pages/settings/Security/CloseAccountPage.js +++ b/src/pages/settings/Security/CloseAccountPage.js @@ -78,7 +78,7 @@ class CloseAccountPage extends Component { const errors = {}; if (_.isEmpty(values.phoneOrEmail) || userEmailOrPhone.toLowerCase() !== values.phoneOrEmail.toLowerCase()) { - errors.phoneOrEmail = this.props.translate('closeAccountPage.enterYourDefaultContactMethod'); + errors.phoneOrEmail = 'closeAccountPage.enterYourDefaultContactMethod'; } return errors; } diff --git a/src/pages/signin/SignInPageLayout/Footer.js b/src/pages/signin/SignInPageLayout/Footer.js index a37654fe41ff..35e63b0699b3 100644 --- a/src/pages/signin/SignInPageLayout/Footer.js +++ b/src/pages/signin/SignInPageLayout/Footer.js @@ -191,7 +191,7 @@ function Footer(props) { ))} {i === 2 && ( - + )} diff --git a/src/pages/signin/Socials.js b/src/pages/signin/Socials.js index 52c21890c381..f7a866d2023b 100644 --- a/src/pages/signin/Socials.js +++ b/src/pages/signin/Socials.js @@ -1,15 +1,14 @@ import React from 'react'; import {View} from 'react-native'; import _ from 'underscore'; +import * as Link from '../../libs/actions/Link'; import Icon from '../../components/Icon'; -import Text from '../../components/Text'; +import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import * as Expensicons from '../../components/Icon/Expensicons'; import themeColors from '../../styles/themes/default'; import styles from '../../styles/styles'; import variables from '../../styles/variables'; import CONST from '../../CONST'; -import Hoverable from '../../components/Hoverable'; -import TextLink from '../../components/TextLink'; const socialsList = [ { @@ -36,27 +35,30 @@ const socialsList = [ function Socials() { return ( - + {_.map(socialsList, (social) => ( - - {(hovered) => ( - - - - - + { + e.preventDefault(); + Link.openExternalLink(social.link); + }} + accessible={false} + style={[styles.mr1, styles.mt1]} + shouldUseAutoHitSlop={false} + > + {({hovered, pressed}) => ( + )} - + ))} - + ); } diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index eaad83fd4a84..5b9773270048 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -53,7 +53,7 @@ function NewTaskPage(props) { if (!values.taskTitle) { // We error if the user doesn't enter a task name - ErrorUtils.addErrorMessage(errors, 'taskTitle', props.translate('newTaskPage.pleaseEnterTaskName')); + ErrorUtils.addErrorMessage(errors, 'taskTitle', 'newTaskPage.pleaseEnterTaskName'); } return errors; diff --git a/src/pages/tasks/NewTaskTitlePage.js b/src/pages/tasks/NewTaskTitlePage.js index 1e232b637de3..b1e7b689f85d 100644 --- a/src/pages/tasks/NewTaskTitlePage.js +++ b/src/pages/tasks/NewTaskTitlePage.js @@ -48,7 +48,7 @@ function NewTaskTitlePage(props) { if (!values.taskTitle) { // We error if the user doesn't enter a task name - ErrorUtils.addErrorMessage(errors, 'taskTitle', props.translate('newTaskPage.pleaseEnterTaskName')); + ErrorUtils.addErrorMessage(errors, 'taskTitle', 'newTaskPage.pleaseEnterTaskName'); } return errors; diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.js index 046294ec95fb..c8ac41c87c88 100644 --- a/src/pages/tasks/TaskTitlePage.js +++ b/src/pages/tasks/TaskTitlePage.js @@ -41,18 +41,15 @@ function TaskTitlePage(props) { * @param {String} values.title * @returns {Object} - An object containing the errors for each inputID */ - const validate = useCallback( - (values) => { - const errors = {}; + const validate = useCallback((values) => { + const errors = {}; - if (_.isEmpty(values.title)) { - errors.title = props.translate('newTaskPage.pleaseEnterTaskName'); - } + if (_.isEmpty(values.title)) { + errors.title = 'newTaskPage.pleaseEnterTaskName'; + } - return errors; - }, - [props], - ); + return errors; + }, []); const submit = useCallback( (values) => { diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index bfabfc274a99..4debb088a43a 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -142,7 +142,7 @@ class WorkspaceInviteMessagePage extends React.Component { validate() { const errorFields = {}; if (_.isEmpty(this.props.invitedMembersDraft)) { - errorFields.welcomeMessage = this.props.translate('workspace.inviteMessage.inviteNoMembersError'); + errorFields.welcomeMessage = 'workspace.inviteMessage.inviteNoMembersError'; } return errorFields; } diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index dc67745b3427..0c1116db2ee4 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -291,7 +291,7 @@ class WorkspaceMembersPage extends React.Component { return; } - errors[member] = this.props.translate('workspace.people.error.cannotRemove'); + errors[member] = 'workspace.people.error.cannotRemove'; }); this.setState({errors}); diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 60583cbc95e8..fd91d1b43214 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -101,20 +101,20 @@ class WorkspaceNewRoomPage extends React.Component { if (!values.roomName || values.roomName === CONST.POLICY.ROOM_PREFIX) { // We error if the user doesn't enter a room name or left blank - ErrorUtils.addErrorMessage(errors, 'roomName', this.props.translate('newRoomPage.pleaseEnterRoomName')); + ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.pleaseEnterRoomName'); } else if (values.roomName !== CONST.POLICY.ROOM_PREFIX && !ValidationUtils.isValidRoomName(values.roomName)) { // We error if the room name has invalid characters - ErrorUtils.addErrorMessage(errors, 'roomName', this.props.translate('newRoomPage.roomNameInvalidError')); + ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomNameInvalidError'); } else if (ValidationUtils.isReservedRoomName(values.roomName)) { // Certain names are reserved for default rooms and should not be used for policy rooms. - ErrorUtils.addErrorMessage(errors, 'roomName', this.props.translate('newRoomPage.roomNameReservedError', {reservedName: values.roomName})); + ErrorUtils.addErrorMessage(errors, 'roomName', ['newRoomPage.roomNameReservedError', {reservedName: values.roomName}]); } else if (ValidationUtils.isExistingRoomName(values.roomName, this.props.reports, values.policyID)) { // Certain names are reserved for default rooms and should not be used for policy rooms. - ErrorUtils.addErrorMessage(errors, 'roomName', this.props.translate('newRoomPage.roomAlreadyExistsError')); + ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomAlreadyExistsError'); } if (!values.policyID) { - errors.policyID = this.props.translate('newRoomPage.pleaseSelectWorkspace'); + errors.policyID = 'newRoomPage.pleaseSelectWorkspace'; } return errors; diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 333e7fb87076..6abde456f40b 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -44,9 +44,6 @@ const defaultProps = { }; function WorkspaceSettingsPage(props) { - const nameIsRequiredError = props.translate('workspace.editor.nameIsRequiredError'); - const nameIsTooLongError = props.translate('workspace.editor.nameIsTooLongError'); - const currencyItems = useMemo(() => { const currencyListKeys = _.keys(props.currencyList); return _.map(currencyListKeys, (currencyCode) => ({ @@ -68,23 +65,20 @@ function WorkspaceSettingsPage(props) { [props.policy.id, props.policy.isPolicyUpdating], ); - const validate = useCallback( - (values) => { - const errors = {}; - const name = values.name.trim(); + const validate = useCallback((values) => { + const errors = {}; + const name = values.name.trim(); - if (!name || !name.length) { - errors.name = nameIsRequiredError; - } else if ([...name].length > CONST.WORKSPACE_NAME_CHARACTER_LIMIT) { - // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 - // code units. - errors.name = nameIsTooLongError; - } + if (!name || !name.length) { + errors.name = 'workspace.editor.nameIsRequiredError'; + } else if ([...name].length > CONST.WORKSPACE_NAME_CHARACTER_LIMIT) { + // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 + // code units. + errors.name = 'workspace.editor.nameIsTooLongError'; + } - return errors; - }, - [nameIsRequiredError, nameIsTooLongError], - ); + return errors; + }, []); const policyName = lodashGet(props.policy, 'name', ''); diff --git a/src/pages/workspace/bills/WorkspaceBillsFirstSection.js b/src/pages/workspace/bills/WorkspaceBillsFirstSection.js index b0fc0e99c8f9..d680c3f62fa4 100644 --- a/src/pages/workspace/bills/WorkspaceBillsFirstSection.js +++ b/src/pages/workspace/bills/WorkspaceBillsFirstSection.js @@ -61,7 +61,7 @@ function WorkspaceBillsFirstSection(props) { containerStyles={[styles.cardSection]} > - + {props.translate('workspace.bills.askYourVendorsBeforeEmail')} {props.user.isFromPublicDomain ? ( Link.openExternalLink('https://community.expensify.com/discussion/7500/how-to-pay-your-company-bills-in-expensify/')}> diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.js b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.js index a13385665196..2150beca2441 100644 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.js +++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage.js @@ -95,7 +95,7 @@ class WorkspaceRateAndUnitPage extends React.Component { const decimalSeparator = this.props.toLocaleDigit('.'); const rateValueRegex = RegExp(String.raw`^\d{1,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{1,3})?$`, 'i'); if (!rateValueRegex.test(values.rate)) { - errors.rate = this.props.translate('workspace.reimburse.invalidRateError'); + errors.rate = 'workspace.reimburse.invalidRateError'; } return errors; } diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.js index b1b802a65208..9005374826b3 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.js @@ -152,7 +152,7 @@ class WorkspaceReimburseView extends React.Component { ]} > - + {this.props.translate('workspace.reimburse.captureNoVBACopyBeforeEmail')} { const errors = {}; ErrorUtils.addErrorMessage(errors, 'username', 'Username cannot be empty'); - expect(errors).toEqual({username: 'Username cannot be empty'}); + expect(errors).toEqual({username: ['Username cannot be empty', {isTranslated: true}]}); }); test('should append an error message to an existing error message for a given inputID', () => { const errors = {username: 'Username cannot be empty'}; ErrorUtils.addErrorMessage(errors, 'username', 'Username must be at least 6 characters long'); - expect(errors).toEqual({username: 'Username cannot be empty\nUsername must be at least 6 characters long'}); + expect(errors).toEqual({username: ['Username cannot be empty\nUsername must be at least 6 characters long', {isTranslated: true}]}); }); test('should add an error to input which does not contain any errors yet', () => { const errors = {username: 'Username cannot be empty'}; ErrorUtils.addErrorMessage(errors, 'password', 'Password cannot be empty'); - expect(errors).toEqual({username: 'Username cannot be empty', password: 'Password cannot be empty'}); + expect(errors).toEqual({username: 'Username cannot be empty', password: ['Password cannot be empty', {isTranslated: true}]}); }); test('should not mutate the errors object when message is empty', () => { @@ -49,7 +49,7 @@ describe('ErrorUtils', () => { ErrorUtils.addErrorMessage(errors, 'username', 'Username must be at least 6 characters long'); ErrorUtils.addErrorMessage(errors, 'username', 'Username must contain at least one letter'); - expect(errors).toEqual({username: 'Username cannot be empty\nUsername must be at least 6 characters long\nUsername must contain at least one letter'}); + expect(errors).toEqual({username: ['Username cannot be empty\nUsername must be at least 6 characters long\nUsername must contain at least one letter', {isTranslated: true}]}); }); test('should append multiple error messages to an existing error message for the same inputID', () => { @@ -58,7 +58,10 @@ describe('ErrorUtils', () => { ErrorUtils.addErrorMessage(errors, 'username', 'Username must not contain special characters'); expect(errors).toEqual({ - username: 'Username cannot be empty\nUsername must be at least 6 characters long\nUsername must contain at least one letter\nUsername must not contain special characters', + username: [ + 'Username cannot be empty\nUsername must be at least 6 characters long\nUsername must contain at least one letter\nUsername must not contain special characters', + {isTranslated: true}, + ], }); }); });