From 5ae65ead66ad2f87b2dcd4b1ab55c87a4b3a17ce Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 26 Jul 2023 16:19:52 -0700 Subject: [PATCH 001/548] Component for indicator messages with close button --- .../DotIndicatorMessageWithClose.js | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/components/DotIndicatorMessageWithClose.js diff --git a/src/components/DotIndicatorMessageWithClose.js b/src/components/DotIndicatorMessageWithClose.js new file mode 100644 index 000000000000..1a0770508eb3 --- /dev/null +++ b/src/components/DotIndicatorMessageWithClose.js @@ -0,0 +1,74 @@ +import React from 'react'; +import _ from 'underscore'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import styles from '../styles/styles'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import DotIndicatorMessage from './DotIndicatorMessage'; +import Tooltip from './Tooltip'; +import CONST from '../CONST'; +import * as StyleUtils from '../styles/StyleUtils'; +import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; +import stylePropTypes from '../styles/stylePropTypes'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; + +const propTypes = { + /** + * In most cases this should just be errors from onxyData + * if you are not passing that data then this needs to be in a similar shape like + * { + * timestamp: 'message', + * } + */ + messages: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))])), + + // The type of message, 'error' shows a red dot, 'success' shows a green dot + type: PropTypes.oneOf(['error', 'success']).isRequired, + + /** A function to run when the X button next to the message is clicked */ + onClose: PropTypes.func, + + /** Additional style object for the container*/ + containerStyles: stylePropTypes, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + messages: {}, + onClose: () => {}, + containerStyles: [], +}; + +function DotIndicatorMessageWithClose(props) { + if (_.isEmpty(props.messages)) { + return null; + } + + return ( + + + + + + + + + ); +} + +DotIndicatorMessageWithClose.propTypes = propTypes; +DotIndicatorMessageWithClose.defaultProps = defaultProps; +DotIndicatorMessageWithClose.displayName = 'DotIndicatorMessageWithClose'; + +export default withLocalize(DotIndicatorMessageWithClose); From 1039385b4d1c94b7f7586ea3aff57e716e171391 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 26 Jul 2023 16:21:29 -0700 Subject: [PATCH 002/548] Show message when members added with primary login --- src/languages/en.js | 1 + src/pages/workspace/WorkspaceMembersPage.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/languages/en.js b/src/languages/en.js index b7a130addf18..a0f73bc41788 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1150,6 +1150,7 @@ export default { cannotRemove: 'You cannot remove yourself or the workspace owner.', genericRemove: 'There was a problem removing that workspace member.', }, + addedWithPrimary: 'Some users were added with their primary logins.', }, card: { header: 'Unlock free Expensify Cards', diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index e758d738d964..028420d60093 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -38,6 +38,7 @@ import PressableWithFeedback from '../../components/Pressable/PressableWithFeedb import usePrevious from '../../hooks/usePrevious'; import Log from '../../libs/Log'; import * as PersonalDetailsUtils from '../../libs/PersonalDetailsUtils'; +import DotIndicatorMessageWithClose from '../../components/DotIndicatorMessageWithClose'; const propTypes = { /** All personal details asssociated with user */ @@ -397,6 +398,7 @@ function WorkspaceMembersPage(props) { }); const policyID = lodashGet(props.route, 'params.policyID'); const policyName = lodashGet(props.policy, 'name'); + const primaryLoginsInvited = props.policy.primaryLoginsInvited || {}; return ( + {data.length > 0 ? ( From fa3adb21e53d8cdb0460c7fe739ba253bbbb6847 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 26 Jul 2023 16:21:57 -0700 Subject: [PATCH 003/548] Style fix --- src/components/DotIndicatorMessageWithClose.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/DotIndicatorMessageWithClose.js b/src/components/DotIndicatorMessageWithClose.js index 1a0770508eb3..fd535475583f 100644 --- a/src/components/DotIndicatorMessageWithClose.js +++ b/src/components/DotIndicatorMessageWithClose.js @@ -29,7 +29,7 @@ const propTypes = { /** A function to run when the X button next to the message is clicked */ onClose: PropTypes.func, - /** Additional style object for the container*/ + /** Additional style object for the container */ containerStyles: stylePropTypes, ...withLocalizePropTypes, @@ -47,7 +47,7 @@ function DotIndicatorMessageWithClose(props) { } return ( - + Date: Wed, 26 Jul 2023 16:29:40 -0700 Subject: [PATCH 004/548] Allow dismissing added with primary message --- src/libs/actions/Policy.js | 5 +++++ src/pages/workspace/WorkspaceMembersPage.js | 1 + 2 files changed, 6 insertions(+) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index ebf9f100bc90..b1364dd9c373 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1144,6 +1144,10 @@ function clearErrors(policyID) { hideWorkspaceAlertMessage(policyID); } +function dismissAddedWithPrimaryMessages(policyID) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {primaryLoginsInvited: null}); +} + export { removeMembers, addMembersToWorkspace, @@ -1173,4 +1177,5 @@ export { setWorkspaceInviteMembersDraft, isPolicyOwner, clearErrors, + dismissAddedWithPrimaryMessages, }; diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 028420d60093..7c396aa0f65d 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -460,6 +460,7 @@ function WorkspaceMembersPage(props) { type="success" messages={_.isEmpty(primaryLoginsInvited) ? null : {0: props.translate('workspace.people.addedWithPrimary')}} containerStyles={[styles.pt3]} + onClose={() => Policy.dismissAddedWithPrimaryMessages(props.route.params.policyID)} /> {data.length > 0 ? ( From 8a449b1a8b70fdc1fb4a82038a5d94f0b65b1a2a Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 26 Jul 2023 18:02:22 -0700 Subject: [PATCH 005/548] Show added by secondary login messages --- src/languages/en.js | 1 + src/pages/workspace/WorkspaceMembersPage.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index a0f73bc41788..b35bf44f6159 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1151,6 +1151,7 @@ export default { genericRemove: 'There was a problem removing that workspace member.', }, addedWithPrimary: 'Some users were added with their primary logins.', + invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`, }, card: { header: 'Unlock free Expensify Cards', diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 7c396aa0f65d..3dadf76b2431 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -342,6 +342,7 @@ function WorkspaceMembersPage(props) { }} onSelectRow={() => toggleUser(item.accountID, item.pendingAction)} /> + {Boolean(item.invitedSecondaryLogin) && {props.translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})}} {(props.session.accountID === item.accountID || item.role === 'admin') && ( @@ -362,6 +363,7 @@ function WorkspaceMembersPage(props) { [selectedEmployees, errors, props.session.accountID, dismissError, toggleUser], ); + const invitedSecondaryToPrimaryLogins = _.invert(props.policy.primaryLoginsInvited); const policyOwner = lodashGet(props.policy, 'owner'); const currentUserLogin = lodashGet(props.currentUserPersonalDetails, 'login'); const removableMembers = {}; @@ -378,6 +380,7 @@ function WorkspaceMembersPage(props) { data.push({ ...policyMember, ...details, + invitedSecondaryLogin: invitedSecondaryToPrimaryLogins[details.login] || '', }); }); data = _.sortBy(data, (value) => value.displayName.toLowerCase()); @@ -398,7 +401,6 @@ function WorkspaceMembersPage(props) { }); const policyID = lodashGet(props.route, 'params.policyID'); const policyName = lodashGet(props.policy, 'name'); - const primaryLoginsInvited = props.policy.primaryLoginsInvited || {}; return ( Policy.dismissAddedWithPrimaryMessages(props.route.params.policyID)} /> From 3afc37b3a183f25584a56acc7759128f0d0d1896 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 3 Aug 2023 18:23:29 -0700 Subject: [PATCH 006/548] Reuse component where it was extracted from --- src/components/OfflineWithFeedback.js | 29 +++++++-------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js index 820cce252205..87963ed26fd9 100644 --- a/src/components/OfflineWithFeedback.js +++ b/src/components/OfflineWithFeedback.js @@ -9,13 +9,9 @@ import CONST from '../CONST'; import networkPropTypes from './networkPropTypes'; import stylePropTypes from '../styles/stylePropTypes'; import styles from '../styles/styles'; -import Tooltip from './Tooltip'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; import * as StyleUtils from '../styles/StyleUtils'; -import DotIndicatorMessage from './DotIndicatorMessage'; import shouldRenderOffscreen from '../libs/shouldRenderOffscreen'; -import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; +import DotIndicatorMessageWithClose from './DotIndicatorMessageWithClose'; /** * This component should be used when we are using the offline pattern B (offline with feedback). @@ -116,23 +112,12 @@ function OfflineWithFeedback(props) { )} {props.shouldShowErrorMessages && hasErrorMessages && ( - - - - - - - - + )} ); From d4c2c737245b230484be65a2f31faf40ddf8c5f6 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 3 Aug 2023 18:23:37 -0700 Subject: [PATCH 007/548] style --- src/pages/workspace/WorkspaceMembersPage.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 3dadf76b2431..647c35b4f6b3 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -342,7 +342,11 @@ function WorkspaceMembersPage(props) { }} onSelectRow={() => toggleUser(item.accountID, item.pendingAction)} /> - {Boolean(item.invitedSecondaryLogin) && {props.translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})}} + {Boolean(item.invitedSecondaryLogin) && ( + + {props.translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})} + + )} {(props.session.accountID === item.accountID || item.role === 'admin') && ( From 9157c287179821b57de11d482fc64134ce615a2b Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 3 Aug 2023 18:36:09 -0700 Subject: [PATCH 008/548] Add informative comments, better variable name --- src/components/DotIndicatorMessage.js | 8 +------- src/libs/actions/Policy.js | 5 +++++ src/pages/workspace/WorkspaceMembersPage.js | 8 +++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/DotIndicatorMessage.js b/src/components/DotIndicatorMessage.js index ac550f34de3f..1073f8c9aedc 100644 --- a/src/components/DotIndicatorMessage.js +++ b/src/components/DotIndicatorMessage.js @@ -10,13 +10,7 @@ import Text from './Text'; import * as Localize from '../libs/Localize'; const propTypes = { - /** - * In most cases this should just be errors from onxyData - * if you are not passing that data then this needs to be in a similar shape like - * { - * timestamp: 'message', - * } - */ + // The error messages to display messages: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))])), // The type of message, 'error' shows a red dot, 'success' shows a green dot diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index b1364dd9c373..28ebb3d29e09 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1144,6 +1144,11 @@ function clearErrors(policyID) { hideWorkspaceAlertMessage(policyID); } +/** + * Dismiss the informative messages about which policy members were added with primary logins when invited with their secondary login. + * + * @param {String} policyID + */ function dismissAddedWithPrimaryMessages(policyID) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {primaryLoginsInvited: null}); } diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 647c35b4f6b3..018ffb717908 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -367,7 +367,7 @@ function WorkspaceMembersPage(props) { [selectedEmployees, errors, props.session.accountID, dismissError, toggleUser], ); - const invitedSecondaryToPrimaryLogins = _.invert(props.policy.primaryLoginsInvited); + const invitedPrimaryToSecondaryLogins = _.invert(props.policy.primaryLoginsInvited); const policyOwner = lodashGet(props.policy, 'owner'); const currentUserLogin = lodashGet(props.currentUserPersonalDetails, 'login'); const removableMembers = {}; @@ -384,7 +384,9 @@ function WorkspaceMembersPage(props) { data.push({ ...policyMember, ...details, - invitedSecondaryLogin: invitedSecondaryToPrimaryLogins[details.login] || '', + + // Note which secondary login was used to invite this primary login + invitedSecondaryLogin: invitedPrimaryToSecondaryLogins[details.login] || '', }); }); data = _.sortBy(data, (value) => value.displayName.toLowerCase()); @@ -464,7 +466,7 @@ function WorkspaceMembersPage(props) { /> Policy.dismissAddedWithPrimaryMessages(props.route.params.policyID)} /> From 0a28b29e21703cdee9083c317c54c182b7fd82a3 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 3 Aug 2023 19:22:53 -0700 Subject: [PATCH 009/548] Add spanish translations --- src/languages/es.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/languages/es.js b/src/languages/es.js index 4006f559eb1f..f2837c60b5e7 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1159,6 +1159,8 @@ export default { cannotRemove: 'No puedes eliminarte ni a ti mismo ni al dueño del espacio de trabajo.', genericRemove: 'Ha ocurrido un problema al eliminar al miembro del espacio de trabajo.', }, + addedWithPrimary: 'Se agregaron algunos usuarios con sus inicios de sesión principales.', + invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por inicio de sesión secundario ${secondaryLogin}.`, }, card: { header: 'Desbloquea Tarjetas Expensify gratis', From f88a4560795434cfe55384c37f30e9d43b9b4fb6 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 3 Aug 2023 19:26:25 -0700 Subject: [PATCH 010/548] Change comment in proper file --- src/components/DotIndicatorMessage.js | 8 +++++++- src/components/DotIndicatorMessageWithClose.js | 8 +------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/DotIndicatorMessage.js b/src/components/DotIndicatorMessage.js index 1073f8c9aedc..ac550f34de3f 100644 --- a/src/components/DotIndicatorMessage.js +++ b/src/components/DotIndicatorMessage.js @@ -10,7 +10,13 @@ import Text from './Text'; import * as Localize from '../libs/Localize'; const propTypes = { - // The error messages to display + /** + * In most cases this should just be errors from onxyData + * if you are not passing that data then this needs to be in a similar shape like + * { + * timestamp: 'message', + * } + */ messages: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))])), // The type of message, 'error' shows a red dot, 'success' shows a green dot diff --git a/src/components/DotIndicatorMessageWithClose.js b/src/components/DotIndicatorMessageWithClose.js index fd535475583f..7626a956489f 100644 --- a/src/components/DotIndicatorMessageWithClose.js +++ b/src/components/DotIndicatorMessageWithClose.js @@ -14,13 +14,7 @@ import stylePropTypes from '../styles/stylePropTypes'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; const propTypes = { - /** - * In most cases this should just be errors from onxyData - * if you are not passing that data then this needs to be in a similar shape like - * { - * timestamp: 'message', - * } - */ + // The error messages to display messages: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))])), // The type of message, 'error' shows a red dot, 'success' shows a green dot From 1a9d3284a1eaf165b1119d5bbc50231747a494e3 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Mon, 7 Aug 2023 17:10:30 -0700 Subject: [PATCH 011/548] Update with native speaker translations --- src/languages/es.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.js b/src/languages/es.js index 3e2d5b820f9e..8592f55d998c 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1184,8 +1184,8 @@ export default { cannotRemove: 'No puedes eliminarte ni a ti mismo ni al dueño del espacio de trabajo.', genericRemove: 'Ha ocurrido un problema al eliminar al miembro del espacio de trabajo.', }, - addedWithPrimary: 'Se agregaron algunos usuarios con sus inicios de sesión principales.', - invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por inicio de sesión secundario ${secondaryLogin}.`, + addedWithPrimary: 'Se agregaron algunos usuarios con sus nombres de usuario principales.', + invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`, }, card: { header: 'Desbloquea Tarjetas Expensify gratis', From 426707f3542c963bf1f268297259d7b21ce9f3ed Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 29 Aug 2023 12:14:41 +0200 Subject: [PATCH 012/548] add getOlderAction and getNewerAction API --- src/libs/actions/Report.js | 94 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 8b898a6aaaea..28adac72dc50 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -721,6 +721,98 @@ function readOldestAction(reportID, reportActionID) { ); } +/** + * Gets the older actions that have not been read yet. + * Normally happens when you scroll up on a chat, and the actions have not been read yet. + * + * @param {String} reportID + * @param {String} reportActionID + */ +function getOlderAction(reportID, reportActionID) { + API.read( + 'GetOlderActions', + { + reportID, + reportActionID, + }, + { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + isLoadingOlderReportActions: true, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + isLoadingOlderReportActions: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + isLoadingOlderReportActions: false, + }, + }, + ], + }, + ); +} + +/** + * Gets the newer actions that have not been read yet. + * Normally happens when you located not in the edge of the list and scroll down on a chat. + * + * @param {String} reportID + * @param {String} reportActionID + */ +function getNewerAction(reportID, reportActionID) { + API.read( + 'GetNewerActions', + { + reportID, + reportActionID, + }, + { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + isLoadingNewerReportActions: true, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + isLoadingNewerReportActions: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + isLoadingNewerReportActions: false, + }, + }, + ], + }, + ); +} + /** * Gets metadata info about links in the provided report action * @@ -1981,4 +2073,6 @@ export { setLastOpenedPublicRoom, flagComment, openLastOpenedPublicRoom, + getOlderAction, + getNewerAction, }; From cfa1c1ef8950aeac57a7a43695455f23bcf9ee10 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 29 Aug 2023 12:21:40 +0200 Subject: [PATCH 013/548] use getOlderAction instead of getOlderAction --- src/pages/home/report/ReportActionsView.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index da475e61f749..d4c410ca03b3 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -138,7 +138,7 @@ function ReportActionsView(props) { */ const loadMoreChats = () => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. - if (props.report.isLoadingMoreReportActions) { + if (props.report.isLoadingOlderReportActions) { return; } @@ -150,7 +150,7 @@ function ReportActionsView(props) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments - Report.readOldestAction(reportID, oldestReportAction.reportActionID); + Report.getOlderAction(reportID, oldestReportAction.reportActionID); }; /** @@ -185,7 +185,7 @@ function ReportActionsView(props) { onLayout={recordTimeToMeasureItemLayout} sortedReportActions={props.reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID.current} - isLoadingMoreReportActions={props.report.isLoadingMoreReportActions} + isLoadingMoreReportActions={props.report.isLoadingOlderReportActions} loadMoreChats={loadMoreChats} policy={props.policy} /> @@ -215,7 +215,7 @@ function arePropsEqual(oldProps, newProps) { return false; } - if (oldProps.report.isLoadingMoreReportActions !== newProps.report.isLoadingMoreReportActions) { + if (oldProps.report.isLoadingOlderReportActions !== newProps.report.isLoadingOlderReportActions) { return false; } From 42fe76a791c8f459b37700034db9c0847a8aa0c3 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 6 Sep 2023 11:22:00 +0200 Subject: [PATCH 014/548] update getNewerReportActions --- .../InvertedFlatList/BaseInvertedFlatList.js | 3 +++ src/libs/actions/Report.js | 12 ++++++---- src/pages/home/ReportScreen.js | 1 + src/pages/home/report/ReportActionsList.js | 18 ++++++++------ src/pages/home/report/ReportActionsView.js | 24 ++++++++++++++++--- src/pages/reportPropTypes.js | 5 +++- src/types/onyx/Report.ts | 5 +++- 7 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index d3fcda0ea5fd..50150c0a1813 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -143,6 +143,9 @@ class BaseInvertedFlatList extends Component { // Commenting the line below as it breaks the unread indicator test // we will look at fixing/reusing this after RN v0.72 // maintainVisibleContentPosition={{minIndexForVisible: 0, autoscrollToTopThreshold: 0}} + maintainVisibleContentPosition={{ + minIndexForVisible: 0, + }} /> ); } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 28adac72dc50..78214e87ecd1 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -408,7 +408,8 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p ? {} : { isLoadingReportActions: true, - isLoadingMoreReportActions: false, + isLoadingOlderReportActions: false, + isLoadingNewerReportActions: false, reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), }, }; @@ -648,7 +649,8 @@ function reconnect(reportID) { key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { isLoadingReportActions: true, - isLoadingMoreReportActions: false, + isLoadingNewerReportActions: false, + isLoadingOlderReportActions: false, reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), }, }, @@ -695,7 +697,7 @@ function readOldestAction(reportID, reportActionID) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - isLoadingMoreReportActions: true, + isLoadingOlderReportActions: true, }, }, ], @@ -704,7 +706,7 @@ function readOldestAction(reportID, reportActionID) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - isLoadingMoreReportActions: false, + isLoadingOlderReportActions: false, }, }, ], @@ -713,7 +715,7 @@ function readOldestAction(reportID, reportActionID) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - isLoadingMoreReportActions: false, + isLoadingOlderReportActions: false, }, }, ], diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 6daa15785921..2f5bcae08f06 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -96,6 +96,7 @@ const defaultProps = { report: { hasOutstandingIOU: false, isLoadingReportActions: false, + isLoadingNewerReportActions: false, }, isComposerFullSize: false, betas: [], diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 7f897ee825fb..44b45d8ea310 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -35,7 +35,7 @@ const propTypes = { mostRecentIOUReportActionID: PropTypes.string, /** Are we loading more report actions? */ - isLoadingMoreReportActions: PropTypes.bool, + isLoadingOlderReportActions: PropTypes.bool, /** Callback executed on list layout */ onLayout: PropTypes.func.isRequired, @@ -44,7 +44,7 @@ const propTypes = { onScroll: PropTypes.func, /** Function to load more chats */ - loadMoreChats: PropTypes.func.isRequired, + loadOlderChats: PropTypes.func.isRequired, /** The policy object for the current route */ policy: PropTypes.shape({ @@ -63,7 +63,8 @@ const defaultProps = { personalDetails: {}, onScroll: () => {}, mostRecentIOUReportActionID: '', - isLoadingMoreReportActions: false, + isLoadingOlderReportActions: false, + isLoadingNewerReportActions: false, ...withCurrentUserPersonalDetailsDefaultProps, }; @@ -100,7 +101,8 @@ function ReportActionsList({ personalDetailsList, currentUserPersonalDetails, hasOutstandingIOU, - loadMoreChats, + loadNewerChats, + loadOlderChats, onLayout, isComposerFullSize, }) { @@ -213,7 +215,7 @@ function ReportActionsList({ const initialNumToRender = useMemo(() => { const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); - return Math.ceil(availableHeight / minimumReportActionHeight); + return Math.ceil(availableHeight / minimumReportActionHeight); // }, [windowHeight]); /** @@ -297,10 +299,12 @@ function ReportActionsList({ keyExtractor={keyExtractor} initialRowHeight={32} initialNumToRender={initialNumToRender} - onEndReached={loadMoreChats} + onEndReached={loadOlderChats} onEndReachedThreshold={0.75} + onStartReached={loadNewerChats} + onStartReachedThreshold={0.75} ListFooterComponent={() => { - if (report.isLoadingMoreReportActions) { + if (report.isLoadingOlderReportActions) { return ; } diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index d4c410ca03b3..ab9d58c1011b 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -59,6 +59,7 @@ function ReportActionsView(props) { useCopySelectionHelper(); const didLayout = useRef(false); + const isFirstRender = useRef(true); const didSubscribeToReportTypingEvents = useRef(false); const hasCachedActions = useRef(_.size(props.reportActions) > 0); @@ -136,7 +137,7 @@ function ReportActionsView(props) { * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadMoreChats = () => { + const loadOlderChats = () => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. if (props.report.isLoadingOlderReportActions) { return; @@ -153,6 +154,21 @@ function ReportActionsView(props) { Report.getOlderAction(reportID, oldestReportAction.reportActionID); }; + /** + * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently + * displaying. + */ + const loadNewerChats = () => { + // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. + if (props.report.isLoadingNewerReportActions || isFirstRender.current || props.report.isLoadingReportActions || props.report.isLoadingOlderReportActions) { + isFirstRender.current = false; + return; + } + const newestReportAction = _.first(props.reportActions); + + Report.getNewerAction(reportID, newestReportAction.reportActionID); + }; + /** * Runs when the FlatList finishes laying out */ @@ -185,8 +201,10 @@ function ReportActionsView(props) { onLayout={recordTimeToMeasureItemLayout} sortedReportActions={props.reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID.current} - isLoadingMoreReportActions={props.report.isLoadingOlderReportActions} - loadMoreChats={loadMoreChats} + isLoadingOlderReportActions={props.report.isLoadingOlderReportActions} + isLoadingNewerReportActions={props.report.isLoadingNewerReportActions} + loadOlderChats={loadOlderChats} + loadNewerChats={loadNewerChats} policy={props.policy} /> diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index da90e0a4ac5c..66396ba661ce 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -14,7 +14,10 @@ export default PropTypes.shape({ icons: PropTypes.arrayOf(avatarPropTypes), /** Are we loading more report actions? */ - isLoadingMoreReportActions: PropTypes.bool, + isLoadingOlderReportActions: PropTypes.bool, + + /** Are we loading newer report actions? */ + isLoadingNewerReportActions: PropTypes.bool, /** Flag to check if the report actions data are loading */ isLoadingReportActions: PropTypes.bool, diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 8660837ba874..a704e72c7a16 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -13,7 +13,10 @@ type Report = { icons?: OnyxCommon.Icon[]; /** Are we loading more report actions? */ - isLoadingMoreReportActions?: boolean; + isLoadingOlderReportActions?: boolean; + + /** Are we loading newer report actions? */ + isLoadingNewerReportActions?: boolean; /** Flag to check if the report actions data are loading */ isLoadingReportActions?: boolean; From 66483bfe391a5b54d8e46ce2592a1d6714f53b88 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 9 Sep 2023 11:45:31 +0200 Subject: [PATCH 015/548] remove comments --- src/pages/home/report/ReportActionsList.js | 2 +- src/pages/home/report/ReportActionsView.js | 33 ++++++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 44b45d8ea310..b3f6a8dc0d59 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -215,7 +215,7 @@ function ReportActionsList({ const initialNumToRender = useMemo(() => { const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); - return Math.ceil(availableHeight / minimumReportActionHeight); // + return Math.ceil(availableHeight / minimumReportActionHeight); }, [windowHeight]); /** diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ab9d58c1011b..d3409afb9d5c 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,4 +1,4 @@ -import React, {useRef, useEffect, useContext, useMemo} from 'react'; +import React, {useRef, useEffect, useContext, useMemo, useCallback} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashGet from 'lodash/get'; @@ -133,41 +133,51 @@ function ReportActionsView(props) { } }, [props.report, didSubscribeToReportTypingEvents, reportID]); + const {oldestReportAction, newestReportAction} = useMemo(() => { + const lengthOfReportActions = props.reportActions.length; + if (lengthOfReportActions === 0) { + return { + oldestReportAction: null, + newestReportAction: null, + }; + } + return { + oldestReportAction: props.reportActions[0], + newestReportAction: props.reportActions[lengthOfReportActions - 1], + }; + }, [props.reportActions]); + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadOlderChats = () => { + const loadOlderChats = useCallback(() => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. if (props.report.isLoadingOlderReportActions) { return; } - const oldestReportAction = _.last(props.reportActions); - // Don't load more chats if we're already at the beginning of the chat history if (oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { return; } - // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderAction(reportID, oldestReportAction.reportActionID); - }; + }, [props.report.isLoadingOlderReportActions, oldestReportAction]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadNewerChats = () => { + const loadNewerChats = useCallback(() => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. if (props.report.isLoadingNewerReportActions || isFirstRender.current || props.report.isLoadingReportActions || props.report.isLoadingOlderReportActions) { isFirstRender.current = false; return; } - const newestReportAction = _.first(props.reportActions); Report.getNewerAction(reportID, newestReportAction.reportActionID); - }; + }, [props.report.isLoadingNewerReportActions, props.report.isLoadingReportActions, props.report.isLoadingOlderReportActions, props.reportActions, newestReportAction]); /** * Runs when the FlatList finishes laying out @@ -201,8 +211,6 @@ function ReportActionsView(props) { onLayout={recordTimeToMeasureItemLayout} sortedReportActions={props.reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID.current} - isLoadingOlderReportActions={props.report.isLoadingOlderReportActions} - isLoadingNewerReportActions={props.report.isLoadingNewerReportActions} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} policy={props.policy} @@ -236,6 +244,9 @@ function arePropsEqual(oldProps, newProps) { if (oldProps.report.isLoadingOlderReportActions !== newProps.report.isLoadingOlderReportActions) { return false; } + if (oldProps.report.isLoadingNewerReportActions !== newProps.report.isLoadingNewerReportActions) { + return false; + } if (oldProps.report.isLoadingReportActions !== newProps.report.isLoadingReportActions) { return false; From 2f69a8328209b6eb8649c12f6ac559f1fb93bcdc Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 11 Sep 2023 21:51:09 +0200 Subject: [PATCH 016/548] maxToRenderPerBatch=5 --- .../InvertedFlatList/BaseInvertedFlatList.js | 3 +-- src/pages/home/report/ReportActionsList.js | 7 +++++ src/pages/home/report/ReportActionsView.js | 26 ++++++------------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 50150c0a1813..4ad3cb37e13d 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -137,12 +137,11 @@ class BaseInvertedFlatList extends Component { // Web requires that items be measured or else crazy things happen when scrolling. getItemLayout={this.props.shouldMeasureItems ? this.getItemLayout : undefined} // We keep this property very low so that chat switching remains fast - maxToRenderPerBatch={1} + maxToRenderPerBatch={5} windowSize={15} // Commenting the line below as it breaks the unread indicator test // we will look at fixing/reusing this after RN v0.72 - // maintainVisibleContentPosition={{minIndexForVisible: 0, autoscrollToTopThreshold: 0}} maintainVisibleContentPosition={{ minIndexForVisible: 0, }} diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index b3f6a8dc0d59..1336b2d42ecd 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -323,6 +323,13 @@ function ReportActionsList({ return null; }} + ListHeaderComponent={() => { + if (report.isLoadingOlderReportActions) { + return ; + } + + return null; + }} keyboardShouldPersistTaps="handled" onLayout={(event) => { setSkeletonViewHeight(event.nativeEvent.layout.height); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f0ded5e451b3..1d477abf4d1c 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -133,51 +133,41 @@ function ReportActionsView(props) { } }, [props.report, didSubscribeToReportTypingEvents, reportID]); - const {oldestReportAction, newestReportAction} = useMemo(() => { - const lengthOfReportActions = props.reportActions.length; - if (lengthOfReportActions === 0) { - return { - oldestReportAction: null, - newestReportAction: null, - }; - } - return { - oldestReportAction: props.reportActions[0], - newestReportAction: props.reportActions[lengthOfReportActions - 1], - }; - }, [props.reportActions]); - /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadOlderChats = useCallback(() => { + const loadOlderChats = () => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. if (props.report.isLoadingOlderReportActions) { return; } + const oldestReportAction = _.last(props.reportActions); + // Don't load more chats if we're already at the beginning of the chat history if (oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderAction(reportID, oldestReportAction.reportActionID); - }, [props.report.isLoadingOlderReportActions, oldestReportAction]); + }; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadNewerChats = useCallback(() => { + const loadNewerChats = () => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. if (props.report.isLoadingNewerReportActions || isFirstRender.current || props.report.isLoadingReportActions || props.report.isLoadingOlderReportActions) { isFirstRender.current = false; return; } + const newestReportAction = _.first(props.reportActions); + Report.getNewerAction(reportID, newestReportAction.reportActionID); - }, [props.report.isLoadingNewerReportActions, props.report.isLoadingReportActions, props.report.isLoadingOlderReportActions, props.reportActions, newestReportAction]); + }; /** * Runs when the FlatList finishes laying out From cff45ee5e15adcd382908a4276a490395d1db5df Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 13 Sep 2023 13:20:40 +0800 Subject: [PATCH 017/548] fix iou CREATED and transaction action created time is the same --- src/libs/ReportUtils.js | 9 ++++++--- src/libs/actions/IOU.js | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 3c4944ef1a5e..8239634b2a8b 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2067,6 +2067,7 @@ function getIOUReportActionMessage(iouReportID, type, total, comment, currency, * @param {Boolean} [isSendMoneyFlow] - Whether this is send money flow * @param {Object} [receipt] * @param {Boolean} [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat + * @param {String} [created] - Action created time * @returns {Object} */ function buildOptimisticIOUReportAction( @@ -2082,6 +2083,7 @@ function buildOptimisticIOUReportAction( isSendMoneyFlow = false, receipt = {}, isOwnPolicyExpenseChat = false, + created = DateUtils.getDBTime(), ) { const IOUReportID = iouReportID || generateReportID(); @@ -2139,7 +2141,7 @@ function buildOptimisticIOUReportAction( ], reportActionID: NumberUtils.rand64(), shouldShow: true, - created: DateUtils.getDBTime(), + created, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, receipt, whisperedToAccountIDs: !_.isEmpty(receipt) ? [currentUserAccountID] : [], @@ -2400,9 +2402,10 @@ function buildOptimisticChatReport( /** * Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically * @param {String} emailCreatingAction + * @param {String} [created] - Action created time * @returns {Object} */ -function buildOptimisticCreatedReportAction(emailCreatingAction) { +function buildOptimisticCreatedReportAction(emailCreatingAction, created = DateUtils.getDBTime()) { return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, @@ -2429,7 +2432,7 @@ function buildOptimisticCreatedReportAction(emailCreatingAction) { ], automatic: false, avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatar(currentUserAccountID)), - created: DateUtils.getDBTime(), + created, shouldShow: true, }; } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 5b79fb6ad4bb..7ea1fbd1dff6 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -459,8 +459,9 @@ function getMoneyRequestInformation( // 3. IOU action for the iouReport // 4. REPORTPREVIEW action for the chatReport // Note: The CREATED action for the IOU report must be optimistically generated before the IOU action so there's no chance that it appears after the IOU action in the chat + const currentTime = DateUtils.getDBTime(); const optimisticCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); - const optimisticCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); + const optimisticCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail, DateUtils.subtractMillisecondsFromDateTime(currentTime, 1)); const iouAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.CREATE, amount, @@ -473,6 +474,8 @@ function getMoneyRequestInformation( false, false, receiptObject, + false, + currentTime, ); let reportPreviewAction = isNewIOUReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID); From d7a37cd79e82e263baf58e1053cdd3aec79fee5f Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 13 Sep 2023 13:32:46 +0800 Subject: [PATCH 018/548] fix iou CREATED and transaction action created time is the same --- src/libs/actions/IOU.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7ea1fbd1dff6..a0787cfaca84 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -875,8 +875,9 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco // 3. IOU action for the iouReport // 4. REPORTPREVIEW action for the chatReport // Note: The CREATED action for the IOU report must be optimistically generated before the IOU action so there's no chance that it appears after the IOU action in the chat + const currentTime = DateUtils.getDBTime(); const oneOnOneCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); - const oneOnOneCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); + const oneOnOneCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit, DateUtils.subtractMillisecondsFromDateTime(currentTime, 1)); const oneOnOneIOUAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.CREATE, splitAmount, @@ -886,6 +887,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco oneOnOneTransaction.transactionID, '', oneOnOneIOUReport.reportID, + undefined, + undefined, + undefined, + undefined, + currentTime, ); // Add optimistic personal details for new participants From 702c960fb6160656aa20eeb563a1392942660461 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 13 Sep 2023 14:20:29 +0800 Subject: [PATCH 019/548] update test --- tests/actions/IOUTest.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index afb06cdb6fb3..902b669b287a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -92,7 +92,7 @@ describe('actions/IOU', () => { iouAction = iouActions[0]; // The CREATED action should not be created after the IOU action - expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + expect(Date.parse(createdAction.created)).toBeLessThan(Date.parse(iouAction.created)); // The IOUReportID should be correct expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); @@ -199,6 +199,7 @@ describe('actions/IOU', () => { }; let iouReportID; let iouAction; + let iouCreatedAction; let transactionID; fetch.pause(); return Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport) @@ -247,10 +248,11 @@ describe('actions/IOU', () => { // The chat report should have a CREATED and an IOU action expect(_.size(allIOUReportActions)).toBe(2); + iouCreatedAction = _.find(allIOUReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); iouAction = _.find(allIOUReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); // The CREATED action should not be created after the IOU action - expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + expect(Date.parse(iouCreatedAction.created)).toBeLessThan(Date.parse(iouAction.created)); // The IOUReportID should be correct expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); @@ -582,7 +584,7 @@ describe('actions/IOU', () => { iouAction = iouActions[0]; // The CREATED action should not be created after the IOU action - expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + expect(Date.parse(createdAction.created)).toBeLessThan(Date.parse(iouAction.created)); // The IOUReportID should be correct expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); @@ -994,17 +996,19 @@ describe('actions/IOU', () => { // Carlos DM should have two reportActions – the existing CREATED action and a pending IOU action expect(_.size(carlosReportActions)).toBe(2); + carlosIOUCreatedAction = _.find(carlosReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); carlosIOUAction = _.find(carlosReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); expect(carlosIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(carlosIOUAction.originalMessage.IOUReportID).toBe(carlosIOUReport.reportID); expect(carlosIOUAction.originalMessage.amount).toBe(amount / 4); expect(carlosIOUAction.originalMessage.comment).toBe(comment); expect(carlosIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(carlosCreatedAction.created)).toBeLessThanOrEqual(Date.parse(carlosIOUAction.created)); + expect(Date.parse(carlosIOUCreatedAction.created)).toBeLessThan(Date.parse(carlosIOUAction.created)); // Jules DM should have three reportActions, the existing CREATED action, the existing IOU action, and a new pending IOU action expect(_.size(julesReportActions)).toBe(3); expect(julesReportActions[julesCreatedAction.reportActionID]).toStrictEqual(julesCreatedAction); + julesIOUCreatedAction = _.find(julesReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); julesIOUAction = _.find( julesReportActions, (reportAction) => @@ -1015,7 +1019,7 @@ describe('actions/IOU', () => { expect(julesIOUAction.originalMessage.amount).toBe(amount / 4); expect(julesIOUAction.originalMessage.comment).toBe(comment); expect(julesIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(julesCreatedAction.created)).toBeLessThanOrEqual(Date.parse(julesIOUAction.created)); + expect(Date.parse(julesIOUCreatedAction.created)).toBeLessThan(Date.parse(julesIOUAction.created)); // Vit DM should have two reportActions – a pending CREATED action and a pending IOU action expect(_.size(vitReportActions)).toBe(2); @@ -1027,7 +1031,7 @@ describe('actions/IOU', () => { expect(vitIOUAction.originalMessage.amount).toBe(amount / 4); expect(vitIOUAction.originalMessage.comment).toBe(comment); expect(vitIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(vitCreatedAction.created)).toBeLessThanOrEqual(Date.parse(vitIOUAction.created)); + expect(Date.parse(vitCreatedAction.created)).toBeLessThan(Date.parse(vitIOUAction.created)); // Group chat should have two reportActions – a pending CREATED action and a pending IOU action w/ type SPLIT expect(_.size(groupReportActions)).toBe(2); From 9548e2a531605a72cc14ed6f39569032525c37bc Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 13 Sep 2023 14:26:41 +0800 Subject: [PATCH 020/548] fix var is not defined --- tests/actions/IOUTest.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 902b669b287a..683865a4058a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -851,9 +851,11 @@ describe('actions/IOU', () => { let carlosIOUReport; let carlosIOUAction; + let carlosIOUCreatedAction; let carlosTransaction; let julesIOUAction; + let julesIOUCreatedAction; let julesTransaction; let vitChatReport; From 70a4a14ed46fe07a66bddb5de76f52e7c816815a Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 14 Sep 2023 18:15:11 +0700 Subject: [PATCH 021/548] fix: warning - non-passive event listener --- src/components/Composer/index.js | 6 +++-- .../HTMLRenderers/PreRenderer/index.js | 6 +++-- .../hasPassiveEventListenerSupport/index.js | 22 +++++++++++++++++++ .../index.native.js | 10 +++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js create mode 100644 src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 44075a4ec1eb..9ab32b549917 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -19,6 +19,7 @@ import isEnterWhileComposition from '../../libs/KeyboardShortcut/isEnterWhileCom import CONST from '../../CONST'; import withNavigation from '../withNavigation'; import ReportActionComposeFocusManager from '../../libs/ReportActionComposeFocusManager'; +import hasPassiveEventListenerSupport from '../../libs/DeviceCapabilities/hasPassiveEventListenerSupport'; const propTypes = { /** Maximum number of lines in the text input */ @@ -140,6 +141,8 @@ const getNextChars = (str, cursorPos) => { return substr.substring(0, spaceIndex); }; +const supportsPassive = hasPassiveEventListenerSupport(); + // Enable Markdown parsing. // On web we like to have the Text Input field always focused so the user can easily type a new chat function Composer({ @@ -339,7 +342,6 @@ function Composer({ } textInput.current.scrollTop += event.deltaY; - event.preventDefault(); event.stopPropagation(); }, []); @@ -384,7 +386,7 @@ function Composer({ if (textInput.current) { document.addEventListener('paste', handlePaste); - textInput.current.addEventListener('wheel', handleWheel); + textInput.current.addEventListener('wheel', handleWheel, supportsPassive ? {passive: true} : false); } return () => { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index efc9e432cba8..c9e6796902bd 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -5,6 +5,9 @@ import htmlRendererPropTypes from '../htmlRendererPropTypes'; import BasePreRenderer from './BasePreRenderer'; import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; import ControlSelection from '../../../../libs/ControlSelection'; +import hasPassiveEventListenerSupport from '../../../../libs/DeviceCapabilities/hasPassiveEventListenerSupport'; + +const supportsPassive = hasPassiveEventListenerSupport(); class PreRenderer extends React.Component { constructor(props) { @@ -18,7 +21,7 @@ class PreRenderer extends React.Component { if (!this.ref) { return; } - this.ref.getScrollableNode().addEventListener('wheel', this.scrollNode); + this.ref.getScrollableNode().addEventListener('wheel', this.scrollNode, supportsPassive ? {passive: true} : false); } componentWillUnmount() { @@ -47,7 +50,6 @@ class PreRenderer extends React.Component { const isScrollingVertically = this.debouncedIsScrollingVertically(event); if (event.currentTarget === node && horizontalOverflow && !isScrollingVertically) { node.scrollLeft += event.deltaX; - event.preventDefault(); event.stopPropagation(); } } diff --git a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js new file mode 100644 index 000000000000..3f836b951abc --- /dev/null +++ b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js @@ -0,0 +1,22 @@ +/** + * Allows us to identify whether the browser supports passive event listener. + * Because older browsers will interpret any object in the 3rd argument of an event listener as capture=true. + * + * @returns {Boolean} + */ + +export default function hasPassiveEventListenerSupport() { + let supportsPassive = false; + try { + const opts = Object.defineProperty({}, 'passive', { + // eslint-disable-next-line getter-return + get() { + supportsPassive = true; + }, + }); + window.addEventListener('testPassive', null, opts); + window.removeEventListener('testPassive', null, opts); + // eslint-disable-next-line no-empty + } catch (e) {} + return supportsPassive; +} diff --git a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js new file mode 100644 index 000000000000..0e1bb5616040 --- /dev/null +++ b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js @@ -0,0 +1,10 @@ +/** + * Allows us to identify whether the browser supports passive event listener. + * Because older browsers will interpret any object in the 3rd argument of an event listener as capture=true. + * + * @returns {Boolean} + */ + +const hasPassiveEventListenerSupport = () => false; + +export default hasPassiveEventListenerSupport; From 35076a4efe88afa9c85ea43e2822924b67970291 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 15 Sep 2023 13:33:41 +0200 Subject: [PATCH 022/548] change loader --- src/pages/home/report/ReportActionsList.js | 19 +++++++++++-------- src/styles/styles.js | 3 +++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 1336b2d42ecd..86087330da9b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -1,5 +1,6 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState, useRef, useMemo} from 'react'; +import {ActivityIndicator, View} from 'react-native'; +import PropTypes from 'prop-types'; import Animated, {useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '../../../components/InvertedFlatList'; @@ -21,6 +22,7 @@ import reportPropTypes from '../../reportPropTypes'; import useLocalize from '../../../hooks/useLocalize'; import useNetwork from '../../../hooks/useNetwork'; import DateUtils from '../../../libs/DateUtils'; +import themeColors from '../../../styles/themes/default'; import FloatingMessageCounter from './FloatingMessageCounter'; import useReportScrollManager from '../../../hooks/useReportScrollManager'; @@ -323,13 +325,6 @@ function ReportActionsList({ return null; }} - ListHeaderComponent={() => { - if (report.isLoadingOlderReportActions) { - return ; - } - - return null; - }} keyboardShouldPersistTaps="handled" onLayout={(event) => { setSkeletonViewHeight(event.nativeEvent.layout.height); @@ -339,6 +334,14 @@ function ReportActionsList({ extraData={extraData} /> + {report.isLoadingNewerReportActions ? ( + + + + ) : null} ); } diff --git a/src/styles/styles.js b/src/styles/styles.js index e81e03726c78..f6dac73e867f 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3959,6 +3959,9 @@ const styles = (theme) => ({ height: 30, width: '100%', }, + bottomReportLoader: { + height: 40, + }, }); // For now we need to export the styles function that takes the theme as an argument From fe223970ff4a3bd994dce2df2f7f12c95032a6d7 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 17 Sep 2023 12:28:07 +0200 Subject: [PATCH 023/548] fix loop --- src/pages/home/report/ReportActionsList.js | 29 +++++++++++++++------- src/pages/home/report/ReportActionsView.js | 13 +++++++--- src/styles/styles.js | 15 ++++++++++- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 86087330da9b..4f5d4dd4f2be 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -285,6 +285,11 @@ function ReportActionsList({ const hideComposer = ReportUtils.shouldDisableWriteActions(report); const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; + const contentContainerStyle = useMemo( + () => [styles.chatContentScrollView, report.isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], + [report.isLoadingNewerReportActions], + ); + return ( <> { + if (report.isLoadingNewerReportActions) { + return ( + + + + ); + } + + return null; + }} keyboardShouldPersistTaps="handled" onLayout={(event) => { setSkeletonViewHeight(event.nativeEvent.layout.height); @@ -334,14 +353,6 @@ function ReportActionsList({ extraData={extraData} /> - {report.isLoadingNewerReportActions ? ( - - - - ) : null} ); } diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 1d477abf4d1c..180ce4369d33 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,4 +1,4 @@ -import React, {useRef, useEffect, useContext, useMemo, useCallback} from 'react'; +import React, {useRef, useEffect, useContext, useMemo} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashGet from 'lodash/get'; @@ -61,6 +61,7 @@ function ReportActionsView(props) { const didLayout = useRef(false); const isFirstRender = useRef(true); const didSubscribeToReportTypingEvents = useRef(false); + const [isFetchNewerWasCalled, setFetchNewerWasCalled] = React.useState(false); const hasCachedActions = useRef(_.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); @@ -157,15 +158,18 @@ function ReportActionsView(props) { * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadNewerChats = () => { + const loadNewerChats = ({distanceFromStart}) => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. - if (props.report.isLoadingNewerReportActions || isFirstRender.current || props.report.isLoadingReportActions || props.report.isLoadingOlderReportActions) { + if (props.report.isLoadingNewerReportActions || props.report.isLoadingReportActions) { isFirstRender.current = false; return; } + if ((!isFetchNewerWasCalled.current && !isFetchNewerWasCalled) || distanceFromStart <= 36) { + setFetchNewerWasCalled(true); + return; + } const newestReportAction = _.first(props.reportActions); - Report.getNewerAction(reportID, newestReportAction.reportActionID); }; @@ -234,6 +238,7 @@ function arePropsEqual(oldProps, newProps) { if (oldProps.report.isLoadingOlderReportActions !== newProps.report.isLoadingOlderReportActions) { return false; } + if (oldProps.report.isLoadingNewerReportActions !== newProps.report.isLoadingNewerReportActions) { return false; } diff --git a/src/styles/styles.js b/src/styles/styles.js index f6dac73e867f..3b1de826a8cb 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1598,6 +1598,11 @@ const styles = (theme) => ({ justifyContent: 'flex-start', paddingBottom: 16, }, + chatContentScrollViewWithHeaderLoader: { + padding: 40, + paddingLeft: 0, + paddingRight: 0, + }, // Chat Item chatItem: { @@ -3960,7 +3965,15 @@ const styles = (theme) => ({ width: '100%', }, bottomReportLoader: { - height: 40, + height: 36, + }, + chatBottomLoader: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + height: 36, }, }); From c6fd83e30562c2e5708679754935b5a448504c28 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 18 Sep 2023 12:17:53 +0700 Subject: [PATCH 024/548] update comment --- .../DeviceCapabilities/hasPassiveEventListenerSupport/index.js | 1 - .../hasPassiveEventListenerSupport/index.native.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js index 3f836b951abc..fdd329451321 100644 --- a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js +++ b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js @@ -1,6 +1,5 @@ /** * Allows us to identify whether the browser supports passive event listener. - * Because older browsers will interpret any object in the 3rd argument of an event listener as capture=true. * * @returns {Boolean} */ diff --git a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js index 0e1bb5616040..555d34063ea7 100644 --- a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js +++ b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js @@ -1,6 +1,5 @@ /** * Allows us to identify whether the browser supports passive event listener. - * Because older browsers will interpret any object in the 3rd argument of an event listener as capture=true. * * @returns {Boolean} */ From a9243e36a0a5858e719dae78fb0b712b70437bad Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Sep 2023 19:09:59 +0200 Subject: [PATCH 025/548] use distanceFromStart for avoiding rerenders --- src/CONST.ts | 1 + src/pages/home/report/ReportActionsList.js | 105 ++++++++++++++------- src/pages/home/report/ReportActionsView.js | 9 +- 3 files changed, 76 insertions(+), 39 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index dd192f1b257c..0ec61648c25f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2650,6 +2650,7 @@ const CONST = { EVENTS: { SCROLLING: 'scrolling', }, + CHAT_HEADER_LOADER_HEIGHT: 36, } as const; export default CONST; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 301b723514bb..c752ec6b27fd 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -334,6 +334,41 @@ function ReportActionsList({ [report.isLoadingNewerReportActions], ); + const listFooterComponent = useCallback(() => { + if (report.isLoadingOlderReportActions) { + return ; + } + + // Make sure the oldest report action loaded is not the first. This is so we do not show the + // skeleton view above the created action in a newly generated optimistic chat or one with not + // that many comments. + const lastReportAction = _.last(sortedReportActions) || {}; + if (report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + return ( + + ); + } + + return null; + }, [report.isLoadingOlderReportActions]); + const listHeaderComponent = useCallback(() => { + if (report.isLoadingNewerReportActions) { + return ( + + + + ); + } + + return null; + }, [report.isLoadingNewerReportActions]); + return ( <> { - if (report.isLoadingOlderReportActions) { - return ; - } - - // Make sure the oldest report action loaded is not the first. This is so we do not show the - // skeleton view above the created action in a newly generated optimistic chat or one with not - // that many comments. - const lastReportAction = _.last(sortedReportActions) || {}; - if (report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { - return ( - - ); - } - - return null; - }} - ListHeaderComponent={() => { - if (report.isLoadingNewerReportActions) { - return ( - - - - ); - } - - return null; - }} + ListFooterComponent={listFooterComponent} + ListHeaderComponent={listHeaderComponent} + // ListFooterComponent={() => { + // if (report.isLoadingOlderReportActions) { + // return ; + // } + + // // Make sure the oldest report action loaded is not the first. This is so we do not show the + // // skeleton view above the created action in a newly generated optimistic chat or one with not + // // that many comments. + // const lastReportAction = _.last(sortedReportActions) || {}; + // if (report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + // return ( + // + // ); + // } + + // return null; + // }} + // ListHeaderComponent={() => { + // if (report.isLoadingNewerReportActions) { + // return ( + // + // + // + // ); + // } + + // return null; + // }} keyboardShouldPersistTaps="handled" onLayout={(event) => { setSkeletonViewHeight(event.nativeEvent.layout.height); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 180ce4369d33..a20715d3e5ae 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -59,9 +59,8 @@ function ReportActionsView(props) { useCopySelectionHelper(); const didLayout = useRef(false); - const isFirstRender = useRef(true); const didSubscribeToReportTypingEvents = useRef(false); - const [isFetchNewerWasCalled, setFetchNewerWasCalled] = React.useState(false); + const isFetchNewerWasCalled = useRef(false); const hasCachedActions = useRef(_.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); @@ -161,12 +160,12 @@ function ReportActionsView(props) { const loadNewerChats = ({distanceFromStart}) => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. if (props.report.isLoadingNewerReportActions || props.report.isLoadingReportActions) { - isFirstRender.current = false; return; } - if ((!isFetchNewerWasCalled.current && !isFetchNewerWasCalled) || distanceFromStart <= 36) { - setFetchNewerWasCalled(true); + // ideally we do not need use distanceFromStart here but due maxToRenderPerBatch and windowSize we receive a lot of renders so times we can se how loadNewerChats is called. we use CONST.CHAT_HEADER_LOADER_HEIGHT to prevent this + if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + isFetchNewerWasCalled.current = true; return; } const newestReportAction = _.first(props.reportActions); From 99cead6ebbd87704c3024ac4008c95e15b4f48aa Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Sep 2023 19:11:16 +0200 Subject: [PATCH 026/548] prettier --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 4ad3cb37e13d..6da1b2c06f0b 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -139,7 +139,6 @@ class BaseInvertedFlatList extends Component { // We keep this property very low so that chat switching remains fast maxToRenderPerBatch={5} windowSize={15} - // Commenting the line below as it breaks the unread indicator test // we will look at fixing/reusing this after RN v0.72 maintainVisibleContentPosition={{ From bd27f3694788233830050ff0ee9d25ef8c875cd8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Sep 2023 19:15:59 +0200 Subject: [PATCH 027/548] clean --- src/pages/home/report/ReportActionsList.js | 105 +++++++-------------- 1 file changed, 34 insertions(+), 71 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index c752ec6b27fd..301b723514bb 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -334,41 +334,6 @@ function ReportActionsList({ [report.isLoadingNewerReportActions], ); - const listFooterComponent = useCallback(() => { - if (report.isLoadingOlderReportActions) { - return ; - } - - // Make sure the oldest report action loaded is not the first. This is so we do not show the - // skeleton view above the created action in a newly generated optimistic chat or one with not - // that many comments. - const lastReportAction = _.last(sortedReportActions) || {}; - if (report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { - return ( - - ); - } - - return null; - }, [report.isLoadingOlderReportActions]); - const listHeaderComponent = useCallback(() => { - if (report.isLoadingNewerReportActions) { - return ( - - - - ); - } - - return null; - }, [report.isLoadingNewerReportActions]); - return ( <> { - // if (report.isLoadingOlderReportActions) { - // return ; - // } - - // // Make sure the oldest report action loaded is not the first. This is so we do not show the - // // skeleton view above the created action in a newly generated optimistic chat or one with not - // // that many comments. - // const lastReportAction = _.last(sortedReportActions) || {}; - // if (report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { - // return ( - // - // ); - // } - - // return null; - // }} - // ListHeaderComponent={() => { - // if (report.isLoadingNewerReportActions) { - // return ( - // - // - // - // ); - // } - - // return null; - // }} + ListFooterComponent={() => { + if (report.isLoadingOlderReportActions) { + return ; + } + + // Make sure the oldest report action loaded is not the first. This is so we do not show the + // skeleton view above the created action in a newly generated optimistic chat or one with not + // that many comments. + const lastReportAction = _.last(sortedReportActions) || {}; + if (report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + return ( + + ); + } + + return null; + }} + ListHeaderComponent={() => { + if (report.isLoadingNewerReportActions) { + return ( + + + + ); + } + + return null; + }} keyboardShouldPersistTaps="handled" onLayout={(event) => { setSkeletonViewHeight(event.nativeEvent.layout.height); From 9fb47925ba26cf626df4ea1f4be21891c386b59c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Sep 2023 13:39:50 +0200 Subject: [PATCH 028/548] use the separate component for android for ListHeaderComponentLoader --- .../ListHeaderComponentLoader.android.js | 16 ++++++++++++++++ .../ListHeaderComponentLoader.js | 17 +++++++++++++++++ src/pages/home/report/ReportActionsList.js | 10 ++-------- src/styles/styles.js | 5 ++++- 4 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js create mode 100644 src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.js diff --git a/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js b/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js new file mode 100644 index 000000000000..54771c5e39c3 --- /dev/null +++ b/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js @@ -0,0 +1,16 @@ +import {View, ActivityIndicator} from 'react-native'; +import styles, {stylesGenerator} from '../../../../styles/styles'; +import themeColors from '../../../../styles/themes/default'; + +function ListHeaderComponentLoader() { + return ( + + + + ); +} + +export default ListHeaderComponentLoader; diff --git a/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.js b/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.js new file mode 100644 index 000000000000..b291c38959ed --- /dev/null +++ b/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.js @@ -0,0 +1,17 @@ +import React from 'react'; +import {View, ActivityIndicator} from 'react-native'; +import styles, {stylesGenerator} from '../../../../styles/styles'; +import themeColors from '../../../../styles/themes/default'; + +function ListHeaderComponentLoader() { + return ( + + + + ); +} + +export default ListHeaderComponentLoader; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 1a3438a37cf9..d6b119551caf 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -25,6 +25,7 @@ import DateUtils from '../../../libs/DateUtils'; import themeColors from '../../../styles/themes/default'; import FloatingMessageCounter from './FloatingMessageCounter'; import useReportScrollManager from '../../../hooks/useReportScrollManager'; +import ListHeaderComponentLoader from './ListHeaderComponentLoader/ListHeaderComponentLoader'; const propTypes = { /** The report currently being looked at */ @@ -385,14 +386,7 @@ function ReportActionsList({ }} ListHeaderComponent={() => { if (report.isLoadingNewerReportActions) { - return ( - - - - ); + return ; } return null; diff --git a/src/styles/styles.js b/src/styles/styles.js index 8e3eaaa724da..b18c5a0c3186 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3998,7 +3998,10 @@ const styles = (theme) => ({ bottom: 0, left: 0, right: 0, - height: 36, + height: CONST.CHAT_HEADER_LOADER_HEIGHT, + }, + chatBottomLoaderAndroid: { + top: - CONST.CHAT_HEADER_LOADER_HEIGHT, }, }); From 84d195394ff3dd47911f0cc5bf1773683833f6c6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Sep 2023 13:55:18 +0200 Subject: [PATCH 029/548] lint --- src/styles/styles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/styles.js b/src/styles/styles.js index b18c5a0c3186..3e02513fc6df 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -4001,7 +4001,7 @@ const styles = (theme) => ({ height: CONST.CHAT_HEADER_LOADER_HEIGHT, }, chatBottomLoaderAndroid: { - top: - CONST.CHAT_HEADER_LOADER_HEIGHT, + top: -CONST.CHAT_HEADER_LOADER_HEIGHT, }, }); From e608638391f557facb96497e942266de89461daf Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 20 Sep 2023 23:32:53 +0200 Subject: [PATCH 030/548] Migrate policy utils lib --- src/libs/{PolicyUtils.js => PolicyUtils.ts} | 116 +++++++------------- src/types/onyx/Policy.ts | 1 + 2 files changed, 41 insertions(+), 76 deletions(-) rename src/libs/{PolicyUtils.js => PolicyUtils.ts} (50%) diff --git a/src/libs/PolicyUtils.js b/src/libs/PolicyUtils.ts similarity index 50% rename from src/libs/PolicyUtils.js rename to src/libs/PolicyUtils.ts index 164f284a4ef5..1f2abfa2b7a8 100644 --- a/src/libs/PolicyUtils.js +++ b/src/libs/PolicyUtils.ts @@ -1,72 +1,56 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; +import * as OnyxTypes from '../types/onyx'; + +type PolicyMemberList = Record; +type PolicyMembersCollection = Record; +type MemberEmailsToAccountIDs = Record; +type PersonalDetailsList = Record; /** * Filter out the active policies, which will exclude policies with pending deletion - * @param {Object} policies - * @returns {Array} */ -function getActivePolicies(policies) { - return _.filter(policies, (policy) => policy && policy.isPolicyExpenseChatEnabled && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); +function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { + return policies.filter((policy) => policy?.isPolicyExpenseChatEnabled && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); } /** * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not. * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} - * - * @param {Object} policyMembers - * @returns {Boolean} */ -function hasPolicyMemberError(policyMembers) { - return _.some(policyMembers, (member) => !_.isEmpty(member.errors)); +function hasPolicyMemberError(policyMembers: PolicyMemberList): boolean { + return Object.values(policyMembers).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. - * - * @param {Object} policy - * @param {Object} policy.errorFields - * @return {Boolean} */ -function hasPolicyErrorFields(policy) { - return _.some(lodashGet(policy, 'errorFields', {}), (fieldErrors) => !_.isEmpty(fieldErrors)); +function hasPolicyErrorFields(policy: OnyxTypes.Policy): boolean { + return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors).length > 0); } /** * Check if the policy has any errors, and if it doesn't, then check if it has any error fields. - * - * @param {Object} policy - * @param {Object} policy.errors - * @param {Object} policy.errorFields - * @return {Boolean} */ -function hasPolicyError(policy) { - return !_.isEmpty(lodashGet(policy, 'errors', {})) ? true : hasPolicyErrorFields(policy); +function hasPolicyError(policy: OnyxTypes.Policy): boolean { + return Object.keys(policy?.errors ?? {}).length > 0 ? true : hasPolicyErrorFields(policy); } /** * Checks if we have any errors stored within the policy custom units. - * - * @param {Object} policy - * @returns {Boolean} */ -function hasCustomUnitsError(policy) { - return !_.isEmpty(_.pick(lodashGet(policy, 'customUnits', {}), 'errors')); +function hasCustomUnitsError(policy: OnyxTypes.Policy): boolean { + return Object.keys(policy?.customUnits?.error ?? {}).length > 0; } /** * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. - * - * @param {Object} policy - * @param {String} policy.id - * @param {Object} policyMembersCollection - * @returns {String} */ -function getPolicyBrickRoadIndicatorStatus(policy, policyMembersCollection) { - const policyMembers = lodashGet(policyMembersCollection, `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy.id}`, {}); +function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembersCollection: PolicyMembersCollection): string { + if (!(`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}` in policyMembersCollection)) return ''; + + const policyMembers = policyMembersCollection[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`]; if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } @@ -79,84 +63,64 @@ function getPolicyBrickRoadIndicatorStatus(policy, policyMembersCollection) { * If online, show the policy pending deletion only if there is an error. * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in * updating the screen. Passing the offline status from the component. - * @param {Object} policy - * @param {Boolean} isOffline - * @returns {Boolean} */ -function shouldShowPolicy(policy, isOffline) { +function shouldShowPolicy(policy: OnyxTypes.Policy, isOffline: boolean): boolean { return ( policy && - policy.isPolicyExpenseChatEnabled && - policy.role === CONST.POLICY.ROLE.ADMIN && - (isOffline || policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policy.errors)) + policy?.isPolicyExpenseChatEnabled && + policy?.role === CONST.POLICY.ROLE.ADMIN && + (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy?.errors).length > 0) ); } -/** - * @param {string} email - * @returns {boolean} - */ -function isExpensifyTeam(email) { +function isExpensifyTeam(email: string): boolean { const emailDomain = Str.extractEmailDomain(email); return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } -/** - * @param {string} email - * @returns {boolean} - */ -function isExpensifyGuideTeam(email) { +function isExpensifyGuideTeam(email: string): boolean { const emailDomain = Str.extractEmailDomain(email); return emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } /** * Checks if the current user is an admin of the policy. - * - * @param {Object} policy - * @returns {Boolean} */ -const isPolicyAdmin = (policy) => lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; +const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; /** - * @param {Object} policyMembers - * @param {Object} personalDetails - * @returns {Object} - * * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. * * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(policyMembers, personalDetails) { - const memberEmailsToAccountIDs = {}; - _.each(policyMembers, (member, accountID) => { - if (!_.isEmpty(member.errors)) { +function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { + const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; + Object.keys(policyMembers).forEach((accountID) => { + const member = policyMembers[accountID]; + if (Object.keys(member?.errors ?? {}).length > 0) { return; } const personalDetail = personalDetails[accountID]; - if (!personalDetail || !personalDetail.login) { + if (!personalDetail?.login) { return; } - memberEmailsToAccountIDs[personalDetail.login] = accountID; + memberEmailsToAccountIDs[personalDetail?.login] = accountID; }); return memberEmailsToAccountIDs; } /** * Get login list that we should not show in the workspace invite options - * - * @param {Object} policyMembers - * @param {Object} personalDetails - * @returns {Array} */ -function getIneligibleInvitees(policyMembers, personalDetails) { - const memberEmailsToExclude = [...CONST.EXPENSIFY_EMAILS]; - _.each(policyMembers, (policyMember, accountID) => { +function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { + const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; + Object.keys(policyMembers).forEach((accountID) => { + const policyMember = policyMembers[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). - if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policyMember.errors)) { + if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } - const memberEmail = lodashGet(personalDetails, `[${accountID}].login`); + const memberEmail = personalDetails[accountID]?.login; if (!memberEmail) { return; } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index cacbb5d15199..10ee569c93e6 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -32,6 +32,7 @@ type Policy = { isFromFullPolicy?: boolean; lastModified?: string; customUnits?: Record; + isPolicyExpenseChatEnabled: boolean; }; export default Policy; From 45b90544c39d8bdaa5c0fdaea999d3a3d95f31e1 Mon Sep 17 00:00:00 2001 From: Srikar Parsi Date: Thu, 21 Sep 2023 13:07:56 +0800 Subject: [PATCH 031/548] boilerplate for changes --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/Report.js | 36 +++++++++++++++++++ .../report/ContextMenu/ContextMenuActions.js | 28 +++++++++++++++ 4 files changed, 66 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 1882b7e97afb..30f6b0c68f8e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -396,6 +396,7 @@ export default { deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}?`, onlyVisible: 'Only visible to', replyInThread: 'Reply in thread', + subscribeToThread: 'Subscribe to thread', flagAsOffensive: 'Flag as offensive', }, emojiReactions: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 87c7a19fed8a..f2786057e107 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -387,6 +387,7 @@ export default { deleteConfirmation: ({action}: DeleteConfirmationParams) => `¿Estás seguro de que quieres eliminar este ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`, onlyVisible: 'Visible sólo para', replyInThread: 'Responder en el hilo', + subscribeToThread: 'NEED TO TRANSLATE', flagAsOffensive: 'Marcar como ofensivo', }, emojiReactions: { diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 55b03110e925..61d6df634dca 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -630,6 +630,42 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction = } } +/** + * This will subscribe to an existing thread, or create a new one and then subsribe to it if necessary + * + * @param {String} childReportID The reportID we are trying to open + * @param {Object} parentReportAction the parent comment of a thread + * @param {String} parentReportID The reportID of the parent + * + */ +function subscribeToChildReport(childReportID = '0', parentReportAction = {}, parentReportID = '0') { + if (childReportID !== '0') { + openReport(childReportID); + Navigation.navigate(ROUTES.getReportRoute(childReportID)); + } else { + const participantAccountIDs = _.uniq([currentUserAccountID, Number(parentReportAction.actorAccountID)]); + const parentReport = allReports[parentReportID]; + const newChat = ReportUtils.buildOptimisticChatReport( + participantAccountIDs, + lodashGet(parentReportAction, ['message', 0, 'text']), + lodashGet(parentReport, 'chatType', ''), + lodashGet(parentReport, 'policyID', CONST.POLICY.OWNER_EMAIL_FAKE), + CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, + false, + '', + undefined, + undefined, + CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + parentReportAction.reportActionID, + parentReportID, + ); + + const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(newChat.participantAccountIDs); + openReport(newChat.reportID, participantLogins, newChat, parentReportAction.reportActionID); + Navigation.navigate(ROUTES.getReportRoute(newChat.reportID)); + } +} + /** * Get the latest report history without marking the report as read. * diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 173bda0e5221..6e7256162be6 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -146,6 +146,34 @@ export default [ }, getDescription: () => {}, }, + { + isAnonymousAction: false, + textTranslateKey: 'reportActionContextMenu.subscribeToThread', + icon: Expensicons.ChatBubble, + successTextTranslateKey: '', + successIcon: null, + shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + if (type !== CONTEXT_MENU_TYPES.REPORT_ACTION) { + return false; + } + const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); + const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; + const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); + return isCommentAction || isReportPreviewAction || isIOUAction; + }, + onPress: (closePopover, {reportAction, reportID}) => { + if (closePopover) { + hideContextMenu(false, () => { + ReportActionComposeFocusManager.focus(); + Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + }); + return; + } + + Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + }, + getDescription: () => {}, + }, { isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.copyURLToClipboard', From cfe420d63697ac44f60590a7ff9de0cbad1ea9ef Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Sep 2023 09:56:19 +0200 Subject: [PATCH 032/548] rename isLoadingReportActions --- .../InvertedFlatList/BaseInvertedFlatList.js | 2 -- src/libs/actions/Policy.js | 2 +- src/libs/actions/Report.js | 20 +++++++++---------- src/pages/home/ReportScreen.js | 6 +++--- src/pages/home/report/ReportActionsList.js | 4 +--- src/pages/home/report/ReportActionsView.js | 8 ++++---- .../withReportAndReportActionOrNotFound.js | 2 +- src/pages/reportPropTypes.js | 2 +- src/types/onyx/Report.ts | 2 +- 9 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 6da1b2c06f0b..0d6138ec0c79 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -139,8 +139,6 @@ class BaseInvertedFlatList extends Component { // We keep this property very low so that chat switching remains fast maxToRenderPerBatch={5} windowSize={15} - // Commenting the line below as it breaks the unread indicator test - // we will look at fixing/reusing this after RN v0.72 maintainVisibleContentPosition={{ minIndexForVisible: 0, }} diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 93b46f2e53da..4534b4d2f27e 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -312,7 +312,7 @@ function createPolicyExpenseChats(policyID, invitedEmailsToAccountIDs) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReport.reportID}`, value: { - isLoadingReportActions: false, + isLoadingInitialReportActions: false, }, }); }); diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index bec9b1c0afc4..085ca77de7ce 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -407,7 +407,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p value: reportActionsExist(reportID) ? {} : { - isLoadingReportActions: true, + isLoadingInitialReportActions: true, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), @@ -417,7 +417,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - isLoadingReportActions: false, + isLoadingInitialReportActions: false, pendingFields: { createChat: null, }, @@ -431,7 +431,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - isLoadingReportActions: false, + isLoadingInitialReportActions: false, }, }; @@ -648,7 +648,7 @@ function reconnect(reportID) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - isLoadingReportActions: true, + isLoadingInitialReportActions: true, isLoadingNewerReportActions: false, isLoadingOlderReportActions: false, reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), @@ -660,7 +660,7 @@ function reconnect(reportID) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - isLoadingReportActions: false, + isLoadingInitialReportActions: false, }, }, ], @@ -669,7 +669,7 @@ function reconnect(reportID) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - isLoadingReportActions: false, + isLoadingInitialReportActions: false, }, }, ], @@ -730,7 +730,7 @@ function readOldestAction(reportID, reportActionID) { * @param {String} reportID * @param {String} reportActionID */ -function getOlderAction(reportID, reportActionID) { +function getOlderActions(reportID, reportActionID) { API.read( 'GetOlderActions', { @@ -776,7 +776,7 @@ function getOlderAction(reportID, reportActionID) { * @param {String} reportID * @param {String} reportActionID */ -function getNewerAction(reportID, reportActionID) { +function getNewerActions(reportID, reportActionID) { API.read( 'GetNewerActions', { @@ -2204,6 +2204,6 @@ export { getReportPrivateNote, clearPrivateNotesError, hasErrorInPrivateNotes, - getOlderAction, - getNewerAction, + getOlderActions, + getNewerActions, }; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 1cbd3a025fa6..a31c868ec72c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -99,7 +99,7 @@ const defaultProps = { reportActions: [], report: { hasOutstandingIOU: false, - isLoadingReportActions: false, + isLoadingInitialReportActions: false, isLoadingNewerReportActions: false, }, isComposerFullSize: false, @@ -161,7 +161,7 @@ function ReportScreen({ const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; // There are no reportActions at all to display and we are still in the process of loading the next set of actions. - const isLoadingInitialReportActions = _.isEmpty(reportActions) && report.isLoadingReportActions; + const isLoadingInitialReportActions = _.isEmpty(reportActions) && report.isLoadingInitialReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; @@ -309,7 +309,7 @@ function ReportScreen({ // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo( - () => (!_.isEmpty(report) && !isDefaultReport && !report.reportID && !isOptimisticDelete && !report.isLoadingReportActions && !isLoading) || shouldHideReport, + () => (!_.isEmpty(report) && !isDefaultReport && !report.reportID && !isOptimisticDelete && !report.isLoadingInitialReportActions && !isLoading) || shouldHideReport, [report, isLoading, shouldHideReport, isDefaultReport, isOptimisticDelete], ); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index d6b119551caf..20ea07b0221c 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -1,5 +1,4 @@ import React, {useCallback, useEffect, useState, useRef, useMemo} from 'react'; -import {ActivityIndicator, View} from 'react-native'; import PropTypes from 'prop-types'; import Animated, {useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; @@ -22,7 +21,6 @@ import reportPropTypes from '../../reportPropTypes'; import useLocalize from '../../../hooks/useLocalize'; import useNetwork from '../../../hooks/useNetwork'; import DateUtils from '../../../libs/DateUtils'; -import themeColors from '../../../styles/themes/default'; import FloatingMessageCounter from './FloatingMessageCounter'; import useReportScrollManager from '../../../hooks/useReportScrollManager'; import ListHeaderComponentLoader from './ListHeaderComponentLoader/ListHeaderComponentLoader'; @@ -373,7 +371,7 @@ function ReportActionsList({ // skeleton view above the created action in a newly generated optimistic chat or one with not // that many comments. const lastReportAction = _.last(sortedReportActions) || {}; - if (report.isLoadingReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + if (report.isLoadingInitialReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { return ( { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. - if (props.report.isLoadingNewerReportActions || props.report.isLoadingReportActions) { + if (props.report.isLoadingNewerReportActions || props.report.isLoadingInitialReportActions) { return; } @@ -176,7 +176,7 @@ function ReportActionsView(props) { return; } const newestReportAction = _.first(props.reportActions); - Report.getNewerAction(reportID, newestReportAction.reportActionID); + Report.getNewerActions(reportID, newestReportAction.reportActionID); }; /** @@ -249,7 +249,7 @@ function arePropsEqual(oldProps, newProps) { return false; } - if (oldProps.report.isLoadingReportActions !== newProps.report.isLoadingReportActions) { + if (oldProps.report.isLoadingInitialReportActions !== newProps.report.isLoadingInitialReportActions) { return false; } diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.js b/src/pages/home/report/withReportAndReportActionOrNotFound.js index 9bf3e73e761c..089b90de39d4 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.js +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.js @@ -94,7 +94,7 @@ export default function (WrappedComponent) { // Perform all the loading checks const isLoadingReport = props.isLoadingReportData && (_.isEmpty(props.report) || !props.report.reportID); - const isLoadingReportAction = _.isEmpty(props.reportActions) || (props.report.isLoadingReportActions && _.isEmpty(getReportAction())); + const isLoadingReportAction = _.isEmpty(props.reportActions) || (props.report.isLoadingInitialReportActions && _.isEmpty(getReportAction())); const shouldHideReport = !isLoadingReport && (_.isEmpty(props.report) || !props.report.reportID || !ReportUtils.canAccessReport(props.report, props.policies, props.betas)); if ((isLoadingReport || isLoadingReportAction) && !shouldHideReport) { diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index 66396ba661ce..c7e93b127619 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -20,7 +20,7 @@ export default PropTypes.shape({ isLoadingNewerReportActions: PropTypes.bool, /** Flag to check if the report actions data are loading */ - isLoadingReportActions: PropTypes.bool, + isLoadingInitialReportActions: PropTypes.bool, /** Whether the user is not an admin of policyExpenseChat chat */ isOwnPolicyExpenseChat: PropTypes.bool, diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 87a58c27bf5b..f28421998a1f 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -19,7 +19,7 @@ type Report = { isLoadingNewerReportActions?: boolean; /** Flag to check if the report actions data are loading */ - isLoadingReportActions?: boolean; + isLoadingInitialReportActions?: boolean; /** Whether the user is not an admin of policyExpenseChat chat */ isOwnPolicyExpenseChat?: boolean; From 120eefaba480b142223b97f7c42682ad51bd275c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Sep 2023 10:33:11 +0200 Subject: [PATCH 033/548] add comment --- .../ListHeaderComponentLoader.android.js | 2 +- .../ListHeaderComponentLoader/ListHeaderComponentLoader.js | 2 +- src/styles/styles.js | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js b/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js index 54771c5e39c3..ff2a8f51fa4c 100644 --- a/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js +++ b/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js @@ -4,7 +4,7 @@ import themeColors from '../../../../styles/themes/default'; function ListHeaderComponentLoader() { return ( - + + ({ paddingBottom: 16, }, chatContentScrollViewWithHeaderLoader: { - padding: 40, + // regular paddingBottom wouldn't work here + padding: CONST.CHAT_HEADER_LOADER_HEIGHT, paddingLeft: 0, paddingRight: 0, }, @@ -3989,9 +3990,6 @@ const styles = (theme) => ({ height: 30, width: '100%', }, - bottomReportLoader: { - height: 36, - }, chatBottomLoader: { position: 'absolute', top: 0, From 484f5961d3bbb87db53367afa0c169ea821f0554 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Sep 2023 13:16:45 +0200 Subject: [PATCH 034/548] add throttle --- src/pages/home/report/ReportActionsView.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index cec385220478..0a7a1cd059a5 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -164,20 +164,28 @@ function ReportActionsView(props) { * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadNewerChats = ({distanceFromStart}) => { + const loadNewerChats = _.throttle(({distanceFromStart}) => { // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. if (props.report.isLoadingNewerReportActions || props.report.isLoadingInitialReportActions) { return; } - // ideally we do not need use distanceFromStart here but due maxToRenderPerBatch and windowSize we receive a lot of renders so times we can se how loadNewerChats is called. we use CONST.CHAT_HEADER_LOADER_HEIGHT to prevent this + // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', + // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. + + // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not + // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. + + // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. + // This should be removed once the issue of frequent re-renders is resolved. + if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { isFetchNewerWasCalled.current = true; return; } const newestReportAction = _.first(props.reportActions); Report.getNewerActions(reportID, newestReportAction.reportActionID); - }; + }, 300); /** * Runs when the FlatList finishes laying out From 5ef510dbcfb42a5357d55fa332705cf5d579cacb Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Sep 2023 16:22:53 +0200 Subject: [PATCH 035/548] create a shared component for header and footer --- src/CONST.ts | 4 + .../ListBoundaryLoader/ListBoundaryLoader.js | 58 ++++++++++++++ .../ListHeaderComponentLoader.android.js | 4 +- .../ListHeaderComponentLoader.js | 4 +- src/pages/home/report/ReportActionsList.js | 76 +++++++++---------- 5 files changed, 104 insertions(+), 42 deletions(-) create mode 100644 src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js rename src/pages/home/report/{ => ListBoundaryLoader}/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js (76%) rename src/pages/home/report/{ => ListBoundaryLoader}/ListHeaderComponentLoader/ListHeaderComponentLoader.js (76%) diff --git a/src/CONST.ts b/src/CONST.ts index 1563b9717870..4d90cff0d1d0 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2666,6 +2666,10 @@ const CONST = { HIDDEN_MARGIN_VERTICAL: 0, HIDDEN_BORDER_BOTTOM_WIDTH: 0, }, + LIST_COMPONENTS: { + HEADER: 'header', + FOOTER: 'footer', + } } as const; export default CONST; diff --git a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js new file mode 100644 index 000000000000..4f9a83d5cafd --- /dev/null +++ b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReportActionsSkeletonView from '../../../../components/ReportActionsSkeletonView'; +import CONST from '../../../../CONST'; +import useNetwork from '../../../../hooks/useNetwork'; +import ListHeaderComponentLoader from './ListHeaderComponentLoader/ListHeaderComponentLoader'; + +const propTypes = { + type: PropTypes.string.isRequired, + isLoadingOlderReportActions: PropTypes.bool, + isLoadingInitialReportActions: PropTypes.bool, + isLoadingNewerReportActions: PropTypes.bool, + skeletonViewHeight: PropTypes.number, + lastReportActionName: PropTypes.string, +}; + +const defaultProps = { + isLoadingOlderReportActions: false, + isLoadingInitialReportActions: false, + isLoadingNewerReportActions: false, + skeletonViewHeight: 0, + lastReportActionName: '', +}; + +function ListBoundaryLoader({type, isLoadingOlderReportActions, isLoadingInitialReportActions, skeletonViewHeight, lastReportActionName, isLoadingNewerReportActions}) { + const {isOffline} = useNetwork(); + // we use two different loading components for header and footer to reduce the jumping effect when you scrolling to the newer reports + if (type === CONST.LIST_COMPONENTS.FOOTER) { + if (isLoadingOlderReportActions) { + return ; + } + + // Make sure the oldest report action loaded is not the first. This is so we do not show the + // skeleton view above the created action in a newly generated optimistic chat or one with not + // that many comments. + // const lastReportAction = _.last(sortedReportActions) || {}; + if (isLoadingInitialReportActions && lastReportActionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + return ( + + ); + } + + return null; + } + if (type === CONST.LIST_COMPONENTS.HEADER && isLoadingNewerReportActions) { + // the styles for android and the rest components are different that's why we use two different components + return ; + } + return null; +} + +ListBoundaryLoader.propTypes = propTypes; +ListBoundaryLoader.defaultProps = defaultProps; + +export default ListBoundaryLoader; diff --git a/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js b/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js similarity index 76% rename from src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js rename to src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js index ff2a8f51fa4c..20abe332692d 100644 --- a/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js +++ b/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js @@ -1,6 +1,6 @@ import {View, ActivityIndicator} from 'react-native'; -import styles, {stylesGenerator} from '../../../../styles/styles'; -import themeColors from '../../../../styles/themes/default'; +import styles, {stylesGenerator} from '../../../../../styles/styles'; +import themeColors from '../../../../../styles/themes/default'; function ListHeaderComponentLoader() { return ( diff --git a/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.js b/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.js similarity index 76% rename from src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.js rename to src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.js index 8390ebf95136..9d4d85a84e87 100644 --- a/src/pages/home/report/ListHeaderComponentLoader/ListHeaderComponentLoader.js +++ b/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.js @@ -1,7 +1,7 @@ import React from 'react'; import {View, ActivityIndicator} from 'react-native'; -import styles, {stylesGenerator} from '../../../../styles/styles'; -import themeColors from '../../../../styles/themes/default'; +import styles, {stylesGenerator} from '../../../../../styles/styles'; +import themeColors from '../../../../../styles/themes/default'; function ListHeaderComponentLoader() { return ( diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 20ea07b0221c..846f93fdda8b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -23,7 +23,9 @@ import useNetwork from '../../../hooks/useNetwork'; import DateUtils from '../../../libs/DateUtils'; import FloatingMessageCounter from './FloatingMessageCounter'; import useReportScrollManager from '../../../hooks/useReportScrollManager'; -import ListHeaderComponentLoader from './ListHeaderComponentLoader/ListHeaderComponentLoader'; +import ListHeaderComponentLoader from './ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader'; +import useWindowDimensions from '../../../hooks/useWindowDimensions'; +import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; const propTypes = { /** The report currently being looked at */ @@ -134,7 +136,6 @@ function ReportActionsList({ useEffect(() => { opacity.value = withTiming(1, {duration: 100}); }, [opacity]); - const [skeletonViewHeight, setSkeletonViewHeight] = useState(0); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because @@ -255,14 +256,20 @@ function ReportActionsList({ /** * Calculates the ideal number of report actions to render in the first render, based on the screen height and on * the height of the smallest report action possible. - * @return {Number} + * @return {{availableContentHeight: Number, initialNumToRender: Number}} */ - const initialNumToRender = useMemo(() => { - const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; + const {availableContentHeight, initialNumToRender} = useMemo(() => { const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); - return Math.ceil(availableHeight / minimumReportActionHeight); - }, [windowHeight]); + const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; + const initialRenderCount = Math.ceil(availableHeight / minimumReportActionHeight); + + return { + availableContentHeight: availableHeight, + initialNumToRender: initialRenderCount, + }; + }, [windowHeight, variables.contentHeaderHeight, styles.chatItem.paddingTop, styles.chatItem.paddingBottom, variables.fontSizeNormalHeight]); + const lastReportAction = useMemo(() => _.last(sortedReportActions) || {}, [sortedReportActions]); /** * Thread's divider line should hide when the first chat in the thread is marked as unread. * This is so that it will not be conflicting with header's separator line. @@ -341,6 +348,27 @@ function ReportActionsList({ [report.isLoadingNewerReportActions], ); + const listFooterComponent = useCallback(() => { + return ( + + ); + }, [report.isLoadingInitialReportActions, report.isLoadingOlderReportActions, availableContentHeight, lastReportAction.actionName]); + + const listHeaderComponent = useCallback(() => { + return ( + + ); + }, [report.isLoadingNewerReportActions]); + return ( <> { - if (report.isLoadingOlderReportActions) { - return ; - } - - // Make sure the oldest report action loaded is not the first. This is so we do not show the - // skeleton view above the created action in a newly generated optimistic chat or one with not - // that many comments. - const lastReportAction = _.last(sortedReportActions) || {}; - if (report.isLoadingInitialReportActions && lastReportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { - return ( - - ); - } - - return null; - }} - ListHeaderComponent={() => { - if (report.isLoadingNewerReportActions) { - return ; - } - - return null; - }} + ListFooterComponent={listFooterComponent} + ListHeaderComponent={listHeaderComponent} keyboardShouldPersistTaps="handled" - onLayout={(event) => { - setSkeletonViewHeight(event.nativeEvent.layout.height); - onLayout(event); - }} + onLayout={onLayout} onScroll={trackVerticalScrolling} extraData={extraData} /> From fae75dd0ddee2ef463dd3c455f7ff64c1cace411 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Sep 2023 16:34:39 +0200 Subject: [PATCH 036/548] lint --- src/pages/home/report/ReportActionsList.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 846f93fdda8b..596e7ab38c66 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -12,7 +12,6 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, import {withPersonalDetails} from '../../../components/OnyxProvider'; import ReportActionItem from './ReportActionItem'; import ReportActionItemParentAction from './ReportActionItemParentAction'; -import ReportActionsSkeletonView from '../../../components/ReportActionsSkeletonView'; import variables from '../../../styles/variables'; import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; import reportActionPropTypes from './reportActionPropTypes'; @@ -23,8 +22,6 @@ import useNetwork from '../../../hooks/useNetwork'; import DateUtils from '../../../libs/DateUtils'; import FloatingMessageCounter from './FloatingMessageCounter'; import useReportScrollManager from '../../../hooks/useReportScrollManager'; -import ListHeaderComponentLoader from './ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader'; -import useWindowDimensions from '../../../hooks/useWindowDimensions'; import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; const propTypes = { @@ -348,8 +345,8 @@ function ReportActionsList({ [report.isLoadingNewerReportActions], ); - const listFooterComponent = useCallback(() => { - return ( + const listFooterComponent = useCallback( + () => ( - ); - }, [report.isLoadingInitialReportActions, report.isLoadingOlderReportActions, availableContentHeight, lastReportAction.actionName]); + ), + [report.isLoadingInitialReportActions, report.isLoadingOlderReportActions, availableContentHeight, lastReportAction.actionName], + ); - const listHeaderComponent = useCallback(() => { - return ( + const listHeaderComponent = useCallback( + () => ( - ); - }, [report.isLoadingNewerReportActions]); + ), + [report.isLoadingNewerReportActions], + ); return ( <> From 2d094f0400ee922e396d76ab822313bb043980b3 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Sep 2023 16:35:11 +0200 Subject: [PATCH 037/548] add const LIST_COMPONENTS --- src/CONST.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 4d90cff0d1d0..8d6e195d54a0 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2667,9 +2667,9 @@ const CONST = { HIDDEN_BORDER_BOTTOM_WIDTH: 0, }, LIST_COMPONENTS: { - HEADER: 'header', - FOOTER: 'footer', - } + HEADER: 'header', + FOOTER: 'footer', + }, } as const; export default CONST; From 5109429e29712e4b9df228d7320a095719063c67 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 22 Sep 2023 09:30:10 +0200 Subject: [PATCH 038/548] add JSDoc --- .../ListBoundaryLoader/ListBoundaryLoader.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js index 4f9a83d5cafd..f291699c5dd4 100644 --- a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js +++ b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js @@ -6,11 +6,22 @@ import useNetwork from '../../../../hooks/useNetwork'; import ListHeaderComponentLoader from './ListHeaderComponentLoader/ListHeaderComponentLoader'; const propTypes = { - type: PropTypes.string.isRequired, + /** type of rendered loader. Can be 'header' or 'footer' */ + type: PropTypes.oneOf([CONST.LIST_COMPONENTS.HEADER, CONST.LIST_COMPONENTS.FOOTER]).isRequired, + + /** Shows if we call fetching older report action */ isLoadingOlderReportActions: PropTypes.bool, + + /* Shows if we call initial loading of report action */ isLoadingInitialReportActions: PropTypes.bool, + + /** Shows if we call fetching newer report action */ isLoadingNewerReportActions: PropTypes.bool, + + /** Height of the skeleton view */ skeletonViewHeight: PropTypes.number, + + /** Name of the last report action */ lastReportActionName: PropTypes.string, }; @@ -38,7 +49,7 @@ function ListBoundaryLoader({type, isLoadingOlderReportActions, isLoadingInitial return ( ); } From 58f417f8039a4d6faab3d0ee6254582beada2fd3 Mon Sep 17 00:00:00 2001 From: Srikar Parsi Date: Fri, 22 Sep 2023 16:19:47 +0800 Subject: [PATCH 039/548] intermediate changes --- src/libs/actions/Report.js | 33 +++++++++++++++++-- .../report/ContextMenu/ContextMenuActions.js | 22 +++++++------ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 61d6df634dca..c2dd3daf49b1 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -638,10 +638,11 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction = * @param {String} parentReportID The reportID of the parent * */ -function subscribeToChildReport(childReportID = '0', parentReportAction = {}, parentReportID = '0') { +function toggleSubscribeToChildReport(childReportID = '0', parentReportAction = {}, parentReportID = '0', prevNotificationPreference) { if (childReportID !== '0') { openReport(childReportID); - Navigation.navigate(ROUTES.getReportRoute(childReportID)); + if (prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE) + updateNotificationPreference(childReportID, CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, ) } else { const participantAccountIDs = _.uniq([currentUserAccountID, Number(parentReportAction.actorAccountID)]); const parentReport = allReports[parentReportID]; @@ -1241,6 +1242,32 @@ function saveReportActionDraftNumberOfLines(reportID, reportActionID, numberOfLi Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}_${reportActionID}`, numberOfLines); } +/** + * @param {String} reportID + * @param {String} previousValue + * @param {String} newValue + */ +function updateNotificationPreference(reportID, previousValue, newValue) { + if (previousValue === newValue) { + return; + } + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: {notificationPreference: newValue}, + }, + ]; + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: {notificationPreference: previousValue}, + }, + ]; + API.write('UpdateReportNotificationPreference', {reportID, notificationPreference: newValue}, {optimisticData, failureData}); +} + /** * @param {String} reportID * @param {String} previousValue @@ -2105,6 +2132,7 @@ export { reconnect, updateWelcomeMessage, updateWriteCapabilityAndNavigate, + updateNotificationPreference, updateNotificationPreferenceAndNavigate, subscribeToReportTypingEvents, unsubscribeFromReportChannel, @@ -2132,6 +2160,7 @@ export { navigateToAndOpenReport, navigateToAndOpenReportWithAccountIDs, navigateToAndOpenChildReport, + toggleSubscribeToChildReport, updatePolicyRoomNameAndNavigate, clearPolicyRoomNameErrors, clearIOUError, diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 6e7256162be6..61bf5abe4674 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -25,6 +25,7 @@ import * as Task from '../../../../libs/actions/Task'; import * as Localize from '../../../../libs/Localize'; import * as TransactionUtils from '../../../../libs/TransactionUtils'; import * as CurrencyUtils from '../../../../libs/CurrencyUtils'; +import Log from '../../../../libs/Log'; /** * Gets the HTML version of the message in an action. @@ -149,7 +150,7 @@ export default [ { isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.subscribeToThread', - icon: Expensicons.ChatBubble, + icon: Expensicons.Chair, successTextTranslateKey: '', successIcon: null, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { @@ -162,15 +163,16 @@ export default [ return isCommentAction || isReportPreviewAction || isIOUAction; }, onPress: (closePopover, {reportAction, reportID}) => { - if (closePopover) { - hideContextMenu(false, () => { - ReportActionComposeFocusManager.focus(); - Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); - }); - return; - } - - Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + Log.info("sparsisparsi start"); + Log.info(JSON.stringify(reportAction)); + Log.info("sparsisparsi done"); + // if (closePopover) { + // hideContextMenu(false, () => { + // ReportActionComposeFocusManager.focus(); + // Report.subscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + // }); + // return; + // } }, getDescription: () => {}, }, From 0849919b2105b4a94dbae97b54f20a82ed57f5d7 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 22 Sep 2023 10:32:11 +0200 Subject: [PATCH 040/548] fix after merge --- src/pages/home/report/ReportActionsList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 596e7ab38c66..052604e521b6 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -114,7 +114,6 @@ function ReportActionsList({ }) { const reportScrollManager = useReportScrollManager(); const {translate} = useLocalize(); - const {isOffline} = useNetwork(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); const [currentUnreadMarker, setCurrentUnreadMarker] = useState(null); From 98dbc4aff23626a061d923945c2fb2293bf7ce30 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 22 Sep 2023 10:52:12 +0200 Subject: [PATCH 041/548] fix: added types to MapboxToken lib --- .../{MapboxToken.js => MapboxToken.ts} | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) rename src/libs/actions/{MapboxToken.js => MapboxToken.ts} (85%) diff --git a/src/libs/actions/MapboxToken.js b/src/libs/actions/MapboxToken.ts similarity index 85% rename from src/libs/actions/MapboxToken.js rename to src/libs/actions/MapboxToken.ts index 2ce56eb1a11e..45441ec8c5ca 100644 --- a/src/libs/actions/MapboxToken.js +++ b/src/libs/actions/MapboxToken.ts @@ -1,26 +1,25 @@ -import _ from 'underscore'; import moment from 'moment'; import Onyx from 'react-native-onyx'; -import {AppState} from 'react-native'; -import lodashGet from 'lodash/get'; +import {AppState, NativeEventSubscription} from 'react-native'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; import CONST from '../../CONST'; import * as ActiveClientManager from '../ActiveClientManager'; +import {MapboxAccessToken, Network} from '../../types/onyx'; -let authToken; +let authToken: string | undefined | null; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - authToken = lodashGet(val, 'authToken', null); + callback: (value) => { + authToken = value?.authToken ?? null; }, }); -let connectionIDForToken; -let connectionIDForNetwork; -let appStateSubscription; -let currentToken; -let refreshTimeoutID; +let connectionIDForToken: number | null; +let connectionIDForNetwork: number | null; +let appStateSubscription: NativeEventSubscription | null; +let currentToken: MapboxAccessToken | null; +let refreshTimeoutID: NodeJS.Timeout; let isCurrentlyFetchingToken = false; const REFRESH_INTERVAL = 1000 * 60 * 25; @@ -38,11 +37,11 @@ const setExpirationTimer = () => { return; } console.debug(`[MapboxToken] Fetching a new token after waiting ${REFRESH_INTERVAL / 1000 / 60} minutes`); - API.read('GetMapboxAccessToken'); + API.read('GetMapboxAccessToken', {}, {}); }, REFRESH_INTERVAL); }; -const hasTokenExpired = () => moment().isAfter(currentToken.expiration); +const hasTokenExpired = () => moment().isAfter(currentToken?.expiration); const clearToken = () => { console.debug('[MapboxToken] Deleting the token stored in Onyx'); @@ -60,12 +59,6 @@ const init = () => { // When the token changes in Onyx, the expiration needs to be checked so a new token can be retrieved. connectionIDForToken = Onyx.connect({ key: ONYXKEYS.MAPBOX_ACCESS_TOKEN, - /** - * @param {Object} token - * @param {String} token.token - * @param {String} token.expiration - * @param {String[]} [token.errors] - */ callback: (token) => { // Only the leader should be in charge of the mapbox token, or else when you have multiple tabs open, the Onyx connection fires multiple times // and it sets up duplicate refresh timers. This would be a big waste of tokens. @@ -82,9 +75,9 @@ const init = () => { // If the token is falsy or an empty object, the token needs to be retrieved from the API. // The API sets a token in Onyx with a 30 minute expiration. - if (_.isEmpty(token)) { + if (Object.keys(token ?? {}).length === 0) { console.debug('[MapboxToken] Token does not exist so fetching one'); - API.read('GetMapboxAccessToken'); + API.read('GetMapboxAccessToken', {}, {}); isCurrentlyFetchingToken = true; return; } @@ -120,13 +113,13 @@ const init = () => { } if (!connectionIDForNetwork) { - let network; + let network: Network | null; connectionIDForNetwork = Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (val) => { // When the network reconnects, check if the token has expired. If it has, then clearing the token will // trigger the fetch of a new one - if (network && network.isOffline && val && !val.isOffline && !isCurrentlyFetchingToken && hasTokenExpired()) { + if (network?.isOffline && val && !val.isOffline && !isCurrentlyFetchingToken && hasTokenExpired()) { console.debug('[MapboxToken] Token is expired after network came online'); clearToken(); } From a4f6834d838f5d075c0fb4ac5e515439b6a3aa94 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 22 Sep 2023 10:53:28 +0200 Subject: [PATCH 042/548] remove redundant dependency --- src/pages/home/report/ReportActionsList.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 052604e521b6..8097d59719fb 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -18,7 +18,6 @@ import reportActionPropTypes from './reportActionPropTypes'; import CONST from '../../../CONST'; import reportPropTypes from '../../reportPropTypes'; import useLocalize from '../../../hooks/useLocalize'; -import useNetwork from '../../../hooks/useNetwork'; import DateUtils from '../../../libs/DateUtils'; import FloatingMessageCounter from './FloatingMessageCounter'; import useReportScrollManager from '../../../hooks/useReportScrollManager'; @@ -263,7 +262,7 @@ function ReportActionsList({ availableContentHeight: availableHeight, initialNumToRender: initialRenderCount, }; - }, [windowHeight, variables.contentHeaderHeight, styles.chatItem.paddingTop, styles.chatItem.paddingBottom, variables.fontSizeNormalHeight]); + }, [windowHeight]); const lastReportAction = useMemo(() => _.last(sortedReportActions) || {}, [sortedReportActions]); /** From 478559309312a717b3cb7e14f85187123f2f2608 Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Wed, 27 Sep 2023 11:35:25 +0800 Subject: [PATCH 043/548] Remove the categories beta --- src/CONST.ts | 1 - src/components/MoneyRequestConfirmationList.js | 2 +- src/components/ReportActionItem/MoneyRequestView.js | 2 +- src/libs/Permissions.ts | 5 ----- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 8aee1b9f1af4..67b62be473b5 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -238,7 +238,6 @@ const CONST = { TASKS: 'tasks', THREADS: 'threads', CUSTOM_STATUS: 'customStatus', - NEW_DOT_CATEGORIES: 'newDotCategories', NEW_DOT_TAGS: 'newDotTags', }, BUTTON_STATES: { diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 0d554ff0eca4..2a488e9810c2 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -194,7 +194,7 @@ function MoneyRequestConfirmationList(props) { const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(ReportUtils.getReport(props.reportID))), [props.reportID]); // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && Permissions.canUseCategories(props.betas) && OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories)); + const shouldShowCategories = isPolicyExpenseChat && OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories)); // Fetches the first tag list of the policy const policyTag = PolicyUtils.getTag(props.policyTags); diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index ec91fb292257..3bd6fdc5673d 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -106,7 +106,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should const policyTagsList = lodashGet(policyTag, 'tags', {}); // Flags for showing categories and tags - const shouldShowCategory = isPolicyExpenseChat && Permissions.canUseCategories(betas) && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories))); + const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories))); const shouldShowTag = isPolicyExpenseChat && Permissions.canUseTags(betas) && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagsList))); const shouldShowBillable = isPolicyExpenseChat && Permissions.canUseTags(betas) && (transactionBillable || !lodashGet(policy, 'disabledFields.defaultBillable', true)); diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 05322472a407..ff96717adf62 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -49,10 +49,6 @@ function canUseCustomStatus(betas: Beta[]): boolean { return betas?.includes(CONST.BETAS.CUSTOM_STATUS) || canUseAllBetas(betas); } -function canUseCategories(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.NEW_DOT_CATEGORIES) || canUseAllBetas(betas); -} - function canUseTags(betas: Beta[]): boolean { return betas?.includes(CONST.BETAS.NEW_DOT_TAGS) || canUseAllBetas(betas); } @@ -74,7 +70,6 @@ export default { canUsePolicyRooms, canUseTasks, canUseCustomStatus, - canUseCategories, canUseTags, canUseLinkPreviews, }; From 7e4e58f3465a9fbc17ac4e752a829d17d147140b Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Wed, 27 Sep 2023 16:03:50 +0800 Subject: [PATCH 044/548] Fix JS error --- src/components/OptionRow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index 8bc016faa6b5..66a716698f2f 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -212,7 +212,7 @@ class OptionRow extends Component { accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={this.props.hoverStyle} - needsOffscreenAlphaCompositing={this.props.option.icons.length >= 2} + needsOffscreenAlphaCompositing={this.props.option.icons && this.props.option.icons.length >= 2} > From f38e0e5b64aebafcc94c0f4c06b2f3c46dbe5ee4 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Sep 2023 11:09:34 +0200 Subject: [PATCH 045/548] Initial work --- src/ONYXKEYS.ts | 2 +- src/libs/ReportActionsUtils.js | 2 +- src/libs/{SidebarUtils.js => SidebarUtils.ts} | 63 ++++++++----------- 3 files changed, 29 insertions(+), 38 deletions(-) rename src/libs/{SidebarUtils.js => SidebarUtils.ts} (91%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6649a33fe15e..51c5fb202f00 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -380,7 +380,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; - [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportAction; + [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: Record; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: string; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 67c44784eeb2..142f072728b2 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -334,7 +334,7 @@ function isReportActionDeprecated(reportAction, key) { * and supported type, it's not deleted and also not closed. * * @param {Object} reportAction - * @param {String} key + * @param {String | Number} key * @returns {Boolean} */ function shouldReportActionBeVisible(reportAction, key) { diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.ts similarity index 91% rename from src/libs/SidebarUtils.js rename to src/libs/SidebarUtils.ts index df676f23ebc7..e1251f9f50bc 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.ts @@ -13,9 +13,11 @@ import * as CollectionUtils from './CollectionUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as UserUtils from './UserUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; +import ReportAction from '../types/onyx/ReportAction'; + +const visibleReportActionItems: Record = {}; +const lastReportActions: Record = {}; -const visibleReportActionItems = {}; -const lastReportActions = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, callback: (actions, key) => { @@ -24,38 +26,37 @@ Onyx.connect({ } const reportID = CollectionUtils.extractCollectionItemID(key); - const actionsArray = ReportActionsUtils.getSortedReportActions(_.toArray(actions)); - lastReportActions[reportID] = _.last(actionsArray); + const actionsArray: ReportAction[] = ReportActionsUtils.getSortedReportActions(Object.values(actions)); + lastReportActions[reportID] = actionsArray[actionsArray.length - 1]; // The report is only visible if it is the last action not deleted that // does not match a closed or created state. - const reportActionsForDisplay = _.filter( - actionsArray, + const reportActionsForDisplay = actionsArray.filter( (reportAction, actionKey) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, actionKey) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); - visibleReportActionItems[reportID] = _.last(reportActionsForDisplay); + visibleReportActionItems[reportID] = reportActionsForDisplay[reportActionsForDisplay.length - 1]; }, }); // Session can remain stale because the only way for the current user to change is to // sign out and sign in, which would clear out all the Onyx // data anyway and cause SidebarLinks to rerender. -let currentUserAccountID; +let currentUserAccountID: number | undefined; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - if (!val) { + callback: (session) => { + if (!session) { return; } - currentUserAccountID = val.accountID; + currentUserAccountID = session.accountID; }, }); -let resolveSidebarIsReadyPromise; +let resolveSidebarIsReadyPromise: (args?: unknown[]) => void; let sidebarIsReadyPromise = new Promise((resolve) => { resolveSidebarIsReadyPromise = resolve; @@ -71,11 +72,11 @@ function isSidebarLoadedReady() { return sidebarIsReadyPromise; } -function compareStringDates(stringA, stringB) { - if (stringA < stringB) { +function compareStringDates(a: string, b: string) { + if (a < b) { return -1; } - if (stringA > stringB) { + if (a > b) { return 1; } return 0; @@ -89,7 +90,7 @@ function setIsSidebarLoadedReady() { const reportIDsCache = new Map(); // Function to set a key-value pair while maintaining the maximum key limit -function setWithLimit(map, key, value) { +function setWithLimit(map: Map, key: TKey, value: TValue) { if (map.size >= 5) { // If the map has reached its limit, remove the first (oldest) key-value pair const firstKey = map.keys().next().value; @@ -102,18 +103,11 @@ function setWithLimit(map, key, value) { let hasInitialReportActions = false; /** - * @param {String} currentReportId - * @param {Object} allReportsDict - * @param {Object} betas - * @param {String[]} policies - * @param {String} priorityMode - * @param {Object} allReportActions - * @returns {String[]} An array of reportIDs sorted in the proper order + * @returns An array of reportIDs sorted in the proper order */ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions) { // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( - // eslint-disable-next-line es/no-optional-chaining [currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], (key, value) => { /** @@ -216,13 +210,13 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p /** * Gets all the data necessary for rendering an OptionRowLHN component * - * @param {Object} report - * @param {Object} reportActions - * @param {Object} personalDetails - * @param {String} preferredLocale - * @param {Object} [policy] - * @param {Object} parentReportAction - * @returns {Object} + * @param report + * @param reportActions + * @param personalDetails + * @param preferredLocale + * @param [policy] + * @param parentReportAction + * @returns */ function getOptionData(report, reportActions, personalDetails, preferredLocale, policy, parentReportAction) { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for @@ -331,14 +325,11 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, } : null; } - let lastMessageText = - hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID && Number(lastActorDetails.accountID) !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; + let lastMessageText = hasMultipleParticipants && lastActorDetails?.accountID && Number(lastActorDetails.accountID) !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; if (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]?.originalMessage?.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), policyName: ReportUtils.getPolicyName(report, false, policy), From 10922f70c17b5fd84aed450c968a4b9b326a4ec4 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 27 Sep 2023 23:31:35 +0530 Subject: [PATCH 046/548] fix: ignore other actions if navigation in progress in worksspace setting options --- src/components/MenuItemList.js | 13 +++++- src/pages/workspace/WorkspaceInitialPage.js | 45 +++++++++++---------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index 90f863ba2bc7..03dbe81c82c4 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -4,18 +4,25 @@ import PropTypes from 'prop-types'; import MenuItem from './MenuItem'; import menuItemPropTypes from './menuItemPropTypes'; import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; -import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; +import { CONTEXT_MENU_TYPES } from '../pages/home/report/ContextMenu/ContextMenuActions'; +import useSingleExecution from '../hooks/useSingleExecution'; +import useWaitForNavigation from '../hooks/useWaitForNavigation'; const propTypes = { /** An array of props that are pass to individual MenuItem components */ menuItems: PropTypes.arrayOf(PropTypes.shape(menuItemPropTypes)), + /** Whether or not to use the single execution hook */ + shouldUseSingleExecution: PropTypes.bool, }; const defaultProps = { menuItems: [], + shouldUseSingleExecution: false, }; function MenuItemList(props) { let popoverAnchor; + const { isExecuting, singleExecution } = useSingleExecution(); + const waitForNavigate = useWaitForNavigation(); /** * Handle the secondary interaction for a menu item. @@ -35,12 +42,14 @@ function MenuItemList(props) { <> {_.map(props.menuItems, (menuItemProps) => ( secondaryInteraction(menuItemProps.link, e) : undefined} ref={(el) => (popoverAnchor = el)} shouldBlockSelection={Boolean(menuItemProps.link)} // eslint-disable-next-line react/jsx-props-no-spreading {...menuItemProps} + disabled={menuItemProps.disabled || isExecuting} + onPress={props.shouldUseSingleExecution ? singleExecution(waitForNavigate(menuItemProps.onPress)) : menuItemProps.onPress} /> ))} diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 567aef1274e1..962b8bfd056e 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -1,8 +1,8 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useState} from 'react'; -import {View, ScrollView} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import React, { useCallback, useEffect, useState } from 'react'; +import { View, ScrollView } from 'react-native'; +import { withOnyx } from 'react-native-onyx'; import PropTypes from 'prop-types'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; @@ -13,12 +13,11 @@ import ConfirmModal from '../../components/ConfirmModal'; import * as Expensicons from '../../components/Icon/Expensicons'; import ScreenWrapper from '../../components/ScreenWrapper'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import MenuItem from '../../components/MenuItem'; import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import compose from '../../libs/compose'; import Avatar from '../../components/Avatar'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; -import {policyPropTypes, policyDefaultProps} from './withPolicy'; +import { policyPropTypes, policyDefaultProps } from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import reportPropTypes from '../reportPropTypes'; import * as Policy from '../../libs/actions/Policy'; @@ -31,6 +30,7 @@ import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursemen import * as ReportUtils from '../../libs/ReportUtils'; import withWindowDimensions from '../../components/withWindowDimensions'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; +import MenuItemList from '../../components/MenuItemList'; const propTypes = { ...policyPropTypes, @@ -171,12 +171,12 @@ function WorkspaceInitialPage(props) { }, { icon: Expensicons.Hashtag, - text: props.translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS}), + text: props.translate('workspace.common.goToRoom', { roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS }), onSelected: () => goToRoom(CONST.REPORT.CHAT_TYPE.POLICY_ADMINS), }, { icon: Expensicons.Hashtag, - text: props.translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE}), + text: props.translate('workspace.common.goToRoom', { roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE }), onSelected: () => goToRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE), }, ]; @@ -186,7 +186,7 @@ function WorkspaceInitialPage(props) { includeSafeAreaPaddingBottom={false} testID={WorkspaceInitialPage.displayName} > - {({safeAreaPaddingBottomStyle}) => ( + {({ safeAreaPaddingBottomStyle }) => ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} shouldShow={_.isEmpty(props.policy) || !PolicyUtils.isPolicyAdmin(props.policy) || PolicyUtils.isPendingDeletePolicy(props.policy)} @@ -250,19 +250,22 @@ function WorkspaceInitialPage(props) { )} - {_.map(menuItems, (item) => ( - item.action()} - shouldShowRightIcon - brickRoadIndicator={item.brickRoadIndicator} - /> - ))} + ({ + key: item.translationKey, + disabled: hasPolicyCreationError, + interactive: !hasPolicyCreationError, + title: props.translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + onPress: item.action, + shouldShowRightIcon: true, + brickRoadIndicator: item.brickRoadIndicator, + })) + } + shouldUseSingleExecution + /> From d21e914f4ae6f0006efe4e47392899a3394dda91 Mon Sep 17 00:00:00 2001 From: isabelastisser <31258516+isabelastisser@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:20:08 -0400 Subject: [PATCH 047/548] Create Security.md This is a new article that needs to be added to ExpensifyHelp. --- .../new-expensify/getting-started/Security.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/articles/new-expensify/getting-started/Security.md diff --git a/docs/articles/new-expensify/getting-started/Security.md b/docs/articles/new-expensify/getting-started/Security.md new file mode 100644 index 000000000000..58bdcb3cf134 --- /dev/null +++ b/docs/articles/new-expensify/getting-started/Security.md @@ -0,0 +1,53 @@ +--- +title: Security + +--- + + +# Overview + +We take security seriously. Our measures align with what banks use to protect sensitive financial data. We regularly test and update our security to stay ahead of any threats. Plus, we're checked daily by McAfee for extra reassurance against hackers. You can verify our security strength below or on the McAfee SECURE site . + +Discover how Expensify safeguards your information below! + +## The Gold Standard of Security + +Expensify follows the highest standard of security, known as the Payment Card Industry Data Security Standard. This standard is used by major companies like PayPal, Visa, and banks to protect online credit card information. It covers many aspects of how systems work together securely. You can learn more about it on the PCI-DSS website. And, Expensify is also compliant with SSAE 16! + + +## Data and Password Encryption + +When you press 'enter,' your data transforms into a secret code, making it super secure. This happens whether it's moving between your browser and our servers or within our server network. In tech talk, we use HTTPS+TLS for all web connections, ensuring your information is encrypted at every stage of the journey. This means your data is always protected! + +## Account Safety + +Protecting your data on our servers is our top priority. We've taken strong measures to ensure your data is safe when it travels between you and us and when it's stored on our servers. +In our first year, we focused on creating a super-reliable, geographically redundant, and PCI compliant data center. This means your data stays safe, and our systems stay up and running. +We use a dual-control key, which only our servers know about. This key is split into two parts and stored in separate secure places, managed by different Expensify employees. +With this setup, sensitive data stays secure and can't be accessed outside our secure servers. + +## Our Commitment to GDPR + +The General Data Protection Regulation (GDPR), introduced by the European Commission, is a set of rules to strengthen and unify data protection for individuals in the European Union (EU). It also addresses the transfer of personal data outside the EU. This regulation applies not only to EU-based organizations but also to those outside the EU that handle the data of EU citizens. The compliance deadline for GDPR was May 25, 2018. + +Our commitment to protecting the privacy of our customer’s data includes: + +- Being active participants in the EU-US Privacy Shield and Swiss-US Privacy Shield Frameworks. +- Undergoing annual SSAE-18 SOC 1 Type 2 audit by qualified, independent third-party auditors. +- Maintaining PCI-DSS compliance. +- Leveraging third-party experts to conduct yearly penetration tests. +- All employees and contractors are subject to background checks (refreshed. annually), sign non-disclosure agreements, and are subject to ongoing security and privacy training. + + +We have worked diligently to ensure we comply with GDPR. Here are some key changes we made: + + +- **Enhanced Security and Data Privacy**: We've strengthened our security measures and carefully reviewed our privacy policies to align with GDPR requirements. +- **Dedicated Data Protection Officer**: We've appointed a dedicated Data Protection Officer who can be reached at privacy@expensify.com for any privacy-related inquiries. +- **Vendor Agreements**: We've signed Data Processing Addendums (DPAs) with all our vendors to ensure your data is handled safely during onward transfers. +- **Transparency**: You can find details about the sub-processors we use on our website. +- **Privacy Shield Certification**: We maintain certifications for the E.U.-U.S. Privacy Shield and the Swiss-U.S. Privacy Shield, which help secure international data transfers. +- **GDPR Compliance**: We have a Data Processing Addendum that outlines the terms to meet GDPR requirements. You can request a copy by contacting concierge@expensify.com. +- **User Control**: Our product tools allow users to export data, manage preferences, and close accounts anytime. + +**Disclaimer**: Please note that the information on this page is for informational purposes only and is not intended as legal advice. It's essential to consult with legal and professional counsel to understand how GDPR may apply to your specific situation. From 0b7a83e8edd8e44971226f86f55c81140b20fc76 Mon Sep 17 00:00:00 2001 From: Srikar Parsi Date: Thu, 28 Sep 2023 18:22:10 +0800 Subject: [PATCH 048/548] icons and frontend wip --- assets/images/bell.svg | 3 ++ assets/images/bellSlash.svg | 3 ++ ios/NewExpensify.xcodeproj/project.pbxproj | 18 +++++++- ios/tmp.xcconfig | 10 ++++- src/components/Icon/Expensicons.js | 4 ++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/Report.js | 1 + .../report/ContextMenu/ContextMenuActions.js | 43 +++++++++++++++++-- 9 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 assets/images/bell.svg create mode 100644 assets/images/bellSlash.svg diff --git a/assets/images/bell.svg b/assets/images/bell.svg new file mode 100644 index 000000000000..a53c9508cbd6 --- /dev/null +++ b/assets/images/bell.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/bellSlash.svg b/assets/images/bellSlash.svg new file mode 100644 index 000000000000..2cacb07f4268 --- /dev/null +++ b/assets/images/bellSlash.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 64ed3fda8b02..d1cd2d066833 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -29,7 +29,7 @@ 70CF6E82262E297300711ADC /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 70CF6E81262E297300711ADC /* BootSplash.storyboard */; }; BDB853621F354EBB84E619C2 /* ExpensifyNewKansas-MediumItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */; }; DD79042B2792E76D004484B4 /* RCTBootSplash.m in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.m */; }; - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */ = {isa = PBXBuildFile; }; E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; }; ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; }; F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; }; @@ -124,7 +124,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, 5A464BC8112CDB1DE1E38F1C /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -722,9 +722,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = NewExpensify/Chat.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 368M544MTT; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/NewExpensify/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -738,6 +740,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.expensify.chat.dev; PRODUCT_NAME = "New Expensify Dev"; PROVISIONING_PROFILE_SPECIFIER = expensify_chat_dev; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = expensify_chat_dev; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -754,9 +757,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = NewExpensify/Chat.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 368M544MTT; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; INFOPLIST_FILE = NewExpensify/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -769,6 +774,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.expensify.chat.dev; PRODUCT_NAME = "New Expensify Dev"; PROVISIONING_PROFILE_SPECIFIER = expensify_chat_dev; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = expensify_chat_dev; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -976,6 +982,7 @@ CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 368M544MTT; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/NewExpensify/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -989,6 +996,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat; PRODUCT_NAME = "New Expensify"; PROVISIONING_PROFILE_SPECIFIER = chat_expensify_appstore; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = chat_expensify_appstore; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1098,6 +1106,7 @@ CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 368M544MTT; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/NewExpensify/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -1111,6 +1120,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.expensify.chat.adhoc; PRODUCT_NAME = "New Expensify AdHoc"; PROVISIONING_PROFILE_SPECIFIER = chat_expensify_appstore; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = expensify_chat_adhoc; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1214,6 +1224,7 @@ CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 368M544MTT; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; INFOPLIST_FILE = NewExpensify/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1226,6 +1237,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat; PRODUCT_NAME = "New Expensify"; PROVISIONING_PROFILE_SPECIFIER = chat_expensify_appstore; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = chat_expensify_appstore; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -1326,6 +1338,7 @@ CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 368M544MTT; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; INFOPLIST_FILE = NewExpensify/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1338,6 +1351,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.expensify.chat.adhoc; PRODUCT_NAME = "New Expensify AdHoc"; PROVISIONING_PROFILE_SPECIFIER = chat_expensify_appstore; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = expensify_chat_adhoc; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/tmp.xcconfig b/ios/tmp.xcconfig index 8b137891791f..2f2502669450 100644 --- a/ios/tmp.xcconfig +++ b/ios/tmp.xcconfig @@ -1 +1,9 @@ - +NEW_EXPENSIFY_URL=https:/$()/new.expensify.com/ +SECURE_EXPENSIFY_URL=https:/$()/secure.expensify.com/ +EXPENSIFY_URL=https:/$()/www.expensify.com/ +EXPENSIFY_PARTNER_NAME=chat-expensify-com +EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66 +PUSHER_APP_KEY=268df511a204fbb60884 +USE_WEB_PROXY=false +ENVIRONMENT=production +SEND_CRASH_REPORTS=true diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js index a0c8b72d755a..0eea0ca7398c 100644 --- a/src/components/Icon/Expensicons.js +++ b/src/components/Icon/Expensicons.js @@ -9,6 +9,8 @@ import ArrowRightLong from '../../../assets/images/arrow-right-long.svg'; import ArrowsUpDown from '../../../assets/images/arrows-updown.svg'; import BackArrow from '../../../assets/images/back-left.svg'; import Bank from '../../../assets/images/bank.svg'; +import Bell from '../../../assets/images/bell.svg'; +import BellSlash from '../../../assets/images/bellSlash.svg'; import Bill from '../../../assets/images/bill.svg'; import Bolt from '../../../assets/images/bolt.svg'; import Briefcase from '../../../assets/images/briefcase.svg'; @@ -138,6 +140,8 @@ export { BackArrow, Bank, Bill, + Bell, + BellSlash, Bolt, Briefcase, Bug, diff --git a/src/languages/en.ts b/src/languages/en.ts index 53b9bab98ced..e038acf7a862 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -400,6 +400,7 @@ export default { onlyVisible: 'Only visible to', replyInThread: 'Reply in thread', subscribeToThread: 'Subscribe to thread', + unsubscribeFromThread: 'Unsubscribe from thread', flagAsOffensive: 'Flag as offensive', }, emojiReactions: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 4eead6ddc0f3..c577e8f4b7ce 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -391,6 +391,7 @@ export default { onlyVisible: 'Visible sólo para', replyInThread: 'Responder en el hilo', subscribeToThread: 'NEED TO TRANSLATE', + unsubscribeFromThread: 'NEED TO TRANSLATE', flagAsOffensive: 'Marcar como ofensivo', }, emojiReactions: { diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index ecb98548d5e1..4d9b74a5a69f 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -706,6 +706,7 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction = * @param {String} childReportID The reportID we are trying to open * @param {Object} parentReportAction the parent comment of a thread * @param {String} parentReportID The reportID of the parent + * @param {String} prevNotificationPreference The previous notification preference for the child report * */ function toggleSubscribeToChildReport(childReportID = '0', parentReportAction = {}, parentReportID = '0', prevNotificationPreference) { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 1e603e40a99e..3fd39ddf25c0 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -138,7 +138,7 @@ export default [ if (closePopover) { hideContextMenu(false, () => { ReportActionComposeFocusManager.focus(); - Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', ''), reportAction, reportID); }); return; } @@ -150,22 +150,57 @@ export default [ { isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.subscribeToThread', - icon: Expensicons.Chair, + // textTranslateKey: lodashGet(reportAction, 'childReportNotificationPreference', '0'), + icon: Expensicons.Bell, + successTextTranslateKey: '', + successIcon: null,g + shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + const subscribed = lodashGet(reportAction, 'childReportNotificationPreference', '') !== "hidden"; + if (type !== CONTEXT_MENU_TYPES.REPORT_ACTION) { + return false; + } + const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); + const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; + const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); + return !subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); + }, + onPress: (closePopover, {reportAction, reportID}) => { + Log.info("sparsisparsi start"); + Log.info(lodashGet(reportAction, 'childReportNotificationPreference', '0')); + Log.info("sparsisparsi done"); + debugger; + // if (closePopover) { + // hideContextMenu(false, () => { + // ReportActionComposeFocusManager.focus(); + // Report.subscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + // }); + // return; + // } + }, + getDescription: () => {}, + }, + { + isAnonymousAction: false, + textTranslateKey: 'reportActionContextMenu.unsubscribeFromThread', + // textTranslateKey: lodashGet(reportAction, 'childReportNotificationPreference', '0'), + icon: Expensicons.BellSlash, successTextTranslateKey: '', successIcon: null, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + const subscribed = lodashGet(reportAction, 'childReportNotificationPreference', '0') !== "hidden"; if (type !== CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; } const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); - return isCommentAction || isReportPreviewAction || isIOUAction; + return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { Log.info("sparsisparsi start"); - Log.info(JSON.stringify(reportAction)); + Log.info(lodashGet(reportAction, 'childReportNotificationPreference', '0')); Log.info("sparsisparsi done"); + debugger; // if (closePopover) { // hideContextMenu(false, () => { // ReportActionComposeFocusManager.focus(); From 255fbab47a128f61f0b0324199b7009f21ccb3dd Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 28 Sep 2023 13:14:33 +0200 Subject: [PATCH 049/548] fix: resolve merge conflicts --- src/libs/actions/MapboxToken.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/MapboxToken.ts b/src/libs/actions/MapboxToken.ts index 3ed8affc230b..e0d979edb74e 100644 --- a/src/libs/actions/MapboxToken.ts +++ b/src/libs/actions/MapboxToken.ts @@ -118,10 +118,10 @@ const init = () => { let network: Network | null; connectionIDForNetwork = Onyx.connect({ key: ONYXKEYS.NETWORK, - callback: (val) => { + callback: (value) => { // When the network reconnects, check if the token has expired. If it has, then clearing the token will // trigger the fetch of a new one - if (network && network.isOffline && val && !val.isOffline) { + if (network && network.isOffline && value && !value.isOffline) { if (Object.keys(currentToken ?? {}).length === 0) { fetchToken(); } else if (!isCurrentlyFetchingToken && hasTokenExpired()) { @@ -129,7 +129,7 @@ const init = () => { clearToken(); } } - network = val; + network = value; }, }); } From 1387d2120ff18a5fc1f638e1198d3a3bbe71f24d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 28 Sep 2023 14:06:21 +0200 Subject: [PATCH 050/548] Fix report metadata --- src/libs/actions/Report.js | 84 ++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 4fe834fe286c..a6e115fe5d9c 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -468,7 +468,6 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, }, - }, ]; @@ -720,12 +719,12 @@ function reconnect(reportID) { key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), - }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { isLoadingInitialReportActions: true, isLoadingNewerReportActions: false, isLoadingOlderReportActions: false, @@ -777,16 +776,13 @@ function readOldestAction(reportID, reportActionID) { isLoadingOlderReportActions: true, }, }, - // { - // onyxMethod: Onyx.METHOD.MERGE, - // key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - // value: { - // isLoadingInitialReportActions: true, - // isLoadingOlderReportActions: false, - // isLoadingNewerReportActions: false, - // }, - - // }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingOlderReportActions: true, + }, + }, ], successData: [ { @@ -796,6 +792,13 @@ function readOldestAction(reportID, reportActionID) { isLoadingOlderReportActions: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingOlderReportActions: false, + }, + }, ], failureData: [ { @@ -805,6 +808,13 @@ function readOldestAction(reportID, reportActionID) { isLoadingOlderReportActions: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingOlderReportActions: true, + }, + }, ], }, ); @@ -833,6 +843,13 @@ function getOlderActions(reportID, reportActionID) { isLoadingOlderReportActions: true, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingOlderReportActions: true, + }, + }, ], successData: [ { @@ -842,6 +859,13 @@ function getOlderActions(reportID, reportActionID) { isLoadingOlderReportActions: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingOlderReportActions: false, + }, + }, ], failureData: [ { @@ -851,6 +875,13 @@ function getOlderActions(reportID, reportActionID) { isLoadingOlderReportActions: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingOlderReportActions: false, + }, + }, ], }, ); @@ -879,6 +910,13 @@ function getNewerActions(reportID, reportActionID) { isLoadingNewerReportActions: true, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingNewerReportActions: true, + }, + }, ], successData: [ { @@ -888,6 +926,13 @@ function getNewerActions(reportID, reportActionID) { isLoadingNewerReportActions: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingNewerReportActions: false, + }, + }, ], failureData: [ { @@ -897,6 +942,13 @@ function getNewerActions(reportID, reportActionID) { isLoadingNewerReportActions: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isLoadingNewerReportActions: false, + }, + }, ], }, ); From a3e609d77666051fac65b079fa85d135d57e58b8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 28 Sep 2023 14:07:47 +0200 Subject: [PATCH 051/548] fix after merge --- .../InvertedFlatList/BaseInvertedFlatList.js | 1 - src/pages/home/ReportScreen.js | 8 +-- .../ListBoundaryLoader/ListBoundaryLoader.js | 8 +-- src/pages/home/report/ReportActionsList.js | 52 ++++++++++--------- src/pages/home/report/ReportActionsView.js | 7 ++- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 0d6138ec0c79..9b69de116787 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -137,7 +137,6 @@ class BaseInvertedFlatList extends Component { // Web requires that items be measured or else crazy things happen when scrolling. getItemLayout={this.props.shouldMeasureItems ? this.getItemLayout : undefined} // We keep this property very low so that chat switching remains fast - maxToRenderPerBatch={5} windowSize={15} maintainVisibleContentPosition={{ minIndexForVisible: 0, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index f7bda23e7c33..a0b7665d7055 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -172,7 +172,7 @@ function ReportScreen({ const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; // There are no reportActions at all to display and we are still in the process of loading the next set of actions. - const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; + const isFirstlyLoadingReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; @@ -371,7 +371,7 @@ function ReportScreen({ !isDefaultReport && !report.reportID && !isOptimisticDelete && - !report.isLoadingInitialReportActions && + !reportMetadata.isLoadingInitialReportActions && !isLoading && !userLeavingStatus) || shouldHideReport, @@ -426,7 +426,7 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && ( + {isReportReadyForDisplay && !isFirstlyLoadingReportActions && !isLoading && ( } + {(!isReportReadyForDisplay || isFirstlyLoadingReportActions || isLoading) && } {isReportReadyForDisplay ? ( {}, mostRecentIOUReportActionID: '', -isLoadingInitialReportActions: false, + isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, ...withCurrentUserPersonalDetailsDefaultProps, @@ -123,8 +123,8 @@ function ReportActionsList({ const [currentUnreadMarker, setCurrentUnreadMarker] = useState(null); const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); - const reportActionSize = useRef(sortedReportActions.length); const firstRenderRef = useRef(true); + const reportActionSize = useRef(sortedReportActions.length); // This state is used to force a re-render when the user manually marks a message as unread // by using a timestamp you can force re-renders without having to worry about if another message was marked as unread before @@ -257,13 +257,13 @@ function ReportActionsList({ /** * Calculates the ideal number of report actions to render in the first render, based on the screen height and on * the height of the smallest report action possible. - * @return {{availableContentHeight: Number, initialNumToRender: Number}} + * @return {Number} */ const initialNumToRender = useMemo(() => { - const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; - const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); - return Math.ceil(availableHeight / minimumReportActionHeight); - }, [windowHeight]); + const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; + const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); + return Math.ceil(availableHeight / minimumReportActionHeight); + }, [windowHeight]); const lastReportAction = useMemo(() => _.last(sortedReportActions) || {}, [sortedReportActions]); /** @@ -327,20 +327,21 @@ function ReportActionsList({ [isLoadingNewerReportActions], ); - const listFooterComponent = useCallback( - () => ( + const listFooterComponent = useCallback(() => { + if (firstRenderRef.current) { + firstRenderRef.current = false; + return null; + } + + return ( - ), - [isLoadingInitialReportActions, isLoadingOlderReportActions, lastReportAction.actionName], - ); - - - + ); + }, [isLoadingInitialReportActions, isLoadingOlderReportActions, lastReportAction.actionName]); const onLayoutInner = useCallback( (event) => { @@ -349,15 +350,18 @@ function ReportActionsList({ [onLayout], ); - const listHeaderComponent = useCallback( - () => ( - - ), - [isLoadingNewerReportActions], - ); + const listHeaderComponent = useCallback(() => { + if (firstRenderRef.current) { + firstRenderRef.current = false; + return null; + } + return ( + + ); + }, [isLoadingNewerReportActions]); return ( <> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index b9db8602c1cd..7acf9eca9349 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -34,6 +34,9 @@ const propTypes = { /** The report actions are loading more data */ isLoadingOlderReportActions: PropTypes.bool, + /** The report actions are loading newer data*/ + isLoadingNewerReportActions: PropTypes.bool, + /** Whether the composer is full size */ /* eslint-disable-next-line react/no-unused-prop-types */ isComposerFullSize: PropTypes.bool.isRequired, @@ -59,6 +62,7 @@ const defaultProps = { policy: null, isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, + isLoadingNewerReportActions: false, }; function ReportActionsView(props) { @@ -184,7 +188,7 @@ function ReportActionsView(props) { } const newestReportAction = _.first(props.reportActions); Report.getNewerActions(reportID, newestReportAction.reportActionID); - }, 300); + }, 700); /** * Runs when the FlatList finishes laying out @@ -222,6 +226,7 @@ function ReportActionsView(props) { loadNewerChats={loadNewerChats} isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} + isLoadingNewerReportActions={props.isLoadingNewerReportActions} policy={props.policy} /> From 35f1ad3dcaff28ad9a88fb323ee328f86a7c4c35 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 28 Sep 2023 14:15:39 +0200 Subject: [PATCH 052/548] lint --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 1 - src/pages/home/ReportScreen.js | 2 +- src/pages/home/report/ReportActionsView.js | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 9b69de116787..544786ea1910 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -136,7 +136,6 @@ class BaseInvertedFlatList extends Component { // Native platforms do not need to measure items and work fine without this. // Web requires that items be measured or else crazy things happen when scrolling. getItemLayout={this.props.shouldMeasureItems ? this.getItemLayout : undefined} - // We keep this property very low so that chat switching remains fast windowSize={15} maintainVisibleContentPosition={{ minIndexForVisible: 0, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index a0b7665d7055..aedc9247a21f 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -375,7 +375,7 @@ function ReportScreen({ !isLoading && !userLeavingStatus) || shouldHideReport, - [report, isLoading, shouldHideReport, isDefaultReport, isOptimisticDelete, userLeavingStatus], + [report, isLoading, shouldHideReport, isDefaultReport, isOptimisticDelete, userLeavingStatus, reportMetadata.isLoadingInitialReportActions], ); return ( diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 7acf9eca9349..a0dee8abdb71 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -34,7 +34,7 @@ const propTypes = { /** The report actions are loading more data */ isLoadingOlderReportActions: PropTypes.bool, - /** The report actions are loading newer data*/ + /** The report actions are loading newer data */ isLoadingNewerReportActions: PropTypes.bool, /** Whether the composer is full size */ From 8113ebf244d214dc2e9d9d02ebb8d93fb88377a1 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Fri, 29 Sep 2023 12:24:07 +0200 Subject: [PATCH 053/548] Type fixes --- src/libs/PolicyUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 1f2abfa2b7a8..5b4ed6fcd208 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -96,11 +96,11 @@ const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CO function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; Object.keys(policyMembers).forEach((accountID) => { - const member = policyMembers[accountID]; + const member = policyMembers?.[accountID]; if (Object.keys(member?.errors ?? {}).length > 0) { return; } - const personalDetail = personalDetails[accountID]; + const personalDetail = personalDetails?.[accountID]; if (!personalDetail?.login) { return; } @@ -115,12 +115,12 @@ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, person function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; Object.keys(policyMembers).forEach((accountID) => { - const policyMember = policyMembers[accountID]; + const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } - const memberEmail = personalDetails[accountID]?.login; + const memberEmail = personalDetails?.[accountID]?.login; if (!memberEmail) { return; } From 837bd88068113e148404d989f4331e8bfef0d7ec Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 2 Oct 2023 10:58:11 +0200 Subject: [PATCH 054/548] Migrate PolicyUtils to TS --- src/libs/PolicyUtils.ts | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index a901384ef668..9b45318e67eb 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -14,7 +14,9 @@ type UnitRate = {rate: number}; * These are policies that we can use to create reports with in NewDot. */ function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { - return policies.filter((policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + return (policies ?? []).filter( + (policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ); } /** @@ -22,14 +24,14 @@ function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} */ function hasPolicyMemberError(policyMembers: PolicyMemberList): boolean { - return Object.values(policyMembers).some((member) => Object.keys(member?.errors ?? {}).length > 0); + return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. */ function hasPolicyErrorFields(policy: OnyxTypes.Policy): boolean { - return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors).length > 0); + return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors ?? {}).length > 0); } /** @@ -87,19 +89,19 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembe function shouldShowPolicy(policy: OnyxTypes.Policy, isOffline: boolean): boolean { return ( policy && - policy.isPolicyExpenseChatEnabled && - policy.role === CONST.POLICY.ROLE.ADMIN && - (isOffline || policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors).length > 0) + policy?.isPolicyExpenseChatEnabled && + policy?.role === CONST.POLICY.ROLE.ADMIN && + (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) ); } function isExpensifyTeam(email: string): boolean { - const emailDomain = Str.extractEmailDomain(email); + const emailDomain = Str.extractEmailDomain(email ?? ''); return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } function isExpensifyGuideTeam(email: string): boolean { - const emailDomain = Str.extractEmailDomain(email); + const emailDomain = Str.extractEmailDomain(email ?? ''); return emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } @@ -115,9 +117,9 @@ const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CO */ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { const memberEmailsToAccountIDs: Record = {}; - Object.keys(policyMembers).forEach((accountID) => { + Object.keys(policyMembers ?? {}).forEach((accountID) => { const member = policyMembers?.[accountID]; - if (Object.keys(member?.errors ?? {}).length > 0) { + if (Object.keys(member?.errors ?? {})?.length > 0) { return; } const personalDetail = personalDetails[accountID]; @@ -134,7 +136,7 @@ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, person */ function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; - Object.keys(policyMembers).forEach((accountID) => { + Object.keys(policyMembers ?? {}).forEach((accountID) => { const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { @@ -154,11 +156,11 @@ function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: * Gets the tag from policy tags, defaults to the first if no key is provided. */ function getTag(policyTags: Record, tagKey?: keyof typeof policyTags) { - if (Object.keys(policyTags).length === 0) { + if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } - const policyTagKey = tagKey ?? Object.keys(policyTags)[0]; + const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; return policyTags?.[policyTagKey] ?? {}; } @@ -167,11 +169,11 @@ function getTag(policyTags: Record, tagKey?: keyof * Gets the first tag name from policy tags. */ function getTagListName(policyTags: Record) { - if (Object.keys(policyTags).length === 0) { + if (Object.keys(policyTags ?? {})?.length === 0) { return ''; } - const policyTagKeys = Object.keys(policyTags)[0] ?? []; + const policyTagKeys = Object.keys(policyTags ?? {})[0] ?? []; return policyTags?.[policyTagKeys]?.name ?? ''; } @@ -180,17 +182,17 @@ function getTagListName(policyTags: Record) { * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. */ function getTagList(policyTags: Record>, tagKey: string) { - if (Object.keys(policyTags).length === 0) { + if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } - const policyTagKey = tagKey ?? Object.keys(policyTags)[0]; + const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; return policyTags?.[policyTagKey]?.tags ?? {}; } function isPendingDeletePolicy(policy: OnyxTypes.Policy): boolean { - return policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } export { From f42364d124e12225486364263fec5eef0b7c31b2 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 2 Oct 2023 12:34:47 +0200 Subject: [PATCH 055/548] [TS migration] Migrate 'SidebarUtils.js' lib to TypeScript --- src/libs/PersonalDetailsUtils.js | 4 +- src/libs/ReportUtils.js | 16 +- src/libs/SidebarUtils.ts | 238 +++++++++++++++++++++--------- src/types/onyx/PersonalDetails.ts | 2 + src/types/onyx/Report.ts | 7 +- 5 files changed, 186 insertions(+), 81 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index a401dea4b911..98bf171812fc 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -17,8 +17,8 @@ Onyx.connect({ }); /** - * @param {Object} passedPersonalDetails - * @param {Array} pathToDisplayName + * @param {Object | Null} passedPersonalDetails + * @param {Array | String} pathToDisplayName * @param {String} [defaultValue] optional default display name value * @returns {String} */ diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 475c1a8bcb8a..da2ef0f04ebf 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -108,9 +108,9 @@ function getPolicyType(report, policies) { /** * Get the policy name from a given report * @param {Object} report - * @param {String} report.policyID - * @param {String} report.oldPolicyName - * @param {String} report.policyName + * @param {String} [report.policyID] + * @param {String} [report.oldPolicyName] + * @param {String} [report.policyName] * @param {Boolean} [returnEmptyIfNotFound] * @param {Object} [policy] * @returns {String} @@ -363,7 +363,7 @@ function isUserCreatedPolicyRoom(report) { /** * Whether the provided report is a Policy Expense chat. * @param {Object} report - * @param {String} report.chatType + * @param {String} [report.chatType] * @returns {Boolean} */ function isPolicyExpenseChat(report) { @@ -389,7 +389,7 @@ function isControlPolicyExpenseReport(report) { /** * Whether the provided report is a chat room * @param {Object} report - * @param {String} report.chatType + * @param {String} [report.chatType] * @returns {Boolean} */ function isChatRoom(report) { @@ -578,8 +578,8 @@ function findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTim /** * Whether the provided report is an archived room * @param {Object} report - * @param {Number} report.stateNum - * @param {Number} report.statusNum + * @param {Number} [report.stateNum] + * @param {Number} [report.statusNum] * @returns {Boolean} */ function isArchivedRoom(report) { @@ -2912,7 +2912,7 @@ function shouldHideReport(report, currentReportId) { * filter out the majority of reports before filtering out very specific minority of reports. * * @param {Object} report - * @param {String} currentReportId + * @param {String | Null} currentReportId * @param {Boolean} isInGSDMode * @param {String[]} betas * @param {Object} policies diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index e1251f9f50bc..938c0f7f0a26 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,8 +1,7 @@ /* eslint-disable rulesdir/prefer-underscore-method */ import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; +import {ValueOf} from 'type-fest'; import ONYXKEYS from '../ONYXKEYS'; import * as ReportUtils from './ReportUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; @@ -14,6 +13,11 @@ import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as UserUtils from './UserUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import ReportAction from '../types/onyx/ReportAction'; +import Beta from '../types/onyx/Beta'; +import Policy from '../types/onyx/Policy'; +import Report from '../types/onyx/Report'; +import {PersonalDetails} from '../types/onyx'; +import * as OnyxCommon from '../types/onyx/OnyxCommon'; const visibleReportActionItems: Record = {}; const lastReportActions: Record = {}; @@ -68,11 +72,11 @@ function resetIsSidebarLoadedReadyPromise() { }); } -function isSidebarLoadedReady() { +function isSidebarLoadedReady(): Promise { return sidebarIsReadyPromise; } -function compareStringDates(a: string, b: string) { +function compareStringDates(a: string, b: string): 0 | 1 | -1 { if (a < b) { return -1; } @@ -87,7 +91,7 @@ function setIsSidebarLoadedReady() { } // Define a cache object to store the memoized results -const reportIDsCache = new Map(); +const reportIDsCache = new Map(); // Function to set a key-value pair while maintaining the maximum key limit function setWithLimit(map: Map, key: TKey, value: TValue) { @@ -105,11 +109,18 @@ let hasInitialReportActions = false; /** * @returns An array of reportIDs sorted in the proper order */ -function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions) { +function getOrderedReportIDs( + currentReportId: string | null, + allReports: Record, + betas: Beta[], + policies: Record, + priorityMode: ValueOf, + allReportActions: Record, +): string[] { // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( - [currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], - (key, value) => { + [currentReportId, allReports, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], + (key, value: unknown) => { /** * Exclude 'participantAccountIDs', 'participants' and 'lastMessageText' not to overwhelm a cached key value with huge data, * which we don't need to store in a cacheKey @@ -117,13 +128,15 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p if (key === 'participantAccountIDs' || key === 'participants' || key === 'lastMessageText') { return undefined; } + return value; }, ); // Check if the result is already in the cache - if (reportIDsCache.has(cachedReportsKey) && hasInitialReportActions) { - return reportIDsCache.get(cachedReportsKey); + const cachedIDs = reportIDsCache.get(cachedReportsKey); + if (cachedIDs && hasInitialReportActions) { + return cachedIDs; } // This is needed to prevent caching when Onyx is empty for a second render @@ -131,7 +144,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; - const allReportsDictValues = Object.values(allReportsDict); + const allReportsDictValues = Object.values(allReports); // Filter out all the reports that shouldn't be displayed const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, true)); @@ -152,7 +165,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p report.displayName = ReportUtils.getReportName(report); // eslint-disable-next-line no-param-reassign - report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReportsDict); + report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReports); }); // The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order: @@ -165,11 +178,11 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p // 5. Archived reports // - Sorted by lastVisibleActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode - const pinnedReports = []; - const outstandingIOUReports = []; - const draftReports = []; - const nonArchivedReports = []; - const archivedReports = []; + const pinnedReports: Report[] = []; + const outstandingIOUReports: Report[] = []; + const draftReports: Report[] = []; + const nonArchivedReports: Report[] = []; + const archivedReports: Report[] = []; reportsToDisplay.forEach((report) => { if (report.isPinned) { pinnedReports.push(report); @@ -185,47 +198,121 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p }); // Sort each group of reports accordingly - pinnedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); - outstandingIOUReports.sort((a, b) => b.iouReportAmount - a.iouReportAmount || a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); - draftReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); + pinnedReports.sort((a, b) => (a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0)); + outstandingIOUReports.sort((a, b) => { + const compareAmounts = a?.iouReportAmount && b?.iouReportAmount ? b.iouReportAmount - a.iouReportAmount : 0; + const compareDisplayNames = a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0; + return compareAmounts || compareDisplayNames; + }); + draftReports.sort((a, b) => (a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0)); if (isInDefaultMode) { - nonArchivedReports.sort( - (a, b) => compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated) || a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()), - ); + nonArchivedReports.sort((a, b) => { + const compareDates = a?.lastVisibleActionCreated && b?.lastVisibleActionCreated ? compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated) : 0; + const compareDisplayNames = a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0; + return compareDates || compareDisplayNames; + }); // For archived reports ensure that most recent reports are at the top by reversing the order - archivedReports.sort((a, b) => compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated)); + archivedReports.sort((a, b) => (a?.lastVisibleActionCreated && b?.lastVisibleActionCreated ? compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated) : 0)); } else { - nonArchivedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); - archivedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); + nonArchivedReports.sort((a, b) => (a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0)); + archivedReports.sort((a, b) => (a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0)); } // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [].concat(pinnedReports, outstandingIOUReports, draftReports, nonArchivedReports, archivedReports).map((report) => report.reportID); + const LHNReports = [...pinnedReports, ...outstandingIOUReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); setWithLimit(reportIDsCache, cachedReportsKey, LHNReports); return LHNReports; } +type OptionData = { + text?: string | null; + alternateText?: string | null; + pendingAction?: OnyxCommon.PendingAction | null; + allReportErrors?: OnyxCommon.Errors | null; + brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; + icons?: Icon[] | null; + tooltipText?: string | null; + ownerAccountID?: number | null; + subtitle?: string | null; + participantsList?: PersonalDetails[] | null; + login?: string | null; + accountID?: number | null; + managerID?: number | null; + reportID?: string | null; + policyID?: string | null; + status?: string | null; + type?: string | null; + stateNum?: ValueOf | null; + statusNum?: ValueOf | null; + phoneNumber?: string | null; + isUnread?: boolean | null; + isUnreadWithMention?: boolean | null; + hasDraftComment?: boolean | null; + keyForList?: string | null; + searchText?: string | null; + isPinned?: boolean | null; + hasOutstandingIOU?: boolean | null; + iouReportID?: string | null; + isIOUReportOwner?: boolean | null; + iouReportAmount?: number | null; + isChatRoom?: boolean | null; + isArchivedRoom?: boolean | null; + shouldShowSubscript?: boolean | null; + isPolicyExpenseChat?: boolean | null; + isMoneyRequestReport?: boolean | null; + isExpenseRequest?: boolean | null; + isWaitingOnBankAccount?: boolean | null; + isLastMessageDeletedParentAction?: boolean | null; + isAllowedToComment?: boolean | null; + isThread?: boolean | null; + isTaskReport?: boolean | null; + isWaitingForTaskCompleteFromAssignee?: boolean | null; + parentReportID?: string | null; + notificationPreference?: string | number | null; + displayNamesWithTooltips?: DisplayNamesWithTooltip[] | null; +}; + +type DisplayNamesWithTooltip = { + displayName?: string; + avatar?: string; + login?: string; + accountID?: number; + pronouns?: string; +}; + +type ActorDetails = { + displayName?: string; + accountID?: number; +}; + +type Icon = { + source?: string; + id?: number; + type?: string; + name?: string; +}; + /** * Gets all the data necessary for rendering an OptionRowLHN component - * - * @param report - * @param reportActions - * @param personalDetails - * @param preferredLocale - * @param [policy] - * @param parentReportAction - * @returns */ -function getOptionData(report, reportActions, personalDetails, preferredLocale, policy, parentReportAction) { +function getOptionData( + report: Report, + reportActions: Record, + personalDetails: Record, + preferredLocale: ValueOf, + policy: Policy, + parentReportAction: ReportAction, +): OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do // a null check here and return early. if (!report || !personalDetails) { return; } - const result = { + + const result: OptionData = { text: null, alternateText: null, pendingAction: null, @@ -263,10 +350,14 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, isWaitingOnBankAccount: false, isLastMessageDeletedParentAction: false, isAllowedToComment: true, + isThread: null, + isTaskReport: null, + isWaitingForTaskCompleteFromAssignee: null, + parentReportID: null, + notificationPreference: null, }; - - const participantPersonalDetailList = _.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs, personalDetails)); - const personalDetail = participantPersonalDetailList[0] || {}; + const participantPersonalDetailList: PersonalDetails[] = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); + const personalDetail = participantPersonalDetailList[0] ?? {}; result.isThread = ReportUtils.isChatThread(report); result.isChatRoom = ReportUtils.isChatRoom(report); @@ -280,8 +371,8 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null; - result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions); - result.brickRoadIndicator = !_.isEmpty(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) as OnyxCommon.Errors; + result.brickRoadIndicator = Object.keys(result.allReportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; @@ -294,30 +385,30 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); - result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs || []); + result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs ?? []); result.hasOutstandingIOU = report.hasOutstandingIOU; - result.parentReportID = report.parentReportID || null; + result.parentReportID = report.parentReportID; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; - result.notificationPreference = report.notificationPreference || null; + result.notificationPreference = report.notificationPreference; result.isAllowedToComment = !ReportUtils.shouldDisableWriteActions(report); const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; const subtitle = ReportUtils.getChatRoomSubtitle(report); - const login = Str.removeSMSDomain(lodashGet(personalDetail, 'login', '')); - const status = lodashGet(personalDetail, 'status', ''); + const login = Str.removeSMSDomain(personalDetail?.login ?? ''); + const status = personalDetail?.status ?? ''; const formattedLogin = Str.isSMSLogin(login) ? LocalePhoneNumber.formatPhoneNumber(login) : login; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); + const displayNamesWithTooltips: DisplayNamesWithTooltip[] = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report); // If the last actor's details are not currently saved in Onyx Collection, // then try to get that from the last report action if that action is valid // to get data from. - let lastActorDetails = personalDetails[report.lastActorAccountID] || null; + let lastActorDetails: ActorDetails | null = report.lastActorAccountID ? personalDetails[report.lastActorAccountID] : null; if (!lastActorDetails && visibleReportActionItems[report.reportID]) { - const lastActorDisplayName = lodashGet(visibleReportActionItems[report.reportID], 'person[0].text'); + const lastActorDisplayName = visibleReportActionItems[report.reportID]?.person?.[0]?.text; lastActorDetails = lastActorDisplayName ? { displayName: lastActorDisplayName, @@ -328,22 +419,25 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, let lastMessageText = hasMultipleParticipants && lastActorDetails?.accountID && Number(lastActorDetails.accountID) !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; - if (result.isArchivedRoom) { - const archiveReason = lastReportActions[report.reportID]?.originalMessage?.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; + const reportAction = lastReportActions[report.reportID]; + if (result.isArchivedRoom && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { + const archiveReason = reportAction?.originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; + lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), policyName: ReportUtils.getPolicyName(report, false, policy), }); } if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport) && !result.isArchivedRoom) { const lastAction = visibleReportActionItems[report.reportID]; - if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.RENAMED) { - const newName = lodashGet(lastAction, 'originalMessage.newName', ''); + + if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + const newName = lastAction?.originalMessage?.newName ?? ''; result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName}); - } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED) { + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED) { result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.reopened')}: ${report.reportName}`; - } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED) { + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED) { result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.completed')}: ${report.reportName}`; } else { result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); @@ -354,19 +448,23 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, // We also add a fullstop after the final name, the word "and" before the final name and commas between all previous names. lastMessageText = Localize.translate(preferredLocale, 'reportActionsView.beginningOfChatHistory') + - _.map(displayNamesWithTooltips, ({displayName, pronouns}, index) => { - const formattedText = _.isEmpty(pronouns) ? displayName : `${displayName} (${pronouns})`; - - if (index === displayNamesWithTooltips.length - 1) { - return `${formattedText}.`; - } - if (index === displayNamesWithTooltips.length - 2) { - return `${formattedText} ${Localize.translate(preferredLocale, 'common.and')}`; - } - if (index < displayNamesWithTooltips.length - 2) { - return `${formattedText},`; - } - }).join(' '); + displayNamesWithTooltips + .map(({displayName, pronouns}, index) => { + const formattedText = !pronouns ? displayName : `${displayName} (${pronouns})`; + + if (index === displayNamesWithTooltips.length - 1) { + return `${formattedText}.`; + } + if (index === displayNamesWithTooltips.length - 2) { + return `${formattedText} ${Localize.translate(preferredLocale, 'common.and')}`; + } + if (index < displayNamesWithTooltips.length - 2) { + return `${formattedText},`; + } + + return ''; + }) + .join(' '); } result.alternateText = lastMessageText || formattedLogin; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 64911dbfecb1..e1c944a95c40 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -40,6 +40,8 @@ type PersonalDetails = { /** Whether timezone is automatically set */ automatic?: boolean; }; + + status?: string; }; export default PersonalDetails; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 88caa683305d..b029e3996963 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -55,7 +55,7 @@ type Report = { reportName?: string; /** ID of the report */ - reportID?: string; + reportID: string; /** The state that the report is currently in */ stateNum?: ValueOf; @@ -83,6 +83,11 @@ type Report = { participantAccountIDs?: number[]; total?: number; currency?: string; + iouReportAmount?: number; + isWaitingOnBankAccount?: boolean; + isLastMessageDeletedParentAction?: boolean; + iouReportID?: string; + pendingFields?: Record; }; export default Report; From 297e8ec24663d2d8f4759f99297c3b65d411819e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 2 Oct 2023 16:42:07 +0200 Subject: [PATCH 056/548] reduce skeleton size to 3 items for fetchMore --- .../ReportActionsSkeletonView/index.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionsSkeletonView/index.js b/src/components/ReportActionsSkeletonView/index.js index 6bdc993c2055..d1254ef567ea 100644 --- a/src/components/ReportActionsSkeletonView/index.js +++ b/src/components/ReportActionsSkeletonView/index.js @@ -7,23 +7,27 @@ import CONST from '../../CONST'; const propTypes = { /** Whether to animate the skeleton view */ shouldAnimate: PropTypes.bool, + + /** Number of possible visible content items */ + possibleVisibleContentItems: PropTypes.number }; const defaultProps = { shouldAnimate: true, + possibleVisibleContentItems: 0 }; -function ReportActionsSkeletonView(props) { +function ReportActionsSkeletonView({shouldAnimate, possibleVisibleContentItems}) { // Determines the number of content items based on container height - const possibleVisibleContentItems = Math.ceil(Dimensions.get('window').height / CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT); + const visibleContentItems = possibleVisibleContentItems || Math.ceil(Dimensions.get('window').height / CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT); const skeletonViewLines = []; - for (let index = 0; index < possibleVisibleContentItems; index++) { + for (let index = 0; index < visibleContentItems; index++) { const iconIndex = (index + 1) % 4; switch (iconIndex) { case 2: skeletonViewLines.push( , @@ -32,7 +36,7 @@ function ReportActionsSkeletonView(props) { case 0: skeletonViewLines.push( , @@ -41,7 +45,7 @@ function ReportActionsSkeletonView(props) { default: skeletonViewLines.push( , From 6a863a488faf15853bea2a30aad7dcbb75428b60 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 2 Oct 2023 18:53:27 +0200 Subject: [PATCH 057/548] remove duplications of REPORT_METADATA --- src/libs/actions/Report.js | 42 ------------------- .../ListBoundaryLoader/ListBoundaryLoader.js | 2 +- src/pages/home/report/ReportActionsList.js | 1 - src/pages/home/report/ReportActionsView.js | 8 +++- src/pages/reportPropTypes.js | 9 ---- 5 files changed, 7 insertions(+), 55 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index b96ae8bec68d..86b51a165ea2 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -769,13 +769,6 @@ function readOldestAction(reportID, reportActionID) { }, { optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingOlderReportActions: true, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, @@ -785,13 +778,6 @@ function readOldestAction(reportID, reportActionID) { }, ], successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingOlderReportActions: false, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, @@ -801,13 +787,6 @@ function readOldestAction(reportID, reportActionID) { }, ], failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingOlderReportActions: false, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, @@ -836,13 +815,6 @@ function getOlderActions(reportID, reportActionID) { }, { optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingOlderReportActions: true, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, @@ -852,13 +824,6 @@ function getOlderActions(reportID, reportActionID) { }, ], successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingOlderReportActions: false, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, @@ -868,13 +833,6 @@ function getOlderActions(reportID, reportActionID) { }, ], failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingOlderReportActions: false, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, diff --git a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js index d4214c237913..ee93f299bf7c 100644 --- a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js +++ b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js @@ -42,7 +42,7 @@ function ListBoundaryLoader({type, isLoadingOlderReportActions, isLoadingInitial // skeleton view above the created action in a newly generated optimistic chat or one with not // that many comments. if (isLoadingInitialReportActions && lastReportActionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { - return ; + return ; } return null; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 0af22d91ccb6..31e48dc0e46b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -120,7 +120,6 @@ function ReportActionsList({ }) { const reportScrollManager = useReportScrollManager(); const {translate} = useLocalize(); - const {isOffline} = useNetwork(); const route = useRoute(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a0dee8abdb71..3970e5c47811 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -188,7 +188,7 @@ function ReportActionsView(props) { } const newestReportAction = _.first(props.reportActions); Report.getNewerActions(reportID, newestReportAction.reportActionID); - }, 700); + }, 500); /** * Runs when the FlatList finishes laying out @@ -255,11 +255,15 @@ function arePropsEqual(oldProps, newProps) { return false; } + if (oldProps.isLoadingInitialReportActions !== newProps.isLoadingInitialReportActions) { + return false; + } + if (oldProps.isLoadingOlderReportActions !== newProps.isLoadingOlderReportActions) { return false; } - if (oldProps.isLoadingInitialReportActions !== newProps.isLoadingInitialReportActions) { + if (oldProps.isLoadingNewerReportActions !== newProps.isLoadingNewerReportActions) { return false; } diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index c7e93b127619..a2c41b5a8147 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -13,15 +13,6 @@ export default PropTypes.shape({ /** List of icons for report participants */ icons: PropTypes.arrayOf(avatarPropTypes), - /** Are we loading more report actions? */ - isLoadingOlderReportActions: PropTypes.bool, - - /** Are we loading newer report actions? */ - isLoadingNewerReportActions: PropTypes.bool, - - /** Flag to check if the report actions data are loading */ - isLoadingInitialReportActions: PropTypes.bool, - /** Whether the user is not an admin of policyExpenseChat chat */ isOwnPolicyExpenseChat: PropTypes.bool, From 08da8c014b6c53e8042bbae41731fdd891619c62 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 3 Oct 2023 10:01:07 +0200 Subject: [PATCH 058/548] Self review the code --- src/libs/SidebarUtils.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 938c0f7f0a26..d3bb9f6491d3 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -350,11 +350,6 @@ function getOptionData( isWaitingOnBankAccount: false, isLastMessageDeletedParentAction: false, isAllowedToComment: true, - isThread: null, - isTaskReport: null, - isWaitingForTaskCompleteFromAssignee: null, - parentReportID: null, - notificationPreference: null, }; const participantPersonalDetailList: PersonalDetails[] = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); const personalDetail = participantPersonalDetailList[0] ?? {}; @@ -387,9 +382,9 @@ function getOptionData( result.keyForList = String(report.reportID); result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs ?? []); result.hasOutstandingIOU = report.hasOutstandingIOU; - result.parentReportID = report.parentReportID; + result.parentReportID = report.parentReportID ?? null; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; - result.notificationPreference = report.notificationPreference; + result.notificationPreference = report.notificationPreference ?? null; result.isAllowedToComment = !ReportUtils.shouldDisableWriteActions(report); const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; From bbcdbd146d234e201c05f74eaf40842b5a8c3811 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Oct 2023 19:17:51 +0200 Subject: [PATCH 059/548] Changes after review of PolicyUtils --- src/ONYXKEYS.ts | 2 +- src/libs/PolicyUtils.ts | 50 ++++++++++++++++++---------------- src/types/onyx/PolicyMember.ts | 3 ++ src/types/onyx/PolicyTag.ts | 3 ++ src/types/onyx/index.ts | 6 ++-- 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a1afc4fef2c1..b01ffbc37141 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -373,7 +373,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTag; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; - [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember; + [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 9b45318e67eb..c0bb7e539bec 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,21 +1,24 @@ +import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; -import * as OnyxTypes from '../types/onyx'; +import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '../types/onyx'; -type PolicyMemberList = Record; -type PolicyMembersCollection = Record; type MemberEmailsToAccountIDs = Record; -type PersonalDetailsList = Record; +type PersonalDetailsList = Record; type UnitRate = {rate: number}; /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. */ -function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { - return (policies ?? []).filter( - (policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, +function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { + if (!policies) { + return; + } + return (Object.values(policies) ?? []).filter( + (policy): policy is Policy => + policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); } @@ -23,28 +26,28 @@ function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not. * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} */ -function hasPolicyMemberError(policyMembers: PolicyMemberList): boolean { +function hasPolicyMemberError(policyMembers: OnyxEntry): boolean { return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. */ -function hasPolicyErrorFields(policy: OnyxTypes.Policy): boolean { +function hasPolicyErrorFields(policy: OnyxEntry): boolean { return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors ?? {}).length > 0); } /** * Check if the policy has any errors, and if it doesn't, then check if it has any error fields. */ -function hasPolicyError(policy: OnyxTypes.Policy): boolean { +function hasPolicyError(policy: OnyxEntry): boolean { return Object.keys(policy?.errors ?? {}).length > 0 ? true : hasPolicyErrorFields(policy); } /** * Checks if we have any errors stored within the policy custom units. */ -function hasCustomUnitsError(policy: OnyxTypes.Policy): boolean { +function hasCustomUnitsError(policy: OnyxEntry): boolean { return Object.keys(policy?.customUnits?.errors ?? {}).length > 0; } @@ -71,8 +74,8 @@ function getUnitRateValue(customUnitRate: UnitRate, toLocaleDigit: (arg: string) /** * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. */ -function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembersCollection: PolicyMembersCollection): string { - const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy.id}`] ?? {}; +function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMembersCollection: OnyxCollection): string { + const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`] ?? {}; if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } @@ -86,8 +89,9 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembe * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in * updating the screen. Passing the offline status from the component. */ -function shouldShowPolicy(policy: OnyxTypes.Policy, isOffline: boolean): boolean { +function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean { return ( + policy !== null && policy && policy?.isPolicyExpenseChatEnabled && policy?.role === CONST.POLICY.ROLE.ADMIN && @@ -108,21 +112,21 @@ function isExpensifyGuideTeam(email: string): boolean { /** * Checks if the current user is an admin of the policy. */ -const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; +const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; /** * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. * * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { +function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs { const memberEmailsToAccountIDs: Record = {}; Object.keys(policyMembers ?? {}).forEach((accountID) => { const member = policyMembers?.[accountID]; if (Object.keys(member?.errors ?? {})?.length > 0) { return; } - const personalDetail = personalDetails[accountID]; + const personalDetail = personalDetails?.[accountID]; if (!personalDetail?.login) { return; } @@ -134,12 +138,12 @@ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, person /** * Get login list that we should not show in the workspace invite options */ -function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { +function getIneligibleInvitees(policyMembers: OnyxEntry, personalDetails: OnyxEntry): string[] { const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; Object.keys(policyMembers ?? {}).forEach((accountID) => { const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). - if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { + if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } const memberEmail = personalDetails?.[accountID]?.login; @@ -155,7 +159,7 @@ function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: /** * Gets the tag from policy tags, defaults to the first if no key is provided. */ -function getTag(policyTags: Record, tagKey?: keyof typeof policyTags) { +function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags) { if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } @@ -168,7 +172,7 @@ function getTag(policyTags: Record, tagKey?: keyof /** * Gets the first tag name from policy tags. */ -function getTagListName(policyTags: Record) { +function getTagListName(policyTags: OnyxEntry) { if (Object.keys(policyTags ?? {})?.length === 0) { return ''; } @@ -181,7 +185,7 @@ function getTagListName(policyTags: Record) { /** * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. */ -function getTagList(policyTags: Record>, tagKey: string) { +function getTagList(policyTags: OnyxCollection, tagKey: string) { if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } @@ -191,7 +195,7 @@ function getTagList(policyTags: Record): boolean { return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } diff --git a/src/types/onyx/PolicyMember.ts b/src/types/onyx/PolicyMember.ts index 055465020c36..60836b276ea0 100644 --- a/src/types/onyx/PolicyMember.ts +++ b/src/types/onyx/PolicyMember.ts @@ -14,4 +14,7 @@ type PolicyMember = { pendingAction?: OnyxCommon.PendingAction; }; +type PolicyMembers = Record; + export default PolicyMember; +export type {PolicyMembers}; diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index fe6bee3a1f31..7807dcc00433 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -10,4 +10,7 @@ type PolicyTag = { 'GL Code': string; }; +type PolicyTags = Record; + export default PolicyTag; +export type {PolicyTags}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e50925e7adf2..9067229a17d9 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -32,7 +32,7 @@ import WalletTransfer from './WalletTransfer'; import MapboxAccessToken from './MapboxAccessToken'; import {OnyxUpdatesFromServer, OnyxUpdateEvent} from './OnyxUpdatesFromServer'; import Download from './Download'; -import PolicyMember from './PolicyMember'; +import PolicyMember, {PolicyMembers} from './PolicyMember'; import Policy from './Policy'; import PolicyCategory from './PolicyCategory'; import Report from './Report'; @@ -45,7 +45,7 @@ import Form, {AddDebitCardForm} from './Form'; import RecentWaypoint from './RecentWaypoint'; import RecentlyUsedCategories from './RecentlyUsedCategories'; import RecentlyUsedTags from './RecentlyUsedTags'; -import PolicyTag from './PolicyTag'; +import PolicyTag, {PolicyTags} from './PolicyTag'; export type { Account, @@ -98,4 +98,6 @@ export type { RecentlyUsedCategories, RecentlyUsedTags, PolicyTag, + PolicyTags, + PolicyMembers, }; From d48bdc8cf21890671488f4cdb467884f81a411a2 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 08:06:57 +0200 Subject: [PATCH 060/548] lint after merge --- src/components/ReportActionsSkeletonView/index.js | 4 ++-- .../home/report/ListBoundaryLoader/ListBoundaryLoader.js | 7 ++++++- src/pages/home/report/ReportActionsView.js | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionsSkeletonView/index.js b/src/components/ReportActionsSkeletonView/index.js index d1254ef567ea..c98470f32613 100644 --- a/src/components/ReportActionsSkeletonView/index.js +++ b/src/components/ReportActionsSkeletonView/index.js @@ -9,12 +9,12 @@ const propTypes = { shouldAnimate: PropTypes.bool, /** Number of possible visible content items */ - possibleVisibleContentItems: PropTypes.number + possibleVisibleContentItems: PropTypes.number, }; const defaultProps = { shouldAnimate: true, - possibleVisibleContentItems: 0 + possibleVisibleContentItems: 0, }; function ReportActionsSkeletonView({shouldAnimate, possibleVisibleContentItems}) { diff --git a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js index ee93f299bf7c..e22a09bfb541 100644 --- a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js +++ b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js @@ -42,7 +42,12 @@ function ListBoundaryLoader({type, isLoadingOlderReportActions, isLoadingInitial // skeleton view above the created action in a newly generated optimistic chat or one with not // that many comments. if (isLoadingInitialReportActions && lastReportActionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { - return ; + return ( + + ); } return null; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3970e5c47811..519bac9e26b8 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -258,7 +258,7 @@ function arePropsEqual(oldProps, newProps) { if (oldProps.isLoadingInitialReportActions !== newProps.isLoadingInitialReportActions) { return false; } - + if (oldProps.isLoadingOlderReportActions !== newProps.isLoadingOlderReportActions) { return false; } From 34ea6f849f3accc5cda93687ea1e92763610b671 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 15:48:30 +0200 Subject: [PATCH 061/548] spacing --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 4535e90a7685..8843b4c693ac 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -123,7 +123,7 @@ function BaseInvertedFlatList(props) { [shouldMeasureItems, measureItemLayout, renderItem], ); - return ( + return ( Date: Thu, 5 Oct 2023 02:57:33 +0000 Subject: [PATCH 062/548] Create Domain-Settings-Overview.md This is my PR for my help article to be added to the new site. --- .../Domain-Settings-Overview.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md diff --git a/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md b/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md new file mode 100644 index 000000000000..de06410baf1c --- /dev/null +++ b/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md @@ -0,0 +1,148 @@ +--- +title: The title of the post, page, or document +description: Want to gain greater control over your company settings in Expensify? Read on to find out more about our Domains feature and how it can help you save time and effort when managing your company expenses. +--- + + +# Overview + + +# How to claim a domain + + +# How to verify a domain + + +# Domain settings + + +# FAQ + From bd08f5ef02d9ad80063e89802723982bef228226 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 5 Oct 2023 09:07:35 +0200 Subject: [PATCH 063/548] comments update --- src/libs/actions/Report.js | 2 +- .../home/report/ListBoundaryLoader/ListBoundaryLoader.js | 1 + src/pages/home/report/ReportActionsView.js | 4 ++-- src/styles/styles.js | 5 +---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 0471387d932a..b15207f4c5af 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -847,7 +847,7 @@ function getOlderActions(reportID, reportActionID) { /** * Gets the newer actions that have not been read yet. - * Normally happens when you located not in the edge of the list and scroll down on a chat. + * Normally happens when you are not located at the bottom of the list and scroll down on a chat. * * @param {String} reportID * @param {String} reportActionID diff --git a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js index e22a09bfb541..44a8584cec6d 100644 --- a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js +++ b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js @@ -53,6 +53,7 @@ function ListBoundaryLoader({type, isLoadingOlderReportActions, isLoadingInitial return null; } if (type === CONST.LIST_COMPONENTS.HEADER && isLoadingNewerReportActions) { + // applied for a header of the list, i.e. when you scroll to the bottom of the list // the styles for android and the rest components are different that's why we use two different components return ; } diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 519bac9e26b8..e0cb6bc668eb 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -175,10 +175,10 @@ function ReportActionsView(props) { // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. - + // // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. - + // // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. diff --git a/src/styles/styles.js b/src/styles/styles.js index 23f51de23cb6..e814122d3c07 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1551,10 +1551,7 @@ const styles = (theme) => ({ paddingBottom: 16, }, chatContentScrollViewWithHeaderLoader: { - // regular paddingBottom wouldn't work here - padding: CONST.CHAT_HEADER_LOADER_HEIGHT, - paddingLeft: 0, - paddingRight: 0, + paddingTop: CONST.CHAT_HEADER_LOADER_HEIGHT, }, // Chat Item From 9f88be898f03c5e7875ea926152eae2aa3c8946f Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:46:40 -0400 Subject: [PATCH 064/548] Update and rename Get-The-Card.md to Request-the-Card.md Renaming file + adding employee-specific "request the expensify card" resource --- .../expensify-card/Get-The-Card.md | 5 -- .../expensify-card/Request-the-Card.md | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) delete mode 100644 docs/articles/expensify-classic/expensify-card/Get-The-Card.md create mode 100644 docs/articles/expensify-classic/expensify-card/Request-the-Card.md diff --git a/docs/articles/expensify-classic/expensify-card/Get-The-Card.md b/docs/articles/expensify-classic/expensify-card/Get-The-Card.md deleted file mode 100644 index e5233a3732a3..000000000000 --- a/docs/articles/expensify-classic/expensify-card/Get-The-Card.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Get the Card -description: Get the Card ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md new file mode 100644 index 000000000000..fa8c81e289dc --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md @@ -0,0 +1,49 @@ +--- +title: Request the Card +description: Details on requesting the Expensify Card as an employee +--- +# Overview + +Once your organization is approved for the Expensify Card, you can request a card! + +This article covers how to request, activate, and replace your physical and virtual Expensify Cards. + +# How to get your first Expensify Card + +An admin in your organization must first enable the Expensify Cards before you can receive a card. After that, an admin may assign you a card by setting a limit. You can think of setting a card limit as “unlocking” access to the card. + +If you haven’t been assigned a limit yet, look for the task on your account's homepage that says, “Ask your admin for the card!”. This task allows you to message your admin team to make that request. + +Once you’re assigned a card limit, we’ll notify you via email to let you know you can request a card. A link within the notification email will take you to your account’s homepage, where you can provide your shipping address for the physical card. Enter your address, and we’ll ship the card to arrive within 3-5 business days. + +Once your physical card arrives in the mail, activate it in Expensify by entering the last four digits of the card in the activation task on your account’s homepage. + +# Virtual Card + +Once assigned a limit, a virtual card is available immediately. You can view the virtual card details via **Settings > Account > Credit Card Import > Show Details**. Feel free to begin transacting with the virtual card while your physical card is in transit – your virtual card and physical card share a limit. + +Please note that you must enable 2-factor authentication on your account if you want to have the option to dispute transactions made on your virtual card. + +# Notifications + +To stay up-to-date on your card’s limit and spending activity, download the Expensify mobile app and enable push notifications. Your card is connected to your Expensify account, so each transaction on your card will trigger a push notification. We’ll also send you a push notification if we detect potentially fraudulent activity and allow you to confirm your purchase. + +# How to request a replacement Expensify Card + +You can request a new card anytime if your Expensify Card is lost, stolen, or damaged. From your Expensify account on the web, head to **Settings > Account > Credit Card Import** and click **Request a New Card**. Confirm the shipping information, complete the prompts, and your new card will arrive in 2 - 3 business days. + +Selecting the “lost” or “stolen” options will deactivate your current card to prevent potentially fraudulent activity. However, choosing the “damaged” option will leave your current card active so you can use it while the new one is shipped to you. + +If you need to cancel your Expensify Card and cannot access the website or mobile app, call our interactive voice recognition phone service (available 24/7). Call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally). + +It's not possible to order a replacement card over the phone, so, if applicable, you would need to handle this step from your Expensify account. + +# FAQ + +## What if I haven’t received my card after multiple weeks? + +Reach out to support, and we can locate a tracking number for the card. If the card shows as delivered, but you still haven’t received it, you’ll need to confirm your address and order a new one. + +## I’m self-employed. Can I set up the Expensify Card as an individual? + +Yep! As long as you have a business bank account and have registered your company with the IRS, you are eligible to use the Expensify Card as an individual business owner. From f73d60100b4fc965ba780113338b9eed28517a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Fri, 6 Oct 2023 10:55:32 +0200 Subject: [PATCH 065/548] first commit --- src/pages/settings/InitialSettingsPage.js | 27 ++++++++-------- .../settings/Wallet/ExpensifyCardPage.js | 31 +++++++++++++------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index d81c9d057174..61b2b5f838eb 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -1,11 +1,11 @@ import lodashGet from 'lodash/get'; -import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react'; -import {View} from 'react-native'; +import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import { View } from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; -import {withOnyx} from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; -import {withNetwork} from '../../components/OnyxProvider'; +import { withNetwork } from '../../components/OnyxProvider'; import styles from '../../styles/styles'; import Text from '../../components/Text'; import * as Session from '../../libs/actions/Session'; @@ -18,11 +18,11 @@ import MenuItem from '../../components/MenuItem'; import themeColors from '../../styles/themes/default'; import SCREENS from '../../SCREENS'; import ROUTES from '../../ROUTES'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import withLocalize, { withLocalizePropTypes } from '../../components/withLocalize'; import compose from '../../libs/compose'; import CONST from '../../CONST'; import Permissions from '../../libs/Permissions'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails, { withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes } from '../../components/withCurrentUserPersonalDetails'; import * as PaymentMethods from '../../libs/actions/PaymentMethods'; import bankAccountPropTypes from '../../components/bankAccountPropTypes'; import cardPropTypes from '../../components/cardPropTypes'; @@ -37,7 +37,7 @@ import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursemen import * as UserUtils from '../../libs/UserUtils'; import policyMemberPropType from '../policyMemberPropType'; import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; -import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; +import { CONTEXT_MENU_TYPES } from '../home/report/ContextMenu/ContextMenuActions'; import * as CurrencyUtils from '../../libs/CurrencyUtils'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import useLocalize from '../../hooks/useLocalize'; @@ -128,12 +128,13 @@ const defaultProps = { allPolicyMembers: {}, ...withCurrentUserPersonalDetailsDefaultProps, }; +window._navigate = () => Navigation.navigate('/settings/wallet/card/Expensify'); function InitialSettingsPage(props) { - const {isExecuting, singleExecution} = useSingleExecution(); + const { isExecuting, singleExecution } = useSingleExecution(); const waitForNavigate = useWaitForNavigation(); const popoverAnchor = useRef(null); - const {translate} = useLocalize(); + const { translate } = useLocalize(); const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); @@ -179,10 +180,10 @@ function InitialSettingsPage(props) { const policyBrickRoadIndicator = !_.isEmpty(props.reimbursementAccount.errors) || - _.chain(props.policies) - .filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) - .some((policy) => PolicyUtils.hasPolicyError(policy) || PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, props.allPolicyMembers)) - .value() + _.chain(props.policies) + .filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) + .some((policy) => PolicyUtils.hasPolicyError(policy) || PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, props.allPolicyMembers)) + .value() ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : null; const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList); diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 026e8147d79f..54088b40c0c9 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; -import React, {useState} from 'react'; -import {ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import React, { useState } from 'react'; +import { ScrollView, View } from 'react-native'; +import { withOnyx } from 'react-native-onyx'; import _ from 'underscore'; import ONYXKEYS from '../../../ONYXKEYS'; import ROUTES from '../../../ROUTES'; @@ -18,6 +18,8 @@ import styles from '../../../styles/styles'; import * as CardUtils from '../../../libs/CardUtils'; import Button from '../../../components/Button'; import CardDetails from './WalletPage/CardDetails'; +// eslint-disable-next-line rulesdir/no-api-in-views +import * as API from '../../../libs/API'; const propTypes = { /* Onyx Props */ @@ -39,15 +41,17 @@ const defaultProps = { function ExpensifyCardPage({ cardList, route: { - params: {domain}, + params: { domain }, }, }) { - const {translate} = useLocalize(); + const { translate } = useLocalize(); const domainCards = CardUtils.getDomainCards(cardList)[domain]; const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; const [shouldShowCardDetails, setShouldShowCardDetails] = useState(false); + const [details, setDetails] = useState({}); + const [loading, setLoading] = useState(false); if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { return ; @@ -57,6 +61,14 @@ function ExpensifyCardPage({ const handleRevealDetails = () => { setShouldShowCardDetails(true); + setLoading(true); + // eslint-disable-next-line + API.makeRequestWithSideEffects('RevealVirtualCardDetails') + .then((val) => { + setDetails(val); + setLoading(false); + }) + .catch(console.log); }; return ( @@ -64,7 +76,7 @@ function ExpensifyCardPage({ includeSafeAreaPaddingBottom={false} testID={ExpensifyCardPage.displayName} > - {({safeAreaPaddingBottomStyle}) => ( + {({ safeAreaPaddingBottomStyle }) => ( <> ) : ( Date: Fri, 6 Oct 2023 17:27:21 +0200 Subject: [PATCH 066/548] move to reducer and error handling --- .../settings/Wallet/ExpensifyCardPage.js | 42 ++++++++------ .../settings/Wallet/revealCardDetailsUtils.ts | 57 +++++++++++++++++++ 2 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 src/pages/settings/Wallet/revealCardDetailsUtils.ts diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 54088b40c0c9..8d42ce149e16 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; -import React, { useState } from 'react'; -import { ScrollView, View } from 'react-native'; -import { withOnyx } from 'react-native-onyx'; +import React, {useReducer} from 'react'; +import {ScrollView, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import ONYXKEYS from '../../../ONYXKEYS'; import ROUTES from '../../../ROUTES'; @@ -20,6 +20,8 @@ import Button from '../../../components/Button'; import CardDetails from './WalletPage/CardDetails'; // eslint-disable-next-line rulesdir/no-api-in-views import * as API from '../../../libs/API'; +import CONST from '../../../CONST'; +import * as revealCardDetailsUtils from './revealCardDetailsUtils'; const propTypes = { /* Onyx Props */ @@ -41,17 +43,15 @@ const defaultProps = { function ExpensifyCardPage({ cardList, route: { - params: { domain }, + params: {domain}, }, }) { - const { translate } = useLocalize(); + const {translate} = useLocalize(); const domainCards = CardUtils.getDomainCards(cardList)[domain]; const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; - const [shouldShowCardDetails, setShouldShowCardDetails] = useState(false); - const [details, setDetails] = useState({}); - const [loading, setLoading] = useState(false); + const [{loading, details, error}, dispatch] = useReducer(revealCardDetailsUtils.reducer, revealCardDetailsUtils.initialState); if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { return ; @@ -60,15 +60,19 @@ function ExpensifyCardPage({ const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); const handleRevealDetails = () => { - setShouldShowCardDetails(true); - setLoading(true); + dispatch({type: 'START'}); // eslint-disable-next-line API.makeRequestWithSideEffects('RevealVirtualCardDetails') - .then((val) => { - setDetails(val); - setLoading(false); + .then((response) => { + if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { + dispatch({type: 'FAIL', payload: response.message}); + return; + } + dispatch({type: 'SUCCESS', payload: response}); }) - .catch(console.log); + .catch((err) => { + dispatch({type: 'FAIL', payload: err.message}); + }); }; return ( @@ -76,7 +80,7 @@ function ExpensifyCardPage({ includeSafeAreaPaddingBottom={false} testID={ExpensifyCardPage.displayName} > - {({ safeAreaPaddingBottomStyle }) => ( + {({safeAreaPaddingBottomStyle}) => ( <> {!_.isEmpty(virtualCard) && ( <> - {shouldShowCardDetails ? ( + {details.pan ? ( ) : ( } /> diff --git a/src/pages/settings/Wallet/revealCardDetailsUtils.ts b/src/pages/settings/Wallet/revealCardDetailsUtils.ts new file mode 100644 index 000000000000..86300e1058b6 --- /dev/null +++ b/src/pages/settings/Wallet/revealCardDetailsUtils.ts @@ -0,0 +1,57 @@ +type State = { + details: { + pan: string; + expiration: string; + cvv: string; + privatePersonalDetails: { + address: { + street: string; + street2: string; + city: string; + state: string; + zip: string; + country: string; + }; + }; + }; + loading: boolean; + error: string; +}; + +type Action = {type: 'START'} | {type: 'SUCCESS'; payload: State['details']} | {type: 'FAIL'; payload: string}; + +const initialState: State = { + details: { + pan: '', + expiration: '', + cvv: '', + privatePersonalDetails: { + address: { + street: '', + street2: '', + city: '', + state: '', + zip: '', + country: '', + }, + }, + }, + loading: false, + error: '', +}; + +const reducer = (state: State, action: Action) => { + switch (action.type) { + case 'START': + return {...state, loading: true}; + case 'SUCCESS': + return {details: action.payload, loading: false, error: ''}; + case 'FAIL': { + return {...state, error: action.payload, loading: false}; + } + default: + return state; + } +}; + +export {initialState, reducer}; From 334061b6f87f81addacb2f4795df22f09405661b Mon Sep 17 00:00:00 2001 From: Corinne Ofstad Date: Fri, 6 Oct 2023 11:16:19 -0500 Subject: [PATCH 067/548] Update Business-Bank-Accounts-USD.md Adding US Business Bank Account Aricle --- .../Business-Bank-Accounts-USD.md | 147 +++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md index 375b00d62eac..29667f14397d 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md @@ -1,5 +1,148 @@ --- title: Business Bank Accounts - USD -description: Business Bank Accounts - USD +description: How to add/remove Business Bank Accounts (US) --- -## Resource Coming Soon! +# Overview +Adding a verified business bank account unlocks a myriad of features and automation in Expensify. +Once you connect your business bank account, you can: +- Pay employee expense reports via direct deposit (US) +- Settle company bills via direct transfer +- Accept invoice payments through direct transfer +- Access the Expensify Card +# How to add a verified business bank account +To connect a business bank account to Expensify, follow the below steps: +1. Go to **Settings > Account > Payments** +2. Click **Add Verified Bank Account** +3. Click **Log into your bank** +4. Click **Continue** +5. When you hit the **Plaid** screen, you'll be shown a list of compatible banks that offer direct online login access +6. Login to the business bank account +- If the bank is not listed, click the X to go back to the connection type +- Here you’ll see the option to **Connect Manually** +- Enter your account and routing numbers +7. Enter your bank login credentials. +- If your bank requires additional security measures, you will be directed to obtain and enter a security code +- If you have more than one account available to choose from, you will be directed to choose the desired account +Next, to verify the bank account, you’ll enter some details about the business as well as some personal information. +## Enter company information +This is where you’ll add the legal business name as well as several other company details. +### Company address +The company address must: +- Be located in the US +- Be a physical location +If you input a maildrop address (PO box, UPS Store, etc.), the address will likely be flagged for review and adding the bank account to Expensify will be delayed. +### Tax Identification Number +This is the identification number that was assigned to the business by the IRS. +### Company website +A company website is required to use most of Expensify’s payment features. When adding the website of the business, format it as, https://www.domain.com. +### Industry Classification Code +You can locate a list of Industry Classification Codes here. +## Enter personal information +Whoever is connecting the bank account to Expensify, must enter their details under the Requestor Information section: +- The address must be a physical address +- The address must be located in the US +- The SSN must be US-issued +This does not need to be a signor on the bank account. If someone other than the Expensify account holder enters their personal information in this section, the details will be flagged for review and adding the bank account to Expensify will be delayed. +## Upload ID +After entering your personal details, you’ll be prompted to click a link or scan a QR code so that you can do the following: +1. Upload the front and back of your ID +2. Use your device to take a selfie and record a short video of yourself +It’s required that your ID is: +- Issued in the US +- Unexpired +## Additional Information +Check the appropriate box under **Additional Information**, accept the agreement terms, and verify that all of the information is true and accurate: +- A Beneficial Owner refers to an **individual** who owns 25% or more of the business. +- If you or another **individual** owns 25% or more of the business, please check the appropriate box +- If someone else owns 25% or more of the business, you will be prompted to provide their personal information +If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section. +# How to validate the bank account +The account you set up can be found under **Settings > Account > Payment > Bank Accounts** section in either **Verifying** or **Pending** status. +If it is **Verifying**, then this means we sent you a message and need more information from you. Please check your Concierge chat which should include a message with specific details about what we require to move forward. +If it is **Pending**, then in 1-2 business days Expensify will administer 3 test transactions to your bank account. Please check your Concierge chat for further instructions. If you do not see these test transactions +After these transactions (2 withdrawals and 1 deposit) have been processed in your account, visit your Expensify Inbox, where you'll see a prompt to input the transaction amounts. +Once you've finished these steps, your business bank account is ready to use in Expensify! +# How to share a verified bank account +Only admins with access to the verified bank account can reimburse employees or pay vendor bills. To grant another admin access to the bank account in Expensify, go to **Settings > Account > Payments > Bank Accounts** and click **"Share"**. Enter their email address, and they will receive instructions from us. Please note, they must be a policy admin on a policy you also have access to in order to share the bank account with them. +When a bank account is shared, it must be revalidated with three new microtransactions to ensure the shared admin has access. This process takes 1-2 business days. Once received, the shared admin can enter the transactions via their Expensify account's Inbox tab. + +Note: A report is shared with all individuals with access to the same business bank account in Expensify for audit purposes. + + +# How to remove access to a verified bank account +This step is important when accountants and staff leave your business. +To remove an admin's access to a shared bank account, go to **Settings > Account > Payments > Shared Business Bank Accounts**. +You'll find a list of individuals who have access to the bank account. Next to each user, you'll see the option to Unshare the bank account. +# How to delete a verified bank account +If you need to delete a bank account from Expensify, run through the following steps: +1. Head to **Settings > Account > Payments** +2. Click the red **Delete** button under the corresponding bank account + +Be cautious, as if it hasn't been shared with someone else, the next user will need to set it up from the beginning. + +If the bank account is set as the settlement account for your Expensify Cards, you’ll need to designate another bank account as your settlement account under **Settings > Domains > Company Cards > Settings** before this account can be deleted. +# Deep Dive + +## Verified bank account requirements + +To add a business bank account to issue reimbursements via ACH (US), to pay invoices (US) or utilize the Expensify Card: +- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We **cannot** accept a PO Box or MailDrop location. +- If you are adding the bank account to Expensify, you must add it from **your** Expensify account settings. +- If you are adding a bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US issued photo ID. For utilizing features related to US ACH, your idea must be issued by the United States. You and any Beneficial Owner (if one exists), must also have a US address +- You must have a valid website for your business to utilize the Expensify Card, or to pay invoices with Expensify. + +## Locked bank account +When you reimburse a report, you authorize Expensify to withdraw the funds from your account. If your bank rejects Expensify’s withdrawal request, your verified bank account is locked until the issue is resolved. + +Withdrawal requests can be rejected due to insufficient funds, or if the bank account has not been enabled for direct debit. +If you need to enable direct debits from your verified bank account, your bank will require the following details: +- The ACH CompanyIDs (1270239450 and 4270239450) +- The ACH Originator Name (Expensify) +To request to unlock the bank account, click **Fix** on your bank account under **Settings > Account > Payments > Bank Accounts**. +This sends a request to our support team to review exactly why the bank account was locked. +Please note, unlocking a bank account can take 4-5 business days to process. +## Error adding ID to Onfido +Expensify is required by both our sponsor bank and federal law to verify the identity of the individual that is initiating the movement of money. We use Onfido to confirm that the person adding a payment method is genuine and not impersonating someone else. + +If you get a generic error message that indicates, "Something's gone wrong", please go through the following steps: + +1. Ensure you are using either Safari (on iPhone) or Chrome (on Android) as your web browser. +2. Check your browser's permissions to make sure that the camera and microphone settings are set to "Allow" +3. Clear your web cache for Safari (on iPhone) or Chrome (on Android). +4. If using a corporate Wi-Fi network, confirm that your corporate firewall isn't blocking the website. +5. Make sure no other apps are overlapping your screen, such as the Facebook Messenger bubble, while recording the video. +6. On iPhone, if using iOS version 15 or later, disable the Hide IP address feature in Safari. +7. If possible, try these steps on another device +8. If you have another phone available, try to follow these steps on that device +If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. + +# FAQ +## What is a Beneficial Owner? + +A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. + + +## What do I do if the Beneficial Owner section only asks for personal details, but our business is owned by another company? + + +Please only indicate you have a Beneficial Owner, if it is an individual that owns 25% or more of the business. + +## Why can’t I input my address or upload my ID? + + +Are you entering a US address? When adding a verified business bank account in Expensify, the individual adding the account, and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account, and then share access with you once verified. + + +## Why am I being asked for documentation when adding my bank account? +When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. +If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. + +If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist. + + +## I don’t see all three microtransactions I need to validate my bank account. What should I do? + +It's a good idea to wait till the end of that second business day. If you still don’t see them, please reach out to your bank and ask them to whitelist our ACH ID's **1270239450** and **4270239450**. Expensify’s ACH Originator Name is "Expensify". + +Make sure to reach out to your Account Manager or to Concierge once you have done so and our team will be able to re-trigger those 3 transactions! + From 1632cbc874ec2d31bc3bbc2b3c13d7259f5eaa4b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 9 Oct 2023 10:16:27 +0200 Subject: [PATCH 068/548] fix: resolve comments --- src/libs/actions/MapboxToken.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/MapboxToken.ts b/src/libs/actions/MapboxToken.ts index 3ddba151e80f..f795adf0df2b 100644 --- a/src/libs/actions/MapboxToken.ts +++ b/src/libs/actions/MapboxToken.ts @@ -1,4 +1,3 @@ -// import _ from 'underscore'; import {isAfter} from 'date-fns'; import Onyx from 'react-native-onyx'; import {AppState, NativeEventSubscription} from 'react-native'; @@ -8,7 +7,7 @@ import CONST from '../../CONST'; import * as ActiveClientManager from '../ActiveClientManager'; import {MapboxAccessToken, Network} from '../../types/onyx'; -let authToken: string | undefined | null; +let authToken: string | null; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (value) => { @@ -20,7 +19,7 @@ let connectionIDForToken: number | null; let connectionIDForNetwork: number | null; let appStateSubscription: NativeEventSubscription | null; let currentToken: MapboxAccessToken | null; -let refreshTimeoutID: NodeJS.Timeout; +let refreshTimeoutID: NodeJS.Timeout | undefined; let isCurrentlyFetchingToken = false; const REFRESH_INTERVAL = 1000 * 60 * 25; From df10ff714fa27a6348a53d2a842fff69d5ad7d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 10:46:19 +0200 Subject: [PATCH 069/548] removed unnecessary helper function --- src/pages/settings/InitialSettingsPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 61b2b5f838eb..fc7997a3d0b6 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -128,7 +128,6 @@ const defaultProps = { allPolicyMembers: {}, ...withCurrentUserPersonalDetailsDefaultProps, }; -window._navigate = () => Navigation.navigate('/settings/wallet/card/Expensify'); function InitialSettingsPage(props) { const { isExecuting, singleExecution } = useSingleExecution(); From 36fb11a5c21d9390ba13b3df51fbd60c316204b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 10:46:51 +0200 Subject: [PATCH 070/548] formatting --- src/pages/settings/InitialSettingsPage.js | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index fc7997a3d0b6..d81c9d057174 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -1,11 +1,11 @@ import lodashGet from 'lodash/get'; -import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; -import { View } from 'react-native'; +import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react'; +import {View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; -import { withOnyx } from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; -import { withNetwork } from '../../components/OnyxProvider'; +import {withNetwork} from '../../components/OnyxProvider'; import styles from '../../styles/styles'; import Text from '../../components/Text'; import * as Session from '../../libs/actions/Session'; @@ -18,11 +18,11 @@ import MenuItem from '../../components/MenuItem'; import themeColors from '../../styles/themes/default'; import SCREENS from '../../SCREENS'; import ROUTES from '../../ROUTES'; -import withLocalize, { withLocalizePropTypes } from '../../components/withLocalize'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; import CONST from '../../CONST'; import Permissions from '../../libs/Permissions'; -import withCurrentUserPersonalDetails, { withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes } from '../../components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; import * as PaymentMethods from '../../libs/actions/PaymentMethods'; import bankAccountPropTypes from '../../components/bankAccountPropTypes'; import cardPropTypes from '../../components/cardPropTypes'; @@ -37,7 +37,7 @@ import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursemen import * as UserUtils from '../../libs/UserUtils'; import policyMemberPropType from '../policyMemberPropType'; import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; -import { CONTEXT_MENU_TYPES } from '../home/report/ContextMenu/ContextMenuActions'; +import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; import * as CurrencyUtils from '../../libs/CurrencyUtils'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import useLocalize from '../../hooks/useLocalize'; @@ -130,10 +130,10 @@ const defaultProps = { }; function InitialSettingsPage(props) { - const { isExecuting, singleExecution } = useSingleExecution(); + const {isExecuting, singleExecution} = useSingleExecution(); const waitForNavigate = useWaitForNavigation(); const popoverAnchor = useRef(null); - const { translate } = useLocalize(); + const {translate} = useLocalize(); const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); @@ -179,10 +179,10 @@ function InitialSettingsPage(props) { const policyBrickRoadIndicator = !_.isEmpty(props.reimbursementAccount.errors) || - _.chain(props.policies) - .filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) - .some((policy) => PolicyUtils.hasPolicyError(policy) || PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, props.allPolicyMembers)) - .value() + _.chain(props.policies) + .filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) + .some((policy) => PolicyUtils.hasPolicyError(policy) || PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, props.allPolicyMembers)) + .value() ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : null; const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList); From bfdb546f3d1c5ef7a5fed8f1b4fafd785f46b4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 10:52:05 +0200 Subject: [PATCH 071/548] var name change to isLoading --- src/pages/settings/Wallet/ExpensifyCardPage.js | 6 +++--- src/pages/settings/Wallet/revealCardDetailsUtils.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 8d42ce149e16..7a7a3a742845 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -51,7 +51,7 @@ function ExpensifyCardPage({ const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; - const [{loading, details, error}, dispatch] = useReducer(revealCardDetailsUtils.reducer, revealCardDetailsUtils.initialState); + const [{isLoading, details, error}, dispatch] = useReducer(revealCardDetailsUtils.reducer, revealCardDetailsUtils.initialState); if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { return ; @@ -119,8 +119,8 @@ function ExpensifyCardPage({ medium text={translate('cardPage.cardDetails.revealDetails')} onPress={handleRevealDetails} - isDisabled={loading} - isLoading={loading} + isDisabled={isLoading} + isLoading={isLoading} /> } /> diff --git a/src/pages/settings/Wallet/revealCardDetailsUtils.ts b/src/pages/settings/Wallet/revealCardDetailsUtils.ts index 86300e1058b6..5e2eb355acea 100644 --- a/src/pages/settings/Wallet/revealCardDetailsUtils.ts +++ b/src/pages/settings/Wallet/revealCardDetailsUtils.ts @@ -14,7 +14,7 @@ type State = { }; }; }; - loading: boolean; + isLoading: boolean; error: string; }; @@ -36,18 +36,18 @@ const initialState: State = { }, }, }, - loading: false, + isLoading: false, error: '', }; -const reducer = (state: State, action: Action) => { +const reducer = (state: State, action: Action): State => { switch (action.type) { case 'START': - return {...state, loading: true}; + return {...state, isLoading: true}; case 'SUCCESS': - return {details: action.payload, loading: false, error: ''}; + return {details: action.payload, isLoading: false, error: ''}; case 'FAIL': { - return {...state, error: action.payload, loading: false}; + return {...state, error: action.payload, isLoading: false}; } default: return state; From 9a8962c1210888f8c91515ec24dae0d39fb27a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 11:12:47 +0200 Subject: [PATCH 072/548] better linting --- src/pages/settings/Wallet/ExpensifyCardPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 7a7a3a742845..09b0bd53bb7e 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -61,7 +61,7 @@ function ExpensifyCardPage({ const handleRevealDetails = () => { dispatch({type: 'START'}); - // eslint-disable-next-line + // eslint-disable-next-line rulesdir/no-api-in-views,rulesdir/no-api-side-effects-method API.makeRequestWithSideEffects('RevealVirtualCardDetails') .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { From c7dd8fa76a6757efa2580a6540524a6f35dc8963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 12:43:06 +0200 Subject: [PATCH 073/548] moved action types to const --- src/pages/settings/Wallet/ExpensifyCardPage.js | 8 ++++---- .../settings/Wallet/revealCardDetailsUtils.ts | 16 +++++++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 09b0bd53bb7e..3e2d8632228a 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -60,18 +60,18 @@ function ExpensifyCardPage({ const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); const handleRevealDetails = () => { - dispatch({type: 'START'}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.start}); // eslint-disable-next-line rulesdir/no-api-in-views,rulesdir/no-api-side-effects-method API.makeRequestWithSideEffects('RevealVirtualCardDetails') .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - dispatch({type: 'FAIL', payload: response.message}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.fail, payload: response.message}); return; } - dispatch({type: 'SUCCESS', payload: response}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.success, payload: response}); }) .catch((err) => { - dispatch({type: 'FAIL', payload: err.message}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.fail, payload: err.message}); }); }; diff --git a/src/pages/settings/Wallet/revealCardDetailsUtils.ts b/src/pages/settings/Wallet/revealCardDetailsUtils.ts index 5e2eb355acea..57e70fa41462 100644 --- a/src/pages/settings/Wallet/revealCardDetailsUtils.ts +++ b/src/pages/settings/Wallet/revealCardDetailsUtils.ts @@ -1,3 +1,9 @@ +const ACTION_TYPES = { + START: 'START', + SUCCESS: 'SUCCESS', + FAIL: 'FAIL', +} as const; + type State = { details: { pan: string; @@ -18,7 +24,7 @@ type State = { error: string; }; -type Action = {type: 'START'} | {type: 'SUCCESS'; payload: State['details']} | {type: 'FAIL'; payload: string}; +type Action = {type: typeof ACTION_TYPES.START} | {type: typeof ACTION_TYPES.SUCCESS; payload: State['details']} | {type: typeof ACTION_TYPES.FAIL; payload: string}; const initialState: State = { details: { @@ -42,11 +48,11 @@ const initialState: State = { const reducer = (state: State, action: Action): State => { switch (action.type) { - case 'START': + case ACTION_TYPES.START: return {...state, isLoading: true}; - case 'SUCCESS': + case ACTION_TYPES.SUCCESS: return {details: action.payload, isLoading: false, error: ''}; - case 'FAIL': { + case ACTION_TYPES.FAIL: { return {...state, error: action.payload, isLoading: false}; } default: @@ -54,4 +60,4 @@ const reducer = (state: State, action: Action): State => { } }; -export {initialState, reducer}; +export {initialState, reducer, ACTION_TYPES}; From 76ef509679e64746faa53336fca77f796ec5c10e Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 9 Oct 2023 17:11:13 +0530 Subject: [PATCH 074/548] added fix for all security, workspace and aboutpage --- src/components/MenuItemList.js | 7 +- src/pages/settings/AboutPage/AboutPage.js | 55 +++++++++----- src/pages/settings/InitialSettingsPage.js | 6 +- .../settings/Security/SecuritySettingsPage.js | 31 ++++---- src/pages/workspace/WorkspaceInitialPage.js | 74 ++++++++++--------- 5 files changed, 98 insertions(+), 75 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index 03dbe81c82c4..48190a4f4d05 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import MenuItem from './MenuItem'; import menuItemPropTypes from './menuItemPropTypes'; import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; -import { CONTEXT_MENU_TYPES } from '../pages/home/report/ContextMenu/ContextMenuActions'; +import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; import useSingleExecution from '../hooks/useSingleExecution'; import useWaitForNavigation from '../hooks/useWaitForNavigation'; @@ -21,8 +21,7 @@ const defaultProps = { function MenuItemList(props) { let popoverAnchor; - const { isExecuting, singleExecution } = useSingleExecution(); - const waitForNavigate = useWaitForNavigation(); + const {isExecuting, singleExecution} = useSingleExecution(); /** * Handle the secondary interaction for a menu item. @@ -49,7 +48,7 @@ function MenuItemList(props) { // eslint-disable-next-line react/jsx-props-no-spreading {...menuItemProps} disabled={menuItemProps.disabled || isExecuting} - onPress={props.shouldUseSingleExecution ? singleExecution(waitForNavigate(menuItemProps.onPress)) : menuItemProps.onPress} + onPress={props.shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} /> ))} diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index 78a300a38057..14cd7be5ef25 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import React from 'react'; +import PropTypes from 'prop-types'; import {ScrollView, View} from 'react-native'; import DeviceInfo from 'react-native-device-info'; import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; @@ -23,10 +24,15 @@ import * as ReportActionContextMenu from '../../home/report/ContextMenu/ReportAc import {CONTEXT_MENU_TYPES} from '../../home/report/ContextMenu/ContextMenuActions'; import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; import * as Environment from '../../../libs/Environment/Environment'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; +import MenuItemList from '../../../components/MenuItemList'; +import {withOnyx} from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; const propTypes = { ...withLocalizePropTypes, ...windowDimensionsPropTypes, + isShortcutsModalOpen: PropTypes.bool, }; function getFlavor() { @@ -42,13 +48,12 @@ function getFlavor() { function AboutPage(props) { let popoverAnchor; + const waitForNavigate = useWaitForNavigation(); const menuItems = [ { translationKey: 'initialSettingsPage.aboutPage.appDownloadLinks', icon: Expensicons.Link, - action: () => { - Navigation.navigate(ROUTES.SETTINGS_APP_DOWNLOAD_LINKS); - }, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_APP_DOWNLOAD_LINKS)), }, { translationKey: 'initialSettingsPage.aboutPage.viewKeyboardShortcuts', @@ -79,7 +84,7 @@ function AboutPage(props) { action: Report.navigateToConciergeChat, }, ]; - + console.log('props.isShortcutsModalOpen: ', props.isShortcutsModalOpen); return ( {props.translate('initialSettingsPage.aboutPage.description')} - {_.map(menuItems, (item) => ( - item.action()} - shouldBlockSelection={Boolean(item.link)} - onSecondaryInteraction={ - !_.isEmpty(item.link) ? (e) => ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, item.link, popoverAnchor) : undefined - } - ref={(el) => (popoverAnchor = el)} - shouldShowRightIcon - /> - ))} + ({ + key: item.translationKey, + title: props.translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + disabled: props.isShortcutsModalOpen, + onPress: item.action, + shouldShowRightIcon: true, + onSecondaryInteraction: !_.isEmpty(item.link) + ? (e) => ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, item.link, popoverAnchor) + : undefined, + ref: (el) => (popoverAnchor = el), + shouldBlockSelection: Boolean(item.link), + }))} + shouldUseSingleExecution + /> @@ -357,7 +358,8 @@ function InitialSettingsPage(props) { diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js index 704ea17422bd..d6f1d2b8fb86 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.js +++ b/src/pages/settings/Security/SecuritySettingsPage.js @@ -15,6 +15,8 @@ import compose from '../../../libs/compose'; import ONYXKEYS from '../../../ONYXKEYS'; import IllustratedHeaderPageLayout from '../../../components/IllustratedHeaderPageLayout'; import * as LottieAnimations from '../../../components/LottieAnimations'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; +import MenuItemList from '../../../components/MenuItemList'; const propTypes = { ...withLocalizePropTypes, @@ -33,18 +35,18 @@ const defaultProps = { }; function SecuritySettingsPage(props) { + const waitForNavigate = useWaitForNavigation(); + const menuItems = [ { translationKey: 'twoFactorAuth.headerTitle', icon: Expensicons.Shield, - action: () => Navigation.navigate(ROUTES.SETTINGS_2FA), + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA)), }, { translationKey: 'closeAccountPage.closeAccount', icon: Expensicons.ClosedSign, - action: () => { - Navigation.navigate(ROUTES.SETTINGS_CLOSE); - }, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), }, ]; @@ -58,16 +60,17 @@ function SecuritySettingsPage(props) { > - {_.map(menuItems, (item) => ( - item.action()} - shouldShowRightIcon - /> - ))} + ({ + key: item.translationKey, + title: props.translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + onPress: item.action, + shouldShowRightIcon: true, + }))} + shouldUseSingleExecution + /> diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 962b8bfd056e..8e79b46c615a 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -1,8 +1,8 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; -import React, { useCallback, useEffect, useState } from 'react'; -import { View, ScrollView } from 'react-native'; -import { withOnyx } from 'react-native-onyx'; +import React, {useCallback, useEffect, useState} from 'react'; +import {View, ScrollView} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; @@ -17,7 +17,7 @@ import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import compose from '../../libs/compose'; import Avatar from '../../components/Avatar'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; -import { policyPropTypes, policyDefaultProps } from './withPolicy'; +import {policyPropTypes, policyDefaultProps} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import reportPropTypes from '../reportPropTypes'; import * as Policy from '../../libs/actions/Policy'; @@ -31,6 +31,10 @@ import * as ReportUtils from '../../libs/ReportUtils'; import withWindowDimensions from '../../components/withWindowDimensions'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import MenuItemList from '../../components/MenuItemList'; +import useWaitForNavigation from '../../hooks/useWaitForNavigation'; +import useSingleExecution from '../../hooks/useSingleExecution'; +import {is} from 'core-js/core/object'; +import MenuItem from '../../components/MenuItem'; const propTypes = { ...policyPropTypes, @@ -69,6 +73,8 @@ function WorkspaceInitialPage(props) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); const hasPolicyCreationError = Boolean(policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors); + const waitForNavigate = useWaitForNavigation(); + const {singleExecution, isExecuting} = useSingleExecution(); /** * Call the delete policy and hide the modal @@ -117,48 +123,49 @@ function WorkspaceInitialPage(props) { { translationKey: 'workspace.common.settings', icon: Expensicons.Gear, - action: () => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id))), brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, { translationKey: 'workspace.common.card', icon: Expensicons.ExpensifyCard, - action: () => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy.id))), }, { translationKey: 'workspace.common.reimburse', icon: Expensicons.Receipt, - action: () => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy.id))), error: hasCustomUnitsError, }, { translationKey: 'workspace.common.bills', icon: Expensicons.Bill, - action: () => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy.id))), }, { translationKey: 'workspace.common.invoices', icon: Expensicons.Invoice, - action: () => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy.id))), }, { translationKey: 'workspace.common.travel', icon: Expensicons.Luggage, - action: () => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy.id))), }, { translationKey: 'workspace.common.members', icon: Expensicons.Users, - action: () => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy.id))), brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, { translationKey: 'workspace.common.bankAccount', icon: Expensicons.Bank, - action: () => + action: waitForNavigate(() => policy.outputCurrency === CONST.CURRENCY.USD ? ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRoute().replace(/\?.*/, '')) : setIsCurrencyModalOpen(true), + ), brickRoadIndicator: !_.isEmpty(props.reimbursementAccount.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, ]; @@ -171,12 +178,12 @@ function WorkspaceInitialPage(props) { }, { icon: Expensicons.Hashtag, - text: props.translate('workspace.common.goToRoom', { roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS }), + text: props.translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS}), onSelected: () => goToRoom(CONST.REPORT.CHAT_TYPE.POLICY_ADMINS), }, { icon: Expensicons.Hashtag, - text: props.translate('workspace.common.goToRoom', { roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE }), + text: props.translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE}), onSelected: () => goToRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE), }, ]; @@ -186,7 +193,7 @@ function WorkspaceInitialPage(props) { includeSafeAreaPaddingBottom={false} testID={WorkspaceInitialPage.displayName} > - {({ safeAreaPaddingBottomStyle }) => ( + {({safeAreaPaddingBottomStyle}) => ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} shouldShow={_.isEmpty(props.policy) || !PolicyUtils.isPolicyAdmin(props.policy) || PolicyUtils.isPendingDeletePolicy(props.policy)} @@ -213,9 +220,9 @@ function WorkspaceInitialPage(props) { openEditor(policy.id)} + onPress={singleExecution(waitForNavigate(() => openEditor(policy.id)))} accessibilityLabel={props.translate('workspace.common.settings')} accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -233,9 +240,9 @@ function WorkspaceInitialPage(props) { {!_.isEmpty(policy.name) && ( openEditor(policy.id)} + onPress={singleExecution(waitForNavigate(() => openEditor(policy.id)))} accessibilityLabel={props.translate('workspace.common.settings')} accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -250,22 +257,19 @@ function WorkspaceInitialPage(props) { )} - ({ - key: item.translationKey, - disabled: hasPolicyCreationError, - interactive: !hasPolicyCreationError, - title: props.translate(item.translationKey), - icon: item.icon, - iconRight: item.iconRight, - onPress: item.action, - shouldShowRightIcon: true, - brickRoadIndicator: item.brickRoadIndicator, - })) - } - shouldUseSingleExecution - /> + {_.map(menuItems, (item) => ( + + ))} From 7d8fda9bc9759d542899b4e6c95374f1c8475125 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 9 Oct 2023 17:17:57 +0530 Subject: [PATCH 075/548] remove console log --- src/pages/settings/AboutPage/AboutPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index 14cd7be5ef25..88900dcbf64a 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -84,7 +84,7 @@ function AboutPage(props) { action: Report.navigateToConciergeChat, }, ]; - console.log('props.isShortcutsModalOpen: ', props.isShortcutsModalOpen); + return ( Date: Mon, 9 Oct 2023 17:27:07 +0530 Subject: [PATCH 076/548] fix linters --- src/components/MenuItemList.js | 9 ++- src/pages/settings/AboutPage/AboutPage.js | 42 ++++++------ src/pages/settings/InitialSettingsPage.js | 64 +++++++++---------- .../settings/Security/SecuritySettingsPage.js | 21 +++--- src/pages/workspace/WorkspaceInitialPage.js | 50 +++++++-------- 5 files changed, 93 insertions(+), 93 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index 48190a4f4d05..f59c31dc6fda 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -1,12 +1,11 @@ +import PropTypes from 'prop-types'; import React from 'react'; import _ from 'underscore'; -import PropTypes from 'prop-types'; +import useSingleExecution from '../hooks/useSingleExecution'; +import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; +import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; import MenuItem from './MenuItem'; import menuItemPropTypes from './menuItemPropTypes'; -import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; -import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; -import useSingleExecution from '../hooks/useSingleExecution'; -import useWaitForNavigation from '../hooks/useWaitForNavigation'; const propTypes = { /** An array of props that are pass to individual MenuItem components */ diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index 88900dcbf64a..ec3325bf1ed7 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -1,33 +1,32 @@ -import _ from 'underscore'; -import React from 'react'; import PropTypes from 'prop-types'; +import React from 'react'; import {ScrollView, View} from 'react-native'; import DeviceInfo from 'react-native-device-info'; -import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; -import Navigation from '../../../libs/Navigation/Navigation'; -import ROUTES from '../../../ROUTES'; -import styles from '../../../styles/styles'; -import Text from '../../../components/Text'; -import TextLink from '../../../components/TextLink'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import Logo from '../../../../assets/images/new-expensify.svg'; +import pkg from '../../../../package.json'; import CONST from '../../../CONST'; +import ONYXKEYS from '../../../ONYXKEYS'; +import ROUTES from '../../../ROUTES'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import * as Expensicons from '../../../components/Icon/Expensicons'; +import MenuItemList from '../../../components/MenuItemList'; import ScreenWrapper from '../../../components/ScreenWrapper'; +import Text from '../../../components/Text'; +import TextLink from '../../../components/TextLink'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; -import MenuItem from '../../../components/MenuItem'; -import Logo from '../../../../assets/images/new-expensify.svg'; -import pkg from '../../../../package.json'; -import * as Report from '../../../libs/actions/Report'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; +import * as Environment from '../../../libs/Environment/Environment'; +import Navigation from '../../../libs/Navigation/Navigation'; +import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; import * as Link from '../../../libs/actions/Link'; +import * as Report from '../../../libs/actions/Report'; import compose from '../../../libs/compose'; -import * as ReportActionContextMenu from '../../home/report/ContextMenu/ReportActionContextMenu'; +import styles from '../../../styles/styles'; import {CONTEXT_MENU_TYPES} from '../../home/report/ContextMenu/ContextMenuActions'; -import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; -import * as Environment from '../../../libs/Environment/Environment'; -import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; -import MenuItemList from '../../../components/MenuItemList'; -import {withOnyx} from 'react-native-onyx'; -import ONYXKEYS from '../../../ONYXKEYS'; +import * as ReportActionContextMenu from '../../home/report/ContextMenu/ReportActionContextMenu'; const propTypes = { ...withLocalizePropTypes, @@ -35,6 +34,10 @@ const propTypes = { isShortcutsModalOpen: PropTypes.bool, }; +const defaultProps = { + isShortcutsModalOpen: false, +}; + function getFlavor() { const bundleId = DeviceInfo.getBundleId(); if (bundleId.includes('dev')) { @@ -161,6 +164,7 @@ function AboutPage(props) { } AboutPage.propTypes = propTypes; +AboutPage.defaultProps = defaultProps; AboutPage.displayName = 'AboutPage'; export default compose( diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 5ed498fcb3d0..619aba06e026 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -1,49 +1,49 @@ import lodashGet from 'lodash/get'; -import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react'; -import {View} from 'react-native'; import PropTypes from 'prop-types'; -import _ from 'underscore'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; -import {withNetwork} from '../../components/OnyxProvider'; -import styles from '../../styles/styles'; -import Text from '../../components/Text'; -import * as Session from '../../libs/actions/Session'; +import _ from 'underscore'; +import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; -import Tooltip from '../../components/Tooltip'; +import ROUTES from '../../ROUTES'; +import SCREENS from '../../SCREENS'; import Avatar from '../../components/Avatar'; -import Navigation from '../../libs/Navigation/Navigation'; +import ConfirmModal from '../../components/ConfirmModal'; +import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; +import HeaderPageLayout from '../../components/HeaderPageLayout'; import * as Expensicons from '../../components/Icon/Expensicons'; import MenuItem from '../../components/MenuItem'; -import themeColors from '../../styles/themes/default'; -import SCREENS from '../../SCREENS'; -import ROUTES from '../../ROUTES'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import compose from '../../libs/compose'; -import CONST from '../../CONST'; -import Permissions from '../../libs/Permissions'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; -import * as PaymentMethods from '../../libs/actions/PaymentMethods'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; +import {withNetwork} from '../../components/OnyxProvider'; +import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; +import Text from '../../components/Text'; +import Tooltip from '../../components/Tooltip'; import bankAccountPropTypes from '../../components/bankAccountPropTypes'; import cardPropTypes from '../../components/cardPropTypes'; -import * as Wallet from '../../libs/actions/Wallet'; -import walletTermsPropTypes from '../EnablePayments/walletTermsPropTypes'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import useLocalize from '../../hooks/useLocalize'; +import useSingleExecution from '../../hooks/useSingleExecution'; +import useWaitForNavigation from '../../hooks/useWaitForNavigation'; +import * as CurrencyUtils from '../../libs/CurrencyUtils'; +import Navigation from '../../libs/Navigation/Navigation'; +import Permissions from '../../libs/Permissions'; import * as PolicyUtils from '../../libs/PolicyUtils'; -import ConfirmModal from '../../components/ConfirmModal'; import * as ReportUtils from '../../libs/ReportUtils'; +import * as UserUtils from '../../libs/UserUtils'; import * as Link from '../../libs/actions/Link'; -import OfflineWithFeedback from '../../components/OfflineWithFeedback'; +import * as PaymentMethods from '../../libs/actions/PaymentMethods'; +import * as Session from '../../libs/actions/Session'; +import * as Wallet from '../../libs/actions/Wallet'; +import compose from '../../libs/compose'; +import styles from '../../styles/styles'; +import themeColors from '../../styles/themes/default'; +import walletTermsPropTypes from '../EnablePayments/walletTermsPropTypes'; import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursementAccountPropTypes'; -import * as UserUtils from '../../libs/UserUtils'; -import policyMemberPropType from '../policyMemberPropType'; -import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; -import * as CurrencyUtils from '../../libs/CurrencyUtils'; -import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; -import useLocalize from '../../hooks/useLocalize'; -import useSingleExecution from '../../hooks/useSingleExecution'; -import useWaitForNavigation from '../../hooks/useWaitForNavigation'; -import HeaderPageLayout from '../../components/HeaderPageLayout'; +import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; +import policyMemberPropType from '../policyMemberPropType'; const propTypes = { /* Onyx Props */ diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js index d6f1d2b8fb86..d759e1a7fd53 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.js +++ b/src/pages/settings/Security/SecuritySettingsPage.js @@ -1,22 +1,21 @@ -import _ from 'underscore'; +import PropTypes from 'prop-types'; import React from 'react'; -import {View, ScrollView} from 'react-native'; +import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import PropTypes from 'prop-types'; -import Navigation from '../../../libs/Navigation/Navigation'; +import _ from 'underscore'; +import ONYXKEYS from '../../../ONYXKEYS'; import ROUTES from '../../../ROUTES'; import SCREENS from '../../../SCREENS'; -import styles from '../../../styles/styles'; import * as Expensicons from '../../../components/Icon/Expensicons'; -import themeColors from '../../../styles/themes/default'; -import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; -import MenuItem from '../../../components/MenuItem'; -import compose from '../../../libs/compose'; -import ONYXKEYS from '../../../ONYXKEYS'; import IllustratedHeaderPageLayout from '../../../components/IllustratedHeaderPageLayout'; import * as LottieAnimations from '../../../components/LottieAnimations'; -import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; import MenuItemList from '../../../components/MenuItemList'; +import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; +import Navigation from '../../../libs/Navigation/Navigation'; +import compose from '../../../libs/compose'; +import styles from '../../../styles/styles'; +import themeColors from '../../../styles/themes/default'; const propTypes = { ...withLocalizePropTypes, diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 8e79b46c615a..c2df7da8b729 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -1,40 +1,38 @@ -import _ from 'underscore'; import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState} from 'react'; -import {View, ScrollView} from 'react-native'; +import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import PropTypes from 'prop-types'; -import Navigation from '../../libs/Navigation/Navigation'; +import _ from 'underscore'; +import CONST from '../../CONST'; +import ONYXKEYS from '../../ONYXKEYS'; import ROUTES from '../../ROUTES'; -import styles from '../../styles/styles'; -import Tooltip from '../../components/Tooltip'; -import Text from '../../components/Text'; +import Avatar from '../../components/Avatar'; +import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '../../components/ConfirmModal'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import * as Expensicons from '../../components/Icon/Expensicons'; +import MenuItem from '../../components/MenuItem'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; +import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import ScreenWrapper from '../../components/ScreenWrapper'; +import Text from '../../components/Text'; +import Tooltip from '../../components/Tooltip'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import HeaderWithBackButton from '../../components/HeaderWithBackButton'; -import compose from '../../libs/compose'; -import Avatar from '../../components/Avatar'; -import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; -import {policyPropTypes, policyDefaultProps} from './withPolicy'; -import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -import reportPropTypes from '../reportPropTypes'; -import * as Policy from '../../libs/actions/Policy'; +import withWindowDimensions from '../../components/withWindowDimensions'; +import useSingleExecution from '../../hooks/useSingleExecution'; +import useWaitForNavigation from '../../hooks/useWaitForNavigation'; +import Navigation from '../../libs/Navigation/Navigation'; import * as PolicyUtils from '../../libs/PolicyUtils'; -import CONST from '../../CONST'; +import * as ReportUtils from '../../libs/ReportUtils'; +import * as Policy from '../../libs/actions/Policy'; import * as ReimbursementAccount from '../../libs/actions/ReimbursementAccount'; -import ONYXKEYS from '../../ONYXKEYS'; -import OfflineWithFeedback from '../../components/OfflineWithFeedback'; +import compose from '../../libs/compose'; +import styles from '../../styles/styles'; import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursementAccountPropTypes'; -import * as ReportUtils from '../../libs/ReportUtils'; -import withWindowDimensions from '../../components/withWindowDimensions'; -import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; -import MenuItemList from '../../components/MenuItemList'; -import useWaitForNavigation from '../../hooks/useWaitForNavigation'; -import useSingleExecution from '../../hooks/useSingleExecution'; -import {is} from 'core-js/core/object'; -import MenuItem from '../../components/MenuItem'; +import reportPropTypes from '../reportPropTypes'; +import {policyDefaultProps, policyPropTypes} from './withPolicy'; +import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; const propTypes = { ...policyPropTypes, From a9f71a42c408943d89ba69576c5c3bb1e2bb7878 Mon Sep 17 00:00:00 2001 From: Pujan Date: Mon, 9 Oct 2023 18:06:29 +0530 Subject: [PATCH 077/548] prevent auto focus for transaction thread empty chat --- .../home/report/ReportActionCompose/ComposerWithSuggestions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index a4777556dda7..e3a67d6f1899 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -110,7 +110,8 @@ function ComposerWithSuggestions({ const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; const isEmptyChat = useMemo(() => _.size(reportActions) === 1, [reportActions]); - const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || isEmptyChat) && shouldShowComposeInput; + const parentAction = ReportActionsUtils.getParentReportAction(report); + const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentAction))) && shouldShowComposeInput; const valueRef = useRef(value); valueRef.current = value; From ccf8f4e213c15aa88f1fc584538e4a63263b11d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 16:12:50 +0200 Subject: [PATCH 078/548] action types fix and cardID added to api call --- src/pages/settings/Wallet/ExpensifyCardPage.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 3e2d8632228a..fe16b9014e19 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -62,16 +62,16 @@ function ExpensifyCardPage({ const handleRevealDetails = () => { dispatch({type: revealCardDetailsUtils.ACTION_TYPES.start}); // eslint-disable-next-line rulesdir/no-api-in-views,rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealVirtualCardDetails') + API.makeRequestWithSideEffects('RevealVirtualCardDetails', {cardID: virtualCard.cardID}) .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.fail, payload: response.message}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.FAIL, payload: response.message}); return; } - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.success, payload: response}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.SUCCESS, payload: response}); }) .catch((err) => { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.fail, payload: err.message}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.FAIL, payload: err.message}); }); }; From cfb4beebd2ba8fbb08ca4a64c80932c165461feb Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 9 Oct 2023 17:05:29 +0200 Subject: [PATCH 079/548] scroll to bottom --- src/pages/home/ReportScreen.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index f69497468670..54a2efe7e4e0 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -259,6 +259,12 @@ function ReportScreen({ const onSubmitComment = useCallback( (text) => { Report.addComment(getReportID(route), text); + // we need to scroll to the bottom of the list after the comment was added + const refID = setTimeout(() => { + flatListRef.current.scrollToOffset({animated: false, offset: 0}); + }, 10); + + return () => clearTimeout(refID); }, [route], ); From 9043f69c1103dc25b6854a326bde8ebf6ff0ec03 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 9 Oct 2023 23:26:56 +0200 Subject: [PATCH 080/548] update after review --- src/ONYXKEYS.ts | 2 +- .../ReportActionsSkeletonView/index.js | 4 +- src/libs/actions/Report.js | 68 ------------------- .../ListBoundaryLoader/ListBoundaryLoader.js | 14 +++- .../ListHeaderComponentLoader.android.js | 16 ----- .../ListHeaderComponentLoader.js | 17 ----- src/pages/home/report/ReportActionsList.js | 8 +++ src/pages/home/report/ReportActionsView.js | 40 +++++------ .../boundaryLoaderStyle/index.android.ts | 13 ++++ src/styles/boundaryLoaderStyle/index.ts | 13 ++++ src/styles/boundaryLoaderStyle/types.ts | 7 ++ src/types/onyx/Report.ts | 9 --- 12 files changed, 74 insertions(+), 137 deletions(-) delete mode 100644 src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js delete mode 100644 src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.js create mode 100644 src/styles/boundaryLoaderStyle/index.android.ts create mode 100644 src/styles/boundaryLoaderStyle/index.ts create mode 100644 src/styles/boundaryLoaderStyle/types.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0a17d3a1d2f7..3aaaff7beda6 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -238,7 +238,7 @@ const ONYXKEYS = { POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', REPORT: 'report_', - // REPORT_METADATA is a perf optimization used to hold loading states (isLoadingReportActions, isLoadingMoreReportActions). + // REPORT_METADATA is a perf optimization used to hold loading states (isLoadingInitialReportActions, isLoadingOlderReportActions, isLoadingNewerReportActions). // A lot of components are connected to the Report entity and do not care about the actions. Setting the loading state // directly on the report caused a lot of unnecessary re-renders REPORT_METADATA: 'reportMetadata_', diff --git a/src/components/ReportActionsSkeletonView/index.js b/src/components/ReportActionsSkeletonView/index.js index c98470f32613..82a419d9995d 100644 --- a/src/components/ReportActionsSkeletonView/index.js +++ b/src/components/ReportActionsSkeletonView/index.js @@ -14,12 +14,12 @@ const propTypes = { const defaultProps = { shouldAnimate: true, - possibleVisibleContentItems: 0, + possibleVisibleContentItems: Math.ceil(Dimensions.get('window').height / CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT), }; function ReportActionsSkeletonView({shouldAnimate, possibleVisibleContentItems}) { // Determines the number of content items based on container height - const visibleContentItems = possibleVisibleContentItems || Math.ceil(Dimensions.get('window').height / CONST.CHAT_SKELETON_VIEW.AVERAGE_ROW_HEIGHT); + const visibleContentItems = possibleVisibleContentItems; const skeletonViewLines = []; for (let index = 0; index < visibleContentItems; index++) { const iconIndex = (index + 1) % 4; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9ec95ebeaa80..b6721f53759b 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -753,52 +753,6 @@ function reconnect(reportID) { ); } -/** - * Gets the older actions that have not been read yet. - * Normally happens when you scroll up on a chat, and the actions have not been read yet. - * - * @param {String} reportID - * @param {String} reportActionID - */ -function readOldestAction(reportID, reportActionID) { - API.read( - 'ReadOldestAction', - { - reportID, - reportActionID, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingOlderReportActions: true, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingOlderReportActions: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingOlderReportActions: true, - }, - }, - ], - }, - ); -} - /** * Gets the older actions that have not been read yet. * Normally happens when you scroll up on a chat, and the actions have not been read yet. @@ -861,13 +815,6 @@ function getNewerActions(reportID, reportActionID) { }, { optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - isLoadingNewerReportActions: true, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, @@ -884,13 +831,6 @@ function getNewerActions(reportID, reportActionID) { isLoadingNewerReportActions: false, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingNewerReportActions: false, - }, - }, ], failureData: [ { @@ -900,13 +840,6 @@ function getNewerActions(reportID, reportActionID) { isLoadingNewerReportActions: false, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isLoadingNewerReportActions: false, - }, - }, ], }, ); @@ -2328,7 +2261,6 @@ export { expandURLPreview, markCommentAsUnread, readNewestAction, - readOldestAction, openReport, openReportFromDeepLink, navigateToAndOpenReport, diff --git a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js index 44a8584cec6d..6e4f23d3a43b 100644 --- a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js +++ b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js @@ -1,9 +1,12 @@ import React from 'react'; +import {ActivityIndicator, View} from 'react-native'; import PropTypes from 'prop-types'; import ReportActionsSkeletonView from '../../../../components/ReportActionsSkeletonView'; import CONST from '../../../../CONST'; import useNetwork from '../../../../hooks/useNetwork'; -import ListHeaderComponentLoader from './ListHeaderComponentLoader/ListHeaderComponentLoader'; +import styles, {stylesGenerator} from '../../../../styles/styles'; +import themeColors from '../../../../styles/themes/default'; +import boundaryLoaderStyles from '../../../../styles/boundaryLoaderStyle/index'; const propTypes = { /** type of rendered loader. Can be 'header' or 'footer' */ @@ -55,7 +58,14 @@ function ListBoundaryLoader({type, isLoadingOlderReportActions, isLoadingInitial if (type === CONST.LIST_COMPONENTS.HEADER && isLoadingNewerReportActions) { // applied for a header of the list, i.e. when you scroll to the bottom of the list // the styles for android and the rest components are different that's why we use two different components - return ; + return ( + + + + ); } return null; } diff --git a/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js b/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js deleted file mode 100644 index 20abe332692d..000000000000 --- a/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.android.js +++ /dev/null @@ -1,16 +0,0 @@ -import {View, ActivityIndicator} from 'react-native'; -import styles, {stylesGenerator} from '../../../../../styles/styles'; -import themeColors from '../../../../../styles/themes/default'; - -function ListHeaderComponentLoader() { - return ( - - - - ); -} - -export default ListHeaderComponentLoader; diff --git a/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.js b/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.js deleted file mode 100644 index 9d4d85a84e87..000000000000 --- a/src/pages/home/report/ListBoundaryLoader/ListHeaderComponentLoader/ListHeaderComponentLoader.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import {View, ActivityIndicator} from 'react-native'; -import styles, {stylesGenerator} from '../../../../../styles/styles'; -import themeColors from '../../../../../styles/themes/default'; - -function ListHeaderComponentLoader() { - return ( - - - - ); -} - -export default ListHeaderComponentLoader; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index cba0e5f3bdb7..72addf2b55a4 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -39,6 +39,9 @@ const propTypes = { /** Are we loading more report actions? */ isLoadingOlderReportActions: PropTypes.bool, + /** Are we loading newer report actions? */ + isLoadingNewerReportActions: PropTypes.bool, + /** Callback executed on list layout */ onLayout: PropTypes.func.isRequired, @@ -48,6 +51,9 @@ const propTypes = { /** Function to load more chats */ loadOlderChats: PropTypes.func.isRequired, + /** Function to load newer chats */ + loadNewerChats: PropTypes.func.isRequired, + /** The policy object for the current route */ policy: PropTypes.shape({ /** The name of the policy */ @@ -344,6 +350,8 @@ function ReportActionsList({ ); const listFooterComponent = useCallback(() => { + // Skip this hook on the first render, as we are not sure if more actions are going to be loaded + // Therefore showing the skeleton on footer might be misleading if (firstRenderRef.current) { firstRenderRef.current = false; return null; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 88ce3faf5f65..07d344381727 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,4 +1,4 @@ -import React, {useRef, useEffect, useContext, useMemo} from 'react'; +import React, {useRef, useEffect, useContext, useMemo, useCallback} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashGet from 'lodash/get'; @@ -167,27 +167,23 @@ function ReportActionsView(props) { * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadNewerChats = _.throttle(({distanceFromStart}) => { - // Only fetch more if we are not already fetching so that we don't initiate duplicate requests. - if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions) { - return; - } - - // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', - // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. - // - // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not - // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. - // - // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. - // This should be removed once the issue of frequent re-renders is resolved. - if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - isFetchNewerWasCalled.current = true; - return; - } - const newestReportAction = _.first(props.reportActions); - Report.getNewerActions(reportID, newestReportAction.reportActionID); - }, 500); + const loadNewerChats = useMemo( + () => + _.throttle(({distanceFromStart}) => { + if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions) { + return; + } + + if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + isFetchNewerWasCalled.current = true; + return; + } + + const newestReportAction = _.first(props.reportActions); + Report.getNewerActions(reportID, newestReportAction.reportActionID); + }, 500), + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID], // List of dependencies + ); /** * Runs when the FlatList finishes laying out diff --git a/src/styles/boundaryLoaderStyle/index.android.ts b/src/styles/boundaryLoaderStyle/index.android.ts new file mode 100644 index 000000000000..39390cf5ed10 --- /dev/null +++ b/src/styles/boundaryLoaderStyle/index.android.ts @@ -0,0 +1,13 @@ +import CONST from '../../CONST'; +import BoundaryLoaderStyles from './types'; + +const boundaryLoaderStyles: BoundaryLoaderStyles = { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: CONST.CHAT_HEADER_LOADER_HEIGHT, + top: -CONST.CHAT_HEADER_LOADER_HEIGHT, +}; + +export default boundaryLoaderStyles; diff --git a/src/styles/boundaryLoaderStyle/index.ts b/src/styles/boundaryLoaderStyle/index.ts new file mode 100644 index 000000000000..bfab66276859 --- /dev/null +++ b/src/styles/boundaryLoaderStyle/index.ts @@ -0,0 +1,13 @@ +import CONST from '../../CONST'; +import OptionAlternateTextPlatformStyles from './types'; + +const optionAlternateTextPlatformStyles: OptionAlternateTextPlatformStyles = { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + height: CONST.CHAT_HEADER_LOADER_HEIGHT, +}; + +export default optionAlternateTextPlatformStyles; diff --git a/src/styles/boundaryLoaderStyle/types.ts b/src/styles/boundaryLoaderStyle/types.ts new file mode 100644 index 000000000000..a0e76a352a12 --- /dev/null +++ b/src/styles/boundaryLoaderStyle/types.ts @@ -0,0 +1,7 @@ +import {ViewStyle} from 'react-native'; + +type BoundaryLoaderStyles = Partial>; + +export default BoundaryLoaderStyles; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index a855a91f4ec1..ca81e2c2946a 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -12,15 +12,6 @@ type Report = { /** List of icons for report participants */ icons?: OnyxCommon.Icon[]; - /** Are we loading more report actions? */ - isLoadingOlderReportActions?: boolean; - - /** Are we loading newer report actions? */ - isLoadingNewerReportActions?: boolean; - - /** Flag to check if the report actions data are loading */ - isLoadingInitialReportActions?: boolean; - /** Whether the user is not an admin of policyExpenseChat chat */ isOwnPolicyExpenseChat?: boolean; From 8ebb4680705756c40d0d2d43d817ed74afd48be2 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 9 Oct 2023 23:28:33 +0200 Subject: [PATCH 081/548] lint types --- src/styles/boundaryLoaderStyle/types.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/styles/boundaryLoaderStyle/types.ts b/src/styles/boundaryLoaderStyle/types.ts index a0e76a352a12..d95efa63929c 100644 --- a/src/styles/boundaryLoaderStyle/types.ts +++ b/src/styles/boundaryLoaderStyle/types.ts @@ -1,7 +1,5 @@ import {ViewStyle} from 'react-native'; -type BoundaryLoaderStyles = Partial>; +type BoundaryLoaderStyles = Partial>; export default BoundaryLoaderStyles; From bf9c309c8099858009af00cb2d1c6383451006e9 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 10 Oct 2023 08:53:16 +0200 Subject: [PATCH 082/548] clean redundant const --- src/components/ReportActionsSkeletonView/index.js | 3 +-- src/pages/home/report/ReportActionsList.js | 10 +++++----- src/pages/home/report/ReportActionsView.js | 4 ++-- src/styles/boundaryLoaderStyle/index.ts | 6 +++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/ReportActionsSkeletonView/index.js b/src/components/ReportActionsSkeletonView/index.js index 82a419d9995d..83a14ecb99e6 100644 --- a/src/components/ReportActionsSkeletonView/index.js +++ b/src/components/ReportActionsSkeletonView/index.js @@ -19,9 +19,8 @@ const defaultProps = { function ReportActionsSkeletonView({shouldAnimate, possibleVisibleContentItems}) { // Determines the number of content items based on container height - const visibleContentItems = possibleVisibleContentItems; const skeletonViewLines = []; - for (let index = 0; index < visibleContentItems; index++) { + for (let index = 0; index < possibleVisibleContentItems; index++) { const iconIndex = (index + 1) % 4; switch (iconIndex) { case 2: diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 72addf2b55a4..a146a559c323 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -132,7 +132,7 @@ function ReportActionsList({ const [currentUnreadMarker, setCurrentUnreadMarker] = useState(null); const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); - const firstRenderRef = useRef(true); + const firstComponentsRenderRef = useRef({header: true, footer: true}); const reportActionSize = useRef(sortedReportActions.length); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); @@ -352,8 +352,8 @@ function ReportActionsList({ const listFooterComponent = useCallback(() => { // Skip this hook on the first render, as we are not sure if more actions are going to be loaded // Therefore showing the skeleton on footer might be misleading - if (firstRenderRef.current) { - firstRenderRef.current = false; + if (firstComponentsRenderRef.current.footer) { + firstComponentsRenderRef.current.footer = false; return null; } @@ -375,8 +375,8 @@ function ReportActionsList({ ); const listHeaderComponent = useCallback(() => { - if (firstRenderRef.current) { - firstRenderRef.current = false; + if (firstComponentsRenderRef.current.header) { + firstComponentsRenderRef.current.header = false; return null; } return ( diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 07d344381727..9be12b1d161a 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,4 +1,4 @@ -import React, {useRef, useEffect, useContext, useMemo, useCallback} from 'react'; +import React, {useRef, useEffect, useContext, useMemo} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashGet from 'lodash/get'; @@ -182,7 +182,7 @@ function ReportActionsView(props) { const newestReportAction = _.first(props.reportActions); Report.getNewerActions(reportID, newestReportAction.reportActionID); }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID], // List of dependencies + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID], ); /** diff --git a/src/styles/boundaryLoaderStyle/index.ts b/src/styles/boundaryLoaderStyle/index.ts index bfab66276859..a8e0a1bf3358 100644 --- a/src/styles/boundaryLoaderStyle/index.ts +++ b/src/styles/boundaryLoaderStyle/index.ts @@ -1,7 +1,7 @@ import CONST from '../../CONST'; -import OptionAlternateTextPlatformStyles from './types'; +import BoundaryLoaderStyles from './types'; -const optionAlternateTextPlatformStyles: OptionAlternateTextPlatformStyles = { +const boundaryLoaderStyles: BoundaryLoaderStyles = { position: 'absolute', top: 0, bottom: 0, @@ -10,4 +10,4 @@ const optionAlternateTextPlatformStyles: OptionAlternateTextPlatformStyles = { height: CONST.CHAT_HEADER_LOADER_HEIGHT, }; -export default optionAlternateTextPlatformStyles; +export default boundaryLoaderStyles; From ef623a4c603bd4b4909f5f740e0d310d047cee24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 10 Oct 2023 13:10:23 +0200 Subject: [PATCH 083/548] address refactoring --- .../settings/Wallet/ExpensifyCardPage.js | 2 +- .../settings/Wallet/revealCardDetailsUtils.ts | 32 ++++++++----------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index fe16b9014e19..892aea0495da 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -104,7 +104,7 @@ function ExpensifyCardPage({ pan={details.pan} expiration={details.expiration} cvv={details.cvv} - privatePersonalDetails={details.privatePersonalDetails} + privatePersonalDetails={{address: details.address}} /> ) : ( Date: Tue, 10 Oct 2023 16:31:10 +0200 Subject: [PATCH 084/548] add comment to isFirstRender --- src/pages/home/report/ReportActionsView.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 9be12b1d161a..f74668d64a8a 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -70,7 +70,7 @@ function ReportActionsView(props) { const reactionListRef = useContext(ReactionListContext); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - const isFetchNewerWasCalled = useRef(false); + const isFirstRender = useRef(false); const hasCachedActions = useRef(_.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); @@ -174,8 +174,9 @@ function ReportActionsView(props) { return; } - if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - isFetchNewerWasCalled.current = true; + // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call + if (!isFirstRender.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + isFirstRender.current = true; return; } From f45bc4454feaa7da57206bd7a64fc30906cfc007 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 10 Oct 2023 17:59:20 +0200 Subject: [PATCH 085/548] update fetchReportIfNeeded --- src/pages/home/ReportScreen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 54a2efe7e4e0..99f5f0ca5d2a 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -238,12 +238,12 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === getReportID(route) && !reportMetadata.isLoadingInitialReportActions) { + if (report.reportID && report.reportID === getReportID(route) && !isFirstlyLoadingReportActions) { return; } Report.openReport(reportIDFromPath); - }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions]); + }, [report.reportID, route, isFirstlyLoadingReportActions]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); From 31abad115fe54f3fdd34ac96aa1ffe17ff702793 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 11 Oct 2023 01:03:30 +0530 Subject: [PATCH 086/548] added requested changes --- src/components/MenuItemList.js | 2 +- src/pages/settings/AboutPage/AboutPage.js | 147 +++++++++--------- src/pages/settings/InitialSettingsPage.js | 64 ++++---- .../settings/Security/SecuritySettingsPage.js | 49 +++--- src/pages/workspace/WorkspaceInitialPage.js | 7 +- 5 files changed, 138 insertions(+), 131 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index f59c31dc6fda..c7e4a2b7c9ca 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -40,7 +40,7 @@ function MenuItemList(props) { <> {_.map(props.menuItems, (menuItemProps) => ( secondaryInteraction(menuItemProps.link, e) : undefined} ref={(el) => (popoverAnchor = el)} shouldBlockSelection={Boolean(menuItemProps.link)} diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index ec3325bf1ed7..97e41f433615 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -1,32 +1,32 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import _ from 'underscore'; +import React, {useMemo, useRef} from 'react'; import {ScrollView, View} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import Logo from '../../../../assets/images/new-expensify.svg'; -import pkg from '../../../../package.json'; +import PropTypes from 'prop-types'; +import DeviceInfo from 'react-native-device-info'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; +import Navigation from '../../../libs/Navigation/Navigation'; +import ROUTES from '../../../ROUTES'; +import styles from '../../../styles/styles'; +import Text from '../../../components/Text'; +import TextLink from '../../../components/TextLink'; import CONST from '../../../CONST'; import ONYXKEYS from '../../../ONYXKEYS'; -import ROUTES from '../../../ROUTES'; -import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import * as Expensicons from '../../../components/Icon/Expensicons'; -import MenuItemList from '../../../components/MenuItemList'; import ScreenWrapper from '../../../components/ScreenWrapper'; -import Text from '../../../components/Text'; -import TextLink from '../../../components/TextLink'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; -import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; -import * as Environment from '../../../libs/Environment/Environment'; -import Navigation from '../../../libs/Navigation/Navigation'; -import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; -import * as Link from '../../../libs/actions/Link'; +import MenuItemList from '../../../components/MenuItemList'; +import Logo from '../../../../assets/images/new-expensify.svg'; +import pkg from '../../../../package.json'; import * as Report from '../../../libs/actions/Report'; +import * as Link from '../../../libs/actions/Link'; import compose from '../../../libs/compose'; -import styles from '../../../styles/styles'; -import {CONTEXT_MENU_TYPES} from '../../home/report/ContextMenu/ContextMenuActions'; import * as ReportActionContextMenu from '../../home/report/ContextMenu/ReportActionContextMenu'; +import {CONTEXT_MENU_TYPES} from '../../home/report/ContextMenu/ContextMenuActions'; +import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; +import * as Environment from '../../../libs/Environment/Environment'; const propTypes = { ...withLocalizePropTypes, @@ -50,43 +50,59 @@ function getFlavor() { } function AboutPage(props) { - let popoverAnchor; + const {translate, isShortcutsModalOpen} = props; + const popoverAnchor = useRef(null); const waitForNavigate = useWaitForNavigation(); - const menuItems = [ - { - translationKey: 'initialSettingsPage.aboutPage.appDownloadLinks', - icon: Expensicons.Link, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_APP_DOWNLOAD_LINKS)), - }, - { - translationKey: 'initialSettingsPage.aboutPage.viewKeyboardShortcuts', - icon: Expensicons.Keyboard, - action: KeyboardShortcuts.showKeyboardShortcutModal, - }, - { - translationKey: 'initialSettingsPage.aboutPage.viewTheCode', - icon: Expensicons.Eye, - iconRight: Expensicons.NewWindow, - action: () => { - Link.openExternalLink(CONST.GITHUB_URL); + + const menuItems = useMemo(() => { + const baseMenuItems = [ + { + translationKey: 'initialSettingsPage.aboutPage.appDownloadLinks', + icon: Expensicons.Link, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_APP_DOWNLOAD_LINKS)), }, - link: CONST.GITHUB_URL, - }, - { - translationKey: 'initialSettingsPage.aboutPage.viewOpenJobs', - icon: Expensicons.MoneyBag, - iconRight: Expensicons.NewWindow, - action: () => { - Link.openExternalLink(CONST.UPWORK_URL); + { + translationKey: 'initialSettingsPage.aboutPage.viewKeyboardShortcuts', + icon: Expensicons.Keyboard, + action: KeyboardShortcuts.showKeyboardShortcutModal, }, - link: CONST.UPWORK_URL, - }, - { - translationKey: 'initialSettingsPage.aboutPage.reportABug', - icon: Expensicons.Bug, - action: Report.navigateToConciergeChat, - }, - ]; + { + translationKey: 'initialSettingsPage.aboutPage.viewTheCode', + icon: Expensicons.Eye, + iconRight: Expensicons.NewWindow, + action: () => { + Link.openExternalLink(CONST.GITHUB_URL); + }, + link: CONST.GITHUB_URL, + }, + { + translationKey: 'initialSettingsPage.aboutPage.viewOpenJobs', + icon: Expensicons.MoneyBag, + iconRight: Expensicons.NewWindow, + action: () => { + Link.openExternalLink(CONST.UPWORK_URL); + }, + link: CONST.UPWORK_URL, + }, + { + translationKey: 'initialSettingsPage.aboutPage.reportABug', + icon: Expensicons.Bug, + action: Report.navigateToConciergeChat, + }, + ]; + return _.map(baseMenuItems, (item) => ({ + key: item.translationKey, + title: translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + disabled: isShortcutsModalOpen, + onPress: item.action, + shouldShowRightIcon: true, + onSecondaryInteraction: !_.isEmpty(item.link) ? (e) => ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, item.link, popoverAnchor) : undefined, + ref: popoverAnchor, + shouldBlockSelection: Boolean(item.link), + })); + }, [isShortcutsModalOpen, translate, waitForNavigate]); return ( ( <> Navigation.goBack(ROUTES.SETTINGS)} /> @@ -113,24 +129,11 @@ function AboutPage(props) { > v{Environment.isInternalTestBuild() ? `${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}${getFlavor()}` : `${pkg.version}${getFlavor()}`} - {props.translate('initialSettingsPage.aboutPage.description')} + {translate('initialSettingsPage.aboutPage.description')} ({ - key: item.translationKey, - title: props.translate(item.translationKey), - icon: item.icon, - iconRight: item.iconRight, - disabled: props.isShortcutsModalOpen, - onPress: item.action, - shouldShowRightIcon: true, - onSecondaryInteraction: !_.isEmpty(item.link) - ? (e) => ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, item.link, popoverAnchor) - : undefined, - ref: (el) => (popoverAnchor = el), - shouldBlockSelection: Boolean(item.link), - }))} + menuItems={menuItems} shouldUseSingleExecution /> @@ -139,19 +142,19 @@ function AboutPage(props) { style={[styles.chatItemMessageHeaderTimestamp]} numberOfLines={1} > - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')} {' '} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')} . diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 619aba06e026..5ed498fcb3d0 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -1,49 +1,49 @@ import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; import _ from 'underscore'; -import CONST from '../../CONST'; -import ONYXKEYS from '../../ONYXKEYS'; -import ROUTES from '../../ROUTES'; -import SCREENS from '../../SCREENS'; -import Avatar from '../../components/Avatar'; -import ConfirmModal from '../../components/ConfirmModal'; +import {withOnyx} from 'react-native-onyx'; import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; -import HeaderPageLayout from '../../components/HeaderPageLayout'; -import * as Expensicons from '../../components/Icon/Expensicons'; -import MenuItem from '../../components/MenuItem'; -import OfflineWithFeedback from '../../components/OfflineWithFeedback'; import {withNetwork} from '../../components/OnyxProvider'; -import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; +import styles from '../../styles/styles'; import Text from '../../components/Text'; +import * as Session from '../../libs/actions/Session'; +import ONYXKEYS from '../../ONYXKEYS'; import Tooltip from '../../components/Tooltip'; -import bankAccountPropTypes from '../../components/bankAccountPropTypes'; -import cardPropTypes from '../../components/cardPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import useLocalize from '../../hooks/useLocalize'; -import useSingleExecution from '../../hooks/useSingleExecution'; -import useWaitForNavigation from '../../hooks/useWaitForNavigation'; -import * as CurrencyUtils from '../../libs/CurrencyUtils'; +import Avatar from '../../components/Avatar'; import Navigation from '../../libs/Navigation/Navigation'; +import * as Expensicons from '../../components/Icon/Expensicons'; +import MenuItem from '../../components/MenuItem'; +import themeColors from '../../styles/themes/default'; +import SCREENS from '../../SCREENS'; +import ROUTES from '../../ROUTES'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import compose from '../../libs/compose'; +import CONST from '../../CONST'; import Permissions from '../../libs/Permissions'; -import * as PolicyUtils from '../../libs/PolicyUtils'; -import * as ReportUtils from '../../libs/ReportUtils'; -import * as UserUtils from '../../libs/UserUtils'; -import * as Link from '../../libs/actions/Link'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; import * as PaymentMethods from '../../libs/actions/PaymentMethods'; -import * as Session from '../../libs/actions/Session'; +import bankAccountPropTypes from '../../components/bankAccountPropTypes'; +import cardPropTypes from '../../components/cardPropTypes'; import * as Wallet from '../../libs/actions/Wallet'; -import compose from '../../libs/compose'; -import styles from '../../styles/styles'; -import themeColors from '../../styles/themes/default'; import walletTermsPropTypes from '../EnablePayments/walletTermsPropTypes'; +import * as PolicyUtils from '../../libs/PolicyUtils'; +import ConfirmModal from '../../components/ConfirmModal'; +import * as ReportUtils from '../../libs/ReportUtils'; +import * as Link from '../../libs/actions/Link'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursementAccountPropTypes'; -import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; -import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; +import * as UserUtils from '../../libs/UserUtils'; import policyMemberPropType from '../policyMemberPropType'; +import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; +import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; +import * as CurrencyUtils from '../../libs/CurrencyUtils'; +import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; +import useLocalize from '../../hooks/useLocalize'; +import useSingleExecution from '../../hooks/useSingleExecution'; +import useWaitForNavigation from '../../hooks/useWaitForNavigation'; +import HeaderPageLayout from '../../components/HeaderPageLayout'; const propTypes = { /* Onyx Props */ diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js index d759e1a7fd53..dac4ed4f872f 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.js +++ b/src/pages/settings/Security/SecuritySettingsPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useMemo} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -34,24 +34,36 @@ const defaultProps = { }; function SecuritySettingsPage(props) { + const {translate} = props; const waitForNavigate = useWaitForNavigation(); - const menuItems = [ - { - translationKey: 'twoFactorAuth.headerTitle', - icon: Expensicons.Shield, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA)), - }, - { - translationKey: 'closeAccountPage.closeAccount', - icon: Expensicons.ClosedSign, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), - }, - ]; + const menuItems = useMemo(() => { + const baseMenuItems = [ + { + translationKey: 'twoFactorAuth.headerTitle', + icon: Expensicons.Shield, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA)), + }, + { + translationKey: 'closeAccountPage.closeAccount', + icon: Expensicons.ClosedSign, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), + }, + ]; + + return _.map(baseMenuItems, (item) => ({ + key: item.translationKey, + title: translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + onPress: item.action, + shouldShowRightIcon: true, + })); + }, [translate, waitForNavigate]); return ( Navigation.goBack(ROUTES.SETTINGS)} shouldShowBackButton illustration={LottieAnimations.Safe} @@ -60,14 +72,7 @@ function SecuritySettingsPage(props) { ({ - key: item.translationKey, - title: props.translate(item.translationKey), - icon: item.icon, - iconRight: item.iconRight, - onPress: item.action, - shouldShowRightIcon: true, - }))} + menuItems={menuItems} shouldUseSingleExecution /> diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index c2df7da8b729..7ab5279cc885 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -159,11 +159,10 @@ function WorkspaceInitialPage(props) { { translationKey: 'workspace.common.bankAccount', icon: Expensicons.Bank, - action: waitForNavigate(() => + action: () => policy.outputCurrency === CONST.CURRENCY.USD - ? ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRoute().replace(/\?.*/, '')) + ? waitForNavigate(() => ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRoute().replace(/\?.*/, '', waitForNavigate)))() : setIsCurrencyModalOpen(true), - ), brickRoadIndicator: !_.isEmpty(props.reimbursementAccount.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, ]; @@ -258,7 +257,7 @@ function WorkspaceInitialPage(props) { {_.map(menuItems, (item) => ( Date: Wed, 11 Oct 2023 01:50:51 +0530 Subject: [PATCH 087/548] added comment why execption made for mapping menuitems --- src/components/MenuItemList.js | 6 +++--- src/pages/workspace/WorkspaceInitialPage.js | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index c7e4a2b7c9ca..d809efc7cb90 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -1,11 +1,11 @@ -import PropTypes from 'prop-types'; import React from 'react'; import _ from 'underscore'; +import PropTypes from 'prop-types'; import useSingleExecution from '../hooks/useSingleExecution'; -import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; -import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; import MenuItem from './MenuItem'; import menuItemPropTypes from './menuItemPropTypes'; +import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; +import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; const propTypes = { /** An array of props that are pass to individual MenuItem components */ diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 7ab5279cc885..97e5d5990de2 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -254,6 +254,10 @@ function WorkspaceInitialPage(props) { )} + {/* + Ideally we should use MenuList component for MenuItems with singleExecution/Navigation Actions. + But Here we need to have a `isExecuting` for profile details click actions also so we are directly mapping menuItems. + */} {_.map(menuItems, (item) => ( Date: Tue, 10 Oct 2023 16:07:59 -0700 Subject: [PATCH 088/548] Create Tips-And-Tricks.md --- .../getting-started/Tips-And-Tricks.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/articles/expensify-classic/getting-started/Tips-And-Tricks.md diff --git a/docs/articles/expensify-classic/getting-started/Tips-And-Tricks.md b/docs/articles/expensify-classic/getting-started/Tips-And-Tricks.md new file mode 100644 index 000000000000..e4fd2d4bc44c --- /dev/null +++ b/docs/articles/expensify-classic/getting-started/Tips-And-Tricks.md @@ -0,0 +1,72 @@ +--- +title: Tips and Tricks +description: How to get started with setup tips for your Expensify account +--- + +# Overview +In this article, we'll outline helpful tips for using Expensify, such as keyboard shortcuts and text formatting. + +# How to Format Text in Expensify +You can use basic markdown in report comments to emphasize or clarify your sentiments. This includes italicizing, bolding, and strikethrough for text, as well as adding basic hyperlinks. +Formatting is consistent across both web and mobile applications, with three markdown options available for your report comments: +- **Bold:** Place an asterisk on either side (*bold*) +- **Italicize:** Place an underscore on either side (_italic_) +- **Strikethrough:** Place a tilde on either side (~strikethrough~) + +# How to Use Keyboard Shortcuts +Keyboard shortcuts can speed things up and simplify tasks. Expensify offers several shortcuts for your convenience. Let's explore them! +- **Shift + ?** - Opens the keyboard shortcuts dialog +- **Shift + G** - Prompts you for a reportID to open the report page for a specific report +- **ESC** - Closes any shortcut dialog window +- **Ctrl+Enter** - Submit a comment on a report from the comment field in the Report History & Comments section. +- **Shift + P** - Takes you to the report’s policy when you’re on a report +- **Shift + →** - Go to the next report +- **Shift + ←** - Go to the previous report +- **Shift + R** - Reloads the current page + +# How to Create a Copy of a Report +If you have identical monthly expenses and want to copy them easily, visit your Reports page, check the box next to the report you would like to duplicate, and click "Copy" to duplicate all expenses (excluding receipt images). +If you prefer, you can create a standard template for certain expenses: +1. Go to the Reports page. +2. Click "New Report." +3. Assign an easily searchable name to the report. +4. Click the green '+' button to add an expense. +5. Choose "New Expense". +6. Select the type of expense (e.g., regular expense, distance, time, etc.). +7. Enter the expense details, code, and any relevant description. +8. Click "Save." +**Pro Tip:** If you use Scheduled Submit, place the template report under your individual workspace to avoid accidental submission. When you're ready to use it, check the report box, copy it, and make necessary changes to the name and workspace. + +# How to Enable Location Access on Web +If you’d like to use features that rely on your current location you will need to enable location permissions for Expensify. You can find instructions for how to enable location settings on the three most common web browsers below. If your browser is not in the list then please do a web search for your browser and “enable location settings”. + +## Chrome +1. Open Chrome +2. At the top right, click the three-dot Menu > Settings +3. Click “Privacy and Security” and then “Site Settings” +4. Click Location +5. Check the “Not allowed to see your location” list to make sure expensify.com and new.expensify.com are not listed. If they are, click the delete icon next to them to allow location access + +## Firefox +1. Open Firefox +2. In the URL bar enter “about:preferences” +3. On the left hand side select “Privacy & Security” +4. Scroll down to Permissions +5. Click on Settings next to Location +6. If location access is blocked for expensify.com or new.expensify.com, you can update it here to allow access + +## Safari +1. In the top menu bar click Safari +2. Then select Settings > Websites +3. Click Location on the left hand side +4. If expensify.com or new.expensify.com have “Deny” set as their access, update it to “Ask” or “Allow” + +# Which browser works best with Expensify? +We recommend using Google Chrome, but you can use Expensify on most major browsers, such as: +- [Google Chrome](www.google.com/chrome/) +- [Mozilla Firefox](www.mozilla.com/firefox) +- [Microsoft Edge](www.microsoft.com/edge) +- [Microsoft Internet Explorer](www.microsoft.com/ie). Please note: Microsoft has discontinued support and security updates for all versions below Version 11. This means those older versions may not work well. Due to the lack of security updates for the older versions, parts of our site may not be accessible. Please update your IE version or choose a different browser. +- [Apple Safari (Apple devices only)](www.apple.com/safari) +- [Opera](www.opera.com) +It's always best practice to make sure you have the most recent updates for your browser and keep your operating system up to date. From 06aeba6a3788bb3fa25aea61c8387eea712969a3 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 11 Oct 2023 09:38:04 +0200 Subject: [PATCH 089/548] replace isFetchNewerWasCalled with isFirstRender --- src/pages/home/report/ReportActionsView.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f74668d64a8a..a6755d394da4 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -70,7 +70,7 @@ function ReportActionsView(props) { const reactionListRef = useContext(ReactionListContext); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - const isFirstRender = useRef(false); + const isFirstRender = useRef(true); const hasCachedActions = useRef(_.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); @@ -174,9 +174,18 @@ function ReportActionsView(props) { return; } + // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', + // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. + // + // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not + // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. + // + // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. + // This should be removed once the issue of frequent re-renders is resolved. + // // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call - if (!isFirstRender.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - isFirstRender.current = true; + if (isFirstRender.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + isFirstRender.current = false; return; } From 3c41ef5638447698d902e9285a8505e5254d6b92 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 11 Oct 2023 11:43:12 +0200 Subject: [PATCH 090/548] Fixes in onyxkeys --- src/ONYXKEYS.ts | 2 +- src/libs/PolicyUtils.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b01ffbc37141..495b9c4595f9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -370,7 +370,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory; - [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTag; + [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTags; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index c0bb7e539bec..7afac2ce5b67 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -13,10 +13,7 @@ type UnitRate = {rate: number}; * These are policies that we can use to create reports with in NewDot. */ function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { - if (!policies) { - return; - } - return (Object.values(policies) ?? []).filter( + return (Object.values(policies ?? {}) ?? []).filter( (policy): policy is Policy => policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); @@ -91,8 +88,7 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMemb */ function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean { return ( - policy !== null && - policy && + !!policy && policy?.isPolicyExpenseChatEnabled && policy?.role === CONST.POLICY.ROLE.ADMIN && (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) @@ -120,7 +116,7 @@ const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === C * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs { - const memberEmailsToAccountIDs: Record = {}; + const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; Object.keys(policyMembers ?? {}).forEach((accountID) => { const member = policyMembers?.[accountID]; if (Object.keys(member?.errors ?? {})?.length > 0) { From 7f3e6c873a587b3dea94402c47c5f4172a8d88ce Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 11 Oct 2023 12:24:36 +0200 Subject: [PATCH 091/548] improve getCategoryOptionTree --- src/libs/OptionsListUtils.js | 19 ++++++++++--------- tests/unit/OptionsListUtilsTest.js | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 82285545b303..a645d73ec998 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -644,7 +644,7 @@ function hasEnabledOptions(options) { * @returns {Array} */ function getCategoryOptionTree(options, isOneLine = false) { - const optionCollection = {}; + const optionCollection = new Map(); _.each(options, (option) => { if (!option.enabled) { @@ -652,17 +652,17 @@ function getCategoryOptionTree(options, isOneLine = false) { } if (isOneLine) { - if (_.has(optionCollection, option.name)) { + if (optionCollection.has(option.name)) { return; } - optionCollection[option.name] = { + optionCollection.set(option.name, { text: option.name, keyForList: option.name, searchText: option.name, tooltipText: option.name, isDisabled: !option.enabled, - }; + }); return; } @@ -670,22 +670,23 @@ function getCategoryOptionTree(options, isOneLine = false) { option.name.split(CONST.PARENT_CHILD_SEPARATOR).forEach((optionName, index, array) => { const indents = _.times(index, () => CONST.INDENTS).join(''); const isChild = array.length - 1 === index; + const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); - if (_.has(optionCollection, optionName)) { + if (optionCollection.has(searchText)) { return; } - optionCollection[optionName] = { + optionCollection.set(searchText, { text: `${indents}${optionName}`, keyForList: optionName, - searchText: array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR), + searchText, tooltipText: optionName, isDisabled: isChild ? !option.enabled : true, - }; + }); }); }); - return _.values(optionCollection); + return Array.from(optionCollection.values()); } /** diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 437d37e625dd..17e4ac1c33cb 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -1342,6 +1342,10 @@ describe('OptionsListUtils', () => { it('getCategoryOptionTree()', () => { const categories = { + Meals: { + enabled: true, + name: 'Meals', + }, Taxi: { enabled: false, name: 'Taxi', @@ -1402,6 +1406,10 @@ describe('OptionsListUtils', () => { enabled: true, name: 'Plain', }, + Audi: { + enabled: true, + name: 'Audi', + }, Health: { enabled: true, name: 'Health', @@ -1416,6 +1424,13 @@ describe('OptionsListUtils', () => { }, }; const result = [ + { + text: 'Meals', + keyForList: 'Meals', + searchText: 'Meals', + tooltipText: 'Meals', + isDisabled: false, + }, { text: 'Restaurant', keyForList: 'Restaurant', @@ -1500,6 +1515,13 @@ describe('OptionsListUtils', () => { tooltipText: 'Plain', isDisabled: false, }, + { + text: 'Audi', + keyForList: 'Audi', + searchText: 'Audi', + tooltipText: 'Audi', + isDisabled: false, + }, { text: 'Health', keyForList: 'Health', From bc9b9522591c04f59cb5069c161f5f1b7cf95f8a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 11 Oct 2023 12:28:23 +0200 Subject: [PATCH 092/548] add one-line test case --- tests/unit/OptionsListUtilsTest.js | 108 +++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 17e4ac1c33cb..b783fdbe950a 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -1565,8 +1565,116 @@ describe('OptionsListUtils', () => { isDisabled: false, }, ]; + const resultOneLine = [ + { + text: 'Meals', + keyForList: 'Meals', + searchText: 'Meals', + tooltipText: 'Meals', + isDisabled: false, + }, + { + text: 'Restaurant', + keyForList: 'Restaurant', + searchText: 'Restaurant', + tooltipText: 'Restaurant', + isDisabled: false, + }, + { + text: 'Food', + keyForList: 'Food', + searchText: 'Food', + tooltipText: 'Food', + isDisabled: false, + }, + { + text: 'Food: Meat', + keyForList: 'Food: Meat', + searchText: 'Food: Meat', + tooltipText: 'Food: Meat', + isDisabled: false, + }, + { + text: 'Food: Milk', + keyForList: 'Food: Milk', + searchText: 'Food: Milk', + tooltipText: 'Food: Milk', + isDisabled: false, + }, + { + text: 'Cars: Audi', + keyForList: 'Cars: Audi', + searchText: 'Cars: Audi', + tooltipText: 'Cars: Audi', + isDisabled: false, + }, + { + text: 'Cars: Mercedes-Benz', + keyForList: 'Cars: Mercedes-Benz', + searchText: 'Cars: Mercedes-Benz', + tooltipText: 'Cars: Mercedes-Benz', + isDisabled: false, + }, + { + text: 'Travel: Meals', + keyForList: 'Travel: Meals', + searchText: 'Travel: Meals', + tooltipText: 'Travel: Meals', + isDisabled: false, + }, + { + text: 'Travel: Meals: Breakfast', + keyForList: 'Travel: Meals: Breakfast', + searchText: 'Travel: Meals: Breakfast', + tooltipText: 'Travel: Meals: Breakfast', + isDisabled: false, + }, + { + text: 'Travel: Meals: Lunch', + keyForList: 'Travel: Meals: Lunch', + searchText: 'Travel: Meals: Lunch', + tooltipText: 'Travel: Meals: Lunch', + isDisabled: false, + }, + { + text: 'Plain', + keyForList: 'Plain', + searchText: 'Plain', + tooltipText: 'Plain', + isDisabled: false, + }, + { + text: 'Audi', + keyForList: 'Audi', + searchText: 'Audi', + tooltipText: 'Audi', + isDisabled: false, + }, + { + text: 'Health', + keyForList: 'Health', + searchText: 'Health', + tooltipText: 'Health', + isDisabled: false, + }, + { + text: 'A: B: C', + keyForList: 'A: B: C', + searchText: 'A: B: C', + tooltipText: 'A: B: C', + isDisabled: false, + }, + { + text: 'A: B: C: D: E', + keyForList: 'A: B: C: D: E', + searchText: 'A: B: C: D: E', + tooltipText: 'A: B: C: D: E', + isDisabled: false, + }, + ]; expect(OptionsListUtils.getCategoryOptionTree(categories)).toStrictEqual(result); + expect(OptionsListUtils.getCategoryOptionTree(categories, true)).toStrictEqual(resultOneLine); }); it('formatMemberForList()', () => { From 045068c1e89f10b93f67f386f186b2689ea44b6d Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 11 Oct 2023 13:35:14 +0200 Subject: [PATCH 093/548] [TS migration] Migrate 'useDragAndDrop.js' hook and 'withEnvironment.js' HOC --- ...withEnvironment.js => withEnvironment.tsx} | 39 ++++++++++--------- .../{useEnvironment.js => useEnvironment.ts} | 10 ++++- src/pages/ShareCodePage.js | 7 +++- 3 files changed, 34 insertions(+), 22 deletions(-) rename src/components/{withEnvironment.js => withEnvironment.tsx} (54%) rename src/hooks/{useEnvironment.js => useEnvironment.ts} (59%) diff --git a/src/components/withEnvironment.js b/src/components/withEnvironment.tsx similarity index 54% rename from src/components/withEnvironment.js rename to src/components/withEnvironment.tsx index 3aa9b86e82c8..6fe84860bb53 100644 --- a/src/components/withEnvironment.js +++ b/src/components/withEnvironment.tsx @@ -1,21 +1,26 @@ -import React, {createContext, useState, useEffect, forwardRef, useContext, useMemo} from 'react'; -import PropTypes from 'prop-types'; +import React, {ComponentType, RefAttributes, ReactNode, createContext, useState, useEffect, forwardRef, useContext, useMemo} from 'react'; +import {ValueOf} from 'type-fest'; import * as Environment from '../libs/Environment/Environment'; import CONST from '../CONST'; import getComponentDisplayName from '../libs/getComponentDisplayName'; -const EnvironmentContext = createContext(null); +type EnvironmentProviderProps = { + /** Actual content wrapped by this component */ + children: ReactNode; +}; -const environmentPropTypes = { +type EnvironmentContextValue = { /** The string value representing the current environment */ - environment: PropTypes.string.isRequired, + environment: ValueOf; /** The string value representing the URL of the current environment */ - environmentURL: PropTypes.string.isRequired, + environmentURL: string; }; -function EnvironmentProvider({children}) { - const [environment, setEnvironment] = useState(CONST.ENVIRONMENT.PRODUCTION); +const EnvironmentContext = createContext(null); + +function EnvironmentProvider({children}: EnvironmentProviderProps) { + const [environment, setEnvironment] = useState>(CONST.ENVIRONMENT.PRODUCTION); const [environmentURL, setEnvironmentURL] = useState(CONST.NEW_EXPENSIFY_URL); useEffect(() => { @@ -24,7 +29,7 @@ function EnvironmentProvider({children}) { }, []); const contextValue = useMemo( - () => ({ + (): EnvironmentContextValue => ({ environment, environmentURL, }), @@ -35,14 +40,11 @@ function EnvironmentProvider({children}) { } EnvironmentProvider.displayName = 'EnvironmentProvider'; -EnvironmentProvider.propTypes = { - /** Actual content wrapped by this component */ - children: PropTypes.node.isRequired, -}; -export default function withEnvironment(WrappedComponent) { - const WithEnvironment = forwardRef((props, ref) => { - const {environment, environmentURL} = useContext(EnvironmentContext); +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function withEnvironment(WrappedComponent: ComponentType) { + const WithEnvironment: ComponentType>> = forwardRef((props, ref) => { + const {environment, environmentURL} = useContext(EnvironmentContext) ?? {}; return ( & { + isProduction: boolean; + isDevelopment: boolean; +}; + +export default function useEnvironment(): UseEnvironment { + const {environment, environmentURL} = useContext(EnvironmentContext) ?? {}; return { environment, environmentURL, diff --git a/src/pages/ShareCodePage.js b/src/pages/ShareCodePage.js index e6d36ebc7070..f0f0ef7d09fd 100644 --- a/src/pages/ShareCodePage.js +++ b/src/pages/ShareCodePage.js @@ -1,6 +1,7 @@ import React from 'react'; import {View, ScrollView} from 'react-native'; import _ from 'underscore'; +import PropTypes from 'prop-types'; import ScreenWrapper from '../components/ScreenWrapper'; import HeaderWithBackButton from '../components/HeaderWithBackButton'; import Navigation from '../libs/Navigation/Navigation'; @@ -20,16 +21,18 @@ import CONST from '../CONST'; import ContextMenuItem from '../components/ContextMenuItem'; import * as UserUtils from '../libs/UserUtils'; import ROUTES from '../ROUTES'; -import withEnvironment, {environmentPropTypes} from '../components/withEnvironment'; +import withEnvironment from '../components/withEnvironment'; import * as Url from '../libs/Url'; const propTypes = { /** The report currently being looked at */ report: reportPropTypes, + /** The string value representing the URL of the current environment */ + environmentURL: PropTypes.string.isRequired, + ...withLocalizePropTypes, ...withCurrentUserPersonalDetailsPropTypes, - ...environmentPropTypes, }; const defaultProps = { From 70c8123e40e181c694755a7bfec0a90e53f8b856 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 11 Oct 2023 14:30:58 +0200 Subject: [PATCH 094/548] Update getComponentDisplayName to accept types with generics --- src/components/withEnvironment.tsx | 2 +- src/libs/getComponentDisplayName.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/withEnvironment.tsx b/src/components/withEnvironment.tsx index 6fe84860bb53..35e4d291da34 100644 --- a/src/components/withEnvironment.tsx +++ b/src/components/withEnvironment.tsx @@ -56,7 +56,7 @@ export default function withEnvironment(component: ComponentType): string { return component.displayName ?? component.name ?? 'Component'; } From ce478db6504993f6c6216803bc1d5f7ef2e48790 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 11 Oct 2023 14:39:02 +0200 Subject: [PATCH 095/548] Put EnvironmentValue into a separate type --- src/components/withEnvironment.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/withEnvironment.tsx b/src/components/withEnvironment.tsx index 35e4d291da34..5476c0562109 100644 --- a/src/components/withEnvironment.tsx +++ b/src/components/withEnvironment.tsx @@ -9,9 +9,11 @@ type EnvironmentProviderProps = { children: ReactNode; }; +type EnvironmentValue = ValueOf; + type EnvironmentContextValue = { /** The string value representing the current environment */ - environment: ValueOf; + environment: EnvironmentValue; /** The string value representing the URL of the current environment */ environmentURL: string; @@ -20,7 +22,7 @@ type EnvironmentContextValue = { const EnvironmentContext = createContext(null); function EnvironmentProvider({children}: EnvironmentProviderProps) { - const [environment, setEnvironment] = useState>(CONST.ENVIRONMENT.PRODUCTION); + const [environment, setEnvironment] = useState(CONST.ENVIRONMENT.PRODUCTION); const [environmentURL, setEnvironmentURL] = useState(CONST.NEW_EXPENSIFY_URL); useEffect(() => { From bb6cd818ebb3b9963c67da198870e8ca5350d19b Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 11 Oct 2023 16:17:31 +0200 Subject: [PATCH 096/548] migrate useReportScrollManager and ReportScreenContext to TypeScript --- .../{index.native.js => index.native.ts} | 17 ++++++++++------- .../{index.js => index.ts} | 18 ++++++++++-------- src/pages/home/ReportScreenContext.js | 6 ------ src/pages/home/ReportScreenContext.ts | 17 +++++++++++++++++ 4 files changed, 37 insertions(+), 21 deletions(-) rename src/hooks/useReportScrollManager/{index.native.js => index.native.ts} (56%) rename src/hooks/useReportScrollManager/{index.js => index.ts} (58%) delete mode 100644 src/pages/home/ReportScreenContext.js create mode 100644 src/pages/home/ReportScreenContext.ts diff --git a/src/hooks/useReportScrollManager/index.native.js b/src/hooks/useReportScrollManager/index.native.ts similarity index 56% rename from src/hooks/useReportScrollManager/index.native.js rename to src/hooks/useReportScrollManager/index.native.ts index d44a40222ca5..e40ff049ca12 100644 --- a/src/hooks/useReportScrollManager/index.native.js +++ b/src/hooks/useReportScrollManager/index.native.ts @@ -1,27 +1,30 @@ import {useContext, useCallback} from 'react'; -import {ActionListContext} from '../../pages/home/ReportScreenContext'; +import {ActionListContext, ActionListContextType} from '../../pages/home/ReportScreenContext'; -function useReportScrollManager() { +function useReportScrollManager(): { + ref: ActionListContextType; + scrollToIndex: (index: number) => void; + scrollToBottom: () => void; +} { const flatListRef = useContext(ActionListContext); /** * Scroll to the provided index. * - * @param {Object} index */ - const scrollToIndex = (index) => { - if (!flatListRef.current) { + const scrollToIndex = (index: number) => { + if (!flatListRef?.current) { return; } - flatListRef.current.scrollToIndex(index); + flatListRef.current.scrollToIndex({index}); }; /** * Scroll to the bottom of the flatlist. */ const scrollToBottom = useCallback(() => { - if (!flatListRef.current) { + if (!flatListRef?.current) { return; } diff --git a/src/hooks/useReportScrollManager/index.js b/src/hooks/useReportScrollManager/index.ts similarity index 58% rename from src/hooks/useReportScrollManager/index.js rename to src/hooks/useReportScrollManager/index.ts index 9a3303504b92..c466b4050266 100644 --- a/src/hooks/useReportScrollManager/index.js +++ b/src/hooks/useReportScrollManager/index.ts @@ -1,29 +1,31 @@ import {useContext, useCallback} from 'react'; -import {ActionListContext} from '../../pages/home/ReportScreenContext'; +import {ActionListContext, ActionListContextType} from '../../pages/home/ReportScreenContext'; -function useReportScrollManager() { +function useReportScrollManager(): { + ref: ActionListContextType; + scrollToIndex: (index: number, isEditing: boolean) => void; + scrollToBottom: () => void; +} { const flatListRef = useContext(ActionListContext); /** * Scroll to the provided index. On non-native implementations we do not want to scroll when we are scrolling because * we are editing a comment. * - * @param {Object} index - * @param {Boolean} isEditing */ - const scrollToIndex = (index, isEditing) => { - if (!flatListRef.current || isEditing) { + const scrollToIndex = (index: number, isEditing: boolean) => { + if (!flatListRef?.current || isEditing) { return; } - flatListRef.current.scrollToIndex(index); + flatListRef.current.scrollToIndex({index}); }; /** * Scroll to the bottom of the flatlist. */ const scrollToBottom = useCallback(() => { - if (!flatListRef.current) { + if (!flatListRef?.current) { return; } diff --git a/src/pages/home/ReportScreenContext.js b/src/pages/home/ReportScreenContext.js deleted file mode 100644 index 1e8d30cf7585..000000000000 --- a/src/pages/home/ReportScreenContext.js +++ /dev/null @@ -1,6 +0,0 @@ -import {createContext} from 'react'; - -const ActionListContext = createContext(); -const ReactionListContext = createContext(); - -export {ActionListContext, ReactionListContext}; diff --git a/src/pages/home/ReportScreenContext.ts b/src/pages/home/ReportScreenContext.ts new file mode 100644 index 000000000000..a74c6d9797ff --- /dev/null +++ b/src/pages/home/ReportScreenContext.ts @@ -0,0 +1,17 @@ +import {RefObject, createContext} from 'react'; +import {FlatList, GestureResponderEvent} from 'react-native'; + +type ReactionListRefType = { + showReactionList: (event: GestureResponderEvent | undefined, reactionListAnchor: Element, emojiName: string, reportActionID: string) => void; + hideReactionList: () => void; + isActiveReportAction: (actionID: number | string) => boolean; +}; + +type ActionListContextType = RefObject> | null; +type ReactionListContextType = RefObject | null; + +const ActionListContext = createContext(null); +const ReactionListContext = createContext(null); + +export {ActionListContext, ReactionListContext}; +export type {ReactionListRefType, ActionListContextType, ReactionListContextType}; From f7ba68a4757a73341437184996652df78490cf45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Wed, 11 Oct 2023 16:22:19 +0200 Subject: [PATCH 097/548] refactoring --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/Card.ts | 23 ++++++++ .../settings/Wallet/ExpensifyCardPage.js | 36 +++++------ .../settings/Wallet/revealCardDetailsUtils.ts | 59 ------------------- src/types/onyx/Card.ts | 15 +++++ 6 files changed, 56 insertions(+), 79 deletions(-) create mode 100644 src/libs/actions/Card.ts delete mode 100644 src/pages/settings/Wallet/revealCardDetailsUtils.ts diff --git a/src/languages/en.ts b/src/languages/en.ts index 9d9d4e0a4d70..2090ee4b76f2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -838,6 +838,7 @@ export default { revealDetails: 'Reveal details', copyCardNumber: 'Copy card number', }, + cardDetailsLoadingFailure: 'An error occurred loading card details, please try again.', }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transfer${amount ? ` ${amount}` : ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 2ad75685a1a2..ed44aeedd6d9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -834,6 +834,7 @@ export default { revealDetails: 'Revelar detalles', copyCardNumber: 'Copiar número de la tarjeta', }, + cardDetailsLoadingFailure: 'Ocurrió un error al cargar los detalles de la tarjeta. Por favor, inténtalo de nuevo.', }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transferir${amount ? ` ${amount}` : ''}`, diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts new file mode 100644 index 000000000000..f75c77c928d9 --- /dev/null +++ b/src/libs/actions/Card.ts @@ -0,0 +1,23 @@ +import * as API from '../API'; +import CONST from '../../CONST'; +import {TCardDetails} from '../../types/onyx/Card'; +import * as Localize from '../Localize'; + +function revealVirtualCardDetails(cardID: string): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects('RevealVirtualCardDetails', {cardID}) + .then((response) => { + if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { + reject(response.message || Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); + return; + } + resolve(response); + }) + .catch((err) => { + reject(err.message); + }); + }); +} + +export default {revealVirtualCardDetails}; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 892aea0495da..4e94a1ecfe95 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useReducer} from 'react'; +import React, {useState} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -18,10 +18,7 @@ import styles from '../../../styles/styles'; import * as CardUtils from '../../../libs/CardUtils'; import Button from '../../../components/Button'; import CardDetails from './WalletPage/CardDetails'; -// eslint-disable-next-line rulesdir/no-api-in-views -import * as API from '../../../libs/API'; -import CONST from '../../../CONST'; -import * as revealCardDetailsUtils from './revealCardDetailsUtils'; +import Card from '../../../libs/actions/Card'; const propTypes = { /* Onyx Props */ @@ -51,7 +48,10 @@ function ExpensifyCardPage({ const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; - const [{isLoading, details, error}, dispatch] = useReducer(revealCardDetailsUtils.reducer, revealCardDetailsUtils.initialState); + // card details state + const [isLoading, setIsLoading] = useState(false); + const [details, setDetails] = useState({}); + const [errorMessage, setErrorMessage] = useState(''); if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { return ; @@ -60,19 +60,15 @@ function ExpensifyCardPage({ const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); const handleRevealDetails = () => { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.start}); - // eslint-disable-next-line rulesdir/no-api-in-views,rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealVirtualCardDetails', {cardID: virtualCard.cardID}) - .then((response) => { - if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.FAIL, payload: response.message}); - return; - } - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.SUCCESS, payload: response}); - }) - .catch((err) => { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.FAIL, payload: err.message}); - }); + setIsLoading(true); + // We can't store the response in Onyx for security reasons. + // That is this action is handled manually and the response is stored in a local state + // Hence the eslint disable here. + // eslint-disable-next-line rulesdir/no-thenable-actions-in-views + Card.revealVirtualCardDetails(virtualCard.cardID) + .then(setDetails) + .catch(setErrorMessage) + .finally(() => setIsLoading(false)); }; return ( @@ -113,7 +109,7 @@ function ExpensifyCardPage({ interactive={false} titleStyle={styles.walletCardNumber} shouldShowRightComponent - error={error} + error={errorMessage} rightComponent={