From 421fbdcc263e02a83285bb753805eb40a09b1d8f Mon Sep 17 00:00:00 2001 From: sahil Date: Wed, 3 Aug 2022 22:48:59 +0530 Subject: [PATCH 001/155] use AddressForm --- .../EnablePayments/AdditionalDetailsStep.js | 53 ++++++------------- 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index 71daa5f13f72..57e3e6fa0804 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -24,7 +24,7 @@ import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButto import * as Wallet from '../../libs/actions/Wallet'; import * as ValidationUtils from '../../libs/ValidationUtils'; import * as LoginUtils from '../../libs/LoginUtils'; -import AddressSearch from '../../components/AddressSearch'; +import AddressForm from '../ReimbursementAccount/AddressForm'; import DatePicker from '../../components/DatePicker'; import FormHelper from '../../libs/FormHelper'; import walletAdditionalDetailsDraftPropTypes from './walletAdditionalDetailsDraftPropTypes'; @@ -310,52 +310,33 @@ class AdditionalDetailsStep extends React.Component { value={this.props.walletAdditionalDetailsDraft.legalLastName || lastName} errorText={this.getErrorText('legalLastName')} /> - { + { const renamedFields = { street: 'addressStreet', state: 'addressState', - zipCode: 'addressZip', city: 'addressCity', + zipCode: 'addressZip', }; _.each(values, (value, inputKey) => { const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); this.clearErrorAndSetValue(renamedInputKey, value); }); }} - errorText={this.getErrorText('addressStreet')} - hint={this.props.translate('common.noPO')} /> - {this.props.walletAdditionalDetailsDraft.addressStreet ? ( - <> - {/** Once the user has started entering his address, show the other address fields (city, state, zip) */} - {/** We'll autofill them when the user selects a full address from the google autocomplete */} - this.clearErrorAndSetValue('addressCity', val)} - value={this.props.walletAdditionalDetailsDraft.addressCity || ''} - errorText={this.getErrorText('addressCity')} - /> - this.clearErrorAndSetValue('addressState', val)} - value={this.props.walletAdditionalDetailsDraft.addressState || ''} - errorText={this.getErrorText('addressState')} - /> - this.clearErrorAndSetValue('addressZip', val)} - value={this.props.walletAdditionalDetailsDraft.addressZip || ''} - errorText={this.getErrorText('addressZip')} - /> - - ) : null} Date: Wed, 3 Aug 2022 22:53:35 +0530 Subject: [PATCH 002/155] add validation for zip code --- src/pages/EnablePayments/AdditionalDetailsStep.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index 57e3e6fa0804..bee80a6ab799 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -185,6 +185,10 @@ class AdditionalDetailsStep extends React.Component { errors.addressStreet = true; } + if (!ValidationUtils.isValidZipCode(this.props.walletAdditionalDetailsDraft.addressZip)) { + errors.addressZip = true; + } + if (!ValidationUtils.isValidUSPhone(this.props.walletAdditionalDetailsDraft.phoneNumber, true)) { errors.phoneNumber = true; } From 8708b50eb55d0d68311c557f720412a5ac8dbe5d Mon Sep 17 00:00:00 2001 From: sahil Date: Wed, 3 Aug 2022 22:54:28 +0530 Subject: [PATCH 003/155] remove unused code --- src/pages/EnablePayments/AdditionalDetailsStep.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index bee80a6ab799..330b1456b205 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -108,10 +108,6 @@ class AdditionalDetailsStep extends React.Component { this.errorTranslationKeys = { legalFirstName: 'bankAccount.error.firstName', legalLastName: 'bankAccount.error.lastName', - addressStreet: 'bankAccount.error.addressStreet', - addressCity: 'bankAccount.error.addressCity', - addressState: 'bankAccount.error.addressState', - addressZip: 'bankAccount.error.zipCode', phoneNumber: 'bankAccount.error.phoneNumber', dob: 'bankAccount.error.dob', age: 'bankAccount.error.age', From ab958a8c6864688c8af806c74b211a1a87aa3553 Mon Sep 17 00:00:00 2001 From: sahil Date: Wed, 3 Aug 2022 23:32:41 +0530 Subject: [PATCH 004/155] remove unused keys --- src/pages/EnablePayments/AdditionalDetailsStep.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index 330b1456b205..e38733fda1a7 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -119,9 +119,6 @@ class AdditionalDetailsStep extends React.Component { legalFirstName: 'additionalDetailsStep.legalFirstNameLabel', legalLastName: 'additionalDetailsStep.legalLastNameLabel', addressStreet: 'common.personalAddress', - addressCity: 'common.city', - addressState: 'common.state', - addressZip: 'common.zip', phoneNumber: 'common.phoneNumber', dob: 'common.dob', ssn: 'common.ssnLast4', From e9ad92f737e8661e8bef69b84a4d94672b6d0f8b Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Thu, 4 Aug 2022 18:41:45 +0530 Subject: [PATCH 005/155] Add new API method SetPasswordForNewUserAndSignin --- src/libs/actions/Session/index.js | 62 +++++++++++++++++++++++++++++++ src/pages/SetPasswordPage.js | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 5059a434ad43..43f172289fa4 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -441,6 +441,67 @@ function changePasswordAndSignIn(authToken, password) { }); } +/** + * Validates new user login, sets a new password and authenticates them + * @param {String} authToken + * @param {String} password + */ +function setPasswordForNewUserAndSignin(authToken, password) { + const optimisticData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: true, + validateCodeExpired: false, + }, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.SESSION, + value: { + error: '', + }, + }, + ]; + + const successData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + }, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.SESSION, + value: { + error: '', + }, + }, + ]; + + const failureData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + error: 'Unable to set Password', + }, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.SESSION, + value: { + error: 'setPasswordPage.passwordNotSet', + }, + }, + ]; + API.write('SetPasswordForNewAccountAndSignin', {authToken, password}, {optimisticData, successData, failureData}); +} + /** * @param {Number} accountID * @param {String} validateCode @@ -539,6 +600,7 @@ function setShouldShowComposeInput(shouldShowComposeInput) { export { fetchAccountDetails, setPassword, + setPasswordForNewUserAndSignin, signIn, signInWithShortLivedToken, signOut, diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index ec5c3fb3599d..c33293dc1333 100755 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -105,7 +105,7 @@ class SetPasswordPage extends Component { const validateCode = lodashGet(this.props.route.params, 'validateCode', ''); if (this.props.userSignUp.authToken) { - Session.changePasswordAndSignIn(this.props.userSignUp.authToken, this.state.password); + Session.setPasswordForNewUserAndSignin(this.props.userSignUp.authToken, this.state.password); } else { Session.setPassword(this.state.password, validateCode, accountID); } From eb766699512a2c1325e38d0bfa768741b5aefbf5 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 15:45:14 +0100 Subject: [PATCH 006/155] add the method --- src/libs/actions/Wallet.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index eb75d3969119..0f4154ca5360 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -351,6 +351,10 @@ function updateCurrentStep(currentStep) { Onyx.merge(ONYXKEYS.USER_WALLET, {currentStep}); } +function answerQuestionsForWallet() { + +} + export { fetchOnfidoToken, activateWallet, From 46567a465a18ee4aa88f71cb9cdf0f9f5c8a6948 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 16:08:37 +0100 Subject: [PATCH 007/155] adding the OnyxData --- src/libs/actions/Wallet.js | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 0f4154ca5360..a5a2d302082b 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -7,6 +7,7 @@ import * as DeprecatedAPI from '../deprecatedAPI'; import CONST from '../../CONST'; import * as PaymentMethods from './PaymentMethods'; import * as Localize from '../Localize'; +import * as API from "../API"; /** * Fetch and save locally the Onfido SDK token and applicantID @@ -351,8 +352,37 @@ function updateCurrentStep(currentStep) { Onyx.merge(ONYXKEYS.USER_WALLET, {currentStep}); } -function answerQuestionsForWallet() { - +/** + * @param {String} idologyAnswers + */ +function answerQuestionsForWallet(idologyAnswers) { + API.write('AnswerQuestionsForWallet', + { + idologyAnswers, + }, + { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + value: { + isLoading: true, + }, + }], + successData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + value: { + isLoading: false, + }, + }], + failureData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + value: { + isLoading: false, + }, + }], + }); } export { From 77f30ded65b930a2bb1a9905decccf4b7a4dc34e Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 16:15:44 +0100 Subject: [PATCH 008/155] calling api from the right js file --- src/libs/actions/BankAccounts.js | 1 + src/libs/actions/Wallet.js | 1 + src/pages/EnablePayments/IdologyQuestions.js | 11 +++++------ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 8fb86b3c25dd..2b7be34287f4 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -34,6 +34,7 @@ export { fetchOnfidoToken, activateWallet, fetchUserWallet, + answerQuestionsForWallet, } from './Wallet'; function clearPersonalBankAccount() { diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index a5a2d302082b..0749055a2d87 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -395,4 +395,5 @@ export { setAdditionalDetailsQuestions, buildIdologyError, updateCurrentStep, + answerQuestionsForWallet, }; diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index cd8ff54411bd..3d711d4236a6 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -103,12 +103,11 @@ class IdologyQuestions extends React.Component { } } - BankAccounts.activateWallet(CONST.WALLET.STEP.ADDITIONAL_DETAILS, { - idologyAnswers: { - answers, - idNumber: this.props.idNumber, - }, - }); + let idologyAnswers = { + answers, + idNumber: this.props.idNumber, + }; + BankAccounts.answerQuestionsForWallet(idologyAnswers); return {answers, isLoading: true}; } From 29ae567a3eb5977c1b37f5e6eaa3e987d82922ae Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 16:21:08 +0100 Subject: [PATCH 009/155] adding onyxKey --- src/pages/EnablePayments/IdologyQuestions.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 3d711d4236a6..5b45963198a0 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -13,6 +13,9 @@ import Text from '../../components/Text'; import TextLink from '../../components/TextLink'; import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; +import compose from "../../libs/compose"; +import {withOnyx} from "react-native-onyx"; +import ONYXKEYS from "../../ONYXKEYS"; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -108,7 +111,7 @@ class IdologyQuestions extends React.Component { idNumber: this.props.idNumber, }; BankAccounts.answerQuestionsForWallet(idologyAnswers); - return {answers, isLoading: true}; + return {answers}; } // Else, show next question @@ -171,4 +174,8 @@ class IdologyQuestions extends React.Component { IdologyQuestions.propTypes = propTypes; IdologyQuestions.defaultProps = defaultProps; -export default withLocalize(IdologyQuestions); +export default compose(withLocalize(IdologyQuestions), withOnyx({ + additionalDetails: { + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + }, +}))(IdologyQuestions); From 30ad2f148fab6b5645ff3ca1581b0a6b094f744d Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 16:25:20 +0100 Subject: [PATCH 010/155] using the isLoading --- src/pages/EnablePayments/IdologyQuestions.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 5b45963198a0..2583300f6afd 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -33,11 +33,18 @@ const propTypes = { /** ID from Idology, referencing those questions */ idNumber: PropTypes.string, + + additionalDetails: PropTypes.shape({ + isLoading: PropTypes.bool, + }), }; const defaultProps = { questions: [], idNumber: '', + additionalDetails: { + isLoading: false, + }, }; class IdologyQuestions extends React.Component { @@ -57,9 +64,6 @@ class IdologyQuestions extends React.Component { /** Any error message */ errorMessage: '', - - /** Did the user just submitted all his answers? */ - isLoading: false, }; } @@ -163,7 +167,7 @@ class IdologyQuestions extends React.Component { this.form.scrollTo({y: 0, animated: true}); }} message={this.state.errorMessage} - isLoading={this.state.isLoading} + isLoading={this.props.additionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From 86fe203e690f98c4ef1e568d954a2e17f0121305 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Wed, 10 Aug 2022 15:14:57 +0100 Subject: [PATCH 011/155] linter requested changes --- src/libs/actions/Wallet.js | 2 +- src/pages/EnablePayments/IdologyQuestions.js | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 0749055a2d87..b2edf801e033 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -7,7 +7,7 @@ import * as DeprecatedAPI from '../deprecatedAPI'; import CONST from '../../CONST'; import * as PaymentMethods from './PaymentMethods'; import * as Localize from '../Localize'; -import * as API from "../API"; +import * as API from '../API'; /** * Fetch and save locally the Onfido SDK token and applicantID diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 2583300f6afd..92d1f47144d8 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -4,18 +4,17 @@ import PropTypes from 'prop-types'; import { View, } from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; import * as BankAccounts from '../../libs/actions/BankAccounts'; -import CONST from '../../CONST'; import Text from '../../components/Text'; import TextLink from '../../components/TextLink'; import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; -import compose from "../../libs/compose"; -import {withOnyx} from "react-native-onyx"; -import ONYXKEYS from "../../ONYXKEYS"; +import compose from '../../libs/compose'; +import ONYXKEYS from '../../ONYXKEYS'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -110,7 +109,7 @@ class IdologyQuestions extends React.Component { } } - let idologyAnswers = { + const idologyAnswers = { answers, idNumber: this.props.idNumber, }; From c31819fcfbf4d090eb121d576342213a1d9533aa Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Wed, 10 Aug 2022 15:34:06 +0100 Subject: [PATCH 012/155] using the errors and error code --- src/pages/EnablePayments/IdologyQuestions.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 92d1f47144d8..9a0eebdc89fd 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -35,6 +35,8 @@ const propTypes = { additionalDetails: PropTypes.shape({ isLoading: PropTypes.bool, + errors: PropTypes.arrayOf(PropTypes.string), + errorCode: PropTypes.string, }), }; @@ -43,6 +45,8 @@ const defaultProps = { idNumber: '', additionalDetails: { isLoading: false, + errors: [], + errorCode: '', }, }; @@ -160,12 +164,12 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={this.state.errorMessage} + message={_.isEmpty(this.props.additionalDetails.errors) ? this.props.additionalDetails.errors.find(x=>x!==undefined) : this.state.errorMessage : } isLoading={this.props.additionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From ed809baab39a77e1a5e2844cb399f4c10651de71 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Wed, 10 Aug 2022 16:53:29 +0100 Subject: [PATCH 013/155] need to json stringify first? --- src/libs/actions/Wallet.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index b2edf801e033..7d59733da7bc 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -356,9 +356,10 @@ function updateCurrentStep(currentStep) { * @param {String} idologyAnswers */ function answerQuestionsForWallet(idologyAnswers) { + const answers = JSON.stringify(idologyAnswers); API.write('AnswerQuestionsForWallet', { - idologyAnswers, + answers, }, { optimisticData: [{ From 7477c23b3ade0a9dce1ac35486f602609bca7b94 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Wed, 10 Aug 2022 16:53:48 +0100 Subject: [PATCH 014/155] I think this is where the error code will be used --- src/pages/EnablePayments/EnablePaymentsPage.js | 15 ++++++++++++++- src/pages/EnablePayments/IdologyQuestions.js | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index 53f25dfeffd7..c8d481649e7e 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -21,17 +21,27 @@ import Navigation from '../../libs/Navigation/Navigation'; import FailedKYC from './FailedKYC'; import compose from '../../libs/compose'; import withLocalize from '../../components/withLocalize'; +import PropTypes from "prop-types"; const propTypes = { /** Information about the network from Onyx */ network: networkPropTypes.isRequired, + + additionalDetails: PropTypes.shape({ + errorCode: PropTypes.string, + }), + ...userWalletPropTypes, }; const defaultProps = { // eslint-disable-next-line react/default-props-match-prop-types userWallet: {}, + + additionalDetails: { + errorCode: '', + }, }; class EnablePaymentsPage extends React.Component { @@ -56,7 +66,7 @@ class EnablePaymentsPage extends React.Component { return ; } - if (this.props.userWallet.shouldShowFailedKYC) { + if (this.props.additionalDetails.errorCode === 'kycFailed') { return ( { this.form.scrollTo({y: 0, animated: true}); From eb673763eba4accfca1107374ea882100fce527a Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Fri, 19 Aug 2022 16:25:06 +0100 Subject: [PATCH 015/155] remove import duplicated --- src/pages/EnablePayments/EnablePaymentsPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index feda0930409a..836c3103b7ba 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -22,7 +22,6 @@ import Navigation from '../../libs/Navigation/Navigation'; import FailedKYC from './FailedKYC'; import compose from '../../libs/compose'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import PropTypes from "prop-types"; const propTypes = { /** Information about the network from Onyx */ From c67d1844abe0c34b50c8b0702d9e09da56a67038 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Fri, 19 Aug 2022 16:34:08 +0100 Subject: [PATCH 016/155] linter removing extra line (bad merge?) --- src/pages/EnablePayments/EnablePaymentsPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index 836c3103b7ba..a9b5247f2e0f 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -27,7 +27,6 @@ const propTypes = { /** Information about the network from Onyx */ network: networkPropTypes.isRequired, - additionalDetails: PropTypes.shape({ errorCode: PropTypes.string, }), From ed6a04a2cecd11352e8431d887110ab23bdd161f Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Fri, 19 Aug 2022 16:58:20 +0100 Subject: [PATCH 017/155] fixing linter, I think it's good this time --- src/pages/EnablePayments/IdologyQuestions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 3344be78ef83..a92a47f450c4 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { View, } from 'react-native'; -import Onyx, {withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; @@ -169,7 +169,7 @@ class IdologyQuestions extends React.Component { onFixTheErrorsLinkPressed={() => { this.form.scrollTo({y: 0, animated: true}); }} - message={_.isEmpty(this.props.additionalDetails.errors) ? this.props.additionalDetails.errors.find(x=>x!==undefined) : this.state.errorMessage : } + message={_.isEmpty(this.props.additionalDetails.errors) ? _.find(this.props.additionalDetails.errors, error => error !== undefined) : this.state.errorMessage} isLoading={this.props.additionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From eb1342803056246277d0e462becee502008bf888 Mon Sep 17 00:00:00 2001 From: Eugene Voloshchak Date: Sat, 20 Aug 2022 19:40:09 +0300 Subject: [PATCH 018/155] Localize decimal separator for the rate field --- src/CONST.js | 1 - .../getPermittedDecimalSeparator/index.ios.js | 4 +++ .../getPermittedDecimalSeparator/index.js | 1 + .../reimburse/WorkspaceReimburseView.js | 25 ++++++++++++------- 4 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 src/libs/getPermittedDecimalSeparator/index.ios.js create mode 100644 src/libs/getPermittedDecimalSeparator/index.js diff --git a/src/CONST.js b/src/CONST.js index 0f90121754ea..425832a9a26d 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -706,7 +706,6 @@ const CONST = { CARD_SECURITY_CODE: /^[0-9]{3,4}$/, CARD_EXPIRATION_DATE: /^(0[1-9]|1[0-2])([^0-9])?([0-9]{4}|([0-9]{2}))$/, PAYPAL_ME_USERNAME: /^[a-zA-Z0-9]+$/, - RATE_VALUE: /^\d{1,8}(\.\d*)?$/, // Adapted from: https://gist.github.com/dperini/729294 // eslint-disable-next-line max-len diff --git a/src/libs/getPermittedDecimalSeparator/index.ios.js b/src/libs/getPermittedDecimalSeparator/index.ios.js new file mode 100644 index 000000000000..d25c2650506a --- /dev/null +++ b/src/libs/getPermittedDecimalSeparator/index.ios.js @@ -0,0 +1,4 @@ + +// On iOS keyboard can only have one symbol at a time (either dot or comma) so we accept both +// Details: https://expensify.slack.com/archives/C01GTK53T8Q/p1658936908481629 +export default () => '.,'; diff --git a/src/libs/getPermittedDecimalSeparator/index.js b/src/libs/getPermittedDecimalSeparator/index.js new file mode 100644 index 000000000000..573e0c9b3225 --- /dev/null +++ b/src/libs/getPermittedDecimalSeparator/index.js @@ -0,0 +1 @@ +export default localizedSeparator => localizedSeparator; diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.js index 9ae0b280e24f..fe918f163e36 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.js @@ -22,6 +22,7 @@ import * as Policy from '../../../libs/actions/Policy'; import withFullPolicy from '../withFullPolicy'; import CONST from '../../../CONST'; import Button from '../../../components/Button'; +import getPermittedDecimalSeparator from '../../../libs/getPermittedDecimalSeparator'; const propTypes = { /** The policy ID currently being configured */ @@ -77,19 +78,29 @@ class WorkspaceReimburseView extends React.Component { } getRateDisplayValue(value) { - const numValue = parseFloat(value); + const numValue = this.getNumericValue(value); if (Number.isNaN(numValue)) { return ''; } + return numValue.toString().replace('.', this.props.toLocaleDigit('.')).substring(0, value.length); + } + + getNumericValue(value) { + const numValue = parseFloat(value.toString().replace(this.props.toLocaleDigit('.'), '.')); + if (Number.isNaN(numValue)) { + return NaN; + } return numValue.toFixed(3); } setRate(value) { - const isInvalidRateValue = value !== '' && !CONST.REGEX.RATE_VALUE.test(value); + const decimalSeparator = this.props.toLocaleDigit('.'); + const rateValueRegex = RegExp(String.raw`^\d{1,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{0,3})?$`, 'i'); + const isInvalidRateValue = value !== '' && !rateValueRegex.test(value); this.setState(prevState => ({ - rateValue: !isInvalidRateValue ? value : prevState.rateValue, + rateValue: !isInvalidRateValue ? this.getRateDisplayValue(value) : prevState.rateValue, }), () => { // Set the corrected value with a delay and sync to the server this.updateRateValueDebounced(this.state.rateValue); @@ -115,20 +126,16 @@ class WorkspaceReimburseView extends React.Component { } updateRateValue(value) { - const numValue = parseFloat(value); + const numValue = this.getNumericValue(value); if (_.isNaN(numValue)) { return; } - this.setState({ - rateValue: numValue.toFixed(3), - }); - Policy.setCustomUnitRate(this.props.policyID, this.state.unitID, { customUnitRateID: this.state.rateID, name: this.state.rateName, - rate: numValue.toFixed(3) * 100, + rate: numValue * 100, }, null); } From d42b51b3ad5bde56882979cef4157a1f71dc6ac2 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 24 Aug 2022 21:59:15 +0530 Subject: [PATCH 019/155] Fix links for Native platforms --- src/components/AnchorForCommentsOnly.js | 107 ------------------ .../BaseAnchorForCommentsOnly.js | 69 +++++++++++ .../anchorForCommentsOnlyPropTypes.js | 42 +++++++ src/components/AnchorForCommentsOnly/index.js | 11 ++ .../AnchorForCommentsOnly/index.native.js | 20 ++++ .../HTMLRenderers/AnchorRenderer.js | 2 +- 6 files changed, 143 insertions(+), 108 deletions(-) delete mode 100644 src/components/AnchorForCommentsOnly.js create mode 100644 src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js create mode 100644 src/components/AnchorForCommentsOnly/anchorForCommentsOnlyPropTypes.js create mode 100644 src/components/AnchorForCommentsOnly/index.js create mode 100644 src/components/AnchorForCommentsOnly/index.native.js diff --git a/src/components/AnchorForCommentsOnly.js b/src/components/AnchorForCommentsOnly.js deleted file mode 100644 index c93ecb416b64..000000000000 --- a/src/components/AnchorForCommentsOnly.js +++ /dev/null @@ -1,107 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import {StyleSheet} from 'react-native'; -import lodashGet from 'lodash/get'; -import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; -import Text from './Text'; -import PressableWithSecondaryInteraction from './PressableWithSecondaryInteraction'; -import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; -import * as ContextMenuActions from '../pages/home/report/ContextMenu/ContextMenuActions'; -import Tooltip from './Tooltip'; -import canUseTouchScreen from '../libs/canUseTouchscreen'; -import styles from '../styles/styles'; -import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; - -const propTypes = { - /** The URL to open */ - href: PropTypes.string, - - /** What headers to send to the linked page (usually noopener and noreferrer) - This is unused in native, but is here for parity with web */ - rel: PropTypes.string, - - /** Used to determine where to open a link ("_blank" is passed for a new tab) - This is unused in native, but is here for parity with web */ - target: PropTypes.string, - - /** Any children to display */ - children: PropTypes.node, - - /** Anchor text of URLs or emails. */ - displayName: PropTypes.string, - - /** Any additional styles to apply */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, - - /** Press handler for the link, when not passed, default href is used to create a link like behaviour */ - onPress: PropTypes.func, - - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - href: '', - rel: '', - target: '', - children: null, - style: {}, - displayName: '', - onPress: undefined, -}; - -/* - * This is a default anchor component for regular links. - */ -const BaseAnchorForCommentsOnly = (props) => { - let linkRef; - const rest = _.omit(props, _.keys(propTypes)); - const linkProps = {}; - if (_.isFunction(props.onPress)) { - linkProps.onPress = props.onPress; - } else { - linkProps.href = props.href; - } - const defaultTextStyle = canUseTouchScreen() || props.isSmallScreenWidth ? {} : styles.userSelectText; - - return ( - { - ReportActionContextMenu.showContextMenu( - Str.isValidEmail(props.displayName) ? ContextMenuActions.CONTEXT_MENU_TYPES.EMAIL : ContextMenuActions.CONTEXT_MENU_TYPES.LINK, - event, - props.href, - lodashGet(linkRef, 'current'), - ); - } - } - > - - linkRef = el} - style={StyleSheet.flatten([props.style, defaultTextStyle])} - accessibilityRole="link" - hrefAttrs={{ - rel: props.rel, - target: props.target, - }} - // eslint-disable-next-line react/jsx-props-no-spreading - {...linkProps} - // eslint-disable-next-line react/jsx-props-no-spreading - {...rest} - > - {props.children} - - - - ); -}; - -BaseAnchorForCommentsOnly.propTypes = propTypes; -BaseAnchorForCommentsOnly.defaultProps = defaultProps; -BaseAnchorForCommentsOnly.displayName = 'BaseAnchorForCommentsOnly'; - -export default withWindowDimensions(BaseAnchorForCommentsOnly); diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js new file mode 100644 index 000000000000..e65b423fa517 --- /dev/null +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js @@ -0,0 +1,69 @@ +import _ from 'underscore'; +import React from 'react'; +import {StyleSheet} from 'react-native'; +import lodashGet from 'lodash/get'; +import Str from 'expensify-common/lib/str'; +import Text from '../Text'; +import PressableWithSecondaryInteraction from '../PressableWithSecondaryInteraction'; +import * as ReportActionContextMenu from '../../pages/home/report/ContextMenu/ReportActionContextMenu'; +import * as ContextMenuActions from '../../pages/home/report/ContextMenu/ContextMenuActions'; +import Tooltip from '../Tooltip'; +import canUseTouchScreen from '../../libs/canUseTouchscreen'; +import styles from '../../styles/styles'; +import withWindowDimensions from '../withWindowDimensions'; +import {propTypes, defaultProps} from './anchorForCommentsOnlyPropTypes'; + +/* + * This is a default anchor component for regular links. + */ +const BaseAnchorForCommentsOnly = (props) => { + let linkRef; + const rest = _.omit(props, _.keys(propTypes)); + const linkProps = {}; + if (_.isFunction(props.onPress)) { + linkProps.onPress = props.onPress; + } else { + linkProps.href = props.href; + } + const defaultTextStyle = canUseTouchScreen() || props.isSmallScreenWidth ? {} : styles.userSelectText; + + return ( + { + ReportActionContextMenu.showContextMenu( + Str.isValidEmail(props.displayName) ? ContextMenuActions.CONTEXT_MENU_TYPES.EMAIL : ContextMenuActions.CONTEXT_MENU_TYPES.LINK, + event, + props.href, + lodashGet(linkRef, 'current'), + ); + } + } + > + + linkRef = el} + style={StyleSheet.flatten([props.style, defaultTextStyle])} + accessibilityRole="link" + hrefAttrs={{ + rel: props.rel, + target: props.target, + }} + // eslint-disable-next-line react/jsx-props-no-spreading + {...linkProps} + // eslint-disable-next-line react/jsx-props-no-spreading + {...rest} + > + {props.children} + + + + ); +}; + +BaseAnchorForCommentsOnly.propTypes = propTypes; +BaseAnchorForCommentsOnly.defaultProps = defaultProps; +BaseAnchorForCommentsOnly.displayName = 'BaseAnchorForCommentsOnly'; + +export default withWindowDimensions(BaseAnchorForCommentsOnly); diff --git a/src/components/AnchorForCommentsOnly/anchorForCommentsOnlyPropTypes.js b/src/components/AnchorForCommentsOnly/anchorForCommentsOnlyPropTypes.js new file mode 100644 index 000000000000..7661efaf0af3 --- /dev/null +++ b/src/components/AnchorForCommentsOnly/anchorForCommentsOnlyPropTypes.js @@ -0,0 +1,42 @@ +import PropTypes from 'prop-types'; +import stylePropTypes from '../../styles/stylePropTypes'; +import {windowDimensionsPropTypes} from '../withWindowDimensions'; + +const propTypes = { + /** The URL to open */ + href: PropTypes.string, + + /** What headers to send to the linked page (usually noopener and noreferrer) + This is unused in native, but is here for parity with web */ + rel: PropTypes.string, + + /** Used to determine where to open a link ("_blank" is passed for a new tab) + This is unused in native, but is here for parity with web */ + target: PropTypes.string, + + /** Any children to display */ + children: PropTypes.node, + + /** Anchor text of URLs or emails. */ + displayName: PropTypes.string, + + /** Any additional styles to apply */ + style: stylePropTypes, + + /** Press handler for the link, when not passed, default href is used to create a link like behaviour */ + onPress: PropTypes.func, + + ...windowDimensionsPropTypes, +}; + +const defaultProps = { + href: '', + rel: '', + target: '', + children: null, + style: {}, + displayName: '', + onPress: undefined, +}; + +export {propTypes, defaultProps}; diff --git a/src/components/AnchorForCommentsOnly/index.js b/src/components/AnchorForCommentsOnly/index.js new file mode 100644 index 000000000000..1526e78007fe --- /dev/null +++ b/src/components/AnchorForCommentsOnly/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import * as anchorForCommentsOnlyPropTypes from './anchorForCommentsOnlyPropTypes'; +import BaseAnchorForCommentsOnly from './BaseAnchorForCommentsOnly'; + +// eslint-disable-next-line react/jsx-props-no-spreading +const AnchorForCommentsOnly = props => ; +AnchorForCommentsOnly.propTypes = anchorForCommentsOnlyPropTypes.propTypes; +AnchorForCommentsOnly.defaultProps = anchorForCommentsOnlyPropTypes.defaultProps; +AnchorForCommentsOnly.displayName = 'AnchorForCommentsOnly'; + +export default AnchorForCommentsOnly; diff --git a/src/components/AnchorForCommentsOnly/index.native.js b/src/components/AnchorForCommentsOnly/index.native.js new file mode 100644 index 000000000000..e692ea57afbb --- /dev/null +++ b/src/components/AnchorForCommentsOnly/index.native.js @@ -0,0 +1,20 @@ +import React from 'react'; +import {Linking} from 'react-native'; +import _ from 'underscore'; + +import * as anchorForCommentsOnlyPropTypes from './anchorForCommentsOnlyPropTypes'; +import BaseAnchorForCommentsOnly from './BaseAnchorForCommentsOnly'; + +// eslint-disable-next-line react/jsx-props-no-spreading +const AnchorForCommentsOnly = (props) => { + const onPress = () => (_.isFunction(props.onPress) ? props.onPress() : Linking.openURL(props.href)); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +}; + +AnchorForCommentsOnly.propTypes = anchorForCommentsOnlyPropTypes.propTypes; +AnchorForCommentsOnly.defaultProps = anchorForCommentsOnlyPropTypes.defaultProps; +AnchorForCommentsOnly.displayName = 'AnchorForCommentsOnly'; + +export default AnchorForCommentsOnly; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js index 9d395a8779b9..36adfe30f6b5 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js @@ -85,7 +85,7 @@ const AnchorRenderer = (props) => { displayName={displayName} // Only pass the press handler for internal links, for public links fallback to default link handling - onPress={internalNewExpensifyPath || internalExpensifyPath ? navigateToLink : undefined} + onPress={(internalNewExpensifyPath || internalExpensifyPath) ? navigateToLink : undefined} > From fed705a508371e463dc87f77fcbeb4c6d22549e2 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 25 Aug 2022 20:03:33 +0200 Subject: [PATCH 020/155] Remove the last pieces out of state --- src/pages/home/sidebar/SidebarLinks.js | 265 +++++++++++++++++-------- 1 file changed, 179 insertions(+), 86 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 6e4fdeefc63c..f315178b70a7 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -120,82 +120,131 @@ function getUnreadReports(reportsObject) { const memoizeGetUnreadReports = memoizeOne(getUnreadReports); class SidebarLinks extends React.Component { - static getRecentReports(props) { - const activeReportID = parseInt(props.currentlyViewedReportID, 10); - const sidebarOptions = OptionsListUtils.getSidebarOptions( - props.reports, - props.personalDetails, - activeReportID, - props.priorityMode, - props.betas, - props.reportActions, - ); - return sidebarOptions.recentReports; - } + // static getRecentReports(props) { + // const activeReportID = parseInt(props.currentlyViewedReportID, 10); + // const sidebarOptions = OptionsListUtils.getSidebarOptions( + // props.reports, + // props.personalDetails, + // activeReportID, + // props.priorityMode, + // props.betas, + // props.reportActions, + // ); + // return sidebarOptions.recentReports; + // } + + // /** + // * Returns true if the sidebar list should be re-ordered + // * + // * @param {Object} nextProps + // * @param {Boolean} hasActiveDraftHistory + // * @param {Array} orderedReports + // * @param {String} currentlyViewedReportID + // * @param {Array} unreadReports + // * @returns {Boolean} + // */ + // static shouldReorder(nextProps, hasActiveDraftHistory, orderedReports, currentlyViewedReportID, unreadReports) { + // // We do not want to re-order reports in the LHN if the only change is the draft comment in the + // // current report. + // + // // We don't need to limit draft comment flashing for small screen widths as LHN is not visible. + // if (nextProps.isSmallScreenWidth) { + // return true; + // } + // + // // Always update if LHN is empty. + // if (orderedReports.length === 0) { + // return true; + // } + // + // const didActiveReportChange = currentlyViewedReportID !== nextProps.currentlyViewedReportID; + // + // // Always re-order the list whenever the active report is changed + // if (didActiveReportChange) { + // return true; + // } + // + // // If any reports have new unread messages, re-order the list + // const nextUnreadReports = memoizeGetUnreadReports(nextProps.reports || {}); + // if (memoizeCheckForNewUnreadReports(nextUnreadReports, unreadReports)) { + // return true; + // } + // + // // If there is an active report that either had or has a draft, we do not want to re-order the list + // if (nextProps.currentlyViewedReportID && hasActiveDraftHistory) { + // return false; + // } + // + // return true; + // } - /** - * Returns true if the sidebar list should be re-ordered - * - * @param {Object} nextProps - * @param {Boolean} hasActiveDraftHistory - * @param {Array} orderedReports - * @param {String} currentlyViewedReportID - * @param {Array} unreadReports - * @returns {Boolean} - */ - static shouldReorder(nextProps, hasActiveDraftHistory, orderedReports, currentlyViewedReportID, unreadReports) { - // We do not want to re-order reports in the LHN if the only change is the draft comment in the - // current report. + constructor(props) { + super(props); - // We don't need to limit draft comment flashing for small screen widths as LHN is not visible. - if (nextProps.isSmallScreenWidth) { - return true; - } + this.getFilteredReports = _.memoize(this.getFilteredReports); - // Always update if LHN is empty. - if (orderedReports.length === 0) { - return true; - } + this.activeReport = { + reportID: props.currentlyViewedReportID, + }; - const didActiveReportChange = currentlyViewedReportID !== nextProps.currentlyViewedReportID; + this.orderedReports = []; + this.priorityMode = props.priorityMode; + this.unreadReports = memoizeGetUnreadReports(props.reports || {}); + } - // Always re-order the list whenever the active report is changed - if (didActiveReportChange) { - return true; - } + static getDerivedStateFromProps(nextProps, prevState) { + // const isActiveReportSame = prevState.activeReport.reportID === nextProps.currentlyViewedReportID; + // const lastMessageTimestamp = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.lastMessageTimestamp`, 0); - // If any reports have new unread messages, re-order the list - const nextUnreadReports = memoizeGetUnreadReports(nextProps.reports || {}); - if (memoizeCheckForNewUnreadReports(nextUnreadReports, unreadReports)) { - return true; - } + // Determines if the active report has a history of draft comments while active. + // let hasDraftHistory; - // If there is an active report that either had or has a draft, we do not want to re-order the list - if (nextProps.currentlyViewedReportID && hasActiveDraftHistory) { - return false; - } + // If the active report has not changed and the message has been sent, set the draft history flag to false so LHN can reorder. + // Otherwise, if the active report has not changed and the flag was previously true, preserve the state so LHN cannot reorder. + // Otherwise, update the flag from the prop value. + // if (isActiveReportSame && prevState.activeReport.lastMessageTimestamp !== lastMessageTimestamp) { + // hasDraftHistory = false; + // } else if (isActiveReportSame && prevState.activeReport.hasDraftHistory) { + // hasDraftHistory = true; + // } else { + // hasDraftHistory = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.hasDraft`, false); + // } - return true; - } + // const shouldReorder = SidebarLinks.shouldReorder(nextProps, hasDraftHistory, prevState.orderedReports, prevState.activeReport.reportID, prevState.unreadReports); + // const switchingPriorityModes = nextProps.priorityMode !== prevState.priorityMode; - constructor(props) { - super(props); + // Build the report options we want to show + // const recentReports = SidebarLinks.getRecentReports(nextProps); - this.state = { - activeReport: { - reportID: props.currentlyViewedReportID, - hasDraftHistory: lodashGet(props.reports, `${ONYXKEYS.COLLECTION.REPORT}${props.currentlyViewedReportID}.hasDraft`, false), - lastMessageTimestamp: lodashGet(props.reports, `${ONYXKEYS.COLLECTION.REPORT}${props.currentlyViewedReportID}.lastMessageTimestamp`, 0), - }, - orderedReports: [], - priorityMode: props.priorityMode, - unreadReports: memoizeGetUnreadReports(props.reports || {}), - }; + // Determine whether we need to keep the previous LHN order + // const orderedReports = shouldReorder || switchingPriorityModes + // ? recentReports + // : _.chain(prevState.orderedReports) + // + // // To preserve the order of the conversations, we map over the previous state's order of reports. + // // Then match and replace older reports with the newer report conversations from recentReports + // .map(orderedReport => _.find(recentReports, recentReport => orderedReport.reportID === recentReport.reportID)) + // + // // Because we are using map, we have to filter out any undefined reports. This happens if recentReports + // // does not have all the conversations in prevState.orderedReports + // .filter(orderedReport => orderedReport !== undefined) + // .value(); + + // return { + // orderedReports, + // priorityMode: nextProps.priorityMode, + // activeReport: { + // reportID: nextProps.currentlyViewedReportID, + // hasDraftHistory, + // lastMessageTimestamp, + // }, + // unreadReports: memoizeGetUnreadReports(nextProps.reports || {}), + // }; } - static getDerivedStateFromProps(nextProps, prevState) { - const isActiveReportSame = prevState.activeReport.reportID === nextProps.currentlyViewedReportID; - const lastMessageTimestamp = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.lastMessageTimestamp`, 0); + getFilteredReports(unfilteredReports) { + const isActiveReportSame = this.activeReport.reportID === this.props.currentlyViewedReportID; + const lastMessageTimestamp = lodashGet(unfilteredReports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.lastMessageTimestamp`, 0); // Determines if the active report has a history of draft comments while active. let hasDraftHistory; @@ -203,44 +252,88 @@ class SidebarLinks extends React.Component { // If the active report has not changed and the message has been sent, set the draft history flag to false so LHN can reorder. // Otherwise, if the active report has not changed and the flag was previously true, preserve the state so LHN cannot reorder. // Otherwise, update the flag from the prop value. - if (isActiveReportSame && prevState.activeReport.lastMessageTimestamp !== lastMessageTimestamp) { + if (isActiveReportSame && this.activeReport.lastMessageTimestamp !== lastMessageTimestamp) { hasDraftHistory = false; - } else if (isActiveReportSame && prevState.activeReport.hasDraftHistory) { + } else if (isActiveReportSame && this.activeReport.hasDraftHistory) { hasDraftHistory = true; } else { - hasDraftHistory = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.hasDraft`, false); + hasDraftHistory = lodashGet(this.props.reports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.hasDraft`, false); } - const shouldReorder = SidebarLinks.shouldReorder(nextProps, hasDraftHistory, prevState.orderedReports, prevState.activeReport.reportID, prevState.unreadReports); - const switchingPriorityModes = nextProps.priorityMode !== prevState.priorityMode; + const shouldReorder = this.shouldReorder(hasDraftHistory); + const switchingPriorityModes = this.props.priorityMode !== this.priorityMode; // Build the report options we want to show - const recentReports = SidebarLinks.getRecentReports(nextProps); + const recentReports = this.getRecentReports(); - // Determine whether we need to keep the previous LHN order - const orderedReports = shouldReorder || switchingPriorityModes + this.orderedReports = shouldReorder || switchingPriorityModes ? recentReports - : _.chain(prevState.orderedReports) + : _.chain(this.orderedReports) - // To preserve the order of the conversations, we map over the previous state's order of reports. - // Then match and replace older reports with the newer report conversations from recentReports + // To preserve the order of the conversations, we map over the previous state's order of reports. + // Then match and replace older reports with the newer report conversations from recentReports .map(orderedReport => _.find(recentReports, recentReport => orderedReport.reportID === recentReport.reportID)) - // Because we are using map, we have to filter out any undefined reports. This happens if recentReports - // does not have all the conversations in prevState.orderedReports + // Because we are using map, we have to filter out any undefined reports. This happens if recentReports + // does not have all the conversations in prevState.orderedReports .filter(orderedReport => orderedReport !== undefined) .value(); - return { - orderedReports, - priorityMode: nextProps.priorityMode, - activeReport: { - reportID: nextProps.currentlyViewedReportID, - hasDraftHistory, - lastMessageTimestamp, - }, - unreadReports: memoizeGetUnreadReports(nextProps.reports || {}), + this.priorityMode = this.props.priorityMode; + this.activeReport = { + reportID: this.props.currentlyViewedReportID, + hasDraftHistory, + lastMessageTimestamp, }; + this.unreadReports = memoizeGetUnreadReports(unfilteredReports || {}); + } + + getRecentReports() { + const activeReportID = parseInt(this.props.currentlyViewedReportID, 10); + const sidebarOptions = OptionsListUtils.getSidebarOptions( + this.props.reports, + this.props.personalDetails, + activeReportID, + this.props.priorityMode, + this.props.betas, + this.props.reportActions, + ); + return sidebarOptions.recentReports; + } + + shouldReorder(hasDraftHistory) { + // We do not want to re-order reports in the LHN if the only change is the draft comment in the + // current report. + + // We don't need to limit draft comment flashing for small screen widths as LHN is not visible. + if (this.props.isSmallScreenWidth) { + return true; + } + + // Always update if LHN is empty. + if (this.orderedReports.length === 0) { + return true; + } + + const didActiveReportChange = this.activeReport.reportID !== this.props.currentlyViewedReportID; + + // Always re-order the list whenever the active report is changed + if (didActiveReportChange) { + return true; + } + + // If any reports have new unread messages, re-order the list + const nextUnreadReports = memoizeGetUnreadReports(this.props.reports || {}); + if (memoizeCheckForNewUnreadReports(nextUnreadReports, this.unreadReports)) { + return true; + } + + // If there is an active report that either had or has a draft, we do not want to re-order the list + if (this.props.currentlyViewedReportID && hasDraftHistory) { + return false; + } + + return true; } showSearchPage() { From 44a2ff60ed4f00429dd6e1759a49c39fd16ef079 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 07:27:42 +0100 Subject: [PATCH 021/155] Simplify the check for unread reports --- src/pages/home/sidebar/SidebarLinks.js | 179 +++++++++++++------------ 1 file changed, 92 insertions(+), 87 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index f315178b70a7..b4fb87f8e254 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -93,31 +93,31 @@ const defaultProps = { isSyncingData: false, }; -/** - * @param {Object} nextUnreadReports - * @param {Object} unreadReports - * @returns {Boolean} - */ -function checkForNewUnreadReports(nextUnreadReports, unreadReports) { - return nextUnreadReports.length > 0 - && _.some(nextUnreadReports, - nextUnreadReport => !_.some(unreadReports, unreadReport => unreadReport.reportID === nextUnreadReport.reportID)); -} -const memoizeCheckForNewUnreadReports = memoizeOne(checkForNewUnreadReports); - -/** - * @param {Object} reportsObject - * @returns {Array} - */ -function getUnreadReports(reportsObject) { - const reports = _.values(reportsObject); - if (reports.length === 0) { - return []; - } - const unreadReports = _.filter(reports, report => report && report.unreadActionCount > 0); - return unreadReports; -} -const memoizeGetUnreadReports = memoizeOne(getUnreadReports); +// /** +// * @param {Object} nextUnreadReports +// * @param {Object} unreadReports +// * @returns {Boolean} +// */ +// function checkForNewUnreadReports(nextUnreadReports, unreadReports) { +// return nextUnreadReports.length > 0 +// && _.some(nextUnreadReports, +// nextUnreadReport => !_.some(unreadReports, unreadReport => unreadReport.reportID === nextUnreadReport.reportID)); +// } +// const memoizeCheckForNewUnreadReports = memoizeOne(checkForNewUnreadReports); +// +// /** +// * @param {Object} reportsObject +// * @returns {Array} +// */ +// function getUnreadReports(reportsObject) { +// const reports = _.values(reportsObject); +// if (reports.length === 0) { +// return []; +// } +// const unreadReports = _.filter(reports, report => report && report.unreadActionCount > 0); +// return unreadReports; +// } +// const memoizeGetUnreadReports = memoizeOne(getUnreadReports); class SidebarLinks extends React.Component { // static getRecentReports(props) { @@ -192,55 +192,55 @@ class SidebarLinks extends React.Component { this.unreadReports = memoizeGetUnreadReports(props.reports || {}); } - static getDerivedStateFromProps(nextProps, prevState) { - // const isActiveReportSame = prevState.activeReport.reportID === nextProps.currentlyViewedReportID; - // const lastMessageTimestamp = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.lastMessageTimestamp`, 0); - - // Determines if the active report has a history of draft comments while active. - // let hasDraftHistory; - - // If the active report has not changed and the message has been sent, set the draft history flag to false so LHN can reorder. - // Otherwise, if the active report has not changed and the flag was previously true, preserve the state so LHN cannot reorder. - // Otherwise, update the flag from the prop value. - // if (isActiveReportSame && prevState.activeReport.lastMessageTimestamp !== lastMessageTimestamp) { - // hasDraftHistory = false; - // } else if (isActiveReportSame && prevState.activeReport.hasDraftHistory) { - // hasDraftHistory = true; - // } else { - // hasDraftHistory = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.hasDraft`, false); - // } - - // const shouldReorder = SidebarLinks.shouldReorder(nextProps, hasDraftHistory, prevState.orderedReports, prevState.activeReport.reportID, prevState.unreadReports); - // const switchingPriorityModes = nextProps.priorityMode !== prevState.priorityMode; - - // Build the report options we want to show - // const recentReports = SidebarLinks.getRecentReports(nextProps); - - // Determine whether we need to keep the previous LHN order - // const orderedReports = shouldReorder || switchingPriorityModes - // ? recentReports - // : _.chain(prevState.orderedReports) - // - // // To preserve the order of the conversations, we map over the previous state's order of reports. - // // Then match and replace older reports with the newer report conversations from recentReports - // .map(orderedReport => _.find(recentReports, recentReport => orderedReport.reportID === recentReport.reportID)) - // - // // Because we are using map, we have to filter out any undefined reports. This happens if recentReports - // // does not have all the conversations in prevState.orderedReports - // .filter(orderedReport => orderedReport !== undefined) - // .value(); - - // return { - // orderedReports, - // priorityMode: nextProps.priorityMode, - // activeReport: { - // reportID: nextProps.currentlyViewedReportID, - // hasDraftHistory, - // lastMessageTimestamp, - // }, - // unreadReports: memoizeGetUnreadReports(nextProps.reports || {}), - // }; - } + // static getDerivedStateFromProps(nextProps, prevState) { + // // const isActiveReportSame = prevState.activeReport.reportID === nextProps.currentlyViewedReportID; + // // const lastMessageTimestamp = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.lastMessageTimestamp`, 0); + // + // // Determines if the active report has a history of draft comments while active. + // // let hasDraftHistory; + // + // // If the active report has not changed and the message has been sent, set the draft history flag to false so LHN can reorder. + // // Otherwise, if the active report has not changed and the flag was previously true, preserve the state so LHN cannot reorder. + // // Otherwise, update the flag from the prop value. + // // if (isActiveReportSame && prevState.activeReport.lastMessageTimestamp !== lastMessageTimestamp) { + // // hasDraftHistory = false; + // // } else if (isActiveReportSame && prevState.activeReport.hasDraftHistory) { + // // hasDraftHistory = true; + // // } else { + // // hasDraftHistory = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.hasDraft`, false); + // // } + // + // // const shouldReorder = SidebarLinks.shouldReorder(nextProps, hasDraftHistory, prevState.orderedReports, prevState.activeReport.reportID, prevState.unreadReports); + // // const switchingPriorityModes = nextProps.priorityMode !== prevState.priorityMode; + // + // // Build the report options we want to show + // // const recentReports = SidebarLinks.getRecentReports(nextProps); + // + // // Determine whether we need to keep the previous LHN order + // // const orderedReports = shouldReorder || switchingPriorityModes + // // ? recentReports + // // : _.chain(prevState.orderedReports) + // // + // // // To preserve the order of the conversations, we map over the previous state's order of reports. + // // // Then match and replace older reports with the newer report conversations from recentReports + // // .map(orderedReport => _.find(recentReports, recentReport => orderedReport.reportID === recentReport.reportID)) + // // + // // // Because we are using map, we have to filter out any undefined reports. This happens if recentReports + // // // does not have all the conversations in prevState.orderedReports + // // .filter(orderedReport => orderedReport !== undefined) + // // .value(); + // + // // return { + // // orderedReports, + // // priorityMode: nextProps.priorityMode, + // // activeReport: { + // // reportID: nextProps.currentlyViewedReportID, + // // hasDraftHistory, + // // lastMessageTimestamp, + // // }, + // // unreadReports: memoizeGetUnreadReports(nextProps.reports || {}), + // // }; + // } getFilteredReports(unfilteredReports) { const isActiveReportSame = this.activeReport.reportID === this.props.currentlyViewedReportID; @@ -302,38 +302,43 @@ class SidebarLinks extends React.Component { } shouldReorder(hasDraftHistory) { - // We do not want to re-order reports in the LHN if the only change is the draft comment in the - // current report. - // We don't need to limit draft comment flashing for small screen widths as LHN is not visible. + // Because: TBD + // @TODO try and figure out why if (this.props.isSmallScreenWidth) { return true; } // Always update if LHN is empty. + // Because: TBD + // @TODO try and figure out why if (this.orderedReports.length === 0) { return true; } - const didActiveReportChange = this.activeReport.reportID !== this.props.currentlyViewedReportID; - // Always re-order the list whenever the active report is changed - if (didActiveReportChange) { - return true; - } - - // If any reports have new unread messages, re-order the list - const nextUnreadReports = memoizeGetUnreadReports(this.props.reports || {}); - if (memoizeCheckForNewUnreadReports(nextUnreadReports, this.unreadReports)) { + // Because: TBD + // @TODO try and figure out why + if (this.activeReport.reportID !== this.props.currentlyViewedReportID) { return true; } // If there is an active report that either had or has a draft, we do not want to re-order the list + // because the position of the report in the list won't change if (this.props.currentlyViewedReportID && hasDraftHistory) { return false; } - return true; + // If any reports have new unread messages, the list needs to be reordered + // because the unread reports need to be placed at the top of the list + const hasNewUnreadReports = _.some(this.props.reports, (report) => { + return report.unreadActionCount > 0 && !this.unreadReports[report.reportID]; + }); + if (hasNewUnreadReports) { + return true; + } + + return false; } showSearchPage() { From 57a4c2e3bba88581e34315d50e80967fe280149e Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 07:38:43 +0100 Subject: [PATCH 022/155] Simplify the map of unread reports --- src/pages/home/sidebar/SidebarLinks.js | 32 ++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index b4fb87f8e254..382569468b64 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -189,7 +189,7 @@ class SidebarLinks extends React.Component { this.orderedReports = []; this.priorityMode = props.priorityMode; - this.unreadReports = memoizeGetUnreadReports(props.reports || {}); + this.unreadReports = this.getUnreadReports(props.reports); } // static getDerivedStateFromProps(nextProps, prevState) { @@ -285,7 +285,31 @@ class SidebarLinks extends React.Component { hasDraftHistory, lastMessageTimestamp, }; - this.unreadReports = memoizeGetUnreadReports(unfilteredReports || {}); + this.unreadReports = this.getUnreadReports(unfilteredReports); + } + + /** + * Create a map of unread reports that looks like this: + * { + * 1: true, + * 2: true, + * } + * This is so that when the new props are compared to the old props, it's + * fast to look up if there are any new unread reports. + * + * @param {Object[]} reports + * @returns {Object} + */ + getUnreadReports(reports) { + return _.reduce(unfilteredReports, (finalUnreadReportMap, report) => { + if (report.unreadActionCount > 0) { + return { + [report.reportID]: true, + ...finalUnreadReportMap, + }; + } + return finalUnreadReportMap; + }, {}); } getRecentReports() { @@ -331,9 +355,7 @@ class SidebarLinks extends React.Component { // If any reports have new unread messages, the list needs to be reordered // because the unread reports need to be placed at the top of the list - const hasNewUnreadReports = _.some(this.props.reports, (report) => { - return report.unreadActionCount > 0 && !this.unreadReports[report.reportID]; - }); + const hasNewUnreadReports = _.some(this.props.reports, report => report.unreadActionCount > 0 && !this.unreadReports[report.reportID]); if (hasNewUnreadReports) { return true; } From 4b40492308160b8eba91d27be6e3d591384225f6 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 07:39:30 +0100 Subject: [PATCH 023/155] Correct outdated comment --- src/pages/home/sidebar/SidebarLinks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 382569468b64..c0c0442009a7 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -368,7 +368,7 @@ class SidebarLinks extends React.Component { } render() { - // Wait until the reports and personalDetails are actually loaded before displaying the LHN + // Wait until the personalDetails are actually loaded before displaying the LHN if (_.isEmpty(this.props.personalDetails)) { return null; } From 00ebe57e4d931d93a4b40dbe9fbc4a0b5b6eac97 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 07:42:02 +0100 Subject: [PATCH 024/155] Rename method to be more apparent --- src/pages/home/sidebar/SidebarLinks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index c0c0442009a7..60a99f90da8f 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -260,7 +260,7 @@ class SidebarLinks extends React.Component { hasDraftHistory = lodashGet(this.props.reports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.hasDraft`, false); } - const shouldReorder = this.shouldReorder(hasDraftHistory); + const shouldReorder = this.shouldReorderReports(hasDraftHistory); const switchingPriorityModes = this.props.priorityMode !== this.priorityMode; // Build the report options we want to show @@ -301,7 +301,7 @@ class SidebarLinks extends React.Component { * @returns {Object} */ getUnreadReports(reports) { - return _.reduce(unfilteredReports, (finalUnreadReportMap, report) => { + return _.reduce(reports, (finalUnreadReportMap, report) => { if (report.unreadActionCount > 0) { return { [report.reportID]: true, @@ -325,7 +325,7 @@ class SidebarLinks extends React.Component { return sidebarOptions.recentReports; } - shouldReorder(hasDraftHistory) { + shouldReorderReports(hasDraftHistory) { // We don't need to limit draft comment flashing for small screen widths as LHN is not visible. // Because: TBD // @TODO try and figure out why From 103b126e819c6112813cc64c9e18a72ebf96939d Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 07:49:52 +0100 Subject: [PATCH 025/155] Access filtered report options direction from render --- src/pages/home/sidebar/SidebarLinks.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 60a99f90da8f..1783f60faa73 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -264,7 +264,7 @@ class SidebarLinks extends React.Component { const switchingPriorityModes = this.props.priorityMode !== this.priorityMode; // Build the report options we want to show - const recentReports = this.getRecentReports(); + const recentReports = this.getRecentReportsOptionListItems(); this.orderedReports = shouldReorder || switchingPriorityModes ? recentReports @@ -286,6 +286,8 @@ class SidebarLinks extends React.Component { lastMessageTimestamp, }; this.unreadReports = this.getUnreadReports(unfilteredReports); + + return this.orderedReports; } /** @@ -312,7 +314,7 @@ class SidebarLinks extends React.Component { }, {}); } - getRecentReports() { + getRecentReportsOptionListItems() { const activeReportID = parseInt(this.props.currentlyViewedReportID, 10); const sidebarOptions = OptionsListUtils.getSidebarOptions( this.props.reports, @@ -377,7 +379,7 @@ class SidebarLinks extends React.Component { const sections = [{ title: '', indexOffset: 0, - data: this.state.orderedReports || [], + data: this.getFilteredReports(this.props.reports), shouldShow: true, }]; From bb57fb055365b9d173e0675adb377024af2aeccb Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 17:33:03 +0200 Subject: [PATCH 026/155] Remove the import of unused library --- src/pages/home/sidebar/SidebarLinks.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 1783f60faa73..1afda46fecba 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -4,7 +4,6 @@ import _ from 'underscore'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; -import memoizeOne from 'memoize-one'; import styles from '../../../styles/styles'; import * as StyleUtils from '../../../styles/StyleUtils'; import ONYXKEYS from '../../../ONYXKEYS'; @@ -431,7 +430,7 @@ class SidebarLinks extends React.Component { {paddingBottom: StyleUtils.getSafeAreaMargins(this.props.insets).marginBottom}, ]} sections={sections} - focusedIndex={_.findIndex(this.state.orderedReports, ( + focusedIndex={_.findIndex(this.orderedReports, ( option => option.reportID === activeReportID ))} onSelectRow={(option) => { From f3a7fa083fad9746ad174cf52748c129ee5c1529 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 17:33:13 +0200 Subject: [PATCH 027/155] Fix a prop warning --- src/pages/home/report/ReportActionsList.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index a78f672d3901..f5559585d2d3 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -50,7 +50,7 @@ const propTypes = { mostRecentIOUReportSequenceNumber: PropTypes.number, /** Are we loading more report actions? */ - isLoadingMoreReportActions: PropTypes.bool.isRequired, + isLoadingMoreReportActions: PropTypes.bool, /** Callback executed on list layout */ onLayout: PropTypes.func.isRequired, @@ -68,6 +68,7 @@ const propTypes = { const defaultProps = { personalDetails: {}, mostRecentIOUReportSequenceNumber: undefined, + isLoadingMoreReportActions: false, }; class ReportActionsList extends React.Component { From 35cfd235dae8e900b88760e7c5a8443e43f4e69a Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 17:49:03 +0200 Subject: [PATCH 028/155] Add some performance timing for filtering the reports --- src/CONST.js | 1 + src/pages/home/sidebar/SidebarLinks.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/CONST.js b/src/CONST.js index a6b7586c217b..0add0513eecc 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -343,6 +343,7 @@ const CONST = { SWITCH_REPORT: 'switch_report', SIDEBAR_LOADED: 'sidebar_loaded', PERSONAL_DETAILS_FORMATTED: 'personal_details_formatted', + SIDEBAR_LINKS_FILTER_REPORTS: 'sidebar_links_filter_reports', COLD: 'cold', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, TOOLTIP_SENSE: 1000, diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 1afda46fecba..c0f3bc48cad0 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -27,6 +27,7 @@ import * as ReportUtils from '../../../libs/ReportUtils'; import networkPropTypes from '../../../components/networkPropTypes'; import {withNetwork} from '../../../components/OnyxProvider'; import withCurrentUserPersonalDetails from '../../../components/withCurrentUserPersonalDetails'; +import Timing from '../../../libs/actions/Timing'; const propTypes = { /** Toggles the navigation menu open and closed */ @@ -180,7 +181,7 @@ class SidebarLinks extends React.Component { constructor(props) { super(props); - this.getFilteredReports = _.memoize(this.getFilteredReports); + this.getFilteredReports = this.getFilteredReports.bind(this); this.activeReport = { reportID: props.currentlyViewedReportID, @@ -375,12 +376,14 @@ class SidebarLinks extends React.Component { } const activeReportID = parseInt(this.props.currentlyViewedReportID, 10); + Timing.start(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); const sections = [{ title: '', indexOffset: 0, data: this.getFilteredReports(this.props.reports), shouldShow: true, }]; + Timing.end(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); return ( From 8e8ba1f926819fe8bb3502f87bd3acb59a9e5db7 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 17:51:28 +0200 Subject: [PATCH 029/155] Rename method to be more clear --- src/pages/home/sidebar/SidebarLinks.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index c0f3bc48cad0..d09ff1192f05 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -181,7 +181,7 @@ class SidebarLinks extends React.Component { constructor(props) { super(props); - this.getFilteredReports = this.getFilteredReports.bind(this); + this.getFilteredAndOrderedReports = this.getFilteredAndOrderedReports.bind(this); this.activeReport = { reportID: props.currentlyViewedReportID, @@ -242,7 +242,7 @@ class SidebarLinks extends React.Component { // // }; // } - getFilteredReports(unfilteredReports) { + getFilteredAndOrderedReports(unfilteredReports) { const isActiveReportSame = this.activeReport.reportID === this.props.currentlyViewedReportID; const lastMessageTimestamp = lodashGet(unfilteredReports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.lastMessageTimestamp`, 0); @@ -266,7 +266,7 @@ class SidebarLinks extends React.Component { // Build the report options we want to show const recentReports = this.getRecentReportsOptionListItems(); - this.orderedReports = shouldReorder || switchingPriorityModes + const orderedReports = shouldReorder || switchingPriorityModes ? recentReports : _.chain(this.orderedReports) @@ -279,6 +279,10 @@ class SidebarLinks extends React.Component { .filter(orderedReport => orderedReport !== undefined) .value(); + // Store these pieces of data on the class so that the next time this method is called + // the previous values can be compared against to tell if something changed which would + // cause the reports to be reordered + this.orderedReports = orderedReports; this.priorityMode = this.props.priorityMode; this.activeReport = { reportID: this.props.currentlyViewedReportID, @@ -380,7 +384,7 @@ class SidebarLinks extends React.Component { const sections = [{ title: '', indexOffset: 0, - data: this.getFilteredReports(this.props.reports), + data: this.getFilteredAndOrderedReports(this.props.reports), shouldShow: true, }]; Timing.end(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); From 6a26a03708c7352b2473c1e76d862e0c365fac74 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 20:16:43 +0200 Subject: [PATCH 030/155] Remove mobile preoptimization --- src/pages/home/sidebar/SidebarLinks.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index d09ff1192f05..0ecee2ff2018 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -332,13 +332,6 @@ class SidebarLinks extends React.Component { } shouldReorderReports(hasDraftHistory) { - // We don't need to limit draft comment flashing for small screen widths as LHN is not visible. - // Because: TBD - // @TODO try and figure out why - if (this.props.isSmallScreenWidth) { - return true; - } - // Always update if LHN is empty. // Because: TBD // @TODO try and figure out why From d97377655124db1839a00dbbbd044acbdcb9a5f4 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Sat, 27 Aug 2022 20:25:22 +0200 Subject: [PATCH 031/155] Correct the language in a comment --- src/pages/home/sidebar/SidebarLinks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 0ecee2ff2018..e51054b6e072 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -270,12 +270,12 @@ class SidebarLinks extends React.Component { ? recentReports : _.chain(this.orderedReports) - // To preserve the order of the conversations, we map over the previous state's order of reports. + // To preserve the order of the conversations, we map over the previous ordered reports. // Then match and replace older reports with the newer report conversations from recentReports .map(orderedReport => _.find(recentReports, recentReport => orderedReport.reportID === recentReport.reportID)) // Because we are using map, we have to filter out any undefined reports. This happens if recentReports - // does not have all the conversations in prevState.orderedReports + // does not have all the conversations in the previous set of orderedReports .filter(orderedReport => orderedReport !== undefined) .value(); From ccf63b3452783ad7bb936e6313286be8c37afc2e Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 12:52:02 +0200 Subject: [PATCH 032/155] correcting the name to walletAdditionalDetails --- src/pages/EnablePayments/IdologyQuestions.js | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index a92a47f450c4..78ed9ac162a6 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -33,9 +33,14 @@ const propTypes = { /** ID from Idology, referencing those questions */ idNumber: PropTypes.string, - additionalDetails: PropTypes.shape({ + walletAdditionalDetails: PropTypes.shape({ + /** Are we waiting for a response? */ isLoading: PropTypes.bool, - errors: PropTypes.arrayOf(PropTypes.string), + + /** Any additional error message to show */ + errors: PropTypes.objectOf(PropTypes.string), + + /** What error do we need to handle */ errorCode: PropTypes.string, }), }; @@ -43,9 +48,9 @@ const propTypes = { const defaultProps = { questions: [], idNumber: '', - additionalDetails: { + walletAdditionalDetails: { isLoading: false, - errors: [], + errors: {}, errorCode: '', }, }; @@ -164,13 +169,13 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={_.isEmpty(this.props.additionalDetails.errors) ? _.find(this.props.additionalDetails.errors, error => error !== undefined) : this.state.errorMessage} - isLoading={this.props.additionalDetails.isLoading} + message={_.isEmpty(this.props.walletAdditionalDetails.errors) ? _.find(this.props.walletAdditionalDetails.errors, error => error !== undefined) : this.state.errorMessage} + isLoading={this.props.walletAdditionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> @@ -182,7 +187,7 @@ class IdologyQuestions extends React.Component { IdologyQuestions.propTypes = propTypes; IdologyQuestions.defaultProps = defaultProps; export default compose(withLocalize(IdologyQuestions), withOnyx({ - additionalDetails: { + walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, }))(IdologyQuestions); From 66fc73481047948d9d4c9a37cf62e9a8c87df295 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 16:24:30 +0200 Subject: [PATCH 033/155] adding some TODO so when I'm done. I can remove this mehtod --- src/libs/actions/Wallet.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index ee66288ff4e3..42c21a4f86db 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -247,6 +247,7 @@ function updatePersonalDetails(personalDetails) { * @param {Object} [parameters.idologyAnswers] * @param {Boolean} [parameters.hasAcceptedTerms] */ +// TODO: this is no longer used, so I need to remove it, for now I need to understand how it works function activateWallet(currentStep, parameters) { let personalDetails; let idologyAnswers; @@ -257,6 +258,7 @@ function activateWallet(currentStep, parameters) { throw new Error('Invalid currentStep passed to activateWallet()'); } + // TODO: need to check what this does setWalletShouldShowFailedKYC(false); if (currentStep === CONST.WALLET.STEP.ONFIDO) { onfidoData = parameters.onfidoData; @@ -303,6 +305,7 @@ function activateWallet(currentStep, parameters) { return; } + // TODO: not sure if we need to do this, and if we do, is it this GH? if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) { if (response.title === CONST.WALLET.ERROR.KBA_NEEDED) { setAdditionalDetailsQuestions(response.data.questions, response.data.idNumber); From b9c7d676997e4dc206bc3d47657f03eef3c594b7 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 16:25:05 +0200 Subject: [PATCH 034/155] adding some handling for what happens when the answers were correct. --- src/libs/actions/BankAccounts.js | 1 + src/libs/actions/Wallet.js | 1 + src/pages/EnablePayments/IdologyQuestions.js | 30 ++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 4d3b0b71b36c..3c7fc568ff72 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -31,6 +31,7 @@ export { answerQuestionsForWallet, verifyIdentity, acceptWalletTerms, + setAdditionalDetailsLoading, } from './Wallet'; function clearPersonalBankAccount() { diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 42c21a4f86db..223637d770ff 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -580,4 +580,5 @@ export { updatePersonalDetails, verifyIdentity, acceptWalletTerms, + setAdditionalDetailsLoading, }; diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 78ed9ac162a6..90ff21191a67 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -15,6 +15,8 @@ import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; +import CONST from "../../CONST"; +import * as PaymentMethods from "../../libs/actions/PaymentMethods"; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -43,6 +45,16 @@ const propTypes = { /** What error do we need to handle */ errorCode: PropTypes.string, }), + + /** User wallet props */ + userWallet: PropTypes.shape({ + + /** The step in the wallet configuration we are in. */ + currentStep: PropTypes.string, + + /** What tier does the user has for their wallet */ + tierName: PropTypes.string, + }), }; const defaultProps = { @@ -53,6 +65,10 @@ const defaultProps = { errors: {}, errorCode: '', }, + userWallet: { + currentStep: '', + tierName: '', + }, }; class IdologyQuestions extends React.Component { @@ -75,6 +91,17 @@ class IdologyQuestions extends React.Component { }; } + componentDidUpdate(prevProps) { + if (this.props.userWallet.tierName !== CONST.WALLET.TIER_NAME.GOLD) { + return; + } + + if (this.props.walletAdditionalDetails.isLoading) { + PaymentMethods.continueSetup(); + BankAccounts.setAdditionalDetailsLoading(false); + } + } + /** * Put question answer in the state. * @param {Number} questionIndex @@ -190,4 +217,7 @@ export default compose(withLocalize(IdologyQuestions), withOnyx({ walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + } }))(IdologyQuestions); From d9153aa550562b435d2bb47bba83d5397d0fcbc6 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 17:01:50 +0200 Subject: [PATCH 035/155] fix jest hopefully --- src/pages/EnablePayments/IdologyQuestions.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 90ff21191a67..ae5bdc65240e 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -213,11 +213,14 @@ class IdologyQuestions extends React.Component { IdologyQuestions.propTypes = propTypes; IdologyQuestions.defaultProps = defaultProps; -export default compose(withLocalize(IdologyQuestions), withOnyx({ - walletAdditionalDetails: { - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - } -}))(IdologyQuestions); +export default compose( + withLocalize(IdologyQuestions), + withOnyx({ + walletAdditionalDetails: { + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + }), +)(IdologyQuestions); From f0d04dbe88d275e74bb1959334bfbd4054620a71 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 17:21:02 +0200 Subject: [PATCH 036/155] adding some more logic --- src/pages/EnablePayments/IdologyQuestions.js | 31 ++++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index ae5bdc65240e..1e24dc1c1c14 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -17,6 +17,12 @@ import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import CONST from "../../CONST"; import * as PaymentMethods from "../../libs/actions/PaymentMethods"; +import lodashGet from "lodash/get"; +import * as PersonalDetails from "../../libs/actions/PersonalDetails"; +import ScreenWrapper from "../../components/ScreenWrapper"; +import HeaderWithCloseButton from "../../components/HeaderWithCloseButton"; +import Navigation from "../../libs/Navigation/Navigation"; +import FailedKYC from "./FailedKYC"; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -85,9 +91,6 @@ class IdologyQuestions extends React.Component { /** Answers from the user */ answers: [], - - /** Any error message */ - errorMessage: '', }; } @@ -114,7 +117,6 @@ class IdologyQuestions extends React.Component { answers[questionIndex] = {question: question.type, answer}; return { answers, - errorMessage: '', }; }); } @@ -127,6 +129,7 @@ class IdologyQuestions extends React.Component { // User must pick an answer if (!prevState.answers[prevState.questionNumber]) { return { + // TODO: I think this should be a merge of the Aditional Details, as an error, that would update everything errorMessage: this.props.translate('additionalDetailsStep.selectAnswer'), }; } @@ -174,6 +177,22 @@ class IdologyQuestions extends React.Component { value: answer, }; })); + const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); + const isErrorVisible = _.size(this.getErrors()) > 0 || !_.isEmpty(errors); + const errorMessage = _.isEmpty(errors) ? '' : _.last(_.values(errors)); + + if (this.props.walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.KYC) { + return ( + // TODO: not sure if this is the correct design + + Navigation.dismissModal()} + /> + + + ); + } return ( @@ -196,12 +215,12 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={_.isEmpty(this.props.walletAdditionalDetails.errors) ? _.find(this.props.walletAdditionalDetails.errors, error => error !== undefined) : this.state.errorMessage} + message={errorMessage} isLoading={this.props.walletAdditionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From de24b0031f2c5912c97bf250b8967f8b7126dc89 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 17:25:42 +0200 Subject: [PATCH 037/155] localize is not a mehtod --- src/pages/EnablePayments/IdologyQuestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 1e24dc1c1c14..161843345235 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -233,7 +233,7 @@ class IdologyQuestions extends React.Component { IdologyQuestions.propTypes = propTypes; IdologyQuestions.defaultProps = defaultProps; export default compose( - withLocalize(IdologyQuestions), + withLocalize, withOnyx({ walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, From b770d0ce445cb7b11b9a1a90db9c9e082de2c9b2 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 30 Aug 2022 17:26:32 +0200 Subject: [PATCH 038/155] Remove old code --- src/pages/home/sidebar/SidebarLinks.js | 134 ------------------------- 1 file changed, 134 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index e51054b6e072..da5c9e4ab86c 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -93,91 +93,7 @@ const defaultProps = { isSyncingData: false, }; -// /** -// * @param {Object} nextUnreadReports -// * @param {Object} unreadReports -// * @returns {Boolean} -// */ -// function checkForNewUnreadReports(nextUnreadReports, unreadReports) { -// return nextUnreadReports.length > 0 -// && _.some(nextUnreadReports, -// nextUnreadReport => !_.some(unreadReports, unreadReport => unreadReport.reportID === nextUnreadReport.reportID)); -// } -// const memoizeCheckForNewUnreadReports = memoizeOne(checkForNewUnreadReports); -// -// /** -// * @param {Object} reportsObject -// * @returns {Array} -// */ -// function getUnreadReports(reportsObject) { -// const reports = _.values(reportsObject); -// if (reports.length === 0) { -// return []; -// } -// const unreadReports = _.filter(reports, report => report && report.unreadActionCount > 0); -// return unreadReports; -// } -// const memoizeGetUnreadReports = memoizeOne(getUnreadReports); - class SidebarLinks extends React.Component { - // static getRecentReports(props) { - // const activeReportID = parseInt(props.currentlyViewedReportID, 10); - // const sidebarOptions = OptionsListUtils.getSidebarOptions( - // props.reports, - // props.personalDetails, - // activeReportID, - // props.priorityMode, - // props.betas, - // props.reportActions, - // ); - // return sidebarOptions.recentReports; - // } - - // /** - // * Returns true if the sidebar list should be re-ordered - // * - // * @param {Object} nextProps - // * @param {Boolean} hasActiveDraftHistory - // * @param {Array} orderedReports - // * @param {String} currentlyViewedReportID - // * @param {Array} unreadReports - // * @returns {Boolean} - // */ - // static shouldReorder(nextProps, hasActiveDraftHistory, orderedReports, currentlyViewedReportID, unreadReports) { - // // We do not want to re-order reports in the LHN if the only change is the draft comment in the - // // current report. - // - // // We don't need to limit draft comment flashing for small screen widths as LHN is not visible. - // if (nextProps.isSmallScreenWidth) { - // return true; - // } - // - // // Always update if LHN is empty. - // if (orderedReports.length === 0) { - // return true; - // } - // - // const didActiveReportChange = currentlyViewedReportID !== nextProps.currentlyViewedReportID; - // - // // Always re-order the list whenever the active report is changed - // if (didActiveReportChange) { - // return true; - // } - // - // // If any reports have new unread messages, re-order the list - // const nextUnreadReports = memoizeGetUnreadReports(nextProps.reports || {}); - // if (memoizeCheckForNewUnreadReports(nextUnreadReports, unreadReports)) { - // return true; - // } - // - // // If there is an active report that either had or has a draft, we do not want to re-order the list - // if (nextProps.currentlyViewedReportID && hasActiveDraftHistory) { - // return false; - // } - // - // return true; - // } - constructor(props) { super(props); @@ -192,56 +108,6 @@ class SidebarLinks extends React.Component { this.unreadReports = this.getUnreadReports(props.reports); } - // static getDerivedStateFromProps(nextProps, prevState) { - // // const isActiveReportSame = prevState.activeReport.reportID === nextProps.currentlyViewedReportID; - // // const lastMessageTimestamp = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.lastMessageTimestamp`, 0); - // - // // Determines if the active report has a history of draft comments while active. - // // let hasDraftHistory; - // - // // If the active report has not changed and the message has been sent, set the draft history flag to false so LHN can reorder. - // // Otherwise, if the active report has not changed and the flag was previously true, preserve the state so LHN cannot reorder. - // // Otherwise, update the flag from the prop value. - // // if (isActiveReportSame && prevState.activeReport.lastMessageTimestamp !== lastMessageTimestamp) { - // // hasDraftHistory = false; - // // } else if (isActiveReportSame && prevState.activeReport.hasDraftHistory) { - // // hasDraftHistory = true; - // // } else { - // // hasDraftHistory = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.hasDraft`, false); - // // } - // - // // const shouldReorder = SidebarLinks.shouldReorder(nextProps, hasDraftHistory, prevState.orderedReports, prevState.activeReport.reportID, prevState.unreadReports); - // // const switchingPriorityModes = nextProps.priorityMode !== prevState.priorityMode; - // - // // Build the report options we want to show - // // const recentReports = SidebarLinks.getRecentReports(nextProps); - // - // // Determine whether we need to keep the previous LHN order - // // const orderedReports = shouldReorder || switchingPriorityModes - // // ? recentReports - // // : _.chain(prevState.orderedReports) - // // - // // // To preserve the order of the conversations, we map over the previous state's order of reports. - // // // Then match and replace older reports with the newer report conversations from recentReports - // // .map(orderedReport => _.find(recentReports, recentReport => orderedReport.reportID === recentReport.reportID)) - // // - // // // Because we are using map, we have to filter out any undefined reports. This happens if recentReports - // // // does not have all the conversations in prevState.orderedReports - // // .filter(orderedReport => orderedReport !== undefined) - // // .value(); - // - // // return { - // // orderedReports, - // // priorityMode: nextProps.priorityMode, - // // activeReport: { - // // reportID: nextProps.currentlyViewedReportID, - // // hasDraftHistory, - // // lastMessageTimestamp, - // // }, - // // unreadReports: memoizeGetUnreadReports(nextProps.reports || {}), - // // }; - // } - getFilteredAndOrderedReports(unfilteredReports) { const isActiveReportSame = this.activeReport.reportID === this.props.currentlyViewedReportID; const lastMessageTimestamp = lodashGet(unfilteredReports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.lastMessageTimestamp`, 0); From 962697294e61ef5625e90c0f582295a4571ad2f7 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 30 Aug 2022 17:33:51 +0200 Subject: [PATCH 039/155] Rename method to make more sense and add comments --- src/pages/home/sidebar/SidebarLinks.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index fb53b5e15021..31ad2b18b9b9 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -117,13 +117,16 @@ class SidebarLinks extends React.Component { hasDraftHistory = lodashGet(this.props.reports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.hasDraft`, false); } - const shouldReorder = this.shouldReorderReports(hasDraftHistory); const switchingPriorityModes = this.props.priorityMode !== this.priorityMode; // Build the report options we want to show const recentReports = this.getRecentReportsOptionListItems(); - const orderedReports = shouldReorder || switchingPriorityModes + // If the order of the reports is different from the last render (or if priority mode is changing) + // then orderedReports is the same as the freshly calculated recentReports. + // If the order of the reports is the same as the last render + // then the data for each report is updated from the data in the new props (not sure why this is necessary) + const orderedReports = this.isReportOrderDifferentThanLastRender(hasDraftHistory) || switchingPriorityModes ? recentReports : _.chain(this.orderedReports) @@ -188,7 +191,7 @@ class SidebarLinks extends React.Component { return sidebarOptions.recentReports; } - shouldReorderReports(hasDraftHistory) { + isReportOrderDifferentThanLastRender(hasDraftHistory) { // Always update if LHN is empty. // Because: TBD // @TODO try and figure out why From 31b42d936e59f63925678a704c1c749334ed0238 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 17:37:13 +0200 Subject: [PATCH 040/155] fixing linter --- src/pages/EnablePayments/IdologyQuestions.js | 31 ++++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 161843345235..13de3aac942c 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -5,6 +5,7 @@ import { View, } from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; @@ -15,14 +16,11 @@ import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; -import CONST from "../../CONST"; -import * as PaymentMethods from "../../libs/actions/PaymentMethods"; -import lodashGet from "lodash/get"; -import * as PersonalDetails from "../../libs/actions/PersonalDetails"; -import ScreenWrapper from "../../components/ScreenWrapper"; -import HeaderWithCloseButton from "../../components/HeaderWithCloseButton"; -import Navigation from "../../libs/Navigation/Navigation"; -import FailedKYC from "./FailedKYC"; +import CONST from '../../CONST'; +import * as PaymentMethods from '../../libs/actions/PaymentMethods'; +import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import Navigation from '../../libs/Navigation/Navigation'; +import FailedKYC from './FailedKYC'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -94,7 +92,7 @@ class IdologyQuestions extends React.Component { }; } - componentDidUpdate(prevProps) { + componentDidUpdate() { if (this.props.userWallet.tierName !== CONST.WALLET.TIER_NAME.GOLD) { return; } @@ -183,6 +181,7 @@ class IdologyQuestions extends React.Component { if (this.props.walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.KYC) { return ( + // TODO: not sure if this is the correct design Date: Tue, 30 Aug 2022 17:40:08 +0200 Subject: [PATCH 041/155] Add comments to the logic for ordering reports --- src/pages/home/sidebar/SidebarLinks.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 31ad2b18b9b9..b5d73c968de9 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -192,33 +192,33 @@ class SidebarLinks extends React.Component { } isReportOrderDifferentThanLastRender(hasDraftHistory) { - // Always update if LHN is empty. - // Because: TBD - // @TODO try and figure out why - if (this.orderedReports.length === 0) { + // If the number of reports changed, then the report order is different + if (this.orderedReports.length !== this.props.reports.length) { return true; } - // Always re-order the list whenever the active report is changed - // Because: TBD - // @TODO try and figure out why + // If the active report changed, then the report order is different if (this.activeReport.reportID !== this.props.currentlyViewedReportID) { return true; } - // If there is an active report that either had or has a draft, we do not want to re-order the list - // because the position of the report in the list won't change + // If the active report has a draft, the order of the reports doesn't change + // because it would cause the reports to reorder when a user starts typing a comment + // and that is an annoying UX (too much stuff jumping around) if (this.props.currentlyViewedReportID && hasDraftHistory) { return false; } - // If any reports have new unread messages, the list needs to be reordered + // If the unread reports have changed, then the report order changes // because the unread reports need to be placed at the top of the list + // @TODO: This can probably be optimized const hasNewUnreadReports = _.some(this.props.reports, report => report.unreadActionCount > 0 && !this.unreadReports[report.reportID]); if (hasNewUnreadReports) { return true; } + // By default, assume that the order of the reports doesn't change + // in order to optimize the rendering return false; } From 16cad5d6588585f6b53bf64b8b5f6cd9947c7dc1 Mon Sep 17 00:00:00 2001 From: Eugene Voloshchak Date: Tue, 30 Aug 2022 21:38:52 +0300 Subject: [PATCH 042/155] Accept both decimal separators on mWeb iOS --- src/libs/getPermittedDecimalSeparator/index.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libs/getPermittedDecimalSeparator/index.js b/src/libs/getPermittedDecimalSeparator/index.js index 573e0c9b3225..9ac4156bc3ff 100644 --- a/src/libs/getPermittedDecimalSeparator/index.js +++ b/src/libs/getPermittedDecimalSeparator/index.js @@ -1 +1,11 @@ -export default localizedSeparator => localizedSeparator; +import getOperatingSystem from '../getOperatingSystem'; +import getPermittedDecimalSeparatorIOS from './index.ios'; +import CONST from '../../CONST'; + +export default (localizedSeparator) => { + if (getOperatingSystem() === CONST.OS.IOS) { + return getPermittedDecimalSeparatorIOS(); + } + + return localizedSeparator; +}; From 56d6d3416125f9d36bc2af37a263e69cd6ea00ec Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 30 Aug 2022 20:45:12 +0200 Subject: [PATCH 043/155] Add a todo --- src/pages/home/sidebar/SidebarLinks.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index b5d73c968de9..392c74e9a50b 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -125,7 +125,8 @@ class SidebarLinks extends React.Component { // If the order of the reports is different from the last render (or if priority mode is changing) // then orderedReports is the same as the freshly calculated recentReports. // If the order of the reports is the same as the last render - // then the data for each report is updated from the data in the new props (not sure why this is necessary) + // then the data for each report is updated from the data in the new props + // @TODO: not sure why this is necessary and see if it can be removed or do something more intuitive const orderedReports = this.isReportOrderDifferentThanLastRender(hasDraftHistory) || switchingPriorityModes ? recentReports : _.chain(this.orderedReports) From 160da4eb48617374ac42867c8ef7c390dd90caed Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Wed, 31 Aug 2022 18:14:53 +0400 Subject: [PATCH 044/155] Remove TODOs and cleanup --- src/libs/actions/BankAccounts.js | 1 - src/libs/actions/Wallet.js | 6 +----- src/pages/EnablePayments/EnablePaymentsPage.js | 10 +--------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 3c7fc568ff72..4d3b0b71b36c 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -31,7 +31,6 @@ export { answerQuestionsForWallet, verifyIdentity, acceptWalletTerms, - setAdditionalDetailsLoading, } from './Wallet'; function clearPersonalBankAccount() { diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 223637d770ff..e8a838fc79c4 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -247,7 +247,6 @@ function updatePersonalDetails(personalDetails) { * @param {Object} [parameters.idologyAnswers] * @param {Boolean} [parameters.hasAcceptedTerms] */ -// TODO: this is no longer used, so I need to remove it, for now I need to understand how it works function activateWallet(currentStep, parameters) { let personalDetails; let idologyAnswers; @@ -258,7 +257,6 @@ function activateWallet(currentStep, parameters) { throw new Error('Invalid currentStep passed to activateWallet()'); } - // TODO: need to check what this does setWalletShouldShowFailedKYC(false); if (currentStep === CONST.WALLET.STEP.ONFIDO) { onfidoData = parameters.onfidoData; @@ -305,7 +303,6 @@ function activateWallet(currentStep, parameters) { return; } - // TODO: not sure if we need to do this, and if we do, is it this GH? if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) { if (response.title === CONST.WALLET.ERROR.KBA_NEEDED) { setAdditionalDetailsQuestions(response.data.questions, response.data.idNumber); @@ -532,7 +529,7 @@ function updateCurrentStep(currentStep) { } /** - * @param {String} idologyAnswers + * @param {Object} idologyAnswers */ function answerQuestionsForWallet(idologyAnswers) { const answers = JSON.stringify(idologyAnswers); @@ -580,5 +577,4 @@ export { updatePersonalDetails, verifyIdentity, acceptWalletTerms, - setAdditionalDetailsLoading, }; diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index 0e098fa5a820..3647343f9e89 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -27,10 +27,6 @@ const propTypes = { /** Information about the network from Onyx */ network: networkPropTypes.isRequired, - additionalDetails: PropTypes.shape({ - errorCode: PropTypes.string, - }), - /** The user's wallet */ userWallet: PropTypes.objectOf(userWalletPropTypes), @@ -39,10 +35,6 @@ const propTypes = { const defaultProps = { userWallet: {}, - - additionalDetails: { - errorCode: '', - }, }; class EnablePaymentsPage extends React.Component { @@ -63,7 +55,7 @@ class EnablePaymentsPage extends React.Component { return ; } - if (this.props.additionalDetails.errorCode === CONST.WALLET.ERROR.KYC) { + if (this.props.userWallet.errorCode === CONST.WALLET.ERROR.KYC) { return ( Date: Wed, 31 Aug 2022 18:36:05 +0400 Subject: [PATCH 045/155] Clean up props and remove un-needed logic --- .../EnablePayments/EnablePaymentsPage.js | 3 -- src/pages/EnablePayments/IdologyQuestions.js | 54 +++++-------------- 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index 3647343f9e89..314419df9a56 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -101,9 +101,6 @@ export default compose( walletAdditionalDetailsDraft: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS_DRAFT, }, - additionalDetails: { - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, - }, }), withNetwork(), )(EnablePaymentsPage); diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 13de3aac942c..af2dcf3853eb 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -21,6 +21,7 @@ import * as PaymentMethods from '../../libs/actions/PaymentMethods'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; import FailedKYC from './FailedKYC'; +import userWalletPropTypes from './userWalletPropTypes'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -51,28 +52,14 @@ const propTypes = { }), /** User wallet props */ - userWallet: PropTypes.shape({ - - /** The step in the wallet configuration we are in. */ - currentStep: PropTypes.string, - - /** What tier does the user has for their wallet */ - tierName: PropTypes.string, - }), + userWallet: PropTypes.shape(userWalletPropTypes), }; const defaultProps = { questions: [], idNumber: '', - walletAdditionalDetails: { - isLoading: false, - errors: {}, - errorCode: '', - }, - userWallet: { - currentStep: '', - tierName: '', - }, + walletAdditionalDetails: {}, + userWallet: {}, }; class IdologyQuestions extends React.Component { @@ -89,6 +76,9 @@ class IdologyQuestions extends React.Component { /** Answers from the user */ answers: [], + + /** Any error message */ + errorMessage: '', }; } @@ -96,11 +86,6 @@ class IdologyQuestions extends React.Component { if (this.props.userWallet.tierName !== CONST.WALLET.TIER_NAME.GOLD) { return; } - - if (this.props.walletAdditionalDetails.isLoading) { - PaymentMethods.continueSetup(); - BankAccounts.setAdditionalDetailsLoading(false); - } } /** @@ -115,6 +100,7 @@ class IdologyQuestions extends React.Component { answers[questionIndex] = {question: question.type, answer}; return { answers, + errorMessage, }; }); } @@ -127,7 +113,6 @@ class IdologyQuestions extends React.Component { // User must pick an answer if (!prevState.answers[prevState.questionNumber]) { return { - // TODO: I think this should be a merge of the Aditional Details, as an error, that would update everything errorMessage: this.props.translate('additionalDetailsStep.selectAnswer'), }; } @@ -146,11 +131,10 @@ class IdologyQuestions extends React.Component { } } - const idologyAnswers = { + BankAccounts.answerQuestionsForWallet({ answers, idNumber: this.props.idNumber, - }; - BankAccounts.answerQuestionsForWallet(idologyAnswers); + }); return {answers}; } @@ -176,22 +160,8 @@ class IdologyQuestions extends React.Component { }; })); const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); - const isErrorVisible = _.size(this.getErrors()) > 0 || !_.isEmpty(errors); - const errorMessage = _.isEmpty(errors) ? '' : _.last(_.values(errors)); - - if (this.props.walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.KYC) { - return ( - - // TODO: not sure if this is the correct design - - Navigation.dismissModal()} - /> - - - ); - } + const isErrorVisible = this.state.errorMessage || _.size(this.getErrors()) > 0 || !_.isEmpty(errors); + const errorMessage = _.isEmpty(errors) ? this.state.errorMessage : _.last(_.values(errors)); return ( From 44e258cf9e7568629104055bf774fdad92f67448 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 31 Aug 2022 17:10:32 +0200 Subject: [PATCH 046/155] Fix proptype warnings --- src/pages/home/ReportScreen.js | 5 ++--- src/pages/home/report/ReportActionCompose.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 717963c3dd49..2f5c2742eb24 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -60,7 +60,7 @@ const propTypes = { isLoadingReportActions: PropTypes.bool, /** ID for the report */ - reportID: PropTypes.string, + reportID: PropTypes.number, }), /** Array of report actions for this report */ @@ -111,8 +111,7 @@ const defaultProps = { * @returns {Number} */ function getReportID(route) { - const params = route.params; - return Number.parseInt(params.reportID, 10); + return route.params.reportID.toString(); } class ReportScreen extends React.Component { diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 61369be57010..a82101de219f 100755 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -57,7 +57,7 @@ const propTypes = { comment: PropTypes.string, /** The ID of the report actions will be created for */ - reportID: PropTypes.number.isRequired, + reportID: PropTypes.string.isRequired, /** Details about any modals being used */ modal: PropTypes.shape({ From f42ed2a20b95a4ebb24d720956232bcc4f0d6b7e Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Wed, 31 Aug 2022 19:27:49 +0400 Subject: [PATCH 047/155] Fix command params for answerQuestionsForWallet --- src/libs/actions/Wallet.js | 7 +++--- src/pages/EnablePayments/IdologyQuestions.js | 24 ++++---------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index e8a838fc79c4..b8b883a67e28 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -531,11 +531,12 @@ function updateCurrentStep(currentStep) { /** * @param {Object} idologyAnswers */ -function answerQuestionsForWallet(idologyAnswers) { - const answers = JSON.stringify(idologyAnswers); +function answerQuestionsForWallet(answers, idNumber) { + const idologyAnswers = JSON.stringify(answers); API.write('AnswerQuestionsForWallet', { - answers, + idologyAnswers, + idNumber }, { optimisticData: [{ diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index af2dcf3853eb..5dc3beb5d697 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -21,7 +21,6 @@ import * as PaymentMethods from '../../libs/actions/PaymentMethods'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; import FailedKYC from './FailedKYC'; -import userWalletPropTypes from './userWalletPropTypes'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -50,16 +49,12 @@ const propTypes = { /** What error do we need to handle */ errorCode: PropTypes.string, }), - - /** User wallet props */ - userWallet: PropTypes.shape(userWalletPropTypes), }; const defaultProps = { questions: [], idNumber: '', walletAdditionalDetails: {}, - userWallet: {}, }; class IdologyQuestions extends React.Component { @@ -82,12 +77,6 @@ class IdologyQuestions extends React.Component { }; } - componentDidUpdate() { - if (this.props.userWallet.tierName !== CONST.WALLET.TIER_NAME.GOLD) { - return; - } - } - /** * Put question answer in the state. * @param {Number} questionIndex @@ -100,7 +89,7 @@ class IdologyQuestions extends React.Component { answers[questionIndex] = {question: question.type, answer}; return { answers, - errorMessage, + errorMessage: '', }; }); } @@ -131,10 +120,7 @@ class IdologyQuestions extends React.Component { } } - BankAccounts.answerQuestionsForWallet({ - answers, - idNumber: this.props.idNumber, - }); + BankAccounts.answerQuestionsForWallet(answers, this.props.idNumber); return {answers}; } @@ -160,7 +146,7 @@ class IdologyQuestions extends React.Component { }; })); const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); - const isErrorVisible = this.state.errorMessage || _.size(this.getErrors()) > 0 || !_.isEmpty(errors); + const isErrorVisible = this.state.errorMessage || !_.isEmpty(errors); const errorMessage = _.isEmpty(errors) ? this.state.errorMessage : _.last(_.values(errors)); return ( @@ -179,6 +165,7 @@ class IdologyQuestions extends React.Component { {question.prompt} this.chooseAnswer(questionIndex, answer)} /> @@ -207,8 +194,5 @@ export default compose( walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, }), )(IdologyQuestions); From 3595d65aedc9f4ff2a4531695389e078ed181b40 Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Wed, 31 Aug 2022 21:39:42 +0400 Subject: [PATCH 048/155] Don't show redundant error --- src/pages/EnablePayments/IdologyQuestions.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 5dc3beb5d697..b726b417cd06 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -145,9 +145,6 @@ class IdologyQuestions extends React.Component { value: answer, }; })); - const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); - const isErrorVisible = this.state.errorMessage || !_.isEmpty(errors); - const errorMessage = _.isEmpty(errors) ? this.state.errorMessage : _.last(_.values(errors)); return ( @@ -171,12 +168,12 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={errorMessage} + message={this.state.errorMessage} isLoading={this.props.walletAdditionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From f1f621ee99fafe8ff50a0175954da1cad780707e Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Wed, 31 Aug 2022 21:42:07 +0400 Subject: [PATCH 049/155] Fix lint errors --- src/libs/actions/Wallet.js | 5 +++-- src/pages/EnablePayments/IdologyQuestions.js | 6 ------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index b8b883a67e28..a4e75f49d010 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -529,14 +529,15 @@ function updateCurrentStep(currentStep) { } /** - * @param {Object} idologyAnswers + * @param {Object} answers + * @param {String} idNumber */ function answerQuestionsForWallet(answers, idNumber) { const idologyAnswers = JSON.stringify(answers); API.write('AnswerQuestionsForWallet', { idologyAnswers, - idNumber + idNumber, }, { optimisticData: [{ diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index b726b417cd06..6d3cf2677339 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -5,7 +5,6 @@ import { View, } from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import lodashGet from 'lodash/get'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; @@ -16,11 +15,6 @@ import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; -import CONST from '../../CONST'; -import * as PaymentMethods from '../../libs/actions/PaymentMethods'; -import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; -import Navigation from '../../libs/Navigation/Navigation'; -import FailedKYC from './FailedKYC'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; From bc6be2ddedfa075a8a816a1079fde5dc06ba16ce Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Thu, 1 Sep 2022 09:44:04 +0400 Subject: [PATCH 050/155] Add offline indicator and display error messages --- src/pages/EnablePayments/IdologyQuestions.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 6d3cf2677339..c4368c8cb9c6 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -5,6 +5,7 @@ import { View, } from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; @@ -15,6 +16,7 @@ import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; +import OfflineIndicator from '../../components/OfflineIndicator'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -140,6 +142,10 @@ class IdologyQuestions extends React.Component { }; })); + const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); + const isErrorVisible = this.state.errorMessage || !_.isEmpty(errors); + const errorMessage = _.isEmpty(errors) ? this.state.errorMessage : _.last(_.values(errors)); + return ( @@ -162,15 +168,16 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={this.state.errorMessage} + message={errorMessage} isLoading={this.props.walletAdditionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> + ); From bd05cecc019cb26d946b57d55e7fc897e9fc1359 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 11:21:13 +0200 Subject: [PATCH 051/155] Remove unnecessary binding --- src/pages/home/sidebar/SidebarLinks.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 3ad2041655ce..d9365e5d2327 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -91,8 +91,6 @@ class SidebarLinks extends React.Component { constructor(props) { super(props); - this.getFilteredAndOrderedReports = this.getFilteredAndOrderedReports.bind(this); - this.activeReport = { reportID: props.currentlyViewedReportID, }; @@ -237,14 +235,15 @@ class SidebarLinks extends React.Component { } const activeReportID = parseInt(this.props.currentlyViewedReportID, 10); - Timing.start(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); + // Timing.start(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); const sections = [{ title: '', indexOffset: 0, data: this.getFilteredAndOrderedReports(this.props.reports), shouldShow: true, }]; - Timing.end(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); + // const sections = []; + // Timing.end(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); return ( From 475f0eddd25867b68df048bc8e0d42074587a149 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 11:22:00 +0200 Subject: [PATCH 052/155] Remove debug code that is not needed --- src/pages/home/sidebar/SidebarLinks.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index d9365e5d2327..3b5c9e8b5521 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -235,15 +235,14 @@ class SidebarLinks extends React.Component { } const activeReportID = parseInt(this.props.currentlyViewedReportID, 10); - // Timing.start(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); + Timing.start(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); const sections = [{ title: '', indexOffset: 0, data: this.getFilteredAndOrderedReports(this.props.reports), shouldShow: true, }]; - // const sections = []; - // Timing.end(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); + Timing.end(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); return ( From 80615c9446ef933bb36b1d35791de1cb9775c3cb Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 11:40:38 +0200 Subject: [PATCH 053/155] Fix proptype warning --- src/components/OptionsSelector/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/OptionsSelector/index.js b/src/components/OptionsSelector/index.js index 14495ecae24d..9415c4e3b279 100644 --- a/src/components/OptionsSelector/index.js +++ b/src/components/OptionsSelector/index.js @@ -1,6 +1,7 @@ import React, {forwardRef} from 'react'; import BaseOptionsSelector from './BaseOptionsSelector'; import {propTypes, defaultProps} from './optionsSelectorPropTypes'; +import withLocalize from '../withLocalize'; const OptionsSelector = forwardRef((props, ref) => ( Date: Thu, 1 Sep 2022 11:41:29 +0200 Subject: [PATCH 054/155] Don't cast activeReportID to string --- src/libs/OptionsListUtils.js | 4 ++-- src/pages/home/sidebar/SidebarLinks.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index d17c4bb4d330..0139a3ad247d 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -349,7 +349,7 @@ function isCurrentUser(userDetails) { * * @param {Object} reports * @param {Object} personalDetails - * @param {Number} activeReportID + * @param {String} activeReportID * @param {Object} options * @returns {Object} * @private @@ -428,7 +428,7 @@ function getOptions(reports, personalDetails, activeReportID, { const shouldFilterReportIfRead = hideReadReports && report.unreadActionCount === 0; const shouldFilterReport = shouldFilterReportIfEmpty || shouldFilterReportIfRead; - if (report.reportID !== activeReportID + if (report.reportID.toString() !== activeReportID.toString() && !report.isPinned && !hasDraftComment && shouldFilterReport diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 3b5c9e8b5521..1b0148e6b53c 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -101,6 +101,8 @@ class SidebarLinks extends React.Component { } getFilteredAndOrderedReports(unfilteredReports) { + return this.getRecentReportsOptionListItems(); + const isActiveReportSame = this.activeReport.reportID === this.props.currentlyViewedReportID; const lastMessageTimestamp = lodashGet(unfilteredReports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.lastMessageTimestamp`, 0); @@ -181,7 +183,7 @@ class SidebarLinks extends React.Component { } getRecentReportsOptionListItems() { - const activeReportID = parseInt(this.props.currentlyViewedReportID, 10); + const activeReportID = this.props.currentlyViewedReportID; const sidebarOptions = OptionsListUtils.getSidebarOptions( this.props.reports, this.props.personalDetails, From 8512aa094133c2155e74ff5a2e295f41a7439ca0 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 11:46:47 +0200 Subject: [PATCH 055/155] Remove all data processing --- src/pages/home/sidebar/SidebarLinks.js | 194 ++++++++++++------------- 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 1b0148e6b53c..41a49e37076b 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -91,71 +91,71 @@ class SidebarLinks extends React.Component { constructor(props) { super(props); - this.activeReport = { - reportID: props.currentlyViewedReportID, - }; - - this.orderedReports = []; - this.priorityMode = props.priorityMode; - this.unreadReports = this.getUnreadReports(props.reports); + // this.activeReport = { + // reportID: props.currentlyViewedReportID, + // }; + // + // this.orderedReports = []; + // this.priorityMode = props.priorityMode; + // this.unreadReports = this.getUnreadReports(props.reports); } getFilteredAndOrderedReports(unfilteredReports) { return this.getRecentReportsOptionListItems(); - const isActiveReportSame = this.activeReport.reportID === this.props.currentlyViewedReportID; - const lastMessageTimestamp = lodashGet(unfilteredReports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.lastMessageTimestamp`, 0); - - // Determines if the active report has a history of draft comments while active. - let hasDraftHistory; - - // If the active report has not changed and the message has been sent, set the draft history flag to false so LHN can reorder. - // Otherwise, if the active report has not changed and the flag was previously true, preserve the state so LHN cannot reorder. - // Otherwise, update the flag from the prop value. - if (isActiveReportSame && this.activeReport.lastMessageTimestamp !== lastMessageTimestamp) { - hasDraftHistory = false; - } else if (isActiveReportSame && this.activeReport.hasDraftHistory) { - hasDraftHistory = true; - } else { - hasDraftHistory = lodashGet(this.props.reports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.hasDraft`, false); - } - - const switchingPriorityModes = this.props.priorityMode !== this.priorityMode; - - // Build the report options we want to show - const recentReports = this.getRecentReportsOptionListItems(); - - // If the order of the reports is different from the last render (or if priority mode is changing) - // then orderedReports is the same as the freshly calculated recentReports. - // If the order of the reports is the same as the last render - // then the data for each report is updated from the data in the new props - // @TODO: not sure why this is necessary and see if it can be removed or do something more intuitive - const orderedReports = this.isReportOrderDifferentThanLastRender(hasDraftHistory) || switchingPriorityModes - ? recentReports - : _.chain(this.orderedReports) - - // To preserve the order of the conversations, we map over the previous ordered reports. - // Then match and replace older reports with the newer report conversations from recentReports - .map(orderedReport => _.find(recentReports, recentReport => orderedReport.reportID === recentReport.reportID)) - - // Because we are using map, we have to filter out any undefined reports. This happens if recentReports - // does not have all the conversations in the previous set of orderedReports - .compact() - .value(); - - // Store these pieces of data on the class so that the next time this method is called - // the previous values can be compared against to tell if something changed which would - // cause the reports to be reordered - this.orderedReports = orderedReports; - this.priorityMode = this.props.priorityMode; - this.activeReport = { - reportID: this.props.currentlyViewedReportID, - hasDraftHistory, - lastMessageTimestamp, - }; - this.unreadReports = this.getUnreadReports(unfilteredReports); - - return this.orderedReports; + // const isActiveReportSame = this.activeReport.reportID === this.props.currentlyViewedReportID; + // const lastMessageTimestamp = lodashGet(unfilteredReports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.lastMessageTimestamp`, 0); + // + // // Determines if the active report has a history of draft comments while active. + // let hasDraftHistory; + // + // // If the active report has not changed and the message has been sent, set the draft history flag to false so LHN can reorder. + // // Otherwise, if the active report has not changed and the flag was previously true, preserve the state so LHN cannot reorder. + // // Otherwise, update the flag from the prop value. + // if (isActiveReportSame && this.activeReport.lastMessageTimestamp !== lastMessageTimestamp) { + // hasDraftHistory = false; + // } else if (isActiveReportSame && this.activeReport.hasDraftHistory) { + // hasDraftHistory = true; + // } else { + // hasDraftHistory = lodashGet(this.props.reports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.hasDraft`, false); + // } + // + // const switchingPriorityModes = this.props.priorityMode !== this.priorityMode; + // + // // Build the report options we want to show + // const recentReports = this.getRecentReportsOptionListItems(); + // + // // If the order of the reports is different from the last render (or if priority mode is changing) + // // then orderedReports is the same as the freshly calculated recentReports. + // // If the order of the reports is the same as the last render + // // then the data for each report is updated from the data in the new props + // // @TODO: not sure why this is necessary and see if it can be removed or do something more intuitive + // const orderedReports = this.isReportOrderDifferentThanLastRender(hasDraftHistory) || switchingPriorityModes + // ? recentReports + // : _.chain(this.orderedReports) + // + // // To preserve the order of the conversations, we map over the previous ordered reports. + // // Then match and replace older reports with the newer report conversations from recentReports + // .map(orderedReport => _.find(recentReports, recentReport => orderedReport.reportID === recentReport.reportID)) + // + // // Because we are using map, we have to filter out any undefined reports. This happens if recentReports + // // does not have all the conversations in the previous set of orderedReports + // .compact() + // .value(); + // + // // Store these pieces of data on the class so that the next time this method is called + // // the previous values can be compared against to tell if something changed which would + // // cause the reports to be reordered + // this.orderedReports = orderedReports; + // this.priorityMode = this.props.priorityMode; + // this.activeReport = { + // reportID: this.props.currentlyViewedReportID, + // hasDraftHistory, + // lastMessageTimestamp, + // }; + // this.unreadReports = this.getUnreadReports(unfilteredReports); + // + // return this.orderedReports; } /** @@ -171,15 +171,15 @@ class SidebarLinks extends React.Component { * @returns {Object} */ getUnreadReports(reports) { - return _.reduce(reports, (finalUnreadReportMap, report) => { - if (report.unreadActionCount > 0) { - return { - [report.reportID]: true, - ...finalUnreadReportMap, - }; - } - return finalUnreadReportMap; - }, {}); + // return _.reduce(reports, (finalUnreadReportMap, report) => { + // if (report.unreadActionCount > 0) { + // return { + // [report.reportID]: true, + // ...finalUnreadReportMap, + // }; + // } + // return finalUnreadReportMap; + // }, {}); } getRecentReportsOptionListItems() { @@ -196,34 +196,34 @@ class SidebarLinks extends React.Component { } isReportOrderDifferentThanLastRender(hasDraftHistory) { - // If the number of reports changed, then the report order is different - if (this.orderedReports.length !== this.props.reports.length) { - return true; - } - - // If the active report changed, then the report order is different - if (this.activeReport.reportID !== this.props.currentlyViewedReportID) { - return true; - } - - // If the active report has a draft, the order of the reports doesn't change - // because it would cause the reports to reorder when a user starts typing a comment - // and that is an annoying UX (too much stuff jumping around) - if (this.props.currentlyViewedReportID && hasDraftHistory) { - return false; - } - - // If the unread reports have changed, then the report order changes - // because the unread reports need to be placed at the top of the list - // @TODO: This can probably be optimized - const hasNewUnreadReports = _.some(this.props.reports, report => report.unreadActionCount > 0 && !this.unreadReports[report.reportID]); - if (hasNewUnreadReports) { - return true; - } - - // By default, assume that the order of the reports doesn't change - // in order to optimize the rendering - return false; + // // If the number of reports changed, then the report order is different + // if (this.orderedReports.length !== this.props.reports.length) { + // return true; + // } + // + // // If the active report changed, then the report order is different + // if (this.activeReport.reportID !== this.props.currentlyViewedReportID) { + // return true; + // } + // + // // If the active report has a draft, the order of the reports doesn't change + // // because it would cause the reports to reorder when a user starts typing a comment + // // and that is an annoying UX (too much stuff jumping around) + // if (this.props.currentlyViewedReportID && hasDraftHistory) { + // return false; + // } + // + // // If the unread reports have changed, then the report order changes + // // because the unread reports need to be placed at the top of the list + // // @TODO: This can probably be optimized + // const hasNewUnreadReports = _.some(this.props.reports, report => report.unreadActionCount > 0 && !this.unreadReports[report.reportID]); + // if (hasNewUnreadReports) { + // return true; + // } + // + // // By default, assume that the order of the reports doesn't change + // // in order to optimize the rendering + // return false; } showSearchPage() { From ee56ec42828b1440824922a210ba973809be48ae Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 11:53:10 +0200 Subject: [PATCH 056/155] Add report actions to the propTypes --- src/pages/home/sidebar/SidebarLinks.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 41a49e37076b..15d9652bb058 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -26,6 +26,7 @@ import * as App from '../../../libs/actions/App'; import * as ReportUtils from '../../../libs/ReportUtils'; import withCurrentUserPersonalDetails from '../../../components/withCurrentUserPersonalDetails'; import Timing from '../../../libs/actions/Timing'; +import reportActionPropTypes from '../report/reportActionPropTypes'; const propTypes = { /** Toggles the navigation menu open and closed */ @@ -53,6 +54,9 @@ const propTypes = { hasDraft: PropTypes.bool, })), + /** All report actions for all reports */ + reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** List of users' personal details */ personalDetails: PropTypes.objectOf(participantPropTypes), @@ -79,6 +83,7 @@ const propTypes = { const defaultProps = { reports: {}, + reportActions: {}, personalDetails: {}, currentUserPersonalDetails: { avatar: ReportUtils.getDefaultAvatar(), @@ -100,6 +105,27 @@ class SidebarLinks extends React.Component { // this.unreadReports = this.getUnreadReports(props.reports); } + componentDidUpdate(prevProps) { + if (!_.isEqual(prevProps.reports, this.props.reports)) { + console.log('!!! props.reports') + } + if (!_.isEqual(prevProps.personalDetails, this.props.personalDetails)) { + console.log('!!! props.personalDetails') + } + if (!_.isEqual(prevProps.currentUserPersonalDetails, this.props.currentUserPersonalDetails)) { + console.log('!!! props.currentUserPersonalDetails') + } + if (!_.isEqual(prevProps.currentlyViewedReportID, this.props.currentlyViewedReportID)) { + console.log('!!! props.currentlyViewedReportID') + } + if (!_.isEqual(prevProps.priorityMode, this.props.priorityMode)) { + console.log('!!! props.priorityMode') + } + if (!_.isEqual(prevProps.reportActions, this.props.reportActions)) { + console.log('!!! props.reportActions') + } + } + getFilteredAndOrderedReports(unfilteredReports) { return this.getRecentReportsOptionListItems(); From e7f7b338ad12650602c48add0d309f1d313a3774 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 11:59:35 +0200 Subject: [PATCH 057/155] WIP on profiling what makes things rerender when switching reports --- src/pages/home/sidebar/SidebarLinks.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 15d9652bb058..5017b8b9c25f 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -108,22 +108,45 @@ class SidebarLinks extends React.Component { componentDidUpdate(prevProps) { if (!_.isEqual(prevProps.reports, this.props.reports)) { console.log('!!! props.reports') + return; } if (!_.isEqual(prevProps.personalDetails, this.props.personalDetails)) { console.log('!!! props.personalDetails') + return; } if (!_.isEqual(prevProps.currentUserPersonalDetails, this.props.currentUserPersonalDetails)) { console.log('!!! props.currentUserPersonalDetails') + return; } if (!_.isEqual(prevProps.currentlyViewedReportID, this.props.currentlyViewedReportID)) { console.log('!!! props.currentlyViewedReportID') + return; } if (!_.isEqual(prevProps.priorityMode, this.props.priorityMode)) { console.log('!!! props.priorityMode') + return; } if (!_.isEqual(prevProps.reportActions, this.props.reportActions)) { console.log('!!! props.reportActions') + return; } + if (!_.isEqual(prevProps.isSmallScreenWidth, this.props.isSmallScreenWidth)) { + console.log('!!! props.isSmallScreenWidth') + return; + } + if (!_.isEqual(prevProps.insets, this.props.insets)) { + console.log('!!! props.insets') + return; + } + if (!_.isEqual(prevProps.onAvatarClick, this.props.onAvatarClick)) { + console.log('!!! props.onAvatarClick') + return; + } + if (!_.isEqual(prevProps.onLinkClick, this.props.onLinkClick)) { + console.log('!!! props.onLinkClick') + return; + } + console.log('!!! unknown') } getFilteredAndOrderedReports(unfilteredReports) { From 8057f213b78b3d40ef88086dd01bcb305be9ca2f Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 12:10:42 +0200 Subject: [PATCH 058/155] Fix proptype warning --- src/components/Tooltip/TooltipRenderedOnPageBody.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Tooltip/TooltipRenderedOnPageBody.js b/src/components/Tooltip/TooltipRenderedOnPageBody.js index d2c40a74b477..3bfcc71fd4c9 100644 --- a/src/components/Tooltip/TooltipRenderedOnPageBody.js +++ b/src/components/Tooltip/TooltipRenderedOnPageBody.js @@ -27,11 +27,11 @@ const propTypes = { /** Any additional amount to manually adjust the horizontal position of the tooltip. A positive value shifts the tooltip to the right, and a negative value shifts it to the left. */ - shiftHorizontal: PropTypes.number.isRequired, + shiftHorizontal: PropTypes.number, /** Any additional amount to manually adjust the vertical position of the tooltip. A positive value shifts the tooltip down, and a negative value shifts it up. */ - shiftVertical: PropTypes.number.isRequired, + shiftVertical: PropTypes.number, /** Text to be shown in the tooltip */ text: PropTypes.string.isRequired, @@ -43,6 +43,11 @@ const propTypes = { numberOfLines: PropTypes.number.isRequired, }; +const defaultProps = { + shiftHorizontal: 0, + shiftVertical: 0, +}; + // Props will change frequently. // On every tooltip hover, we update the position in state which will result in re-rendering. // We also update the state on layout changes which will be triggered often. @@ -132,5 +137,6 @@ class TooltipRenderedOnPageBody extends React.PureComponent { } TooltipRenderedOnPageBody.propTypes = propTypes; +TooltipRenderedOnPageBody.defaultProps = defaultProps; export default TooltipRenderedOnPageBody; From ff4994827b932856e3a4b99aa5e723c4dab6fec9 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 12:51:02 +0200 Subject: [PATCH 059/155] WIP optimizing create option --- src/libs/OptionsListUtils.js | 49 ++++++++++++++------------ src/libs/ReportUtils.js | 7 ++-- src/pages/home/sidebar/SidebarLinks.js | 44 ----------------------- 3 files changed, 31 insertions(+), 69 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 0139a3ad247d..bf208198dae4 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -224,29 +224,31 @@ function createOption(logins, personalDetails, report, reportActions = {}, { const isArchivedRoom = ReportUtils.isArchivedRoom(report); const hasMultipleParticipants = personalDetailList.length > 1 || isChatRoom || isPolicyExpenseChat; const personalDetail = personalDetailList[0]; - const hasOutstandingIOU = lodashGet(report, 'hasOutstandingIOU', false); - const iouReport = hasOutstandingIOU - ? lodashGet(iouReports, `${ONYXKEYS.COLLECTION.REPORT_IOUS}${report.iouReportID}`, {}) - : {}; - - const lastActorDetails = report ? _.find(personalDetailList, {login: report.lastActorEmail}) : null; - const lastMessageTextFromReport = ReportUtils.isReportMessageAttachment({text: lodashGet(report, 'lastMessageText', ''), html: lodashGet(report, 'lastMessageHtml', '')}) - ? `[${Localize.translateLocal('common.attachment')}]` - : Str.htmlDecode(lodashGet(report, 'lastMessageText', '')); + const hasOutstandingIOU = report && report.hasOutstandingIOU || false; + const iouReport = hasOutstandingIOU && iouReports[`${ONYXKEYS.COLLECTION.REPORT_IOUS}${report.iouReportID}`] || {}; + const lastActorDetails = report && personalDetailMap[report.lastActorEmail] || null; + let lastMessageTextFromReport = ''; + if (report) { + if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml})) { + lastMessageTextFromReport = `[${Localize.translateLocal('common.attachment')}]`; + } else { + lastMessageTextFromReport = Str.htmlDecode(report ? report.lastMessageText : ''); + } + } let lastMessageText = report && hasMultipleParticipants && lastActorDetails ? `${lastActorDetails.displayName}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; if (isPolicyExpenseChat && isArchivedRoom) { - const archiveReason = lodashGet(lastReportActions[report.reportID], 'originalMessage.reason', CONST.REPORT.ARCHIVE_REASON.DEFAULT); + const archiveReason = lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: lodashGet(lastActorDetails, 'displayName', report.lastActorEmail), + displayName: archiveReason.displayName || report.lastActorEmail, policyName: ReportUtils.getPolicyName(report, policies), }); } - const tooltipText = ReportUtils.getReportParticipantsTitle(lodashGet(report, ['participants'], [])); + const tooltipText = report && ReportUtils.getReportParticipantsTitle(report.participants || []) || null; const subtitle = ReportUtils.getChatRoomSubtitle(report, policies); const reportName = ReportUtils.getReportName(report, personalDetailMap, policies); let alternateText; @@ -263,9 +265,9 @@ function createOption(logins, personalDetails, report, reportActions = {}, { text: reportName, alternateText, brickRoadIndicator: getBrickRoadIndicatorStatusForReport(report, reportActions), - icons: ReportUtils.getIcons(report, personalDetails, policies, lodashGet(personalDetail, ['avatar'])), + icons: ReportUtils.getIcons(report, personalDetails, policies, personalDetail.avatar), tooltipText, - ownerEmail: lodashGet(report, ['ownerEmail']), + ownerEmail: report ? report.ownerEmail : null, subtitle, participantsList: personalDetailList, @@ -276,14 +278,14 @@ function createOption(logins, personalDetails, report, reportActions = {}, { phoneNumber: !hasMultipleParticipants ? personalDetail.phoneNumber : null, payPalMeAddress: !hasMultipleParticipants ? personalDetail.payPalMeAddress : null, isUnread: report ? report.unreadActionCount > 0 : null, - hasDraftComment: lodashGet(report, 'hasDraft', false), + hasDraftComment: report ? report.hasDraft : false, keyForList: report ? String(report.reportID) : personalDetail.login, searchText: getSearchText(report, reportName, personalDetailList, isChatRoom || isPolicyExpenseChat), - isPinned: lodashGet(report, 'isPinned', false), + isPinned: report ? report.isPinned : false, hasOutstandingIOU, - iouReportID: lodashGet(report, 'iouReportID'), - isIOUReportOwner: lodashGet(iouReport, 'ownerEmail', '') === currentUserLogin, - iouReportAmount: lodashGet(iouReport, 'total', 0), + iouReportID: report ? report.iouReportID : null, + isIOUReportOwner: iouReport ? iouReport.ownerEmail === currentUserLogin : false, + iouReportAmount: iouReport ? iouReport.total : 0, isChatRoom, isArchivedRoom, shouldShowSubscript: isPolicyExpenseChat && !report.isOwnPolicyExpenseChat && !isArchivedRoom, @@ -405,7 +407,7 @@ function getOptions(reports, personalDetails, activeReportID, { const isChatRoom = ReportUtils.isChatRoom(report); const isDefaultRoom = ReportUtils.isDefaultRoom(report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - const logins = lodashGet(report, ['participants'], []); + const logins = report.participants || []; // Report data can sometimes be incomplete. If we have no logins or reportID then we will skip this entry. const shouldFilterNoParticipants = _.isEmpty(logins) && !isChatRoom && !isDefaultRoom && !isPolicyExpenseChat; @@ -413,9 +415,10 @@ function getOptions(reports, personalDetails, activeReportID, { return; } - const hasDraftComment = lodashGet(report, 'hasDraft', false); - const iouReportOwner = lodashGet(report, 'hasOutstandingIOU', false) - ? lodashGet(iouReports, [`${ONYXKEYS.COLLECTION.REPORT_IOUS}${report.iouReportID}`, 'ownerEmail'], '') + const hasDraftComment = report.hasDraft || false; + const iouReport = report.iouReportID && iouReports[`${ONYXKEYS.COLLECTION.REPORT_IOUS}${report.iouReportID}`]; + const iouReportOwner = report.hasOutstandingIOU && iouReport + ? iouReport.ownerEmail : ''; const reportContainsIOUDebt = iouReportOwner && iouReportOwner !== currentUserLogin; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 5cef373dd1ad..a058a0bb7bf8 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -244,8 +244,11 @@ function isArchivedRoom(report) { * @returns {String} */ function getPolicyName(report, policies) { - const defaultValue = report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable'); - return lodashGet(policies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'name'], defaultValue); + const policyName = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] && policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`].name || false; + if (!policyName) { + return report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable'); + } + return policyName; } /** diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 5017b8b9c25f..e35c1e9acce4 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -105,50 +105,6 @@ class SidebarLinks extends React.Component { // this.unreadReports = this.getUnreadReports(props.reports); } - componentDidUpdate(prevProps) { - if (!_.isEqual(prevProps.reports, this.props.reports)) { - console.log('!!! props.reports') - return; - } - if (!_.isEqual(prevProps.personalDetails, this.props.personalDetails)) { - console.log('!!! props.personalDetails') - return; - } - if (!_.isEqual(prevProps.currentUserPersonalDetails, this.props.currentUserPersonalDetails)) { - console.log('!!! props.currentUserPersonalDetails') - return; - } - if (!_.isEqual(prevProps.currentlyViewedReportID, this.props.currentlyViewedReportID)) { - console.log('!!! props.currentlyViewedReportID') - return; - } - if (!_.isEqual(prevProps.priorityMode, this.props.priorityMode)) { - console.log('!!! props.priorityMode') - return; - } - if (!_.isEqual(prevProps.reportActions, this.props.reportActions)) { - console.log('!!! props.reportActions') - return; - } - if (!_.isEqual(prevProps.isSmallScreenWidth, this.props.isSmallScreenWidth)) { - console.log('!!! props.isSmallScreenWidth') - return; - } - if (!_.isEqual(prevProps.insets, this.props.insets)) { - console.log('!!! props.insets') - return; - } - if (!_.isEqual(prevProps.onAvatarClick, this.props.onAvatarClick)) { - console.log('!!! props.onAvatarClick') - return; - } - if (!_.isEqual(prevProps.onLinkClick, this.props.onLinkClick)) { - console.log('!!! props.onLinkClick') - return; - } - console.log('!!! unknown') - } - getFilteredAndOrderedReports(unfilteredReports) { return this.getRecentReportsOptionListItems(); From 7c51a86bfc9dcb992f0add3bc5e2c65eb7d482d6 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 14:16:08 +0200 Subject: [PATCH 060/155] WIP optimizing create option --- src/libs/OptionsListUtils.js | 23 ++++++++++++----------- src/libs/ReportUtils.js | 10 ++++------ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index bf208198dae4..919255e52df2 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -154,28 +154,29 @@ function getParticipantNames(personalDetailList) { * @return {String} */ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolicyExpenseChat) { - const searchTerms = []; + let searchTerms = []; if (!isChatRoomOrPolicyExpenseChat) { - _.each(personalDetailList, (personalDetail) => { - searchTerms.push(personalDetail.displayName); - searchTerms.push(personalDetail.login.replace(/\./g, '')); - }); + for (let i = 0; i < personalDetailList.length; i++) { + const personalDetail = personalDetailList[i]; + searchTerms = searchTerms.concat([personalDetail.displayName, personalDetail.login.replace(/\./g, '')]); + } } if (report) { - searchTerms.push(...reportName); - searchTerms.push(..._.map(reportName.split(','), name => name.trim())); + Array.prototype.push.apply(searchTerms, reportName.split('')); + Array.prototype.push.apply(searchTerms, reportName.split(',')); if (isChatRoomOrPolicyExpenseChat) { const chatRoomSubtitle = ReportUtils.getChatRoomSubtitle(report, policies); - searchTerms.push(...chatRoomSubtitle); - searchTerms.push(..._.map(chatRoomSubtitle.split(','), name => name.trim())); + Array.prototype.push.apply(searchTerms, chatRoomSubtitle.split('')); + Array.prototype.push.apply(searchTerms, chatRoomSubtitle.split(',')); } else { - searchTerms.push(...report.participants); + searchTerms = searchTerms.concat(report.participants); } } - return _.unique(searchTerms).join(' '); + const finalSearchTerms = _.unique(searchTerms).join(' '); + return finalSearchTerms; } /** diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index a058a0bb7bf8..55aa46ff2ea8 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -498,7 +498,7 @@ function getReportName(report, personalDetailsForParticipants = {}, policies = { } if (isPolicyExpenseChat(report)) { - const reportOwnerPersonalDetails = lodashGet(personalDetailsForParticipants, report.ownerEmail); + const reportOwnerPersonalDetails = personalDetailsForParticipants[report.ownerEmail]; const reportOwnerDisplayName = getDisplayNameForParticipant(reportOwnerPersonalDetails) || report.ownerEmail || report.reportName; formattedName = report.isOwnPolicyExpenseChat ? getPolicyName(report, policies) : reportOwnerDisplayName; } @@ -513,11 +513,9 @@ function getReportName(report, personalDetailsForParticipants = {}, policies = { // Not a room or PolicyExpenseChat, generate title from participants const participants = _.without(lodashGet(report, 'participants', []), sessionEmail); - const displayNamesWithTooltips = getDisplayNamesWithTooltips( - _.isEmpty(personalDetailsForParticipants) ? participants : personalDetailsForParticipants, - participants.length > 1, - ); - return _.map(displayNamesWithTooltips, ({displayName}) => displayName).join(', '); + const isMultipleParticipantReport = participants.length > 1; + const participantsToGetTheNamesOf = _.isEmpty(personalDetailsForParticipants) ? participants : personalDetailsForParticipants; + return _.map(participantsToGetTheNamesOf, participant => getDisplayNameForParticipant(participant, isMultipleParticipantReport)).join(', '); } /** From fd60c45f2d3728f53694b6ff1480ed036be371e2 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 14:34:29 +0200 Subject: [PATCH 061/155] WIP optimizing create option --- src/libs/OptionsListUtils.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 919255e52df2..a4eb25e9d6c8 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -143,6 +143,27 @@ function getParticipantNames(personalDetailList) { return participantNames; } +/** + * A very optimized method to remove unique items from an array. + * Taken from https://stackoverflow.com/a/9229821/9114791 + * + * @param {Array} items + * @returns {Array} + */ +function uniqFast(items) { + const seenItems = {}; + const result = []; + let j = 0; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (seenItems[item] !== 1) { + seenItems[item] = 1; + result[j++] = item; + } + } + return result; +} + /** * Returns a string with all relevant search terms. * Default should be serachable by policy/domain name but not by participants. @@ -175,7 +196,7 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic } } - const finalSearchTerms = _.unique(searchTerms).join(' '); + const finalSearchTerms = uniqFast(searchTerms).join(' '); return finalSearchTerms; } From 9fad973fa9a0130be828a3bfbaddddeaf4debf6f Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 15:29:51 +0200 Subject: [PATCH 062/155] Finished optimizing createOption --- src/libs/OptionsListUtils.js | 157 +++++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 61 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index a4eb25e9d6c8..6351b6165bac 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -239,80 +239,115 @@ function createOption(logins, personalDetails, report, reportActions = {}, { showChatPreviewLine = false, forcePolicyNamePreview = false, }) { - const isChatRoom = ReportUtils.isChatRoom(report); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); + const result = { + text: null, + alternateText: null, + brickRoadIndicator: null, + icons: null, + tooltipText: null, + ownerEmail: null, + subtitle: null, + participantsList: null, + login: null, + reportID: null, + phoneNumber: null, + payPalMeAddress: null, + isUnread: null, + hasDraftComment: false, + keyForList: null, + searchText: null, + isPinned: false, + hasOutstandingIOU: false, + iouReportID: null, + isIOUReportOwner: null, + iouReportAmount: 0, + isChatRoom: false, + isArchivedRoom: false, + shouldShowSubscript: false, + isPolicyExpenseChat: false, + }; + const personalDetailMap = getPersonalDetailsForLogins(logins, personalDetails); const personalDetailList = _.values(personalDetailMap); - const isArchivedRoom = ReportUtils.isArchivedRoom(report); - const hasMultipleParticipants = personalDetailList.length > 1 || isChatRoom || isPolicyExpenseChat; const personalDetail = personalDetailList[0]; - const hasOutstandingIOU = report && report.hasOutstandingIOU || false; - const iouReport = hasOutstandingIOU && iouReports[`${ONYXKEYS.COLLECTION.REPORT_IOUS}${report.iouReportID}`] || {}; - const lastActorDetails = report && personalDetailMap[report.lastActorEmail] || null; - let lastMessageTextFromReport = ''; + let hasMultipleParticipants = personalDetailList.length > 1; + let subtitle; + if (report) { + result.isChatRoom = ReportUtils.isChatRoom(report); + result.isArchivedRoom = ReportUtils.isArchivedRoom(report); + result.isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); + result.shouldShowSubscript = result.isPolicyExpenseChat && !report.isOwnPolicyExpenseChat && !result.isArchivedRoom; + result.brickRoadIndicator = getBrickRoadIndicatorStatusForReport(report, reportActions); + result.ownerEmail = report.ownerEmail; + result.reportID = report.reportID; + result.isUnread = report.unreadActionCount > 0; + result.hasDraftComment = report.hasDraft; + result.isPinned = report.isPinned; + result.iouReportID = report.iouReportID; + result.keyForList = String(report.reportID); + result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participants || []); + result.hasOutstandingIOU = report.hasOutstandingIOU; + + hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; + subtitle = ReportUtils.getChatRoomSubtitle(report, policies); + + let lastMessageTextFromReport = ''; if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml})) { lastMessageTextFromReport = `[${Localize.translateLocal('common.attachment')}]`; } else { lastMessageTextFromReport = Str.htmlDecode(report ? report.lastMessageText : ''); } + + const lastActorDetails = personalDetailMap[report.lastActorEmail] || null; + let lastMessageText = hasMultipleParticipants && lastActorDetails + ? `${lastActorDetails.displayName}: ` + : ''; + lastMessageText += report ? lastMessageTextFromReport : ''; + + if (result.isPolicyExpenseChat && result.isArchivedRoom) { + const archiveReason = lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; + lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { + displayName: archiveReason.displayName || report.lastActorEmail, + policyName: ReportUtils.getPolicyName(report, policies), + }); + } + + if (result.isChatRoom || result.isPolicyExpenseChat) { + result.alternateText = (showChatPreviewLine && !forcePolicyNamePreview && lastMessageText) + ? lastMessageText + : subtitle; + } else { + result.alternateText = (showChatPreviewLine && lastMessageText) + ? lastMessageText + : Str.removeSMSDomain(personalDetail.login); + } + } else { + result.keyForList = personalDetail.login; } - let lastMessageText = report && hasMultipleParticipants && lastActorDetails - ? `${lastActorDetails.displayName}: ` - : ''; - lastMessageText += report ? lastMessageTextFromReport : ''; - - if (isPolicyExpenseChat && isArchivedRoom) { - const archiveReason = lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; - lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || report.lastActorEmail, - policyName: ReportUtils.getPolicyName(report, policies), - }); + + if (result.hasOutstandingIOU) { + const iouReport = iouReports[`${ONYXKEYS.COLLECTION.REPORT_IOUS}${report.iouReportID}`] || null; + if (iouReport) { + result.isIOUReportOwner = iouReport.ownerEmail === currentUserLogin; + result.iouReportAmount = iouReport.total; + } } - const tooltipText = report && ReportUtils.getReportParticipantsTitle(report.participants || []) || null; - const subtitle = ReportUtils.getChatRoomSubtitle(report, policies); - const reportName = ReportUtils.getReportName(report, personalDetailMap, policies); - let alternateText; - if (isChatRoom || isPolicyExpenseChat) { - alternateText = (showChatPreviewLine && !forcePolicyNamePreview && lastMessageText) - ? lastMessageText - : subtitle; - } else { - alternateText = (showChatPreviewLine && lastMessageText) - ? lastMessageText - : Str.removeSMSDomain(personalDetail.login); + if (!hasMultipleParticipants) { + result.login = personalDetail.login; + result.phoneNumber = personalDetail.phoneNumber; + result.payPalMeAddress = personalDetail.payPalMeAddress; } - return { - text: reportName, - alternateText, - brickRoadIndicator: getBrickRoadIndicatorStatusForReport(report, reportActions), - icons: ReportUtils.getIcons(report, personalDetails, policies, personalDetail.avatar), - tooltipText, - ownerEmail: report ? report.ownerEmail : null, - subtitle, - participantsList: personalDetailList, - - // It doesn't make sense to provide a login in the case of a report with multiple participants since - // there isn't any one single login to refer to for a report. - login: !hasMultipleParticipants ? personalDetail.login : null, - reportID: report ? report.reportID : null, - phoneNumber: !hasMultipleParticipants ? personalDetail.phoneNumber : null, - payPalMeAddress: !hasMultipleParticipants ? personalDetail.payPalMeAddress : null, - isUnread: report ? report.unreadActionCount > 0 : null, - hasDraftComment: report ? report.hasDraft : false, - keyForList: report ? String(report.reportID) : personalDetail.login, - searchText: getSearchText(report, reportName, personalDetailList, isChatRoom || isPolicyExpenseChat), - isPinned: report ? report.isPinned : false, - hasOutstandingIOU, - iouReportID: report ? report.iouReportID : null, - isIOUReportOwner: iouReport ? iouReport.ownerEmail === currentUserLogin : false, - iouReportAmount: iouReport ? iouReport.total : 0, - isChatRoom, - isArchivedRoom, - shouldShowSubscript: isPolicyExpenseChat && !report.isOwnPolicyExpenseChat && !isArchivedRoom, - isPolicyExpenseChat, - }; + + const reportName = ReportUtils.getReportName(report, personalDetailMap, policies); + result.text = reportName; + result.subtitle = subtitle; + result.participantsList = personalDetailList; + result.icons = ReportUtils.getIcons(report, personalDetails, policies, personalDetail.avatar); + result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom || result.isPolicyExpenseChat); + + return result; } /** From ceb2ea954cec4810eab77506a8794a146d9080cf Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 15:32:11 +0200 Subject: [PATCH 063/155] Remove memoization and imports --- src/libs/OptionsListUtils.js | 5 +---- src/pages/home/sidebar/SidebarLinks.js | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 6351b6165bac..80f726988832 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -3,7 +3,6 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; -import memoizeOne from 'memoize-one'; import Str from 'expensify-common/lib/str'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; @@ -817,7 +816,7 @@ function getMemberInviteOptions( * @param {Object} reportActions * @returns {Object} */ -function calculateSidebarOptions(reports, personalDetails, activeReportID, priorityMode, betas, reportActions) { +function getSidebarOptions(reports, personalDetails, activeReportID, priorityMode, betas, reportActions) { let sideBarOptions = { prioritizeIOUDebts: true, prioritizeReportsWithDraftComments: true, @@ -841,8 +840,6 @@ function calculateSidebarOptions(reports, personalDetails, activeReportID, prior }); } -const getSidebarOptions = memoizeOne(calculateSidebarOptions); - /** * Helper method that returns the text to be used for the header's message and title (if any) * diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index e35c1e9acce4..fa202bbc36c2 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -3,7 +3,6 @@ import {View, TouchableOpacity} from 'react-native'; import _ from 'underscore'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import lodashGet from 'lodash/get'; import styles from '../../../styles/styles'; import * as StyleUtils from '../../../styles/StyleUtils'; import ONYXKEYS from '../../../ONYXKEYS'; From 2ef3622b00336316e72acf4789ab8424079308dd Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 16:10:08 +0200 Subject: [PATCH 064/155] Memoize getting the option list items --- src/pages/home/sidebar/SidebarLinks.js | 149 +++---------------------- 1 file changed, 18 insertions(+), 131 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index fa202bbc36c2..f4bdda7ec179 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -3,6 +3,7 @@ import {View, TouchableOpacity} from 'react-native'; import _ from 'underscore'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import memoizeOne from 'memoize-one'; import styles from '../../../styles/styles'; import * as StyleUtils from '../../../styles/StyleUtils'; import ONYXKEYS from '../../../ONYXKEYS'; @@ -94,142 +95,21 @@ const defaultProps = { class SidebarLinks extends React.Component { constructor(props) { super(props); - - // this.activeReport = { - // reportID: props.currentlyViewedReportID, - // }; - // - // this.orderedReports = []; - // this.priorityMode = props.priorityMode; - // this.unreadReports = this.getUnreadReports(props.reports); - } - - getFilteredAndOrderedReports(unfilteredReports) { - return this.getRecentReportsOptionListItems(); - - // const isActiveReportSame = this.activeReport.reportID === this.props.currentlyViewedReportID; - // const lastMessageTimestamp = lodashGet(unfilteredReports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.lastMessageTimestamp`, 0); - // - // // Determines if the active report has a history of draft comments while active. - // let hasDraftHistory; - // - // // If the active report has not changed and the message has been sent, set the draft history flag to false so LHN can reorder. - // // Otherwise, if the active report has not changed and the flag was previously true, preserve the state so LHN cannot reorder. - // // Otherwise, update the flag from the prop value. - // if (isActiveReportSame && this.activeReport.lastMessageTimestamp !== lastMessageTimestamp) { - // hasDraftHistory = false; - // } else if (isActiveReportSame && this.activeReport.hasDraftHistory) { - // hasDraftHistory = true; - // } else { - // hasDraftHistory = lodashGet(this.props.reports, `${ONYXKEYS.COLLECTION.REPORT}${this.props.currentlyViewedReportID}.hasDraft`, false); - // } - // - // const switchingPriorityModes = this.props.priorityMode !== this.priorityMode; - // - // // Build the report options we want to show - // const recentReports = this.getRecentReportsOptionListItems(); - // - // // If the order of the reports is different from the last render (or if priority mode is changing) - // // then orderedReports is the same as the freshly calculated recentReports. - // // If the order of the reports is the same as the last render - // // then the data for each report is updated from the data in the new props - // // @TODO: not sure why this is necessary and see if it can be removed or do something more intuitive - // const orderedReports = this.isReportOrderDifferentThanLastRender(hasDraftHistory) || switchingPriorityModes - // ? recentReports - // : _.chain(this.orderedReports) - // - // // To preserve the order of the conversations, we map over the previous ordered reports. - // // Then match and replace older reports with the newer report conversations from recentReports - // .map(orderedReport => _.find(recentReports, recentReport => orderedReport.reportID === recentReport.reportID)) - // - // // Because we are using map, we have to filter out any undefined reports. This happens if recentReports - // // does not have all the conversations in the previous set of orderedReports - // .compact() - // .value(); - // - // // Store these pieces of data on the class so that the next time this method is called - // // the previous values can be compared against to tell if something changed which would - // // cause the reports to be reordered - // this.orderedReports = orderedReports; - // this.priorityMode = this.props.priorityMode; - // this.activeReport = { - // reportID: this.props.currentlyViewedReportID, - // hasDraftHistory, - // lastMessageTimestamp, - // }; - // this.unreadReports = this.getUnreadReports(unfilteredReports); - // - // return this.orderedReports; - } - - /** - * Create a map of unread reports that looks like this: - * { - * 1: true, - * 2: true, - * } - * This is so that when the new props are compared to the old props, it's - * fast to look up if there are any new unread reports. - * - * @param {Object[]} reports - * @returns {Object} - */ - getUnreadReports(reports) { - // return _.reduce(reports, (finalUnreadReportMap, report) => { - // if (report.unreadActionCount > 0) { - // return { - // [report.reportID]: true, - // ...finalUnreadReportMap, - // }; - // } - // return finalUnreadReportMap; - // }, {}); + this.getRecentReportsOptionListItems = memoizeOne(this.getRecentReportsOptionListItems.bind(this)); } - getRecentReportsOptionListItems() { - const activeReportID = this.props.currentlyViewedReportID; + getRecentReportsOptionListItems(activeReportID, priorityMode, unorderedReports, personalDetails, betas, reportActions) { const sidebarOptions = OptionsListUtils.getSidebarOptions( - this.props.reports, - this.props.personalDetails, + unorderedReports, + personalDetails, activeReportID, - this.props.priorityMode, - this.props.betas, - this.props.reportActions, + priorityMode, + betas, + reportActions, ); return sidebarOptions.recentReports; } - isReportOrderDifferentThanLastRender(hasDraftHistory) { - // // If the number of reports changed, then the report order is different - // if (this.orderedReports.length !== this.props.reports.length) { - // return true; - // } - // - // // If the active report changed, then the report order is different - // if (this.activeReport.reportID !== this.props.currentlyViewedReportID) { - // return true; - // } - // - // // If the active report has a draft, the order of the reports doesn't change - // // because it would cause the reports to reorder when a user starts typing a comment - // // and that is an annoying UX (too much stuff jumping around) - // if (this.props.currentlyViewedReportID && hasDraftHistory) { - // return false; - // } - // - // // If the unread reports have changed, then the report order changes - // // because the unread reports need to be placed at the top of the list - // // @TODO: This can probably be optimized - // const hasNewUnreadReports = _.some(this.props.reports, report => report.unreadActionCount > 0 && !this.unreadReports[report.reportID]); - // if (hasNewUnreadReports) { - // return true; - // } - // - // // By default, assume that the order of the reports doesn't change - // // in order to optimize the rendering - // return false; - } - showSearchPage() { Navigation.navigate(ROUTES.SEARCH); } @@ -240,12 +120,19 @@ class SidebarLinks extends React.Component { return null; } - const activeReportID = parseInt(this.props.currentlyViewedReportID, 10); Timing.start(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); + const optionListItems = this.getRecentReportsOptionListItems( + this.props.currentlyViewedReportID, + this.props.priorityMode, + this.props.reports, + this.props.personalDetails, + this.props.betas, + this.props.reportActions, + ); const sections = [{ title: '', indexOffset: 0, - data: this.getFilteredAndOrderedReports(this.props.reports), + data: optionListItems, shouldShow: true, }]; Timing.end(CONST.TIMING.SIDEBAR_LINKS_FILTER_REPORTS); @@ -297,7 +184,7 @@ class SidebarLinks extends React.Component { ]} sections={sections} focusedIndex={_.findIndex(this.orderedReports, ( - option => option.reportID === activeReportID + option => option.reportID.toString() === this.props.currentlyViewedReportID.toString() ))} onSelectRow={(option) => { Navigation.navigate(ROUTES.getReportRoute(option.reportID)); From 9ceb909b0c8c13553d4ad9d39d608af513cfa141 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 1 Sep 2022 16:30:05 +0200 Subject: [PATCH 065/155] Fix lint issues --- src/libs/OptionsListUtils.js | 3 ++- src/libs/ReportUtils.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 80f726988832..a1e5027bf9fd 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -305,7 +305,8 @@ function createOption(logins, personalDetails, report, reportActions = {}, { lastMessageText += report ? lastMessageTextFromReport : ''; if (result.isPolicyExpenseChat && result.isArchivedRoom) { - const archiveReason = lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; + const archiveReason = (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) + || CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { displayName: archiveReason.displayName || report.lastActorEmail, policyName: ReportUtils.getPolicyName(report, policies), diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 55aa46ff2ea8..ac36f15649a3 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -244,7 +244,7 @@ function isArchivedRoom(report) { * @returns {String} */ function getPolicyName(report, policies) { - const policyName = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] && policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`].name || false; + const policyName = (policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] && policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`].name) || false; if (!policyName) { return report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable'); } From 41671c724443e00ec4b8e1de5be307525262ce7c Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Fri, 2 Sep 2022 10:52:37 +0200 Subject: [PATCH 066/155] Handle API call and change button text --- src/pages/SetPasswordPage.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index ae46a1c53c76..b7d15d9fd610 100755 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -30,6 +30,9 @@ const propTypes = { /** Whether a sign on form is loading (being submitted) */ isLoading: PropTypes.bool, + + /** If account is validated or not */ + validated: PropTypes.bool, }), /** The credentials of the logged in person */ @@ -75,15 +78,6 @@ class SetPasswordPage extends Component { }; } - componentDidMount() { - const accountID = lodashGet(this.props.route.params, 'accountID', ''); - const validateCode = lodashGet(this.props.route.params, 'validateCode', ''); - if (this.props.credentials.authToken) { - return; - } - Session.validateEmail(accountID, validateCode); - } - validateAndSubmitForm() { if (!this.state.isFormValid) { return; @@ -91,15 +85,16 @@ class SetPasswordPage extends Component { const accountID = lodashGet(this.props.route.params, 'accountID', ''); const validateCode = lodashGet(this.props.route.params, 'validateCode', ''); - if (this.props.credentials.authToken) { - Session.changePasswordAndSignIn(this.props.credentials.authToken, this.state.password); - } else { + if (this.props.account.validated) { Session.setPassword(this.state.password, validateCode, accountID); + } else { + Session.setPasswordForNewAccountAndSignin(accountID, validateCode, this.state.password); + // Session.changePasswordAndSignIn(this.props.credentials.authToken, this.state.password); } } render() { - const buttonText = !this.props.account.validated ? this.props.translate('setPasswordPage.validateAccount') : this.props.translate('setPasswordPage.setPassword'); + const buttonText = this.props.translate('setPasswordPage.setPassword'); const sessionError = this.props.session.error && this.props.translate(this.props.session.error); const error = sessionError || this.props.account.error; return ( From f4e191d0c84d1d6a81ec5770ef7190cea26cfd6a Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Fri, 2 Sep 2022 17:55:38 +0200 Subject: [PATCH 067/155] Update errors to error --- src/CONST.js | 2 +- src/libs/actions/Session/index.js | 39 ++++++++++++++++-------------- src/libs/actions/SignInRedirect.js | 3 ++- src/libs/actions/User.js | 3 ++- src/pages/SetPasswordPage.js | 12 ++++----- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 4b0bd6f4a502..59a80df5c244 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -408,7 +408,7 @@ const CONST = { FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis', }, DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, - DEFAULT_ACCOUNT_DATA: {error: '', success: '', loading: false}, + DEFAULT_ACCOUNT_DATA: {errors: '', success: '', loading: false}, APP_STATE: { ACTIVE: 'active', BACKGROUND: 'background', diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index dd6d247e7b5f..8ec90f4a89b2 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -43,7 +43,7 @@ function setSuccessfulSignInData(data) { PushNotification.register(data.accountID); Onyx.merge(ONYXKEYS.SESSION, { shouldShowComposeInput: true, - error: null, + errors: null, ..._.pick(data, 'authToken', 'accountID', 'email', 'encryptedAuthToken'), }); } @@ -188,7 +188,7 @@ function createTemporaryLogin(authToken, email) { }) .then((createLoginResponse) => { if (createLoginResponse.jsonCode !== 200) { - Onyx.merge(ONYXKEYS.ACCOUNT, {error: createLoginResponse.message}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: createLoginResponse.message}); return createLoginResponse; } @@ -250,7 +250,7 @@ function signIn(password, twoFactorAuthCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, isLoading: false}); return; } - Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal(errorMessage), isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: Localize.translateLocal(errorMessage), isLoading: false}); return; } @@ -344,7 +344,7 @@ function setPassword(password, validateCode, accountID) { } // This request can fail if the password is not complex enough - Onyx.merge(ONYXKEYS.ACCOUNT, {error: response.message}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: response.message}); }) .finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); @@ -386,7 +386,6 @@ function cleanupSession() { function clearAccountMessages() { Onyx.merge(ONYXKEYS.ACCOUNT, { - error: '', success: '', errors: [], isLoading: false, @@ -418,35 +417,35 @@ function changePasswordAndSignIn(authToken, password) { } if (responsePassword.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) { // authToken has expired, and we have the account email, so we request a new magic link. - Onyx.merge(ONYXKEYS.ACCOUNT, {error: null}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: null}); resetPassword(); Navigation.navigate(ROUTES.HOME); return; } - Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.passwordNotSet'}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal('setPasswordPage.passwordNotSet')}}); }); } /** * Validates new user login, sets a new password and authenticates them - * @param {String} authToken + * @param {Number} accountID + * @param {String} validateCode * @param {String} password */ -function setPasswordForNewUserAndSignin(authToken, password) { +function setPasswordForNewAccountAndSignin(accountID, validateCode, password) { const optimisticData = [ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, value: { isLoading: true, - validateCodeExpired: false, }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.SESSION, value: { - error: '', + errors: null, }, }, ]; @@ -457,13 +456,14 @@ function setPasswordForNewUserAndSignin(authToken, password) { key: ONYXKEYS.ACCOUNT, value: { isLoading: false, + errors: null, }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.SESSION, value: { - error: '', + errors: null, }, }, ]; @@ -474,18 +474,21 @@ function setPasswordForNewUserAndSignin(authToken, password) { key: ONYXKEYS.ACCOUNT, value: { isLoading: false, - error: 'Unable to set Password', }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.SESSION, value: { - error: 'setPasswordPage.passwordNotSet', + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('setPasswordPage.passwordNotSet'), + }, }, }, ]; - API.write('SetPasswordForNewAccountAndSignin', {authToken, password}, {optimisticData, successData, failureData}); + API.write('SetPasswordForNewAccountAndSignin', { + accountID, validateCode, password, + }, {optimisticData, successData, failureData}); } /** @@ -495,7 +498,7 @@ function setPasswordForNewUserAndSignin(authToken, password) { * @param {String} authToken */ function validateEmail(accountID, validateCode) { - Onyx.merge(ONYXKEYS.SESSION, {error: ''}); + Onyx.merge(ONYXKEYS.SESSION, {errors: null}); DeprecatedAPI.ValidateEmail({ accountID, validateCode, @@ -509,7 +512,7 @@ function validateEmail(accountID, validateCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {validated: true}); } if (responseValidate.jsonCode === 401) { - Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.setPasswordLinkInvalid'}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translate('setPasswordPage.setPasswordLinkInvalid')}}); } }); } @@ -582,7 +585,7 @@ function setShouldShowComposeInput(shouldShowComposeInput) { export { beginSignIn, setPassword, - setPasswordForNewUserAndSignin, + setPasswordForNewAccountAndSignin, signIn, signInWithShortLivedToken, signOut, diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 597de6d2ecd8..4542d943c7d7 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; +import DateUtils from '../DateUtils'; let currentActiveClients; Onyx.connect({ @@ -44,7 +45,7 @@ function clearStorageAndRedirect(errorMessage) { } // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set`. - Onyx.merge(ONYXKEYS.SESSION, {error: errorMessage}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); }); } diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index a7b71d9fd3b5..bf67d7d42d29 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -18,6 +18,7 @@ import * as Link from './Link'; import getSkinToneEmojiFromIndex from '../../components/EmojiPicker/getSkinToneEmojiFromIndex'; import * as SequentialQueue from '../Network/SequentialQueue'; import PusherUtils from '../PusherUtils'; +import DateUtils from '../DateUtils'; let sessionAuthToken = ''; let currentUserAccountID = ''; @@ -233,7 +234,7 @@ function validateLogin(accountID, validateCode) { } } else { const error = lodashGet(response, 'message', 'Unable to validate login.'); - Onyx.merge(ONYXKEYS.ACCOUNT, {error}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: error}}); } }).finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index b7d15d9fd610..0f35f098e54e 100755 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -19,6 +19,7 @@ import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import compose from '../libs/compose'; import NewPasswordForm from './settings/NewPasswordForm'; import FormAlertWithSubmitButton from '../components/FormAlertWithSubmitButton'; +import * as ErrorUtils from '../libs/ErrorUtils'; const propTypes = { /* Onyx Props */ @@ -26,7 +27,7 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ /** An error message to display to the user */ - error: PropTypes.string, + errors: PropTypes.string, /** Whether a sign on form is loading (being submitted) */ isLoading: PropTypes.bool, @@ -47,7 +48,7 @@ const propTypes = { /** Session object */ session: PropTypes.shape({ /** An error message to display to the user */ - error: PropTypes.string, + errors: PropTypes.string, }), /** The accountID and validateCode are passed via the URL */ @@ -61,7 +62,7 @@ const defaultProps = { credentials: {}, route: validateLinkDefaultProps, session: { - error: '', + errors: '', authToken: '', }, }; @@ -89,14 +90,13 @@ class SetPasswordPage extends Component { Session.setPassword(this.state.password, validateCode, accountID); } else { Session.setPasswordForNewAccountAndSignin(accountID, validateCode, this.state.password); - // Session.changePasswordAndSignIn(this.props.credentials.authToken, this.state.password); } } render() { const buttonText = this.props.translate('setPasswordPage.setPassword'); - const sessionError = this.props.session.error && this.props.translate(this.props.session.error); - const error = sessionError || this.props.account.error; + const sessionError = this.props.session.errors && ErrorUtils.getLatestErrorMessage(this.props.session); + const error = sessionError || ErrorUtils.getLatestErrorMessage(this.props.account); return ( Date: Mon, 5 Sep 2022 09:36:20 +0100 Subject: [PATCH 068/155] Remove unused HOC --- src/components/OptionsSelector/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/OptionsSelector/index.js b/src/components/OptionsSelector/index.js index eea20274561e..9f7c924e427f 100644 --- a/src/components/OptionsSelector/index.js +++ b/src/components/OptionsSelector/index.js @@ -11,4 +11,4 @@ const OptionsSelector = forwardRef((props, ref) => ( OptionsSelector.displayName = 'OptionsSelector'; -export default withLocalize(OptionsSelector); +export default OptionsSelector; From 42ece5b3f399e8d2d563768f7ef0af1326d78dbf Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 Sep 2022 09:50:36 +0100 Subject: [PATCH 069/155] Add error boundary to rendered component in tests --- tests/unit/LHNOrderTest.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/tests/unit/LHNOrderTest.js b/tests/unit/LHNOrderTest.js index 3e411f7ca251..e085d6001289 100644 --- a/tests/unit/LHNOrderTest.js +++ b/tests/unit/LHNOrderTest.js @@ -158,18 +158,37 @@ Onyx.init({ }); function getDefaultRenderedSidebarLinks() { + class ErrorBoundary extends React.Component { + // Error boundaries have to implement this method. It's for providing a fallback UI, but + // we don't need that for unit testing, so this is basically a no-op. + static getDerivedStateFromError(error) { + return {error}; + } + + componentDidCatch(error, errorInfo) { + console.error(error, errorInfo); + } + + render() { + // eslint-disable-next-line react/prop-types + return this.props.children; + } + } + // Wrap the SideBarLinks inside of LocaleContextProvider so that all the locale props // are passed to the component. If this is not done, then all the locale props are missing // and there are a lot of render warnings. It needs to be done like this because normally in // our app (App.js) is when the react application is wrapped in the context providers return render(( - {}} - insets={fakeInsets} - onAvatarClick={() => {}} - isSmallScreenWidth={false} - /> + + {}} + insets={fakeInsets} + onAvatarClick={() => {}} + isSmallScreenWidth={false} + /> + )); } From 2394c80b189b2e3179a0777d19b1b35ae83186c6 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Mon, 5 Sep 2022 12:27:06 +0100 Subject: [PATCH 070/155] Prevent setting empty error --- src/libs/actions/SignInRedirect.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 4542d943c7d7..4806242051dd 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -44,8 +44,10 @@ function clearStorageAndRedirect(errorMessage) { Onyx.set(ONYXKEYS.NETWORK, {isOffline}); } - // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set`. - Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); + // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` + if (errorMessage) { + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); + } }); } From 66d147d3987747860caf2eaf5b32634dd66a4b01 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 Sep 2022 16:20:35 +0100 Subject: [PATCH 071/155] Add an early return to protect against null reports --- src/libs/OptionsListUtils.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e3505ad53a67..fb969a157c48 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -462,6 +462,9 @@ function getOptions(reports, personalDetails, activeReportID, { const allReportOptions = []; _.each(orderedReports, (report) => { + if (!report) { + return null; + } const isChatRoom = ReportUtils.isChatRoom(report); const isDefaultRoom = ReportUtils.isDefaultRoom(report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); @@ -469,7 +472,7 @@ function getOptions(reports, personalDetails, activeReportID, { // Report data can sometimes be incomplete. If we have no logins or reportID then we will skip this entry. const shouldFilterNoParticipants = _.isEmpty(logins) && !isChatRoom && !isDefaultRoom && !isPolicyExpenseChat; - if (!report || !report.reportID || shouldFilterNoParticipants) { + if (!report.reportID || shouldFilterNoParticipants) { return; } From d03e6fbed7c5720cac8c6d7a569ba50af6611064 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 Sep 2022 16:29:03 +0100 Subject: [PATCH 072/155] Add a method comment about performance --- src/libs/OptionsListUtils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index fb969a157c48..e2a0567cdd4b 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -167,6 +167,10 @@ function uniqFast(items) { * Returns a string with all relevant search terms. * Default should be serachable by policy/domain name but not by participants. * + * This method must be incredibly performant. It was found to be a big performance bottleneck + * when dealing with accounts that have thousands of reports. For loops are more efficient than _.each + * Array.prototype.push.apply is faster than using the spread operator, and concat() is faster than push(). + * * @param {Object} report * @param {String} reportName * @param {Array} personalDetailList From 47aa0b8f3ab00f50c1381aad0d9b4221f7da037f Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 Sep 2022 16:31:37 +0100 Subject: [PATCH 073/155] Keep policy name as a string and simplify the return --- src/libs/ReportUtils.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 53e80e0ab32f..45d317e355b4 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -246,11 +246,11 @@ function isArchivedRoom(report) { * @returns {String} */ function getPolicyName(report, policies) { - const policyName = (policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] && policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`].name) || false; - if (!policyName) { - return report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable'); - } - return policyName; + const policyName = ( + policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] + && policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`].name + ) || ''; + return policyName || report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable'); } /** From 99532c20083b3d648c715a978826f727fbd6b60f Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 Sep 2022 16:59:45 +0100 Subject: [PATCH 074/155] Simplify the return statement --- src/libs/OptionsListUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e2a0567cdd4b..a4c4d93642a5 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -199,8 +199,7 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic } } - const finalSearchTerms = uniqFast(searchTerms).join(' '); - return finalSearchTerms; + return uniqFast(searchTerms).join(' '); } /** @@ -259,6 +258,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { hasDraftComment: false, keyForList: null, searchText: null, + isDefaultRoom: false, isPinned: false, hasOutstandingIOU: false, iouReportID: null, From 988e7c6d3b40e6068fcc2a30922b718f565b27c2 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 Sep 2022 17:20:16 +0100 Subject: [PATCH 075/155] Fix the check for unread reports --- src/libs/OptionsListUtils.js | 2 +- src/libs/ReportUtils.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index a4c4d93642a5..96e9eb92d895 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -285,7 +285,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { result.brickRoadIndicator = getBrickRoadIndicatorStatusForReport(report, reportActions); result.ownerEmail = report.ownerEmail; result.reportID = report.reportID; - result.isUnread = report.unreadActionCount > 0; + result.isUnread = ReportUtils.isUnread(report); result.hasDraftComment = report.hasDraft; result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 45d317e355b4..4ce8ecf45def 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -626,8 +626,8 @@ function buildOptimisticIOUReportAction(type, amount, comment, paymentType = '', * @returns {Boolean} */ function isUnread(report) { - const lastReadSequenceNumber = lodashGet(report, 'lastReadSequenceNumber', 0); - const maxSequenceNumber = lodashGet(report, 'maxSequenceNumber', 0); + const lastReadSequenceNumber = report.lastReadSequenceNumber || 0; + const maxSequenceNumber = report.maxSequenceNumber || 0; return lastReadSequenceNumber < maxSequenceNumber; } From 74f1dc6ebe4f9f9bc04e0afc4fc1d16f821504dd Mon Sep 17 00:00:00 2001 From: Varsha Date: Mon, 5 Sep 2022 21:58:08 +0530 Subject: [PATCH 076/155] center loading indicator --- src/pages/AddPersonalBankAccountPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js index dc5245702c88..248811ba7348 100644 --- a/src/pages/AddPersonalBankAccountPage.js +++ b/src/pages/AddPersonalBankAccountPage.js @@ -164,7 +164,7 @@ class AddPersonalBankAccountPage extends React.Component { ) : ( - + { this.setState({ From 233aa13cc1d13aa8f23f41fd4c4a1513c840999d Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Tue, 6 Sep 2022 10:16:08 +0100 Subject: [PATCH 077/155] Update error props and displaying error --- src/pages/SetPasswordPage.js | 6 +++--- src/pages/settings/PasswordPage.js | 7 ++++--- src/pages/signin/LoginForm.js | 2 +- src/pages/signin/PasswordForm.js | 3 ++- src/pages/signin/SignInPage.js | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index 0f35f098e54e..82c7ca263b1c 100755 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -27,7 +27,7 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ /** An error message to display to the user */ - errors: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), /** Whether a sign on form is loading (being submitted) */ isLoading: PropTypes.bool, @@ -48,7 +48,7 @@ const propTypes = { /** Session object */ session: PropTypes.shape({ /** An error message to display to the user */ - errors: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), }), /** The accountID and validateCode are passed via the URL */ @@ -62,7 +62,7 @@ const defaultProps = { credentials: {}, route: validateLinkDefaultProps, session: { - errors: '', + errors: null, authToken: '', }, }; diff --git a/src/pages/settings/PasswordPage.js b/src/pages/settings/PasswordPage.js index 69270c2bacaf..590d12b7a550 100755 --- a/src/pages/settings/PasswordPage.js +++ b/src/pages/settings/PasswordPage.js @@ -18,6 +18,7 @@ import FixedFooter from '../../components/FixedFooter'; import TextInput from '../../components/TextInput'; import * as Session from '../../libs/actions/Session'; import PasswordConfirmationScreen from './PasswordConfirmationScreen'; +import * as ErrorUtils from '../../libs/ErrorUtils'; const propTypes = { /* Onyx Props */ @@ -25,7 +26,7 @@ const propTypes = { /** Holds information about the users account that is logging in */ account: PropTypes.shape({ /** An error message to display to the user */ - error: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), /** Success message to display when necessary */ success: PropTypes.string, @@ -208,9 +209,9 @@ class PasswordPage extends Component { )} - {_.every(this.state.errors, error => !error) && !_.isEmpty(this.props.account.error) && ( + {_.every(this.state.errors, error => !error) && !_.isEmpty(this.props.account.errors) && ( - {this.props.account.error} + {ErrorUtils.getLatestErrorMessage(this.props.account)} )} diff --git a/src/pages/signin/LoginForm.js b/src/pages/signin/LoginForm.js index 2490103b97e3..b6557ec413a3 100755 --- a/src/pages/signin/LoginForm.js +++ b/src/pages/signin/LoginForm.js @@ -32,7 +32,7 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ /** An error message to display to the user */ - error: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), /** Success message to display when necessary */ success: PropTypes.string, diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js index 2d3d0e418e53..c70f25633121 100755 --- a/src/pages/signin/PasswordForm.js +++ b/src/pages/signin/PasswordForm.js @@ -20,6 +20,7 @@ import * as ComponentUtils from '../../libs/ComponentUtils'; import * as ValidationUtils from '../../libs/ValidationUtils'; import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '../../components/withToggleVisibilityView'; import canFocusInputOnScreenFocus from '../../libs/canFocusInputOnScreenFocus'; +import * as ErrorUtils from '../../libs/ErrorUtils'; const propTypes = { /* Onyx Props */ @@ -179,7 +180,7 @@ class PasswordForm extends React.Component { {!this.state.formError && this.props.account && !_.isEmpty(this.props.account.error) && ( - {this.props.account.error} + {ErrorUtils.getLatestErrorMessage(this.props.account)} )} diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index c19d8ba09fe9..065a3206ea94 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -20,7 +20,7 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ /** Error to display when there is an account error returned */ - error: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), /** Whether the account is validated */ validated: PropTypes.bool, From 403632528ff64bf7ff00aa4349dbd1d287f99bea Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Tue, 6 Sep 2022 05:01:37 -0600 Subject: [PATCH 078/155] Update src/libs/OptionsListUtils.js Co-authored-by: Marc Glasser --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 96e9eb92d895..d9f0c658cfaf 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -467,7 +467,7 @@ function getOptions(reports, personalDetails, activeReportID, { const allReportOptions = []; _.each(orderedReports, (report) => { if (!report) { - return null; + return; } const isChatRoom = ReportUtils.isChatRoom(report); const isDefaultRoom = ReportUtils.isDefaultRoom(report); From 6c855239f1efbc7ab9a079c177292309297c2ca1 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 6 Sep 2022 15:13:56 +0100 Subject: [PATCH 079/155] rm deprecated validateBankAccount --- src/libs/actions/BankAccounts.js | 1 - .../actions/ReimbursementAccount/index.js | 2 - .../validateBankAccount.js | 54 ------------------- src/libs/deprecatedAPI.js | 6 --- 4 files changed, 63 deletions(-) delete mode 100644 src/libs/actions/ReimbursementAccount/validateBankAccount.js diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 43b8b7d2c056..c597429a2614 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -13,7 +13,6 @@ export { setBankAccountFormValidationErrors, resetReimbursementAccount, resetFreePlanBankAccount, - validateBankAccount, hideBankAccountErrors, setWorkspaceIDForReimbursementAccount, setBankAccountSubStep, diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js index 40481a15a06a..50bbae591692 100644 --- a/src/libs/actions/ReimbursementAccount/index.js +++ b/src/libs/actions/ReimbursementAccount/index.js @@ -1,6 +1,5 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../../ONYXKEYS'; -import validateBankAccount from './validateBankAccount'; import setupWithdrawalAccount from './setupWithdrawalAccount'; import fetchFreePlanVerifiedBankAccount from './fetchFreePlanVerifiedBankAccount'; import resetFreePlanBankAccount from './resetFreePlanBankAccount'; @@ -57,7 +56,6 @@ export { setupWithdrawalAccount, fetchFreePlanVerifiedBankAccount, resetFreePlanBankAccount, - validateBankAccount, setBankAccountSubStep, hideBankAccountErrors, setWorkspaceIDForReimbursementAccount, diff --git a/src/libs/actions/ReimbursementAccount/validateBankAccount.js b/src/libs/actions/ReimbursementAccount/validateBankAccount.js deleted file mode 100644 index ace044899289..000000000000 --- a/src/libs/actions/ReimbursementAccount/validateBankAccount.js +++ /dev/null @@ -1,54 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '../../../ONYXKEYS'; -import * as DeprecatedAPI from '../../deprecatedAPI'; -import BankAccount from '../../models/BankAccount'; -import CONST from '../../../CONST'; -import * as Localize from '../../Localize'; -import * as errors from './errors'; - -/** - * @param {Number} bankAccountID - * @param {String} validateCode - */ -function validateBankAccount(bankAccountID, validateCode) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: true}); - - DeprecatedAPI.BankAccount_Validate({bankAccountID, validateCode}) - .then((response) => { - if (response.jsonCode === 200) { - Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, null); - DeprecatedAPI.User_IsUsingExpensifyCard() - .then(({isUsingExpensifyCard}) => { - const reimbursementAccount = { - loading: false, - error: '', - achData: {state: BankAccount.STATE.OPEN}, - }; - - reimbursementAccount.achData.currentStep = CONST.BANK_ACCOUNT.STEP.ENABLE; - Onyx.merge(ONYXKEYS.USER, {isUsingExpensifyCard}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, reimbursementAccount); - }); - return; - } - - // User has input the validate code incorrectly many times so we will return early in this case and not let them enter the amounts again. - if (response.message === CONST.BANK_ACCOUNT.ERROR.MAX_VALIDATION_ATTEMPTS_REACHED) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false, maxAttemptsReached: true}); - return; - } - - // If the validation amounts entered were incorrect, show specific error - if (response.message === CONST.BANK_ACCOUNT.ERROR.INCORRECT_VALIDATION_AMOUNTS) { - errors.showBankAccountErrorModal(Localize.translateLocal('bankAccount.error.validationAmounts')); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); - return; - } - - // We are generically showing any other backend errors that might pop up in the validate step - errors.showBankAccountErrorModal(response.message); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); - }); -} - -export default validateBankAccount; diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index 26a25ff2c9e7..ee971b76b0ba 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -412,12 +412,6 @@ function Policy_Employees_Merge(parameters) { return Network.post(commandName, {...parameters, returnPersonalDetails: true}); } -function BankAccount_Validate(parameters) { - const commandName = 'ValidateBankAccount'; - requireParameters(['bankAccountID', 'validateCode'], parameters, commandName); - return Network.post(commandName, parameters, CONST.NETWORK.METHOD.POST); -} - /** * @param {*} parameters * @returns {Promise} From d46606ed1a61c1845faa5da1fab89a295d734204 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 6 Sep 2022 15:20:48 +0100 Subject: [PATCH 080/155] create new api method --- src/CONST.js | 1 + src/libs/actions/BankAccounts.js | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/CONST.js b/src/CONST.js index a575a51cf1c6..867b719bdb5d 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -97,6 +97,7 @@ const CONST = { STATE: { VERIFYING: 'VERIFYING', PENDING: 'PENDING', + OPEN: 'OPEN', }, MAX_LENGTH: { SSN: 4, diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index c597429a2614..eb85cb28f20a 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -113,9 +113,43 @@ function deletePaymentBankAccount(bankAccountID) { }); } +/** + * @param {Number} bankAccountID + * @param {String} validateCode + */ +function validateBankAccount(bankAccountID, validateCode) { + API.write('ValidateBankAccountWithTransactions', { + bankAccountID, + validateCode, + }, { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + errors: null, + achData: { + state: CONST.BANK_ACCOUNT.STATE.OPEN, + currentStep: CONST.BANK_ACCOUNT.STEP.ENABLE, + }, + }, + }], + failureData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + achData: { + state: CONST.BANK_ACCOUNT.STATE.VERIFYING, + currentStep: CONST.BANK_ACCOUNT.STEP.VALIDATION, + }, + }, + }], + }); +} + export { addPersonalBankAccount, deletePaymentBankAccount, clearPersonalBankAccount, clearPlaid, + validateBankAccount, }; From 513ac116c03b584acc74e33622702c396fe1aa8d Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 6 Sep 2022 15:35:22 +0100 Subject: [PATCH 081/155] fix lint --- src/libs/deprecatedAPI.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index ee971b76b0ba..1604ccb27765 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -597,7 +597,6 @@ function GetStatementPDF(parameters) { export { BankAccount_SetupWithdrawal, - BankAccount_Validate, ChangePassword, CreateChatReport, CreateLogin, From 85b2f50d5230e6685ea81835cb484551cf58c47b Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 6 Sep 2022 15:52:21 +0100 Subject: [PATCH 082/155] fix lint --- src/pages/ReimbursementAccount/ValidationStep.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js index 4a827780018c..e78aafee36d8 100644 --- a/src/pages/ReimbursementAccount/ValidationStep.js +++ b/src/pages/ReimbursementAccount/ValidationStep.js @@ -27,6 +27,7 @@ import Section from '../../components/Section'; import CONST from '../../CONST'; import Button from '../../components/Button'; import MenuItem from '../../components/MenuItem'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; const propTypes = { ...withLocalizePropTypes, @@ -175,6 +176,10 @@ class ValidationStep extends React.Component { shouldShowBackButton shouldShowStepCounter={!isVerifying} /> + {/* */} {maxAttemptsReached && ( From 2857702c458d9f660ccdba5a7cb8229d91b55c4f Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 6 Sep 2022 15:53:17 +0100 Subject: [PATCH 083/155] rm unused component --- src/pages/ReimbursementAccount/ValidationStep.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js index e78aafee36d8..4a827780018c 100644 --- a/src/pages/ReimbursementAccount/ValidationStep.js +++ b/src/pages/ReimbursementAccount/ValidationStep.js @@ -27,7 +27,6 @@ import Section from '../../components/Section'; import CONST from '../../CONST'; import Button from '../../components/Button'; import MenuItem from '../../components/MenuItem'; -import OfflineWithFeedback from '../../components/OfflineWithFeedback'; const propTypes = { ...withLocalizePropTypes, @@ -176,10 +175,6 @@ class ValidationStep extends React.Component { shouldShowBackButton shouldShowStepCounter={!isVerifying} /> - {/* */} {maxAttemptsReached && ( From f6b88c55124202c9b0f50d275866c1cb46ebd15e Mon Sep 17 00:00:00 2001 From: Varsha Date: Wed, 7 Sep 2022 00:02:34 +0530 Subject: [PATCH 084/155] center loading indicator step1 --- src/pages/ReimbursementAccount/BankAccountStep.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index 88525a81a211..a38a8b915403 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -275,7 +275,7 @@ class BankAccountStep extends React.Component { )} {subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID && ( - + { From d58f41bcfcbc0c906b27dc2831b6b9a3e23970d9 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 7 Sep 2022 13:16:55 +0100 Subject: [PATCH 085/155] add proptypes, loading state, change to pattern c --- src/libs/actions/BankAccounts.js | 18 +++++++++--------- .../ReimbursementAccountForm.js | 2 +- .../reimbursementAccountPropTypes.js | 6 ++++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index eb85cb28f20a..eb95a22e1d7e 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -126,21 +126,21 @@ function validateBankAccount(bankAccountID, validateCode) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, value: { - errors: null, - achData: { - state: CONST.BANK_ACCOUNT.STATE.OPEN, - currentStep: CONST.BANK_ACCOUNT.STEP.ENABLE, - }, + isLoading: true, + }, + }], + successData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: false, }, }], failureData: [{ onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, value: { - achData: { - state: CONST.BANK_ACCOUNT.STATE.VERIFYING, - currentStep: CONST.BANK_ACCOUNT.STEP.VALIDATION, - }, + isLoading: false, }, }], }); diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js index 4969ccd77c45..25179e16e8c2 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js @@ -60,7 +60,7 @@ class ReimbursementAccountForm extends React.Component { }} message={this.props.reimbursementAccount.error} isMessageHtml={this.props.reimbursementAccount.isErrorHtml} - isLoading={this.props.reimbursementAccount.loading} + isLoading={this.props.reimbursementAccount.loading || this.props.reimbursementAccount.isLoading} /> ); diff --git a/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js b/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js index 2f9595d8baf0..79e7dff36a3d 100644 --- a/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js +++ b/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js @@ -31,5 +31,11 @@ export default PropTypes.shape({ errors: PropTypes.objectOf(PropTypes.oneOfType([ PropTypes.bool, PropTypes.arrayOf(PropTypes.objectOf(PropTypes.bool)), + + /** + * Errors from api calls on the specific user + * {: 'error message', : 'error message 2'} + */ + PropTypes.string, ])), }); From 55c9fdd4e7a87126466bdee9902066e48ce943f9 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 7 Sep 2022 15:29:09 +0100 Subject: [PATCH 086/155] display errors --- src/pages/ReimbursementAccount/ReimbursementAccountForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js index 25179e16e8c2..f7e29bdd2ced 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js @@ -58,7 +58,7 @@ class ReimbursementAccountForm extends React.Component { onFixTheErrorsLinkPressed={() => { this.form.scrollTo({y: 0, animated: true}); }} - message={this.props.reimbursementAccount.error} + message={this.props.reimbursementAccount.error || _.values(this.props.reimbursementAccount.errors)[0]} isMessageHtml={this.props.reimbursementAccount.isErrorHtml} isLoading={this.props.reimbursementAccount.loading || this.props.reimbursementAccount.isLoading} /> From dd75b944a9c0de00e1aa25ae600364ffc16ebfc4 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 7 Sep 2022 15:30:20 +0100 Subject: [PATCH 087/155] rm error on resubmit --- src/libs/actions/BankAccounts.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index eb95a22e1d7e..38c63a9a7723 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -127,6 +127,7 @@ function validateBankAccount(bankAccountID, validateCode) { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, value: { isLoading: true, + errors: null, }, }], successData: [{ From a7e2ac213c721829dc2f110728dc553fb7aa259b Mon Sep 17 00:00:00 2001 From: Jack Nam Date: Wed, 7 Sep 2022 17:13:27 +0100 Subject: [PATCH 088/155] Update Policy.js --- src/libs/actions/Policy.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 64f190168720..2694844c6917 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -887,15 +887,30 @@ function createWorkspace() { key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, value: {pendingAction: null}, }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, + value: {pendingAction: null}, + }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, value: {pendingAction: null}, }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, + value: {pendingAction: null}, + }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, value: {pendingAction: null}, + } + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: {pendingAction: null}, }], failureData: [{ onyxMethod: CONST.ONYX.METHOD.SET, @@ -913,7 +928,7 @@ function createWorkspace() { value: null, }, { - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: CONST.ONYX.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, value: null, }, @@ -923,7 +938,7 @@ function createWorkspace() { value: null, }, { - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: CONST.ONYX.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, value: null, }, @@ -933,7 +948,7 @@ function createWorkspace() { value: null, }, { - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: CONST.ONYX.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, value: null, }], From 9985634bfba9416d8bd63a1a4bf731d306d37061 Mon Sep 17 00:00:00 2001 From: Jack Nam Date: Wed, 7 Sep 2022 17:24:25 +0100 Subject: [PATCH 089/155] Update Policy.js --- src/libs/actions/Policy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 2694844c6917..6d4b2cc26b59 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -906,7 +906,7 @@ function createWorkspace() { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, value: {pendingAction: null}, - } + }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, From 1e5a1ecacb2c361dc9ec51ba34c08a0954a27a9c Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Wed, 7 Sep 2022 17:54:02 +0100 Subject: [PATCH 090/155] Use errors object in Onyx key --- src/CONST.js | 2 +- src/libs/actions/Session/index.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 6e73e7cb5ab8..5440e746ad3a 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -408,7 +408,7 @@ const CONST = { FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis', }, DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, - DEFAULT_ACCOUNT_DATA: {errors: '', success: '', loading: false}, + DEFAULT_ACCOUNT_DATA: {errors: null, success: '', loading: false}, APP_STATE: { ACTIVE: 'active', BACKGROUND: 'background', diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 10d329fc64c9..ec2661a13a1b 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -188,7 +188,7 @@ function createTemporaryLogin(authToken, email) { }) .then((createLoginResponse) => { if (createLoginResponse.jsonCode !== 200) { - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: createLoginResponse.message}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translate('createLoginResponse.message')}}); return createLoginResponse; } @@ -250,7 +250,7 @@ function signIn(password, twoFactorAuthCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, isLoading: false}); return; } - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: Localize.translateLocal(errorMessage), isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}, isLoading: false}); return; } @@ -348,7 +348,7 @@ function setPassword(password, validateCode, accountID) { } // This request can fail if the password is not complex enough - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: response.message}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds]: response.message}}); }) .finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); From a48e38a34b7aeccf7d7265084eea2a66fb0f28e7 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Wed, 7 Sep 2022 17:54:27 +0100 Subject: [PATCH 091/155] Suppress cyclic dependency error temporarily --- src/libs/Authentication.js | 1 + src/libs/DateUtils.js | 1 + src/libs/Middleware/Reauthentication.js | 1 + src/libs/Middleware/index.js | 1 + src/libs/actions/NameValuePair.js | 1 + src/libs/actions/PersonalDetails.js | 2 ++ src/libs/actions/SignInRedirect.js | 1 + src/libs/deprecatedAPI.js | 1 + 8 files changed, 9 insertions(+) diff --git a/src/libs/Authentication.js b/src/libs/Authentication.js index cd32c958eb25..85d1a4a2c573 100644 --- a/src/libs/Authentication.js +++ b/src/libs/Authentication.js @@ -3,6 +3,7 @@ import * as Network from './Network'; import * as NetworkStore from './Network/NetworkStore'; import updateSessionAuthTokens from './actions/Session/updateSessionAuthTokens'; import CONFIG from '../CONFIG'; +// eslint-disable-next-line import/no-cycle import redirectToSignIn from './actions/SignInRedirect'; import CONST from '../CONST'; import Log from './Log'; diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index 0c432888b3b8..13aef6919e59 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -9,6 +9,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; import * as Localize from './Localize'; +// eslint-disable-next-line import/no-cycle import * as PersonalDetails from './actions/PersonalDetails'; import * as CurrentDate from './actions/CurrentDate'; diff --git a/src/libs/Middleware/Reauthentication.js b/src/libs/Middleware/Reauthentication.js index 4f346dda9563..79b76ca19f41 100644 --- a/src/libs/Middleware/Reauthentication.js +++ b/src/libs/Middleware/Reauthentication.js @@ -2,6 +2,7 @@ import lodashGet from 'lodash/get'; import CONST from '../../CONST'; import * as NetworkStore from '../Network/NetworkStore'; import * as MainQueue from '../Network/MainQueue'; +// eslint-disable-next-line import/no-cycle import * as Authentication from '../Authentication'; import * as PersistedRequests from '../actions/PersistedRequests'; import * as Request from '../Request'; diff --git a/src/libs/Middleware/index.js b/src/libs/Middleware/index.js index 4e270b009c1d..62c5d6c1aaf9 100644 --- a/src/libs/Middleware/index.js +++ b/src/libs/Middleware/index.js @@ -1,4 +1,5 @@ import Logging from './Logging'; +// eslint-disable-next-line import/no-cycle import Reauthentication from './Reauthentication'; import RecheckConnection from './RecheckConnection'; import Retry from './Retry'; diff --git a/src/libs/actions/NameValuePair.js b/src/libs/actions/NameValuePair.js index 17563e8a8b27..8363b2b961c3 100644 --- a/src/libs/actions/NameValuePair.js +++ b/src/libs/actions/NameValuePair.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; +// eslint-disable-next-line import/no-cycle import * as DeprecatedAPI from '../deprecatedAPI'; /** diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 55bc10f84cd6..92a39ea9a505 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -6,7 +6,9 @@ import Str from 'expensify-common/lib/str'; import ONYXKEYS from '../../ONYXKEYS'; import CONST from '../../CONST'; import * as API from '../API'; +// eslint-disable-next-line import/no-cycle import * as DeprecatedAPI from '../deprecatedAPI'; +// eslint-disable-next-line import/no-cycle import NameValuePair from './NameValuePair'; import * as LoginUtils from '../LoginUtils'; import * as ReportUtils from '../ReportUtils'; diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 4806242051dd..fc88042a8809 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; +// eslint-disable-next-line import/no-cycle import DateUtils from '../DateUtils'; let currentActiveClients; diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index 26a25ff2c9e7..a1646f6cee23 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -3,6 +3,7 @@ import isViaExpensifyCashNative from './isViaExpensifyCashNative'; import requireParameters from './requireParameters'; import * as Request from './Request'; import * as Network from './Network'; +// eslint-disable-next-line import/no-cycle import * as Middleware from './Middleware'; import CONST from '../CONST'; From 7f9d0ca6ef5795118186e37f88c03f8c5723cd22 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Wed, 7 Sep 2022 18:51:00 +0100 Subject: [PATCH 092/155] Fix lint error by using multilingual error message --- src/libs/actions/SignInRedirect.js | 3 ++- src/libs/actions/User.js | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index fc88042a8809..6741fabfb4c8 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -3,6 +3,7 @@ import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; // eslint-disable-next-line import/no-cycle import DateUtils from '../DateUtils'; +import * as Localize from '../Localize'; let currentActiveClients; Onyx.connect({ @@ -47,7 +48,7 @@ function clearStorageAndRedirect(errorMessage) { // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` if (errorMessage) { - Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}}); } }); } diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index bf67d7d42d29..2ea286a56c98 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -233,8 +233,7 @@ function validateLogin(accountID, validateCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {success}); } } else { - const error = lodashGet(response, 'message', 'Unable to validate login.'); - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: error}}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal('resendValidationForm.validationCodeFailedMessage')}}); } }).finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); From a41a30a15ad8d44087d554b6f92750d37600d66c Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 8 Sep 2022 08:38:34 +0100 Subject: [PATCH 093/155] Fix the active report equality check --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index d9f0c658cfaf..55640f8bfbbc 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -497,7 +497,7 @@ function getOptions(reports, personalDetails, activeReportID, { const shouldFilterReportIfRead = hideReadReports && !ReportUtils.isUnread(report); const shouldFilterReport = shouldFilterReportIfEmpty || shouldFilterReportIfRead; - if (report.reportID !== activeReportID + if (report.reportID.toString() !== activeReportID.toString() && (!report.isPinned || isDefaultRoom) && !hasDraftComment && shouldFilterReport From 6b87f562e4d4879e03ba6b4179b028d400b08e70 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 8 Sep 2022 11:43:18 +0100 Subject: [PATCH 094/155] Fix the active report equality check --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 55640f8bfbbc..483e58ea9668 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -497,7 +497,7 @@ function getOptions(reports, personalDetails, activeReportID, { const shouldFilterReportIfRead = hideReadReports && !ReportUtils.isUnread(report); const shouldFilterReport = shouldFilterReportIfEmpty || shouldFilterReportIfRead; - if (report.reportID.toString() !== activeReportID.toString() + if (report.reportID.toString() !== activeReportID && (!report.isPinned || isDefaultRoom) && !hasDraftComment && shouldFilterReport From 629c4c4a66963ed36752edf4de81b140d73e22f8 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 8 Sep 2022 11:46:53 +0100 Subject: [PATCH 095/155] Add a comment about why we use an error boundary --- tests/unit/LHNOrderTest.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/LHNOrderTest.js b/tests/unit/LHNOrderTest.js index e085d6001289..3a0194f0f480 100644 --- a/tests/unit/LHNOrderTest.js +++ b/tests/unit/LHNOrderTest.js @@ -158,6 +158,11 @@ Onyx.init({ }); function getDefaultRenderedSidebarLinks() { + // An ErrorBoundary needs to be added to the rendering so that any errors that happen while the component + // renders are logged to the console. Without an error boundary, Jest only reports the error like "The above error + // occurred in your component", except, there is no "above error". It's just swallowed up by Jest somewhere. + // With the ErrorBoundary, those errors are caught and logged to the console so you can find exactly which error + // might be causing a rendering issue when developing tests. class ErrorBoundary extends React.Component { // Error boundaries have to implement this method. It's for providing a fallback UI, but // we don't need that for unit testing, so this is basically a no-op. From 55bd67fa48766351c6bb2bbd5633312814f1039e Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 8 Sep 2022 11:47:55 +0100 Subject: [PATCH 096/155] Fix return data type --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 86bcc92bdd36..5c5182a6880e 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -104,7 +104,7 @@ const defaultProps = { * @param {Object} route * @param {Object} route.params * @param {String} route.params.reportID - * @returns {Number} + * @returns {String} */ function getReportID(route) { return route.params.reportID.toString(); From 84b1be42fb202270ef7cc81960cd0a2f1c88d148 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 8 Sep 2022 12:00:00 +0100 Subject: [PATCH 097/155] Remove an unnecessary string cast --- src/pages/home/sidebar/SidebarLinks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index be36fef880e4..1257d29cbdb8 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -180,8 +180,8 @@ class SidebarLinks extends React.Component { {paddingBottom: StyleUtils.getSafeAreaMargins(this.props.insets).marginBottom}, ]} sections={sections} - focusedIndex={_.findIndex(this.orderedReports, ( - option => option.reportID.toString() === this.props.currentlyViewedReportID.toString() + focusedIndex={_.findIndex(optionListItems, ( + option => option.reportID.toString() === this.props.currentlyViewedReportID ))} onSelectRow={(option) => { Navigation.navigate(ROUTES.getReportRoute(option.reportID)); From 5ca3b5ebb7b53c236a0376e3069c2a92ff9ed00f Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 8 Sep 2022 12:06:28 +0100 Subject: [PATCH 098/155] Add JSDocs --- src/pages/home/sidebar/SidebarLinks.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 1257d29cbdb8..462d14e9f3ba 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -95,6 +95,15 @@ class SidebarLinks extends React.Component { this.getRecentReportsOptionListItems = memoizeOne(this.getRecentReportsOptionListItems.bind(this)); } + /** + * @param {String} activeReportID + * @param {String} priorityMode + * @param {Object[]} unorderedReports + * @param {Object} personalDetails + * @param {String[]} betas + * @param {Object} reportActions + * @returns {Object[]} + */ getRecentReportsOptionListItems(activeReportID, priorityMode, unorderedReports, personalDetails, betas, reportActions) { const sidebarOptions = OptionsListUtils.getSidebarOptions( unorderedReports, From 82f80adafc311a725e61d6c1c499e5365a558b5a Mon Sep 17 00:00:00 2001 From: Jack Nam Date: Fri, 9 Sep 2022 09:57:23 +0100 Subject: [PATCH 099/155] update the first entry --- src/libs/actions/Policy.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 6d4b2cc26b59..d5036145090d 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -890,7 +890,11 @@ function createWorkspace() { { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, - value: {pendingAction: null}, + value: { + 0: { + pendingAction: null, + }, + }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, @@ -900,7 +904,11 @@ function createWorkspace() { { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, - value: {pendingAction: null}, + value: { + 0: { + pendingAction: null, + }, + }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, @@ -910,7 +918,11 @@ function createWorkspace() { { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: {pendingAction: null}, + value: { + 0: { + pendingAction: null, + }, + }, }], failureData: [{ onyxMethod: CONST.ONYX.METHOD.SET, From 7b7bad66083773cd05f4b471019f279ae8790b93 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sat, 10 Sep 2022 13:43:32 +0530 Subject: [PATCH 100/155] install storybook webpack 5 --- package-lock.json | 809 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + 2 files changed, 810 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 86d00a5728d9..3544000e2f7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,6 +105,8 @@ "@storybook/addon-essentials": "^6.5.9", "@storybook/addon-react-native-web": "0.0.19--canary.37.cb55428.0", "@storybook/addons": "^6.5.9", + "@storybook/builder-webpack5": "^6.5.10", + "@storybook/manager-webpack5": "^6.5.10", "@storybook/react": "^6.5.9", "@storybook/theming": "^6.5.9", "@svgr/webpack": "^5.5.0", @@ -8407,6 +8409,188 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/@storybook/builder-webpack5": { + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-6.5.10.tgz", + "integrity": "sha512-Hcsm/TzGRXHndgQCftt+pzI7GQJRqAv8A8ie5b3aFcodhJfK0qzZsQD4Y4ZWxXh1I/xe5t74Kl2qUJ40PX+geA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.10", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/preview-web": "6.5.10", + "@storybook/router": "6.5.10", + "@storybook/semver": "^7.3.2", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", + "@types/node": "^14.0.10 || ^16.0.0", + "babel-loader": "^8.0.0", + "babel-plugin-named-exports-order": "^0.0.2", + "browser-assert": "^1.2.1", + "case-sensitive-paths-webpack-plugin": "^2.3.0", + "core-js": "^3.8.2", + "css-loader": "^5.0.1", + "fork-ts-checker-webpack-plugin": "^6.0.4", + "glob": "^7.1.6", + "glob-promise": "^3.4.0", + "html-webpack-plugin": "^5.0.0", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "stable": "^0.1.8", + "style-loader": "^2.0.0", + "terser-webpack-plugin": "^5.0.3", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2", + "webpack": "^5.9.0", + "webpack-dev-middleware": "^4.1.0", + "webpack-hot-middleware": "^2.25.1", + "webpack-virtual-modules": "^0.4.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { + "version": "16.11.58", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.58.tgz", + "integrity": "sha512-uMVxJ111wpHzkx/vshZFb6Qni3BOMnlWLq7q9jrwej7Yw/KvjsEbpxCCxw+hLKxexFMc8YmpG8J9tnEe/rKsIg==", + "dev": true + }, + "node_modules/@storybook/builder-webpack5/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/@storybook/builder-webpack5/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/terser-webpack-plugin": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", + "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.14", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.14.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/webpack-dev-middleware": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", + "integrity": "sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==", + "dev": true, + "dependencies": { + "colorette": "^1.2.2", + "mem": "^8.1.1", + "memfs": "^3.2.2", + "mime-types": "^2.1.30", + "range-parser": "^1.2.1", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= v10.23.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/webpack-virtual-modules": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.4.4.tgz", + "integrity": "sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==", + "dev": true + }, "node_modules/@storybook/channel-postmessage": { "version": "6.5.10", "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.5.10.tgz", @@ -11221,6 +11405,237 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/@storybook/manager-webpack5": { + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/manager-webpack5/-/manager-webpack5-6.5.10.tgz", + "integrity": "sha512-uRo+6e5MiVOtyFVMYIKVqvpDveCjHyzXBfetSYR7rKEZoaDMEnLLiuF7DIH12lzxwmzCJ1gIc4lf5HFiTMNkgw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.10", + "@babel/plugin-transform-template-literals": "^7.12.1", + "@babel/preset-react": "^7.12.10", + "@storybook/addons": "6.5.10", + "@storybook/core-client": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/theming": "6.5.10", + "@storybook/ui": "6.5.10", + "@types/node": "^14.0.10 || ^16.0.0", + "babel-loader": "^8.0.0", + "case-sensitive-paths-webpack-plugin": "^2.3.0", + "chalk": "^4.1.0", + "core-js": "^3.8.2", + "css-loader": "^5.0.1", + "express": "^4.17.1", + "find-up": "^5.0.0", + "fs-extra": "^9.0.1", + "html-webpack-plugin": "^5.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "read-pkg-up": "^7.0.1", + "regenerator-runtime": "^0.13.7", + "resolve-from": "^5.0.0", + "style-loader": "^2.0.0", + "telejson": "^6.0.8", + "terser-webpack-plugin": "^5.0.3", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2", + "webpack": "^5.9.0", + "webpack-dev-middleware": "^4.1.0", + "webpack-virtual-modules": "^0.4.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/@types/node": { + "version": "16.11.58", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.58.tgz", + "integrity": "sha512-uMVxJ111wpHzkx/vshZFb6Qni3BOMnlWLq7q9jrwej7Yw/KvjsEbpxCCxw+hLKxexFMc8YmpG8J9tnEe/rKsIg==", + "dev": true + }, + "node_modules/@storybook/manager-webpack5/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@storybook/manager-webpack5/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/terser-webpack-plugin": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", + "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.14", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.14.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/webpack-dev-middleware": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", + "integrity": "sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==", + "dev": true, + "dependencies": { + "colorette": "^1.2.2", + "mem": "^8.1.1", + "memfs": "^3.2.2", + "mime-types": "^2.1.30", + "range-parser": "^1.2.1", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= v10.23.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@storybook/manager-webpack5/node_modules/webpack-virtual-modules": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.4.4.tgz", + "integrity": "sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==", + "dev": true + }, "node_modules/@storybook/mdx1-csf": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@storybook/mdx1-csf/-/mdx1-csf-0.0.1.tgz", @@ -15062,6 +15477,12 @@ "node": ">= 8.0.0" } }, + "node_modules/babel-plugin-named-exports-order": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-named-exports-order/-/babel-plugin-named-exports-order-0.0.2.tgz", + "integrity": "sha512-OgOYHOLoRK+/mvXU9imKHlG6GkPLYrUCvFXG/CM93R/aNNO8pOOF4aS+S8CCHMDQoNSeiOYEZb/G6RwL95Jktw==", + "dev": true + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.2.tgz", @@ -15778,6 +16199,12 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, + "node_modules/browser-assert": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", + "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", + "dev": true + }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -29241,6 +29668,18 @@ "tmpl": "1.0.5" } }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -29390,6 +29829,31 @@ "node": ">= 0.6" } }, + "node_modules/mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "dev": true, + "dependencies": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/mem?sponsor=1" + } + }, + "node_modules/mem/node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/memfs": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.7.tgz", @@ -32244,6 +32708,15 @@ "node": ">=6" } }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", @@ -44475,7 +44948,7 @@ "@oguzhnatly/react-native-image-manipulator": { "version": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", "integrity": "sha512-PvrSoCq5PS1MA5ZWUpB0khfzH6sM8SI6YiVl4i2SItPr7IeRxiWfI4n45VhBCCElc1z5GhAwTZOBaIzXTX7/og==", - "from": "@oguzhnatly/react-native-image-manipulator@https://github.com/Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050" + "from": "@oguzhnatly/react-native-image-manipulator@github:Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050" }, "@onfido/active-video-capture": { "version": "0.0.1", @@ -47691,6 +48164,134 @@ } } }, + "@storybook/builder-webpack5": { + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-6.5.10.tgz", + "integrity": "sha512-Hcsm/TzGRXHndgQCftt+pzI7GQJRqAv8A8ie5b3aFcodhJfK0qzZsQD4Y4ZWxXh1I/xe5t74Kl2qUJ40PX+geA==", + "dev": true, + "requires": { + "@babel/core": "^7.12.10", + "@storybook/addons": "6.5.10", + "@storybook/api": "6.5.10", + "@storybook/channel-postmessage": "6.5.10", + "@storybook/channels": "6.5.10", + "@storybook/client-api": "6.5.10", + "@storybook/client-logger": "6.5.10", + "@storybook/components": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/core-events": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/preview-web": "6.5.10", + "@storybook/router": "6.5.10", + "@storybook/semver": "^7.3.2", + "@storybook/store": "6.5.10", + "@storybook/theming": "6.5.10", + "@types/node": "^14.0.10 || ^16.0.0", + "babel-loader": "^8.0.0", + "babel-plugin-named-exports-order": "^0.0.2", + "browser-assert": "^1.2.1", + "case-sensitive-paths-webpack-plugin": "^2.3.0", + "core-js": "^3.8.2", + "css-loader": "^5.0.1", + "fork-ts-checker-webpack-plugin": "^6.0.4", + "glob": "^7.1.6", + "glob-promise": "^3.4.0", + "html-webpack-plugin": "^5.0.0", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "stable": "^0.1.8", + "style-loader": "^2.0.0", + "terser-webpack-plugin": "^5.0.3", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2", + "webpack": "^5.9.0", + "webpack-dev-middleware": "^4.1.0", + "webpack-hot-middleware": "^2.25.1", + "webpack-virtual-modules": "^0.4.1" + }, + "dependencies": { + "@types/node": { + "version": "16.11.58", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.58.tgz", + "integrity": "sha512-uMVxJ111wpHzkx/vshZFb6Qni3BOMnlWLq7q9jrwej7Yw/KvjsEbpxCCxw+hLKxexFMc8YmpG8J9tnEe/rKsIg==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "terser-webpack-plugin": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", + "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.14", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.14.1" + } + }, + "webpack-dev-middleware": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", + "integrity": "sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "mem": "^8.1.1", + "memfs": "^3.2.2", + "mime-types": "^2.1.30", + "range-parser": "^1.2.1", + "schema-utils": "^3.0.0" + } + }, + "webpack-virtual-modules": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.4.4.tgz", + "integrity": "sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==", + "dev": true + } + } + }, "@storybook/channel-postmessage": { "version": "6.5.10", "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.5.10.tgz", @@ -49970,6 +50571,167 @@ } } }, + "@storybook/manager-webpack5": { + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@storybook/manager-webpack5/-/manager-webpack5-6.5.10.tgz", + "integrity": "sha512-uRo+6e5MiVOtyFVMYIKVqvpDveCjHyzXBfetSYR7rKEZoaDMEnLLiuF7DIH12lzxwmzCJ1gIc4lf5HFiTMNkgw==", + "dev": true, + "requires": { + "@babel/core": "^7.12.10", + "@babel/plugin-transform-template-literals": "^7.12.1", + "@babel/preset-react": "^7.12.10", + "@storybook/addons": "6.5.10", + "@storybook/core-client": "6.5.10", + "@storybook/core-common": "6.5.10", + "@storybook/node-logger": "6.5.10", + "@storybook/theming": "6.5.10", + "@storybook/ui": "6.5.10", + "@types/node": "^14.0.10 || ^16.0.0", + "babel-loader": "^8.0.0", + "case-sensitive-paths-webpack-plugin": "^2.3.0", + "chalk": "^4.1.0", + "core-js": "^3.8.2", + "css-loader": "^5.0.1", + "express": "^4.17.1", + "find-up": "^5.0.0", + "fs-extra": "^9.0.1", + "html-webpack-plugin": "^5.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "read-pkg-up": "^7.0.1", + "regenerator-runtime": "^0.13.7", + "resolve-from": "^5.0.0", + "style-loader": "^2.0.0", + "telejson": "^6.0.8", + "terser-webpack-plugin": "^5.0.3", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2", + "webpack": "^5.9.0", + "webpack-dev-middleware": "^4.1.0", + "webpack-virtual-modules": "^0.4.1" + }, + "dependencies": { + "@types/node": { + "version": "16.11.58", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.58.tgz", + "integrity": "sha512-uMVxJ111wpHzkx/vshZFb6Qni3BOMnlWLq7q9jrwej7Yw/KvjsEbpxCCxw+hLKxexFMc8YmpG8J9tnEe/rKsIg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "terser-webpack-plugin": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", + "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.14", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.14.1" + } + }, + "webpack-dev-middleware": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz", + "integrity": "sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "mem": "^8.1.1", + "memfs": "^3.2.2", + "mime-types": "^2.1.30", + "range-parser": "^1.2.1", + "schema-utils": "^3.0.0" + } + }, + "webpack-virtual-modules": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.4.4.tgz", + "integrity": "sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==", + "dev": true + } + } + }, "@storybook/mdx1-csf": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@storybook/mdx1-csf/-/mdx1-csf-0.0.1.tgz", @@ -52973,6 +53735,12 @@ "resolve": "^1.13.1" } }, + "babel-plugin-named-exports-order": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-named-exports-order/-/babel-plugin-named-exports-order-0.0.2.tgz", + "integrity": "sha512-OgOYHOLoRK+/mvXU9imKHlG6GkPLYrUCvFXG/CM93R/aNNO8pOOF4aS+S8CCHMDQoNSeiOYEZb/G6RwL95Jktw==", + "dev": true + }, "babel-plugin-polyfill-corejs2": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.2.tgz", @@ -53583,6 +54351,12 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, + "browser-assert": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", + "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", + "dev": true + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -63849,6 +64623,15 @@ "tmpl": "1.0.5" } }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -63963,6 +64746,24 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true }, + "mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, + "dependencies": { + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true + } + } + }, "memfs": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.7.tgz", @@ -66233,6 +67034,12 @@ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", "dev": true }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "dev": true + }, "p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", diff --git a/package.json b/package.json index 84b08c8b96fe..85be9649caed 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,8 @@ "@storybook/addon-essentials": "^6.5.9", "@storybook/addon-react-native-web": "0.0.19--canary.37.cb55428.0", "@storybook/addons": "^6.5.9", + "@storybook/builder-webpack5": "^6.5.10", + "@storybook/manager-webpack5": "^6.5.10", "@storybook/react": "^6.5.9", "@storybook/theming": "^6.5.9", "@svgr/webpack": "^5.5.0", From 9c7b4126813bd70e51973ee7f1cfae2e49a7bd96 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sat, 10 Sep 2022 13:44:04 +0530 Subject: [PATCH 101/155] configure storybook to use webpack 5 --- .storybook/main.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.storybook/main.js b/.storybook/main.js index ad5effb2dbf6..456725e35191 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -12,4 +12,7 @@ module.exports = { './public', '../assets/css', ], + core: { + builder: "webpack5" + } }; From deb697cc662249269941d7bd61d6f4ff39f916c5 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sat, 10 Sep 2022 14:00:18 +0530 Subject: [PATCH 102/155] fix lint errors --- .storybook/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.storybook/main.js b/.storybook/main.js index 456725e35191..9df86b68be3f 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -13,6 +13,6 @@ module.exports = { '../assets/css', ], core: { - builder: "webpack5" - } + builder: 'webpack5' + }, }; From f7cfd8581c46217cb832a3e189c7b462504f9823 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sat, 10 Sep 2022 14:04:38 +0530 Subject: [PATCH 103/155] fix lint errors --- .storybook/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.storybook/main.js b/.storybook/main.js index 9df86b68be3f..d34252604b41 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -13,6 +13,6 @@ module.exports = { '../assets/css', ], core: { - builder: 'webpack5' + builder: 'webpack5', }, }; From e8aef52a5f18814bb7729e071df638478426077a Mon Sep 17 00:00:00 2001 From: luan <103875612+b1tjoy@users.noreply.github.com> Date: Mon, 12 Sep 2022 19:26:38 +0800 Subject: [PATCH 104/155] format link as markdown link --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 986804658b17..7c5dbadad342 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", "dotenv": "^8.2.0", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#e0ec0b9abca4d7b417ba0b016f8725856cfeeece", + "expensify-common": "git+https://github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", "fbjs": "^3.0.2", "file-loader": "^6.0.0", "html-entities": "^1.3.1", @@ -22142,8 +22142,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#e0ec0b9abca4d7b417ba0b016f8725856cfeeece", - "integrity": "sha512-RXp32klPtDw6UK57f3Ku0Gw1wOq9STfMRaF8gH8XZ7pa0kTOA53itpvGfHr81Xuk960oIowt4SIo3zbzG+GK2w==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", + "integrity": "sha512-FiHBWAGkJ9UC+cpdB2bDY+2lAOklvFAIDzga9c2z3iTzEvhYYf2uCeTIjDFYIJviEibBkiIUELjDr9wwcepmNA==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -58490,9 +58490,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#e0ec0b9abca4d7b417ba0b016f8725856cfeeece", - "integrity": "sha512-RXp32klPtDw6UK57f3Ku0Gw1wOq9STfMRaF8gH8XZ7pa0kTOA53itpvGfHr81Xuk960oIowt4SIo3zbzG+GK2w==", - "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#e0ec0b9abca4d7b417ba0b016f8725856cfeeece", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", + "integrity": "sha512-FiHBWAGkJ9UC+cpdB2bDY+2lAOklvFAIDzga9c2z3iTzEvhYYf2uCeTIjDFYIJviEibBkiIUELjDr9wwcepmNA==", + "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", diff --git a/package.json b/package.json index 79801c1101cf..2b8381368930 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", "dotenv": "^8.2.0", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#e0ec0b9abca4d7b417ba0b016f8725856cfeeece", + "expensify-common": "git+https://github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", "fbjs": "^3.0.2", "file-loader": "^6.0.0", "html-entities": "^1.3.1", From 4579ab205dfc56d60b6b323636b61808907269b1 Mon Sep 17 00:00:00 2001 From: Mohammad Luthfi Fathur Rahman Date: Mon, 12 Sep 2022 22:06:34 +0700 Subject: [PATCH 105/155] adding screen listener to participants page --- src/libs/Navigation/AppNavigator/AuthScreens.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index a363dd84ba95..11767b963a8a 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -268,6 +268,7 @@ class AuthScreens extends React.Component { name="Participants" options={modalScreenOptions} component={ModalStackNavigators.ReportParticipantsModalStackNavigator} + listeners={modalScreenListeners} /> Date: Tue, 13 Sep 2022 08:38:42 +0700 Subject: [PATCH 106/155] adding screen listener to IOU detail page --- src/libs/Navigation/AppNavigator/AuthScreens.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 11767b963a8a..a2f9d082e3c3 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -292,6 +292,7 @@ class AuthScreens extends React.Component { name="IOU_Details" options={modalScreenOptions} component={ModalStackNavigators.IOUDetailsModalStackNavigator} + listeners={modalScreenListeners} /> Date: Tue, 13 Sep 2022 13:52:07 +0200 Subject: [PATCH 107/155] Update package-lock with image manipulator fork --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 94a10ec734cc..51d098c57b64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44475,7 +44475,7 @@ "@oguzhnatly/react-native-image-manipulator": { "version": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", "integrity": "sha512-PvrSoCq5PS1MA5ZWUpB0khfzH6sM8SI6YiVl4i2SItPr7IeRxiWfI4n45VhBCCElc1z5GhAwTZOBaIzXTX7/og==", - "from": "@oguzhnatly/react-native-image-manipulator@https://github.com/Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050" + "from": "@oguzhnatly/react-native-image-manipulator@github:Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050" }, "@onfido/active-video-capture": { "version": "0.0.1", From b199711695ef50814fc1ed80b68204b33ad7a410 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 9 Sep 2022 11:20:36 +0100 Subject: [PATCH 108/155] add new test files and mocks Add mock Fix UrbanAirship mock Get main drawer to render rename to navigation test Just render the sidebar links for now Fix tests and make ReportScreen render Add report action and verify the action renders Test for report-action-item visibility Mock disconnect for Pusher add way to set initial url for testing; Test for drawer status Test for the unread indicator Mock local notification and make sure only one notification appears upgrade test library dependencies and use act to get rid of weird warning undo test version bumps Fix tests Add comment Test behavior of unread indicator in report and sidebar Rename back to unread indicators Test new messages badge indicator refactor and improve Report screen visibility check Update mock for AppState Clear new line indicator when report page is in view and we return from background remove extra logs add JSDocs Check the LHN lastMessageText Fix ReportTest Check for pendingAction when deleting comment Fix deleted comments not showing strikethrough while offline Stop allowing deletion or editing of pendingAction add comments as users cannot be updated correctly via sequenceNumber Rollback some changes Use sequenceNumber for handledReportActions for now Remove log add doc --- .../@react-native-firebase/crashlytics.js | 1 + __mocks__/pusher-js/react-native.js | 8 +- __mocks__/react-native-safe-area-context.js | 44 ++ __mocks__/react-native.js | 66 +++ __mocks__/urbanairship-react-native.js | 12 +- jest/setup.js | 140 ++++++- package-lock.json | 2 +- package.json | 1 + src/components/Checkbox.js | 3 +- src/components/DisplayNames/index.native.js | 2 +- src/components/OptionRow.js | 9 +- src/components/OptionsList/BaseOptionsList.js | 1 + src/components/ReportWelcomeText.js | 3 +- src/components/Text.js | 4 + src/components/UnreadActionIndicator.js | 2 +- src/libs/ReportActionsUtils.js | 71 +++- src/libs/ReportUtils.js | 2 + src/libs/actions/Report.js | 69 ++- src/libs/actions/ReportActions.js | 101 ----- src/pages/home/HeaderView.js | 18 +- src/pages/home/ReportHeaderViewBackButton.js | 30 ++ src/pages/home/ReportScreen.js | 6 + .../FloatingMessageCounterContainer/index.js | 2 +- .../report/FloatingMessageCounter/index.js | 2 +- src/pages/home/report/ReportActionCompose.js | 3 +- src/pages/home/report/ReportActionItem.js | 4 +- .../home/report/ReportActionItemCreated.js | 13 +- .../home/report/ReportActionItemMessage.js | 45 +- src/pages/home/report/ReportActionsList.js | 12 +- src/pages/home/report/ReportActionsView.js | 11 +- src/pages/home/sidebar/SidebarLinks.js | 2 +- src/pages/signin/LoginForm.js | 2 +- src/setup/platformSetup/index.native.js | 2 +- src/styles/styles.js | 4 - tests/actions/ReportTest.js | 18 +- tests/ui/UnreadIndicatorsTest.js | 395 ++++++++++++++++++ tests/unit/LHNOrderTest.js | 7 - tests/unit/loginTest.js | 12 - tests/utils/TestHelper.js | 78 +++- 39 files changed, 951 insertions(+), 256 deletions(-) create mode 100644 __mocks__/@react-native-firebase/crashlytics.js create mode 100644 __mocks__/react-native-safe-area-context.js create mode 100644 __mocks__/react-native.js create mode 100644 src/pages/home/ReportHeaderViewBackButton.js create mode 100644 tests/ui/UnreadIndicatorsTest.js diff --git a/__mocks__/@react-native-firebase/crashlytics.js b/__mocks__/@react-native-firebase/crashlytics.js new file mode 100644 index 000000000000..ff8b4c56321a --- /dev/null +++ b/__mocks__/@react-native-firebase/crashlytics.js @@ -0,0 +1 @@ +export default {}; diff --git a/__mocks__/pusher-js/react-native.js b/__mocks__/pusher-js/react-native.js index 0cb6afb4bb1d..1edec34ffb14 100644 --- a/__mocks__/pusher-js/react-native.js +++ b/__mocks__/pusher-js/react-native.js @@ -1,3 +1,9 @@ import {PusherMock} from 'pusher-js-mock'; -export default PusherMock; +class PusherMockWithDisconnect extends PusherMock { + disconnect() { + return jest.fn(); + } +} + +export default PusherMockWithDisconnect; diff --git a/__mocks__/react-native-safe-area-context.js b/__mocks__/react-native-safe-area-context.js new file mode 100644 index 000000000000..82152de8de11 --- /dev/null +++ b/__mocks__/react-native-safe-area-context.js @@ -0,0 +1,44 @@ +import React, {forwardRef} from 'react'; +import {View} from 'react-native'; + +const insets = { + top: 0, right: 0, bottom: 0, left: 0, +}; + +function withSafeAreaInsets(WrappedComponent) { + const WithSafeAreaInsets = props => ( + + ); + return forwardRef((props, ref) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + )); +} + +const SafeAreaView = View; +const SafeAreaProvider = props => props.children; +const SafeAreaConsumer = props => props.children(insets); +const SafeAreaInsetsContext = { + Consumer: SafeAreaConsumer, +}; + +const useSafeAreaFrame = jest.fn(() => ({ + x: 0, y: 0, width: 390, height: 844, +})); +const useSafeAreaInsets = jest.fn(() => insets); + +export { + SafeAreaProvider, + SafeAreaConsumer, + SafeAreaInsetsContext, + withSafeAreaInsets, + SafeAreaView, + useSafeAreaFrame, + useSafeAreaInsets, +}; diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js new file mode 100644 index 000000000000..3e407252dc78 --- /dev/null +++ b/__mocks__/react-native.js @@ -0,0 +1,66 @@ +/* eslint-disable arrow-body-style */ +// eslint-disable-next-line no-restricted-imports +import * as ReactNative from 'react-native'; +import _ from 'underscore'; + +jest.doMock('react-native', () => { + let url = 'https://new.expensify.com/'; + const getInitialURL = () => { + return Promise.resolve(url); + }; + + let appState = 'active'; + let count = 0; + const changeListeners = {}; + return Object.setPrototypeOf( + { + NativeModules: { + ...ReactNative.NativeModules, + BootSplash: { + getVisibilityStatus: jest.fn(), + hide: jest.fn(), + }, + StartupTimer: {stop: jest.fn()}, + }, + Linking: { + ...ReactNative.Linking, + getInitialURL, + setInitialURL(newUrl) { + url = newUrl; + }, + }, + AppState: { + ...ReactNative.AppState, + get currentState() { + return appState; + }, + emitCurrentTestState(state) { + appState = state; + _.each(changeListeners, listener => listener(appState)); + }, + addEventListener(type, listener) { + if (type === 'change') { + const originalCount = count; + changeListeners[originalCount] = listener; + ++count; + return { + remove: () => { + delete changeListeners[originalCount]; + }, + }; + } + + return ReactNative.AppState.addEventListener(type, listener); + }, + }, + Dimensions: { + ...ReactNative.Dimensions, + addEventListener: jest.fn(), + get: () => ({ + width: 300, height: 700, scale: 1, fontScale: 1, + }), + }, + }, + ReactNative, + ); +}); diff --git a/__mocks__/urbanairship-react-native.js b/__mocks__/urbanairship-react-native.js index c4e8a3939325..efbb5d87f92b 100644 --- a/__mocks__/urbanairship-react-native.js +++ b/__mocks__/urbanairship-react-native.js @@ -3,10 +3,20 @@ const EventType = { PushReceived: 'pushReceived', }; -export default { +const UrbanAirship = { setUserNotificationsEnabled: jest.fn(), + clearNotifications: jest.fn(), + addListener: jest.fn(), + getNamedUser: jest.fn(), + enableUserPushNotifications: () => Promise.resolve(false), + setNamedUser: jest.fn(), + removeAllListeners: jest.fn(), + setBadgeNumber: jest.fn(), }; +export default UrbanAirship; + export { EventType, + UrbanAirship, }; diff --git a/jest/setup.js b/jest/setup.js index c380ebb56669..627d43466b74 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -1,15 +1,143 @@ +import fs from 'fs'; +import path from 'path'; import 'react-native-gesture-handler/jestSetup'; +import _ from 'underscore'; require('react-native-reanimated/lib/reanimated2/jestUtils').setUpTests(); // Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); -// Set up manual mocks for methods used in the actions so our test does not fail. -jest.mock('../src/libs/Notification/PushNotification', () => ({ - // There is no need for a jest.fn() since we don't need to make assertions against it. - register: () => {}, - deregister: () => {}, +jest.mock('react-native-blob-util', () => ({})); + +jest.mock('react-native-reanimated', () => { + const Reanimated = require('react-native-reanimated/mock'); + + // The mock for `call` immediately calls the callback which is incorrect + // So we override it with a no-op + Reanimated.default.call = () => {}; + return Reanimated; +}); + +// uses and we need to mock the imported crashlytics module +// due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 +jest.mock('@react-native-firebase/crashlytics', () => () => ({ + log: jest.fn(), + recordError: jest.fn(), })); -jest.mock('react-native-blob-util', () => ({})); +jest.mock('../src/libs/BootSplash', () => ({ + hide: jest.fn(), + getVisibilityStatus: jest.fn().mockResolvedValue('hidden'), +})); + +jest.mock('../src/libs/Notification/LocalNotification', () => ({ + showCommentNotification: jest.fn(), +})); + +/** + * @param {String} imagePath + */ +function mockImages(imagePath) { + const imageFilenames = fs.readdirSync(path.resolve(__dirname, `../assets/${imagePath}/`)); + // eslint-disable-next-line rulesdir/prefer-early-return + _.each(imageFilenames, (fileName) => { + if (/\.svg/.test(fileName)) { + jest.mock(`../assets/${imagePath}/${fileName}`, () => () => ''); + } + }); +} + +// Mock all images so that Icons and other assets cannot break tests +mockImages('images'); +mockImages('images/avatars'); +mockImages('images/bankicons'); +mockImages('images/product-illustrations'); +jest.mock('../src/components/Icon/Expensicons', () => ({ + ActiveRoomAvatar: () => '', + AdminRoomAvatar: () => '', + Android: () => '', + AnnounceRoomAvatar: () => '', + Apple: () => '', + ArrowRight: () => '', + BackArrow: () => '', + Bank: () => '', + Bill: () => '', + Bolt: () => '', + Briefcase: () => '', + Bug: () => '', + Building: () => '', + Camera: () => '', + Cash: () => '', + ChatBubble: () => '', + Checkmark: () => '', + CircleHourglass: () => '', + Clipboard: () => '', + Close: () => '', + ClosedSign: () => '', + Collapse: () => '', + Concierge: () => '', + Connect: () => '', + CreditCard: () => '', + DeletedRoomAvatar: () => '', + DomainRoomAvatar: () => '', + DotIndicator: () => '', + DownArrow: () => '', + Download: () => '', + Emoji: () => '', + Exclamation: () => '', + Exit: () => '', + ExpensifyCard: () => '', + Expand: () => '', + Eye: () => '', + EyeDisabled: () => '', + FallbackAvatar: () => '', + FallbackWorkspaceAvatar: () => '', + Gallery: () => '', + Gear: () => '', + Hashtag: () => '', + ImageCropMask: () => '', + Info: () => '', + Invoice: () => '', + Key: () => '', + Keyboard: () => '', + Link: () => '', + LinkCopy: () => '', + Lock: () => '', + Luggage: () => '', + MagnifyingGlass: () => '', + Mail: () => '', + MoneyBag: () => '', + MoneyCircle: () => '', + Monitor: () => '', + NewWindow: () => '', + NewWorkspace: () => '', + Offline: () => '', + OfflineCloud: () => '', + Paperclip: () => '', + PayPal: () => '', + Paycheck: () => '', + Pencil: () => '', + Phone: () => '', + Pin: () => '', + PinCircle: () => '', + Plus: () => '', + Printer: () => '', + Profile: () => '', + QuestionMark: () => '', + Receipt: () => '', + ReceiptSearch: () => '', + Rotate: () => '', + RotateLeft: () => '', + Send: () => '', + Sync: () => '', + ThreeDots: () => '', + Transfer: () => '', + Trashcan: () => '', + UpArrow: () => '', + Upload: () => '', + Users: () => '', + Wallet: () => '', + Workspace: () => '', + Zoom: () => '', +})); diff --git a/package-lock.json b/package-lock.json index b13378e833d2..1662a0358d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44475,7 +44475,7 @@ "@oguzhnatly/react-native-image-manipulator": { "version": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", "integrity": "sha512-PvrSoCq5PS1MA5ZWUpB0khfzH6sM8SI6YiVl4i2SItPr7IeRxiWfI4n45VhBCCElc1z5GhAwTZOBaIzXTX7/og==", - "from": "@oguzhnatly/react-native-image-manipulator@https://github.com/Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050" + "from": "@oguzhnatly/react-native-image-manipulator@github:Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050" }, "@onfido/active-video-capture": { "version": "0.0.1", diff --git a/package.json b/package.json index f9a67bfa8cee..272255fac56d 100644 --- a/package.json +++ b/package.json @@ -201,6 +201,7 @@ "/node_modules/" ], "testMatch": [ + "**/tests/ui/**/*.[jt]s?(x)", "**/tests/unit/**/*.[jt]s?(x)", "**/tests/actions/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)" diff --git a/src/components/Checkbox.js b/src/components/Checkbox.js index ab5dde69f505..cc94b2e842a1 100644 --- a/src/components/Checkbox.js +++ b/src/components/Checkbox.js @@ -8,7 +8,7 @@ import * as Expensicons from './Icon/Expensicons'; const propTypes = { /** Whether checkbox is checked */ - isChecked: PropTypes.bool.isRequired, + isChecked: PropTypes.bool, /** A function that is called when the box/label is pressed */ onPress: PropTypes.func.isRequired, @@ -33,6 +33,7 @@ const propTypes = { }; const defaultProps = { + isChecked: false, hasError: false, disabled: false, style: [], diff --git a/src/components/DisplayNames/index.native.js b/src/components/DisplayNames/index.native.js index 6ccb53888fd6..91ed0406a0d7 100644 --- a/src/components/DisplayNames/index.native.js +++ b/src/components/DisplayNames/index.native.js @@ -4,7 +4,7 @@ import Text from '../Text'; // As we don't have to show tooltips of the Native platform so we simply render the full display names list. const DisplayNames = props => ( - + {props.fullTitle} ); diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index cadae24e801e..40471941aba4 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -128,7 +128,10 @@ const OptionRow = (props) => { touchableRef = el} onPress={(e) => { - e.preventDefault(); + if (e) { + e.preventDefault(); + } + props.onSelectRow(props.option, touchableRef); }} disabled={props.isDisabled} @@ -145,7 +148,7 @@ const OptionRow = (props) => { props.isDisabled && styles.cursorDisabled, ]} > - + { } { /> {props.option.alternateText ? ( diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index 17ce25e26551..e5524c49dab5 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -223,6 +223,7 @@ class BaseOptionsList extends Component { ) : null} { diff --git a/src/components/Text.js b/src/components/Text.js index 84a78d3e37c5..47a037ae735f 100644 --- a/src/components/Text.js +++ b/src/components/Text.js @@ -26,6 +26,9 @@ const propTypes = { /** Any additional styles to apply */ // eslint-disable-next-line react/forbid-prop-types style: PropTypes.any, + + /** Optional testID for testing library */ + testID: PropTypes.string, }; const defaultProps = { color: themeColors.text, @@ -34,6 +37,7 @@ const defaultProps = { textAlign: 'left', children: null, style: {}, + testID: undefined, }; const Text = React.forwardRef(({ diff --git a/src/components/UnreadActionIndicator.js b/src/components/UnreadActionIndicator.js index 389413e4f38e..32a48b650fe3 100755 --- a/src/components/UnreadActionIndicator.js +++ b/src/components/UnreadActionIndicator.js @@ -5,7 +5,7 @@ import Text from './Text'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; const UnreadActionIndicator = props => ( - + {props.translate('common.new')} diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 29f22e0f68c0..a86027cb35ef 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -1,6 +1,25 @@ import lodashGet from 'lodash/get'; import _ from 'underscore'; +import lodashMerge from 'lodash/merge'; +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; +import Onyx from 'react-native-onyx'; +import * as CollectionUtils from './CollectionUtils'; import CONST from '../CONST'; +import ONYXKEYS from '../ONYXKEYS'; +import * as ReportUtils from './ReportUtils'; + +const allReportActions = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + callback: (actions, key) => { + if (!key || !actions) { + return; + } + + const reportID = CollectionUtils.extractCollectionItemID(key); + allReportActions[reportID] = actions; + }, +}); /** * @param {Object} reportAction @@ -22,7 +41,9 @@ function getSortedReportActions(reportActions) { return _.chain(reportActions) .sortBy('sequenceNumber') .filter(action => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU - || (action.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !isDeletedAction(action)) + + // All comment actions are shown unless they are deleted and non-pending + || (action.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && (!isDeletedAction(action) || !_.isEmpty(action.pendingAction))) || action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED || action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) .map((item, index) => ({action: item, index})) @@ -76,7 +97,55 @@ function isConsecutiveActionMadeByPreviousActor(reportActions, actionIndex) { return currentAction.action.actorEmail === previousAction.action.actorEmail; } +/** + * Get the message text for the last action that was not deleted + * @param {Number} reportID + * @param {Object} [actionsToMerge] + * @return {String} + */ +function getLastVisibleMessageText(reportID, actionsToMerge = {}) { + const parser = new ExpensiMark(); + const actions = _.toArray(lodashMerge({}, allReportActions[reportID], actionsToMerge)); + const sortedActions = _.sortBy(actions, 'sequenceNumber'); + const lastMessageIndex = _.findLastIndex(sortedActions, action => ( + !isDeletedAction(action) + )); + const htmlText = lodashGet(actions, [lastMessageIndex, 'message', 0, 'html'], ''); + const messageText = parser.htmlToText(htmlText); + return ReportUtils.formatReportLastMessageText(messageText); +} + +/** + * @param {Number} reportID + * @param {Object} [actionsToMerge] + * @param {Number} deletedSequenceNumber + * @param {Number} lastReadSequenceNumber + * @return {String} + */ +function getNewLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = {}, deletedSequenceNumber, lastReadSequenceNumber) { + // If the action we are deleting is unread then just return the current last read sequence number + if (deletedSequenceNumber > lastReadSequenceNumber) { + return lastReadSequenceNumber; + } + + // Otherwise, we must find the first previous index of an action that is not deleted and less than the lastReadSequenceNumber + const actions = _.toArray(lodashMerge({}, allReportActions[reportID], actionsToMerge)); + const sortedActions = _.sortBy(actions, 'sequenceNumber'); + const lastMessageIndex = _.findLastIndex(sortedActions, action => ( + !isDeletedAction(action) && action.sequenceNumber <= lastReadSequenceNumber + )); + + // It's possible we won't find any and in that case the last read should be reset + if (lastMessageIndex < 0) { + return 0; + } + + return actions[lastMessageIndex].sequenceNumber; +} + export { + getNewLastReadSequenceNumberForDeletedAction, + getLastVisibleMessageText, getSortedReportActions, getMostRecentIOUReportSequenceNumber, isDeletedAction, diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index e48939c5e7e9..1cc13e65c7f7 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -101,6 +101,7 @@ function canEditReportAction(reportAction) { && reportAction.reportActionID && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !isReportMessageAttachment(lodashGet(reportAction, ['message', 0], {})) + && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } @@ -116,6 +117,7 @@ function canDeleteReportAction(reportAction) { return reportAction.actorEmail === sessionEmail && reportAction.reportActionID && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT + && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 3d9d75cda781..9d977290278d 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -11,7 +11,6 @@ import PushNotification from '../Notification/PushNotification'; import * as PersonalDetails from './PersonalDetails'; import Navigation from '../Navigation/Navigation'; import * as ActiveClientManager from '../ActiveClientManager'; -import Visibility from '../Visibility'; import ROUTES from '../../ROUTES'; import Timing from './Timing'; import * as DeprecatedAPI from '../deprecatedAPI'; @@ -21,12 +20,12 @@ import CONST from '../../CONST'; import Log from '../Log'; import * as LoginUtils from '../LoginUtils'; import * as ReportUtils from '../ReportUtils'; -import * as ReportActions from './ReportActions'; import Growl from '../Growl'; import * as Localize from '../Localize'; import DateUtils from '../DateUtils'; import * as ReportActionsUtils from '../ReportActionsUtils'; import * as NumberUtils from '../NumberUtils'; +import Visibility from '../Visibility'; let currentUserEmail; let currentUserAccountID; @@ -802,10 +801,20 @@ function addActions(reportID, text = '', file) { const lastAction = attachmentAction || reportCommentAction; // We need a newSequenceNumber that is n larger than the current depending on how many actions we are adding. - const actionCount = text && file ? 2 : 1; const highestSequenceNumber = getMaxSequenceNumber(reportID); + const actionCount = text && file ? 2 : 1; const newSequenceNumber = highestSequenceNumber + actionCount; + // We guess at what these sequenceNumbers are to enable marking as unread while offline + if (text && file) { + reportCommentAction.sequenceNumber = highestSequenceNumber + 1; + attachmentAction.sequenceNumber = highestSequenceNumber + 2; + } else if (file) { + attachmentAction.sequenceNumber = highestSequenceNumber + 1; + } else { + reportCommentAction.sequenceNumber = highestSequenceNumber + 1; + } + // Update the report in Onyx to have the new sequence number const optimisticReport = { maxSequenceNumber: newSequenceNumber, @@ -815,13 +824,14 @@ function addActions(reportID, text = '', file) { lastReadSequenceNumber: newSequenceNumber, }; - // Optimistically add the new actions to the store before waiting to save them to the server + // Optimistically add the new actions to the store before waiting to save them to the server. We use the clientID + // so that we can later unset these messages via the server by sending {[clientID]: null} const optimisticReportActions = {}; if (text) { - optimisticReportActions[reportCommentAction.sequenceNumber] = reportCommentAction; + optimisticReportActions[reportCommentAction.clientID] = reportCommentAction; } if (file) { - optimisticReportActions[attachmentAction.sequenceNumber] = attachmentAction; + optimisticReportActions[attachmentAction.clientID] = attachmentAction; } const parameters = { @@ -1159,26 +1169,32 @@ Onyx.connect({ */ function deleteReportComment(reportID, reportAction) { const sequenceNumber = reportAction.sequenceNumber; - - // We are not updating the message content here so the message can re-appear as strike-throughed - // if the user goes offline. The API will update the message content to empty strings on success. + const deletedMessage = [{ + type: 'COMMENT', + html: '', + text: '', + isEdited: true, + }]; const optimisticReportActions = { [sequenceNumber]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + previousMessage: reportAction.message, + message: deletedMessage, }, }; - // If we are deleting the last visible message, let's find the previous visible one - // and update the lastMessageText in the chat preview. + // If we are deleting the last visible message, let's find the previous visible one and update the lastMessageText in the LHN. + // Similarly, we are deleting the last read comment will want to update the lastReadSequenceNumber to use the previous visible message. + const lastMessageText = ReportActionsUtils.getLastVisibleMessageText(reportID, optimisticReportActions); + const lastReadSequenceNumber = ReportActionsUtils.getNewLastReadSequenceNumberForDeletedAction( + reportID, + optimisticReportActions, + reportAction.sequenceNumber, + getLastReadSequenceNumber(reportID), + ); const optimisticReport = { - lastMessageText: ReportActions.getLastVisibleMessageText(reportID, { - [sequenceNumber]: { - message: [{ - html: '', - text: '', - }], - }, - }), + lastMessageText, + lastReadSequenceNumber, }; // If the API call fails we must show the original message again, so we revert the message content back to how it was @@ -1191,6 +1207,7 @@ function deleteReportComment(reportID, reportAction) { [sequenceNumber]: { message: reportAction.message, pendingAction: null, + previousMessage: null, }, }, }, @@ -1203,6 +1220,7 @@ function deleteReportComment(reportID, reportAction) { value: { [sequenceNumber]: { pendingAction: null, + previousMessage: null, }, }, }, @@ -1511,11 +1529,14 @@ function viewNewReportAction(reportID, action) { const lastReadSequenceNumber = getLastReadSequenceNumber(reportID); const updatedReportObject = {}; - // When handling an action from the current user we can assume that their last read actionID has been updated in the server, but not necessarily reflected - // locally so we will update the lastReadSequenceNumber to mark the report as read. + // When handling an action from the current user we can assume that their last read actionID has been updated in the server, + // but not necessarily reflected locally so we will update the lastReadSequenceNumber to mark the report as read. if (isFromCurrentUser) { updatedReportObject.lastVisitedTimestamp = Date.now(); updatedReportObject.lastReadSequenceNumber = action.pendingAction ? lastReadSequenceNumber : action.sequenceNumber; + updatedReportObject.maxSequenceNumber = action.sequenceNumber; + } else { + updatedReportObject.maxSequenceNumber = action.sequenceNumber; } Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, updatedReportObject); @@ -1589,7 +1610,9 @@ Onyx.connect({ return; } - if (action.isLoading) { + // We don't want to process any new actions that have a pendingAction field as this means they are "optimistic" and no notifications + // should be created for them + if (!_.isEmpty(action.pendingAction)) { return; } @@ -1599,6 +1622,8 @@ Onyx.connect({ // If we are past the deadline to notify for this comment don't do it if (moment.utc(action.timestamp * 1000).isBefore(moment.utc().subtract(10, 'seconds'))) { + handledReportActions[reportID] = handledReportActions[reportID] || {}; + handledReportActions[reportID][action.sequenceNumber] = true; return; } diff --git a/src/libs/actions/ReportActions.js b/src/libs/actions/ReportActions.js index 504e915ef76b..f1b7c3380acc 100644 --- a/src/libs/actions/ReportActions.js +++ b/src/libs/actions/ReportActions.js @@ -1,103 +1,5 @@ -import _ from 'underscore'; import Onyx from 'react-native-onyx'; -import lodashGet from 'lodash/get'; -import lodashMerge from 'lodash/merge'; -import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import ONYXKEYS from '../../ONYXKEYS'; -import * as CollectionUtils from '../CollectionUtils'; -import CONST from '../../CONST'; -import * as ReportUtils from '../ReportUtils'; -import * as ReportActionsUtils from '../ReportActionsUtils'; - -/** - * Map of the most recent non-loading sequenceNumber for a reportActions_* key in Onyx by reportID. - * - * What's the difference between reportMaxSequenceNumbers and reportActionsMaxSequenceNumbers? - * - * Knowing the maxSequenceNumber for a report does not necessarily mean we have stored the report actions for that - * report. To understand and optimize which reportActions we need to fetch we also keep track of the max sequenceNumber - * for the stored reportActions in reportActionsMaxSequenceNumbers. This allows us to initially download all - * reportActions when the app starts up and then only download the actions that we need when the app reconnects. - * - * This information should only be used in the correct contexts. In most cases, reportMaxSequenceNumbers should be - * referenced and not the locally stored reportAction's max sequenceNumber. - */ -const reportActionsMaxSequenceNumbers = {}; -const reportActions = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { - return; - } - - const reportID = CollectionUtils.extractCollectionItemID(key); - const actionsArray = _.toArray(actions); - reportActions[reportID] = actionsArray; - const mostRecentNonLoadingActionIndex = _.findLastIndex(actionsArray, action => !action.isLoading); - const mostRecentAction = actionsArray[mostRecentNonLoadingActionIndex]; - if (!mostRecentAction || _.isUndefined(mostRecentAction.sequenceNumber)) { - return; - } - - reportActionsMaxSequenceNumbers[reportID] = mostRecentAction.sequenceNumber; - }, -}); - -/** - * Get the count of deleted messages after a sequence number of a report - * @param {Number|String} reportID - * @param {Number} sequenceNumber - * @return {Number} - */ -function getDeletedCommentsCount(reportID, sequenceNumber) { - if (!reportActions[reportID]) { - return 0; - } - - return _.reduce(reportActions[reportID], (numDeletedMessages, action) => { - if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || action.sequenceNumber <= sequenceNumber) { - return numDeletedMessages; - } - - // Empty ADDCOMMENT actions typically mean they have been deleted - const message = _.first(lodashGet(action, 'message', null)); - const html = lodashGet(message, 'html', ''); - return _.isEmpty(html) ? numDeletedMessages + 1 : numDeletedMessages; - }, 0); -} - -/** - * Get the message text for the last action that was not deleted - * @param {Number} reportID - * @param {Object} [actionsToMerge] - * @return {String} - */ -function getLastVisibleMessageText(reportID, actionsToMerge = {}) { - const parser = new ExpensiMark(); - const existingReportActions = _.indexBy(reportActions[reportID], 'sequenceNumber'); - const actions = _.toArray(lodashMerge({}, existingReportActions, actionsToMerge)); - const lastMessageIndex = _.findLastIndex(actions, action => ( - !ReportActionsUtils.isDeletedAction(action) - )); - const htmlText = lodashGet(actions, [lastMessageIndex, 'message', 0, 'html'], ''); - const messageText = parser.htmlToText(htmlText); - return ReportUtils.formatReportLastMessageText(messageText); -} - -/** - * @param {Number} reportID - * @param {Number} sequenceNumber - * @param {Number} currentUserAccountID - * @param {Object} [actionsToMerge] - * @returns {Boolean} - */ -function isFromCurrentUser(reportID, sequenceNumber, currentUserAccountID, actionsToMerge = {}) { - const existingReportActions = _.indexBy(reportActions[reportID], 'sequenceNumber'); - const action = lodashMerge({}, existingReportActions, actionsToMerge)[sequenceNumber]; - return action.actorAccountID === currentUserAccountID; -} /** * @param {Number} reportID @@ -122,9 +24,6 @@ function clearReportActionErrors(reportID, sequenceNumber) { } export { - getDeletedCommentsCount, - getLastVisibleMessageText, clearReportActionErrors, - isFromCurrentUser, deleteOptimisticReportAction, }; diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 93cd78ea63c5..ea5db9406a7e 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -26,6 +26,7 @@ import Text from '../../components/Text'; import Tooltip from '../../components/Tooltip'; import variables from '../../styles/variables'; import colors from '../../styles/colors'; +import ReportHeaderViewBackButton from './ReportHeaderViewBackButton'; const propTypes = { /** Toggles the navigationMenu open and closed */ @@ -49,7 +50,7 @@ const propTypes = { policies: PropTypes.shape({ /** Name of the policy */ name: PropTypes.string, - }).isRequired, + }), /** Personal details of all the users */ personalDetails: PropTypes.objectOf(participantPropTypes), @@ -60,6 +61,7 @@ const propTypes = { const defaultProps = { personalDetails: {}, + policies: {}, report: null, }; @@ -89,17 +91,13 @@ const HeaderView = (props) => { const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies); const brickRoadIndicator = ReportUtils.hasReportNameError(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; return ( - + {props.isSmallScreenWidth && ( - - - - - + )} {Boolean(props.report && title) && ( ( + + + + + +); + +ReportHeaderViewBackButton.displayName = 'ReportHeaderViewBackButton'; +ReportHeaderViewBackButton.propTypes = propTypes; +export default ReportHeaderViewBackButton; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index f852dfb2626f..4e43bb06956c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -26,6 +26,7 @@ import addViewportResizeListener from '../../libs/VisualViewport'; import {withNetwork} from '../../components/OnyxProvider'; import compose from '../../libs/compose'; import networkPropTypes from '../../components/networkPropTypes'; +import withDrawerState, {withDrawerPropTypes} from '../../components/withDrawerState'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -80,6 +81,8 @@ const propTypes = { /** Information about the network */ network: networkPropTypes.isRequired, + + ...withDrawerPropTypes, }; const defaultProps = { @@ -234,6 +237,7 @@ class ReportScreen extends React.Component { /> this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} @@ -250,6 +254,7 @@ class ReportScreen extends React.Component { report={this.props.report} session={this.props.session} isComposerFullSize={this.props.isComposerFullSize} + isDrawerOpen={this.props.isDrawerOpen} /> )} {(isArchivedRoom || this.props.session.shouldShowComposeInput) && ( @@ -285,6 +290,7 @@ ReportScreen.propTypes = propTypes; ReportScreen.defaultProps = defaultProps; export default compose( + withDrawerState, withNetwork(), withOnyx({ isSidebarLoaded: { diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js index c5a9ed5078eb..a367eea5f602 100644 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js +++ b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js @@ -4,7 +4,7 @@ import styles from '../../../../../styles/styles'; import floatingMessageCounterContainerPropTypes from './floatingMessageCounterContainerPropTypes'; const FloatingMessageCounterContainer = props => ( - + {props.children} ); diff --git a/src/pages/home/report/FloatingMessageCounter/index.js b/src/pages/home/report/FloatingMessageCounter/index.js index 7025d5a67a4f..8af8266b0a87 100644 --- a/src/pages/home/report/FloatingMessageCounter/index.js +++ b/src/pages/home/report/FloatingMessageCounter/index.js @@ -62,7 +62,7 @@ class FloatingMessageCounter extends PureComponent { render() { return ( - + {hovered => ( - + {this.props.shouldDisplayNewIndicator && ( - + )} { const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies); - return ( - ReportUtils.navigateToDetailsPage(props.report)}> diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js index a066b2be34f8..d226ac9ced90 100644 --- a/src/pages/home/report/ReportActionItemMessage.js +++ b/src/pages/home/report/ReportActionItemMessage.js @@ -6,18 +6,12 @@ import lodashGet from 'lodash/get'; import styles from '../../../styles/styles'; import ReportActionItemFragment from './ReportActionItemFragment'; import reportActionPropTypes from './reportActionPropTypes'; -import {withNetwork} from '../../../components/OnyxProvider'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; -import compose from '../../../libs/compose'; -import networkPropTypes from '../../../components/networkPropTypes'; const propTypes = { /** The report action */ action: PropTypes.shape(reportActionPropTypes).isRequired, - /** Information about the network */ - network: networkPropTypes.isRequired, - /** Additional styles to add after local styles. */ style: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.object), @@ -32,31 +26,24 @@ const defaultProps = { style: [], }; -const ReportActionItemMessage = (props) => { - const isUnsent = props.network.isOffline && props.action.isLoading; - - return ( - - {_.map(_.compact(props.action.message), (fragment, index) => ( - - ))} - - ); -}; +const ReportActionItemMessage = props => ( + + {_.map(_.compact(props.action.previousMessage || props.action.message), (fragment, index) => ( + + ))} + +); ReportActionItemMessage.propTypes = propTypes; ReportActionItemMessage.defaultProps = defaultProps; ReportActionItemMessage.displayName = 'ReportActionItemMessage'; -export default compose( - withNetwork(), - withLocalize, -)(ReportActionItemMessage); +export default withLocalize(ReportActionItemMessage); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index bdfe34ce2d1c..77e54bd39162 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -106,14 +106,14 @@ class ReportActionsList extends React.Component { } /** - * Create a unique key for Each Action in the FlatList. - * We use the reportActionId that is a string representation of a random 64-bit int, which should be - * random enought to avoid colisions + * Create a unique key for each action in the FlatList. + * We use the reportActionID that is a string representation of a random 64-bit int, which should be + * random enough to avoid collisions * @param {Object} item * @return {String} */ keyExtractor(item) { - return `${item.action.reportActionID}${item.action.sequenceNumber}`; + return `${item.action.clientID}${item.action.reportActionID}${item.action.sequenceNumber}`; } /** @@ -135,7 +135,8 @@ class ReportActionsList extends React.Component { // When the new indicator should not be displayed we explicitly set it to 0. The marker should never be shown above the // created action (which will have sequenceNumber of 0) so we use 0 to indicate "hidden". const shouldDisplayNewIndicator = this.props.newMarkerSequenceNumber > 0 - && item.action.sequenceNumber === this.props.newMarkerSequenceNumber; + && item.action.sequenceNumber === this.props.newMarkerSequenceNumber + && !ReportActionsUtils.isDeletedAction(item.action); return ( { + this.appStateChangeListener = AppState.addEventListener('change', () => { if (!this.getIsReportFullyVisible()) { return; } // If the app user becomes active and they have no unread actions we clear the new marker to sync their device // e.g. they could have read these messages on another device and only just become active here - if (state === 'active' && !ReportUtils.isUnread(this.props.report)) { - this.setState({newMarkerSequenceNumber: 0}); - } - Report.openReport(this.props.report.reportID); + this.setState({newMarkerSequenceNumber: 0}); }); Report.subscribeToReportTypingEvents(this.props.report.reportID); @@ -383,7 +381,6 @@ ReportActionsView.defaultProps = defaultProps; export default compose( Performance.withRenderTrace({id: ' rendering'}), withWindowDimensions, - withDrawerState, withLocalize, withNetwork(), )(ReportActionsView); diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 7028b796a0ea..417c4030a11b 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -253,7 +253,7 @@ class SidebarLinks extends React.Component { }]; return ( - + - + this.input = el} label={this.props.translate('loginForm.phoneOrEmail')} diff --git a/src/setup/platformSetup/index.native.js b/src/setup/platformSetup/index.native.js index cd55b6a280a4..02a0629db1d4 100644 --- a/src/setup/platformSetup/index.native.js +++ b/src/setup/platformSetup/index.native.js @@ -23,7 +23,7 @@ export default function () { Report.subscribeToReportCommentPushNotifications(); // Setup Flipper plugins when on dev - if (__DEV__) { + if (__DEV__ && typeof jest === 'undefined') { require('flipper-plugin-bridgespy-client'); } diff --git a/src/styles/styles.js b/src/styles/styles.js index 467a5609a4bf..8ee0006b18fe 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1330,10 +1330,6 @@ const styles = { ...wordBreak.breakWord, }, - chatItemUnsentMessage: { - opacity: 0.6, - }, - chatItemMessageLink: { color: colors.blue, fontSize: variables.fontSizeNormal, diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index c8d95f9dcabb..cbf04d3adb58 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -74,7 +74,7 @@ describe('actions/Report', () => { callback: val => reportActions = val, }); - let sequenceNumber; + let clientID; // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) @@ -92,8 +92,8 @@ describe('actions/Report', () => { .then(() => { const resultAction = _.first(_.values(reportActions)); - // Store the generated sequenceNumber so that we can send it with our mock Pusher update - sequenceNumber = resultAction.sequenceNumber; + // Store the generated clientID so that we can send it with our mock Pusher update + clientID = resultAction.clientID; expect(resultAction.message).toEqual(REPORT_ACTION.message); expect(resultAction.person).toEqual(REPORT_ACTION.person); expect(resultAction.pendingAction).toEqual(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -120,7 +120,7 @@ describe('actions/Report', () => { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, value: { - [sequenceNumber]: null, + [clientID]: null, [ACTION_ID]: actionWithoutLoading, }, }, @@ -348,9 +348,9 @@ describe('actions/Report', () => { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, value: { - [_.toArray(reportActions)[1].sequenceNumber]: null, - [_.toArray(reportActions)[2].sequenceNumber]: null, - [_.toArray(reportActions)[3].sequenceNumber]: null, + [_.toArray(reportActions)[1].clientID]: null, + [_.toArray(reportActions)[2].clientID]: null, + [_.toArray(reportActions)[3].clientID]: null, 2: { ...USER_1_BASE_ACTION, message: [{type: 'COMMENT', html: 'Current User Comment 1', text: 'Current User Comment 1'}], @@ -374,7 +374,7 @@ describe('actions/Report', () => { }) .then(() => { // If the user deletes a comment that is before the last read - Report.deleteReportComment(REPORT_ID, reportActions[2]); + Report.deleteReportComment(REPORT_ID, {...reportActions[2], sequenceNumber: 2, clientID: null}); return waitForPromisesToResolve(); }) .then(() => { @@ -392,7 +392,7 @@ describe('actions/Report', () => { expect(report.lastReadSequenceNumber).toBe(2); // If the user deletes the last comment after the last read the lastMessageText will reflect the new last comment - Report.deleteReportComment(REPORT_ID, reportActions[4]); + Report.deleteReportComment(REPORT_ID, {...reportActions[4], sequenceNumber: 4, clientID: null}); return waitForPromisesToResolve(); }) .then(() => { diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js new file mode 100644 index 000000000000..d26b6bae09d5 --- /dev/null +++ b/tests/ui/UnreadIndicatorsTest.js @@ -0,0 +1,395 @@ +/* eslint-disable @lwc/lwc/no-async-await */ +import React from 'react'; +import Onyx from 'react-native-onyx'; +import {Linking, AppState} from 'react-native'; +import {fireEvent, render, act} from '@testing-library/react-native'; +import lodashGet from 'lodash/get'; +import moment from 'moment'; +import App from '../../src/App'; +import CONST from '../../src/CONST'; +import ONYXKEYS from '../../src/ONYXKEYS'; +import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import * as TestHelper from '../utils/TestHelper'; +import appSetup from '../../src/setup'; +import fontWeightBold from '../../src/styles/fontWeight/bold'; +import * as AppActions from '../../src/libs/actions/App'; +import ReportHeaderViewBackButton from '../../src/pages/home/ReportHeaderViewBackButton'; +import ReportActionsView from '../../src/pages/home/report/ReportActionsView'; +import * as NumberUtils from '../../src/libs/NumberUtils'; +import LocalNotification from '../../src/libs/Notification/LocalNotification'; +import * as Report from '../../src/libs/actions/Report'; + +beforeAll(() => { + global.fetch = TestHelper.getGlobalFetchMock(); + + // We need a bit more time for this test in some places + jest.setTimeout(30000); + Linking.setInitialURL('https://new.expensify.com/r/1'); + appSetup(); +}); + +/** + * @param {RenderAPI} renderedApp + */ +function scrollUpToRevealNewMessagesBadge(renderedApp) { + fireEvent.scroll(renderedApp.getByTestId('report-actions-list'), { + nativeEvent: { + contentOffset: { + y: 250, + }, + contentSize: { + // Dimensions of the scrollable content + height: 500, + width: 100, + }, + layoutMeasurement: { + // Dimensions of the device + height: 700, + width: 300, + }, + }, + }); + + // We advance the timer since we must wait for the animation to end + // and the new style to be reflected + jest.advanceTimersByTime(100); +} + +/** + * @param {RenderAPI} renderedApp + * @return {Boolean} + */ +function isNewMessagesBadgeVisible(renderedApp) { + const badge = renderedApp.getByTestId('new-messages-badge'); + return badge.props.style.transform[0].translateY === 10; +} + +/** + * @param {RenderAPI} renderedApp + * @return {Promise} + */ +async function navigateToSidebar(renderedApp) { + const reportHeader = renderedApp.getByTestId('report-header'); + const reportHeaderBackButton = reportHeader.findByType(ReportHeaderViewBackButton); + fireEvent(reportHeaderBackButton, 'press'); + return waitForPromisesToResolve(); +} + +/** + * @param {RenderAPI} renderedApp + * @param {Number} index + * @return {Promise} + */ +async function navigateToSidebarOption(renderedApp, index) { + const optionRows = renderedApp.getAllByTestId('option-row'); + fireEvent(optionRows[index], 'press'); + await waitForPromisesToResolve(); +} + +/** + * @param {RenderAPI} renderedApp + * @return {Boolean} + */ +function isDrawerOpen(renderedApp) { + const reportScreen = renderedApp.getByTestId('report-screen'); + return reportScreen.findByType(ReportActionsView).props.isDrawerOpen; +} + +test('Unread LHN Status, “New Messages” badge, and New Line Indicator', async () => { + const REPORT_ID = 1; + const USER_A_ACCOUNT_ID = 1; + const USER_A_EMAIL = 'user_a@test.com'; + const USER_B_ACCOUNT_ID = 2; + const USER_B_EMAIL = 'user_b@test.com'; + const USER_C_ACCOUNT_ID = 3; + const USER_C_EMAIL = 'user_c@test.com'; + + // Render the App and sign in as a test user. + const renderedApp = render(); + + // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some + // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act + await act(async () => { + await waitForPromisesToResolve(); + await waitForPromisesToResolve(); + }); + + const loginForm = renderedApp.getByTestId('login-form'); + expect(loginForm).toBeTruthy(); + + TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + await waitForPromisesToResolve(); + + const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + + // Simulate setting an unread report and personal details + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 9, + lastReadSequenceNumber: 1, + lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), // Ten minutes ago + lastMessageText: 'Test', + participants: [USER_B_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: MOMENT_TEN_MINUTES_AGO.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), + 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), + 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), + 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), + 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), + 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), + 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), + 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), + 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); + await waitForPromisesToResolve(); + + // Verify no notifications are created for these older messages + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); + + // Verify the sidebar links are rendered + const sidebarLinks = renderedApp.getByTestId('sidebar-links'); + expect(sidebarLinks).toBeTruthy(); + + // And verify that the Report screen is rendered after manually setting the sidebar as loaded + // since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(true); + await waitForPromisesToResolve(); + + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Verify there is only one option in the sidebar + let optionRows = renderedApp.getAllByTestId('option-row'); + expect(optionRows.length).toBe(1); + + // And that the text is bold + const displayNameText = renderedApp.getByTestId('option-row-display-name'); + expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + await navigateToSidebarOption(renderedApp, 0); + + // Verify that the report screen is rendered and the drawer is closed + expect(isDrawerOpen(renderedApp)).toBe(false); + + // That the report actions are visible along with the created action + const createdAction = renderedApp.getByTestId('report-action-created'); + expect(createdAction).toBeTruthy(); + const reportComments = renderedApp.getAllByTestId('report-action-item'); + expect(reportComments.length).toBe(9); + + // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will + // have a sequenceNumber of 2 + let unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(2); + + // Scroll up and verify that the "New messages" badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) + const updatedDisplayNameText = renderedApp.getByTestId('option-row-display-name'); + expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + + // Tap on the back button to return to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the LHN is now open + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Navigate to the report again + await navigateToSidebarOption(renderedApp, 0); + + // Verify the unread indicator is no longer present + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Scroll and verify that the new messages badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + + // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant + const NEW_REPORT_ID = 2; + const NEW_REPORT_CREATED_MOMENT = moment(); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { + reportID: NEW_REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 1, + lastReadSequenceNumber: 0, + lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), + lastMessageText: 'Comment 1', + participants: [USER_C_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: NEW_REPORT_CREATED_MOMENT.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorEmail: USER_C_EMAIL, + person: [{type: 'TEXT', style: 'strong', text: 'User C'}], + sequenceNumber: 1, + timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), + message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], + reportActionID: NumberUtils.rand64(), + }, + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + }); + await waitForPromisesToResolve(); + + // Verify notification was created as the new message that has arrived is very recent + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); + + // Navigate back to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the new report option appears in the LHN + optionRows = renderedApp.getAllByTestId('option-row'); + expect(optionRows.length).toBe(2); + + // Verify the text for the new chat is bold and above the previous indicating it has not yet been read + let displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + const firstReportOption = displayNameTexts[0]; + expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); + + const secondReportOption = displayNameTexts[1]; + expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); + + // Tap the new report option and navigate back to the sidebar again via the back button + await navigateToSidebarOption(renderedApp, 0); + + // Verify that all report options appear in a "read" state + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Tap the previous report between User A and User B + await navigateToSidebarOption(renderedApp, 1); + + // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then + // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly + Report.markCommentAsUnread(REPORT_ID, 3); + await waitForPromisesToResolve(); + + // Verify the indicator appears above the last action + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(3); + + // Scroll up and verify the new messages badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // Navigate to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the report is marked as unread in the sidebar + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and back to the sidebar + await navigateToSidebarOption(renderedApp, 1); + await navigateToSidebar(renderedApp); + + // Verify the report is now marked as read + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and verify the new line indicator is missing + await navigateToSidebarOption(renderedApp, 1); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // Scroll up and verify the badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Navigate to the LHN + await navigateToSidebar(renderedApp); + + // Simulate another new message on the report with User B + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 10: TestHelper.buildTestReportComment(USER_B_EMAIL, 10, moment().unix()), + }); + await waitForPromisesToResolve(); + + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the report again and verify the indicator exists + await navigateToSidebarOption(renderedApp, 1); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + + // Leave a comment as the current user and verify the indicator is removed + Report.addComment(REPORT_ID, 'Current User Comment 1'); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // Mark a previous comment as unread and verify the unread action indicator returns + Report.markCommentAsUnread(REPORT_ID, 9); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + + // Trigger the app going inactive and active again + AppState.emitCurrentTestState('background'); + AppState.emitCurrentTestState('active'); + + // Verify the new line is cleared + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // As the current user add several comments + Report.addComment(REPORT_ID, 'Current User Comment 2'); + await waitForPromisesToResolve(); + + Report.addComment(REPORT_ID, 'Current User Comment 3'); + await waitForPromisesToResolve(); + + Report.addComment(REPORT_ID, 'Current User Comment 4'); + await waitForPromisesToResolve(); + + // Mark the last comment as "unread" and verify the unread indicator appears + Report.markCommentAsUnread(REPORT_ID, 14); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); +}); diff --git a/tests/unit/LHNOrderTest.js b/tests/unit/LHNOrderTest.js index 3e411f7ca251..d705761a8c20 100644 --- a/tests/unit/LHNOrderTest.js +++ b/tests/unit/LHNOrderTest.js @@ -174,13 +174,6 @@ function getDefaultRenderedSidebarLinks() { )); } -// Icons need to be explicitly mocked. The testing library throws an error when trying to render them -jest.mock('../../src/components/Icon/Expensicons', () => ({ - MagnifyingGlass: () => '', - Pencil: () => '', - Pin: () => '', -})); - describe('Sidebar', () => { describe('in default mode', () => { // Clear out Onyx after each test so that each test starts with a clean slate diff --git a/tests/unit/loginTest.js b/tests/unit/loginTest.js index 830378294c30..c8bcc6c6f00f 100644 --- a/tests/unit/loginTest.js +++ b/tests/unit/loginTest.js @@ -9,18 +9,6 @@ import React from 'react'; import renderer from 'react-test-renderer'; import App from '../../src/App'; -/* uses and we need to mock the imported crashlytics module -* due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 */ -jest.mock('@react-native-firebase/crashlytics', () => () => ({ - log: jest.fn(), - recordError: jest.fn(), -})); - -jest.mock('../../src/libs/BootSplash', () => ({ - hide: jest.fn(), - getVisibilityStatus: jest.fn().mockResolvedValue('hidden'), -})); - describe('AppComponent', () => { it('renders correctly', () => { renderer.create(); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 92af57ba2c2c..319732eca13e 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -5,18 +5,43 @@ import HttpUtils from '../../src/libs/HttpUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; import waitForPromisesToResolve from './waitForPromisesToResolve'; import * as ReportUtils from '../../src/libs/ReportUtils'; +import * as NumberUtils from '../../src/libs/NumberUtils'; + +/** + * @param {String} login + * @param {Number} accountID + * @param {String} [firstName] + * @returns {Promise} + */ +function buildPersonalDetails(login, accountID, firstName = 'Test') { + const avatar = ReportUtils.getDefaultAvatar(login); + return { + accountID, + login, + avatar, + avatarThumbnail: avatar, + displayName: `${firstName} User`, + firstName, + lastName: 'User', + pronouns: '', + timezone: CONST.DEFAULT_TIME_ZONE, + payPalMeAddress: '', + phoneNumber: '', + }; +} /** * Simulate signing in and make sure all API calls in this flow succeed. Every time we add * a mockImplementationOnce() we are altering what Network.post() will return. * - * @param {Number} accountID - * @param {String} login - * @param {String} password - * @param {String} authToken + * @param {Number} [accountID] + * @param {String} [login] + * @param {String} [password] + * @param {String} [authToken] + * @param {String} [firstName] * @return {Promise} */ -function signInWithTestUser(accountID = 1, login = 'test@user.com', password = 'Password1', authToken = 'asdfqwerty') { +function signInWithTestUser(accountID = 1, login = 'test@user.com', password = 'Password1', authToken = 'asdfqwerty', firstName = 'Test') { const originalXhr = HttpUtils.xhr; HttpUtils.xhr = jest.fn(); HttpUtils.xhr.mockImplementation(() => Promise.resolve({ @@ -35,6 +60,13 @@ function signInWithTestUser(accountID = 1, login = 'test@user.com', password = ' validated: true, }, }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [login]: buildPersonalDetails(login, accountID, firstName), + }, + }, ], jsonCode: 200, })); @@ -86,28 +118,34 @@ function getGlobalFetchMock() { * @returns {Promise} */ function setPersonalDetails(login, accountID) { - const avatar = ReportUtils.getDefaultAvatar(login); - const details = { - accountID, - login, - avatar, - avatarThumbnail: avatar, - displayName: 'Test User', - firstName: 'Test', - lastName: 'User', - pronouns: '', - timezone: CONST.DEFAULT_TIME_ZONE, - payPalMeAddress: '', - phoneNumber: '', - }; Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [login]: details, + [login]: buildPersonalDetails(login, accountID), }); return waitForPromisesToResolve(); } +/** + * @param {String} actorEmail + * @param {Number} sequenceNumber + * @param {Number} timestamp + * @returns {Object} + */ +function buildTestReportComment(actorEmail, sequenceNumber, timestamp) { + return { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorEmail, + person: [{type: 'TEXT', style: 'strong', text: 'User B'}], + sequenceNumber, + timestamp, + message: [{type: 'COMMENT', html: 'Comment 1', text: `Comment ${sequenceNumber}`}], + reportActionID: NumberUtils.rand64(), + }; +} + export { getGlobalFetchMock, signInWithTestUser, setPersonalDetails, + buildPersonalDetails, + buildTestReportComment, }; From f1dc53487835f580eb7e6c1c69a3082c5848e9ce Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 13 Sep 2022 12:08:41 -0400 Subject: [PATCH 109/155] remove unused import --- src/pages/home/report/ReportActionsView.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 96808af88280..b60ac8acb975 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -30,7 +30,6 @@ import CopySelectionHelper from '../../../components/CopySelectionHelper'; import EmojiPicker from '../../../components/EmojiPicker/EmojiPicker'; import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; import * as ReportUtils from '../../../libs/ReportUtils'; -import Log from '../../../libs/Log'; const propTypes = { /* Onyx Props */ From 0a84843aa3bad17ac862c7f0f27b3570534f20b9 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Tue, 13 Sep 2022 13:01:51 -0700 Subject: [PATCH 110/155] remove usages of onyxRates --- src/libs/actions/Policy.js | 8 ++++---- .../workspace/reimburse/WorkspaceReimburseView.js | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index d8af8be7d7f1..5fac77a56380 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -529,7 +529,7 @@ function clearCustomUnitErrors(policyID, customUnitID, customUnitRateID) { [customUnitID]: { errors: null, pendingAction: null, - onyxRates: { + rates: { [customUnitRateID]: { errors: null, pendingAction: null, @@ -626,7 +626,7 @@ function updateCustomUnitRate(policyID, currentCustomUnitRate, customUnitID, new value: { customUnits: { [customUnitID]: { - onyxRates: { + rates: { [newCustomUnitRate.customUnitRateID]: { ...newCustomUnitRate, errors: null, @@ -646,7 +646,7 @@ function updateCustomUnitRate(policyID, currentCustomUnitRate, customUnitID, new value: { customUnits: { [customUnitID]: { - onyxRates: { + rates: { [newCustomUnitRate.customUnitRateID]: { pendingAction: null, }, @@ -664,7 +664,7 @@ function updateCustomUnitRate(policyID, currentCustomUnitRate, customUnitID, new value: { customUnits: { [customUnitID]: { - onyxRates: { + rates: { [currentCustomUnitRate.customUnitRateID]: { ...currentCustomUnitRate, errors: { diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.js index cead7dbc828a..37ddbfee899c 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.js @@ -40,7 +40,7 @@ const propTypes = { attributes: PropTypes.shape({ unit: PropTypes.string, }), - onyxRates: PropTypes.objectOf( + rates: PropTypes.objectOf( PropTypes.shape({ customUnitRateID: PropTypes.string, name: PropTypes.string, @@ -63,7 +63,7 @@ class WorkspaceReimburseView extends React.Component { constructor(props) { super(props); const distanceCustomUnit = _.find(lodashGet(props, 'policy.customUnits', {}), unit => unit.name === 'Distance'); - const customUnitRate = _.find(lodashGet(distanceCustomUnit, 'onyxRates', {}), rate => rate.name === 'Default Rate'); + const customUnitRate = _.find(lodashGet(distanceCustomUnit, 'rates', {}), rate => rate.name === 'Default Rate'); this.state = { unitID: lodashGet(distanceCustomUnit, 'customUnitID', ''), @@ -99,7 +99,7 @@ class WorkspaceReimburseView extends React.Component { .values() .findWhere({name: CONST.CUSTOM_UNITS.NAME_DISTANCE}) .value(); - const customUnitRate = _.find(lodashGet(distanceCustomUnit, 'onyxRates', {}), rate => rate.name === 'Default Rate'); + const customUnitRate = _.find(lodashGet(distanceCustomUnit, 'rates', {}), rate => rate.name === 'Default Rate'); this.setState({ unitID: lodashGet(distanceCustomUnit, 'customUnitID', ''), unitName: lodashGet(distanceCustomUnit, 'name', ''), @@ -177,7 +177,7 @@ class WorkspaceReimburseView extends React.Component { }); const distanceCustomUnit = _.find(lodashGet(this.props, 'policy.customUnits', {}), unit => unit.name === 'Distance'); - const currentCustomUnitRate = lodashGet(distanceCustomUnit, ['onyxRates', this.state.unitRateID], {}); + const currentCustomUnitRate = lodashGet(distanceCustomUnit, ['rates', this.state.unitRateID], {}); Policy.updateCustomUnitRate(this.props.policyID, currentCustomUnitRate, this.state.unitID, { ...currentCustomUnitRate, rate: numValue.toFixed(3) * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, @@ -223,10 +223,10 @@ class WorkspaceReimburseView extends React.Component { Policy.clearCustomUnitErrors(this.props.policyID, this.state.unitID, this.state.unitRateID)} > From f67be5a222e5f751423c771f3f827f764aaf0f5f Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Tue, 13 Sep 2022 16:14:42 -0700 Subject: [PATCH 111/155] Revert "Refactor withFullPolicy" --- src/components/AvatarWithIndicator.js | 4 +- src/components/RoomNameInput.js | 4 ++ src/pages/ReportSettingsPage.js | 15 +++++ .../workspace/WorkspaceBankAccountPage.js | 4 +- src/pages/workspace/WorkspaceInitialPage.js | 8 +-- src/pages/workspace/WorkspaceInvitePage.js | 8 +-- src/pages/workspace/WorkspaceMembersPage.js | 8 +-- src/pages/workspace/WorkspaceNewRoomPage.js | 8 ++- .../workspace/WorkspacePageWithSections.js | 4 +- src/pages/workspace/WorkspaceSettingsPage.js | 8 +-- .../reimburse/WorkspaceReimburseView.js | 4 +- .../{withPolicy.js => withFullPolicy.js} | 56 +++++++++++++------ 12 files changed, 88 insertions(+), 43 deletions(-) rename src/pages/workspace/{withPolicy.js => withFullPolicy.js} (64%) diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 1bd517cfd642..afa59dfbdd9e 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -11,7 +11,7 @@ import policyMemberPropType from '../pages/policyMemberPropType'; import bankAccountPropTypes from './bankAccountPropTypes'; import cardPropTypes from './cardPropTypes'; import userWalletPropTypes from '../pages/EnablePayments/userWalletPropTypes'; -import {policyPropTypes} from '../pages/workspace/withPolicy'; +import {fullPolicyPropTypes} from '../pages/workspace/withFullPolicy'; import walletTermsPropTypes from '../pages/EnablePayments/walletTermsPropTypes'; import * as PolicyUtils from '../libs/PolicyUtils'; import * as PaymentMethods from '../libs/actions/PaymentMethods'; @@ -30,7 +30,7 @@ const propTypes = { policiesMemberList: PropTypes.objectOf(policyMemberPropType), /** All the user's policies (from Onyx via withFullPolicy) */ - policies: PropTypes.objectOf(policyPropTypes.policy), + policies: PropTypes.objectOf(fullPolicyPropTypes.policy), /** List of bank accounts */ bankAccountList: PropTypes.objectOf(bankAccountPropTypes), diff --git a/src/components/RoomNameInput.js b/src/components/RoomNameInput.js index 55e1a2296a7a..8bd14fe9408c 100644 --- a/src/components/RoomNameInput.js +++ b/src/components/RoomNameInput.js @@ -5,6 +5,7 @@ import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; import compose from '../libs/compose'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import withFullPolicy, {fullPolicyDefaultProps, fullPolicyPropTypes} from '../pages/workspace/withFullPolicy'; import TextInput from './TextInput'; const propTypes = { @@ -21,6 +22,7 @@ const propTypes = { errorText: PropTypes.string, ...withLocalizePropTypes, + ...fullPolicyPropTypes, /* Onyx Props */ @@ -51,6 +53,7 @@ const defaultProps = { initialValue: '', disabled: false, errorText: '', + ...fullPolicyDefaultProps, forwardedRef: () => {}, }; @@ -111,6 +114,7 @@ RoomNameInput.defaultProps = defaultProps; export default compose( withLocalize, + withFullPolicy, withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js index 0fe5d285fb92..9efc59dc940e 100644 --- a/src/pages/ReportSettingsPage.js +++ b/src/pages/ReportSettingsPage.js @@ -18,6 +18,7 @@ import Text from '../components/Text'; import Button from '../components/Button'; import RoomNameInput from '../components/RoomNameInput'; import Picker from '../components/Picker'; +import withFullPolicy, {fullPolicyDefaultProps, fullPolicyPropTypes} from './workspace/withFullPolicy'; import * as ValidationUtils from '../libs/ValidationUtils'; import OfflineWithFeedback from '../components/OfflineWithFeedback'; @@ -30,6 +31,7 @@ const propTypes = { }), }).isRequired, + ...fullPolicyPropTypes, ...withLocalizePropTypes, /* Onyx Props */ @@ -77,6 +79,17 @@ const propTypes = { }).isRequired, }; +const defaultProps = { + ...fullPolicyDefaultProps, + report: { + reportID: 0, + reportName: '', + policyID: '', + notificationPreference: '', + visibility: '', + }, +}; + class ReportSettingsPage extends Component { constructor(props) { super(props); @@ -274,9 +287,11 @@ class ReportSettingsPage extends Component { } ReportSettingsPage.propTypes = propTypes; +ReportSettingsPage.defaultProps = defaultProps; export default compose( withLocalize, + withFullPolicy, withOnyx({ report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, diff --git a/src/pages/workspace/WorkspaceBankAccountPage.js b/src/pages/workspace/WorkspaceBankAccountPage.js index 64bd69cb9bd3..6b95d4685ddd 100644 --- a/src/pages/workspace/WorkspaceBankAccountPage.js +++ b/src/pages/workspace/WorkspaceBankAccountPage.js @@ -21,7 +21,7 @@ import Section from '../../components/Section'; import WorkspaceResetBankAccountModal from './WorkspaceResetBankAccountModal'; import styles from '../../styles/styles'; import CONST from '../../CONST'; -import withPolicy from './withPolicy'; +import withFullPolicy from './withFullPolicy'; import Button from '../../components/Button'; import MenuItem from '../../components/MenuItem'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; @@ -131,5 +131,5 @@ export default compose( key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, }), - withPolicy, + withFullPolicy, )(WorkspaceBankAccountPage); diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index cbcccda54c62..6445933dec42 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -20,7 +20,7 @@ import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import compose from '../../libs/compose'; import Avatar from '../../components/Avatar'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; -import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; +import withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy'; import * as Policy from '../../libs/actions/Policy'; import * as PolicyUtils from '../../libs/PolicyUtils'; import CONST from '../../CONST'; @@ -30,7 +30,7 @@ import policyMemberPropType from '../policyMemberPropType'; import OfflineWithFeedback from '../../components/OfflineWithFeedback'; const propTypes = { - ...policyPropTypes, + ...fullPolicyPropTypes, ...withLocalizePropTypes, /** The employee list of this policy (coming from Onyx) */ @@ -38,7 +38,7 @@ const propTypes = { }; const defaultProps = { - ...policyDefaultProps, + ...fullPolicyDefaultProps, policyMemberList: {}, }; @@ -248,7 +248,7 @@ WorkspaceInitialPage.defaultProps = defaultProps; export default compose( withLocalize, - withPolicy, + withFullPolicy, withOnyx({ policyMemberList: { key: ({policy}) => `${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policy.id}`, diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 86fcc48b9757..dbefba433b49 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -20,7 +20,7 @@ import CONST from '../../CONST'; import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; import * as Link from '../../libs/actions/Link'; import Text from '../../components/Text'; -import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; +import withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy'; import {withNetwork} from '../../components/OnyxProvider'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; import networkPropTypes from '../../components/networkPropTypes'; @@ -53,12 +53,12 @@ const propTypes = { }), }).isRequired, - ...policyPropTypes, + ...fullPolicyPropTypes, ...withLocalizePropTypes, ...networkPropTypes, }; -const defaultProps = policyDefaultProps; +const defaultProps = fullPolicyDefaultProps; class WorkspaceInvitePage extends React.Component { constructor(props) { @@ -352,7 +352,7 @@ WorkspaceInvitePage.defaultProps = defaultProps; export default compose( withLocalize, - withPolicy, + withFullPolicy, withNetwork(), withOnyx({ personalDetails: { diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 1de2d1759565..db12309ce7b7 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -25,7 +25,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/ import OptionRow from '../../components/OptionRow'; import CheckboxWithTooltip from '../../components/CheckboxWithTooltip'; import Hoverable from '../../components/Hoverable'; -import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; +import withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy'; import CONST from '../../CONST'; import OfflineWithFeedback from '../../components/OfflineWithFeedback'; import {withNetwork} from '../../components/OnyxProvider'; @@ -45,13 +45,13 @@ const propTypes = { }), }).isRequired, - ...policyPropTypes, + ...fullPolicyPropTypes, ...withLocalizePropTypes, ...windowDimensionsPropTypes, ...networkPropTypes, }; -const defaultProps = policyDefaultProps; +const defaultProps = fullPolicyDefaultProps; class WorkspaceMembersPage extends React.Component { constructor(props) { @@ -367,7 +367,7 @@ WorkspaceMembersPage.defaultProps = defaultProps; export default compose( withLocalize, withWindowDimensions, - withPolicy, + withFullPolicy, withNetwork(), withOnyx({ personalDetails: { diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 440362bb91eb..1dae4d8d5b67 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -3,6 +3,7 @@ import {ScrollView, View} from 'react-native'; import _ from 'underscore'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; +import withFullPolicy, {fullPolicyDefaultProps, fullPolicyPropTypes} from './withFullPolicy'; import * as Report from '../../libs/actions/Report'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; @@ -34,17 +35,17 @@ const propTypes = { policyID: PropTypes.string, }).isRequired, - /** List of betas available to current user */ - betas: PropTypes.arrayOf(PropTypes.string), - /** Are we loading the createPolicyRoom command */ isLoadingCreatePolicyRoom: PropTypes.bool, + ...fullPolicyPropTypes, + ...withLocalizePropTypes, }; const defaultProps = { betas: [], isLoadingCreatePolicyRoom: false, + ...fullPolicyDefaultProps, }; class WorkspaceNewRoomPage extends React.Component { @@ -202,6 +203,7 @@ WorkspaceNewRoomPage.propTypes = propTypes; WorkspaceNewRoomPage.defaultProps = defaultProps; export default compose( + withFullPolicy, withOnyx({ betas: { key: ONYXKEYS.BETAS, diff --git a/src/pages/workspace/WorkspacePageWithSections.js b/src/pages/workspace/WorkspacePageWithSections.js index 7c0ecacc0d8a..d6fc3b3a09e6 100644 --- a/src/pages/workspace/WorkspacePageWithSections.js +++ b/src/pages/workspace/WorkspacePageWithSections.js @@ -15,7 +15,7 @@ import * as BankAccounts from '../../libs/actions/BankAccounts'; import BankAccount from '../../libs/models/BankAccount'; import reimbursementAccountPropTypes from '../ReimbursementAccount/reimbursementAccountPropTypes'; import userPropTypes from '../settings/userPropTypes'; -import withPolicy from './withPolicy'; +import withFullPolicy from './withFullPolicy'; import {withNetwork} from '../../components/OnyxProvider'; import networkPropTypes from '../../components/networkPropTypes'; @@ -131,6 +131,6 @@ export default compose( key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, }), - withPolicy, + withFullPolicy, withNetwork(), )(WorkspacePageWithSections); diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 40c370abb92b..a35c1fc690a2 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -19,18 +19,18 @@ import Picker from '../../components/Picker'; import TextInput from '../../components/TextInput'; import FixedFooter from '../../components/FixedFooter'; import WorkspacePageWithSections from './WorkspacePageWithSections'; -import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; +import withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy'; import {withNetwork} from '../../components/OnyxProvider'; import OfflineWithFeedback from '../../components/OfflineWithFeedback'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; const propTypes = { - ...policyPropTypes, + ...fullPolicyPropTypes, ...withLocalizePropTypes, }; const defaultProps = { - ...policyDefaultProps, + ...fullPolicyDefaultProps, }; class WorkspaceSettingsPage extends React.Component { @@ -162,7 +162,7 @@ WorkspaceSettingsPage.propTypes = propTypes; WorkspaceSettingsPage.defaultProps = defaultProps; export default compose( - withPolicy, + withFullPolicy, withOnyx({ currencyList: {key: ONYXKEYS.CURRENCY_LIST}, }), diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.js index cead7dbc828a..053e49348872 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.js @@ -17,7 +17,7 @@ import * as Link from '../../../libs/actions/Link'; import compose from '../../../libs/compose'; import ONYXKEYS from '../../../ONYXKEYS'; import * as Policy from '../../../libs/actions/Policy'; -import withPolicy from '../withPolicy'; +import withFullPolicy from '../withFullPolicy'; import CONST from '../../../CONST'; import Button from '../../../components/Button'; import {withNetwork} from '../../../components/OnyxProvider'; @@ -302,7 +302,7 @@ class WorkspaceReimburseView extends React.Component { WorkspaceReimburseView.propTypes = propTypes; export default compose( - withPolicy, + withFullPolicy, withLocalize, withNetwork(), withOnyx({ diff --git a/src/pages/workspace/withPolicy.js b/src/pages/workspace/withFullPolicy.js similarity index 64% rename from src/pages/workspace/withPolicy.js rename to src/pages/workspace/withFullPolicy.js index 34182871eb4f..45f608d685b2 100644 --- a/src/pages/workspace/withPolicy.js +++ b/src/pages/workspace/withFullPolicy.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import React from 'react'; import PropTypes from 'prop-types'; +import Str from 'expensify-common/lib/str'; import {withOnyx} from 'react-native-onyx'; import {useNavigationState} from '@react-navigation/native'; import CONST from '../../CONST'; @@ -10,6 +11,9 @@ import * as Policy from '../../libs/actions/Policy'; import ONYXKEYS from '../../ONYXKEYS'; import policyMemberPropType from '../policyMemberPropType'; +let previousRouteName = ''; +let previousRoutePolicyID = ''; + /** * @param {Object} route * @returns {String} @@ -18,7 +22,20 @@ function getPolicyIDFromRoute(route) { return lodashGet(route, 'params.policyID', ''); } -const policyPropTypes = { +/** + * @param {String} routeName + * @param {String} policyID + * @returns {Boolean} + */ +function isPreviousRouteInSameWorkspace(routeName, policyID) { + return ( + Str.startsWith(routeName, 'Workspace') + && Str.startsWith(previousRouteName, 'Workspace') + && policyID === previousRoutePolicyID + ); +} + +const fullPolicyPropTypes = { /** The policy object for the current route */ policy: PropTypes.shape({ /** The ID of the policy */ @@ -61,12 +78,12 @@ const policyPropTypes = { policyMemberList: PropTypes.objectOf(policyMemberPropType), }; -const policyDefaultProps = { +const fullPolicyDefaultProps = { policy: {}, }; /* - * HOC for connecting a policy in Onyx corresponding to the policyID in route params + * HOC for loading a full policy. It checks the route params and if current route has a policyID that the previous route did not, it full-loads that policy. */ export default function (WrappedComponent) { const propTypes = { @@ -74,39 +91,46 @@ export default function (WrappedComponent) { * That way, if a ref is passed to a component wrapped in the HOC, the ref is a reference to the wrapped component, not the HOC. */ forwardedRef: PropTypes.func, - ...policyPropTypes, + ...fullPolicyPropTypes, }; const defaultProps = { forwardedRef: () => {}, - ...policyDefaultProps, + ...fullPolicyDefaultProps, }; - const WithPolicy = (props) => { + const WithFullPolicy = (props) => { const currentRoute = _.last(useNavigationState(state => state.routes || [])); const policyID = getPolicyIDFromRoute(currentRoute); + const isFromFullPolicy = lodashGet(props, 'policy.isFromFullPolicy', false) || lodashGet(props, `policy.policy_${policyID}.isFromFullPolicy`, false); - if (_.isString(policyID) && !_.isEmpty(policyID)) { + if (_.isString(policyID) && !_.isEmpty(policyID) && (!isFromFullPolicy || !isPreviousRouteInSameWorkspace(currentRoute.name, policyID))) { + Policy.loadFullPolicy(policyID); Policy.updateLastAccessedWorkspace(policyID); } - const rest = _.omit(props, ['forwardedRef']); + previousRouteName = currentRoute.name; + previousRoutePolicyID = policyID; + + const rest = _.omit(props, ['forwardedRef', 'policy', 'policyMemberList']); return ( ); }; - WithPolicy.propTypes = propTypes; - WithPolicy.defaultProps = defaultProps; - WithPolicy.displayName = `withFullPolicy(${getComponentDisplayName(WrappedComponent)})`; - const withPolicy = React.forwardRef((props, ref) => ( + WithFullPolicy.propTypes = propTypes; + WithFullPolicy.defaultProps = defaultProps; + WithFullPolicy.displayName = `withFullPolicy(${getComponentDisplayName(WrappedComponent)})`; + const withFullPolicy = React.forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading - + )); return withOnyx({ @@ -116,10 +140,10 @@ export default function (WrappedComponent) { policyMemberList: { key: props => `${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${getPolicyIDFromRoute(props.route)}`, }, - })(withPolicy); + })(withFullPolicy); } export { - policyPropTypes, - policyDefaultProps, + fullPolicyPropTypes, + fullPolicyDefaultProps, }; From dbde7cb6698270cdb3e3474822208f1363adba8d Mon Sep 17 00:00:00 2001 From: OSBotify Date: Tue, 13 Sep 2022 23:33:46 +0000 Subject: [PATCH 112/155] Update version to 1.2.0-1 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6d255b30bbd0..1c763bbe0322 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020000 - versionName "1.2.0-0" + versionCode 1001020001 + versionName "1.2.0-1" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index abfba4e7a42e..bdd67ee9596f 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.0 + 1.2.0.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index cee9276b44e3..ffe355408294 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.0 + 1.2.0.1 diff --git a/package-lock.json b/package-lock.json index 51d098c57b64..d5f14734b6aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-0", + "version": "1.2.0-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-0", + "version": "1.2.0-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 952b00a1de18..2c15140ec557 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-0", + "version": "1.2.0-1", "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.", From 3e90f5e54daca1ca60dd57bcca35624907e2ecc4 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 11:31:18 -0400 Subject: [PATCH 113/155] Make requested changes --- contributingGuides/STYLE.md | 2 +- jest/setup.js | 10 +- src/libs/ReportActionsUtils.js | 8 +- tests/ui/UnreadIndicatorsTest.js | 591 ++++++++++++++++--------------- tests/utils/TestHelper.js | 2 +- 5 files changed, 315 insertions(+), 298 deletions(-) diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 62e64bede72c..6402d27e2e85 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -388,7 +388,7 @@ So, if a new language feature isn't something we have agreed to support it's off Here are a couple of things we would ask that you *avoid* to help maintain consistency in our codebase: -- **Async/Await** - Use the native `Promise` instead +- **Async/Await** - Use the native `Promise` instead. Async/Await is permitted in test files only. - **Optional Chaining** - Use `lodashGet()` to fetch a nested value instead - **Null Coalescing Operator** - Use `lodashGet()` or `||` to set a default value for a possibly `undefined` or `null` variable diff --git a/jest/setup.js b/jest/setup.js index 627d43466b74..d6911480710d 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -26,11 +26,16 @@ jest.mock('@react-native-firebase/crashlytics', () => () => ({ recordError: jest.fn(), })); +// The main app uses a NativeModule called BootSplash to show/hide a splash screen. Since we can't use this in the node environment +// where tests run we simulate a behavior where the splash screen is always hidden (similar to web which has no splash screen at all). jest.mock('../src/libs/BootSplash', () => ({ hide: jest.fn(), getVisibilityStatus: jest.fn().mockResolvedValue('hidden'), })); +// Local notifications (a.k.a. browser notifications) do not run in native code. Our jest tests will also run against +// any index.native.js files as they are using a react-native plugin. However, it is useful to mock this behavior so that we +// can test the expected web behavior and see if a browser notification would be shown or not. jest.mock('../src/libs/Notification/LocalNotification', () => ({ showCommentNotification: jest.fn(), })); @@ -48,7 +53,10 @@ function mockImages(imagePath) { }); } -// Mock all images so that Icons and other assets cannot break tests +// We are mock all images so that Icons and other assets cannot break tests. In the testing environment, importing things like .svg +// directly will lead to undefined variables instead of a component or string (which is what React expects). Loading these assets is +// not required as the test environment does not actually render any UI anywhere and just needs them to noop so the test renderer +// (which is a virtual implemented DOM) can do it's thing. mockImages('images'); mockImages('images/avatars'); mockImages('images/bankicons'); diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index a86027cb35ef..157891dd8aaa 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -110,7 +110,11 @@ function getLastVisibleMessageText(reportID, actionsToMerge = {}) { const lastMessageIndex = _.findLastIndex(sortedActions, action => ( !isDeletedAction(action) )); - const htmlText = lodashGet(actions, [lastMessageIndex, 'message', 0, 'html'], ''); + if (lastMessageIndex < 0) { + return ''; + } + + const htmlText = lodashGet(sortedActions, [lastMessageIndex, 'message', 0, 'html'], ''); const messageText = parser.htmlToText(htmlText); return ReportUtils.formatReportLastMessageText(messageText); } @@ -140,7 +144,7 @@ function getNewLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = return 0; } - return actions[lastMessageIndex].sequenceNumber; + return sortedActions[lastMessageIndex].sequenceNumber; } export { diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index d26b6bae09d5..cc0defa51e62 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -20,6 +20,11 @@ import LocalNotification from '../../src/libs/Notification/LocalNotification'; import * as Report from '../../src/libs/actions/Report'; beforeAll(() => { + // In this test, we are generically mocking the responses of all API requests by mocking fetch() and having it + // return 200. In other tests, we might mock HttpUtils.xhr() with a more specific mock data response (which means + // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling + // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to + // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. global.fetch = TestHelper.getGlobalFetchMock(); // We need a bit more time for this test in some places @@ -70,7 +75,7 @@ function isNewMessagesBadgeVisible(renderedApp) { */ async function navigateToSidebar(renderedApp) { const reportHeader = renderedApp.getByTestId('report-header'); - const reportHeaderBackButton = reportHeader.findByType(ReportHeaderViewBackButton); + const reportHeaderBackButton = await reportHeader.findByType(ReportHeaderViewBackButton); fireEvent(reportHeaderBackButton, 'press'); return waitForPromisesToResolve(); } @@ -80,10 +85,10 @@ async function navigateToSidebar(renderedApp) { * @param {Number} index * @return {Promise} */ -async function navigateToSidebarOption(renderedApp, index) { +function navigateToSidebarOption(renderedApp, index) { const optionRows = renderedApp.getAllByTestId('option-row'); fireEvent(optionRows[index], 'press'); - await waitForPromisesToResolve(); + return waitForPromisesToResolve(); } /** @@ -95,301 +100,301 @@ function isDrawerOpen(renderedApp) { return reportScreen.findByType(ReportActionsView).props.isDrawerOpen; } -test('Unread LHN Status, “New Messages” badge, and New Line Indicator', async () => { - const REPORT_ID = 1; - const USER_A_ACCOUNT_ID = 1; - const USER_A_EMAIL = 'user_a@test.com'; - const USER_B_ACCOUNT_ID = 2; - const USER_B_EMAIL = 'user_b@test.com'; - const USER_C_ACCOUNT_ID = 3; - const USER_C_EMAIL = 'user_c@test.com'; - - // Render the App and sign in as a test user. - const renderedApp = render(); - - // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some - // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act - await act(async () => { +describe('Unread Indicators', () => { + it('Shows correct LHN Status, “New Messages” badge, and New Line Indicators', async () => { + const REPORT_ID = 1; + const USER_A_ACCOUNT_ID = 1; + const USER_A_EMAIL = 'user_a@test.com'; + const USER_B_ACCOUNT_ID = 2; + const USER_B_EMAIL = 'user_b@test.com'; + const USER_C_ACCOUNT_ID = 3; + const USER_C_EMAIL = 'user_c@test.com'; + + // Render the App and sign in as a test user. + const renderedApp = render(); + + // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some + // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act + await act(async () => { + await waitForPromisesToResolve(); + }); + + const loginForm = renderedApp.queryAllByTestId('login-form'); + expect(loginForm.length).toBe(1); + + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + + const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + + // Simulate setting an unread report and personal details + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 9, + lastReadSequenceNumber: 1, + lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), + lastMessageText: 'Test', + participants: [USER_B_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: MOMENT_TEN_MINUTES_AGO.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), + 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), + 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), + 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), + 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), + 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), + 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), + 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), + 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); await waitForPromisesToResolve(); + + // Verify no notifications are created for these older messages + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); + + // Verify the sidebar links are rendered + const sidebarLinks = renderedApp.queryAllByTestId('sidebar-links'); + expect(sidebarLinks.length).toBe(1); + + // And verify that the Report screen is rendered after manually setting the sidebar as loaded + // since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(true); await waitForPromisesToResolve(); - }); - const loginForm = renderedApp.getByTestId('login-form'); - expect(loginForm).toBeTruthy(); + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Verify there is only one option in the sidebar + let optionRows = renderedApp.getAllByTestId('option-row'); + expect(optionRows.length).toBe(1); + + // And that the text is bold + const displayNameText = renderedApp.getByTestId('option-row-display-name'); + expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + await navigateToSidebarOption(renderedApp, 0); + + // Verify that the report screen is rendered and the drawer is closed + expect(isDrawerOpen(renderedApp)).toBe(false); + + // That the report actions are visible along with the created action + const createdAction = renderedApp.getByTestId('report-action-created'); + expect(createdAction).toBeTruthy(); + const reportComments = renderedApp.getAllByTestId('report-action-item'); + expect(reportComments.length).toBe(9); + + // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will + // have a sequenceNumber of 2 + let unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(2); + + // Scroll up and verify that the "New messages" badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) + const updatedDisplayNameText = renderedApp.getByTestId('option-row-display-name'); + expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + + // Tap on the back button to return to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the LHN is now open + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Navigate to the report again + await navigateToSidebarOption(renderedApp, 0); + + // Verify the unread indicator is no longer present + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Scroll and verify that the new messages badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + + // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant + const NEW_REPORT_ID = 2; + const NEW_REPORT_CREATED_MOMENT = moment(); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { + reportID: NEW_REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 1, + lastReadSequenceNumber: 0, + lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), + lastMessageText: 'Comment 1', + participants: [USER_C_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: NEW_REPORT_CREATED_MOMENT.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorEmail: USER_C_EMAIL, + person: [{type: 'TEXT', style: 'strong', text: 'User C'}], + sequenceNumber: 1, + timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), + message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], + reportActionID: NumberUtils.rand64(), + }, + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + }); + await waitForPromisesToResolve(); - TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); - await waitForPromisesToResolve(); + // Verify notification was created as the new message that has arrived is very recent + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); - const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + // Navigate back to the sidebar + await navigateToSidebar(renderedApp); - // Simulate setting an unread report and personal details - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 9, - lastReadSequenceNumber: 1, - lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), // Ten minutes ago - lastMessageText: 'Test', - participants: [USER_B_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: MOMENT_TEN_MINUTES_AGO.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), - 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), - 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), - 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), - 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), - 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), - 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), - 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), - 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); - await waitForPromisesToResolve(); - - // Verify no notifications are created for these older messages - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); - - // Verify the sidebar links are rendered - const sidebarLinks = renderedApp.getByTestId('sidebar-links'); - expect(sidebarLinks).toBeTruthy(); - - // And verify that the Report screen is rendered after manually setting the sidebar as loaded - // since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(true); - await waitForPromisesToResolve(); - - expect(isDrawerOpen(renderedApp)).toBe(true); - - // Verify there is only one option in the sidebar - let optionRows = renderedApp.getAllByTestId('option-row'); - expect(optionRows.length).toBe(1); - - // And that the text is bold - const displayNameText = renderedApp.getByTestId('option-row-display-name'); - expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - await navigateToSidebarOption(renderedApp, 0); - - // Verify that the report screen is rendered and the drawer is closed - expect(isDrawerOpen(renderedApp)).toBe(false); - - // That the report actions are visible along with the created action - const createdAction = renderedApp.getByTestId('report-action-created'); - expect(createdAction).toBeTruthy(); - const reportComments = renderedApp.getAllByTestId('report-action-item'); - expect(reportComments.length).toBe(9); - - // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will - // have a sequenceNumber of 2 - let unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); - let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); - expect(sequenceNumber).toBe(2); - - // Scroll up and verify that the "New messages" badge appears - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); - - // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByTestId('option-row-display-name'); - expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - - // Tap on the back button to return to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the LHN is now open - expect(isDrawerOpen(renderedApp)).toBe(true); - - // Navigate to the report again - await navigateToSidebarOption(renderedApp, 0); - - // Verify the unread indicator is no longer present - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(0); - expect(isDrawerOpen(renderedApp)).toBe(false); - - // Scroll and verify that the new messages badge is hidden - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); - - // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant - const NEW_REPORT_ID = 2; - const NEW_REPORT_CREATED_MOMENT = moment(); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { - reportID: NEW_REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 1, - lastReadSequenceNumber: 0, - lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), - lastMessageText: 'Comment 1', - participants: [USER_C_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: NEW_REPORT_CREATED_MOMENT.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: { - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - actorEmail: USER_C_EMAIL, - person: [{type: 'TEXT', style: 'strong', text: 'User C'}], - sequenceNumber: 1, - timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), - message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], - reportActionID: NumberUtils.rand64(), - }, - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), - }); - await waitForPromisesToResolve(); - - // Verify notification was created as the new message that has arrived is very recent - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); - - // Navigate back to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the new report option appears in the LHN - optionRows = renderedApp.getAllByTestId('option-row'); - expect(optionRows.length).toBe(2); - - // Verify the text for the new chat is bold and above the previous indicating it has not yet been read - let displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - const firstReportOption = displayNameTexts[0]; - expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); - - const secondReportOption = displayNameTexts[1]; - expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); - - // Tap the new report option and navigate back to the sidebar again via the back button - await navigateToSidebarOption(renderedApp, 0); - - // Verify that all report options appear in a "read" state - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - - // Tap the previous report between User A and User B - await navigateToSidebarOption(renderedApp, 1); - - // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then - // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly - Report.markCommentAsUnread(REPORT_ID, 3); - await waitForPromisesToResolve(); - - // Verify the indicator appears above the last action - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); - sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); - expect(sequenceNumber).toBe(3); - - // Scroll up and verify the new messages badge appears - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); - - // Navigate to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the report is marked as unread in the sidebar - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - - // Navigate to the report again and back to the sidebar - await navigateToSidebarOption(renderedApp, 1); - await navigateToSidebar(renderedApp); - - // Verify the report is now marked as read - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - - // Navigate to the report again and verify the new line indicator is missing - await navigateToSidebarOption(renderedApp, 1); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(0); - - // Scroll up and verify the badge is hidden - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); - expect(isDrawerOpen(renderedApp)).toBe(false); - - // Navigate to the LHN - await navigateToSidebar(renderedApp); - - // Simulate another new message on the report with User B - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 10: TestHelper.buildTestReportComment(USER_B_EMAIL, 10, moment().unix()), + // Verify the new report option appears in the LHN + optionRows = renderedApp.getAllByTestId('option-row'); + expect(optionRows.length).toBe(2); + + // Verify the text for the new chat is bold and above the previous indicating it has not yet been read + let displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + const firstReportOption = displayNameTexts[0]; + expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); + + const secondReportOption = displayNameTexts[1]; + expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); + + // Tap the new report option and navigate back to the sidebar again via the back button + await navigateToSidebarOption(renderedApp, 0); + + // Verify that all report options appear in a "read" state + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Tap the previous report between User A and User B + await navigateToSidebarOption(renderedApp, 1); + + // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then + // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly + Report.markCommentAsUnread(REPORT_ID, 3); + await waitForPromisesToResolve(); + + // Verify the indicator appears above the last action + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(3); + + // Scroll up and verify the new messages badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // Navigate to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the report is marked as unread in the sidebar + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and back to the sidebar + await navigateToSidebarOption(renderedApp, 1); + await navigateToSidebar(renderedApp); + + // Verify the report is now marked as read + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and verify the new line indicator is missing + await navigateToSidebarOption(renderedApp, 1); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // Scroll up and verify the badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Navigate to the LHN + await navigateToSidebar(renderedApp); + + // Simulate another new message on the report with User B + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 10: TestHelper.buildTestReportComment(USER_B_EMAIL, 10, moment().unix()), + }); + await waitForPromisesToResolve(); + + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the report again and verify the indicator exists + await navigateToSidebarOption(renderedApp, 1); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + + // Leave a comment as the current user and verify the indicator is removed + Report.addComment(REPORT_ID, 'Current User Comment 1'); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // Mark a previous comment as unread and verify the unread action indicator returns + Report.markCommentAsUnread(REPORT_ID, 9); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + + // Trigger the app going inactive and active again + AppState.emitCurrentTestState('background'); + AppState.emitCurrentTestState('active'); + + // Verify the new line is cleared + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // As the current user add several comments + Report.addComment(REPORT_ID, 'Current User Comment 2'); + await waitForPromisesToResolve(); + + Report.addComment(REPORT_ID, 'Current User Comment 3'); + await waitForPromisesToResolve(); + + Report.addComment(REPORT_ID, 'Current User Comment 4'); + await waitForPromisesToResolve(); + + // Mark the last comment as "unread" and verify the unread indicator appears + Report.markCommentAsUnread(REPORT_ID, 14); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); }); - await waitForPromisesToResolve(); - - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - // Navigate to the report again and verify the indicator exists - await navigateToSidebarOption(renderedApp, 1); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); - - // Leave a comment as the current user and verify the indicator is removed - Report.addComment(REPORT_ID, 'Current User Comment 1'); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(0); - - // Mark a previous comment as unread and verify the unread action indicator returns - Report.markCommentAsUnread(REPORT_ID, 9); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); - - // Trigger the app going inactive and active again - AppState.emitCurrentTestState('background'); - AppState.emitCurrentTestState('active'); - - // Verify the new line is cleared - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(0); - - // As the current user add several comments - Report.addComment(REPORT_ID, 'Current User Comment 2'); - await waitForPromisesToResolve(); - - Report.addComment(REPORT_ID, 'Current User Comment 3'); - await waitForPromisesToResolve(); - - Report.addComment(REPORT_ID, 'Current User Comment 4'); - await waitForPromisesToResolve(); - - // Mark the last comment as "unread" and verify the unread indicator appears - Report.markCommentAsUnread(REPORT_ID, 14); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 319732eca13e..eea6ae43461f 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -11,7 +11,7 @@ import * as NumberUtils from '../../src/libs/NumberUtils'; * @param {String} login * @param {Number} accountID * @param {String} [firstName] - * @returns {Promise} + * @returns {Object} */ function buildPersonalDetails(login, accountID, firstName = 'Test') { const avatar = ReportUtils.getDefaultAvatar(login); From 973de5c964596501d6adf4bcf900dbcb699817bb Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 12:02:02 -0400 Subject: [PATCH 114/155] Remove docs from react-navigation setup and link to react-navigation docs --- jest/setup.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index d6911480710d..5c84427e5205 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -5,16 +5,13 @@ import _ from 'underscore'; require('react-native-reanimated/lib/reanimated2/jestUtils').setUpTests(); -// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing -jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); - jest.mock('react-native-blob-util', () => ({})); +// These two mocks are required as per setup instructions for react-navigation testing +// https://reactnavigation.org/docs/testing/#mocking-native-modules +jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); jest.mock('react-native-reanimated', () => { const Reanimated = require('react-native-reanimated/mock'); - - // The mock for `call` immediately calls the callback which is incorrect - // So we override it with a no-op Reanimated.default.call = () => {}; return Reanimated; }); From 261c712e1b8118de955bd31e6cf5b070dd285b3d Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 12:38:59 -0400 Subject: [PATCH 115/155] Clean up all testID and use accessibilityHint or accessibilityLabel instead --- src/components/DisplayNames/index.native.js | 2 +- src/components/OptionRow.js | 5 +- src/components/OptionsList/BaseOptionsList.js | 2 +- src/components/Text.js | 4 -- src/components/UnreadActionIndicator.js | 2 +- src/pages/home/HeaderView.js | 2 +- src/pages/home/ReportHeaderViewBackButton.js | 1 + src/pages/home/ReportScreen.js | 2 +- .../FloatingMessageCounterContainer/index.js | 5 +- .../report/FloatingMessageCounter/index.js | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- .../home/report/ReportActionItemCreated.js | 2 +- src/pages/home/report/ReportActionsList.js | 3 +- src/pages/home/sidebar/SidebarLinks.js | 3 +- src/pages/signin/LoginForm.js | 2 +- tests/ui/UnreadIndicatorsTest.js | 56 +++++++++---------- 16 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/components/DisplayNames/index.native.js b/src/components/DisplayNames/index.native.js index 91ed0406a0d7..743a8036b965 100644 --- a/src/components/DisplayNames/index.native.js +++ b/src/components/DisplayNames/index.native.js @@ -4,7 +4,7 @@ import Text from '../Text'; // As we don't have to show tooltips of the Native platform so we simply render the full display names list. const DisplayNames = props => ( - + {props.fullTitle} ); diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index 40471941aba4..e408387b1a3f 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -148,7 +148,7 @@ const OptionRow = (props) => { props.isDisabled && styles.cursorDisabled, ]} > - + { } { /> {props.option.alternateText ? ( diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index e5524c49dab5..c9f983a9720f 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -166,6 +166,7 @@ class BaseOptionsList extends Component { renderItem({item, index, section}) { return ( ) : null} ( - + {props.translate('common.new')} diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index ea5db9406a7e..6e5b625e7491 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -91,7 +91,7 @@ const HeaderView = (props) => { const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies); const brickRoadIndicator = ReportUtils.hasReportNameError(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; return ( - + {props.isSmallScreenWidth && ( ( diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 4e43bb06956c..7ac2117071c0 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -237,7 +237,7 @@ class ReportScreen extends React.Component { /> this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js index a367eea5f602..91ad8203ce81 100644 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js +++ b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js @@ -4,7 +4,10 @@ import styles from '../../../../../styles/styles'; import floatingMessageCounterContainerPropTypes from './floatingMessageCounterContainerPropTypes'; const FloatingMessageCounterContainer = props => ( - + {props.children} ); diff --git a/src/pages/home/report/FloatingMessageCounter/index.js b/src/pages/home/report/FloatingMessageCounter/index.js index 8af8266b0a87..9cfb9ba5818f 100644 --- a/src/pages/home/report/FloatingMessageCounter/index.js +++ b/src/pages/home/report/FloatingMessageCounter/index.js @@ -62,7 +62,7 @@ class FloatingMessageCounter extends PureComponent { render() { return ( - + {hovered => ( - + {this.props.shouldDisplayNewIndicator && ( )} diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js index 10b5fcd31f5e..93bc71a3eab6 100644 --- a/src/pages/home/report/ReportActionItemCreated.js +++ b/src/pages/home/report/ReportActionItemCreated.js @@ -39,7 +39,7 @@ const ReportActionItemCreated = (props) => { const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies); return ( + - + this.input = el} label={this.props.translate('loginForm.phoneOrEmail')} diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index cc0defa51e62..d13953880d91 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -13,7 +13,6 @@ import * as TestHelper from '../utils/TestHelper'; import appSetup from '../../src/setup'; import fontWeightBold from '../../src/styles/fontWeight/bold'; import * as AppActions from '../../src/libs/actions/App'; -import ReportHeaderViewBackButton from '../../src/pages/home/ReportHeaderViewBackButton'; import ReportActionsView from '../../src/pages/home/report/ReportActionsView'; import * as NumberUtils from '../../src/libs/NumberUtils'; import LocalNotification from '../../src/libs/Notification/LocalNotification'; @@ -37,7 +36,7 @@ beforeAll(() => { * @param {RenderAPI} renderedApp */ function scrollUpToRevealNewMessagesBadge(renderedApp) { - fireEvent.scroll(renderedApp.getByTestId('report-actions-list'), { + fireEvent.scroll(renderedApp.getByA11yLabel('List of chat messages'), { nativeEvent: { contentOffset: { y: 250, @@ -65,7 +64,7 @@ function scrollUpToRevealNewMessagesBadge(renderedApp) { * @return {Boolean} */ function isNewMessagesBadgeVisible(renderedApp) { - const badge = renderedApp.getByTestId('new-messages-badge'); + const badge = renderedApp.getByA11yHint('Scroll to newest messages'); return badge.props.style.transform[0].translateY === 10; } @@ -74,8 +73,7 @@ function isNewMessagesBadgeVisible(renderedApp) { * @return {Promise} */ async function navigateToSidebar(renderedApp) { - const reportHeader = renderedApp.getByTestId('report-header'); - const reportHeaderBackButton = await reportHeader.findByType(ReportHeaderViewBackButton); + const reportHeaderBackButton = renderedApp.getByA11yHint('Navigate back to chats list'); fireEvent(reportHeaderBackButton, 'press'); return waitForPromisesToResolve(); } @@ -86,7 +84,7 @@ async function navigateToSidebar(renderedApp) { * @return {Promise} */ function navigateToSidebarOption(renderedApp, index) { - const optionRows = renderedApp.getAllByTestId('option-row'); + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); fireEvent(optionRows[index], 'press'); return waitForPromisesToResolve(); } @@ -96,7 +94,7 @@ function navigateToSidebarOption(renderedApp, index) { * @return {Boolean} */ function isDrawerOpen(renderedApp) { - const reportScreen = renderedApp.getByTestId('report-screen'); + const reportScreen = renderedApp.getByA11yLabel('Main chat area'); return reportScreen.findByType(ReportActionsView).props.isDrawerOpen; } @@ -119,7 +117,7 @@ describe('Unread Indicators', () => { await waitForPromisesToResolve(); }); - const loginForm = renderedApp.queryAllByTestId('login-form'); + const loginForm = renderedApp.queryAllByA11yLabel('Login form'); expect(loginForm.length).toBe(1); await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); @@ -163,7 +161,7 @@ describe('Unread Indicators', () => { expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); // Verify the sidebar links are rendered - const sidebarLinks = renderedApp.queryAllByTestId('sidebar-links'); + const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); expect(sidebarLinks.length).toBe(1); // And verify that the Report screen is rendered after manually setting the sidebar as loaded @@ -174,11 +172,11 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(true); // Verify there is only one option in the sidebar - let optionRows = renderedApp.getAllByTestId('option-row'); + let optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); expect(optionRows.length).toBe(1); // And that the text is bold - const displayNameText = renderedApp.getByTestId('option-row-display-name'); + const displayNameText = renderedApp.getByA11yLabel('Chat user display names'); expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); await navigateToSidebarOption(renderedApp, 0); @@ -187,14 +185,14 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(false); // That the report actions are visible along with the created action - const createdAction = renderedApp.getByTestId('report-action-created'); + const createdAction = renderedApp.getByA11yLabel('Chat welcome message'); expect(createdAction).toBeTruthy(); - const reportComments = renderedApp.getAllByTestId('report-action-item'); + const reportComments = renderedApp.getAllByA11yLabel('Chat message'); expect(reportComments.length).toBe(9); // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will // have a sequenceNumber of 2 - let unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(2); @@ -204,7 +202,7 @@ describe('Unread Indicators', () => { expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByTestId('option-row-display-name'); + const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); // Tap on the back button to return to the sidebar @@ -217,7 +215,7 @@ describe('Unread Indicators', () => { await navigateToSidebarOption(renderedApp, 0); // Verify the unread indicator is no longer present - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); expect(isDrawerOpen(renderedApp)).toBe(false); @@ -267,11 +265,11 @@ describe('Unread Indicators', () => { await navigateToSidebar(renderedApp); // Verify the new report option appears in the LHN - optionRows = renderedApp.getAllByTestId('option-row'); + optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); expect(optionRows.length).toBe(2); // Verify the text for the new chat is bold and above the previous indicating it has not yet been read - let displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); const firstReportOption = displayNameTexts[0]; expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); @@ -285,7 +283,7 @@ describe('Unread Indicators', () => { await navigateToSidebarOption(renderedApp, 0); // Verify that all report options appear in a "read" state - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); @@ -301,7 +299,7 @@ describe('Unread Indicators', () => { await waitForPromisesToResolve(); // Verify the indicator appears above the last action - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(3); @@ -314,7 +312,7 @@ describe('Unread Indicators', () => { await navigateToSidebar(renderedApp); // Verify the report is marked as unread in the sidebar - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); @@ -324,7 +322,7 @@ describe('Unread Indicators', () => { await navigateToSidebar(renderedApp); // Verify the report is now marked as read - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); @@ -332,7 +330,7 @@ describe('Unread Indicators', () => { // Navigate to the report again and verify the new line indicator is missing await navigateToSidebarOption(renderedApp, 1); await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); // Scroll up and verify the badge is hidden @@ -349,7 +347,7 @@ describe('Unread Indicators', () => { }); await waitForPromisesToResolve(); - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); @@ -358,19 +356,19 @@ describe('Unread Indicators', () => { // Navigate to the report again and verify the indicator exists await navigateToSidebarOption(renderedApp, 1); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); // Leave a comment as the current user and verify the indicator is removed Report.addComment(REPORT_ID, 'Current User Comment 1'); await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); // Mark a previous comment as unread and verify the unread action indicator returns Report.markCommentAsUnread(REPORT_ID, 9); await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); // Trigger the app going inactive and active again @@ -378,7 +376,7 @@ describe('Unread Indicators', () => { AppState.emitCurrentTestState('active'); // Verify the new line is cleared - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); // As the current user add several comments @@ -394,7 +392,7 @@ describe('Unread Indicators', () => { // Mark the last comment as "unread" and verify the unread indicator appears Report.markCommentAsUnread(REPORT_ID, 14); await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); }); }); From ac9fe71febeec7b2e95575a2c0b370d3d9e7de28 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 12:49:39 -0400 Subject: [PATCH 116/155] Use accessibilityElementsHidden --- src/pages/home/ReportScreen.js | 1 - src/pages/home/sidebar/SidebarLinks.js | 10 +++++++++- tests/ui/UnreadIndicatorsTest.js | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 7ac2117071c0..92b334ebabb6 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -237,7 +237,6 @@ class ReportScreen extends React.Component { /> this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index a3250b6d4f4f..c6c905a2030d 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -26,6 +26,8 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import * as App from '../../../libs/actions/App'; import * as ReportUtils from '../../../libs/ReportUtils'; import withCurrentUserPersonalDetails from '../../../components/withCurrentUserPersonalDetails'; +import withDrawerState from '../../../components/withDrawerState'; +import withWindowDimensions from '../../../components/withWindowDimensions'; const propTypes = { /** Toggles the navigation menu open and closed */ @@ -253,7 +255,11 @@ class SidebarLinks extends React.Component { }]; return ( - + { From 68fed0c20d29468cf248612bb2f3ef3e7882de19 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 12:54:59 -0400 Subject: [PATCH 117/155] Undo unnecessary refactor of back button --- src/pages/home/HeaderView.js | 14 +++++---- src/pages/home/ReportHeaderViewBackButton.js | 31 -------------------- 2 files changed, 9 insertions(+), 36 deletions(-) delete mode 100644 src/pages/home/ReportHeaderViewBackButton.js diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 6e5b625e7491..69a8b524c263 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -26,7 +26,6 @@ import Text from '../../components/Text'; import Tooltip from '../../components/Tooltip'; import variables from '../../styles/variables'; import colors from '../../styles/colors'; -import ReportHeaderViewBackButton from './ReportHeaderViewBackButton'; const propTypes = { /** Toggles the navigationMenu open and closed */ @@ -94,10 +93,15 @@ const HeaderView = (props) => { {props.isSmallScreenWidth && ( - + + + + + )} {Boolean(props.report && title) && ( ( - - - - - -); - -ReportHeaderViewBackButton.displayName = 'ReportHeaderViewBackButton'; -ReportHeaderViewBackButton.propTypes = propTypes; -export default ReportHeaderViewBackButton; From 07df0b275e542ad4ff1044c0f757bd1ccea40258 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Wed, 14 Sep 2022 23:42:00 +0530 Subject: [PATCH 118/155] Address review - Fix wrong code --- src/libs/actions/Session/index.js | 2 +- src/pages/signin/PasswordForm.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index ec2661a13a1b..3ee2a8afc3aa 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -348,7 +348,7 @@ function setPassword(password, validateCode, accountID) { } // This request can fail if the password is not complex enough - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds]: response.message}}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: response.message}}); }) .finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js index c70f25633121..fd6400031f74 100755 --- a/src/pages/signin/PasswordForm.js +++ b/src/pages/signin/PasswordForm.js @@ -178,7 +178,7 @@ class PasswordForm extends React.Component { )} - {!this.state.formError && this.props.account && !_.isEmpty(this.props.account.error) && ( + {!this.state.formError && this.props.account && !_.isEmpty(this.props.account.errors) && ( {ErrorUtils.getLatestErrorMessage(this.props.account)} From bab2d0b853c1ac07290fa45421924d9eb75e4c34 Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Wed, 14 Sep 2022 11:48:35 -0700 Subject: [PATCH 119/155] Empty PR to CP to staging, to trigger new staging deploy From f4392a51e5be969ad86c0bee4746ce4e0831345c Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 14 Sep 2022 20:16:53 +0000 Subject: [PATCH 120/155] Update version to 1.2.0-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1c763bbe0322..79bde43a3489 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020001 - versionName "1.2.0-1" + versionCode 1001020002 + versionName "1.2.0-2" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index bdd67ee9596f..3822af1db3c1 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.1 + 1.2.0.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index ffe355408294..822157be53fe 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.1 + 1.2.0.2 diff --git a/package-lock.json b/package-lock.json index 1d2e6ea45ed5..781d6fe121e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-1", + "version": "1.2.0-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-1", + "version": "1.2.0-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 297038eb0441..6483e40e3cab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-1", + "version": "1.2.0-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.", From efc144b57a68e3263c71be5b896d8de14f572817 Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Wed, 14 Sep 2022 13:32:22 -0700 Subject: [PATCH 121/155] Dummy change to allow new staging CP --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb3a256efd22..20d4921fc261 100644 --- a/README.md +++ b/README.md @@ -329,7 +329,7 @@ This application is built with the following principles. 1. **Cross Platform 99.9999%** 1. A feature isn't done until it works on all platforms. Accordingly, don't even bother writing a platform-specific code block because you're just going to need to undo it. - 1. If the reason you can't write cross platform code is because there is a bug in ReactNative that is preventing it from working, the correct action is to fix RN and submit a PR upstream -- not to hack around RN bugs with platform-specific code paths. + 1. If the reason you can't write cross-platform code is because there is a bug in ReactNative that is preventing it from working, the correct action is to fix RN and submit a PR upstream -- not to hack around RN bugs with platform-specific code paths. 1. If there is a feature that simply doesn't exist on all platforms and thus doesn't exist in RN, rather than doing if (platform=iOS) { }, instead write a "shim" library that is implemented with NOOPs on the other platforms. For example, rather than injecting platform-specific multi-tab code (which can only work on browsers, because it's the only platform with multiple tabs), write a TabManager class that just is NOOP for non-browser platforms. This encapsulates the platform-specific code into a platform library, rather than sprinkling through the business logic. 1. Put all platform specific code in dedicated files and folders, like /platform, and reject any PR that attempts to put platform-specific code anywhere else. This maintains a strict separation between business logic and platform code. From 71a75717835b8ad3758baf1988b82a5c74e8db4b Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 14 Sep 2022 20:40:45 +0000 Subject: [PATCH 122/155] Update version to 1.2.0-3 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 79bde43a3489..ce4081548358 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020002 - versionName "1.2.0-2" + versionCode 1001020003 + versionName "1.2.0-3" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 3822af1db3c1..87d627262c98 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.2 + 1.2.0.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 822157be53fe..2cb28eee4959 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.2 + 1.2.0.3 diff --git a/package-lock.json b/package-lock.json index 781d6fe121e5..d84fad11ceaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-2", + "version": "1.2.0-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-2", + "version": "1.2.0-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6483e40e3cab..4b41be8d1189 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-2", + "version": "1.2.0-3", "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.", From ab9fb29779fd252c0ee3bfaa58d50d9b0a29e981 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 14 Sep 2022 15:12:52 -0600 Subject: [PATCH 123/155] add errorutils --- src/pages/ReimbursementAccount/ReimbursementAccountForm.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js index f7e29bdd2ced..5b44e4b55571 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js @@ -13,6 +13,7 @@ import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButto import CONST from '../../CONST'; import FormScrollView from '../../components/FormScrollView'; import * as BankAccounts from '../../libs/actions/BankAccounts'; +import * as ErrorUtils from '../../libs/ErrorUtils'; const propTypes = { /** Data for the bank account actively being set up */ @@ -58,7 +59,7 @@ class ReimbursementAccountForm extends React.Component { onFixTheErrorsLinkPressed={() => { this.form.scrollTo({y: 0, animated: true}); }} - message={this.props.reimbursementAccount.error || _.values(this.props.reimbursementAccount.errors)[0]} + message={this.props.reimbursementAccount.error || ErrorUtils.getLastestErrorMessage(this.props.reimbursementAccount.errors)} isMessageHtml={this.props.reimbursementAccount.isErrorHtml} isLoading={this.props.reimbursementAccount.loading || this.props.reimbursementAccount.isLoading} /> From f6dd0d7b8da5b9f2aedce6a6e23faa401842e4f8 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 14 Sep 2022 15:15:03 -0600 Subject: [PATCH 124/155] fix function name --- src/pages/ReimbursementAccount/ReimbursementAccountForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js index 5b44e4b55571..3542131a5710 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js @@ -59,7 +59,7 @@ class ReimbursementAccountForm extends React.Component { onFixTheErrorsLinkPressed={() => { this.form.scrollTo({y: 0, animated: true}); }} - message={this.props.reimbursementAccount.error || ErrorUtils.getLastestErrorMessage(this.props.reimbursementAccount.errors)} + message={this.props.reimbursementAccount.error || ErrorUtils.getLatestErrorMessage(this.props.reimbursementAccount.errors)} isMessageHtml={this.props.reimbursementAccount.isErrorHtml} isLoading={this.props.reimbursementAccount.loading || this.props.reimbursementAccount.isLoading} /> From 3c097924a3403d1db36f3e8fded9b668f36143b3 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Wed, 14 Sep 2022 15:56:47 -0600 Subject: [PATCH 125/155] use correct error path, create getErrorMessage --- src/pages/ReimbursementAccount/ReimbursementAccountForm.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js index 3542131a5710..48c24e620a29 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js @@ -34,6 +34,11 @@ class ReimbursementAccountForm extends React.Component { BankAccounts.resetReimbursementAccount(); } + getErrorMessage() { + const latestErrorMessage = ErrorUtils.getLatestErrorMessage(this.props.reimbursementAccount); + return this.props.reimbursementAccount.error || (typeof latestErrorMessage === 'string' ? latestErrorMessage : ''); + } + render() { const isErrorVisible = _.size(lodashGet(this.props, 'reimbursementAccount.errors', {})) > 0 || lodashGet(this.props, 'reimbursementAccount.error', '').length > 0; @@ -59,7 +64,7 @@ class ReimbursementAccountForm extends React.Component { onFixTheErrorsLinkPressed={() => { this.form.scrollTo({y: 0, animated: true}); }} - message={this.props.reimbursementAccount.error || ErrorUtils.getLatestErrorMessage(this.props.reimbursementAccount.errors)} + message={this.getErrorMessage()} isMessageHtml={this.props.reimbursementAccount.isErrorHtml} isLoading={this.props.reimbursementAccount.loading || this.props.reimbursementAccount.isLoading} /> From fc4f8c6b9facd6af9a34711f43f622be8cf59e21 Mon Sep 17 00:00:00 2001 From: Jules Rosser Date: Thu, 15 Sep 2022 11:27:14 +0100 Subject: [PATCH 126/155] do not pass default avatar when building option --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 2ab2d89ee9c8..b56c08f7ca5a 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -349,7 +349,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { result.text = reportName; result.subtitle = subtitle; result.participantsList = personalDetailList; - result.icons = ReportUtils.getIcons(report, personalDetails, policies, personalDetail.avatar); + result.icons = ReportUtils.getIcons(report, personalDetails, policies); result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom || result.isPolicyExpenseChat); return result; From 53c53a69d0fff8dc58aae580cf384ac75c3f0697 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 15 Sep 2022 10:44:15 +0000 Subject: [PATCH 127/155] Update version to 1.2.0-4 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ce4081548358..b683c8e32809 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020003 - versionName "1.2.0-3" + versionCode 1001020004 + versionName "1.2.0-4" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 87d627262c98..cea7a6ce044c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.3 + 1.2.0.4 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 2cb28eee4959..3a11abf9877f 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.3 + 1.2.0.4 diff --git a/package-lock.json b/package-lock.json index d84fad11ceaa..85d48adab0bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-3", + "version": "1.2.0-4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-3", + "version": "1.2.0-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4b41be8d1189..af9bb8549e34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-3", + "version": "1.2.0-4", "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.", From a8bc73441f747f054bab73309777d5639295cffc Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Thu, 15 Sep 2022 15:09:53 +0100 Subject: [PATCH 128/155] Update param type --- src/libs/actions/Wallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index a4e75f49d010..262eee68e9f9 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -529,7 +529,7 @@ function updateCurrentStep(currentStep) { } /** - * @param {Object} answers + * @param {Array} answers * @param {String} idNumber */ function answerQuestionsForWallet(answers, idNumber) { From 5ed68a88b722a944020c1739ed53bf17388ecc19 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 15 Sep 2022 09:13:47 -0600 Subject: [PATCH 129/155] rm unused consts --- src/CONST.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index dc1a653bcd4e..c0b07509bd72 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -52,8 +52,6 @@ const CONST = { MAX_ROUTING_NUMBER: '402 Maximum Size Exceeded routingNumber', MISSING_INCORPORATION_STATE: '402 Missing incorporationState in additionalData', MISSING_INCORPORATION_TYPE: '402 Missing incorporationType in additionalData', - MAX_VALIDATION_ATTEMPTS_REACHED: 'Validation for this bank account has been disabled due to too many incorrect attempts. Please contact us.', - INCORRECT_VALIDATION_AMOUNTS: 'The validate code you entered is incorrect, please try again.', }, STEP: { // In the order they appear in the VBA flow From 37060d0349a1912d0b378134b019d046409bef91 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Thu, 15 Sep 2022 17:27:11 +0200 Subject: [PATCH 130/155] Improve logging for api requests --- src/libs/Middleware/Logging.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/libs/Middleware/Logging.js b/src/libs/Middleware/Logging.js index fe63e16291ad..4586eeb2a8a5 100644 --- a/src/libs/Middleware/Logging.js +++ b/src/libs/Middleware/Logging.js @@ -58,7 +58,7 @@ function Logging(response, request) { // Cancelled requests are normal and can happen when a user logs out. No extra handling is needed here besides // remove the request from the PersistedRequests if the request exists. if (error.name === CONST.ERROR.REQUEST_CANCELLED) { - Log.info('[Network] Error: Request canceled', false, request); + Log.info('[Network] API request error: Request canceled', false, request); if (persisted) { PersistedRequests.remove(request); } @@ -66,11 +66,17 @@ function Logging(response, request) { // Re-throw this error so the next handler can manage it throw error; } + const logParams = { + message: error.message, + status: error.status, + title: error.title, + request, + }; if (error.message === CONST.ERROR.FAILED_TO_FETCH) { // Throw when we get a "Failed to fetch" error so we can retry. Very common if a user is offline or experiencing an unlikely scenario like // incorrect url, bad cors headers returned by the server, DNS lookup failure etc. - Log.hmmm('[Network] Error: Failed to fetch', {message: error.message, status: error.status}); + Log.hmmm('[Network] API request error: Failed to fetch', logParams); } else if (_.contains([ CONST.ERROR.IOS_NETWORK_CONNECTION_LOST, CONST.ERROR.NETWORK_REQUEST_FAILED, @@ -80,37 +86,33 @@ function Logging(response, request) { ], error.message)) { // These errors seem to happen for native devices with interrupted connections. Often we will see logs about Pusher disconnecting together with these. // This type of error may also indicate a problem with SSL certs. - Log.hmmm('[Network] Error: Connection interruption likely', {message: error.message, status: error.status}); + Log.hmmm('[Network] API request error: Connection interruption likely', logParams); } else if (_.contains([CONST.ERROR.FIREFOX_DOCUMENT_LOAD_ABORTED, CONST.ERROR.SAFARI_DOCUMENT_LOAD_ABORTED], error.message)) { // This message can be observed page load is interrupted (closed or navigated away). - Log.hmmm('[Network] Error: User likely navigated away from or closed browser', {message: error.message, status: error.status}); + Log.hmmm('[Network] API request error: User likely navigated away from or closed browser', logParams); } else if (error.message === CONST.ERROR.IOS_LOAD_FAILED) { // Not yet clear why this message occurs, but it is specific to iOS and tends to happen around the same time as a Pusher code 1006 // which is when a websocket disconnects. So it seems likely to be a spotty connection scenario. - Log.hmmm('[Network] Error: iOS Load Failed error', {message: error.message, status: error.status}); + Log.hmmm('[Network] API request error: iOS Load Failed error', logParams); } else if (error.message === CONST.ERROR.SAFARI_CANNOT_PARSE_RESPONSE) { // Another cryptic Apple error message. Unclear why this can happen, but some speculation it can be fixed by a browser restart. - Log.hmmm('[Network] Error: Safari "cannot parse response"', {message: error.message, status: error.status}); + Log.hmmm('[Network] API request error: Safari "cannot parse response"', logParams); } else if (error.message === CONST.ERROR.GATEWAY_TIMEOUT) { // This error seems to only throw on dev when localhost:8080 tries to access the production web server. It's unclear whether this can happen on production or if // it's a sign that the web server is down. - Log.hmmm('[Network] Error: Gateway Timeout error', {message: error.message, status: error.status}); + Log.hmmm('[Network] API request error: Gateway Timeout error', logParams); } else if (request.command === 'AuthenticatePusher') { // AuthenticatePusher requests can return with fetch errors and no message. It happens because we return a non 200 header like 403 Forbidden. // This is common to see if we are subscribing to a bad channel related to something the user shouldn't be able to access. There's no additional information // we can get about these requests. - Log.hmmm('[Network] Error: AuthenticatePusher', {message: error.message, status: error.status}); + Log.hmmm('[Network] API request error: AuthenticatePusher', logParams); } else if (error.message === CONST.ERROR.EXPENSIFY_SERVICE_INTERRUPTED) { // Expensify site is down completely OR // Auth (database connection) is down / bedrock has timed out while making a request. We currently can't tell the difference between Auth down and bedrock timing out. - Log.hmmm('[Network] Error: Expensify service interrupted or timed out', {error: error.title, status: error.status}); + Log.hmmm('[Network] API request error: Expensify service interrupted or timed out', logParams); } else { // If we get any error that is not known log an alert so we can learn more about it and document it here. - Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} unknown error caught while processing request - ${error.message}`, { - command: request.command, - message: error.message, - status: error.status, - }, false); + Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} unknown API request error caught while processing request`, logParams, false); } throw new Error(CONST.ERROR.XHR_FAILED); From efb1d62e1874e77e4deb6bad46a449e343a1f934 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 13:23:50 -0400 Subject: [PATCH 131/155] Remove jest.setTimeout() by breaking test up into smaller chunks --- tests/ui/UnreadIndicatorsTest.js | 280 +++++++++++++++++-------------- 1 file changed, 152 insertions(+), 128 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 32013bcb1b38..b05d2b77ff14 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -13,7 +13,6 @@ import * as TestHelper from '../utils/TestHelper'; import appSetup from '../../src/setup'; import fontWeightBold from '../../src/styles/fontWeight/bold'; import * as AppActions from '../../src/libs/actions/App'; -import ReportActionsView from '../../src/pages/home/report/ReportActionsView'; import * as NumberUtils from '../../src/libs/NumberUtils'; import LocalNotification from '../../src/libs/Notification/LocalNotification'; import * as Report from '../../src/libs/actions/Report'; @@ -27,7 +26,6 @@ beforeAll(() => { global.fetch = TestHelper.getGlobalFetchMock(); // We need a bit more time for this test in some places - jest.setTimeout(30000); Linking.setInitialURL('https://new.expensify.com/r/1'); appSetup(); }); @@ -98,64 +96,77 @@ function isDrawerOpen(renderedApp) { return !lodashGet(sidebarLinks, [0, 'props', 'accessibilityElementsHidden']); } -describe('Unread Indicators', () => { - it('Shows correct LHN Status, “New Messages” badge, and New Line Indicators', async () => { - const REPORT_ID = 1; - const USER_A_ACCOUNT_ID = 1; - const USER_A_EMAIL = 'user_a@test.com'; - const USER_B_ACCOUNT_ID = 2; - const USER_B_EMAIL = 'user_b@test.com'; - const USER_C_ACCOUNT_ID = 3; - const USER_C_EMAIL = 'user_c@test.com'; - - // Render the App and sign in as a test user. - const renderedApp = render(); - - // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some - // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act - await act(async () => { - await waitForPromisesToResolve(); - }); +const REPORT_ID = 1; +const USER_A_ACCOUNT_ID = 1; +const USER_A_EMAIL = 'user_a@test.com'; +const USER_B_ACCOUNT_ID = 2; +const USER_B_EMAIL = 'user_b@test.com'; +const USER_C_ACCOUNT_ID = 3; +const USER_C_EMAIL = 'user_c@test.com'; - const loginForm = renderedApp.queryAllByA11yLabel('Login form'); - expect(loginForm.length).toBe(1); +/** + * @returns {RenderAPI} + */ +async function signInAndGetAppWithUnreadChat() { + // Render the App and sign in as a test user. + const renderedApp = render(); - await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some + // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act + await act(async () => { + await waitForPromisesToResolve(); + }); - const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + const loginForm = renderedApp.queryAllByA11yLabel('Login form'); + expect(loginForm.length).toBe(1); - // Simulate setting an unread report and personal details - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 9, - lastReadSequenceNumber: 1, - lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), - lastMessageText: 'Test', - participants: [USER_B_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: MOMENT_TEN_MINUTES_AGO.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), - 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), - 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), - 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), - 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), - 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), - 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), - 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), - 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); - await waitForPromisesToResolve(); + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + + const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + + // Simulate setting an unread report and personal details + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 9, + lastReadSequenceNumber: 1, + lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), + lastMessageText: 'Test', + participants: [USER_B_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: MOMENT_TEN_MINUTES_AGO.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), + 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), + 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), + 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), + 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), + 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), + 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), + 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), + 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(true); + await waitForPromisesToResolve(); + return renderedApp; +} + +describe('Unread Indicators', () => { + afterEach(Onyx.clear); + + it('Display bold in the LHN for unread chat and new line indicator above the chat message when we navigate to it', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); // Verify no notifications are created for these older messages expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); @@ -163,16 +174,10 @@ describe('Unread Indicators', () => { // Verify the sidebar links are rendered const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); expect(sidebarLinks.length).toBe(1); - - // And verify that the Report screen is rendered after manually setting the sidebar as loaded - // since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(true); - await waitForPromisesToResolve(); - expect(isDrawerOpen(renderedApp)).toBe(true); // Verify there is only one option in the sidebar - let optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); expect(optionRows.length).toBe(1); // And that the text is bold @@ -192,36 +197,50 @@ describe('Unread Indicators', () => { // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will // have a sequenceNumber of 2 - let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); - let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(2); // Scroll up and verify that the "New messages" badge appears scrollUpToRevealNewMessagesBadge(renderedApp); expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + }); - // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); - expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + it('Clear the new line indicator and bold when we navigate away from a chat that is now read', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); + + // Navigate to the unread chat from the sidebar + await navigateToSidebarOption(renderedApp, 0); + expect(isDrawerOpen(renderedApp)).toBe(false); - // Tap on the back button to return to the sidebar + // Then navigate back to the sidebar await navigateToSidebar(renderedApp); // Verify the LHN is now open expect(isDrawerOpen(renderedApp)).toBe(true); - // Navigate to the report again + // Verify that the option row in the LHN is no longer bold (since OpenReport marked it as read) + const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); + expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + + // Tap on the chat again await navigateToSidebarOption(renderedApp, 0); - // Verify the unread indicator is no longer present - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + // Verify the unread indicator is not present + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); expect(isDrawerOpen(renderedApp)).toBe(false); - // Scroll and verify that the new messages badge is hidden + // Scroll and verify that the new messages badge is also hidden scrollUpToRevealNewMessagesBadge(renderedApp); expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + }); + + it('Shows a browser notification and bold text when a new message arrives for a chat that is read', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); + + // Read the chat by navigating to it then return to the LHN // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant const NEW_REPORT_ID = 2; @@ -261,14 +280,14 @@ describe('Unread Indicators', () => { // Verify notification was created as the new message that has arrived is very recent expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); - // Navigate back to the sidebar + // // Navigate back to the sidebar await navigateToSidebar(renderedApp); - // Verify the new report option appears in the LHN - optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + // // Verify the new report option appears in the LHN + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); expect(optionRows.length).toBe(2); - // Verify the text for the new chat is bold and above the previous indicating it has not yet been read + // Verify the text for both chats are bold indicating that nothing has not yet been read let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); const firstReportOption = displayNameTexts[0]; @@ -276,22 +295,26 @@ describe('Unread Indicators', () => { expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); const secondReportOption = displayNameTexts[1]; - expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); // Tap the new report option and navigate back to the sidebar again via the back button await navigateToSidebarOption(renderedApp, 0); - // Verify that all report options appear in a "read" state + // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + }); - // Tap the previous report between User A and User B - await navigateToSidebarOption(renderedApp, 1); + it('Manually marking a chat message as read shows the new line indicator and updates the LHN', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); + + // Navigate to the unread report + await navigateToSidebarOption(renderedApp, 0); // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly @@ -299,9 +322,9 @@ describe('Unread Indicators', () => { await waitForPromisesToResolve(); // Verify the indicator appears above the last action - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); - sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(3); // Scroll up and verify the new messages badge appears @@ -312,51 +335,45 @@ describe('Unread Indicators', () => { await navigateToSidebar(renderedApp); // Verify the report is marked as unread in the sidebar - displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); // Navigate to the report again and back to the sidebar - await navigateToSidebarOption(renderedApp, 1); + await navigateToSidebarOption(renderedApp, 0); await navigateToSidebar(renderedApp); // Verify the report is now marked as read displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); // Navigate to the report again and verify the new line indicator is missing - await navigateToSidebarOption(renderedApp, 1); - await waitForPromisesToResolve(); + await navigateToSidebarOption(renderedApp, 0); unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); - // Scroll up and verify the badge is hidden + // Scroll up and verify the "New messages" badge is hidden scrollUpToRevealNewMessagesBadge(renderedApp); expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); - expect(isDrawerOpen(renderedApp)).toBe(false); + }); - // Navigate to the LHN - await navigateToSidebar(renderedApp); + it('Removes the new line indicator when a new message is created by the current user', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); - // Simulate another new message on the report with User B - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 10: TestHelper.buildTestReportComment(USER_B_EMAIL, 10, moment().unix()), - }); - await waitForPromisesToResolve(); + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(isDrawerOpen(renderedApp)).toBe(true); - displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - // Navigate to the report again and verify the indicator exists - await navigateToSidebarOption(renderedApp, 1); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + // Navigate to the report and verify the indicator is present + await navigateToSidebarOption(renderedApp, 0); + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); // Leave a comment as the current user and verify the indicator is removed @@ -364,6 +381,29 @@ describe('Unread Indicators', () => { await waitForPromisesToResolve(); unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); + }); + + it('Clears the new line indicator when the user moves the App to the background', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); + + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(isDrawerOpen(renderedApp)).toBe(true); + + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the chat and verify the new line indicator is present + await navigateToSidebarOption(renderedApp, 0); + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + + // Then back to the LHN - then back to the chat again and verify the new line indicator has cleared + await navigateToSidebar(renderedApp); + await navigateToSidebarOption(renderedApp, 0); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); // Mark a previous comment as unread and verify the unread action indicator returns Report.markCommentAsUnread(REPORT_ID, 9); @@ -378,21 +418,5 @@ describe('Unread Indicators', () => { // Verify the new line is cleared unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); - - // As the current user add several comments - Report.addComment(REPORT_ID, 'Current User Comment 2'); - await waitForPromisesToResolve(); - - Report.addComment(REPORT_ID, 'Current User Comment 3'); - await waitForPromisesToResolve(); - - Report.addComment(REPORT_ID, 'Current User Comment 4'); - await waitForPromisesToResolve(); - - // Mark the last comment as "unread" and verify the unread indicator appears - Report.markCommentAsUnread(REPORT_ID, 14); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); }); }); From c38bd3d62cce54c2e9398e38e8036bb65c0650cf Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 15 Sep 2022 18:30:15 +0100 Subject: [PATCH 132/155] Fix the c+ checklist template --- .../javascript/contributorChecklist/contributorChecklist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/javascript/contributorChecklist/contributorChecklist.js b/.github/actions/javascript/contributorChecklist/contributorChecklist.js index 62036078d24c..552e1c374071 100644 --- a/.github/actions/javascript/contributorChecklist/contributorChecklist.js +++ b/.github/actions/javascript/contributorChecklist/contributorChecklist.js @@ -65,7 +65,7 @@ const completedContributorPlusChecklist = `- [x] I have verified the author chec - [x] Android / Chrome - [x] MacOS / Chrome - [x] MacOS / Desktop -- [x] I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed) +- [x] If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack - [x] I verified proper code patterns were followed (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code)) - [x] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. \`toggleReport\` and not \`onIconClick\`). - [x] I verified that comments were added to code that is not self explanatory From be75ac25965f2ffcb656d2df5fc9cf5ffd726a96 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 15 Sep 2022 17:31:00 +0000 Subject: [PATCH 133/155] Update version to 1.2.0-5 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b683c8e32809..2bc3c6839023 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020004 - versionName "1.2.0-4" + versionCode 1001020005 + versionName "1.2.0-5" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index cea7a6ce044c..25250fb78772 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.4 + 1.2.0.5 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3a11abf9877f..e9b832b4f5cc 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.4 + 1.2.0.5 diff --git a/package-lock.json b/package-lock.json index 85d48adab0bc..c0c4cbb1ac54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-4", + "version": "1.2.0-5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-4", + "version": "1.2.0-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index af9bb8549e34..b466d4d45bbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-4", + "version": "1.2.0-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From c1c41bd975f1654a60c219efd059aab031f43198 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 15 Sep 2022 18:41:54 +0100 Subject: [PATCH 134/155] Build the actions --- .github/actions/javascript/contributorChecklist/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/javascript/contributorChecklist/index.js b/.github/actions/javascript/contributorChecklist/index.js index 1041b6906cfd..94d6cf8e0829 100644 --- a/.github/actions/javascript/contributorChecklist/index.js +++ b/.github/actions/javascript/contributorChecklist/index.js @@ -75,7 +75,7 @@ const completedContributorPlusChecklist = `- [x] I have verified the author chec - [x] Android / Chrome - [x] MacOS / Chrome - [x] MacOS / Desktop -- [x] I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed) +- [x] If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack - [x] I verified proper code patterns were followed (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code)) - [x] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. \`toggleReport\` and not \`onIconClick\`). - [x] I verified that comments were added to code that is not self explanatory From 18367421f260003630e554647d02481d021f7415 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 13:56:52 -0400 Subject: [PATCH 135/155] Extract the one part of tests that actually does need async/await to work. Use regular promises in the actual tests themselves --- contributingGuides/STYLE.md | 2 +- tests/ui/UnreadIndicatorsTest.js | 639 ++++++++++-------- .../utils/waitForPromisesToResolveWithAct.js | 21 + 3 files changed, 364 insertions(+), 298 deletions(-) create mode 100644 tests/utils/waitForPromisesToResolveWithAct.js diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 6402d27e2e85..62e64bede72c 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -388,7 +388,7 @@ So, if a new language feature isn't something we have agreed to support it's off Here are a couple of things we would ask that you *avoid* to help maintain consistency in our codebase: -- **Async/Await** - Use the native `Promise` instead. Async/Await is permitted in test files only. +- **Async/Await** - Use the native `Promise` instead - **Optional Chaining** - Use `lodashGet()` to fetch a nested value instead - **Null Coalescing Operator** - Use `lodashGet()` or `||` to set a default value for a possibly `undefined` or `null` variable diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index b05d2b77ff14..45172e574f40 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -1,14 +1,14 @@ -/* eslint-disable @lwc/lwc/no-async-await */ import React from 'react'; import Onyx from 'react-native-onyx'; import {Linking, AppState} from 'react-native'; -import {fireEvent, render, act} from '@testing-library/react-native'; +import {fireEvent, render} from '@testing-library/react-native'; import lodashGet from 'lodash/get'; import moment from 'moment'; import App from '../../src/App'; import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import waitForPromisesToResolveWithAct from '../utils/waitForPromisesToResolveWithAct'; import * as TestHelper from '../utils/TestHelper'; import appSetup from '../../src/setup'; import fontWeightBold from '../../src/styles/fontWeight/bold'; @@ -70,7 +70,7 @@ function isNewMessagesBadgeVisible(renderedApp) { * @param {RenderAPI} renderedApp * @return {Promise} */ -async function navigateToSidebar(renderedApp) { +function navigateToSidebar(renderedApp) { const reportHeaderBackButton = renderedApp.getByA11yHint('Navigate back to chats list'); fireEvent(reportHeaderBackButton, 'press'); return waitForPromisesToResolve(); @@ -105,318 +105,363 @@ const USER_C_ACCOUNT_ID = 3; const USER_C_EMAIL = 'user_c@test.com'; /** + * Sets up a test with a logged in user that has one unread chat from another user. Returns the test instance. + * * @returns {RenderAPI} */ -async function signInAndGetAppWithUnreadChat() { +function signInAndGetAppWithUnreadChat() { // Render the App and sign in as a test user. const renderedApp = render(); - - // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some - // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act - await act(async () => { - await waitForPromisesToResolve(); - }); - - const loginForm = renderedApp.queryAllByA11yLabel('Login form'); - expect(loginForm.length).toBe(1); - - await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); - - const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); - - // Simulate setting an unread report and personal details - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 9, - lastReadSequenceNumber: 1, - lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), - lastMessageText: 'Test', - participants: [USER_B_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: MOMENT_TEN_MINUTES_AGO.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), - 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), - 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), - 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), - 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), - 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), - 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), - 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), - 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); - - // We manually setting the sidebar as loaded since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(true); - await waitForPromisesToResolve(); - return renderedApp; + return waitForPromisesToResolveWithAct() + .then(() => { + const loginForm = renderedApp.queryAllByA11yLabel('Login form'); + expect(loginForm.length).toBe(1); + + return TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + }) + .then(() => { + const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + + // Simulate setting an unread report and personal details + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 9, + lastReadSequenceNumber: 1, + lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), + lastMessageText: 'Test', + participants: [USER_B_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: MOMENT_TEN_MINUTES_AGO.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), + 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), + 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), + 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), + 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), + 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), + 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), + 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), + 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(true); + return waitForPromisesToResolve(); + }) + .then(() => renderedApp); } describe('Unread Indicators', () => { afterEach(Onyx.clear); - it('Display bold in the LHN for unread chat and new line indicator above the chat message when we navigate to it', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Verify no notifications are created for these older messages - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); - - // Verify the sidebar links are rendered - const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); - expect(sidebarLinks.length).toBe(1); - expect(isDrawerOpen(renderedApp)).toBe(true); - - // Verify there is only one option in the sidebar - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); - expect(optionRows.length).toBe(1); - - // And that the text is bold - const displayNameText = renderedApp.getByA11yLabel('Chat user display names'); - expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - await navigateToSidebarOption(renderedApp, 0); - - // Verify that the report screen is rendered and the drawer is closed - expect(isDrawerOpen(renderedApp)).toBe(false); - - // That the report actions are visible along with the created action - const createdAction = renderedApp.getByA11yLabel('Chat welcome message'); - expect(createdAction).toBeTruthy(); - const reportComments = renderedApp.getAllByA11yLabel('Chat message'); - expect(reportComments.length).toBe(9); - - // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will - // have a sequenceNumber of 2 - const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); - expect(sequenceNumber).toBe(2); - - // Scroll up and verify that the "New messages" badge appears - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + it('Display bold in the LHN for unread chat and new line indicator above the chat message when we navigate to it', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Verify no notifications are created for these older messages + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); + + // Verify the sidebar links are rendered + const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); + expect(sidebarLinks.length).toBe(1); + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Verify there is only one option in the sidebar + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + expect(optionRows.length).toBe(1); + + // And that the text is bold + const displayNameText = renderedApp.getByA11yLabel('Chat user display names'); + expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // Verify that the report screen is rendered and the drawer is closed + expect(isDrawerOpen(renderedApp)).toBe(false); + + // That the report actions are visible along with the created action + const createdAction = renderedApp.getByA11yLabel('Chat welcome message'); + expect(createdAction).toBeTruthy(); + const reportComments = renderedApp.getAllByA11yLabel('Chat message'); + expect(reportComments.length).toBe(9); + + // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will + // have a sequenceNumber of 2 + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(2); + + // Scroll up and verify that the "New messages" badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + }); }); - it('Clear the new line indicator and bold when we navigate away from a chat that is now read', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Navigate to the unread chat from the sidebar - await navigateToSidebarOption(renderedApp, 0); - expect(isDrawerOpen(renderedApp)).toBe(false); - - // Then navigate back to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the LHN is now open - expect(isDrawerOpen(renderedApp)).toBe(true); - - // Verify that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); - expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - - // Tap on the chat again - await navigateToSidebarOption(renderedApp, 0); - - // Verify the unread indicator is not present - const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); - expect(isDrawerOpen(renderedApp)).toBe(false); - - // Scroll and verify that the new messages badge is also hidden - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + it('Clear the new line indicator and bold when we navigate away from a chat that is now read', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Navigate to the unread chat from the sidebar + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Then navigate back to the sidebar + return navigateToSidebar(renderedApp); + }) + .then(() => { + // Verify the LHN is now open + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Verify that the option row in the LHN is no longer bold (since OpenReport marked it as read) + const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); + expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + + // Tap on the chat again + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // Verify the unread indicator is not present + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Scroll and verify that the new messages badge is also hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + }); }); - it('Shows a browser notification and bold text when a new message arrives for a chat that is read', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Read the chat by navigating to it then return to the LHN - - // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant - const NEW_REPORT_ID = 2; - const NEW_REPORT_CREATED_MOMENT = moment(); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { - reportID: NEW_REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 1, - lastReadSequenceNumber: 0, - lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), - lastMessageText: 'Comment 1', - participants: [USER_C_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: NEW_REPORT_CREATED_MOMENT.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: { - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - actorEmail: USER_C_EMAIL, - person: [{type: 'TEXT', style: 'strong', text: 'User C'}], - sequenceNumber: 1, - timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), - message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], - reportActionID: NumberUtils.rand64(), - }, - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), - }); - await waitForPromisesToResolve(); - - // Verify notification was created as the new message that has arrived is very recent - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); - - // // Navigate back to the sidebar - await navigateToSidebar(renderedApp); - - // // Verify the new report option appears in the LHN - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); - expect(optionRows.length).toBe(2); - - // Verify the text for both chats are bold indicating that nothing has not yet been read - let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - const firstReportOption = displayNameTexts[0]; - expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); - - const secondReportOption = displayNameTexts[1]; - expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); - - // Tap the new report option and navigate back to the sidebar again via the back button - await navigateToSidebarOption(renderedApp, 0); - - // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread - displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + it('Shows a browser notification and bold text when a new message arrives for a chat that is read', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant + const NEW_REPORT_ID = 2; + const NEW_REPORT_CREATED_MOMENT = moment(); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { + reportID: NEW_REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 1, + lastReadSequenceNumber: 0, + lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), + lastMessageText: 'Comment 1', + participants: [USER_C_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: NEW_REPORT_CREATED_MOMENT.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorEmail: USER_C_EMAIL, + person: [{type: 'TEXT', style: 'strong', text: 'User C'}], + sequenceNumber: 1, + timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), + message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], + reportActionID: NumberUtils.rand64(), + }, + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + }); + return waitForPromisesToResolve(); + }) + .then(() => { + // Verify notification was created as the new message that has arrived is very recent + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); + + // // Navigate back to the sidebar + return navigateToSidebar(renderedApp); + }) + .then(() => { + // // Verify the new report option appears in the LHN + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + expect(optionRows.length).toBe(2); + + // Verify the text for both chats are bold indicating that nothing has not yet been read + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(2); + const firstReportOption = displayNameTexts[0]; + expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); + + const secondReportOption = displayNameTexts[1]; + expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); + + // Tap the new report option and navigate back to the sidebar again via the back button + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + }); }); - it('Manually marking a chat message as read shows the new line indicator and updates the LHN', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Navigate to the unread report - await navigateToSidebarOption(renderedApp, 0); - - // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then - // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly - Report.markCommentAsUnread(REPORT_ID, 3); - await waitForPromisesToResolve(); - - // Verify the indicator appears above the last action - let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); - expect(sequenceNumber).toBe(3); - - // Scroll up and verify the new messages badge appears - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); - - // Navigate to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the report is marked as unread in the sidebar - let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); - - // Navigate to the report again and back to the sidebar - await navigateToSidebarOption(renderedApp, 0); - await navigateToSidebar(renderedApp); - - // Verify the report is now marked as read - displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); - - // Navigate to the report again and verify the new line indicator is missing - await navigateToSidebarOption(renderedApp, 0); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); - - // Scroll up and verify the "New messages" badge is hidden - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + it('Manually marking a chat message as read shows the new line indicator and updates the LHN', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Navigate to the unread report + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then + // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly + Report.markCommentAsUnread(REPORT_ID, 3); + return waitForPromisesToResolve(); + }) + .then(() => { + // Verify the indicator appears above the last action + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(3); + + // Scroll up and verify the new messages badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // Navigate to the sidebar + return navigateToSidebar(renderedApp); + }) + .then(() => { + // Verify the report is marked as unread in the sidebar + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and back to the sidebar + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => navigateToSidebar(renderedApp)) + .then(() => { + // Verify the report is now marked as read + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and verify the new line indicator is missing + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + + // Scroll up and verify the "New messages" badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + }); }); - it('Removes the new line indicator when a new message is created by the current user', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Verify we are on the LHN and that the chat shows as unread in the LHN - expect(isDrawerOpen(renderedApp)).toBe(true); - - const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - // Navigate to the report and verify the indicator is present - await navigateToSidebarOption(renderedApp, 0); - let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - - // Leave a comment as the current user and verify the indicator is removed - Report.addComment(REPORT_ID, 'Current User Comment 1'); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + it('Removes the new line indicator when a new message is created by the current user', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(isDrawerOpen(renderedApp)).toBe(true); + + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the report and verify the indicator is present + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + + // Leave a comment as the current user and verify the indicator is removed + Report.addComment(REPORT_ID, 'Current User Comment 1'); + return waitForPromisesToResolve(); + }) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + }); }); - it('Clears the new line indicator when the user moves the App to the background', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Verify we are on the LHN and that the chat shows as unread in the LHN - expect(isDrawerOpen(renderedApp)).toBe(true); - - const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - // Navigate to the chat and verify the new line indicator is present - await navigateToSidebarOption(renderedApp, 0); - let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - - // Then back to the LHN - then back to the chat again and verify the new line indicator has cleared - await navigateToSidebar(renderedApp); - await navigateToSidebarOption(renderedApp, 0); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); - - // Mark a previous comment as unread and verify the unread action indicator returns - Report.markCommentAsUnread(REPORT_ID, 9); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - - // Trigger the app going inactive and active again - AppState.emitCurrentTestState('background'); - AppState.emitCurrentTestState('active'); - - // Verify the new line is cleared - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + it('Clears the new line indicator when the user moves the App to the background', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(isDrawerOpen(renderedApp)).toBe(true); + + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the chat and verify the new line indicator is present + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + + // Then back to the LHN - then back to the chat again and verify the new line indicator has cleared + return navigateToSidebar(renderedApp); + }) + .then(() => navigateToSidebarOption(renderedApp, 0)) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + + // Mark a previous comment as unread and verify the unread action indicator returns + Report.markCommentAsUnread(REPORT_ID, 9); + return waitForPromisesToResolve(); + }) + .then(() => { + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + + // Trigger the app going inactive and active again + AppState.emitCurrentTestState('background'); + AppState.emitCurrentTestState('active'); + + // Verify the new line is cleared + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + }); }); }); diff --git a/tests/utils/waitForPromisesToResolveWithAct.js b/tests/utils/waitForPromisesToResolveWithAct.js new file mode 100644 index 000000000000..37d5ccb041ce --- /dev/null +++ b/tests/utils/waitForPromisesToResolveWithAct.js @@ -0,0 +1,21 @@ +import {act} from '@testing-library/react-native'; +import waitForPromisesToResolve from './waitForPromisesToResolve'; + +/** + * This method is necessary because react-navigation's NavigationContainer makes an internal state update when parsing the + * linkingConfig to get the initialState. This throws some warnings related to async code if we do not wrap this call in an act(). + * + * See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act + * + * This apparently will not work unless we use async/await because of some kind of voodoo magic inside the react-test-renderer + * so we are suppressing our lint rule and avoiding async/await everywhere else in our tests. + * + * @returns {Promise} + */ +// eslint-disable-next-line @lwc/lwc/no-async-await +export default async function waitForPromisesToResolveWithAct() { + // eslint-disable-next-line @lwc/lwc/no-async-await + await act(async () => { + await waitForPromisesToResolve(); + }); +} From c350685dfb652e51827207874a9e7175265ffe83 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 14:02:27 -0400 Subject: [PATCH 136/155] Cleanup Expensicons mock --- jest/setup.js | 93 +++------------------------------------------------ 1 file changed, 5 insertions(+), 88 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 5c84427e5205..9a0f864ab4b6 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -58,91 +58,8 @@ mockImages('images'); mockImages('images/avatars'); mockImages('images/bankicons'); mockImages('images/product-illustrations'); -jest.mock('../src/components/Icon/Expensicons', () => ({ - ActiveRoomAvatar: () => '', - AdminRoomAvatar: () => '', - Android: () => '', - AnnounceRoomAvatar: () => '', - Apple: () => '', - ArrowRight: () => '', - BackArrow: () => '', - Bank: () => '', - Bill: () => '', - Bolt: () => '', - Briefcase: () => '', - Bug: () => '', - Building: () => '', - Camera: () => '', - Cash: () => '', - ChatBubble: () => '', - Checkmark: () => '', - CircleHourglass: () => '', - Clipboard: () => '', - Close: () => '', - ClosedSign: () => '', - Collapse: () => '', - Concierge: () => '', - Connect: () => '', - CreditCard: () => '', - DeletedRoomAvatar: () => '', - DomainRoomAvatar: () => '', - DotIndicator: () => '', - DownArrow: () => '', - Download: () => '', - Emoji: () => '', - Exclamation: () => '', - Exit: () => '', - ExpensifyCard: () => '', - Expand: () => '', - Eye: () => '', - EyeDisabled: () => '', - FallbackAvatar: () => '', - FallbackWorkspaceAvatar: () => '', - Gallery: () => '', - Gear: () => '', - Hashtag: () => '', - ImageCropMask: () => '', - Info: () => '', - Invoice: () => '', - Key: () => '', - Keyboard: () => '', - Link: () => '', - LinkCopy: () => '', - Lock: () => '', - Luggage: () => '', - MagnifyingGlass: () => '', - Mail: () => '', - MoneyBag: () => '', - MoneyCircle: () => '', - Monitor: () => '', - NewWindow: () => '', - NewWorkspace: () => '', - Offline: () => '', - OfflineCloud: () => '', - Paperclip: () => '', - PayPal: () => '', - Paycheck: () => '', - Pencil: () => '', - Phone: () => '', - Pin: () => '', - PinCircle: () => '', - Plus: () => '', - Printer: () => '', - Profile: () => '', - QuestionMark: () => '', - Receipt: () => '', - ReceiptSearch: () => '', - Rotate: () => '', - RotateLeft: () => '', - Send: () => '', - Sync: () => '', - ThreeDots: () => '', - Transfer: () => '', - Trashcan: () => '', - UpArrow: () => '', - Upload: () => '', - Users: () => '', - Wallet: () => '', - Workspace: () => '', - Zoom: () => '', -})); +jest.mock('../src/components/Icon/Expensicons', () => { + const reduce = require('underscore').reduce; + const Expensicons = jest.requireActual('../src/components/Icon/Expensicons'); + return reduce(Expensicons, (prev, _curr, key) => ({...prev, [key]: () => ''}), {}); +}); From f51ed16f150e23fc80a48e40bc59bf7870e03af3 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 14:06:49 -0400 Subject: [PATCH 137/155] Clean up function names --- __mocks__/react-native.js | 6 ++---- src/libs/ReportActionsUtils.js | 4 ++-- src/libs/actions/Report.js | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js index 3e407252dc78..e2630c1ef632 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.js @@ -1,13 +1,10 @@ -/* eslint-disable arrow-body-style */ // eslint-disable-next-line no-restricted-imports import * as ReactNative from 'react-native'; import _ from 'underscore'; jest.doMock('react-native', () => { let url = 'https://new.expensify.com/'; - const getInitialURL = () => { - return Promise.resolve(url); - }; + const getInitialURL = () => Promise.resolve(url); let appState = 'active'; let count = 0; @@ -57,6 +54,7 @@ jest.doMock('react-native', () => { ...ReactNative.Dimensions, addEventListener: jest.fn(), get: () => ({ + // Tests will run with the app in a typical small screen size width: 300, height: 700, scale: 1, fontScale: 1, }), }, diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 157891dd8aaa..1b49c722e642 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -126,7 +126,7 @@ function getLastVisibleMessageText(reportID, actionsToMerge = {}) { * @param {Number} lastReadSequenceNumber * @return {String} */ -function getNewLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = {}, deletedSequenceNumber, lastReadSequenceNumber) { +function getOptimisticLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = {}, deletedSequenceNumber, lastReadSequenceNumber) { // If the action we are deleting is unread then just return the current last read sequence number if (deletedSequenceNumber > lastReadSequenceNumber) { return lastReadSequenceNumber; @@ -148,7 +148,7 @@ function getNewLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = } export { - getNewLastReadSequenceNumberForDeletedAction, + getOptimisticLastReadSequenceNumberForDeletedAction, getLastVisibleMessageText, getSortedReportActions, getMostRecentIOUReportSequenceNumber, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9d977290278d..4897d56ed02f 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1186,7 +1186,7 @@ function deleteReportComment(reportID, reportAction) { // If we are deleting the last visible message, let's find the previous visible one and update the lastMessageText in the LHN. // Similarly, we are deleting the last read comment will want to update the lastReadSequenceNumber to use the previous visible message. const lastMessageText = ReportActionsUtils.getLastVisibleMessageText(reportID, optimisticReportActions); - const lastReadSequenceNumber = ReportActionsUtils.getNewLastReadSequenceNumberForDeletedAction( + const lastReadSequenceNumber = ReportActionsUtils.getOptimisticLastReadSequenceNumberForDeletedAction( reportID, optimisticReportActions, reportAction.sequenceNumber, From 536f5395cb28cfb0bc3e6359c31f3c3cef728265 Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Thu, 15 Sep 2022 13:44:21 -0700 Subject: [PATCH 138/155] Use existing personal detail's avatar when it exists --- src/libs/OptionsListUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index b56c08f7ca5a..6d58122f0423 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -272,7 +272,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { const personalDetailMap = getPersonalDetailsForLogins(logins, personalDetails); const personalDetailList = _.values(personalDetailMap); - const personalDetail = personalDetailList[0]; + const personalDetail = personalDetailList[0] || {}; let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; @@ -349,7 +349,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { result.text = reportName; result.subtitle = subtitle; result.participantsList = personalDetailList; - result.icons = ReportUtils.getIcons(report, personalDetails, policies); + result.icons = ReportUtils.getIcons(report, personalDetails, policies, personalDetail.avatar); result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom || result.isPolicyExpenseChat); return result; From 6328e5e9f7227c716ef141819c868e183da3d21d Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Thu, 15 Sep 2022 14:35:49 -0700 Subject: [PATCH 139/155] Add missing `alternateText` option in option --- src/libs/OptionsListUtils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 6d58122f0423..cc8a2b38b994 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -343,6 +343,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { result.login = personalDetail.login; result.phoneNumber = personalDetail.phoneNumber; result.payPalMeAddress = personalDetail.payPalMeAddress; + result.alternateText = Str.removeSMSDomain(personalDetail.login); } const reportName = ReportUtils.getReportName(report, personalDetailMap, policies); From b25ec27b641fe8468011316a1704456d63ddbebf Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 15 Sep 2022 21:46:15 +0000 Subject: [PATCH 140/155] Update version to 1.2.0-6 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2bc3c6839023..4f708c805d62 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020005 - versionName "1.2.0-5" + versionCode 1001020006 + versionName "1.2.0-6" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 25250fb78772..b61ebf609f94 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.5 + 1.2.0.6 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e9b832b4f5cc..3ebed1a25b84 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.5 + 1.2.0.6 diff --git a/package-lock.json b/package-lock.json index c0c4cbb1ac54..0d14d4ef4171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-5", + "version": "1.2.0-6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-5", + "version": "1.2.0-6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b466d4d45bbc..7b4887d093e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-5", + "version": "1.2.0-6", "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.", From 947a4fc6950e5a17bbe14143cdc5eb7b1555a36d Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 15 Sep 2022 22:32:31 +0000 Subject: [PATCH 141/155] Update version to 1.2.0-7 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 4f708c805d62..2494c07a08e7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020006 - versionName "1.2.0-6" + versionCode 1001020007 + versionName "1.2.0-7" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index b61ebf609f94..c5ab77c495f8 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.6 + 1.2.0.7 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3ebed1a25b84..4552d4e4c02d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.6 + 1.2.0.7 diff --git a/package-lock.json b/package-lock.json index 0d14d4ef4171..54086977ed84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-6", + "version": "1.2.0-7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-6", + "version": "1.2.0-7", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7b4887d093e9..cfbca0601256 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-6", + "version": "1.2.0-7", "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.", From 193b8a342927c62d7cfc9b13afea66acd1400c9f Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Thu, 15 Sep 2022 15:55:54 -0700 Subject: [PATCH 142/155] Only use personal detail login as `alternateText` if we're not making the option for a report --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index cc8a2b38b994..9f86d55add8f 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -329,6 +329,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { } } else { result.keyForList = personalDetail.login; + result.alternateText = Str.removeSMSDomain(personalDetail.login); } if (result.hasOutstandingIOU) { @@ -343,7 +344,6 @@ function createOption(logins, personalDetails, report, reportActions = {}, { result.login = personalDetail.login; result.phoneNumber = personalDetail.phoneNumber; result.payPalMeAddress = personalDetail.payPalMeAddress; - result.alternateText = Str.removeSMSDomain(personalDetail.login); } const reportName = ReportUtils.getReportName(report, personalDetailMap, policies); From e1efe56208b5185cd9f97d12ace1ba872f149f24 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 15 Sep 2022 23:19:28 +0000 Subject: [PATCH 143/155] Update version to 1.2.0-8 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2494c07a08e7..77885e4bc00e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020007 - versionName "1.2.0-7" + versionCode 1001020008 + versionName "1.2.0-8" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index c5ab77c495f8..9ab467740b11 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.7 + 1.2.0.8 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 4552d4e4c02d..92ecd597d886 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.7 + 1.2.0.8 diff --git a/package-lock.json b/package-lock.json index 54086977ed84..150485a684ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-7", + "version": "1.2.0-8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-7", + "version": "1.2.0-8", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index cfbca0601256..cc68c11ce720 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-7", + "version": "1.2.0-8", "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.", From 8abe3dd36d813505185ed71ee777adde4924f695 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 16 Sep 2022 01:28:51 +0000 Subject: [PATCH 144/155] Update version to 1.2.1-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 77885e4bc00e..4ee66d3fb79d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020008 - versionName "1.2.0-8" + versionCode 1001020100 + versionName "1.2.1-0" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 9ab467740b11..99e91a3df77c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.0 + 1.2.1 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.8 + 1.2.1.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 92ecd597d886..272bb944f4ff 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.0 + 1.2.1 CFBundleSignature ???? CFBundleVersion - 1.2.0.8 + 1.2.1.0 diff --git a/package-lock.json b/package-lock.json index 150485a684ea..50f91287d1fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-8", + "version": "1.2.1-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-8", + "version": "1.2.1-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index cc68c11ce720..fb1a983ae489 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-8", + "version": "1.2.1-0", "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.", From ff196b198e5567b4e65102e80f1b0ebe2df5304b Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 23:48:25 -0400 Subject: [PATCH 145/155] Add test for deleting comment --- src/components/OptionRow.js | 1 + src/components/OptionsList/BaseOptionsList.js | 1 + src/pages/home/sidebar/SidebarLinks.js | 1 + tests/ui/UnreadIndicatorsTest.js | 54 +++++++++++++++++++ tests/utils/TestHelper.js | 2 +- 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index e408387b1a3f..6ac45e98709d 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -196,6 +196,7 @@ const OptionRow = (props) => { /> {props.option.alternateText ? ( diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index c9f983a9720f..c8337b87acab 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -166,6 +166,7 @@ class BaseOptionsList extends Component { renderItem({item, index, section}) { return ( { // In this test, we are generically mocking the responses of all API requests by mocking fetch() and having it @@ -464,4 +465,57 @@ describe('Unread Indicators', () => { expect(unreadIndicator.length).toBe(0); }); }); + + it('Displays the correct chat message preview in the LHN when a comment is added then deleted', () => { + let reportActions; + let lastReportAction; + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: val => reportActions = val, + }); + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Navigate to the chat and simulate leaving a comment from the current user + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // Leave a comment as the current user + Report.addComment(REPORT_ID, 'Current User Comment 1'); + return waitForPromisesToResolve(); + }) + .then(() => { + // Simulate the response from the server so that the comment can be deleted in this test + lastReportAction = {...CollectionUtils.lastItem(reportActions)}; + delete lastReportAction[lastReportAction.clientID]; + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + lastMessageText: lastReportAction.message[0].text, + lastMessageTimestamp: lastReportAction.timestamp, + lastActorEmail: lastReportAction.actorEmail, + maxSequenceNumber: lastReportAction.sequenceNumber, + reportID: REPORT_ID, + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + [lastReportAction.clientID]: null, + 10: lastReportAction, + }); + return waitForPromisesToResolve(); + }) + .then(() => { + // Verify the chat preview text matches the last comment from the current user + const alternateText = renderedApp.queryAllByA11yLabel('Last chat message preview'); + expect(alternateText.length).toBe(1); + expect(alternateText[0].props.children).toBe('Current User Comment 1'); + + Report.deleteReportComment(REPORT_ID, lastReportAction); + return waitForPromisesToResolve(); + }) + .then(() => { + const alternateText = renderedApp.queryAllByA11yLabel('Last chat message preview'); + expect(alternateText.length).toBe(1); + expect(alternateText[0].props.children).toBe('Comment 9'); + }); + }); }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index eea6ae43461f..032f97fe2b1e 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -137,7 +137,7 @@ function buildTestReportComment(actorEmail, sequenceNumber, timestamp) { person: [{type: 'TEXT', style: 'strong', text: 'User B'}], sequenceNumber, timestamp, - message: [{type: 'COMMENT', html: 'Comment 1', text: `Comment ${sequenceNumber}`}], + message: [{type: 'COMMENT', html: `Comment ${sequenceNumber}`, text: `Comment ${sequenceNumber}`}], reportActionID: NumberUtils.rand64(), }; } From 7b76284a011aa08a4ad24aab7b867eaed725b395 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 18:34:02 -1000 Subject: [PATCH 146/155] improve a couple of comments and fix some incorrect changes --- jest/setup.js | 2 +- src/libs/actions/Report.js | 4 ++-- src/pages/home/HeaderView.js | 2 +- src/pages/home/report/FloatingMessageCounter/index.js | 5 ++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 9a0f864ab4b6..9016683290d1 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -50,7 +50,7 @@ function mockImages(imagePath) { }); } -// We are mock all images so that Icons and other assets cannot break tests. In the testing environment, importing things like .svg +// We are mocking all images so that Icons and other assets cannot break tests. In the testing environment, importing things like .svg // directly will lead to undefined variables instead of a component or string (which is what React expects). Loading these assets is // not required as the test environment does not actually render any UI anywhere and just needs them to noop so the test renderer // (which is a virtual implemented DOM) can do it's thing. diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 4897d56ed02f..82a23a115fd0 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -11,6 +11,7 @@ import PushNotification from '../Notification/PushNotification'; import * as PersonalDetails from './PersonalDetails'; import Navigation from '../Navigation/Navigation'; import * as ActiveClientManager from '../ActiveClientManager'; +import Visibility from '../Visibility'; import ROUTES from '../../ROUTES'; import Timing from './Timing'; import * as DeprecatedAPI from '../deprecatedAPI'; @@ -25,7 +26,6 @@ import * as Localize from '../Localize'; import DateUtils from '../DateUtils'; import * as ReportActionsUtils from '../ReportActionsUtils'; import * as NumberUtils from '../NumberUtils'; -import Visibility from '../Visibility'; let currentUserEmail; let currentUserAccountID; @@ -805,7 +805,7 @@ function addActions(reportID, text = '', file) { const actionCount = text && file ? 2 : 1; const newSequenceNumber = highestSequenceNumber + actionCount; - // We guess at what these sequenceNumbers are to enable marking as unread while offline + // We're giving our best guess at what these sequenceNumbers are to enable marking as unread while offline. if (text && file) { reportCommentAction.sequenceNumber = highestSequenceNumber + 1; attachmentAction.sequenceNumber = highestSequenceNumber + 2; diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 69a8b524c263..967523fe235a 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -93,7 +93,7 @@ const HeaderView = (props) => { {props.isSmallScreenWidth && ( - + + Date: Thu, 15 Sep 2022 19:43:40 -1000 Subject: [PATCH 147/155] Must mock useDrawerStatus in LHNOrderTest --- tests/unit/LHNOrderTest.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/LHNOrderTest.js b/tests/unit/LHNOrderTest.js index 68944b326a9d..5fbc395b6f93 100644 --- a/tests/unit/LHNOrderTest.js +++ b/tests/unit/LHNOrderTest.js @@ -198,6 +198,16 @@ function getDefaultRenderedSidebarLinks() { )); } +jest.mock('@react-navigation/drawer', () => { + const drawer = jest.requireActual('@react-navigation/drawer'); + return { + ...drawer, + useDrawerStatus() { + return 'open'; + }, + }; +}); + describe('Sidebar', () => { describe('in default mode', () => { // Clear out Onyx after each test so that each test starts with a clean slate From b6d9615d0d25d26addef67ab6a89c709d3c2c117 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 19:58:59 -1000 Subject: [PATCH 148/155] Fix icons and withDrawerState for LHNOrderTest --- jest/setup.js | 9 ++++++++- src/pages/home/sidebar/SidebarLinks.js | 2 -- .../home/sidebar/SidebarScreen/BaseSidebarScreen.js | 4 +++- tests/unit/LHNOrderTest.js | 10 ---------- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 9016683290d1..07609b943522 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -61,5 +61,12 @@ mockImages('images/product-illustrations'); jest.mock('../src/components/Icon/Expensicons', () => { const reduce = require('underscore').reduce; const Expensicons = jest.requireActual('../src/components/Icon/Expensicons'); - return reduce(Expensicons, (prev, _curr, key) => ({...prev, [key]: () => ''}), {}); + return reduce(Expensicons, (prev, _curr, key) => { + const fn = () => ''; + Object.defineProperty(fn, 'name', { + value: key, + configurable: true, + }); + return {...prev, [key]: fn}; + }, {}); }); diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 563f4ea8ca7c..98ef2aa5f97d 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -25,7 +25,6 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import * as App from '../../../libs/actions/App'; import * as ReportUtils from '../../../libs/ReportUtils'; import withCurrentUserPersonalDetails from '../../../components/withCurrentUserPersonalDetails'; -import withDrawerState from '../../../components/withDrawerState'; import withWindowDimensions from '../../../components/withWindowDimensions'; import Timing from '../../../libs/actions/Timing'; import reportActionPropTypes from '../report/reportActionPropTypes'; @@ -222,7 +221,6 @@ SidebarLinks.defaultProps = defaultProps; export default compose( withLocalize, withCurrentUserPersonalDetails, - withDrawerState, withWindowDimensions, withOnyx({ reports: { diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js index 42eac4ea43c2..ff3b97c3f778 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js @@ -18,6 +18,7 @@ import * as Policy from '../../../../libs/actions/Policy'; import Performance from '../../../../libs/Performance'; import * as Welcome from '../../../../libs/actions/Welcome'; import {sidebarPropTypes, sidebarDefaultProps} from './sidebarPropTypes'; +import withDrawerState from '../../../../components/withDrawerState'; const propTypes = { @@ -110,6 +111,7 @@ class BaseSidebarScreen extends Component { insets={insets} onAvatarClick={this.navigateToSettings} isSmallScreenWidth={this.props.isSmallScreenWidth} + isDrawerOpen={this.props.isDrawerOpen} /> { - const drawer = jest.requireActual('@react-navigation/drawer'); - return { - ...drawer, - useDrawerStatus() { - return 'open'; - }, - }; -}); - describe('Sidebar', () => { describe('in default mode', () => { // Clear out Onyx after each test so that each test starts with a clean slate From b3ab1ba3c7c0a99d9604f1df0562f7684f468222 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 20:03:49 -1000 Subject: [PATCH 149/155] improve comment and add explanation for icon name setting --- jest/setup.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 07609b943522..722e67007f13 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -62,11 +62,10 @@ jest.mock('../src/components/Icon/Expensicons', () => { const reduce = require('underscore').reduce; const Expensicons = jest.requireActual('../src/components/Icon/Expensicons'); return reduce(Expensicons, (prev, _curr, key) => { + // We set the name of the anonymous mock function here so we can dynamically build the list of mocks and access the + // "name" property to use in accessibility hints for element querying const fn = () => ''; - Object.defineProperty(fn, 'name', { - value: key, - configurable: true, - }); + Object.defineProperty(fn, 'name', {value: key}); return {...prev, [key]: fn}; }, {}); }); From 7b28f6dd6d413d35ba2c1ba8c736c6e3aaf4c3b6 Mon Sep 17 00:00:00 2001 From: Srikar Parsi Date: Fri, 16 Sep 2022 12:58:39 -0400 Subject: [PATCH 150/155] update expensify common dependency hash --- .env.temp | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .env.temp diff --git a/.env.temp b/.env.temp new file mode 100644 index 000000000000..193575ed4718 --- /dev/null +++ b/.env.temp @@ -0,0 +1 @@ +PUSHER_DEV_SUFFIX=-14fec3ac47964662914a7b6a69ba5e41 diff --git a/package.json b/package.json index fb1a983ae489..b64e917dd214 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", "dotenv": "^8.2.0", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", + "expensify-common": "git+https://github.com/Expensify/expensify-common.git#a9e791c1190052e934c20efdcd7cd4429b4680cb", "fbjs": "^3.0.2", "file-loader": "^6.0.0", "html-entities": "^1.3.1", From 8b438bb82adae212a56fc30743b24341a83e9265 Mon Sep 17 00:00:00 2001 From: Srikar Parsi Date: Fri, 16 Sep 2022 14:57:42 -0400 Subject: [PATCH 151/155] package-lock --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50f91287d1fd..4f5ed43862d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", "dotenv": "^8.2.0", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", + "expensify-common": "git+https://github.com/Expensify/expensify-common.git#a9e791c1190052e934c20efdcd7cd4429b4680cb", "fbjs": "^3.0.2", "file-loader": "^6.0.0", "html-entities": "^1.3.1", @@ -22569,8 +22569,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", - "integrity": "sha512-FiHBWAGkJ9UC+cpdB2bDY+2lAOklvFAIDzga9c2z3iTzEvhYYf2uCeTIjDFYIJviEibBkiIUELjDr9wwcepmNA==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a9e791c1190052e934c20efdcd7cd4429b4680cb", + "integrity": "sha512-5CmN+8u0OA/faTtOLDcswp92aRiJgJjIxCWeyp7J1hitFGRFs+T/1xk+/3FGvz8mzSnPc66ETf4e3kzTMWem2Q==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -59264,9 +59264,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", - "integrity": "sha512-FiHBWAGkJ9UC+cpdB2bDY+2lAOklvFAIDzga9c2z3iTzEvhYYf2uCeTIjDFYIJviEibBkiIUELjDr9wwcepmNA==", - "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#662e70f1f75ace4ab81365be312efbf28f16e0a3", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#a9e791c1190052e934c20efdcd7cd4429b4680cb", + "integrity": "sha512-5CmN+8u0OA/faTtOLDcswp92aRiJgJjIxCWeyp7J1hitFGRFs+T/1xk+/3FGvz8mzSnPc66ETf4e3kzTMWem2Q==", + "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#a9e791c1190052e934c20efdcd7cd4429b4680cb", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", From 6b8536a5eea2a474e880dc0f99258cc8bd22db08 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 16 Sep 2022 10:38:37 -1000 Subject: [PATCH 152/155] Make some requested changes and improve comments further --- .../@react-native-firebase/crashlytics.js | 7 +- __mocks__/react-native.js | 16 +++-- jest/setup.js | 7 -- src/CONST.js | 7 ++ src/libs/ReportActionsUtils.js | 2 +- tests/ui/UnreadIndicatorsTest.js | 72 +++++++++---------- .../utils/waitForPromisesToResolveWithAct.js | 12 ++++ 7 files changed, 73 insertions(+), 50 deletions(-) diff --git a/__mocks__/@react-native-firebase/crashlytics.js b/__mocks__/@react-native-firebase/crashlytics.js index ff8b4c56321a..0cd842b4765d 100644 --- a/__mocks__/@react-native-firebase/crashlytics.js +++ b/__mocks__/@react-native-firebase/crashlytics.js @@ -1 +1,6 @@ -export default {}; +// uses and we need to mock the imported crashlytics module +// due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 +export default { + log: jest.fn(), + recordError: jest.fn(), +}; diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js index e2630c1ef632..495745fa8c66 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.js @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import * as ReactNative from 'react-native'; import _ from 'underscore'; +import CONST from '../src/CONST'; jest.doMock('react-native', () => { let url = 'https://new.expensify.com/'; @@ -9,6 +10,13 @@ jest.doMock('react-native', () => { let appState = 'active'; let count = 0; const changeListeners = {}; + + // Tests will run with the app in a typical small screen size by default. We do this since the react-native test renderer + // runs against index.native.js source and so anything that is testing a component reliant on withWindowDimensions() + // would be most commonly assumed to be on a mobile phone vs. a tablet or desktop style view. This behavior can be + // overridden by explicitly setting the dimensions inside a test via Dimensions.set() + let dimensions = CONST.TESTING.SCREEN_SIZE.SMALL; + return Object.setPrototypeOf( { NativeModules: { @@ -53,10 +61,10 @@ jest.doMock('react-native', () => { Dimensions: { ...ReactNative.Dimensions, addEventListener: jest.fn(), - get: () => ({ - // Tests will run with the app in a typical small screen size - width: 300, height: 700, scale: 1, fontScale: 1, - }), + get: () => dimensions, + set: (newDimensions) => { + dimensions = newDimensions; + }, }, }, ReactNative, diff --git a/jest/setup.js b/jest/setup.js index 722e67007f13..d62bf1e6468e 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -16,13 +16,6 @@ jest.mock('react-native-reanimated', () => { return Reanimated; }); -// uses and we need to mock the imported crashlytics module -// due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 -jest.mock('@react-native-firebase/crashlytics', () => () => ({ - log: jest.fn(), - recordError: jest.fn(), -})); - // The main app uses a NativeModule called BootSplash to show/hide a splash screen. Since we can't use this in the node environment // where tests run we simulate a behavior where the splash screen is always hidden (similar to web which has no splash screen at all). jest.mock('../src/libs/BootSplash', () => ({ diff --git a/src/CONST.js b/src/CONST.js index bc443e5e3fba..c5f954527670 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -827,6 +827,13 @@ const CONST = { INCORRECT_PASSWORD: 2, }, }, + TESTING: { + SCREEN_SIZE: { + SMALL: { + width: 300, height: 700, scale: 1, fontScale: 1, + }, + }, + }, }; export default CONST; diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 1b49c722e642..86fbbfca799d 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -124,7 +124,7 @@ function getLastVisibleMessageText(reportID, actionsToMerge = {}) { * @param {Object} [actionsToMerge] * @param {Number} deletedSequenceNumber * @param {Number} lastReadSequenceNumber - * @return {String} + * @return {Number} */ function getOptimisticLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = {}, deletedSequenceNumber, lastReadSequenceNumber) { // If the action we are deleting is unread then just return the current last read sequence number diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 93630d4f661d..10adbab78db5 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -25,8 +25,6 @@ beforeAll(() => { // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. global.fetch = TestHelper.getGlobalFetchMock(); - - // We need a bit more time for this test in some places Linking.setInitialURL('https://new.expensify.com/r/1'); appSetup(); }); @@ -35,7 +33,7 @@ beforeAll(() => { * @param {RenderAPI} renderedApp */ function scrollUpToRevealNewMessagesBadge(renderedApp) { - fireEvent.scroll(renderedApp.getByA11yLabel('List of chat messages'), { + fireEvent.scroll(renderedApp.queryByA11yLabel('List of chat messages'), { nativeEvent: { contentOffset: { y: 250, @@ -63,7 +61,7 @@ function scrollUpToRevealNewMessagesBadge(renderedApp) { * @return {Boolean} */ function isNewMessagesBadgeVisible(renderedApp) { - const badge = renderedApp.getByA11yHint('Scroll to newest messages'); + const badge = renderedApp.queryByA11yHint('Scroll to newest messages'); return badge.props.style.transform[0].translateY === 10; } @@ -72,7 +70,7 @@ function isNewMessagesBadgeVisible(renderedApp) { * @return {Promise} */ function navigateToSidebar(renderedApp) { - const reportHeaderBackButton = renderedApp.getByA11yHint('Navigate back to chats list'); + const reportHeaderBackButton = renderedApp.queryByA11yHint('Navigate back to chats list'); fireEvent(reportHeaderBackButton, 'press'); return waitForPromisesToResolve(); } @@ -83,7 +81,7 @@ function navigateToSidebar(renderedApp) { * @return {Promise} */ function navigateToSidebarOption(renderedApp, index) { - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + const optionRows = renderedApp.queryAllByA11yHint('Navigates to a chat'); fireEvent(optionRows[index], 'press'); return waitForPromisesToResolve(); } @@ -116,7 +114,7 @@ function signInAndGetAppWithUnreadChat() { return waitForPromisesToResolveWithAct() .then(() => { const loginForm = renderedApp.queryAllByA11yLabel('Login form'); - expect(loginForm.length).toBe(1); + expect(loginForm).toHaveLength(1); return TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); }) @@ -172,19 +170,19 @@ describe('Unread Indicators', () => { renderedApp = testInstance; // Verify no notifications are created for these older messages - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); + expect(LocalNotification.showCommentNotification.mock.calls).toHaveLength(0); // Verify the sidebar links are rendered const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); - expect(sidebarLinks.length).toBe(1); + expect(sidebarLinks).toHaveLength(1); expect(isDrawerOpen(renderedApp)).toBe(true); // Verify there is only one option in the sidebar - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); - expect(optionRows.length).toBe(1); + const optionRows = renderedApp.queryAllByA11yHint('Navigates to a chat'); + expect(optionRows).toHaveLength(1); // And that the text is bold - const displayNameText = renderedApp.getByA11yLabel('Chat user display names'); + const displayNameText = renderedApp.queryByA11yLabel('Chat user display names'); expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); return navigateToSidebarOption(renderedApp, 0); @@ -194,15 +192,15 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(false); // That the report actions are visible along with the created action - const createdAction = renderedApp.getByA11yLabel('Chat welcome message'); + const createdAction = renderedApp.queryByA11yLabel('Chat welcome message'); expect(createdAction).toBeTruthy(); - const reportComments = renderedApp.getAllByA11yLabel('Chat message'); - expect(reportComments.length).toBe(9); + const reportComments = renderedApp.queryAllByA11yLabel('Chat message'); + expect(reportComments).toHaveLength(9); // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will // have a sequenceNumber of 2 const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(2); @@ -232,7 +230,7 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(true); // Verify that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); + const updatedDisplayNameText = renderedApp.queryByA11yLabel('Chat user display names'); expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); // Tap on the chat again @@ -241,7 +239,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the unread indicator is not present const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); expect(isDrawerOpen(renderedApp)).toBe(false); // Scroll and verify that the new messages badge is also hidden @@ -293,19 +291,19 @@ describe('Unread Indicators', () => { }) .then(() => { // Verify notification was created as the new message that has arrived is very recent - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); + expect(LocalNotification.showCommentNotification.mock.calls).toHaveLength(1); // // Navigate back to the sidebar return navigateToSidebar(renderedApp); }) .then(() => { // // Verify the new report option appears in the LHN - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); - expect(optionRows.length).toBe(2); + const optionRows = renderedApp.queryAllByA11yHint('Navigates to a chat'); + expect(optionRows).toHaveLength(2); // Verify the text for both chats are bold indicating that nothing has not yet been read const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); + expect(displayNameTexts).toHaveLength(2); const firstReportOption = displayNameTexts[0]; expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); @@ -320,7 +318,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); + expect(displayNameTexts).toHaveLength(2); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); @@ -346,7 +344,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the indicator appears above the last action const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(3); @@ -360,7 +358,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the report is marked as unread in the sidebar const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); + expect(displayNameTexts).toHaveLength(1); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); @@ -371,7 +369,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the report is now marked as read const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); + expect(displayNameTexts).toHaveLength(1); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); @@ -380,7 +378,7 @@ describe('Unread Indicators', () => { }) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); // Scroll up and verify the "New messages" badge is hidden scrollUpToRevealNewMessagesBadge(renderedApp); @@ -398,7 +396,7 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(true); const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); + expect(displayNameTexts).toHaveLength(1); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); @@ -407,7 +405,7 @@ describe('Unread Indicators', () => { }) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); // Leave a comment as the current user and verify the indicator is removed Report.addComment(REPORT_ID, 'Current User Comment 1'); @@ -415,7 +413,7 @@ describe('Unread Indicators', () => { }) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); }); }); @@ -429,7 +427,7 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(true); const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); + expect(displayNameTexts).toHaveLength(1); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); @@ -438,7 +436,7 @@ describe('Unread Indicators', () => { }) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); // Then back to the LHN - then back to the chat again and verify the new line indicator has cleared return navigateToSidebar(renderedApp); @@ -446,7 +444,7 @@ describe('Unread Indicators', () => { .then(() => navigateToSidebarOption(renderedApp, 0)) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); // Mark a previous comment as unread and verify the unread action indicator returns Report.markCommentAsUnread(REPORT_ID, 9); @@ -454,7 +452,7 @@ describe('Unread Indicators', () => { }) .then(() => { let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); // Trigger the app going inactive and active again AppState.emitCurrentTestState('background'); @@ -462,7 +460,7 @@ describe('Unread Indicators', () => { // Verify the new line is cleared unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); }); }); @@ -506,7 +504,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the chat preview text matches the last comment from the current user const alternateText = renderedApp.queryAllByA11yLabel('Last chat message preview'); - expect(alternateText.length).toBe(1); + expect(alternateText).toHaveLength(1); expect(alternateText[0].props.children).toBe('Current User Comment 1'); Report.deleteReportComment(REPORT_ID, lastReportAction); @@ -514,7 +512,7 @@ describe('Unread Indicators', () => { }) .then(() => { const alternateText = renderedApp.queryAllByA11yLabel('Last chat message preview'); - expect(alternateText.length).toBe(1); + expect(alternateText).toHaveLength(1); expect(alternateText[0].props.children).toBe('Comment 9'); }); }); diff --git a/tests/utils/waitForPromisesToResolveWithAct.js b/tests/utils/waitForPromisesToResolveWithAct.js index 37d5ccb041ce..eaef0f3b1a9d 100644 --- a/tests/utils/waitForPromisesToResolveWithAct.js +++ b/tests/utils/waitForPromisesToResolveWithAct.js @@ -10,6 +10,18 @@ import waitForPromisesToResolve from './waitForPromisesToResolve'; * This apparently will not work unless we use async/await because of some kind of voodoo magic inside the react-test-renderer * so we are suppressing our lint rule and avoiding async/await everywhere else in our tests. * + * When to use this: + * + * - If you see the jest output complaining about needing to use act() AND the callback inside the act() returns a promise. We can't + * call .then() on act() and the test renderer wants us to use async/await so use this function instead. + * + * + * When not to use this: + * + * - You're not rendering any react components at all in your tests, but have some async logic you need to wait for e.g. Onyx.merge(). Use waitForPromisesToResolve(). + * - You're writing UI tests but don't see any errors or warnings related to using act(). You probably don't need this in that case and should use waitForPromisesToResolve(). + * - You're writing UI test and do see a warning about using act(), but there's no asynchronous code that needs to run inside act(). + * * @returns {Promise} */ // eslint-disable-next-line @lwc/lwc/no-async-await From e961332cc29d847968c816b50ced538dfe5b278c Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 16 Sep 2022 16:36:39 -0600 Subject: [PATCH 153/155] fix lint errors --- src/libs/actions/Session/index.js | 5 ++++- src/libs/actions/SignInRedirect.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 3ee2a8afc3aa..696642d31ff2 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -250,7 +250,9 @@ function signIn(password, twoFactorAuthCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, isLoading: false}); return; } - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}, isLoading: false}); + + // eslint-disable-next-line rulesdir/prefer-localization + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: errorMessage}, isLoading: false}); return; } @@ -348,6 +350,7 @@ function setPassword(password, validateCode, accountID) { } // This request can fail if the password is not complex enough + // eslint-disable-next-line rulesdir/prefer-localization Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: response.message}}); }) .finally(() => { diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 6741fabfb4c8..c0388ef8d430 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -48,7 +48,8 @@ function clearStorageAndRedirect(errorMessage) { // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` if (errorMessage) { - Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}}); + // eslint-disable-next-line rulesdir/prefer-localization + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); } }); } From 37bfdfcc45730d464b12310f18675af0154e6451 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 16 Sep 2022 16:43:18 -0600 Subject: [PATCH 154/155] rm unused import --- src/libs/actions/SignInRedirect.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index c0388ef8d430..997470023ef6 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -3,7 +3,6 @@ import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; // eslint-disable-next-line import/no-cycle import DateUtils from '../DateUtils'; -import * as Localize from '../Localize'; let currentActiveClients; Onyx.connect({ From d36b1753a99f091880075a2425f3e491d908977b Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 16 Sep 2022 16:55:57 -0600 Subject: [PATCH 155/155] revert changes --- src/libs/actions/Session/index.js | 4 +--- src/libs/actions/SignInRedirect.js | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 696642d31ff2..86e2a0089cab 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -250,9 +250,7 @@ function signIn(password, twoFactorAuthCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, isLoading: false}); return; } - - // eslint-disable-next-line rulesdir/prefer-localization - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: errorMessage}, isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}, isLoading: false}); return; } diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 997470023ef6..6741fabfb4c8 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -3,6 +3,7 @@ import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; // eslint-disable-next-line import/no-cycle import DateUtils from '../DateUtils'; +import * as Localize from '../Localize'; let currentActiveClients; Onyx.connect({ @@ -47,8 +48,7 @@ function clearStorageAndRedirect(errorMessage) { // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` if (errorMessage) { - // eslint-disable-next-line rulesdir/prefer-localization - Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}}); } }); }