diff --git a/android/app/build.gradle b/android/app/build.gradle index ef3d9dbf8bd6..6dc1f7b011a4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001039614 - versionName "1.3.96-14" + versionCode 1001039701 + versionName "1.3.97-1" } flavorDimensions "default" diff --git a/docs/redirects.csv b/docs/redirects.csv index d4fb7723bd0f..42e5bd005cae 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -1,2 +1,8 @@ sourceURL,targetURL https://community.expensify.com/discussion/5634/deep-dive-how-long-will-it-take-for-me-to-receive-my-reimbursement,https://help.expensify.com/articles/expensify-classic/get-paid-back/reports/Reimbursements +https://community.expensify.com/discussion/4925/how-to-dispute-an-expensify-card-transaction,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction#gsc.tab=0 +https://community.expensify.com/discussion/5184/faq-how-am-i-protected-from-fraud-using-the-expensify-card,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction#gsc.tab=0 +https://community.expensify.com/discussion/4887/deep-dive-understanding-your-expensify-card-statement,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements#gsc.tab=0 +https://community.expensify.com/discussion/4883/how-to-export-your-expensify-card-statement,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements#gsc.tab=0 +https://community.expensify.com/discussion/9528/how-to-understand-the-amount-owed-figure,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements#gsc.tab=0 +https://community.expensify.com/discussion/4806/how-to-pay-your-expensify-card-balance,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements#gsc.tab=0 diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f5385273e895..dffa6d42e2ed 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.96 + 1.3.97 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.96.14 + 1.3.97.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 6d2707b5aca6..f69808a8fff5 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.96 + 1.3.97 CFBundleSignature ???? CFBundleVersion - 1.3.96.14 + 1.3.97.1 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 97143f53b867..d94e36b0b3c9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -234,8 +234,8 @@ PODS: - libwebp/demux - libwebp/webp (1.2.4) - lottie-ios (4.3.3) - - lottie-react-native (6.3.1): - - lottie-ios (~> 4.3.0) + - lottie-react-native (6.4.0): + - lottie-ios (~> 4.3.3) - React-Core - MapboxCommon (23.6.0) - MapboxCoreMaps (10.14.0): @@ -1203,7 +1203,7 @@ SPEC CHECKSUMS: libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef lottie-ios: 25e7b2675dad5c3ddad369ac9baab03560c5bfdd - lottie-react-native: c9f1db4f4124dcce9f8159e65d8dc6e8bcb11fb4 + lottie-react-native: 3a3084faddd3891c276f23fd6e797b83f2021bbc MapboxCommon: 4a0251dd470ee37e7fadda8e285c01921a5e1eb0 MapboxCoreMaps: eb07203bbb0b1509395db5ab89cd3ad6c2e3c04c MapboxMaps: af50ec61a7eb3b032c3f7962c6bd671d93d2a209 diff --git a/package-lock.json b/package-lock.json index fda16afb2712..6383425bd013 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.96-14", + "version": "1.3.97-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.96-14", + "version": "1.3.97-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -95,7 +95,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.111", + "react-native-onyx": "1.0.115", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -143,6 +143,7 @@ "@dword-design/eslint-plugin-import-alias": "^4.0.8", "@electron/notarize": "^2.1.0", "@jest/globals": "^29.5.0", + "@ngneat/falso": "^7.1.1", "@octokit/core": "4.0.4", "@octokit/plugin-paginate-rest": "3.1.0", "@octokit/plugin-throttling": "4.1.0", @@ -5602,6 +5603,16 @@ "@types/react-native": "*" } }, + "node_modules/@ngneat/falso": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@ngneat/falso/-/falso-7.1.1.tgz", + "integrity": "sha512-/5HuJDaZHXl3WVdgvYBAM52OSYbSKfiNazVOZOw/3KjeZ6dQW9F0QCG+W6z52lUu5MZvp/TkPGaVRtoz6h9T1w==", + "dev": true, + "dependencies": { + "seedrandom": "3.0.5", + "uuid": "8.3.2" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -44331,13 +44342,13 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.111", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.111.tgz", - "integrity": "sha512-6drd5Grhkyq4oyt2+Bu6t7JYK5tqaARc0YP7taEHK9jLbhjdC4E9MPLJR2FVXiORkQCPOoyy1Gqmb4AUVIsvxg==", + "version": "1.0.115", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.115.tgz", + "integrity": "sha512-uPrJcw3Ta/EFL3Mh3iUggZ7EeEwLTSSSc5iUkKAA+a9Y8kBo8+6MWup9VCM/4wgysZbf3VHUGJCWQ8H3vWKgUg==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", - "underscore": "^1.13.1" + "underscore": "^1.13.6" }, "engines": { "node": ">=16.15.1 <=18.17.1", @@ -56665,6 +56676,16 @@ "csstype": "^3.0.8" } }, + "@ngneat/falso": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@ngneat/falso/-/falso-7.1.1.tgz", + "integrity": "sha512-/5HuJDaZHXl3WVdgvYBAM52OSYbSKfiNazVOZOw/3KjeZ6dQW9F0QCG+W6z52lUu5MZvp/TkPGaVRtoz6h9T1w==", + "dev": true, + "requires": { + "seedrandom": "3.0.5", + "uuid": "8.3.2" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -84623,13 +84644,13 @@ } }, "react-native-onyx": { - "version": "1.0.111", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.111.tgz", - "integrity": "sha512-6drd5Grhkyq4oyt2+Bu6t7JYK5tqaARc0YP7taEHK9jLbhjdC4E9MPLJR2FVXiORkQCPOoyy1Gqmb4AUVIsvxg==", + "version": "1.0.115", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.115.tgz", + "integrity": "sha512-uPrJcw3Ta/EFL3Mh3iUggZ7EeEwLTSSSc5iUkKAA+a9Y8kBo8+6MWup9VCM/4wgysZbf3VHUGJCWQ8H3vWKgUg==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", - "underscore": "^1.13.1" + "underscore": "^1.13.6" } }, "react-native-pager-view": { diff --git a/package.json b/package.json index ee4e0a273aa4..8f8fac687317 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.96-14", + "version": "1.3.97-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -144,7 +144,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.111", + "react-native-onyx": "1.0.115", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -192,6 +192,7 @@ "@dword-design/eslint-plugin-import-alias": "^4.0.8", "@electron/notarize": "^2.1.0", "@jest/globals": "^29.5.0", + "@ngneat/falso": "^7.1.1", "@octokit/core": "4.0.4", "@octokit/plugin-paginate-rest": "3.1.0", "@octokit/plugin-throttling": "4.1.0", diff --git a/src/App.js b/src/App.js index bff766c1235f..698dfe4437b2 100644 --- a/src/App.js +++ b/src/App.js @@ -24,7 +24,6 @@ import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import * as Session from './libs/actions/Session'; import * as Environment from './libs/Environment/Environment'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; -import {SidebarNavigationContextProvider} from './pages/home/sidebar/SidebarNavigationContext'; import ThemeProvider from './styles/themes/ThemeProvider'; import ThemeStylesProvider from './styles/ThemeStylesProvider'; @@ -65,7 +64,6 @@ function App() { EnvironmentProvider, ThemeProvider, ThemeStylesProvider, - SidebarNavigationContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 8cb2bb6c8893..ce9329d909ae 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2782,12 +2782,10 @@ const CONST = { DEFAULT_COORDINATE: [-122.4021, 37.7911], STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq', }, - ONYX_UPDATE_TYPES: { HTTPS: 'https', PUSHER: 'pusher', }, - EVENTS: { SCROLLING: 'scrolling', }, @@ -2806,13 +2804,6 @@ const CONST = { FOOTER: 'footer', }, - GLOBAL_NAVIGATION_OPTION: { - HOME: 'home', - CHATS: 'chats', - SPEND: 'spend', - WORKSPACES: 'workspaces', - }, - MISSING_TRANSLATION: 'MISSING TRANSLATION', SEARCH_MAX_LENGTH: 500, diff --git a/src/GLOBAL_NAVIGATION_MAPPING.ts b/src/GLOBAL_NAVIGATION_MAPPING.ts deleted file mode 100644 index f879c508ff31..000000000000 --- a/src/GLOBAL_NAVIGATION_MAPPING.ts +++ /dev/null @@ -1,9 +0,0 @@ -import CONST from './CONST'; -import SCREENS from './SCREENS'; - -export default { - [CONST.GLOBAL_NAVIGATION_OPTION.HOME]: [SCREENS.HOME_OLDDOT], - [CONST.GLOBAL_NAVIGATION_OPTION.CHATS]: [SCREENS.REPORT], - [CONST.GLOBAL_NAVIGATION_OPTION.SPEND]: [SCREENS.EXPENSES_OLDDOT, SCREENS.REPORTS_OLDDOT, SCREENS.INSIGHTS_OLDDOT], - [CONST.GLOBAL_NAVIGATION_OPTION.WORKSPACES]: [SCREENS.INDIVIDUAL_WORKSPACES_OLDDOT, SCREENS.GROUPS_WORKSPACES_OLDDOT, SCREENS.CARDS_AND_DOMAINS_OLDDOT], -} as const; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 864e8934ad88..ed9cc6ae987c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -363,17 +363,4 @@ export default { SAASTR: 'saastr', SBE: 'sbe', MONEY2020: 'money2020', - - // Iframe screens from olddot - HOME_OLDDOT: 'home', - - // Spend tab - EXPENSES_OLDDOT: 'expenses', - REPORTS_OLDDOT: 'reports', - INSIGHTS_OLDDOT: 'insights', - - // Workspaces tab - INDIVIDUALS_OLDDOT: 'individual_workspaces', - GROUPS_OLDDOT: 'group_workspaces', - CARDS_AND_DOMAINS_OLDDOT: 'cards-and-domains', } as const; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8ef787edec2e..f7de8cfab4b6 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,17 +27,4 @@ export default { SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', SAML_SIGN_IN: 'SAMLSignIn', - - // Iframe screens from olddot - HOME_OLDDOT: 'Home_OLDDOT', - - // Spend tab - EXPENSES_OLDDOT: 'Expenses_OLDDOT', - REPORTS_OLDDOT: 'Reports_OLDDOT', - INSIGHTS_OLDDOT: 'Insights_OLDDOT', - - // Workspaces tab - INDIVIDUAL_WORKSPACES_OLDDOT: 'IndividualWorkspaces_OLDDOT', - GROUPS_WORKSPACES_OLDDOT: 'GroupWorkspaces_OLDDOT', - CARDS_AND_DOMAINS_OLDDOT: 'CardsAndDomains_OLDDOT', } as const; diff --git a/src/components/EnvironmentBadge.js b/src/components/EnvironmentBadge.js index a9236fc50abe..674eaa5c2840 100644 --- a/src/components/EnvironmentBadge.js +++ b/src/components/EnvironmentBadge.js @@ -28,7 +28,7 @@ function EnvironmentBadge() { success={environment === CONST.ENVIRONMENT.STAGING || environment === CONST.ENVIRONMENT.ADHOC} error={environment !== CONST.ENVIRONMENT.STAGING && environment !== CONST.ENVIRONMENT.ADHOC} text={text} - badgeStyles={[styles.alignSelfEnd, styles.headerEnvBadge, styles.ml1]} + badgeStyles={[styles.alignSelfEnd, styles.headerEnvBadge]} textStyles={[styles.headerEnvBadgeText]} environment={environment} /> diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index d8a5a0256e62..c0e01cab2954 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -4,7 +4,6 @@ import {Animated, Easing, View} from 'react-native'; import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; -import variables from '@styles/variables'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; @@ -101,8 +100,6 @@ class FloatingActionButton extends PureComponent { style={[styles.floatingActionButton, StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} > diff --git a/src/components/HTMLEngineProvider/htmlEngineUtils.js b/src/components/HTMLEngineProvider/htmlEngineUtils.js index 3c93873845b1..4495cb8ff136 100644 --- a/src/components/HTMLEngineProvider/htmlEngineUtils.js +++ b/src/components/HTMLEngineProvider/htmlEngineUtils.js @@ -1,3 +1,5 @@ +import lodashGet from 'lodash/get'; + const MAX_IMG_DIMENSIONS = 512; /** @@ -52,7 +54,7 @@ function isChildOfNode(tnode, predicate) { * @returns {Boolean} */ function isChildOfComment(tnode) { - return isChildOfNode(tnode, (node) => isCommentTag(node.domNode.name)); + return isChildOfNode(tnode, (node) => isCommentTag(lodashGet(node, 'domNode.name', ''))); } /** @@ -62,7 +64,7 @@ function isChildOfComment(tnode) { * @returns {Boolean} */ function isChildOfH1(tnode) { - return isChildOfNode(tnode, (node) => node.domNode.name.toLowerCase() === 'h1'); + return isChildOfNode(tnode, (node) => lodashGet(node, 'domNode.name', '').toLowerCase() === 'h1'); } export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1}; diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 685b8763781d..eb5a7eb7a685 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -58,7 +58,7 @@ const propTypes = { }; const defaultProps = { - hoverStyle: styles.sidebarLinkHoverLHN, + hoverStyle: styles.sidebarLinkHover, viewMode: 'default', onSelectRow: () => {}, style: null, @@ -112,7 +112,7 @@ function OptionRowLHN(props) { : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); const hoveredBackgroundColor = props.hoverStyle && props.hoverStyle.backgroundColor ? props.hoverStyle.backgroundColor : themeColors.sidebar; - const focusedBackgroundColor = styles.sidebarLinkActiveLHN.backgroundColor; + const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; const defaultSubscriptSize = optionItem.isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; @@ -199,8 +199,8 @@ function OptionRowLHN(props) { styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween, - styles.sidebarLinkLHN, - styles.sidebarLinkInnerLHN, + styles.sidebarLink, + styles.sidebarLinkInner, StyleUtils.getBackgroundColorStyle(themeColors.sidebar), props.isFocused ? styles.sidebarLinkActive : null, (hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle : null, diff --git a/src/components/Lottie/Lottie.tsx b/src/components/Lottie/Lottie.tsx index 6ee3bb544ed7..80152399a6de 100644 --- a/src/components/Lottie/Lottie.tsx +++ b/src/components/Lottie/Lottie.tsx @@ -1,18 +1,21 @@ import LottieView, {LottieViewProps} from 'lottie-react-native'; import React, {forwardRef} from 'react'; +import {View} from 'react-native'; import styles from '@styles/styles'; const Lottie = forwardRef((props: LottieViewProps, ref) => { const aspectRatioStyle = styles.aspectRatioLottie(props.source); return ( - + + + ); }); diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index e3ba0dbd7c2f..f749807cbe2d 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -144,6 +144,9 @@ function BaseSelectionList({ // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member const [focusedIndex, setFocusedIndex] = useState(() => _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey)); + // initialFocusedIndex is needed only on component did mount event, don't need to update value + // eslint-disable-next-line react-hooks/exhaustive-deps + const initialFocusedIndex = useMemo(() => (focusedIndex > -1 ? focusedIndex : undefined), []); // Disable `Enter` shortcut if the active element is a button or checkbox const disableEnterShortcut = activeElement && [CONST.ACCESSIBILITY_ROLE.BUTTON, CONST.ACCESSIBILITY_ROLE.CHECKBOX].includes(activeElement.role); @@ -307,14 +310,9 @@ function BaseSelectionList({ /> ); }; - - const scrollToFocusedIndexOnFirstRender = useCallback(() => { - if (!firstLayoutRef.current) { - return; - } - scrollToIndex(focusedIndex, false); + const handleFirstLayout = useCallback(() => { firstLayoutRef.current = false; - }, [focusedIndex, scrollToIndex]); + }, []); const updateAndScrollToFocusedIndex = useCallback( (newFocusedIndex) => { @@ -454,7 +452,8 @@ function BaseSelectionList({ viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}} testID="selection-list" style={[styles.flexGrow0]} - onLayout={scrollToFocusedIndexOnFirstRender} + onLayout={handleFirstLayout} + initialScrollIndex={initialFocusedIndex} /> {children} diff --git a/src/languages/en.ts b/src/languages/en.ts index d99b3c7d04d1..819382ee66b9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1907,7 +1907,4 @@ export default { guaranteed: 'Guaranteed eReceipt', transactionDate: 'Transaction date', }, - globalNavigationOptions: { - chats: 'Chats', - }, } satisfies TranslationBase; diff --git a/src/languages/es.ts b/src/languages/es.ts index dea7760a35ce..198d5d36b121 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2391,7 +2391,4 @@ export default { guaranteed: 'eRecibo garantizado', transactionDate: 'Fecha de transacción', }, - globalNavigationOptions: { - chats: 'Chats', // "Chats" is the accepted term colloqially in Spanish, this is not a bug!! - }, } satisfies EnglishTranslation; diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index de6c4a64237b..7564955020fa 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -8,7 +8,6 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import getStateFromPath from './getStateFromPath'; -import originalGetTopMostCentralPaneRouteName from './getTopMostCentralPaneRouteName'; import originalGetTopmostReportActionId from './getTopmostReportActionID'; import originalGetTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; @@ -47,9 +46,6 @@ function canNavigate(methodName, params = {}) { // Re-exporting the getTopmostReportId here to fill in default value for state. The getTopmostReportId isn't defined in this file to avoid cyclic dependencies. const getTopmostReportId = (state = navigationRef.getState()) => originalGetTopmostReportId(state); -// Re-exporting the getTopMostCentralPaneRouteName here to fill in default value for state. The getTopMostCentralPaneRouteName isn't defined in this file to avoid cyclic dependencies. -const getTopMostCentralPaneRouteName = (state = navigationRef.getState()) => originalGetTopMostCentralPaneRouteName(state); - // Re-exporting the getTopmostReportActionID here to fill in default value for state. The getTopmostReportActionID isn't defined in this file to avoid cyclic dependencies. const getTopmostReportActionId = (state = navigationRef.getState()) => originalGetTopmostReportActionId(state); @@ -284,7 +280,6 @@ export default { setIsNavigationReady, getTopmostReportId, getRouteNameFromStateEvent, - getTopMostCentralPaneRouteName, getTopmostReportActionId, }; diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index 3478f7b8ed8d..2373066cf4bd 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -1,13 +1,12 @@ import {DefaultTheme, getPathFromState, NavigationContainer} from '@react-navigation/native'; import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useRef} from 'react'; +import React, {useEffect, useRef} from 'react'; import {Easing, interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated'; import useCurrentReportID from '@hooks/useCurrentReportID'; import useFlipper from '@hooks/useFlipper'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Log from '@libs/Log'; import StatusBar from '@libs/StatusBar'; -import {SidebarNavigationContext} from '@pages/home/sidebar/SidebarNavigationContext'; import themeColors from '@styles/themes/default'; import AppNavigator from './AppNavigator'; import linkingConfig from './linkingConfig'; @@ -54,7 +53,6 @@ function parseAndLogRoute(state) { function NavigationRoot(props) { useFlipper(navigationRef); const firstRenderRef = useRef(true); - const globalNavigation = useContext(SidebarNavigationContext); const {updateCurrentReportID} = useCurrentReportID(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -130,9 +128,6 @@ function NavigationRoot(props) { }, 0); parseAndLogRoute(state); animateStatusBarBackgroundColor(); - - // Update the global navigation to show the correct selected menu items. - globalNavigation.updateFromNavigationState(state); }; return ( diff --git a/src/libs/Navigation/getTopMostCentralPaneRouteName.js b/src/libs/Navigation/getTopMostCentralPaneRouteName.js deleted file mode 100644 index f833575a397a..000000000000 --- a/src/libs/Navigation/getTopMostCentralPaneRouteName.js +++ /dev/null @@ -1,32 +0,0 @@ -import lodashFindLast from 'lodash/findLast'; - -/** - * Find the name of top most central pane route. - * - * @param {Object} state - The react-navigation state - * @returns {String | undefined} - It's possible that there is no central pane in the state. - */ -function getTopMostCentralPaneRouteName(state) { - if (!state) { - return undefined; - } - const topmostCentralPane = lodashFindLast(state.routes, (route) => route.name === 'CentralPaneNavigator'); - - if (!topmostCentralPane) { - return undefined; - } - - if (topmostCentralPane.state && topmostCentralPane.state.routes) { - // State may don't have index in some cases. But in this case there will be only one route in state. - return topmostCentralPane.state.routes[topmostCentralPane.state.index || 0].name; - } - - if (topmostCentralPane.params) { - // State may don't have inner state in some cases (e.g generating actions from path). But in this case there will be params available. - return topmostCentralPane.params.screen; - } - - return undefined; -} - -export default getTopMostCentralPaneRouteName; diff --git a/src/libs/Navigation/linkTo.js b/src/libs/Navigation/linkTo.js index e77d787ab4f8..286074914cf7 100644 --- a/src/libs/Navigation/linkTo.js +++ b/src/libs/Navigation/linkTo.js @@ -3,7 +3,6 @@ import _ from 'lodash'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import getStateFromPath from './getStateFromPath'; -import getTopMostCentralPaneRouteName from './getTopMostCentralPaneRouteName'; import getTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; @@ -62,15 +61,12 @@ export default function linkTo(navigation, path, type) { // If action type is different than NAVIGATE we can't change it to the PUSH safely if (action.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) { - // Make sure that we are pushing a screen that is not currently on top of the stack. - const shouldPushIfCentralPane = - action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && - (getTopMostCentralPaneRouteName(root.getState()) !== getTopMostCentralPaneRouteName(state) || getTopmostReportId(root.getState()) !== getTopmostReportId(state)); - // In case if type is 'FORCED_UP' we replace current screen with the provided. This means the current screen no longer exists in the stack if (type === CONST.NAVIGATION.TYPE.FORCED_UP) { action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; - } else if (shouldPushIfCentralPane) { + + // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack + } else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(root.getState()) !== getTopmostReportId(state)) { action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; // If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 22118f992591..ba95961e983b 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -482,7 +482,7 @@ function isChatThread(report) { * @returns {Boolean} */ function isDM(report) { - return !getChatType(report); + return isChatReport(report) && !getChatType(report); } /** @@ -3617,18 +3617,23 @@ function hasIOUWaitingOnCurrentUserBankAccount(chatReport) { * - in an open or submitted expense report tied to a policy expense chat the user owns * - employee can request money in submitted expense report only if the policy has Instant Submit settings turned on * - in an IOU report, which is not settled yet - * - in DM chat + * - in a 1:1 DM chat * * @param {Object} report - * @param {Array} participants + * @param {Array} otherParticipants * @returns {Boolean} */ -function canRequestMoney(report, participants) { - // User cannot request money in chat thread or in task report - if (isChatThread(report) || isTaskReport(report)) { +function canRequestMoney(report, otherParticipants) { + // User cannot request money in chat thread or in task report or in chat room + if (isChatThread(report) || isTaskReport(report) || isChatRoom(report)) { return false; } + // Users can only request money in DMs if they are a 1:1 DM + if (isDM(report)) { + return otherParticipants.length === 1; + } + // Prevent requesting money if pending IOU report waiting for their bank account already exists if (hasIOUWaitingOnCurrentUserBankAccount(report)) { return false; @@ -3641,7 +3646,7 @@ function canRequestMoney(report, participants) { } // In case there are no other participants than the current user and it's not user's own policy expense chat, they can't request money from such report - if (participants.length === 0 && !isOwnPolicyExpenseChat) { + if (otherParticipants.length === 0 && !isOwnPolicyExpenseChat) { return false; } @@ -3683,8 +3688,6 @@ function getMoneyRequestOptions(report, reportParticipants) { return []; } - const participants = _.filter(reportParticipants, (accountID) => currentUserPersonalDetails.accountID !== accountID); - // We don't allow IOU actions if an Expensify account is a participant of the report, unless the policy that the report is on is owned by an Expensify account const doParticipantsIncludeExpensifyAccounts = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; const isPolicyOwnedByExpensifyAccounts = report.policyID ? CONST.EXPENSIFY_ACCOUNT_IDS.includes(getPolicy(report.policyID).ownerAccountID || 0) : false; @@ -3692,30 +3695,29 @@ function getMoneyRequestOptions(report, reportParticipants) { return []; } - const hasSingleParticipantInReport = participants.length === 1; - const hasMultipleParticipants = participants.length > 1; + const otherParticipants = _.filter(reportParticipants, (accountID) => currentUserPersonalDetails.accountID !== accountID); + const hasSingleOtherParticipantInReport = otherParticipants.length === 1; + const hasMultipleOtherParticipants = otherParticipants.length > 1; + let options = []; // User created policy rooms and default rooms like #admins or #announce will always have the Split Bill option - // unless there are no participants at all (e.g. #admins room for a policy with only 1 admin) - // DM chats will have the Split Bill option only when there are at least 3 people in the chat. - // There is no Split Bill option for IOU or Expense reports which are threads - if ( - (isChatRoom(report) && participants.length > 0) || - (hasMultipleParticipants && !isPolicyExpenseChat(report) && !isMoneyRequestReport(report)) || - (isControlPolicyExpenseChat(report) && report.isOwnPolicyExpenseChat) - ) { - return [CONST.IOU.TYPE.SPLIT]; + // unless there are no other participants at all (e.g. #admins room for a policy with only 1 admin) + // DM chats will have the Split Bill option only when there are at least 2 other people in the chat. + // Your own workspace chats will have the split bill option. + if ((isChatRoom(report) && otherParticipants.length > 0) || (isDM(report) && hasMultipleOtherParticipants) || (isPolicyExpenseChat(report) && report.isOwnPolicyExpenseChat)) { + options = [CONST.IOU.TYPE.SPLIT]; } - // DM chats that only have 2 people will see the Send / Request money options. - // IOU and open or processing expense reports should show the Request option. - // Workspace chats should only see the Request money option or Split option in case of Control policies - return [ - ...(canRequestMoney(report, participants) ? [CONST.IOU.TYPE.REQUEST] : []), + if (canRequestMoney(report, otherParticipants)) { + options = [...options, CONST.IOU.TYPE.REQUEST]; + } - // Send money option should be visible only in DMs - ...(isChatReport(report) && !isPolicyExpenseChat(report) && hasSingleParticipantInReport ? [CONST.IOU.TYPE.SEND] : []), - ]; + // Send money option should be visible only in 1:1 DMs + if (isDM(report) && hasSingleOtherParticipantInReport) { + options = [...options, CONST.IOU.TYPE.SEND]; + } + + return options; } /** diff --git a/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js b/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js deleted file mode 100644 index 5c28681a6cfa..000000000000 --- a/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js +++ /dev/null @@ -1,64 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import Icon from '@components/Icon'; -import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import Text from '@components/Text'; -import styles from '@styles/styles'; -import * as StyleUtils from '@styles/StyleUtils'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; - -const propTypes = { - /** Icon to display */ - icon: PropTypes.elementType, - - /** Text to display for the item */ - title: PropTypes.string, - - /** Function to fire when component is pressed */ - onPress: PropTypes.func, - - /** Whether item is focused or active */ - isFocused: PropTypes.bool, -}; - -const defaultProps = { - icon: undefined, - isFocused: false, - onPress: () => {}, - title: '', -}; - -const GlobalNavigationMenuItem = React.forwardRef(({icon, title, isFocused, onPress}, ref) => ( - !isFocused && onPress()} - style={styles.globalNavigationItemContainer} - ref={ref} - role={CONST.ACCESSIBILITY_ROLE.MENUITEM} - accessibilityLabel={title} - > - {({pressed}) => ( - - - - - - {title} - - - - )} - -)); - -GlobalNavigationMenuItem.propTypes = propTypes; -GlobalNavigationMenuItem.defaultProps = defaultProps; -GlobalNavigationMenuItem.displayName = 'GlobalNavigationMenuItem'; - -export default GlobalNavigationMenuItem; diff --git a/src/pages/home/sidebar/GlobalNavigation/index.js b/src/pages/home/sidebar/GlobalNavigation/index.js deleted file mode 100644 index 569ebf6fd0a8..000000000000 --- a/src/pages/home/sidebar/GlobalNavigation/index.js +++ /dev/null @@ -1,51 +0,0 @@ -import React, {useContext, useMemo} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import * as Expensicons from '@components/Icon/Expensicons'; -import useLocalize from '@hooks/useLocalize'; -import Navigation from '@libs/Navigation/Navigation'; -import {SidebarNavigationContext} from '@pages/home/sidebar/SidebarNavigationContext'; -import SignInOrAvatarWithOptionalStatus from '@pages/home/sidebar/SignInOrAvatarWithOptionalStatus'; -import styles from '@styles/styles'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import GlobalNavigationMenuItem from './GlobalNavigationMenuItem'; - -function GlobalNavigation() { - const sidebarNavigation = useContext(SidebarNavigationContext); - const {translate} = useLocalize(); - const items = useMemo( - () => [ - { - icon: Expensicons.ChatBubble, - text: translate('globalNavigationOptions.chats'), - value: CONST.GLOBAL_NAVIGATION_OPTION.CHATS, - onSelected: () => { - Navigation.navigate(ROUTES.REPORT); - }, - }, - ], - [translate], - ); - - return ( - - - - {_.map(items, (item) => ( - item.onSelected(item.value)} - isFocused={sidebarNavigation.selectedGlobalNavigationOption === item.value} - /> - ))} - - - ); -} - -GlobalNavigation.displayName = 'GlobalNavigation'; - -export default GlobalNavigation; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 1f5a07194732..ad981a190a70 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -3,13 +3,13 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef} from 'react'; import {InteractionManager, View} from 'react-native'; import _ from 'underscore'; +import LogoComponent from '@assets/images/expensify-wordmark.svg'; import Header from '@components/Header'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -21,13 +21,19 @@ import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportA import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; +import defaultTheme from '@styles/themes/default'; +import variables from '@styles/variables'; import * as App from '@userActions/App'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus'; const basePropTypes = { + /** Toggles the navigation menu open and closed */ + onLinkClick: PropTypes.func.isRequired, + /** Safe area insets required for mobile devices margins */ insets: safeAreaInsetPropTypes.isRequired, }; @@ -143,12 +149,18 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority return (
{translate('globalNavigationOptions.chats')}} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + title={ + + } + accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} shouldShowEnvironmentBadge /> @@ -161,6 +173,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority + ); diff --git a/src/pages/home/sidebar/SidebarNavigationContext.js b/src/pages/home/sidebar/SidebarNavigationContext.js index d39c24178f8d..e69de29bb2d1 100644 --- a/src/pages/home/sidebar/SidebarNavigationContext.js +++ b/src/pages/home/sidebar/SidebarNavigationContext.js @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useState} from 'react'; -import _ from 'underscore'; -import Navigation from '@libs/Navigation/Navigation'; -import CONST from '@src/CONST'; -import GLOBAL_NAVIGATION_MAPPING from '@src/GLOBAL_NAVIGATION_MAPPING'; - -const propTypes = { - /** Children to wrap. The part of app that should have acces to this context */ - children: PropTypes.node.isRequired, -}; - -const SidebarNavigationContext = React.createContext({ - selectedGlobalNavigationOption: undefined, - selectedSubNavigationOption: undefined, - updateFromNavigationState: () => {}, -}); - -const mapSubNavigationOptionToGlobalNavigationOption = (SubNavigationOption) => - _.findKey(GLOBAL_NAVIGATION_MAPPING, (globalNavigationOptions) => globalNavigationOptions.includes(SubNavigationOption)); - -function SidebarNavigationContextProvider({children}) { - const [selectedGlobalNavigationOption, setSelectedGlobalNavigationOption] = useState(CONST.GLOBAL_NAVIGATION_OPTION.CHATS); - const [selectedSubNavigationOption, setSelectedSubNavigationOption] = useState(); - - const updateFromNavigationState = useCallback((navigationState) => { - const topmostCentralPaneRouteName = Navigation.getTopMostCentralPaneRouteName(navigationState); - if (!topmostCentralPaneRouteName) { - return; - } - - setSelectedSubNavigationOption(topmostCentralPaneRouteName); - setSelectedGlobalNavigationOption(mapSubNavigationOptionToGlobalNavigationOption(topmostCentralPaneRouteName)); - }, []); - - const contextValue = useMemo( - () => ({ - selectedGlobalNavigationOption, - selectedSubNavigationOption, - updateFromNavigationState, - }), - [selectedGlobalNavigationOption, selectedSubNavigationOption, updateFromNavigationState], - ); - - return {children}; -} - -SidebarNavigationContextProvider.propTypes = propTypes; - -export {SidebarNavigationContextProvider, SidebarNavigationContext}; diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js index cd8bff2e5945..0d2930220bcd 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js @@ -1,19 +1,13 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; import ScreenWrapper from '@components/ScreenWrapper'; import * as Browser from '@libs/Browser'; import Performance from '@libs/Performance'; -import GlobalNavigation from '@pages/home/sidebar/GlobalNavigation'; -import SubNavigation from '@pages/home/sidebar/SubNavigation/SubNavigation'; +import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import styles from '@styles/styles'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; - -const propTypes = { - /** Children to wrap (floating button). */ - children: PropTypes.node.isRequired, -}; +import sidebarPropTypes from './sidebarPropTypes'; /** * Function called when a pinned chat is selected. @@ -24,6 +18,11 @@ const startTimer = () => { }; function BaseSidebarScreen(props) { + useEffect(() => { + Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); + Timing.start(CONST.TIMING.SIDEBAR_LOADED, true); + }, []); + return ( {({insets}) => ( <> - - - + {props.children} @@ -47,7 +46,7 @@ function BaseSidebarScreen(props) { ); } -BaseSidebarScreen.propTypes = propTypes; +BaseSidebarScreen.propTypes = sidebarPropTypes; BaseSidebarScreen.displayName = 'BaseSidebarScreen'; export default BaseSidebarScreen; diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.js index 6f2fa1f9944a..0b4c520c78a2 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.js +++ b/src/pages/home/sidebar/SidebarScreen/index.js @@ -3,6 +3,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import FreezeWrapper from '@libs/Navigation/FreezeWrapper'; import BaseSidebarScreen from './BaseSidebarScreen'; import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover'; +import sidebarPropTypes from './sidebarPropTypes'; function SidebarScreen(props) { const popoverModal = useRef(null); @@ -48,6 +49,7 @@ function SidebarScreen(props) { ); } +SidebarScreen.propTypes = sidebarPropTypes; SidebarScreen.displayName = 'SidebarScreen'; export default SidebarScreen; diff --git a/src/pages/home/sidebar/SidebarScreen/index.native.js b/src/pages/home/sidebar/SidebarScreen/index.native.js index bb4d08d33ea7..36724c02d278 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.native.js +++ b/src/pages/home/sidebar/SidebarScreen/index.native.js @@ -3,6 +3,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import FreezeWrapper from '@libs/Navigation/FreezeWrapper'; import BaseSidebarScreen from './BaseSidebarScreen'; import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover'; +import sidebarPropTypes from './sidebarPropTypes'; function SidebarScreen(props) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -18,6 +19,7 @@ function SidebarScreen(props) { ); } +SidebarScreen.propTypes = sidebarPropTypes; SidebarScreen.displayName = 'SidebarScreen'; export default SidebarScreen; diff --git a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js new file mode 100644 index 000000000000..61a9194bb1e5 --- /dev/null +++ b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js @@ -0,0 +1,7 @@ +import PropTypes from 'prop-types'; + +const sidebarPropTypes = { + /** Callback when onLayout of sidebar is called */ + onLayout: PropTypes.func, +}; +export default sidebarPropTypes; diff --git a/src/pages/home/sidebar/SubNavigation/SubNavigation.js b/src/pages/home/sidebar/SubNavigation/SubNavigation.js deleted file mode 100644 index ceb1d40dca50..000000000000 --- a/src/pages/home/sidebar/SubNavigation/SubNavigation.js +++ /dev/null @@ -1,38 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useEffect} from 'react'; -import {View} from 'react-native'; -import Performance from '@libs/Performance'; -import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; -import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; -import styles from '@styles/styles'; -import Timing from '@userActions/Timing'; -import CONST from '@src/CONST'; - -const propTypes = { - /** Function called when a pinned chat is selected. */ - onLinkClick: PropTypes.func.isRequired, - - /** Insets for SidebarLInksData */ - insets: safeAreaInsetPropTypes.isRequired, -}; - -function SubNavigation({onLinkClick, insets}) { - useEffect(() => { - Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); - Timing.start(CONST.TIMING.SIDEBAR_LOADED, true); - }, []); - - return ( - - - - ); -} - -SubNavigation.propTypes = propTypes; -SubNavigation.displayName = 'SubNavigation'; - -export default SubNavigation; diff --git a/src/styles/getContextMenuItemStyles/index.js b/src/styles/getContextMenuItemStyles/index.js deleted file mode 100644 index 4116ac75ce05..000000000000 --- a/src/styles/getContextMenuItemStyles/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import styles from '@styles/styles'; -import variables from '@styles/variables'; - -export default (windowWidth) => { - if (windowWidth > variables.mobileResponsiveWidthBreakpoint) { - return [styles.popoverMenuItem, styles.contextMenuItemPopoverMaxWidth]; - } - return [styles.popoverMenuItem]; -}; diff --git a/src/styles/getContextMenuItemStyles/index.native.js b/src/styles/getContextMenuItemStyles/index.native.js deleted file mode 100644 index cbb048a68d2f..000000000000 --- a/src/styles/getContextMenuItemStyles/index.native.js +++ /dev/null @@ -1,3 +0,0 @@ -import styles from '@styles/styles'; - -export default () => [styles.popoverMenuItem]; diff --git a/src/styles/getContextMenuItemStyles/index.native.ts b/src/styles/getContextMenuItemStyles/index.native.ts new file mode 100644 index 000000000000..798e64fb348c --- /dev/null +++ b/src/styles/getContextMenuItemStyles/index.native.ts @@ -0,0 +1,6 @@ +import styles from '@styles/styles'; +import GetContextMenuItemStyle from './types'; + +const getContextMenuItemStyle: GetContextMenuItemStyle = () => [styles.popoverMenuItem]; + +export default getContextMenuItemStyle; diff --git a/src/styles/getContextMenuItemStyles/index.ts b/src/styles/getContextMenuItemStyles/index.ts new file mode 100644 index 000000000000..3f5a8049d31d --- /dev/null +++ b/src/styles/getContextMenuItemStyles/index.ts @@ -0,0 +1,12 @@ +import styles from '@styles/styles'; +import variables from '@styles/variables'; +import GetContextMenuItemStyle from './types'; + +const GetContextMenuItemStyles: GetContextMenuItemStyle = (windowWidth) => { + if (windowWidth && windowWidth > variables.mobileResponsiveWidthBreakpoint) { + return [styles.popoverMenuItem, styles.contextMenuItemPopoverMaxWidth]; + } + return [styles.popoverMenuItem]; +}; + +export default GetContextMenuItemStyles; diff --git a/src/styles/getContextMenuItemStyles/types.ts b/src/styles/getContextMenuItemStyles/types.ts new file mode 100644 index 000000000000..102f63575bd9 --- /dev/null +++ b/src/styles/getContextMenuItemStyles/types.ts @@ -0,0 +1,5 @@ +import {ViewStyle} from 'react-native'; + +type GetContextMenuItemStyle = (windowWidth?: number) => ViewStyle[]; + +export default GetContextMenuItemStyle; diff --git a/src/styles/styles.ts b/src/styles/styles.ts index d3c026f4487a..1e2a148c758d 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -1352,7 +1352,7 @@ const styles = (theme: ThemeColors) => floatingActionButtonContainer: { position: 'absolute', - left: 16, + right: 20, // The bottom of the floating action button should align with the bottom of the compose box. // The value should be equal to the height + marginBottom + marginTop of chatItemComposeSecondaryRow @@ -3917,34 +3917,6 @@ const styles = (theme: ThemeColors) => marginBottom: 16, }, - globalNavigation: { - width: variables.globalNavigationWidth, - backgroundColor: theme.highlightBG, - }, - - globalNavigationMenuContainer: { - marginTop: 13, - }, - - globalAndSubNavigationContainer: { - backgroundColor: theme.highlightBG, - }, - - globalNavigationSelectionIndicator: (isFocused: boolean) => ({ - width: 4, - height: 52, - borderTopRightRadius: variables.componentBorderRadiusRounded, - borderBottomRightRadius: variables.componentBorderRadiusRounded, - backgroundColor: isFocused ? theme.iconMenu : theme.transparent, - }), - - globalNavigationMenuItem: (isFocused: boolean) => (isFocused ? {color: theme.text, fontWeight: fontWeightBold, fontFamily: fontFamily.EXP_NEUE_BOLD} : {color: theme.icon}), - - globalNavigationItemContainer: { - width: variables.globalNavigationWidth, - height: variables.globalNavigationWidth, - }, - walletCard: { borderRadius: variables.componentBorderRadiusLarge, position: 'relative', diff --git a/src/styles/themes/default.ts b/src/styles/themes/default.ts index dd92b1ce71d9..db54da7f35ac 100644 --- a/src/styles/themes/default.ts +++ b/src/styles/themes/default.ts @@ -43,7 +43,7 @@ const darkTheme = { hoverComponentBG: colors.darkHighlightBackground, activeComponentBG: colors.darkBorders, signInSidebar: colors.green800, - sidebar: colors.darkAppBackground, + sidebar: colors.darkHighlightBackground, sidebarHover: colors.darkAppBackground, heading: colors.darkPrimaryText, textLight: colors.darkPrimaryText, diff --git a/src/styles/themes/light.ts b/src/styles/themes/light.ts index 97fe2322945a..d2655689d90c 100644 --- a/src/styles/themes/light.ts +++ b/src/styles/themes/light.ts @@ -43,7 +43,7 @@ const lightTheme = { hoverComponentBG: colors.lightHighlightBackground, activeComponentBG: colors.lightBorders, signInSidebar: colors.green800, - sidebar: colors.lightAppBackground, + sidebar: colors.lightHighlightBackground, sidebarHover: colors.lightBorders, heading: colors.lightPrimaryText, textLight: colors.white, diff --git a/src/styles/utilities/spacing.ts b/src/styles/utilities/spacing.ts index f88692bcc85b..7d568847ab65 100644 --- a/src/styles/utilities/spacing.ts +++ b/src/styles/utilities/spacing.ts @@ -477,10 +477,6 @@ export default { paddingTop: 20, }, - pt6: { - paddingTop: 24, - }, - pt8: { paddingTop: 32, }, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 7bad3b1b0fb7..18800f5748d9 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -83,8 +83,7 @@ export default { mobileResponsiveWidthBreakpoint: 800, tabletResponsiveWidthBreakpoint: 1024, safeInsertPercentage: 0.7, - globalNavigationWidth: 72, - sideBarWidth: 303 + 72, + sideBarWidth: 375, pdfPageMaxWidth: 992, tooltipzIndex: 10050, gutterWidth: 12, diff --git a/tests/README.md b/tests/README.md index dd5b5fc1635f..6170006cb9bb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -15,6 +15,36 @@ - To simulate a network request succeeding or failing we can mock the expected response first and then manually trigger the action that calls that API command. - [Mocking the response of `HttpUtils.xhr()`](https://github.com/Expensify/App/blob/ca2fa88a5789b82463d35eddc3d57f70a7286868/tests/actions/SessionTest.js#L25-L32) is the best way to simulate various API conditions so we can verify whether a result occurs or not. +## Mocking collections / collection items + +When unit testing an interface with Jest/performance testing with Reassure you might need to work with collections of data. These often get tricky to generate and maintain. To help with this we have a few helper methods located in `tests/utils/collections/`. + +- `createCollection()` - Creates a collection of data (`Record`) with a given number of items (default=500). This is useful for eg. testing the performance of a component with a large number of items. You can use it to populate Onyx. +- `createRandom*()` - like `createRandomPolicy`, these functions are responsible for generating a randomised object of the given type. You can use them as your defaults when calling `createCollection()` or as standalone utilities. + +Basic example: +```ts +const policies = createCollection(item => `policies_${item.id}`, createRandomPolicy); + +/** + Output: + { + "policies_0": policyItem0, + "policies_1": policyItem1, + ... + } +*/ +``` + +Example with overrides: + +```ts +const policies = createCollection( + item => `policies_${item.id}`, + index => ({ ...createRandomPolicy(index), isPinned: true }) +); +``` + ## Mocking `node_modules`, user modules, and what belongs in `jest/setup.js` If you need to mock a library that exists in `node_modules` then add it to the `__mocks__` folder in the root of the project. More information about this [here](https://jestjs.io/docs/manual-mocks#mocking-node-modules). If you need to mock an individual library you should create a mock module in a `__mocks__` subdirectory adjacent to the library as explained [here](https://jestjs.io/docs/manual-mocks#mocking-user-modules). However, keep in mind that when you do this you also must manually require the mock by calling something like `jest.mock('../../src/libs/Log');` at the top of an individual test file. If every test in the app will need something to be mocked that's a good case for adding it to `jest/setup.js`, but we should generally avoid adding user mocks or `node_modules` mocks to this file. Please use the `__mocks__` subdirectories wherever appropriate. diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index a29b2727c847..e0c98b1793f1 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -452,7 +452,7 @@ describe('ReportUtils', () => { expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); - it("it's a group chat report", () => { + it("it's a group DM report", () => { const report = { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.CHAT, @@ -465,17 +465,6 @@ describe('ReportUtils', () => { }); describe('return only money request option if', () => { - it("it is user's own policy expense chat", () => { - const report = { - ...LHNTestUtils.getFakeReport(), - chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - isOwnPolicyExpenseChat: true, - }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, ...participantsAccountIDs], [CONST.BETAS.IOU_SEND]); - expect(moneyRequestOptions.length).toBe(1); - expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); - }); - it("it is an expense report tied to user's own policy expense chat", () => { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, { reportID: '101', @@ -520,15 +509,29 @@ describe('ReportUtils', () => { }); }); - it('return both iou send and request money in DM', () => { - const report = { - ...LHNTestUtils.getFakeReport(), - type: CONST.REPORT.TYPE.CHAT, - }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, participantsAccountIDs[0]], [CONST.BETAS.IOU_SEND]); - expect(moneyRequestOptions.length).toBe(2); - expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); - expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND)).toBe(true); + describe('return multiple money request option if', () => { + it("it is user's own policy expense chat", () => { + const report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + isOwnPolicyExpenseChat: true, + }; + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, ...participantsAccountIDs], [CONST.BETAS.IOU_SEND]); + expect(moneyRequestOptions.length).toBe(2); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); + }); + + it('it is a 1:1 DM', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + type: CONST.REPORT.TYPE.CHAT, + }; + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, participantsAccountIDs[0]], [CONST.BETAS.IOU_SEND]); + expect(moneyRequestOptions.length).toBe(2); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND)).toBe(true); + }); }); }); diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 72d7d64f1283..546853b8893b 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -306,6 +306,7 @@ function MockedSidebarLinks({currentReportID}) { return ( {}} insets={{ top: 0, left: 0, @@ -314,7 +315,6 @@ function MockedSidebarLinks({currentReportID}) { }} isSmallScreenWidth={false} currentReportID={currentReportID} - onLinkClick={() => {}} /> ); diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts new file mode 100644 index 000000000000..565957b7649e --- /dev/null +++ b/tests/utils/collections/createCollection.ts @@ -0,0 +1,11 @@ +export default function createCollection(createKey: (item: T, index: number) => string, createItem: (index: number) => T, length = 500): Record { + const map: Record = {}; + + for (let i = 0; i < length; i++) { + const item = createItem(i); + const itemKey = createKey(item, i); + map[itemKey] = item; + } + + return map; +} diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts new file mode 100644 index 000000000000..266c8bba2d72 --- /dev/null +++ b/tests/utils/collections/policies.ts @@ -0,0 +1,26 @@ +import {rand, randAvatar, randBoolean, randCurrencyCode, randEmail, randPastDate, randWord} from '@ngneat/falso'; +import CONST from '@src/CONST'; +import type {Policy} from '@src/types/onyx'; + +export default function createRandomPolicy(index: number): Policy { + return { + id: index.toString(), + name: randWord(), + type: rand(Object.values(CONST.POLICY.TYPE)), + areChatRoomsEnabled: randBoolean(), + autoReporting: randBoolean(), + isPolicyExpenseChatEnabled: randBoolean(), + autoReportingFrequency: rand(Object.values(CONST.POLICY.AUTO_REPORTING_FREQUENCIES)), + outputCurrency: randCurrencyCode(), + role: rand(Object.values(CONST.POLICY.ROLE)), + owner: randEmail(), + ownerAccountID: index, + avatar: randAvatar(), + isFromFullPolicy: randBoolean(), + lastModified: randPastDate().toISOString(), + pendingAction: rand(Object.values(CONST.RED_BRICK_ROAD_PENDING_ACTION)), + errors: {}, + customUnits: {}, + errorFields: {}, + }; +} diff --git a/tests/utils/collections/reports.ts b/tests/utils/collections/reports.ts new file mode 100644 index 000000000000..a52df9e1df41 --- /dev/null +++ b/tests/utils/collections/reports.ts @@ -0,0 +1,22 @@ +import {rand, randBoolean, randCurrencyCode, randEmail, randWord} from '@ngneat/falso'; +import CONST from '@src/CONST'; +import type {Report} from '@src/types/onyx'; + +export default function createRandomReport(index: number): Report { + return { + reportID: index.toString(), + chatType: rand(Object.values(CONST.REPORT.CHAT_TYPE)), + currency: randCurrencyCode(), + displayName: randWord(), + hasDraft: randBoolean(), + ownerEmail: randEmail(), + ownerAccountID: index, + isPinned: randBoolean(), + isOptimisticReport: randBoolean(), + isOwnPolicyExpenseChat: randBoolean(), + isWaitingOnBankAccount: randBoolean(), + isLastMessageDeletedParentAction: randBoolean(), + policyID: index.toString(), + reportName: randWord(), + }; +}