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(),
+ };
+}