diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 755ab6dbaa60..6ded44d7059f 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -354,6 +354,6 @@ jobs:
IOS: ${{ needs.iOS.result }}
WEB: ${{ needs.web.result }}
ANDROID_LINK: ${{steps.get_android_path.outputs.android_path}}
- DESKTOP_LINK: https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/${{ env.PULL_REQUEST_NUMBER }}/NewExpensifyAdHoc.dmg
+ DESKTOP_LINK: https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/${{ env.PULL_REQUEST_NUMBER }}/NewExpensify.dmg
IOS_LINK: ${{steps.get_ios_path.outputs.ios_path}}
WEB_LINK: https://${{ env.PULL_REQUEST_NUMBER }}.pr-testing.expensify.com
diff --git a/.storybook/theme.js b/.storybook/theme.js
index 0867f6a830b5..96631764726f 100644
--- a/.storybook/theme.js
+++ b/.storybook/theme.js
@@ -7,17 +7,17 @@ export default create({
fontBase: 'ExpensifyNeue-Regular',
fontCode: 'monospace',
base: 'dark',
- appBg: colors.greenHighlightBackground,
- colorPrimary: colors.greenDefaultButton,
+ appBg: colors.darkHighlightBackground,
+ colorPrimary: colors.darkDefaultButton,
colorSecondary: colors.green,
- appContentBg: colors.greenAppBackground,
- textColor: colors.white,
- barTextColor: colors.white,
+ appContentBg: colors.darkAppBackground,
+ textColor: colors.darkPrimaryText,
+ barTextColor: colors.darkPrimaryText,
barSelectedColor: colors.green,
- barBg: colors.greenAppBackground,
- appBorderColor: colors.greenBorders,
- inputBg: colors.greenHighlightBackground,
- inputBorder: colors.greenBorders,
+ barBg: colors.darkAppBackground,
+ appBorderColor: colors.darkBorders,
+ inputBg: colors.darkHighlightBackground,
+ inputBorder: colors.darkBorders,
appBorderRadius: 8,
inputBorderRadius: 8,
});
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 1867a8cf85d2..d9eb60471773 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 1001037002
- versionName "1.3.70-2"
+ versionCode 1001037005
+ versionName "1.3.70-5"
}
flavorDimensions "default"
diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.js
index a5478dbd8f78..da87c93ee367 100644
--- a/config/electronBuilder.config.js
+++ b/config/electronBuilder.config.js
@@ -21,24 +21,6 @@ const macIcon = {
adhoc: './desktop/icon-adhoc.png',
};
-const appIds = {
- production: 'com.expensifyreactnative.chat',
- staging: 'com.expensifyreactnative.dev.chat',
- adhoc: 'com.expensifyreactnative.adhoc.chat',
-};
-
-const productNames = {
- production: 'New Expensify',
- staging: 'New Expensify Dev',
- adhoc: 'New Expensify AdHoc',
-};
-
-const artifactNames = {
- production: 'NewExpensify.dmg',
- staging: 'NewExpensifyDev.dmg',
- adhoc: 'NewExpensifyAdHoc.dmg',
-};
-
const isCorrectElectronEnv = ['production', 'staging', 'adhoc'].includes(process.env.ELECTRON_ENV);
if (!isCorrectElectronEnv) {
@@ -50,8 +32,8 @@ if (!isCorrectElectronEnv) {
* It can be used to create local builds of the same, by omitting the `--publish` flag
*/
module.exports = {
- appId: appIds[process.env.ELECTRON_ENV],
- productName: productNames[process.env.ELECTRON_ENV],
+ appId: 'com.expensifyreactnative.chat',
+ productName: 'New Expensify',
extraMetadata: {
version,
},
@@ -64,8 +46,8 @@ module.exports = {
type: 'distribution',
},
dmg: {
- title: productNames[process.env.ELECTRON_ENV],
- artifactName: artifactNames[process.env.ELECTRON_ENV],
+ title: 'New Expensify',
+ artifactName: 'NewExpensify.dmg',
internetEnabled: true,
},
publish: [
@@ -83,7 +65,7 @@ module.exports = {
output: 'desktop-build',
},
protocols: {
- name: productNames[process.env.ELECTRON_ENV],
+ name: 'New Expensify',
schemes: ['new-expensify'],
},
};
diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js
index 01ebb00b288c..f0f335536c20 100644
--- a/docs/assets/js/main.js
+++ b/docs/assets/js/main.js
@@ -206,13 +206,6 @@ window.addEventListener('DOMContentLoaded', () => {
// If there is a fixed article scroll container, set to calculate titles' offset
scrollContainer: 'content-area',
-
- // onclick function to apply to all links in toc. will be called with
- // the event as the first parameter, and this can be used to stop,
- // propagation, prevent default or perform action
- onClick() {
- toggleHeaderMenu();
- },
});
}
@@ -226,6 +219,18 @@ window.addEventListener('DOMContentLoaded', () => {
const articleContent = document.getElementById('article-content');
const lhnContent = document.getElementById('lhn-content');
+
+ // This event listener checks if a link clicked in the LHN points to some section of the same page and toggles
+ // the LHN menu in responsive view.
+ lhnContent.addEventListener('click', (event) => {
+ const clickedLink = event.target;
+ if (clickedLink) {
+ const href = clickedLink.getAttribute('href');
+ if (href && href.startsWith('#') && !!document.getElementById(href.slice(1))) {
+ toggleHeaderMenu();
+ }
+ }
+ });
lhnContent.addEventListener('wheel', (e) => {
const scrollTop = lhnContent.scrollTop;
const isScrollingPastLHNTop = e.deltaY < 0 && scrollTop === 0;
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 9e4501eddea5..440e48e0c83d 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.70.2
+ 1.3.70.5
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index fd93684a1da3..9d7fc804acd9 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.70.2
+ 1.3.70.5
diff --git a/package-lock.json b/package-lock.json
index f36d0e88f52b..3e08356a76ef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.70-2",
+ "version": "1.3.70-5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.70-2",
+ "version": "1.3.70-5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -65,7 +65,7 @@
"patch-package": "^8.0.0",
"process": "^0.11.10",
"prop-types": "^15.7.2",
- "pusher-js": "7.4.0",
+ "pusher-js": "8.3.0",
"react": "18.2.0",
"react-collapse": "^5.1.0",
"react-content-loader": "^6.1.0",
@@ -39745,10 +39745,10 @@
}
},
"node_modules/pusher-js": {
- "version": "7.4.0",
- "license": "MIT",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.3.0.tgz",
+ "integrity": "sha512-6GohP06WlVeomAQQe9qWh1IDzd3+InluWt+ZUOcecVK1SEQkg6a8uYVsvxSJm7cbccfmHhE0jDkmhKIhue8vmA==",
"dependencies": {
- "@types/node": "^14.14.31",
"tweetnacl": "^1.0.3"
}
},
@@ -39760,11 +39760,6 @@
"node": ">=4.2.4"
}
},
- "node_modules/pusher-js/node_modules/@types/node": {
- "version": "14.18.56",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.56.tgz",
- "integrity": "sha512-+k+57NVS9opgrEn5l9c0gvD1r6C+PtyhVE4BTnMMRwiEA8ZO8uFcs6Yy2sXIy0eC95ZurBtRSvhZiHXBysbl6w=="
- },
"node_modules/qrcode": {
"version": "1.5.3",
"license": "MIT",
@@ -75499,17 +75494,11 @@
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA=="
},
"pusher-js": {
- "version": "7.4.0",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.3.0.tgz",
+ "integrity": "sha512-6GohP06WlVeomAQQe9qWh1IDzd3+InluWt+ZUOcecVK1SEQkg6a8uYVsvxSJm7cbccfmHhE0jDkmhKIhue8vmA==",
"requires": {
- "@types/node": "^14.14.31",
"tweetnacl": "^1.0.3"
- },
- "dependencies": {
- "@types/node": {
- "version": "14.18.56",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.56.tgz",
- "integrity": "sha512-+k+57NVS9opgrEn5l9c0gvD1r6C+PtyhVE4BTnMMRwiEA8ZO8uFcs6Yy2sXIy0eC95ZurBtRSvhZiHXBysbl6w=="
- }
}
},
"pusher-js-mock": {
diff --git a/package.json b/package.json
index 315d01ea5dc3..221c3fa7cbf2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.70-2",
+ "version": "1.3.70-5",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -107,7 +107,7 @@
"patch-package": "^8.0.0",
"process": "^0.11.10",
"prop-types": "^15.7.2",
- "pusher-js": "7.4.0",
+ "pusher-js": "8.3.0",
"react": "18.2.0",
"react-collapse": "^5.1.0",
"react-content-loader": "^6.1.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index 1ef2f3e83246..93576e0ccf9d 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -293,8 +293,8 @@ const CONST = {
},
type: KEYBOARD_SHORTCUT_NAVIGATION_TYPE,
},
- NEW_GROUP: {
- descriptionKey: 'newGroup',
+ NEW_CHAT: {
+ descriptionKey: 'newChat',
shortcutKey: 'K',
modifiers: ['CTRL', 'SHIFT'],
trigger: {
@@ -1342,6 +1342,7 @@ const CONST = {
SETTINGS: 'settings',
LEAVE_ROOM: 'leaveRoom',
WELCOME_MESSAGE: 'welcomeMessage',
+ PRIVATE_NOTES: 'privateNotes',
},
EDIT_REQUEST_FIELD: {
AMOUNT: 'amount',
@@ -2624,6 +2625,9 @@ const CONST = {
DISABLED: 'DISABLED',
},
TAB: {
+ NEW_CHAT_TAB_ID: 'NewChatTab',
+ NEW_CHAT: 'chat',
+ NEW_ROOM: 'room',
RECEIPT_TAB_ID: 'ReceiptTab',
MANUAL: 'manual',
SCAN: 'scan',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 2e0b75910bae..80afc4d5ffee 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -290,6 +290,7 @@ const ONYXKEYS = {
SETTINGS_STATUS_SET_FORM: 'settingsStatusSetForm',
SETTINGS_STATUS_CLEAR_AFTER_FORM: 'settingsStatusClearAfterForm',
SETTINGS_STATUS_SET_CLEAR_AFTER_FORM: 'settingsStatusSetClearAfterForm',
+ PRIVATE_NOTES_FORM: 'privateNotesForm',
I_KNOW_A_TEACHER_FORM: 'iKnowTeacherForm',
INTRO_SCHOOL_PRINCIPAL_FORM: 'introSchoolPrincipalForm',
},
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 3bbdf4709cfc..9459708c893b 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -13,7 +13,6 @@ type ParseReportRouteParams = {
const REPORT = 'r';
const IOU_REQUEST = 'request/new';
-const IOU_BILL = 'split/new';
const IOU_SEND = 'send/new';
const NEW_TASK = 'new/task';
const SETTINGS_PERSONAL_DETAILS = 'settings/profile/personal-details';
@@ -67,11 +66,12 @@ export default {
SETTINGS_2FA: 'settings/security/two-factor-auth',
SETTINGS_STATUS,
SETTINGS_STATUS_SET,
- NEW_GROUP: 'new/group',
+ NEW: 'new',
NEW_CHAT: 'new/chat',
+ NEW_ROOM: 'new/room',
NEW_TASK,
REPORT,
- REPORT_WITH_ID: 'r/:reportID/:reportActionID?',
+ REPORT_WITH_ID: 'r/:reportID?/:reportActionID?',
EDIT_REQUEST: 'r/:threadReportID/edit/:field',
getEditRequestRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}`,
EDIT_CURRENCY_REQUEST: 'r/:threadReportID/edit/currency',
@@ -86,7 +86,6 @@ export default {
CONCIERGE: 'concierge',
IOU_REQUEST,
- IOU_BILL,
IOU_SEND,
// To see the available iouType, please refer to CONST.IOU.MONEY_REQUEST_TYPE
@@ -104,6 +103,7 @@ export default {
MONEY_REQUEST_SCAN_TAB: ':iouType/new/:reportID?/scan',
MONEY_REQUEST_DISTANCE_TAB: ':iouType/new/:reportID?/distance',
MONEY_REQUEST_WAYPOINT: ':iouType/new/waypoint/:waypointIndex',
+ MONEY_REQUEST_ADDRESS: ':iouType/new/address/:reportID?',
IOU_SEND_ADD_BANK_ACCOUNT: `${IOU_SEND}/add-bank-account`,
IOU_SEND_ADD_DEBIT_CARD: `${IOU_SEND}/add-debit-card`,
IOU_SEND_ENABLE_PAYMENTS: `${IOU_SEND}/enable-payments`,
@@ -118,6 +118,7 @@ export default {
getMoneyRequestMerchantRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}`,
getMoneyRequestDistanceTabRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance`,
getMoneyRequestWaypointRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}`,
+ getMoneyRequestAddressRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}`,
getMoneyRequestTagRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}`,
SPLIT_BILL_DETAILS: `r/:reportID/split/:reportActionID`,
getSplitBillDetailsRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}`,
@@ -172,6 +173,14 @@ export default {
GOOGLE_SIGN_IN: 'sign-in-with-google',
DESKTOP_SIGN_IN_REDIRECT: 'desktop-signin-redirect',
+ // Routes related to private notes added to the report
+ PRIVATE_NOTES_VIEW: 'r/:reportID/notes/:accountID',
+ getPrivateNotesViewRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}`,
+ PRIVATE_NOTES_LIST: 'r/:reportID/notes',
+ getPrivateNotesListRoute: (reportID: string) => `r/${reportID}/notes`,
+ PRIVATE_NOTES_EDIT: 'r/:reportID/notes/:accountID/edit',
+ getPrivateNotesEditRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit`,
+
// This is a special validation URL that will take the user to /workspace/new after validation. This is used
// when linking users from e.com in order to share a session in this app.
ENABLE_PAYMENTS: 'enable-payments',
diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js
index dbe7e46ff6aa..e2843ba7fae8 100644
--- a/src/components/AddPlaidBankAccount.js
+++ b/src/components/AddPlaidBankAccount.js
@@ -1,6 +1,7 @@
import _ from 'underscore';
-import React, {useEffect, useRef, useCallback} from 'react';
+import React, {useEffect, useRef, useCallback, useMemo} from 'react';
import {ActivityIndicator, View} from 'react-native';
+import {useIsFocused} from '@react-navigation/native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
@@ -38,6 +39,9 @@ const propTypes = {
/** Fired when the user exits the Plaid flow */
onExitPlaid: PropTypes.func,
+ /** Fired when the screen is blurred */
+ onBlurPlaid: PropTypes.func,
+
/** Fired when the user selects an account */
onSelect: PropTypes.func,
@@ -61,6 +65,7 @@ const defaultProps = {
selectedPlaidAccountID: '',
plaidLinkToken: '',
onExitPlaid: () => {},
+ onBlurPlaid: () => {},
onSelect: () => {},
text: '',
receivedRedirectURI: null,
@@ -75,6 +80,7 @@ function AddPlaidBankAccount({
selectedPlaidAccountID,
plaidLinkToken,
onExitPlaid,
+ onBlurPlaid,
onSelect,
text,
receivedRedirectURI,
@@ -88,6 +94,7 @@ function AddPlaidBankAccount({
const {translate} = useLocalize();
const {isOffline} = useNetwork();
+ const isFocused = useIsFocused();
/**
* @returns {String}
@@ -102,6 +109,11 @@ function AddPlaidBankAccount({
}
};
+ /**
+ * @returns {Array}
+ */
+ const plaidBankAccounts = useMemo(() => lodashGet(plaidData, 'bankAccounts') || [], [plaidData]);
+
/**
* @returns {Boolean}
* I'm using useCallback so the useEffect which uses this function doesn't run on every render.
@@ -151,6 +163,13 @@ function AddPlaidBankAccount({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ useEffect(() => {
+ if (isFocused || plaidBankAccounts.length) {
+ return;
+ }
+ onBlurPlaid();
+ }, [isFocused, onBlurPlaid, plaidBankAccounts.length]);
+
useEffect(() => {
// If we are coming back from offline and we haven't authenticated with Plaid yet, we need to re-run our call to kick off Plaid
// previousNetworkState.current also makes sure that this doesn't run on the first render.
@@ -160,7 +179,6 @@ function AddPlaidBankAccount({
previousNetworkState.current = isOffline;
}, [allowDebit, bankAccountID, isAuthenticatedWithPlaid, isOffline]);
- const plaidBankAccounts = lodashGet(plaidData, 'bankAccounts') || [];
const token = getPlaidLinkToken();
const options = _.map(plaidBankAccounts, (account) => ({
value: account.plaidAccountID,
diff --git a/src/components/AnimatedStep/index.js b/src/components/AnimatedStep/index.js
index a8b9b80fcc0e..5b0dc8bc78fa 100644
--- a/src/components/AnimatedStep/index.js
+++ b/src/components/AnimatedStep/index.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import * as Animatable from 'react-native-animatable';
import CONST from '../../CONST';
import styles from '../../styles/styles';
+import useNativeDriver from '../../libs/useNativeDriver';
const propTypes = {
/** Children to wrap in AnimatedStep. */
@@ -47,7 +48,7 @@ function AnimatedStep(props) {
}}
duration={CONST.ANIMATED_TRANSITION}
animation={getAnimationStyle(props.direction)}
- useNativeDriver
+ useNativeDriver={useNativeDriver}
style={props.style}
>
{props.children}
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index d39906faf3a3..bbb0662132d2 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -25,6 +25,7 @@ import HeaderGap from './HeaderGap';
import SafeAreaConsumer from './SafeAreaConsumer';
import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
import reportPropTypes from '../pages/reportPropTypes';
+import useNativeDriver from '../libs/useNativeDriver';
/**
* Modal render prop component that exposes modal launching triggers that can be used
@@ -294,7 +295,7 @@ function AttachmentModal(props) {
Animated.timing(confirmButtonFadeAnimation, {
toValue,
duration: 100,
- useNativeDriver: true,
+ useNativeDriver,
}).start();
},
[confirmButtonFadeAnimation],
diff --git a/src/components/ConfirmedRoute.js b/src/components/ConfirmedRoute.js
index 6790e8ae4d65..4bcdc4738a3c 100644
--- a/src/components/ConfirmedRoute.js
+++ b/src/components/ConfirmedRoute.js
@@ -16,7 +16,7 @@ import transactionPropTypes from './transactionPropTypes';
import BlockingView from './BlockingViews/BlockingView';
import useNetwork from '../hooks/useNetwork';
import useLocalize from '../hooks/useLocalize';
-import MapView from './MapView';
+import DistanceMapView from './DistanceMapView';
const propTypes = {
/** Transaction that stores the distance request data */
@@ -90,7 +90,7 @@ function ConfirmedRoute({mapboxAccessToken, transaction}) {
return (
<>
{!isOffline && Boolean(mapboxAccessToken.token) ? (
-
diff --git a/src/components/DistanceMapView/distanceMapViewPropTypes.js b/src/components/DistanceMapView/distanceMapViewPropTypes.js
new file mode 100644
index 000000000000..05068cbc9b34
--- /dev/null
+++ b/src/components/DistanceMapView/distanceMapViewPropTypes.js
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ // Public access token to be used to fetch map data from Mapbox.
+ accessToken: PropTypes.string.isRequired,
+
+ // Style applied to MapView component. Note some of the View Style props are not available on ViewMap
+ style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+
+ // Link to the style JSON document.
+ styleURL: PropTypes.string,
+
+ // Whether map can tilt in the vertical direction.
+ pitchEnabled: PropTypes.bool,
+
+ // Padding to apply when the map is adjusted to fit waypoints and directions
+ mapPadding: PropTypes.number,
+
+ // Initial coordinate and zoom level
+ initialState: PropTypes.shape({
+ location: PropTypes.arrayOf(PropTypes.number).isRequired,
+ zoom: PropTypes.number.isRequired,
+ }),
+
+ // Locations on which to put markers
+ waypoints: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string,
+ coordinate: PropTypes.arrayOf(PropTypes.number),
+ markerComponent: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+ }),
+ ),
+
+ // List of coordinates which together forms a direction.
+ directionCoordinates: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)),
+
+ // Callback to call when the map is idle / ready
+ onMapReady: PropTypes.func,
+
+ // Optional additional styles to be applied to the overlay
+ overlayStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+};
+
+const defaultProps = {
+ styleURL: undefined,
+ pitchEnabled: false,
+ mapPadding: 0,
+ initialState: undefined,
+ waypoints: undefined,
+ directionCoordinates: undefined,
+ onMapReady: () => {},
+ overlayStyle: undefined,
+};
+
+export {propTypes, defaultProps};
diff --git a/src/components/DistanceMapView/index.android.js b/src/components/DistanceMapView/index.android.js
new file mode 100644
index 000000000000..ea72fb4de299
--- /dev/null
+++ b/src/components/DistanceMapView/index.android.js
@@ -0,0 +1,48 @@
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import _ from 'underscore';
+import BlockingView from '../BlockingViews/BlockingView';
+import MapView from '../MapView';
+import styles from '../../styles/styles';
+import useNetwork from '../../hooks/useNetwork';
+import useLocalize from '../../hooks/useLocalize';
+import * as Expensicons from '../Icon/Expensicons';
+import * as StyleUtils from '../../styles/StyleUtils';
+import * as distanceMapViewPropTypes from './distanceMapViewPropTypes';
+
+function DistanceMapView(props) {
+ const [isMapReady, setIsMapReady] = useState(false);
+ const {isOffline} = useNetwork();
+ const {translate} = useLocalize();
+
+ return (
+ <>
+ {
+ if (isMapReady) {
+ return;
+ }
+ setIsMapReady(true);
+ }}
+ />
+ {!isMapReady && (
+
+
+
+ )}
+ >
+ );
+}
+
+DistanceMapView.propTypes = distanceMapViewPropTypes.propTypes;
+DistanceMapView.defaultProps = distanceMapViewPropTypes.defaultProps;
+DistanceMapView.displayName = 'DistanceMapView';
+
+export default DistanceMapView;
diff --git a/src/components/DistanceMapView/index.js b/src/components/DistanceMapView/index.js
new file mode 100644
index 000000000000..24bdf99382d1
--- /dev/null
+++ b/src/components/DistanceMapView/index.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import _ from 'underscore';
+import MapView from '../MapView';
+import * as distanceMapViewPropTypes from './distanceMapViewPropTypes';
+
+function DistanceMapView(props) {
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+}
+
+DistanceMapView.propTypes = distanceMapViewPropTypes.propTypes;
+DistanceMapView.defaultProps = distanceMapViewPropTypes.defaultProps;
+DistanceMapView.displayName = 'DistanceMapView';
+
+export default DistanceMapView;
diff --git a/src/components/DistanceRequest.js b/src/components/DistanceRequest.js
index 9de98f365475..bf5a4cb9548b 100644
--- a/src/components/DistanceRequest.js
+++ b/src/components/DistanceRequest.js
@@ -26,9 +26,10 @@ import Navigation from '../libs/Navigation/Navigation';
import * as MapboxToken from '../libs/actions/MapboxToken';
import * as Transaction from '../libs/actions/Transaction';
import * as TransactionUtils from '../libs/TransactionUtils';
+import * as IOUUtils from '../libs/IOUUtils';
import Button from './Button';
-import MapView from './MapView';
+import DistanceMapView from './DistanceMapView';
import LinearGradient from './LinearGradient';
import * as Expensicons from './Icon/Expensicons';
import BlockingView from './BlockingViews/BlockingView';
@@ -38,6 +39,9 @@ import {iouPropTypes} from '../pages/iou/propTypes';
import reportPropTypes from '../pages/reportPropTypes';
import * as IOU from '../libs/actions/IOU';
import * as StyleUtils from '../styles/StyleUtils';
+import ScreenWrapper from './ScreenWrapper';
+import FullPageNotFoundView from './BlockingViews/FullPageNotFoundView';
+import HeaderWithBackButton from './HeaderWithBackButton';
const MAX_WAYPOINTS = 25;
const MAX_WAYPOINTS_TO_DISPLAY = 4;
@@ -63,6 +67,18 @@ const propTypes = {
/** Time when the token will expire in ISO 8601 */
expiration: PropTypes.string,
}),
+
+ /** React Navigation route */
+ route: PropTypes.shape({
+ /** Params from the route */
+ params: PropTypes.shape({
+ /** The type of IOU report, i.e. bill, request, send */
+ iouType: PropTypes.string,
+
+ /** The report ID of the IOU */
+ reportID: PropTypes.string,
+ }),
+ }).isRequired,
};
const defaultProps = {
@@ -75,13 +91,14 @@ const defaultProps = {
},
};
-function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken}) {
+function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, route}) {
const [shouldShowGradient, setShouldShowGradient] = useState(false);
const [scrollContainerHeight, setScrollContainerHeight] = useState(0);
const [scrollContentHeight, setScrollContentHeight] = useState(0);
const {isOffline} = useNetwork();
const {translate} = useLocalize();
+ const isEditing = lodashGet(route, 'path', '').includes('address');
const reportID = lodashGet(report, 'reportID', '');
const waypoints = useMemo(() => lodashGet(transaction, 'comment.waypoints', {}), [transaction]);
const previousWaypoints = usePrevious(waypoints);
@@ -170,7 +187,20 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
useEffect(updateGradientVisibility, [scrollContainerHeight, scrollContentHeight]);
- return (
+ const navigateBack = () => {
+ Navigation.goBack(isEditing ? ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID) : null);
+ };
+
+ const navigateToNextPage = () => {
+ if (isEditing) {
+ Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
+ return;
+ }
+
+ IOU.navigateToNextPage(iou, iouType, reportID, report);
+ };
+
+ const content = (
{!isOffline && Boolean(mapboxAccessToken.token) ? (
-
) : (
@@ -265,12 +296,35 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
);
+
+ if (!isEditing) {
+ return content;
+ }
+
+ return (
+
+ {({safeAreaPaddingBottomStyle}) => (
+
+
+
+ {content}
+
+
+ )}
+
+ );
}
DistanceRequest.displayName = 'DistanceRequest';
diff --git a/src/components/DotIndicatorMessage.js b/src/components/DotIndicatorMessage.js
index ac550f34de3f..b3528b43dc75 100644
--- a/src/components/DotIndicatorMessage.js
+++ b/src/components/DotIndicatorMessage.js
@@ -5,7 +5,7 @@ import {View} from 'react-native';
import styles from '../styles/styles';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
-import colors from '../styles/colors';
+import themeColors from '../styles/themes/default';
import Text from './Text';
import * as Localize from '../libs/Localize';
@@ -57,7 +57,7 @@ function DotIndicatorMessage(props) {
diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js
index 40d91ff03267..a12b089ddf97 100644
--- a/src/components/EmojiPicker/EmojiPicker.js
+++ b/src/components/EmojiPicker/EmojiPicker.js
@@ -114,9 +114,11 @@ const EmojiPicker = forwardRef((props, ref) => {
*/
const isActive = (id) => Boolean(id) && id === activeID;
+ const clearActive = () => setActiveID(null);
+
const resetEmojiPopoverAnchor = () => (emojiPopoverAnchor.current = null);
- useImperativeHandle(ref, () => ({showEmojiPicker, isActive, hideEmojiPicker, isEmojiPickerVisible, resetEmojiPopoverAnchor}));
+ useImperativeHandle(ref, () => ({showEmojiPicker, isActive, clearActive, hideEmojiPicker, isEmojiPickerVisible, resetEmojiPopoverAnchor}));
useEffect(() => {
const emojiPopoverDimensionListener = Dimensions.addEventListener('change', () => {
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index 61f6981edbbe..d3268ebc54b0 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -21,7 +21,6 @@ import * as EmojiUtils from '../../../libs/EmojiUtils';
import CategoryShortcutBar from '../CategoryShortcutBar';
import TextInput from '../../TextInput';
import isEnterWhileComposition from '../../../libs/KeyboardShortcut/isEnterWhileComposition';
-import canFocusInputOnScreenFocus from '../../../libs/canFocusInputOnScreenFocus';
const propTypes = {
/** Function to add the selected emoji to the main compose text input */
@@ -59,10 +58,6 @@ class EmojiPickerMenu extends Component {
// Ref for emoji FlatList
this.emojiList = undefined;
- // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will
- // prevent auto focus when open picker for mobile device
- this.shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();
-
this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300);
this.highlightAdjacentEmoji = this.highlightAdjacentEmoji.bind(this);
this.setupEventHandlers = this.setupEventHandlers.bind(this);
@@ -101,7 +96,7 @@ class EmojiPickerMenu extends Component {
// get a ref to the inner textInput element e.g. if we do
// this.textInput = el} /> this will not
// return a ref to the component, but rather the HTML element by default
- if (this.shouldFocusInputOnScreenFocus && this.props.forwardedRef && _.isFunction(this.props.forwardedRef)) {
+ if (this.props.forwardedRef && _.isFunction(this.props.forwardedRef)) {
this.props.forwardedRef(this.searchInput);
}
this.setupEventHandlers();
@@ -507,7 +502,6 @@ class EmojiPickerMenu extends Component {
onChangeText={this.filterEmojis}
defaultValue=""
ref={(el) => (this.searchInput = el)}
- autoFocus={this.shouldFocusInputOnScreenFocus}
selectTextOnFocus={this.state.selectTextOnFocus}
onSelectionChange={this.onSelectionChange}
onFocus={() => this.setState({isFocused: true, highlightedIndex: -1, isUsingKeyboardMovement: false})}
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
index bfdaf1c13d1b..5cd956dae56b 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
@@ -25,6 +25,9 @@ const propTypes = {
/** Function to add the selected emoji to the main compose text input */
onEmojiSelected: PropTypes.func.isRequired,
+ /** The ref to the search input (may be null on small screen widths) */
+ forwardedRef: PropTypes.func,
+
/** Stores user's preferred skin tone */
preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
@@ -37,11 +40,12 @@ const propTypes = {
};
const defaultProps = {
+ forwardedRef: () => {},
preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
frequentlyUsedEmojis: [],
};
-function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, translate, frequentlyUsedEmojis}) {
+function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, translate, frequentlyUsedEmojis, forwardedRef}) {
const emojiList = useAnimatedRef();
// eslint-disable-next-line react-hooks/exhaustive-deps
const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]);
@@ -168,6 +172,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t
accessibilityLabel={translate('common.search')}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
onChangeText={filterEmojis}
+ ref={forwardedRef}
/>
{!isFiltered && (
diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js
index 728e56792ddb..b51a8b07537c 100644
--- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js
@@ -72,15 +72,16 @@ class EmojiPickerMenuItem extends PureComponent {
this.props.onPress(this.props.emoji)}
+ onPressOut={Browser.isMobile() ? this.props.onHoverOut : undefined}
onHoverIn={this.props.onHoverIn}
onHoverOut={this.props.onHoverOut}
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
ref={(ref) => (this.ref = ref)}
style={({pressed}) => [
- Browser.isMobile() && StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)),
this.props.isHighlighted && this.props.isUsingKeyboardMovement ? styles.emojiItemKeyboardHighlighted : {},
this.props.isHighlighted && !this.props.isUsingKeyboardMovement ? styles.emojiItemHighlighted : {},
+ Browser.isMobile() && StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)),
styles.emojiItem,
]}
accessibilityLabel={this.props.emoji}
diff --git a/src/components/FormHelpMessage.js b/src/components/FormHelpMessage.js
index df8befe5af30..f7366f8dfef6 100644
--- a/src/components/FormHelpMessage.js
+++ b/src/components/FormHelpMessage.js
@@ -5,7 +5,7 @@ import {View} from 'react-native';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import Text from './Text';
-import colors from '../styles/colors';
+import themeColors from '../styles/themes/default';
import styles from '../styles/styles';
import stylePropTypes from '../styles/stylePropTypes';
import * as Localize from '../libs/Localize';
@@ -42,7 +42,7 @@ function FormHelpMessage(props) {
{props.isError && (
)}
diff --git a/src/components/GrowlNotification/index.js b/src/components/GrowlNotification/index.js
index 60bd1bf00587..a06185ac3320 100644
--- a/src/components/GrowlNotification/index.js
+++ b/src/components/GrowlNotification/index.js
@@ -1,7 +1,7 @@
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {Directions, FlingGestureHandler, State} from 'react-native-gesture-handler';
import {View, Animated} from 'react-native';
-import colors from '../../styles/colors';
+import themeColors from '../../styles/themes/default';
import Text from '../Text';
import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
@@ -10,19 +10,20 @@ import GrowlNotificationContainer from './GrowlNotificationContainer';
import CONST from '../../CONST';
import * as Growl from '../../libs/Growl';
import * as Pressables from '../Pressable';
+import useNativeDriver from '../../libs/useNativeDriver';
const types = {
[CONST.GROWL.SUCCESS]: {
icon: Expensicons.Checkmark,
- iconColor: colors.green,
+ iconColor: themeColors.success,
},
[CONST.GROWL.ERROR]: {
icon: Expensicons.Exclamation,
- iconColor: colors.red,
+ iconColor: themeColors.danger,
},
[CONST.GROWL.WARNING]: {
icon: Expensicons.Exclamation,
- iconColor: colors.yellow,
+ iconColor: themeColors.warning,
},
};
@@ -59,7 +60,7 @@ function GrowlNotification(_, ref) {
Animated.spring(translateY, {
toValue: val,
duration: 80,
- useNativeDriver: true,
+ useNativeDriver,
}).start();
},
[translateY],
diff --git a/src/components/IllustratedHeaderPageLayout.js b/src/components/IllustratedHeaderPageLayout.js
index be4cb12d935e..92a9c8b8552b 100644
--- a/src/components/IllustratedHeaderPageLayout.js
+++ b/src/components/IllustratedHeaderPageLayout.js
@@ -12,6 +12,7 @@ import * as StyleUtils from '../styles/StyleUtils';
import useWindowDimensions from '../hooks/useWindowDimensions';
import FixedFooter from './FixedFooter';
import useNetwork from '../hooks/useNetwork';
+import * as Browser from '../libs/Browser';
const propTypes = {
...headerWithBackButtonPropTypes,
@@ -39,14 +40,16 @@ const defaultProps = {
};
function IllustratedHeaderPageLayout({backgroundColor, children, illustration, footer, overlayContent, ...propsToPassToHeader}) {
- const {windowHeight} = useWindowDimensions();
const {isOffline} = useNetwork();
+ const {isSmallScreenWidth, windowHeight} = useWindowDimensions();
+ const appBGColor = StyleUtils.getBackgroundColorStyle(themeColors.appBG);
+
return (
{({safeAreaPaddingBottomStyle}) => (
<>
@@ -56,12 +59,19 @@ function IllustratedHeaderPageLayout({backgroundColor, children, illustration, f
titleColor={backgroundColor === themeColors.appBG ? undefined : themeColors.textColorfulBackground}
iconFill={backgroundColor === themeColors.appBG ? undefined : themeColors.iconColorfulBackground}
/>
-
+
+ {/* Safari on ios/mac has a bug where overscrolling the page scrollview shows green the background color. This is a workaround to fix that. https://github.com/Expensify/App/issues/23422 */}
+ {Browser.isSafari() && (
+
+
+
+
+ )}
-
+ {!Browser.isSafari() && }
{overlayContent && overlayContent()}
- {children}
+ {children}
{!_.isNull(footer) && {footer}}
diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js
index 75be8c67fbf1..f5a293701454 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.js
+++ b/src/components/LHNOptionsList/OptionRowLHN.js
@@ -12,7 +12,6 @@ import * as Expensicons from '../Icon/Expensicons';
import MultipleAvatars from '../MultipleAvatars';
import Hoverable from '../Hoverable';
import DisplayNames from '../DisplayNames';
-import colors from '../../styles/colors';
import Text from '../Text';
import SubscriptAvatar from '../SubscriptAvatar';
import CONST from '../../CONST';
@@ -246,7 +245,7 @@ function OptionRowLHN(props) {
)}
diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx
index 0431a02695ec..7a2248ffafb9 100644
--- a/src/components/MapView/MapView.tsx
+++ b/src/components/MapView/MapView.tsx
@@ -11,7 +11,7 @@ import CONST from '../../CONST';
import {MapViewProps, MapViewHandle} from './MapViewTypes';
-const MapView = forwardRef(({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates}, ref) => {
+const MapView = forwardRef(({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => {
const cameraRef = useRef(null);
const [isIdle, setIsIdle] = useState(false);
@@ -56,6 +56,9 @@ const MapView = forwardRef(({accessToken, style, ma
const setMapIdle = (e: MapState) => {
if (e.gestures.isGestureActive) return;
setIsIdle(true);
+ if (onMapReady) {
+ onMapReady();
+ }
};
return (
diff --git a/src/components/MapView/MapViewTypes.ts b/src/components/MapView/MapViewTypes.ts
index cf5abeed02b2..de32528d077e 100644
--- a/src/components/MapView/MapViewTypes.ts
+++ b/src/components/MapView/MapViewTypes.ts
@@ -18,6 +18,8 @@ type MapViewProps = {
waypoints?: WayPoint[];
// List of coordinates which together forms a direction.
directionCoordinates?: Array<[number, number]>;
+ // Callback to call when the map is idle / ready.
+ onMapReady?: () => void;
};
type DirectionProps = {
diff --git a/src/components/MentionSuggestions.js b/src/components/MentionSuggestions.js
index 4b0129635269..11df8a597ded 100644
--- a/src/components/MentionSuggestions.js
+++ b/src/components/MentionSuggestions.js
@@ -3,6 +3,7 @@ import {View} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import styles from '../styles/styles';
+import themeColors from '../styles/themes/default';
import * as StyleUtils from '../styles/StyleUtils';
import Text from './Text';
import CONST from '../CONST';
@@ -79,7 +80,7 @@ function MentionSuggestions(props) {
size={isIcon ? CONST.AVATAR_SIZE.MENTION_ICON : CONST.AVATAR_SIZE.SMALLER}
name={item.icons[0].name}
type={item.icons[0].type}
- fill={styles.success}
+ fill={themeColors.success}
/>
{
@@ -221,7 +222,9 @@ const MenuItem = React.forwardRef((props, ref) => {
)}
- {Boolean(props.title) && (
+ {Boolean(props.title) && Boolean(props.shouldRenderAsHTML) && }
+
+ {Boolean(props.title) && !props.shouldRenderAsHTML && (
{
)}
diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js
index ab9d420f949c..fc64d8f38243 100644
--- a/src/components/Modal/BaseModal.js
+++ b/src/components/Modal/BaseModal.js
@@ -13,6 +13,7 @@ import useWindowDimensions from '../../hooks/useWindowDimensions';
import variables from '../../styles/variables';
import CONST from '../../CONST';
import ComposerFocusManager from '../../libs/ComposerFocusManager';
+import useNativeDriver from '../../libs/useNativeDriver';
const propTypes = {
...modalPropTypes,
@@ -40,7 +41,7 @@ function BaseModal({
fullscreen,
animationIn,
animationOut,
- useNativeDriver,
+ useNativeDriver: useNativeDriverProp,
hideModalContentWhileAnimating,
animationInTiming,
animationOutTiming,
@@ -187,7 +188,7 @@ function BaseModal({
deviceWidth={windowWidth}
animationIn={animationIn || modalStyleAnimationIn}
animationOut={animationOut || modalStyleAnimationOut}
- useNativeDriver={useNativeDriver}
+ useNativeDriver={useNativeDriverProp && useNativeDriver}
hideModalContentWhileAnimating={hideModalContentWhileAnimating}
animationInTiming={animationInTiming}
animationOutTiming={animationOutTiming}
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 966f5f4340a7..da98d324681e 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -496,7 +496,7 @@ function MoneyRequestConfirmationList(props) {
description={translate('common.distance')}
style={[styles.moneyRequestMenuItem, styles.mb2]}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.getMoneyRequestRoute(props.iouType, props.reportID))}
+ onPress={() => Navigation.navigate(ROUTES.getMoneyRequestAddressRoute(props.iouType, props.reportID))}
disabled={didConfirm || props.isReadOnly || !isTypeRequest}
/>
) : (
diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js
index 50aff23dc9d0..a07510f7603d 100644
--- a/src/components/OptionRow.js
+++ b/src/components/OptionRow.js
@@ -8,6 +8,7 @@ import * as StyleUtils from '../styles/StyleUtils';
import optionPropTypes from './optionPropTypes';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
+import Button from './Button';
import MultipleAvatars from './MultipleAvatars';
import Hoverable from './Hoverable';
import DisplayNames from './DisplayNames';
@@ -39,6 +40,15 @@ const propTypes = {
/** Whether we should show the selected state */
showSelectedState: PropTypes.bool,
+ /** Whether to show a button pill instead of a tickbox */
+ shouldShowSelectedStateAsButton: PropTypes.bool,
+
+ /** Text for button pill */
+ selectedStateButtonText: PropTypes.string,
+
+ /** Callback to fire when the multiple selector (tickbox or button) is clicked */
+ onSelectedStatePressed: PropTypes.func,
+
/** Whether we highlight selected option */
highlightSelected: PropTypes.bool,
@@ -71,6 +81,9 @@ const propTypes = {
const defaultProps = {
hoverStyle: styles.sidebarLinkHover,
showSelectedState: false,
+ shouldShowSelectedStateAsButton: false,
+ selectedStateButtonText: 'Select',
+ onSelectedStatePressed: () => {},
highlightSelected: false,
isSelected: false,
boldStyle: false,
@@ -100,6 +113,7 @@ class OptionRow extends Component {
this.props.isMultilineSupported !== nextProps.isMultilineSupported ||
this.props.isSelected !== nextProps.isSelected ||
this.props.shouldHaveOptionSeparator !== nextProps.shouldHaveOptionSeparator ||
+ this.props.selectedStateButtonText !== nextProps.selectedStateButtonText ||
this.props.showSelectedState !== nextProps.showSelectedState ||
this.props.highlightSelected !== nextProps.highlightSelected ||
this.props.showTitleTooltip !== nextProps.showTitleTooltip ||
@@ -259,7 +273,26 @@ class OptionRow extends Component {
/>
)}
- {this.props.showSelectedState && }
+ {this.props.showSelectedState && (
+ <>
+ {this.props.shouldShowSelectedStateAsButton && !this.props.isSelected ? (
+
diff --git a/src/components/ValidateCode/JustSignedInModal.js b/src/components/ValidateCode/JustSignedInModal.js
index 40b25e4a19e0..e96505470eba 100644
--- a/src/components/ValidateCode/JustSignedInModal.js
+++ b/src/components/ValidateCode/JustSignedInModal.js
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
-import colors from '../../styles/colors';
+import themeColors from '../../styles/themes/default';
import styles from '../../styles/styles';
import Icon from '../Icon';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
@@ -41,7 +41,7 @@ function JustSignedInModal(props) {
diff --git a/src/components/ValidateCode/ValidateCodeModal.js b/src/components/ValidateCode/ValidateCodeModal.js
index 344c3107b8cf..eabb21eea4a9 100644
--- a/src/components/ValidateCode/ValidateCodeModal.js
+++ b/src/components/ValidateCode/ValidateCodeModal.js
@@ -4,7 +4,7 @@ import {compose} from 'underscore';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
import {View} from 'react-native';
-import colors from '../../styles/colors';
+import themeColors from '../../styles/themes/default';
import styles from '../../styles/styles';
import Icon from '../Icon';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
@@ -71,7 +71,7 @@ function ValidateCodeModal(props) {
diff --git a/src/components/WalletStatementModal/index.js b/src/components/WalletStatementModal/index.js
index 8d7d000ad72d..84109217b18f 100644
--- a/src/components/WalletStatementModal/index.js
+++ b/src/components/WalletStatementModal/index.js
@@ -32,7 +32,7 @@ function WalletStatementModal({statementPageURL, session}) {
}
if (event.data.type === 'STATEMENT_NAVIGATE' && event.data.url) {
- const iouRoutes = [ROUTES.IOU_REQUEST, ROUTES.IOU_SEND, ROUTES.IOU_BILL];
+ const iouRoutes = [ROUTES.IOU_REQUEST, ROUTES.IOU_SEND];
const navigateToIOURoute = _.find(iouRoutes, (iouRoute) => event.data.url.includes(iouRoute));
if (navigateToIOURoute) {
Navigation.navigate(navigateToIOURoute);
diff --git a/src/components/WalletStatementModal/index.native.js b/src/components/WalletStatementModal/index.native.js
index 86737da158c8..590431274da5 100644
--- a/src/components/WalletStatementModal/index.native.js
+++ b/src/components/WalletStatementModal/index.native.js
@@ -37,7 +37,7 @@ class WalletStatementModal extends React.Component {
}
if (type === 'STATEMENT_NAVIGATE' && url) {
- const iouRoutes = [ROUTES.IOU_REQUEST, ROUTES.IOU_SEND, ROUTES.IOU_BILL];
+ const iouRoutes = [ROUTES.IOU_REQUEST, ROUTES.IOU_SEND];
const navigateToIOURoute = _.find(iouRoutes, (iouRoute) => url.includes(iouRoute));
if (navigateToIOURoute) {
this.webview.stopLoading();
diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js
index cc89f9a7b80b..53216ab7cdc7 100644
--- a/src/components/menuItemPropTypes.js
+++ b/src/components/menuItemPropTypes.js
@@ -144,6 +144,9 @@ const propTypes = {
/** Should we grey out the menu item when it is disabled? */
shouldGreyOutWhenDisabled: PropTypes.bool,
+
+ /** Should render the content in HTML format */
+ shouldRenderAsHTML: PropTypes.bool,
};
export default propTypes;
diff --git a/src/components/withCurrentUserPersonalDetails.js b/src/components/withCurrentUserPersonalDetails.js
index 87a046e66983..7a47ea7cc712 100644
--- a/src/components/withCurrentUserPersonalDetails.js
+++ b/src/components/withCurrentUserPersonalDetails.js
@@ -36,7 +36,8 @@ export default function (WrappedComponent) {
function WithCurrentUserPersonalDetails(props) {
const accountID = props.session.accountID;
- const currentUserPersonalDetails = useMemo(() => ({...props.personalDetails[accountID], accountID}), [props.personalDetails, accountID]);
+ const accountPersonalDetails = props.personalDetails[accountID];
+ const currentUserPersonalDetails = useMemo(() => ({...accountPersonalDetails, accountID}), [accountPersonalDetails, accountID]);
return (
{
const convertedDistance = convertDistanceUnit(distance, unit);
- return convertedDistance * rate;
+ const roundedDistance = convertedDistance.toFixed(2);
+ return roundedDistance * rate;
};
export default {getDefaultMileageRate, getDistanceMerchant, getDistanceRequestAmount};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index 37b7087b6ad4..e0197805f09c 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -159,7 +159,7 @@ class AuthScreens extends React.Component {
Timing.end(CONST.TIMING.HOMEPAGE_INITIAL_RENDER);
const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH;
- const groupShortcutConfig = CONST.KEYBOARD_SHORTCUTS.NEW_GROUP;
+ const chatShortcutConfig = CONST.KEYBOARD_SHORTCUTS.NEW_CHAT;
// Listen for the key K being pressed so that focus can be given to
// the chat switcher, or new group chat
@@ -178,18 +178,18 @@ class AuthScreens extends React.Component {
searchShortcutConfig.modifiers,
true,
);
- this.unsubscribeGroupShortcut = KeyboardShortcut.subscribe(
- groupShortcutConfig.shortcutKey,
+ this.unsubscribeChatShortcut = KeyboardShortcut.subscribe(
+ chatShortcutConfig.shortcutKey,
() => {
Modal.close(() => {
- if (Navigation.isActiveRoute(ROUTES.NEW_GROUP)) {
+ if (Navigation.isActiveRoute(ROUTES.NEW_CHAT)) {
return;
}
- Navigation.navigate(ROUTES.NEW_GROUP);
+ Navigation.navigate(ROUTES.NEW_CHAT);
});
},
- groupShortcutConfig.descriptionKey,
- groupShortcutConfig.modifiers,
+ chatShortcutConfig.descriptionKey,
+ chatShortcutConfig.modifiers,
true,
);
}
@@ -202,8 +202,8 @@ class AuthScreens extends React.Component {
if (this.unsubscribeSearchShortcut) {
this.unsubscribeSearchShortcut();
}
- if (this.unsubscribeGroupShortcut) {
- this.unsubscribeGroupShortcut();
+ if (this.unsubscribeChatShortcut) {
+ this.unsubscribeChatShortcut();
}
Session.cleanupSession();
clearInterval(this.interval);
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index 851f7aff3a8d..c5bb02354641 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -132,6 +132,13 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator([
},
name: 'Money_Request_Waypoint',
},
+ {
+ getComponent: () => {
+ const DistanceRequestEditPage = require('../../../pages/iou/DistanceRequestPage').default;
+ return DistanceRequestEditPage;
+ },
+ name: 'Money_Request_Address',
+ },
]);
const SplitDetailsModalStackNavigator = createModalStackNavigator([
@@ -266,21 +273,11 @@ const SearchModalStackNavigator = createModalStackNavigator([
},
]);
-const NewGroupModalStackNavigator = createModalStackNavigator([
- {
- getComponent: () => {
- const NewGroupPage = require('../../../pages/NewGroupPage').default;
- return NewGroupPage;
- },
- name: 'NewGroup_Root',
- },
-]);
-
const NewChatModalStackNavigator = createModalStackNavigator([
{
getComponent: () => {
- const NewChatPage = require('../../../pages/NewChatPage').default;
- return NewChatPage;
+ const NewChatSelectorPage = require('../../../pages/NewChatSelectorPage').default;
+ return NewChatSelectorPage;
},
name: 'NewChat_Root',
},
@@ -672,13 +669,6 @@ const SettingsModalStackNavigator = createModalStackNavigator([
},
name: 'Workspace_Invite_Message',
},
- {
- getComponent: () => {
- const WorkspaceNewRoomPage = require('../../../pages/workspace/WorkspaceNewRoomPage').default;
- return WorkspaceNewRoomPage;
- },
- name: 'Workspace_NewRoom',
- },
{
getComponent: () => {
const ReimbursementAccountPage = require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default;
@@ -770,6 +760,30 @@ const EditRequestStackNavigator = createModalStackNavigator([
},
]);
+const PrivateNotesModalStackNavigator = createModalStackNavigator([
+ {
+ getComponent: () => {
+ const PrivateNotesPage = require('../../../pages/PrivateNotes/PrivateNotesViewPage').default;
+ return PrivateNotesPage;
+ },
+ name: 'PrivateNotes_View',
+ },
+ {
+ getComponent: () => {
+ const PrivateNotesListPage = require('../../../pages/PrivateNotes/PrivateNotesListPage').default;
+ return PrivateNotesListPage;
+ },
+ name: 'PrivateNotes_List',
+ },
+ {
+ getComponent: () => {
+ const PrivateNotesEditPage = require('../../../pages/PrivateNotes/PrivateNotesEditPage').default;
+ return PrivateNotesEditPage;
+ },
+ name: 'PrivateNotes_Edit',
+ },
+]);
+
const SignInModalStackNavigator = createModalStackNavigator([
{
getComponent: () => {
@@ -791,7 +805,6 @@ export {
ReportWelcomeMessageModalStackNavigator,
ReportParticipantsModalStackNavigator,
SearchModalStackNavigator,
- NewGroupModalStackNavigator,
NewChatModalStackNavigator,
NewTaskModalStackNavigator,
SettingsModalStackNavigator,
@@ -801,6 +814,7 @@ export {
WalletStatementStackNavigator,
FlagCommentStackNavigator,
EditRequestStackNavigator,
+ PrivateNotesModalStackNavigator,
NewTeachersUniteNavigator,
SignInModalStackNavigator,
};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js
index f6ea89ecd088..27a15fa3d763 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js
@@ -32,14 +32,6 @@ function RightModalNavigator(props) {
name="NewChat"
component={ModalStackNavigators.NewChatModalStackNavigator}
/>
-
+
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index 62aac8c48e25..14ee2b895831 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -204,9 +204,6 @@ export default {
Workspace_Invite_Message: {
path: ROUTES.WORKSPACE_INVITE_MESSAGE,
},
- Workspace_NewRoom: {
- path: ROUTES.WORKSPACE_NEW_ROOM,
- },
ReimbursementAccount: {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN,
exact: true,
@@ -216,6 +213,13 @@ export default {
},
},
},
+ Private_Notes: {
+ screens: {
+ PrivateNotes_View: ROUTES.PRIVATE_NOTES_VIEW,
+ PrivateNotes_List: ROUTES.PRIVATE_NOTES_LIST,
+ PrivateNotes_Edit: ROUTES.PRIVATE_NOTES_EDIT,
+ },
+ },
Report_Details: {
screens: {
Report_Details_Root: ROUTES.REPORT_WITH_ID_DETAILS,
@@ -243,14 +247,22 @@ export default {
Report_WelcomeMessage_Root: ROUTES.REPORT_WELCOME_MESSAGE,
},
},
- NewGroup: {
- screens: {
- NewGroup_Root: ROUTES.NEW_GROUP,
- },
- },
NewChat: {
screens: {
- NewChat_Root: ROUTES.NEW_CHAT,
+ NewChat_Root: {
+ path: ROUTES.NEW,
+ exact: true,
+ screens: {
+ chat: {
+ path: ROUTES.NEW_CHAT,
+ exact: true,
+ },
+ room: {
+ path: ROUTES.NEW_ROOM,
+ exact: true,
+ },
+ },
+ },
},
},
NewTask: {
@@ -321,6 +333,7 @@ export default {
Money_Request_Tag: ROUTES.MONEY_REQUEST_TAG,
Money_Request_Merchant: ROUTES.MONEY_REQUEST_MERCHANT,
Money_Request_Waypoint: ROUTES.MONEY_REQUEST_WAYPOINT,
+ Money_Request_Address: ROUTES.MONEY_REQUEST_ADDRESS,
IOU_Send_Enable_Payments: ROUTES.IOU_SEND_ENABLE_PAYMENTS,
IOU_Send_Add_Bank_Account: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT,
IOU_Send_Add_Debit_Card: ROUTES.IOU_SEND_ADD_DEBIT_CARD,
diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js
index 1d4966826492..a401dea4b911 100644
--- a/src/libs/PersonalDetailsUtils.js
+++ b/src/libs/PersonalDetailsUtils.js
@@ -4,6 +4,7 @@ import _ from 'underscore';
import ONYXKEYS from '../ONYXKEYS';
import * as Localize from './Localize';
import * as UserUtils from './UserUtils';
+import * as LocalePhoneNumber from './LocalePhoneNumber';
let personalDetails = [];
let allPersonalDetails = {};
@@ -115,7 +116,7 @@ function getNewPersonalDetailsOnyxData(logins, accountIDs) {
login,
accountID,
avatar: UserUtils.getDefaultAvatarURL(accountID),
- displayName: login,
+ displayName: LocalePhoneNumber.formatPhoneNumber(login),
};
/**
diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js
index 20337bd9802f..fde847bd9bfc 100644
--- a/src/libs/ReportActionsUtils.js
+++ b/src/libs/ReportActionsUtils.js
@@ -619,6 +619,17 @@ function getAllReportActions(reportID) {
return lodashGet(allReportActions, reportID, []);
}
+/**
+ * Check whether a report action is an attachment (a file, such as an image or a zip).
+ *
+ * @param {Object} reportAction report action
+ * @returns {Boolean}
+ */
+function isReportActionAttachment(reportAction) {
+ const message = _.first(lodashGet(reportAction, 'message', [{}]));
+ return _.has(reportAction, 'isAttachment') ? reportAction.isAttachment : isReportMessageAttachment(message);
+}
+
export {
getSortedReportActions,
getLastVisibleAction,
@@ -656,4 +667,5 @@ export {
isSplitBillAction,
isTaskAction,
getAllReportActions,
+ isReportActionAttachment,
};
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index 3b3f7b976ba6..f24959c4bac2 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -1606,7 +1606,7 @@ function getReportName(report, policy = undefined) {
return getTransactionReportName(parentReportAction);
}
- const isAttachment = _.has(parentReportAction, 'isAttachment') ? parentReportAction.isAttachment : isReportMessageAttachment(_.last(lodashGet(parentReportAction, 'message', [{}])));
+ const isAttachment = ReportActionsUtils.isReportActionAttachment(parentReportAction);
const parentReportActionMessage = lodashGet(parentReportAction, ['message', 0, 'text'], '').replace(/(\r\n|\n|\r)/gm, ' ');
if (isAttachment && parentReportActionMessage) {
return `[${Localize.translateLocal('common.attachment')}]`;
diff --git a/src/libs/Request.js b/src/libs/Request.ts
similarity index 53%
rename from src/libs/Request.js
rename to src/libs/Request.ts
index 577dcf3cb85d..459deaf89e1e 100644
--- a/src/libs/Request.js
+++ b/src/libs/Request.ts
@@ -1,43 +1,29 @@
-import _ from 'underscore';
import HttpUtils from './HttpUtils';
import enhanceParameters from './Network/enhanceParameters';
import * as NetworkStore from './Network/NetworkStore';
+import Request from '../types/onyx/Request';
-let middlewares = [];
+type Middleware = (response: unknown, request: Request, isFromSequentialQueue: boolean) => Promise;
-/**
- * @param {Object} request
- * @param {String} request.command
- * @param {Object} request.data
- * @param {String} request.type
- * @param {Boolean} request.shouldUseSecure
- * @returns {Promise}
- */
-function makeXHR(request) {
- const finalParameters = enhanceParameters(request.command, request.data);
+let middlewares: Middleware[] = [];
+
+function makeXHR(request: Request): Promise {
+ const finalParameters = enhanceParameters(request.command, request?.data ?? {});
return NetworkStore.hasReadRequiredDataFromStorage().then(() => {
// If we're using the Supportal token and this is not a Supportal request
// let's just return a promise that will resolve itself.
if (NetworkStore.getSupportAuthToken() && !NetworkStore.isSupportRequest(request.command)) {
- return new Promise((resolve) => resolve());
+ return new Promise((resolve) => resolve());
}
return HttpUtils.xhr(request.command, finalParameters, request.type, request.shouldUseSecure);
});
}
-/**
- * @param {Object} request
- * @param {Boolean} [isFromSequentialQueue]
- * @returns {Promise}
- */
-function processWithMiddleware(request, isFromSequentialQueue = false) {
- return _.reduce(middlewares, (last, middleware) => middleware(last, request, isFromSequentialQueue), makeXHR(request));
+function processWithMiddleware(request: Request, isFromSequentialQueue = false): Promise {
+ return middlewares.reduce((last, middleware) => middleware(last, request, isFromSequentialQueue), makeXHR(request));
}
-/**
- * @param {Function} middleware
- */
-function use(middleware) {
+function use(middleware: Middleware) {
middlewares.push(middleware);
}
diff --git a/src/libs/actions/EmojiPickerAction.js b/src/libs/actions/EmojiPickerAction.js
index 84621af3a5b4..70c7ebabbe20 100644
--- a/src/libs/actions/EmojiPickerAction.js
+++ b/src/libs/actions/EmojiPickerAction.js
@@ -45,6 +45,13 @@ function isActive(id) {
return emojiPickerRef.current.isActive(id);
}
+function clearActive() {
+ if (!emojiPickerRef.current) {
+ return;
+ }
+ return emojiPickerRef.current.clearActive();
+}
+
function isEmojiPickerVisible() {
if (!emojiPickerRef.current) {
return;
@@ -59,4 +66,4 @@ function resetEmojiPopoverAnchor() {
return emojiPickerRef.current.resetEmojiPopoverAnchor();
}
-export {emojiPickerRef, showEmojiPicker, hideEmojiPicker, isActive, isEmojiPickerVisible, resetEmojiPopoverAnchor};
+export {emojiPickerRef, showEmojiPicker, hideEmojiPicker, isActive, clearActive, isEmojiPickerVisible, resetEmojiPopoverAnchor};
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index 8f18119203be..3cefcd00ed60 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -22,6 +22,7 @@ import * as UserUtils from '../UserUtils';
import * as Report from './Report';
import * as NumberUtils from '../NumberUtils';
import ReceiptGeneric from '../../../assets/images/receipt-generic.png';
+import * as LocalePhoneNumber from '../LocalePhoneNumber';
let allReports;
Onyx.connect({
@@ -88,7 +89,9 @@ function resetMoneyRequestInfo(id = '') {
amount: 0,
currency: lodashGet(currentUserPersonalDetails, 'localCurrencyCode', CONST.CURRENCY.USD),
comment: '',
+ // TODO: remove participants after all instances of iou.participants will be replaced with iou.participantAccountIDs
participants: [],
+ participantAccountIDs: [],
merchant: CONST.TRANSACTION.DEFAULT_MERCHANT,
category: '',
created,
@@ -492,7 +495,7 @@ function getMoneyRequestInformation(
[payerAccountID]: {
accountID: payerAccountID,
avatar: UserUtils.getDefaultAvatarURL(payerAccountID),
- displayName: participant.displayName || payerEmail,
+ displayName: LocalePhoneNumber.formatPhoneNumber(participant.displayName || payerEmail),
login: participant.login,
},
}
@@ -894,7 +897,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
[accountID]: {
accountID,
avatar: UserUtils.getDefaultAvatarURL(accountID),
- displayName: participant.displayName || email,
+ displayName: LocalePhoneNumber.formatPhoneNumber(participant.displayName || email),
login: participant.login,
},
}
@@ -1949,7 +1952,9 @@ function resetMoneyRequestCategory() {
* @param {Object[]} participants
*/
function setMoneyRequestParticipants(participants) {
- Onyx.merge(ONYXKEYS.IOU, {participants});
+ // TODO: temporarily we want to save both participants and participantAccountIDs, then we can remove participants (and rename the function)
+ // more info: https://github.com/Expensify/App/issues/25714#issuecomment-1712924903 and https://github.com/Expensify/App/issues/25714#issuecomment-1716335802
+ Onyx.merge(ONYXKEYS.IOU, {participants, participantAccountIDs: _.map(participants, 'accountID')});
}
/**
diff --git a/src/libs/actions/OnyxUpdates.js b/src/libs/actions/OnyxUpdates.ts
similarity index 62%
rename from src/libs/actions/OnyxUpdates.js
rename to src/libs/actions/OnyxUpdates.ts
index 8e45e7dd2e66..50a4fdffc3ae 100644
--- a/src/libs/actions/OnyxUpdates.js
+++ b/src/libs/actions/OnyxUpdates.ts
@@ -1,28 +1,25 @@
-import Onyx from 'react-native-onyx';
-import _ from 'underscore';
+import Onyx, {OnyxEntry} from 'react-native-onyx';
+import {Merge} from 'type-fest';
import PusherUtils from '../PusherUtils';
import ONYXKEYS from '../../ONYXKEYS';
import * as QueuedOnyxUpdates from './QueuedOnyxUpdates';
import CONST from '../../CONST';
+import {OnyxUpdatesFromServer, OnyxUpdateEvent, Request} from '../../types/onyx';
+import Response from '../../types/onyx/Response';
// This key needs to be separate from ONYXKEYS.ONYX_UPDATES_FROM_SERVER so that it can be updated without triggering the callback when the server IDs are updated. If that
// callback were triggered it would lead to duplicate processing of server updates.
-let lastUpdateIDAppliedToClient = 0;
+let lastUpdateIDAppliedToClient: OnyxEntry = 0;
Onyx.connect({
key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
callback: (val) => (lastUpdateIDAppliedToClient = val),
});
-/**
- * @param {Object} request
- * @param {Object} response
- * @returns {Promise}
- */
-function applyHTTPSOnyxUpdates(request, response) {
+function applyHTTPSOnyxUpdates(request: Request, response: Response) {
console.debug('[OnyxUpdateManager] Applying https update');
// For most requests we can immediately update Onyx. For write requests we queue the updates and apply them after the sequential queue has flushed to prevent a replay effect in
// the UI. See https://github.com/Expensify/App/issues/12775 for more info.
- const updateHandler = request.data.apiRequestType === CONST.API_REQUEST_TYPE.WRITE ? QueuedOnyxUpdates.queueOnyxUpdates : Onyx.update;
+ const updateHandler = request?.data?.apiRequestType === CONST.API_REQUEST_TYPE.WRITE ? QueuedOnyxUpdates.queueOnyxUpdates : Onyx.update;
// First apply any onyx data updates that are being sent back from the API. We wait for this to complete and then
// apply successData or failureData. This ensures that we do not update any pending, loading, or other UI states contained
@@ -46,55 +43,45 @@ function applyHTTPSOnyxUpdates(request, response) {
});
}
-/**
- * @param {Array} updates
- * @returns {Promise}
- */
-function applyPusherOnyxUpdates(updates) {
+function applyPusherOnyxUpdates(updates: OnyxUpdateEvent[]) {
console.debug('[OnyxUpdateManager] Applying pusher update');
- const pusherEventPromises = _.map(updates, (update) => PusherUtils.triggerMultiEventHandler(update.eventType, update.data));
+ const pusherEventPromises = updates.map((update) => PusherUtils.triggerMultiEventHandler(update.eventType, update.data));
return Promise.all(pusherEventPromises).then(() => {
console.debug('[OnyxUpdateManager] Done applying Pusher update');
});
}
/**
- * @param {Object[]} updateParams
- * @param {String} updateParams.type
- * @param {Number} updateParams.lastUpdateID
- * @param {Object} [updateParams.request] Exists if updateParams.type === 'https'
- * @param {Object} [updateParams.response] Exists if updateParams.type === 'https'
- * @param {Object} [updateParams.updates] Exists if updateParams.type === 'pusher'
- * @returns {Promise}
+ * @param [updateParams.request] Exists if updateParams.type === 'https'
+ * @param [updateParams.response] Exists if updateParams.type === 'https'
+ * @param [updateParams.updates] Exists if updateParams.type === 'pusher'
*/
-function apply({lastUpdateID, type, request, response, updates}) {
+function apply({lastUpdateID, type, request, response, updates}: Merge): Promise;
+function apply({lastUpdateID, type, request, response, updates}: Merge): Promise;
+function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFromServer): Promise | undefined {
console.debug(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, {request, response, updates});
- if (lastUpdateID && lastUpdateID < lastUpdateIDAppliedToClient) {
+ if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) < lastUpdateIDAppliedToClient) {
console.debug('[OnyxUpdateManager] Update received was older than current state, returning without applying the updates');
return Promise.resolve();
}
- if (lastUpdateID && lastUpdateID > lastUpdateIDAppliedToClient) {
- Onyx.merge(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateID);
+ if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) > lastUpdateIDAppliedToClient) {
+ Onyx.merge(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, Number(lastUpdateID));
}
- if (type === CONST.ONYX_UPDATE_TYPES.HTTPS) {
+ if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response) {
return applyHTTPSOnyxUpdates(request, response);
}
- if (type === CONST.ONYX_UPDATE_TYPES.PUSHER) {
+ if (type === CONST.ONYX_UPDATE_TYPES.PUSHER && updates) {
return applyPusherOnyxUpdates(updates);
}
}
/**
- * @param {Object[]} updateParams
- * @param {String} updateParams.type
- * @param {Object} [updateParams.request] Exists if updateParams.type === 'https'
- * @param {Object} [updateParams.response] Exists if updateParams.type === 'https'
- * @param {Object} [updateParams.updates] Exists if updateParams.type === 'pusher'
- * @param {Number} [updateParams.lastUpdateID]
- * @param {Number} [updateParams.previousUpdateID]
+ * @param [updateParams.request] Exists if updateParams.type === 'https'
+ * @param [updateParams.response] Exists if updateParams.type === 'https'
+ * @param [updateParams.updates] Exists if updateParams.type === 'pusher'
*/
-function saveUpdateInformation(updateParams) {
+function saveUpdateInformation(updateParams: OnyxUpdatesFromServer) {
// Always use set() here so that the updateParams are never merged and always unique to the request that came in
Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, updateParams);
}
@@ -102,10 +89,9 @@ function saveUpdateInformation(updateParams) {
/**
* This function will receive the previousUpdateID from any request/pusher update that has it, compare to our current app state
* and return if an update is needed
- * @param {Number} previousUpdateID The previousUpdateID contained in the response object
- * @returns {Boolean}
+ * @param previousUpdateID The previousUpdateID contained in the response object
*/
-function doesClientNeedToBeUpdated(previousUpdateID = 0) {
+function doesClientNeedToBeUpdated(previousUpdateID = 0): boolean {
// If no previousUpdateID is sent, this is not a WRITE request so we don't need to update our current state
if (!previousUpdateID) {
return false;
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 2961784d5825..5cf0e51279a9 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -1933,6 +1933,136 @@ function flagComment(reportID, reportAction, severity) {
API.write('FlagComment', parameters, {optimisticData, successData, failureData});
}
+/**
+ * Updates a given user's private notes on a report
+ *
+ * @param {String} reportID
+ * @param {Number} accountID
+ * @param {String} note
+ */
+const updatePrivateNotes = (reportID, accountID, note) => {
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ privateNotes: {
+ [accountID]: {
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ errors: null,
+ note,
+ },
+ },
+ },
+ },
+ ];
+
+ const successData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ privateNotes: {
+ [accountID]: {
+ pendingAction: null,
+ errors: null,
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ privateNotes: {
+ [accountID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError("Private notes couldn't be saved"),
+ },
+ },
+ },
+ },
+ ];
+
+ API.write(
+ 'UpdateReportPrivateNote',
+ {
+ reportID,
+ privateNotes: note,
+ },
+ {optimisticData, successData, failureData},
+ );
+};
+
+/**
+ * Fetches all the private notes for a given report
+ *
+ * @param {String} reportID
+ */
+function getReportPrivateNote(reportID) {
+ if (_.isEmpty(reportID)) {
+ return;
+ }
+ API.read(
+ 'GetReportPrivateNote',
+ {
+ reportID,
+ },
+ {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ isLoadingPrivateNotes: true,
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ isLoadingPrivateNotes: false,
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {
+ isLoadingPrivateNotes: false,
+ },
+ },
+ ],
+ },
+ );
+}
+
+/**
+ * Checks if there are any errors in the private notes for a given report
+ *
+ * @param {Object} report
+ * @returns {Boolean} Returns true if there are errors in any of the private notes on the report
+ */
+function hasErrorInPrivateNotes(report) {
+ const privateNotes = lodashGet(report, 'privateNotes', {});
+ return _.some(privateNotes, (privateNote) => !_.isEmpty(privateNote.errors));
+}
+
+/**
+ * Clears all errors associated with a given private note
+ *
+ * @param {String} reportID
+ * @param {Number} accountID
+ */
+function clearPrivateNotesError(reportID, accountID) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {privateNotes: {[accountID]: {errors: null}}});
+}
+
export {
addComment,
addAttachment,
@@ -1980,4 +2110,8 @@ export {
setLastOpenedPublicRoom,
flagComment,
openLastOpenedPublicRoom,
+ updatePrivateNotes,
+ getReportPrivateNote,
+ clearPrivateNotesError,
+ hasErrorInPrivateNotes,
};
diff --git a/src/libs/asyncOpenURL/index.js b/src/libs/asyncOpenURL/index.ts
similarity index 60%
rename from src/libs/asyncOpenURL/index.js
rename to src/libs/asyncOpenURL/index.ts
index e69777c5483c..5307049ee923 100644
--- a/src/libs/asyncOpenURL/index.js
+++ b/src/libs/asyncOpenURL/index.ts
@@ -1,6 +1,7 @@
import {Linking} from 'react-native';
+import AsyncOpenURL from './types';
-export default function asyncOpenURL(promise, url) {
+const asyncOpenURL: AsyncOpenURL = (promise, url) => {
if (!url) {
return;
}
@@ -8,4 +9,6 @@ export default function asyncOpenURL(promise, url) {
promise.then((params) => {
Linking.openURL(typeof url === 'string' ? url : url(params));
});
-}
+};
+
+export default asyncOpenURL;
diff --git a/src/libs/asyncOpenURL/index.website.js b/src/libs/asyncOpenURL/index.website.ts
similarity index 61%
rename from src/libs/asyncOpenURL/index.website.js
rename to src/libs/asyncOpenURL/index.website.ts
index e1c491450c18..d503644c1392 100644
--- a/src/libs/asyncOpenURL/index.website.js
+++ b/src/libs/asyncOpenURL/index.website.ts
@@ -1,13 +1,11 @@
import {Linking} from 'react-native';
+import AsyncOpenURL from './types';
/**
* Prevents Safari from blocking pop-up window when opened within async call.
- *
- * @param {Promise} promise
- * @param {string} url
- * @param {Boolean} shouldSkipCustomSafariLogic When true, we will use `Linking.openURL` even if the browser is Safari.
+ * @param shouldSkipCustomSafariLogic When true, we will use `Linking.openURL` even if the browser is Safari.
*/
-export default function asyncOpenURL(promise, url, shouldSkipCustomSafariLogic) {
+const asyncOpenURL: AsyncOpenURL = (promise, url, shouldSkipCustomSafariLogic) => {
if (!url) {
return;
}
@@ -22,8 +20,13 @@ export default function asyncOpenURL(promise, url, shouldSkipCustomSafariLogic)
const windowRef = window.open();
promise
.then((params) => {
+ if (!windowRef) {
+ return;
+ }
windowRef.location = typeof url === 'string' ? url : url(params);
})
- .catch(() => windowRef.close());
+ .catch(() => windowRef?.close());
}
-}
+};
+
+export default asyncOpenURL;
diff --git a/src/libs/asyncOpenURL/types.ts b/src/libs/asyncOpenURL/types.ts
new file mode 100644
index 000000000000..bf24756b0cc2
--- /dev/null
+++ b/src/libs/asyncOpenURL/types.ts
@@ -0,0 +1,3 @@
+type AsyncOpenURL = (promise: Promise, url: string | ((params: T) => string), shouldSkipCustomSafariLogic?: boolean) => void;
+
+export default AsyncOpenURL;
diff --git a/src/libs/calculateAnchorPosition.js b/src/libs/calculateAnchorPosition.js
deleted file mode 100644
index c886c9ac3712..000000000000
--- a/src/libs/calculateAnchorPosition.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import lodashGet from 'lodash/get';
-import CONST from '../CONST';
-
-/**
- * Gets the x,y position of the passed in component for the purpose of anchoring another component to it.
- *
- * @param {Element} anchorComponent
- * @param {{horizontal: string, vertical: string}} anchorOriginValue - Optional parameter
- * @return {Promise}
- */
-export default function calculateAnchorPosition(anchorComponent, anchorOriginValue) {
- return new Promise((resolve) => {
- if (!anchorComponent) {
- return resolve({horizontal: 0, vertical: 0});
- }
- anchorComponent.measureInWindow((x, y, width, height) => {
- if (
- lodashGet(anchorOriginValue, 'vertical') === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP &&
- lodashGet(anchorOriginValue, 'horizontal') === CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT
- ) {
- return resolve({horizontal: x, vertical: y + height + lodashGet(anchorOriginValue, 'shiftVertical', 0)});
- }
- return resolve({horizontal: x + width, vertical: y});
- });
- });
-}
diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts
new file mode 100644
index 000000000000..39fb3032ee09
--- /dev/null
+++ b/src/libs/calculateAnchorPosition.ts
@@ -0,0 +1,32 @@
+/* eslint-disable no-console */
+import {ValueOf} from 'type-fest';
+import {View} from 'react-native';
+import CONST from '../CONST';
+
+type AnchorOrigin = {
+ horizontal: ValueOf;
+ vertical: ValueOf;
+ shiftVertical?: number;
+};
+
+type AnchorPosition = {
+ horizontal: number;
+ vertical: number;
+};
+
+/**
+ * Gets the x,y position of the passed in component for the purpose of anchoring another component to it.
+ */
+export default function calculateAnchorPosition(anchorComponent: View, anchorOrigin?: AnchorOrigin): Promise {
+ return new Promise((resolve) => {
+ if (!anchorComponent) {
+ return resolve({horizontal: 0, vertical: 0});
+ }
+ anchorComponent.measureInWindow((x, y, width, height) => {
+ if (anchorOrigin?.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP && anchorOrigin?.horizontal === CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT) {
+ return resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0)});
+ }
+ return resolve({horizontal: x + width, vertical: y});
+ });
+ });
+}
diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.js b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.js
deleted file mode 100644
index 4f3e8c5de2c8..000000000000
--- a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as Composer from '../actions/Composer';
-
-export default () => {
- Composer.setShouldShowComposeInput(true);
-};
diff --git a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.js b/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.js
deleted file mode 100644
index 488769741715..000000000000
--- a/src/libs/openReportActionComposeViewWhenClosingMessageEdit/index.native.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import {Keyboard} from 'react-native';
-import * as Composer from '../actions/Composer';
-
-export default () => {
- const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
- Composer.setShouldShowComposeInput(true);
- keyboardDidHideListener.remove();
- });
-};
diff --git a/src/libs/searchCountryOptions.js b/src/libs/searchCountryOptions.js
deleted file mode 100644
index 9b0357a17a65..000000000000
--- a/src/libs/searchCountryOptions.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import _ from 'lodash';
-import StringUtils from './StringUtils';
-
-/**
- * Searches the countries/states data and returns sorted results based on the search query
- * @param {String} searchValue
- * @param {Object[]} countriesData - An array of country data objects
- * @returns {Object[]} An array of countries/states sorted based on the search query
- */
-function searchCountryOptions(searchValue, countriesData) {
- if (_.isEmpty(searchValue)) {
- return countriesData;
- }
-
- const trimmedSearchValue = StringUtils.sanitizeString(searchValue);
- if (_.isEmpty(trimmedSearchValue)) {
- return [];
- }
-
- const filteredData = _.filter(countriesData, (country) => _.includes(country.searchValue, trimmedSearchValue));
-
- // sort by country code
- return _.sortBy(filteredData, (country) => (_.toLower(country.value) === trimmedSearchValue ? -1 : 1));
-}
-
-export default searchCountryOptions;
diff --git a/src/libs/searchCountryOptions.ts b/src/libs/searchCountryOptions.ts
new file mode 100644
index 000000000000..8fb1cc9c37f3
--- /dev/null
+++ b/src/libs/searchCountryOptions.ts
@@ -0,0 +1,39 @@
+import StringUtils from './StringUtils';
+
+type CountryData = {
+ value: string;
+ keyForList: string;
+ text: string;
+ isSelected: boolean;
+ searchValue: string;
+};
+
+/**
+ * Searches the countries/states data and returns sorted results based on the search query
+ * @param countriesData - An array of country data objects
+ * @returns An array of countries/states sorted based on the search query
+ */
+function searchCountryOptions(searchValue: string, countriesData: CountryData[]): CountryData[] {
+ if (!searchValue) {
+ return countriesData;
+ }
+
+ const trimmedSearchValue = StringUtils.sanitizeString(searchValue);
+ if (!trimmedSearchValue) {
+ return [];
+ }
+
+ const filteredData = countriesData.filter((country) => country.searchValue.includes(trimmedSearchValue));
+
+ return filteredData.sort((a, b) => {
+ if (a.value.toLowerCase() === trimmedSearchValue) {
+ return -1;
+ }
+ if (b.value.toLowerCase() === trimmedSearchValue) {
+ return 1;
+ }
+ return 0;
+ });
+}
+
+export default searchCountryOptions;
diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/index.js b/src/libs/setShouldShowComposeInputKeyboardAware/index.js
new file mode 100644
index 000000000000..a8ad5f54a65f
--- /dev/null
+++ b/src/libs/setShouldShowComposeInputKeyboardAware/index.js
@@ -0,0 +1,5 @@
+import * as Composer from '../actions/Composer';
+
+export default (shouldShow) => {
+ Composer.setShouldShowComposeInput(shouldShow);
+};
diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/index.native.js b/src/libs/setShouldShowComposeInputKeyboardAware/index.native.js
new file mode 100644
index 000000000000..147d21d51168
--- /dev/null
+++ b/src/libs/setShouldShowComposeInputKeyboardAware/index.native.js
@@ -0,0 +1,26 @@
+import {Keyboard} from 'react-native';
+import * as Composer from '../actions/Composer';
+
+let keyboardDidHideListener = null;
+export default (shouldShow) => {
+ if (keyboardDidHideListener) {
+ keyboardDidHideListener.remove();
+ keyboardDidHideListener = null;
+ }
+
+ if (!shouldShow) {
+ Composer.setShouldShowComposeInput(false);
+ return;
+ }
+
+ // If keyboard is already hidden, we should show composer immediately because keyboardDidHide event won't be called
+ if (!Keyboard.isVisible()) {
+ Composer.setShouldShowComposeInput(true);
+ return;
+ }
+
+ keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
+ Composer.setShouldShowComposeInput(true);
+ keyboardDidHideListener.remove();
+ });
+};
diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js
index 266515e29c2c..0094d174df21 100644
--- a/src/pages/EditRequestPage.js
+++ b/src/pages/EditRequestPage.js
@@ -89,7 +89,9 @@ function EditRequestPage({report, route, parentReport, policy, session}) {
if (canEdit) {
return;
}
- Navigation.dismissModal();
+ Navigation.isNavigationReady().then(() => {
+ Navigation.dismissModal();
+ });
}, [canEdit]);
// Update the transaction object and close the modal
diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.js b/src/pages/LogInWithShortLivedAuthTokenPage.js
index 1679d3aa8b5d..62eff262611d 100644
--- a/src/pages/LogInWithShortLivedAuthTokenPage.js
+++ b/src/pages/LogInWithShortLivedAuthTokenPage.js
@@ -8,7 +8,7 @@ import * as Session from '../libs/actions/Session';
import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';
import Navigation from '../libs/Navigation/Navigation';
import styles from '../styles/styles';
-import colors from '../styles/colors';
+import themeColors from '../styles/themes/default';
import Icon from '../components/Icon';
import * as Expensicons from '../components/Icon/Expensicons';
import * as Illustrations from '../components/Icon/Illustrations';
@@ -93,7 +93,7 @@ function LogInWithShortLivedAuthTokenPage(props) {
diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js
index 4a753c8632bd..e72cb9a3f79b 100755
--- a/src/pages/NewChatPage.js
+++ b/src/pages/NewChatPage.js
@@ -5,24 +5,23 @@ import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import OptionsSelector from '../components/OptionsSelector';
import * as OptionsListUtils from '../libs/OptionsListUtils';
+import Permissions from '../libs/Permissions';
import * as ReportUtils from '../libs/ReportUtils';
import ONYXKEYS from '../ONYXKEYS';
import styles from '../styles/styles';
import * as Report from '../libs/actions/Report';
import CONST from '../CONST';
import withWindowDimensions, {windowDimensionsPropTypes} from '../components/withWindowDimensions';
-import HeaderWithBackButton from '../components/HeaderWithBackButton';
import ScreenWrapper from '../components/ScreenWrapper';
+import KeyboardAvoidingView from '../components/KeyboardAvoidingView';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import * as Browser from '../libs/Browser';
import compose from '../libs/compose';
import personalDetailsPropType from './personalDetailsPropType';
import reportPropTypes from './reportPropTypes';
+import variables from '../styles/variables';
const propTypes = {
- /** Whether screen is used to create group chat */
- isGroupChat: PropTypes.bool,
-
/** Beta features list */
betas: PropTypes.arrayOf(PropTypes.string),
@@ -38,7 +37,6 @@ const propTypes = {
};
const defaultProps = {
- isGroupChat: false,
betas: [],
personalDetails: {},
reports: {},
@@ -46,7 +44,7 @@ const defaultProps = {
const excludedGroupEmails = _.without(CONST.EXPENSIFY_EMAILS, CONST.EMAIL.CONCIERGE);
-function NewChatPage(props) {
+function NewChatPage({betas, isGroupChat, personalDetails, reports, translate}) {
const [searchTerm, setSearchTerm] = useState('');
const [filteredRecentReports, setFilteredRecentReports] = useState([]);
const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]);
@@ -60,28 +58,26 @@ function NewChatPage(props) {
searchTerm,
maxParticipantsReached,
);
- const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(props.personalDetails);
+ const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails);
const sections = useMemo(() => {
const sectionsList = [];
let indexOffset = 0;
- if (props.isGroupChat) {
- sectionsList.push({
- title: undefined,
- data: selectedOptions,
- shouldShow: !_.isEmpty(selectedOptions),
- indexOffset,
- });
- indexOffset += selectedOptions.length;
+ sectionsList.push({
+ title: undefined,
+ data: selectedOptions,
+ shouldShow: !_.isEmpty(selectedOptions),
+ indexOffset,
+ });
+ indexOffset += selectedOptions.length;
- if (maxParticipantsReached) {
- return sectionsList;
- }
+ if (maxParticipantsReached) {
+ return sectionsList;
}
sectionsList.push({
- title: props.translate('common.recents'),
+ title: translate('common.recents'),
data: filteredRecentReports,
shouldShow: !_.isEmpty(filteredRecentReports),
indexOffset,
@@ -89,7 +85,7 @@ function NewChatPage(props) {
indexOffset += filteredRecentReports.length;
sectionsList.push({
- title: props.translate('common.contacts'),
+ title: translate('common.contacts'),
data: filteredPersonalDetails,
shouldShow: !_.isEmpty(filteredPersonalDetails),
indexOffset,
@@ -106,8 +102,7 @@ function NewChatPage(props) {
}
return sectionsList;
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, maxParticipantsReached, props.isGroupChat, selectedOptions]);
+ }, [translate, filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, maxParticipantsReached, selectedOptions]);
/**
* Removes a selected option from list if already selected. If not already selected add this option to the list.
@@ -124,18 +119,15 @@ function NewChatPage(props) {
newSelectedOptions = [...selectedOptions, option];
}
- const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions(
- props.reports,
- props.personalDetails,
- props.betas,
- searchTerm,
- newSelectedOptions,
- excludedGroupEmails,
- );
+ const {
+ recentReports,
+ personalDetails: newChatPersonalDetails,
+ userToInvite,
+ } = OptionsListUtils.getNewChatOptions(reports, personalDetails, betas, searchTerm, newSelectedOptions, excludedGroupEmails);
setSelectedOptions(newSelectedOptions);
setFilteredRecentReports(recentReports);
- setFilteredPersonalDetails(personalDetails);
+ setFilteredPersonalDetails(newChatPersonalDetails);
setFilteredUserToInvite(userToInvite);
}
@@ -154,9 +146,6 @@ function NewChatPage(props) {
* or navigates to the existing chat if one with those participants already exists.
*/
const createGroup = () => {
- if (!props.isGroupChat) {
- return;
- }
const logins = _.pluck(selectedOptions, 'login');
if (logins.length < 1) {
return;
@@ -165,49 +154,58 @@ function NewChatPage(props) {
};
useEffect(() => {
- const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions(
- props.reports,
- props.personalDetails,
- props.betas,
- searchTerm,
- selectedOptions,
- props.isGroupChat ? excludedGroupEmails : [],
- );
+ const {
+ recentReports,
+ personalDetails: newChatPersonalDetails,
+ userToInvite,
+ } = OptionsListUtils.getNewChatOptions(reports, personalDetails, betas, searchTerm, selectedOptions, isGroupChat ? excludedGroupEmails : []);
setFilteredRecentReports(recentReports);
- setFilteredPersonalDetails(personalDetails);
+ setFilteredPersonalDetails(newChatPersonalDetails);
setFilteredUserToInvite(userToInvite);
- // props.betas and props.isGroupChat are not added as dependencies since they don't change during the component lifecycle
+ // props.betas is not added as dependency since it doesn't change during the component lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.reports, props.personalDetails, searchTerm]);
+ }, [reports, personalDetails, searchTerm]);
return (
- {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
- <>
-
+ {({safeAreaPaddingBottomStyle, insets}) => (
+
0 ? safeAreaPaddingBottomStyle : {}]}>
toggleOption(option)}
sections={sections}
selectedOptions={selectedOptions}
value={searchTerm}
- onSelectRow={(option) => (props.isGroupChat ? toggleOption(option) : createChat(option))}
+ onSelectRow={(option) => createChat(option)}
onChangeText={setSearchTerm}
headerMessage={headerMessage}
boldStyle
- shouldFocusOnSelectRow={props.isGroupChat && !Browser.isMobile()}
- shouldShowConfirmButton={props.isGroupChat}
- shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
- confirmButtonText={props.translate('newChatPage.createGroup')}
+ shouldFocusOnSelectRow={!Browser.isMobile()}
+ shouldShowOptions={isOptionsDataReady}
+ shouldShowConfirmButton
+ confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')}
onConfirmSelection={createGroup}
- textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')}
+ textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
/>
- >
+
)}
);
diff --git a/src/pages/NewChatSelectorPage.js b/src/pages/NewChatSelectorPage.js
new file mode 100755
index 000000000000..89a3fd1adc72
--- /dev/null
+++ b/src/pages/NewChatSelectorPage.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import OnyxTabNavigator, {TopTab} from '../libs/Navigation/OnyxTabNavigator';
+import TabSelector from '../components/TabSelector/TabSelector';
+import Navigation from '../libs/Navigation/Navigation';
+import Permissions from '../libs/Permissions';
+import NewChatPage from './NewChatPage';
+import WorkspaceNewRoomPage from './workspace/WorkspaceNewRoomPage';
+import CONST from '../CONST';
+import withWindowDimensions, {windowDimensionsPropTypes} from '../components/withWindowDimensions';
+import HeaderWithBackButton from '../components/HeaderWithBackButton';
+import ScreenWrapper from '../components/ScreenWrapper';
+import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
+import compose from '../libs/compose';
+
+const propTypes = {
+ ...windowDimensionsPropTypes,
+
+ ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+ betas: [],
+ personalDetails: {},
+ reports: {},
+};
+
+function NewChatSelectorPage(props) {
+ return (
+
+
+ {Permissions.canUsePolicyRooms(props.betas) ? (
+ (
+
+ )}
+ >
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
+
+NewChatSelectorPage.propTypes = propTypes;
+NewChatSelectorPage.defaultProps = defaultProps;
+NewChatSelectorPage.displayName = 'NewChatPage';
+
+export default compose(withLocalize, withWindowDimensions)(NewChatSelectorPage);
diff --git a/src/pages/NewGroupPage.js b/src/pages/NewGroupPage.js
deleted file mode 100755
index 63f90016e63e..000000000000
--- a/src/pages/NewGroupPage.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-import NewChatPage from './NewChatPage';
-
-function NewGroupPage(props) {
- return (
-
- );
-}
-
-NewGroupPage.displayName = 'NewGroupPage';
-
-export default NewGroupPage;
diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.js
new file mode 100644
index 000000000000..4cada83941ac
--- /dev/null
+++ b/src/pages/PrivateNotes/PrivateNotesEditPage.js
@@ -0,0 +1,158 @@
+import React, {useState, useRef} from 'react';
+import PropTypes from 'prop-types';
+import {View, Keyboard} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import lodashGet from 'lodash/get';
+import Str from 'expensify-common/lib/str';
+import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import _ from 'underscore';
+import withLocalize from '../../components/withLocalize';
+import ScreenWrapper from '../../components/ScreenWrapper';
+import HeaderWithBackButton from '../../components/HeaderWithBackButton';
+import Navigation from '../../libs/Navigation/Navigation';
+import styles from '../../styles/styles';
+import compose from '../../libs/compose';
+import ONYXKEYS from '../../ONYXKEYS';
+import TextInput from '../../components/TextInput';
+import CONST from '../../CONST';
+import Text from '../../components/Text';
+import Form from '../../components/Form';
+import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
+import reportPropTypes from '../reportPropTypes';
+import personalDetailsPropType from '../personalDetailsPropType';
+import * as Report from '../../libs/actions/Report';
+import useLocalize from '../../hooks/useLocalize';
+import OfflineWithFeedback from '../../components/OfflineWithFeedback';
+import focusAndUpdateMultilineInputRange from '../../libs/focusAndUpdateMultilineInputRange';
+
+const propTypes = {
+ /** All of the personal details for everyone */
+ personalDetailsList: PropTypes.objectOf(personalDetailsPropType),
+
+ /** The report currently being looked at */
+ report: reportPropTypes,
+ route: PropTypes.shape({
+ /** Params from the URL path */
+ params: PropTypes.shape({
+ /** reportID and accountID passed via route: /r/:reportID/notes */
+ reportID: PropTypes.string,
+ accountID: PropTypes.string,
+ }),
+ }).isRequired,
+
+ /** Session of currently logged in user */
+ session: PropTypes.shape({
+ /** Currently logged in user accountID */
+ accountID: PropTypes.number,
+ }),
+};
+
+const defaultProps = {
+ report: {},
+ session: {
+ accountID: null,
+ },
+ personalDetailsList: {},
+};
+
+function PrivateNotesEditPage({route, personalDetailsList, session, report}) {
+ const {translate} = useLocalize();
+
+ // We need to edit the note in markdown format, but display it in HTML format
+ const parser = new ExpensiMark();
+ const [privateNote, setPrivateNote] = useState(parser.htmlToMarkdown(lodashGet(report, ['privateNotes', route.params.accountID, 'note'], '')).trim());
+ const isCurrentUserNote = Number(session.accountID) === Number(route.params.accountID);
+
+ // To focus on the input field when the page loads
+ const privateNotesInput = useRef(null);
+
+ const savePrivateNote = () => {
+ const editedNote = parser.replace(privateNote);
+ Report.updatePrivateNotes(report.reportID, route.params.accountID, editedNote);
+ Keyboard.dismiss();
+
+ // Take user back to the PrivateNotesView page
+ Navigation.goBack();
+ };
+
+ return (
+ focusAndUpdateMultilineInputRange(privateNotesInput.current)}
+ >
+ Navigation.goBack()}
+ >
+ Navigation.dismissModal()}
+ onBackButtonPress={() => Navigation.goBack()}
+ />
+
+
+
+ {translate(
+ Str.extractEmailDomain(lodashGet(personalDetailsList, [route.params.accountID, 'login'], '')) === CONST.EMAIL.GUIDES_DOMAIN
+ ? 'privateNotes.sharedNoteMessage'
+ : 'privateNotes.personalNoteMessage',
+ )}
+
+
+
+
+
+
+ );
+}
+
+PrivateNotesEditPage.displayName = 'PrivateNotesEditPage';
+PrivateNotesEditPage.propTypes = propTypes;
+PrivateNotesEditPage.defaultProps = defaultProps;
+
+export default compose(
+ withLocalize,
+ withOnyx({
+ report: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID.toString()}`,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ personalDetailsList: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ }),
+)(PrivateNotesEditPage);
diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.js
new file mode 100644
index 000000000000..5ea081a12f25
--- /dev/null
+++ b/src/pages/PrivateNotes/PrivateNotesListPage.js
@@ -0,0 +1,158 @@
+import React, {useMemo, useEffect} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import PropTypes from 'prop-types';
+import _ from 'underscore';
+import lodashGet from 'lodash/get';
+import Navigation from '../../libs/Navigation/Navigation';
+import ONYXKEYS from '../../ONYXKEYS';
+import CONST from '../../CONST';
+import styles from '../../styles/styles';
+import compose from '../../libs/compose';
+import OfflineWithFeedback from '../../components/OfflineWithFeedback';
+import MenuItem from '../../components/MenuItem';
+import useLocalize from '../../hooks/useLocalize';
+import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator';
+import * as Report from '../../libs/actions/Report';
+import personalDetailsPropType from '../personalDetailsPropType';
+import * as UserUtils from '../../libs/UserUtils';
+import reportPropTypes from '../reportPropTypes';
+import ScreenWrapper from '../../components/ScreenWrapper';
+import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
+import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
+import HeaderWithBackButton from '../../components/HeaderWithBackButton';
+import {withNetwork} from '../../components/OnyxProvider';
+import networkPropTypes from '../../components/networkPropTypes';
+import ROUTES from '../../ROUTES';
+
+const propTypes = {
+ /** The report currently being looked at */
+ report: reportPropTypes,
+ route: PropTypes.shape({
+ /** Params from the URL path */
+ params: PropTypes.shape({
+ /** reportID and accountID passed via route: /r/:reportID/notes */
+ reportID: PropTypes.string,
+ accountID: PropTypes.string,
+ }),
+ }).isRequired,
+
+ /** Session info for the currently logged in user. */
+ session: PropTypes.shape({
+ /** Currently logged in user accountID */
+ accountID: PropTypes.number,
+ }),
+
+ /** All of the personal details for everyone */
+ personalDetailsList: PropTypes.objectOf(personalDetailsPropType),
+
+ /** Information about the network */
+ network: networkPropTypes.isRequired,
+ ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+ report: {},
+ session: {
+ accountID: null,
+ },
+ personalDetailsList: {},
+};
+
+function PrivateNotesListPage({report, personalDetailsList, network, session}) {
+ const {translate} = useLocalize();
+
+ useEffect(() => {
+ if (network.isOffline) {
+ return;
+ }
+ Report.getReportPrivateNote(report.reportID);
+ }, [report.reportID, network.isOffline]);
+
+ /**
+ * Gets the menu item for each workspace
+ *
+ * @param {Object} item
+ * @param {Number} index
+ * @returns {JSX}
+ */
+ function getMenuItem(item, index) {
+ const keyTitle = item.translationKey ? translate(item.translationKey) : item.title;
+
+ return (
+
+
+
+ );
+ }
+
+ /**
+ * Returns a list of private notes on the given chat report
+ * @returns {Array} the menu item list
+ */
+ const privateNotes = useMemo(() => {
+ const privateNoteBrickRoadIndicator = (accountID) => (!_.isEmpty(lodashGet(report, ['privateNotes', accountID, 'errors'], '')) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '');
+ return _.chain(lodashGet(report, 'privateNotes', {}))
+ .map((privateNote, accountID) => ({
+ title: Number(lodashGet(session, 'accountID', null)) === Number(accountID) ? 'My note' : lodashGet(personalDetailsList, [accountID, 'login'], ''),
+ icon: UserUtils.getAvatar(lodashGet(personalDetailsList, [accountID, 'avatar'], UserUtils.getDefaultAvatar(accountID)), accountID),
+ iconType: CONST.ICON_TYPE_AVATAR,
+ action: () => Navigation.navigate(ROUTES.getPrivateNotesViewRoute(report.reportID, accountID)),
+ brickRoadIndicator: privateNoteBrickRoadIndicator(accountID),
+ }))
+ .value();
+ }, [report, personalDetailsList, session]);
+
+ return (
+
+ Navigation.goBack()}
+ >
+ Navigation.dismissModal()}
+ onBackButtonPress={() => Navigation.goBack()}
+ />
+ {report.isLoadingPrivateNotes && _.isEmpty(lodashGet(report, 'privateNotes', {})) ? (
+
+ ) : (
+ _.map(privateNotes, (item, index) => getMenuItem(item, index))
+ )}
+
+
+ );
+}
+
+PrivateNotesListPage.propTypes = propTypes;
+PrivateNotesListPage.defaultProps = defaultProps;
+
+export default compose(
+ withLocalize,
+ withOnyx({
+ report: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID.toString()}`,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ personalDetailsList: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ }),
+ withNetwork(),
+)(PrivateNotesListPage);
diff --git a/src/pages/PrivateNotes/PrivateNotesViewPage.js b/src/pages/PrivateNotes/PrivateNotesViewPage.js
new file mode 100644
index 000000000000..86814ed4dc92
--- /dev/null
+++ b/src/pages/PrivateNotes/PrivateNotesViewPage.js
@@ -0,0 +1,109 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {ScrollView} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import lodashGet from 'lodash/get';
+import _ from 'underscore';
+import withLocalize from '../../components/withLocalize';
+import ScreenWrapper from '../../components/ScreenWrapper';
+import HeaderWithBackButton from '../../components/HeaderWithBackButton';
+import Navigation from '../../libs/Navigation/Navigation';
+import styles from '../../styles/styles';
+import compose from '../../libs/compose';
+import ONYXKEYS from '../../ONYXKEYS';
+import ROUTES from '../../ROUTES';
+import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
+import reportPropTypes from '../reportPropTypes';
+import personalDetailsPropType from '../personalDetailsPropType';
+import useLocalize from '../../hooks/useLocalize';
+import OfflineWithFeedback from '../../components/OfflineWithFeedback';
+import MenuItemWithTopDescription from '../../components/MenuItemWithTopDescription';
+import CONST from '../../CONST';
+
+const propTypes = {
+ /** All of the personal details for everyone */
+ personalDetailsList: PropTypes.objectOf(personalDetailsPropType),
+
+ /** The report currently being looked at */
+ report: reportPropTypes,
+ route: PropTypes.shape({
+ /** Params from the URL path */
+ params: PropTypes.shape({
+ /** reportID and accountID passed via route: /r/:reportID/notes */
+ reportID: PropTypes.string,
+ accountID: PropTypes.string,
+ }),
+ }).isRequired,
+
+ /** Session of currently logged in user */
+ session: PropTypes.shape({
+ /** Currently logged in user accountID */
+ accountID: PropTypes.number,
+ }),
+};
+
+const defaultProps = {
+ report: {},
+ session: {
+ accountID: null,
+ },
+ personalDetailsList: {},
+};
+
+function PrivateNotesViewPage({route, personalDetailsList, session, report}) {
+ const {translate} = useLocalize();
+ const isCurrentUserNote = Number(session.accountID) === Number(route.params.accountID);
+ const privateNote = lodashGet(report, ['privateNotes', route.params.accountID, 'note'], '');
+
+ return (
+
+ Navigation.goBack()}
+ >
+ Navigation.dismissModal()}
+ onBackButtonPress={() => Navigation.goBack()}
+ />
+
+
+ isCurrentUserNote && Navigation.navigate(ROUTES.getPrivateNotesEditRoute(report.reportID, route.params.accountID))}
+ shouldShowRightIcon={isCurrentUserNote}
+ numberOfLinesTitle={0}
+ shouldRenderAsHTML
+ brickRoadIndicator={!_.isEmpty(lodashGet(report, ['privateNotes', route.params.accountID, 'errors'], '')) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ disabled={!isCurrentUserNote}
+ shouldGreyOutWhenDisabled={false}
+ />
+
+
+
+
+ );
+}
+
+PrivateNotesViewPage.displayName = 'PrivateNotesViewPage';
+PrivateNotesViewPage.propTypes = propTypes;
+PrivateNotesViewPage.defaultProps = defaultProps;
+
+export default compose(
+ withLocalize,
+ withOnyx({
+ report: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID.toString()}`,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ personalDetailsList: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ }),
+)(PrivateNotesViewPage);
diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js
index 22cac40cf29c..b306164a8ba0 100755
--- a/src/pages/ProfilePage.js
+++ b/src/pages/ProfilePage.js
@@ -36,6 +36,7 @@ import * as Illustrations from '../components/Icon/Illustrations';
import variables from '../styles/variables';
import * as ValidationUtils from '../libs/ValidationUtils';
import Permissions from '../libs/Permissions';
+import ROUTES from '../ROUTES';
const matchType = PropTypes.shape({
params: PropTypes.shape({
@@ -140,6 +141,8 @@ function ProfilePage(props) {
const navigateBackTo = lodashGet(props.route, 'params.backTo', '');
+ const chatReportWithCurrentUser = !isCurrentUser && !Session.isAnonymousUser() ? ReportUtils.getChatByParticipants([accountID]) : 0;
+
return (
)}
+ {!_.isEmpty(chatReportWithCurrentUser) && (
+
diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js
index 7a3373a8428c..3a9e0f5c2eb8 100644
--- a/src/pages/ReportDetailsPage.js
+++ b/src/pages/ReportDetailsPage.js
@@ -112,6 +112,18 @@ function ReportDetailsPage(props) {
});
}
+ // Prevent displaying private notes option for threads and task reports
+ if (!isThread && !ReportUtils.isTaskReport(props.report)) {
+ items.push({
+ key: CONST.REPORT_DETAILS_MENU_ITEM.PRIVATE_NOTES,
+ translationKey: 'privateNotes.title',
+ icon: Expensicons.Pencil,
+ isAnonymousAction: false,
+ action: () => Navigation.navigate(ROUTES.getPrivateNotesListRoute(props.report.reportID)),
+ brickRoadIndicator: Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '',
+ });
+ }
+
if (isUserCreatedPolicyRoom || canLeaveRoom || isThread) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.LEAVE_ROOM,
@@ -123,7 +135,7 @@ function ReportDetailsPage(props) {
}
return items;
- }, [props.report.reportID, participants, isArchivedRoom, shouldDisableSettings, isThread, isUserCreatedPolicyRoom, canLeaveRoom]);
+ }, [props.report, participants, isArchivedRoom, shouldDisableSettings, isThread, isUserCreatedPolicyRoom, canLeaveRoom]);
const displayNamesWithTooltips = useMemo(() => {
const hasMultipleParticipants = participants.length > 1;
@@ -187,7 +199,7 @@ function ReportDetailsPage(props) {
onPress={item.action}
isAnonymousAction={item.isAnonymousAction}
shouldShowRightIcon
- brickRoadIndicator={brickRoadIndicator}
+ brickRoadIndicator={brickRoadIndicator || item.brickRoadIndicator}
/>
);
})}
diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js
index dd42ed80c3d4..67d4ebb57876 100755
--- a/src/pages/SearchPage.js
+++ b/src/pages/SearchPage.js
@@ -170,6 +170,7 @@ function SearchPage({betas, personalDetails, reports}) {
textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
onLayout={searchRendered}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
+ autoFocus
/>
>
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index 74994e4dc9d0..8d389a8c8581 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -20,7 +20,7 @@ import CONST from '../../CONST';
import * as ReportUtils from '../../libs/ReportUtils';
import Text from '../../components/Text';
import Tooltip from '../../components/Tooltip';
-import colors from '../../styles/colors';
+import themeColors from '../../styles/themes/default';
import reportPropTypes from '../reportPropTypes';
import ONYXKEYS from '../../ONYXKEYS';
import ThreeDotsMenu from '../../components/ThreeDotsMenu';
@@ -209,7 +209,7 @@ function HeaderView(props) {
)}
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index 0e0f5944d3d8..004087c22308 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -247,9 +247,10 @@ function ReportScreen({
useEffect(() => {
const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => {
+ const isTopMostReportID = Navigation.getTopmostReportId() === getReportID(route);
// If the report is not fully visible (AKA on small screen devices and LHR is open) or the report is optimistic (AKA not yet created)
// we don't need to call openReport
- if (!getIsReportFullyVisible(isTopMostReportId) || report.isOptimisticReport) {
+ if (!getIsReportFullyVisible(isTopMostReportID) || report.isOptimisticReport) {
return;
}
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js
index bac79e1863e5..556366a96792 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.js
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js
@@ -10,7 +10,7 @@ import * as ReportUtils from '../../../../libs/ReportUtils';
import * as ReportActionsUtils from '../../../../libs/ReportActionsUtils';
import * as PersonalDetailsUtils from '../../../../libs/PersonalDetailsUtils';
import ReportActionComposeFocusManager from '../../../../libs/ReportActionComposeFocusManager';
-import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu';
+import {hideContextMenu, showDeleteModal, clearActiveReportAction} from './ReportActionContextMenu';
import CONST from '../../../../CONST';
import getAttachmentDetails from '../../../../libs/fileDownload/getAttachmentDetails';
import fileDownload from '../../../../libs/fileDownload';
@@ -95,9 +95,9 @@ export default [
successTextTranslateKey: 'common.download',
successIcon: Expensicons.Download,
shouldShow: (type, reportAction) => {
- const message = _.last(lodashGet(reportAction, 'message', [{}]));
- const isAttachment = _.has(reportAction, 'isAttachment') ? reportAction.isAttachment : ReportUtils.isReportMessageAttachment(message);
- return isAttachment && message.html !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction);
+ const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
+ const messageHtml = lodashGet(reportAction, ['message', 0, 'html']);
+ return isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction);
},
onPress: (closePopover, {reportAction}) => {
const message = _.last(lodashGet(reportAction, 'message', [{}]));
@@ -175,9 +175,7 @@ export default [
successTextTranslateKey: 'reportActionContextMenu.copied',
successIcon: Expensicons.Checkmark,
shouldShow: (type, reportAction) =>
- type === CONTEXT_MENU_TYPES.REPORT_ACTION &&
- !ReportUtils.isReportMessageAttachment(_.last(lodashGet(reportAction, ['message'], [{}]))) &&
- !ReportActionsUtils.isMessageDeleted(reportAction),
+ type === CONTEXT_MENU_TYPES.REPORT_ACTION && !ReportActionsUtils.isReportActionAttachment(reportAction) && !ReportActionsUtils.isMessageDeleted(reportAction),
// If return value is true, we switch the `text` and `icon` on
// `ContextMenuItem` with `successText` and `successIcon` which will fallback to
@@ -189,7 +187,7 @@ export default [
const originalMessage = _.get(reportAction, 'originalMessage', {});
const messageHtml = isTaskAction ? lodashGet(originalMessage, 'html', '') : lodashGet(message, 'html', '');
- const isAttachment = _.has(reportAction, 'isAttachment') ? reportAction.isAttachment : ReportUtils.isReportMessageAttachment(message);
+ const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
if (!isAttachment) {
const content = selection || messageHtml;
if (isReportPreviewAction) {
@@ -208,9 +206,8 @@ export default [
Clipboard.setHtml(content, plainText);
}
}
- } else {
- Clipboard.setString(messageHtml);
}
+
if (closePopover) {
hideContextMenu(true, ReportActionComposeFocusManager.focus);
}
@@ -225,8 +222,7 @@ export default [
successIcon: Expensicons.Checkmark,
successTextTranslateKey: 'reportActionContextMenu.copied',
shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget) => {
- const message = _.last(lodashGet(reportAction, 'message', [{}]));
- const isAttachment = _.has(reportAction, 'isAttachment') ? reportAction.isAttachment : ReportUtils.isReportMessageAttachment(message);
+ const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
// Only hide the copylink menu item when context menu is opened over img element.
const isAttachmentTarget = lodashGet(menuTarget, 'tagName') === 'IMG' && isAttachment;
@@ -321,12 +317,12 @@ export default [
onPress: (closePopover, {reportID, reportAction}) => {
if (closePopover) {
// Hide popover, then call showDeleteConfirmModal
- hideContextMenu(false, () => showDeleteModal(reportID, reportAction));
+ hideContextMenu(false, () => showDeleteModal(reportID, reportAction, true, clearActiveReportAction, clearActiveReportAction));
return;
}
// No popover to hide, call showDeleteConfirmModal immediately
- showDeleteModal(reportID, reportAction);
+ showDeleteModal(reportID, reportAction, true, clearActiveReportAction, clearActiveReportAction);
},
getDescription: () => {},
},
diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js
index 3633fb99fc32..dd0813132a8e 100644
--- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js
+++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js
@@ -23,6 +23,7 @@ class PopoverReportActionContextMenu extends React.Component {
reportID: '0',
reportActionID: '0',
originalReportID: '0',
+ reportAction: {},
selection: '',
reportActionDraftMessage: '',
isPopoverVisible: false,
@@ -57,6 +58,7 @@ class PopoverReportActionContextMenu extends React.Component {
this.runAndResetOnPopoverHide = this.runAndResetOnPopoverHide.bind(this);
this.getContextMenuMeasuredLocation = this.getContextMenuMeasuredLocation.bind(this);
this.isActiveReportAction = this.isActiveReportAction.bind(this);
+ this.clearActiveReportAction = this.clearActiveReportAction.bind(this);
this.dimensionsEventListener = null;
@@ -113,7 +115,11 @@ class PopoverReportActionContextMenu extends React.Component {
* @return {Boolean}
*/
isActiveReportAction(actionID) {
- return Boolean(actionID) && this.state.reportActionID === actionID;
+ return Boolean(actionID) && (this.state.reportActionID === actionID || this.state.reportAction.reportActionID === actionID);
+ }
+
+ clearActiveReportAction() {
+ this.setState({reportID: '0', reportAction: {}});
}
/**
@@ -332,10 +338,7 @@ class PopoverReportActionContextMenu extends React.Component {
shouldSetModalVisibility={this.state.shouldSetModalVisibilityForDeleteConfirmation}
onConfirm={this.confirmDeleteAndHideModal}
onCancel={this.hideDeleteModal}
- onModalHide={() => {
- this.setState({reportID: '0', reportAction: {}});
- this.callbackWhenDeleteModalHide();
- }}
+ onModalHide={this.callbackWhenDeleteModalHide}
prompt={this.props.translate('reportActionContextMenu.deleteConfirmation', {action: this.state.reportAction})}
confirmText={this.props.translate('common.delete')}
cancelText={this.props.translate('common.cancel')}
diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js
index f07bf17bcb1e..9467ff19b2f5 100644
--- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js
+++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js
@@ -128,4 +128,11 @@ function isActiveReportAction(actionID) {
return contextMenuRef.current.isActiveReportAction(actionID);
}
-export {contextMenuRef, showContextMenu, hideContextMenu, isActiveReportAction, showDeleteModal, hideDeleteModal};
+function clearActiveReportAction() {
+ if (!contextMenuRef.current) {
+ return;
+ }
+ return contextMenuRef.current.clearActiveReportAction();
+}
+
+export {contextMenuRef, showContextMenu, hideContextMenu, isActiveReportAction, clearActiveReportAction, showDeleteModal, hideDeleteModal};
diff --git a/src/pages/home/report/FloatingMessageCounter/index.js b/src/pages/home/report/FloatingMessageCounter/index.js
index 73fe02df129b..c477b8137bea 100644
--- a/src/pages/home/report/FloatingMessageCounter/index.js
+++ b/src/pages/home/report/FloatingMessageCounter/index.js
@@ -9,6 +9,7 @@ import * as Expensicons from '../../../../components/Icon/Expensicons';
import themeColors from '../../../../styles/themes/default';
import useLocalize from '../../../../hooks/useLocalize';
import FloatingMessageCounterContainer from './FloatingMessageCounterContainer';
+import useNativeDriver from '../../../../libs/useNativeDriver';
const propTypes = {
/** Whether the New Messages indicator is active */
@@ -34,7 +35,7 @@ function FloatingMessageCounter(props) {
Animated.spring(translateY, {
toValue: MARKER_ACTIVE_TRANSLATE_Y,
duration: 80,
- useNativeDriver: true,
+ useNativeDriver,
}).start();
}, [translateY]);
@@ -42,7 +43,7 @@ function FloatingMessageCounter(props) {
Animated.spring(translateY, {
toValue: MARKER_INACTIVE_TRANSLATE_Y,
duration: 80,
- useNativeDriver: true,
+ useNativeDriver,
}).start();
}, [translateY]);
diff --git a/src/pages/home/report/LinkPreviewer.js b/src/pages/home/report/LinkPreviewer.js
index 7c88e9d54126..4fcbb0dc0569 100644
--- a/src/pages/home/report/LinkPreviewer.js
+++ b/src/pages/home/report/LinkPreviewer.js
@@ -8,7 +8,7 @@ import TextLink from '../../../components/TextLink';
import * as StyleUtils from '../../../styles/StyleUtils';
import styles from '../../../styles/styles';
import variables from '../../../styles/variables';
-import colors from '../../../styles/colors';
+import themeColors from '../../../styles/themes/default';
const IMAGE_TYPES = ['jpg', 'jpeg', 'png'];
const MAX_IMAGE_HEIGHT = 180;
@@ -99,7 +99,7 @@ function LinkPreviewer(props) {
{!_.isEmpty(title) && (
{title}
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js
index 89ed9ded787e..ddcd43cd8cd0 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js
@@ -279,7 +279,9 @@ function ReportActionCompose({
const onBlur = useCallback((e) => {
setIsFocused(false);
- suggestionsRef.current.resetSuggestions();
+ if (suggestionsRef.current) {
+ suggestionsRef.current.resetSuggestions();
+ }
if (e.relatedTarget && e.relatedTarget === actionButtonRef.current) {
isKeyboardVisibleWhenShowingModalRef.current = true;
}
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 061f66513932..fae5c518bbfe 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -138,6 +138,12 @@ function ReportActionItem(props) {
const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action);
const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID);
+ // When active action changes, we need to update the `isContextMenuActive` state
+ const isActiveReportActionForMenu = ReportActionContextMenu.isActiveReportAction(props.action.reportActionID);
+ useEffect(() => {
+ setIsContextMenuActive(isActiveReportActionForMenu);
+ }, [isActiveReportActionForMenu]);
+
const updateHiddenState = useCallback(
(isHiddenValue) => {
setIsHidden(isHiddenValue);
@@ -599,7 +605,7 @@ function ReportActionItem(props) {
needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)}
>
{isWhisper && (
-
+
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js
index fab54846ab14..43ff5c00a4d5 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.js
+++ b/src/pages/home/report/ReportActionItemMessageEdit.js
@@ -14,7 +14,7 @@ import containerComposeStyles from '../../../styles/containerComposeStyles';
import Composer from '../../../components/Composer';
import * as Report from '../../../libs/actions/Report';
import {withReportActionsDrafts} from '../../../components/OnyxProvider';
-import openReportActionComposeViewWhenClosingMessageEdit from '../../../libs/openReportActionComposeViewWhenClosingMessageEdit';
+import setShouldShowComposeInputKeyboardAware from '../../../libs/setShouldShowComposeInputKeyboardAware';
import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager';
import EmojiPickerButton from '../../../components/EmojiPicker/EmojiPickerButton';
import Icon from '../../../components/Icon';
@@ -28,7 +28,6 @@ import ExceededCommentLength from '../../../components/ExceededCommentLength';
import CONST from '../../../CONST';
import refPropTypes from '../../../components/refPropTypes';
import * as ComposerUtils from '../../../libs/ComposerUtils';
-import * as ComposerActions from '../../../libs/actions/Composer';
import * as User from '../../../libs/actions/User';
import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback';
import getButtonState from '../../../libs/getButtonState';
@@ -84,8 +83,6 @@ const defaultProps = {
};
// native ids
-const saveButtonID = 'saveButton';
-const cancelButtonID = 'cancelButton';
const emojiButtonID = 'emojiButton';
const messageEditInput = 'messageEditInput';
@@ -130,6 +127,12 @@ function ReportActionItemMessageEdit(props) {
isFocusedRef.current = isFocused;
}, [isFocused]);
+ // We consider the report action active if it's focused, its emoji picker is open or its context menu is open
+ const isActive = useCallback(
+ () => isFocusedRef.current || EmojiPickerAction.isActive(props.action.reportActionID) || ReportActionContextMenu.isActiveReportAction(props.action.reportActionID),
+ [props.action.reportActionID],
+ );
+
useEffect(() => {
// For mobile Safari, updating the selection prop on an unfocused input will cause it to automatically gain focus
// and subsequent programmatic focus shifts (e.g., modal focus trap) to show the blue frame (:focus-visible style),
@@ -145,16 +148,23 @@ function ReportActionItemMessageEdit(props) {
}
return () => {
- // Skip if this is not the focused message so the other edit composer stays focused.
- // In small screen devices, when EmojiPicker is shown, the current edit message will lose focus, we need to check this case as well.
- if (!isFocusedRef.current && !EmojiPickerAction.isActive(props.action.reportActionID)) {
+ // Skip if the current report action is not active
+ if (!isActive()) {
return;
}
+ if (EmojiPickerAction.isActive(props.action.reportActionID)) {
+ EmojiPickerAction.clearActive();
+ }
+ if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) {
+ ReportActionContextMenu.clearActiveReportAction();
+ }
+
// Show the main composer when the focused message is deleted from another client
// to prevent the main composer stays hidden until we swtich to another chat.
- ComposerActions.setShouldShowComposeInput(true);
+ setShouldShowComposeInputKeyboardAware(true);
};
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- this cleanup needs to be called only on unmount
}, [props.action.reportActionID]);
/**
@@ -227,9 +237,11 @@ function ReportActionItemMessageEdit(props) {
const deleteDraft = useCallback(() => {
debouncedSaveDraft.cancel();
Report.saveReportActionDraft(props.reportID, props.action.reportActionID, '');
- ComposerActions.setShouldShowComposeInput(true);
- ReportActionComposeFocusManager.clear();
- ReportActionComposeFocusManager.focus();
+
+ if (isActive()) {
+ ReportActionComposeFocusManager.clear();
+ ReportActionComposeFocusManager.focus();
+ }
// Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report.
if (props.index === 0) {
@@ -238,7 +250,7 @@ function ReportActionItemMessageEdit(props) {
keyboardDidHideListener.remove();
});
}
- }, [props.action.reportActionID, debouncedSaveDraft, props.index, props.reportID, reportScrollManager]);
+ }, [props.action.reportActionID, debouncedSaveDraft, props.index, props.reportID, reportScrollManager, isActive]);
/**
* Save the draft of the comment to be the new comment message. This will take the comment out of "edit mode" with
@@ -273,6 +285,7 @@ function ReportActionItemMessageEdit(props) {
// When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting.
if (!trimmedNewDraft) {
+ textInputRef.current.blur();
ReportActionContextMenu.showDeleteModal(props.reportID, props.action, false, deleteDraft, () => InteractionManager.runAfterInteractions(() => textInputRef.current.focus()));
return;
}
@@ -325,7 +338,6 @@ function ReportActionItemMessageEdit(props) {
e.preventDefault()}
>
{({hovered, pressed}) => (
{
setIsFocused(true);
reportScrollManager.scrollToIndex({animated: true, index: props.index}, true);
- ComposerActions.setShouldShowComposeInput(false);
+ setShouldShowComposeInputKeyboardAware(false);
+
+ // Clear active report action when another action gets focused
+ if (!EmojiPickerAction.isActive(props.action.reportActionID)) {
+ EmojiPickerAction.clearActive();
+ }
+ if (!ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) {
+ ReportActionContextMenu.clearActiveReportAction();
+ }
}}
onBlur={(event) => {
setIsFocused(false);
const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id');
-
- // Return to prevent re-render when save/cancel button is pressed which cancels the onPress event by re-rendering
- if (_.contains([saveButtonID, cancelButtonID, emojiButtonID], relatedTargetId)) {
- return;
- }
-
- if (messageEditInput === relatedTargetId) {
+ if (_.contains([messageEditInput, emojiButtonID], relatedTargetId)) {
return;
}
- openReportActionComposeViewWhenClosingMessageEdit();
+ setShouldShowComposeInputKeyboardAware(true);
}}
selection={selection}
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
@@ -407,12 +423,13 @@ function ReportActionItemMessageEdit(props) {
e.preventDefault()}
>
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
index b96ad4f4bfef..418b7c89aa91 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
@@ -12,7 +12,6 @@ import NAVIGATORS from '../../../../NAVIGATORS';
import SCREENS from '../../../../SCREENS';
import Permissions from '../../../../libs/Permissions';
import * as Policy from '../../../../libs/actions/Policy';
-import * as PolicyUtils from '../../../../libs/PolicyUtils';
import PopoverMenu from '../../../../components/PopoverMenu';
import CONST from '../../../../CONST';
import FloatingActionButton from '../../../../components/FloatingActionButton';
@@ -180,8 +179,6 @@ function FloatingActionButtonAndPopover(props) {
},
}));
- const workspaces = PolicyUtils.getActivePolicies(props.allPolicies);
-
return (
interceptAnonymousUser(() => Navigation.navigate(ROUTES.NEW_CHAT)),
- },
- {
- icon: Expensicons.Users,
- text: props.translate('sidebarScreen.newGroup'),
- onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.NEW_GROUP)),
+ text: props.translate('sidebarScreen.fabNewChat'),
+ onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.NEW)),
},
- ...(Permissions.canUsePolicyRooms(props.betas) && workspaces.length
- ? [
- {
- icon: Expensicons.Hashtag,
- text: props.translate('sidebarScreen.newRoom'),
- onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_NEW_ROOM)),
- },
- ]
- : []),
...(Permissions.canUseIOUSend(props.betas)
? [
{
@@ -229,11 +212,6 @@ function FloatingActionButtonAndPopover(props) {
text: props.translate('sidebarScreen.saveTheWorld'),
onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TEACHERS_UNITE)),
},
- {
- icon: Expensicons.Receipt,
- text: props.translate('iou.splitBill'),
- onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT)),
- },
...(Permissions.canUseTasks(props.betas)
? [
{
@@ -260,7 +238,7 @@ function FloatingActionButtonAndPopover(props) {
anchorRef={anchorRef}
/>
);
diff --git a/src/pages/iou/MoneyRequestDatePage.js b/src/pages/iou/MoneyRequestDatePage.js
index 428a1a3b4593..68af1a6244b6 100644
--- a/src/pages/iou/MoneyRequestDatePage.js
+++ b/src/pages/iou/MoneyRequestDatePage.js
@@ -61,10 +61,10 @@ function MoneyRequestDatePage({iou, route, selectedTab}) {
IOU.resetMoneyRequestInfo(moneyRequestId);
}
- if (!isDistanceRequest && (_.isEmpty(iou.participants) || (iou.amount === 0 && !iou.receiptPath) || shouldReset)) {
+ if (!isDistanceRequest && (_.isEmpty(iou.participantAccountIDs) || (iou.amount === 0 && !iou.receiptPath) || shouldReset)) {
Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true);
}
- }, [iou.id, iou.participants, iou.amount, iou.receiptPath, iouType, reportID, isDistanceRequest]);
+ }, [iou.id, iou.participantAccountIDs, iou.amount, iou.receiptPath, iouType, reportID, isDistanceRequest]);
function navigateBack() {
Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js
index 0678088073d3..63aea67ce598 100644
--- a/src/pages/iou/MoneyRequestDescriptionPage.js
+++ b/src/pages/iou/MoneyRequestDescriptionPage.js
@@ -65,10 +65,10 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) {
IOU.resetMoneyRequestInfo(moneyRequestId);
}
- if (!isDistanceRequest && (_.isEmpty(iou.participants) || (iou.amount === 0 && !iou.receiptPath) || shouldReset)) {
+ if (!isDistanceRequest && (_.isEmpty(iou.participantAccountIDs) || (iou.amount === 0 && !iou.receiptPath) || shouldReset)) {
Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true);
}
- }, [iou.id, iou.participants, iou.amount, iou.receiptPath, iouType, reportID, isDistanceRequest]);
+ }, [iou.id, iou.participantAccountIDs, iou.amount, iou.receiptPath, iouType, reportID, isDistanceRequest]);
function navigateBack() {
Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
diff --git a/src/pages/iou/MoneyRequestMerchantPage.js b/src/pages/iou/MoneyRequestMerchantPage.js
index 9a49b0259bd2..f8ae810eafba 100644
--- a/src/pages/iou/MoneyRequestMerchantPage.js
+++ b/src/pages/iou/MoneyRequestMerchantPage.js
@@ -58,10 +58,10 @@ function MoneyRequestMerchantPage({iou, route}) {
IOU.resetMoneyRequestInfo(moneyRequestId);
}
- if (_.isEmpty(iou.participants) || (iou.amount === 0 && !iou.receiptPath) || shouldReset) {
+ if (_.isEmpty(iou.participantAccountIDs) || (iou.amount === 0 && !iou.receiptPath) || shouldReset) {
Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true);
}
- }, [iou.id, iou.participants, iou.amount, iou.receiptPath, iouType, reportID]);
+ }, [iou.id, iou.participantAccountIDs, iou.amount, iou.receiptPath, iouType, reportID]);
function navigateBack() {
Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js
index 1c653271ea7d..38a5b9c82c07 100644
--- a/src/pages/iou/steps/MoneyRequestConfirmPage.js
+++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js
@@ -258,12 +258,24 @@ function MoneyRequestConfirmPage(props) {
[props.iou.amount, props.iou.comment, participants, props.iou.currency, props.currentUserPersonalDetails.accountID, props.report],
);
+ const headerTitle = () => {
+ if (isDistanceRequest) {
+ return props.translate('common.distance');
+ }
+
+ if (iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT) {
+ return props.translate('iou.split');
+ }
+
+ return props.translate('tabSelector.manual');
+ };
+
return (
{({safeAreaPaddingBottomStyle}) => (
{/*
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
index b44b956ac547..1d9f12a9cdbb 100644
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
@@ -1,23 +1,22 @@
-import React, {useEffect, useRef} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
+import _ from 'underscore';
import CONST from '../../../../CONST';
import ONYXKEYS from '../../../../ONYXKEYS';
import ROUTES from '../../../../ROUTES';
-import MoneyRequestParticipantsSplitSelector from './MoneyRequestParticipantsSplitSelector';
import MoneyRequestParticipantsSelector from './MoneyRequestParticipantsSelector';
import styles from '../../../../styles/styles';
import ScreenWrapper from '../../../../components/ScreenWrapper';
-import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
import Navigation from '../../../../libs/Navigation/Navigation';
-import compose from '../../../../libs/compose';
import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities';
import HeaderWithBackButton from '../../../../components/HeaderWithBackButton';
import * as IOU from '../../../../libs/actions/IOU';
import * as MoneyRequestUtils from '../../../../libs/MoneyRequestUtils';
import {iouPropTypes, iouDefaultProps} from '../../propTypes';
+import useLocalize from '../../../../hooks/useLocalize';
const propTypes = {
/** React Navigation route */
@@ -37,23 +36,35 @@ const propTypes = {
/** The current tab we have navigated to in the request modal. String that corresponds to the request type. */
selectedTab: PropTypes.oneOf([CONST.TAB.DISTANCE, CONST.TAB.MANUAL, CONST.TAB.SCAN]).isRequired,
-
- ...withLocalizePropTypes,
};
const defaultProps = {
iou: iouDefaultProps,
};
-function MoneyRequestParticipantsPage(props) {
- const prevMoneyRequestId = useRef(props.iou.id);
- const iouType = useRef(lodashGet(props.route, 'params.iouType', ''));
- const reportID = useRef(lodashGet(props.route, 'params.reportID', ''));
+function MoneyRequestParticipantsPage({iou, selectedTab, route}) {
+ const {translate} = useLocalize();
+ const prevMoneyRequestId = useRef(iou.id);
+ const isNewReportIDSelectedLocally = useRef(false);
const optionsSelectorRef = useRef();
- const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType.current, props.selectedTab);
+ const iouType = useRef(lodashGet(route, 'params.iouType', ''));
+ const reportID = useRef(lodashGet(route, 'params.reportID', ''));
+ const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType.current, selectedTab);
+ const isSplitRequest = iou.id === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT;
+ const [headerTitle, setHeaderTitle] = useState();
- const navigateToNextStep = () => {
- Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(iouType.current, reportID.current));
+ useEffect(() => {
+ if (isDistanceRequest) {
+ setHeaderTitle(translate('common.distance'));
+ return;
+ }
+
+ setHeaderTitle(_.isEmpty(iou.participants) ? translate('tabSelector.manual') : translate('iou.split'));
+ }, [iou.participants, isDistanceRequest, translate]);
+
+ const navigateToNextStep = (moneyRequestType) => {
+ IOU.setMoneyRequestId(moneyRequestType);
+ Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(moneyRequestType, reportID.current));
};
const navigateBack = (forceFallback = false) => {
@@ -62,9 +73,9 @@ function MoneyRequestParticipantsPage(props) {
useEffect(() => {
// ID in Onyx could change by initiating a new request in a separate browser tab or completing a request
- if (prevMoneyRequestId.current !== props.iou.id) {
+ if (prevMoneyRequestId.current !== iou.id) {
// The ID is cleared on completing a request. In that case, we will do nothing
- if (!isDistanceRequest && props.iou.id) {
+ if (iou.id && !isDistanceRequest && !isSplitRequest && !isNewReportIDSelectedLocally.current) {
navigateBack(true);
}
return;
@@ -72,18 +83,18 @@ function MoneyRequestParticipantsPage(props) {
// Reset the money request Onyx if the ID in Onyx does not match the ID from params
const moneyRequestId = `${iouType.current}${reportID.current}`;
- const shouldReset = props.iou.id !== moneyRequestId;
+ const shouldReset = iou.id !== moneyRequestId && !isNewReportIDSelectedLocally.current;
if (shouldReset) {
IOU.resetMoneyRequestInfo(moneyRequestId);
}
- if (!isDistanceRequest && ((props.iou.amount === 0 && !props.iou.receiptPath) || shouldReset)) {
+ if (!isDistanceRequest && ((iou.amount === 0 && !iou.receiptPath) || shouldReset)) {
navigateBack(true);
}
return () => {
- prevMoneyRequestId.current = props.iou.id;
+ prevMoneyRequestId.current = iou.id;
};
- }, [props.iou.amount, props.iou.id, props.iou.receiptPath, isDistanceRequest]);
+ }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest]);
return (
(
- {iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT ? (
-
- ) : (
- (optionsSelectorRef.current = el)}
- onStepComplete={navigateToNextStep}
- onAddParticipants={IOU.setMoneyRequestParticipants}
- safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
- iouType={iouType.current}
- isDistanceRequest={isDistanceRequest}
- />
- )}
+ (optionsSelectorRef.current = el)}
+ participants={iou.participants}
+ onAddParticipants={IOU.setMoneyRequestParticipants}
+ navigateToRequest={() => navigateToNextStep(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)}
+ navigateToSplit={() => navigateToNextStep(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT)}
+ safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
+ iouType={iouType.current}
+ isDistanceRequest={isDistanceRequest}
+ />
)}
@@ -124,12 +128,11 @@ MoneyRequestParticipantsPage.displayName = 'IOUParticipantsPage';
MoneyRequestParticipantsPage.propTypes = propTypes;
MoneyRequestParticipantsPage.defaultProps = defaultProps;
-export default compose(
- withLocalize,
- withOnyx({
- iou: {key: ONYXKEYS.IOU},
- selectedTab: {
- key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`,
- },
- }),
-)(MoneyRequestParticipantsPage);
+export default withOnyx({
+ iou: {
+ key: ONYXKEYS.IOU,
+ },
+ selectedTab: {
+ key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`,
+ },
+})(MoneyRequestParticipantsPage);
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
index 693a55b14e07..9ff787ebe21b 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
@@ -1,12 +1,15 @@
-import React, {Component} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
+import ONYXKEYS from '../../../../ONYXKEYS';
+import styles from '../../../../styles/styles';
+import OptionsSelector from '../../../../components/OptionsSelector';
import * as OptionsListUtils from '../../../../libs/OptionsListUtils';
import * as ReportUtils from '../../../../libs/ReportUtils';
-import OptionsSelector from '../../../../components/OptionsSelector';
-import ONYXKEYS from '../../../../ONYXKEYS';
import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
+import * as Browser from '../../../../libs/Browser';
import compose from '../../../../libs/compose';
import CONST from '../../../../CONST';
import personalDetailsPropType from '../../../personalDetailsPropType';
@@ -17,15 +20,29 @@ const propTypes = {
/** Beta features list */
betas: PropTypes.arrayOf(PropTypes.string),
+ /** Callback to request parent modal to go to next step, which should be split */
+ navigateToRequest: PropTypes.func.isRequired,
+
+ /** Callback to request parent modal to go to next step, which should be split */
+ navigateToSplit: PropTypes.func.isRequired,
+
/** A ref to forward to options selector's text input */
forwardedRef: refPropTypes,
- /** Callback to inform parent modal of success */
- onStepComplete: PropTypes.func.isRequired,
-
/** Callback to add participants in MoneyRequestModal */
onAddParticipants: PropTypes.func.isRequired,
+ /** Selected participants from MoneyRequestModal with login */
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ accountID: PropTypes.number,
+ login: PropTypes.string,
+ isPolicyExpenseChat: PropTypes.bool,
+ isOwnPolicyExpenseChat: PropTypes.bool,
+ selected: PropTypes.bool,
+ }),
+ ),
+
/** All of the personal details for everyone */
personalDetails: PropTypes.objectOf(personalDetailsPropType),
@@ -45,6 +62,7 @@ const propTypes = {
};
const defaultProps = {
+ participants: [],
forwardedRef: undefined,
safeAreaPaddingBottomStyle: {},
personalDetails: {},
@@ -53,137 +71,193 @@ const defaultProps = {
isDistanceRequest: false,
};
-class MoneyRequestParticipantsSelector extends Component {
- constructor(props) {
- super(props);
+function MoneyRequestParticipantsSelector({
+ forwardedRef,
+ betas,
+ participants,
+ personalDetails,
+ reports,
+ translate,
+ navigateToRequest,
+ navigateToSplit,
+ onAddParticipants,
+ safeAreaPaddingBottomStyle,
+ iouType,
+ isDistanceRequest,
+}) {
+ const [searchTerm, setSearchTerm] = useState('');
+ const [newChatOptions, setNewChatOptions] = useState({
+ recentReports: [],
+ personalDetails: [],
+ userToInvite: null,
+ });
- this.addSingleParticipant = this.addSingleParticipant.bind(this);
- this.updateOptionsWithSearchTerm = this.updateOptionsWithSearchTerm.bind(this);
-
- const {recentReports, personalDetails, userToInvite} = this.getRequestOptions();
-
- this.state = {
- recentReports,
- personalDetails,
- userToInvite,
- searchTerm: '',
- };
- }
-
- componentDidUpdate(prevProps) {
- if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) {
- return;
- }
- this.updateOptionsWithSearchTerm(this.state.searchTerm);
- }
-
- /**
- * @param {string} searchTerm
- * @returns {Object}
- */
- getRequestOptions(searchTerm = '') {
- return OptionsListUtils.getNewChatOptions(
- this.props.reports,
- this.props.personalDetails,
- this.props.betas,
- searchTerm,
- [],
- CONST.EXPENSIFY_EMAILS,
-
- // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user
- // sees the option to request money from their admin on their own Workspace Chat.
- this.props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST,
-
- // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features.
- !this.props.isDistanceRequest,
- );
- }
+ const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
/**
* Returns the sections needed for the OptionsSelector
*
* @returns {Array}
*/
- getSections() {
- const sections = [];
+ const sections = useMemo(() => {
+ const newSections = [];
let indexOffset = 0;
- sections.push({
- title: this.props.translate('common.recents'),
- data: this.state.recentReports,
- shouldShow: !_.isEmpty(this.state.recentReports),
+ newSections.push({
+ title: undefined,
+ data: OptionsListUtils.getParticipantsOptions(participants, personalDetails),
+ shouldShow: true,
indexOffset,
});
- indexOffset += this.state.recentReports.length;
+ indexOffset += participants.length;
+
+ if (maxParticipantsReached) {
+ return newSections;
+ }
- sections.push({
- title: this.props.translate('common.contacts'),
- data: this.state.personalDetails,
- shouldShow: !_.isEmpty(this.state.personalDetails),
+ newSections.push({
+ title: translate('common.recents'),
+ data: newChatOptions.recentReports,
+ shouldShow: !_.isEmpty(newChatOptions.recentReports),
indexOffset,
});
- indexOffset += this.state.personalDetails.length;
+ indexOffset += newChatOptions.recentReports.length;
- if (this.state.userToInvite && !OptionsListUtils.isCurrentUser(this.state.userToInvite)) {
- sections.push({
+ newSections.push({
+ title: translate('common.contacts'),
+ data: newChatOptions.personalDetails,
+ shouldShow: !_.isEmpty(newChatOptions.personalDetails),
+ indexOffset,
+ });
+ indexOffset += newChatOptions.personalDetails.length;
+
+ if (newChatOptions.userToInvite && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) {
+ newSections.push({
undefined,
- data: [this.state.userToInvite],
+ data: [newChatOptions.userToInvite],
shouldShow: true,
indexOffset,
});
}
- return sections;
- }
-
- updateOptionsWithSearchTerm(searchTerm = '') {
- const {recentReports, personalDetails, userToInvite} = this.getRequestOptions(searchTerm);
- this.setState({
- searchTerm,
- recentReports,
- userToInvite,
- personalDetails,
- });
- }
+ return newSections;
+ }, [maxParticipantsReached, newChatOptions, participants, personalDetails, translate]);
/**
* Adds a single participant to the request
*
* @param {Object} option
*/
- addSingleParticipant(option) {
- this.props.onAddParticipants([{accountID: option.accountID, login: option.login, isPolicyExpenseChat: option.isPolicyExpenseChat, reportID: option.reportID, selected: true}]);
- this.props.onStepComplete();
- }
-
- render() {
- const headerMessage = OptionsListUtils.getHeaderMessage(
- this.state.personalDetails.length + this.state.recentReports.length !== 0,
- Boolean(this.state.userToInvite),
- this.state.searchTerm,
+ const addSingleParticipant = (option) => {
+ onAddParticipants([{accountID: option.accountID, login: option.login, isPolicyExpenseChat: option.isPolicyExpenseChat, reportID: option.reportID, selected: true}]);
+ navigateToRequest();
+ };
+
+ /**
+ * Removes a selected option from list if already selected. If not already selected add this option to the list.
+ * @param {Object} option
+ */
+ const addParticipantToSelection = useCallback(
+ (option) => {
+ const isOptionInList = _.some(participants, (selectedOption) => selectedOption.accountID === option.accountID);
+
+ let newSelectedOptions;
+
+ if (isOptionInList) {
+ newSelectedOptions = _.reject(participants, (selectedOption) => selectedOption.accountID === option.accountID);
+ } else {
+ newSelectedOptions = [...participants, {accountID: option.accountID, login: option.login, selected: true}];
+ }
+
+ onAddParticipants(newSelectedOptions);
+
+ const chatOptions = OptionsListUtils.getNewChatOptions(
+ reports,
+ personalDetails,
+ betas,
+ isOptionInList ? searchTerm : '',
+ newSelectedOptions,
+ CONST.EXPENSIFY_EMAILS,
+
+ // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user
+ // sees the option to request money from their admin on their own Workspace Chat.
+ iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST,
+
+ // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features.
+ !isDistanceRequest,
+ );
+
+ setNewChatOptions({
+ recentReports: chatOptions.recentReports,
+ personalDetails: chatOptions.personalDetails,
+ userToInvite: chatOptions.userToInvite,
+ });
+ },
+ [participants, onAddParticipants, reports, personalDetails, betas, searchTerm, iouType, isDistanceRequest],
+ );
+
+ const headerMessage = OptionsListUtils.getHeaderMessage(
+ newChatOptions.personalDetails.length + newChatOptions.recentReports.length !== 0,
+ Boolean(newChatOptions.userToInvite),
+ searchTerm,
+ maxParticipantsReached,
+ );
+ const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails);
+
+ useEffect(() => {
+ const chatOptions = OptionsListUtils.getNewChatOptions(
+ reports,
+ personalDetails,
+ betas,
+ searchTerm,
+ participants,
+ CONST.EXPENSIFY_EMAILS,
+
+ // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user
+ // sees the option to request money from their admin on their own Workspace Chat.
+ iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST,
+
+ // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features.
+ !isDistanceRequest,
);
- const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails);
+ setNewChatOptions({
+ recentReports: chatOptions.recentReports,
+ personalDetails: chatOptions.personalDetails,
+ userToInvite: chatOptions.userToInvite,
+ });
+ }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, isDistanceRequest]);
- return (
+ return (
+ 0 ? safeAreaPaddingBottomStyle : {}]}>
- );
- }
+
+ );
}
MoneyRequestParticipantsSelector.propTypes = propTypes;
MoneyRequestParticipantsSelector.defaultProps = defaultProps;
+MoneyRequestParticipantsSelector.displayName = 'MoneyRequestParticipantsSelector';
export default compose(
withLocalize,
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSplitSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSplitSelector.js
deleted file mode 100755
index 2ebddbdd8741..000000000000
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSplitSelector.js
+++ /dev/null
@@ -1,206 +0,0 @@
-import React, {useCallback, useEffect, useMemo, useState} from 'react';
-import {View} from 'react-native';
-import PropTypes from 'prop-types';
-import _ from 'underscore';
-import {withOnyx} from 'react-native-onyx';
-import ONYXKEYS from '../../../../ONYXKEYS';
-import styles from '../../../../styles/styles';
-import OptionsSelector from '../../../../components/OptionsSelector';
-import * as OptionsListUtils from '../../../../libs/OptionsListUtils';
-import * as ReportUtils from '../../../../libs/ReportUtils';
-import CONST from '../../../../CONST';
-import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
-import compose from '../../../../libs/compose';
-import personalDetailsPropType from '../../../personalDetailsPropType';
-import * as Browser from '../../../../libs/Browser';
-import reportPropTypes from '../../../reportPropTypes';
-
-const propTypes = {
- /** Beta features list */
- betas: PropTypes.arrayOf(PropTypes.string),
-
- /** Callback to inform parent modal of success */
- onStepComplete: PropTypes.func.isRequired,
-
- /** Callback to add participants in MoneyRequestModal */
- onAddParticipants: PropTypes.func.isRequired,
-
- /** Selected participants from MoneyRequestModal with login */
- participants: PropTypes.arrayOf(
- PropTypes.shape({
- accountID: PropTypes.number,
- login: PropTypes.string,
- isPolicyExpenseChat: PropTypes.bool,
- isOwnPolicyExpenseChat: PropTypes.bool,
- selected: PropTypes.bool,
- }),
- ),
-
- /** All of the personal details for everyone */
- personalDetails: PropTypes.objectOf(personalDetailsPropType),
-
- /** All reports shared with the user */
- reports: PropTypes.objectOf(reportPropTypes),
-
- /** padding bottom style of safe area */
- safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- participants: [],
- betas: [],
- personalDetails: {},
- reports: {},
- safeAreaPaddingBottomStyle: {},
-};
-
-function MoneyRequestParticipantsSplitSelector({betas, participants, personalDetails, reports, translate, onAddParticipants, onStepComplete, safeAreaPaddingBottomStyle}) {
- const [searchTerm, setSearchTerm] = useState('');
- const [newChatOptions, setNewChatOptions] = useState({
- recentReports: [],
- personalDetails: [],
- userToInvite: null,
- });
-
- const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
-
- /**
- * Returns the sections needed for the OptionsSelector
- *
- * @param {Boolean} maxParticipantsReached
- * @returns {Array}
- */
- const sections = useMemo(() => {
- const newSections = [];
- let indexOffset = 0;
-
- newSections.push({
- title: undefined,
- data: OptionsListUtils.getParticipantsOptions(participants, personalDetails),
- shouldShow: true,
- indexOffset,
- });
- indexOffset += participants.length;
-
- if (maxParticipantsReached) {
- return newSections;
- }
-
- newSections.push({
- title: translate('common.recents'),
- data: newChatOptions.recentReports,
- shouldShow: !_.isEmpty(newChatOptions.recentReports),
- indexOffset,
- });
- indexOffset += newChatOptions.recentReports.length;
-
- newSections.push({
- title: translate('common.contacts'),
- data: newChatOptions.personalDetails,
- shouldShow: !_.isEmpty(newChatOptions.personalDetails),
- indexOffset,
- });
- indexOffset += newChatOptions.personalDetails.length;
-
- if (newChatOptions.userToInvite && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) {
- newSections.push({
- undefined,
- data: [newChatOptions.userToInvite],
- shouldShow: true,
- indexOffset,
- });
- }
-
- return newSections;
- }, [maxParticipantsReached, newChatOptions, participants, personalDetails, translate]);
-
- /**
- * Removes a selected option from list if already selected. If not already selected add this option to the list.
- * @param {Object} option
- */
- const toggleOption = useCallback(
- (option) => {
- const isOptionInList = _.some(participants, (selectedOption) => selectedOption.accountID === option.accountID);
-
- let newSelectedOptions;
-
- if (isOptionInList) {
- newSelectedOptions = _.reject(participants, (selectedOption) => selectedOption.accountID === option.accountID);
- } else {
- newSelectedOptions = [...participants, {accountID: option.accountID, login: option.login, selected: true, searchText: option.searchText}];
- }
-
- onAddParticipants(newSelectedOptions);
-
- const chatOptions = OptionsListUtils.getNewChatOptions(reports, personalDetails, betas, isOptionInList ? searchTerm : '', newSelectedOptions, CONST.EXPENSIFY_EMAILS);
-
- setNewChatOptions({
- recentReports: chatOptions.recentReports,
- personalDetails: chatOptions.personalDetails,
- userToInvite: chatOptions.userToInvite,
- });
- },
- [searchTerm, participants, onAddParticipants, reports, personalDetails, betas, setNewChatOptions],
- );
-
- const headerMessage = OptionsListUtils.getHeaderMessage(
- newChatOptions.personalDetails.length + newChatOptions.recentReports.length !== 0,
- Boolean(newChatOptions.userToInvite),
- searchTerm,
- maxParticipantsReached,
- _.some(participants, (participant) => participant.searchText.toLowerCase().includes(searchTerm.toLowerCase())),
- );
- const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails);
-
- useEffect(() => {
- const chatOptions = OptionsListUtils.getNewChatOptions(reports, personalDetails, betas, searchTerm, participants, CONST.EXPENSIFY_EMAILS);
- setNewChatOptions({
- recentReports: chatOptions.recentReports,
- personalDetails: chatOptions.personalDetails,
- userToInvite: chatOptions.userToInvite,
- });
- }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions]);
-
- return (
- 0 ? safeAreaPaddingBottomStyle : {}]}>
-
-
- );
-}
-
-MoneyRequestParticipantsSplitSelector.propTypes = propTypes;
-MoneyRequestParticipantsSplitSelector.defaultProps = defaultProps;
-MoneyRequestParticipantsSplitSelector.displayName = 'MoneyRequestParticipantsSplitSelector';
-
-export default compose(
- withLocalize,
- withOnyx({
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
- betas: {
- key: ONYXKEYS.BETAS,
- },
- }),
-)(MoneyRequestParticipantsSplitSelector);
diff --git a/src/pages/iou/steps/NewRequestAmountPage.js b/src/pages/iou/steps/NewRequestAmountPage.js
index 0179c211ee55..6712a8c7cd81 100644
--- a/src/pages/iou/steps/NewRequestAmountPage.js
+++ b/src/pages/iou/steps/NewRequestAmountPage.js
@@ -115,7 +115,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) {
IOU.resetMoneyRequestInfo(moneyRequestID);
}
- if (!isDistanceRequestTab && (_.isEmpty(iou.participants) || iou.amount === 0 || shouldReset)) {
+ if (!isDistanceRequestTab && (_.isEmpty(iou.participantAccountIDs) || iou.amount === 0 || shouldReset)) {
Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true);
}
}
@@ -123,7 +123,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) {
return () => {
prevMoneyRequestID.current = iou.id;
};
- }, [iou.participants, iou.amount, iou.id, isEditing, iouType, reportID, isDistanceRequestTab]);
+ }, [iou.participantAccountIDs, iou.amount, iou.id, isEditing, iouType, reportID, isDistanceRequestTab]);
const navigateBack = () => {
Navigation.goBack(isEditing ? ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID) : null);
diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.js b/src/pages/settings/Profile/CustomStatus/StatusPage.js
index 0d7e1c09454d..f178b25fd0fb 100644
--- a/src/pages/settings/Profile/CustomStatus/StatusPage.js
+++ b/src/pages/settings/Profile/CustomStatus/StatusPage.js
@@ -68,7 +68,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js
index 0d6599631b8d..8f93434e6845 100644
--- a/src/pages/workspace/WorkspaceInitialPage.js
+++ b/src/pages/workspace/WorkspaceInitialPage.js
@@ -104,7 +104,7 @@ function WorkspaceInitialPage(props) {
const goToRoom = useCallback(
(type) => {
const room = _.find(props.reports, (report) => report && report.policyID === policy.id && report.chatType === type && !ReportUtils.isThread(report));
- Navigation.navigate(ROUTES.getReportRoute(room.reportID));
+ Navigation.dismissModal(room.reportID);
},
[props.reports, policy],
);
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js
index 0321394b37bc..f112744842b3 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.js
@@ -3,13 +3,14 @@ import {View} from 'react-native';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
+import withNavigationFocus, {withNavigationFocusPropTypes} from '../../components/withNavigationFocus';
import * as Report from '../../libs/actions/Report';
import useLocalize from '../../hooks/useLocalize';
-import HeaderWithBackButton from '../../components/HeaderWithBackButton';
-import ScreenWrapper from '../../components/ScreenWrapper';
import styles from '../../styles/styles';
import RoomNameInput from '../../components/RoomNameInput';
import Picker from '../../components/Picker';
+import KeyboardAvoidingView from '../../components/KeyboardAvoidingView';
+import ScreenWrapper from '../../components/ScreenWrapper';
import ONYXKEYS from '../../ONYXKEYS';
import CONST from '../../CONST';
import Text from '../../components/Text';
@@ -19,9 +20,10 @@ import * as ValidationUtils from '../../libs/ValidationUtils';
import * as ReportUtils from '../../libs/ReportUtils';
import * as PolicyUtils from '../../libs/PolicyUtils';
import Form from '../../components/Form';
-import shouldDelayFocus from '../../libs/shouldDelayFocus';
import policyMemberPropType from '../policyMemberPropType';
import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
+import compose from '../../libs/compose';
+import variables from '../../styles/variables';
const propTypes = {
/** All reports shared with the user */
@@ -55,6 +57,8 @@ const propTypes = {
/** A collection of objects for all policies which key policy member objects by accountIDs */
allPolicyMembers: PropTypes.objectOf(PropTypes.objectOf(policyMemberPropType)),
+
+ ...withNavigationFocusPropTypes,
};
const defaultProps = {
betas: [],
@@ -140,60 +144,72 @@ function WorkspaceNewRoomPage(props) {
);
return (
-
-
-
-
-
-
+
+
+ {({insets}) => (
+
+
+
+ )}
+
+
);
}
@@ -201,17 +217,20 @@ WorkspaceNewRoomPage.propTypes = propTypes;
WorkspaceNewRoomPage.defaultProps = defaultProps;
WorkspaceNewRoomPage.displayName = 'WorkspaceNewRoomPage';
-export default withOnyx({
- betas: {
- key: ONYXKEYS.BETAS,
- },
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- },
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
- allPolicyMembers: {
- key: ONYXKEYS.COLLECTION.POLICY_MEMBERS,
- },
-})(WorkspaceNewRoomPage);
+export default compose(
+ withNavigationFocus,
+ withOnyx({
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ },
+ allPolicyMembers: {
+ key: ONYXKEYS.COLLECTION.POLICY_MEMBERS,
+ },
+ }),
+)(WorkspaceNewRoomPage);
diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts
index 0701adb83313..ec06bb07c3fe 100644
--- a/src/styles/StyleUtils.ts
+++ b/src/styles/StyleUtils.ts
@@ -13,7 +13,6 @@ import spacing from './utilities/spacing';
import * as UserUtils from '../libs/UserUtils';
import * as Browser from '../libs/Browser';
import cursor from './utilities/cursor';
-import * as NumberUtils from '../libs/NumberUtils';
type ColorValue = ValueOf;
type AvatarSizeName = ValueOf;
@@ -572,36 +571,6 @@ function getEmojiPickerStyle(isSmallScreenWidth: boolean): ViewStyle | CSSProper
};
}
-/**
- * Get the random promo color and image for Login page
- */
-function getLoginPagePromoStyle(): ViewStyle | CSSProperties {
- const promos = [
- {
- backgroundColor: colors.green,
- backgroundImageUri: `${CONST.CLOUDFRONT_URL}/images/homepage/brand-stories/freeplan_green.svg`,
- },
- {
- backgroundColor: colors.orange,
- backgroundImageUri: `${CONST.CLOUDFRONT_URL}/images/homepage/brand-stories/freeplan_orange.svg`,
- },
- {
- backgroundColor: colors.pink,
- backgroundImageUri: `${CONST.CLOUDFRONT_URL}/images/homepage/brand-stories/freeplan_pink.svg`,
- },
- {
- backgroundColor: colors.blue,
- backgroundImageUri: `${CONST.CLOUDFRONT_URL}/images/homepage/brand-stories/freeplan_blue.svg`,
- },
- {
- backgroundColor: colors.ivory,
- backgroundImageUri: `${CONST.CLOUDFRONT_URL}/images/homepage/brand-stories/cpa-card.svg`,
- redirectUri: `${CONST.USE_EXPENSIFY_URL}/accountants`,
- },
- ];
- return promos[NumberUtils.generateRandomInt(0, 4)];
-}
-
/**
* Generate the styles for the ReportActionItem wrapper view.
*/
@@ -614,7 +583,7 @@ function getReportActionItemStyle(isHovered = false, isLoading = false): ViewSty
backgroundColor: isHovered
? themeColors.hoverComponentBG
: // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android
- colors.transparent,
+ themeColors.transparent,
opacity: isLoading ? 0.5 : 1,
...styles.cursorInitial,
};
@@ -987,7 +956,7 @@ function getAutoCompleteSuggestionContainerStyle(itemsHeight: number, shouldIncl
* Select the correct color for text.
*/
function getColoredBackgroundStyle(isColored: boolean): ViewStyle | CSSProperties {
- return {backgroundColor: isColored ? colors.blueLink : undefined};
+ return {backgroundColor: isColored ? themeColors.link : undefined};
}
function getEmojiReactionBubbleStyle(isHovered: boolean, hasUserReacted: boolean, isContextMenu = false): ViewStyle | CSSProperties {
@@ -1035,7 +1004,7 @@ function getEmojiReactionCounterTextStyle(hasUserReacted: boolean): TextStyle |
return {color: themeColors.reactionActiveText};
}
- return {color: themeColors.textLight};
+ return {color: themeColors.text};
}
/**
@@ -1272,7 +1241,6 @@ export {
getModalPaddingStyles,
getFontFamilyMonospace,
getEmojiPickerStyle,
- getLoginPagePromoStyle,
getReportActionItemStyle,
getMiniReportActionContextMenuWrapperStyle,
getKeyboardShortcutsModalWidth,
diff --git a/src/styles/animation/SpinningIndicatorAnimation.js b/src/styles/animation/SpinningIndicatorAnimation.js
index 8e7fd0277221..29044f0d127a 100644
--- a/src/styles/animation/SpinningIndicatorAnimation.js
+++ b/src/styles/animation/SpinningIndicatorAnimation.js
@@ -42,7 +42,7 @@ class SpinningIndicatorAnimation {
toValue: 1.666,
tension: 1,
isInteraction: false,
- useNativeDriver: true,
+ useNativeDriver,
}).start();
}
@@ -56,7 +56,7 @@ class SpinningIndicatorAnimation {
toValue: 1,
tension: 1,
isInteraction: false,
- useNativeDriver: true,
+ useNativeDriver,
}).start(() => {
this.rotate.resetAnimation();
this.scale.resetAnimation();
diff --git a/src/styles/colors.js b/src/styles/colors.js
index 2c70cb4a8a78..9ac3226a1b80 100644
--- a/src/styles/colors.js
+++ b/src/styles/colors.js
@@ -2,44 +2,44 @@
* DO NOT import colors.js into files. Use ../themes/default.js instead.
*/
export default {
- dark: '#0b1b34',
black: '#000000',
- blue: '#0185ff',
- blueHover: '#B0D9FF',
+ white: '#FFFFFF',
ivory: '#fffaf0',
- orange: '#FF7101',
- pink: '#F68DFE',
green: '#03D47C',
greenHover: '#00C271',
greenPressed: '#35DD96',
- greenDisabled: '80E9BD',
red: '#F25730',
redHover: '#DE4822',
redPressed: '#F57959',
- redDisabled: '#F8AA97',
- yellow: '#FED607',
transparent: 'transparent',
// Dark Mode Theme Colors
- greenAppBackground: '#061B09',
- greenHighlightBackground: '#07271F',
- greenBorders: '#1A3D32',
- greenBordersLighter: '#2B5548',
- greenIcons: '#8B9C8F',
- greenSupportingText: '#AFBBB0',
- white: '#E7ECE9',
- blueLink: '#5AB0FF',
- blueLinkHover: '#B0D9FF',
- blueLinkPreview: '#2EAAE2',
- greenDefaultButton: '#184E3D',
- greenDefaultButtonHover: '#2C6755',
- greenDefaultButtonPressed: '#467164',
- greenDefaultButtonDisabled: '#8BA69E',
- midnight: '#002140',
+ darkAppBackground: '#061B09',
+ darkHighlightBackground: '#07271F',
+ darkBorders: '#1A3D32',
+ darkIcons: '#8B9C8F',
+ darkSupportingText: '#AFBBB0',
+ darkPrimaryText: '#E7ECE9',
+ darkDefaultButton: '#184E3D',
+ darkDefaultButtonHover: '#2C6755',
+ darkDefaultButtonPressed: '#467164',
+
+ // Light Mode Theme Colors
+ lightAppBackground: '#FCFBF9',
+ lightHighlightBackground: '#F8F4F0',
+ lightBorders: '#EBE6DF',
+ lightBordersLighter: '#2B5548',
+ lightIcons: '#A2A9A3',
+ lightSupportingText: '#76847E',
+ lightPrimaryText: '#002E22',
+ lightDefaultButton: '#EEEBE7',
+ lightDefaultButtonHover: '#E3DFD9',
+ lightDefaultButtonPressed: '#D2CCC3',
// Brand Colors from Figma
blue100: '#B0D9FF',
blue200: '#8DC8FF',
+ blue300: '#5AB0FF',
blue400: '#0185FF',
blue500: '#0676DE',
blue600: '#0164BF',
@@ -48,41 +48,46 @@ export default {
green100: '#B1F2D6',
green200: '#8EECC4',
+ green300: '#5BE3AA',
green400: '#03D47C',
+ green500: '#00B268',
green600: '#008C59',
green700: '#085239',
green800: '#002E22',
+ yellow100: '#FFF2B2',
yellow200: '#FFED8F',
+ yellow300: '#FEE45E',
yellow400: '#FED607',
+ yellow500: '#E4BC07',
+ yellow600: '#D18000',
yellow700: '#722B03',
yellow800: '#401102',
+ tangerine100: '#FFD7B0',
tangerine200: '#FFC68C',
+ tangerine300: '#FFA75A',
tangerine400: '#FF7101',
+ tangerine500: '#F25730',
+ tangerine600: '#BF3013',
tangerine700: '#780505',
tangerine800: '#400000',
+ pink100: '#FCDCFF',
pink200: '#FBCCFF',
+ pink300: '#F9B5FE',
pink400: '#F68DFE',
+ pink500: '#E96DF2',
pink600: '#CF4CD9',
pink700: '#712A76',
pink800: '#49225B',
+ ice100: '#DFFDFE',
ice200: '#CCF7FF',
+ ice300: '#A5FBFF',
ice400: '#50EEF6',
ice500: '#4ED7DE',
+ ice600: '#4BA6A6',
ice700: '#28736D',
ice800: '#134038',
-
- orange800: '#400000',
-
- // DEPRECATED COLORS. Do not reference these colors. Will be deleted in color switch PR.
- gray1: '#FAFAFA',
- gray2: '#ECECEC',
- gray3: '#C6C9CA',
- gray4: '#7D8B8F',
- oldRed: '#fc3826',
- oldRedHover: '#e13826',
- oldRedDisabled: '#fea29a',
};
diff --git a/src/styles/getTooltipStyles.ts b/src/styles/getTooltipStyles.ts
index 3f9de9c78b97..97402467ab4c 100644
--- a/src/styles/getTooltipStyles.ts
+++ b/src/styles/getTooltipStyles.ts
@@ -2,7 +2,6 @@ import {CSSProperties} from 'react';
import {TextStyle, View, ViewStyle} from 'react-native';
import spacing from './utilities/spacing';
import styles from './styles';
-import colors from './colors';
import themeColors from './themes/default';
import fontFamily from './fontFamily';
import variables from './variables';
@@ -267,13 +266,13 @@ export default function getTooltipStyles(
pointerStyle: {
width: 0,
height: 0,
- backgroundColor: colors.transparent,
+ backgroundColor: themeColors.transparent,
borderStyle: 'solid',
borderLeftWidth: POINTER_WIDTH / 2,
borderRightWidth: POINTER_WIDTH / 2,
borderTopWidth: POINTER_HEIGHT,
- borderLeftColor: colors.transparent,
- borderRightColor: colors.transparent,
+ borderLeftColor: themeColors.transparent,
+ borderRightColor: themeColors.transparent,
borderTopColor: themeColors.heading,
...pointerAdditionalStyle,
},
diff --git a/src/styles/styles.js b/src/styles/styles.js
index ff247009085f..23966d1a1a14 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -26,7 +26,6 @@ import * as Browser from '../libs/Browser';
import cursor from './utilities/cursor';
import userSelect from './utilities/userSelect';
import textUnderline from './utilities/textUnderline';
-import Colors from './colors';
// touchCallout is an iOS safari only property that controls the display of the callout information when you touch and hold a target
const touchCalloutNone = Browser.isMobileSafari() ? {WebkitTouchCallout: 'none'} : {};
@@ -815,8 +814,8 @@ const styles = (theme) => ({
borderRadius: 28,
borderStyle: 'solid',
borderWidth: 8,
- backgroundColor: Colors.greenHighlightBackground,
- borderColor: Colors.greenAppBackground,
+ backgroundColor: theme.highlightBG,
+ borderColor: theme.appBG,
},
permissionView: {
@@ -1550,7 +1549,7 @@ const styles = (theme) => ({
top: 0,
bottom: 0,
right: 0,
- backgroundColor: Colors.black,
+ backgroundColor: theme.shadow,
opacity: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, variables.overlayOpacity],
@@ -1811,6 +1810,7 @@ const styles = (theme) => ({
paddingTop: 2,
paddingBottom: 2,
height: CONST.EMOJI_PICKER_ITEM_HEIGHT,
+ ...userSelect.userSelectNone,
},
emojiItemHighlighted: {
@@ -3429,7 +3429,7 @@ const styles = (theme) => ({
},
fontColorReactionLabel: {
- color: '#586A64',
+ color: theme.tooltipSupportingText,
},
reactionEmojiTitle: {
@@ -3438,7 +3438,7 @@ const styles = (theme) => ({
},
textReactionSenders: {
- color: theme.dark,
+ color: theme.tooltipPrimaryText,
...wordBreak.breakWord,
},
@@ -3720,8 +3720,8 @@ const styles = (theme) => ({
},
tabSelectorButton: {
- height: 40,
- padding: 12,
+ height: variables.tabSelectorButtonHeight,
+ padding: variables.tabSelectorButtonPadding,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
@@ -3756,6 +3756,15 @@ const styles = (theme) => ({
right: 0,
}),
+ dualColorOverscrollSpacer: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ zIndex: -1,
+ },
+
willChangeTransform: {
willChange: 'transform',
},
@@ -3929,19 +3938,31 @@ const styles = (theme) => ({
overflow: 'hidden',
},
+ mapViewOverlay: {
+ flex: 1,
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ borderRadius: variables.componentBorderRadiusLarge,
+ overflow: 'hidden',
+ backgroundColor: theme.highlightBG,
+ ...sizing.w100,
+ ...sizing.h100,
+ },
+
confirmationListMapItem: {
...spacing.m5,
height: 200,
},
mapDirection: {
- lineColor: Colors.green,
+ lineColor: theme.success,
lineWidth: 7,
},
mapDirectionLayer: {
layout: {'line-join': 'round', 'line-cap': 'round'},
- paint: {'line-color': Colors.green, 'line-width': 7},
+ paint: {'line-color': theme.success, 'line-width': 7},
},
mapPendingView: {
diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js
index 5f69a7746cdc..c101a668666b 100644
--- a/src/styles/themes/default.js
+++ b/src/styles/themes/default.js
@@ -5,71 +5,68 @@ import ROUTES from '../../ROUTES';
const darkTheme = {
// Figma keys
- appBG: colors.greenAppBackground,
- splashBG: colors.green,
- highlightBG: colors.greenHighlightBackground,
- border: colors.greenBorders,
- borderLighter: colors.greenBordersLighter,
- borderFocus: colors.green,
- icon: colors.greenIcons,
- iconMenu: colors.green,
- iconHovered: colors.white,
- iconSuccessFill: colors.green,
- iconReversed: colors.greenAppBackground,
+ appBG: colors.darkAppBackground,
+ splashBG: colors.green400,
+ highlightBG: colors.darkHighlightBackground,
+ border: colors.darkBorders,
+ borderLighter: colors.darkDefaultButton,
+ borderFocus: colors.green400,
+ icon: colors.darkIcons,
+ iconMenu: colors.green400,
+ iconHovered: colors.darkPrimaryText,
+ iconSuccessFill: colors.green400,
+ iconReversed: colors.darkAppBackground,
iconColorfulBackground: `${colors.ivory}cc`,
- textSupporting: colors.greenSupportingText,
- text: colors.white,
+ textSupporting: colors.darkSupportingText,
+ text: colors.darkPrimaryText,
textColorfulBackground: colors.ivory,
- link: colors.blueLink,
- linkHover: colors.blueLinkHover,
- buttonDefaultBG: colors.greenDefaultButton,
- buttonDisabledBG: colors.greenDefaultButtonDisabled,
- buttonHoveredBG: colors.greenDefaultButtonHover,
- buttonPressedBG: colors.greenDefaultButtonPressed,
+ link: colors.blue300,
+ linkHover: colors.blue100,
+ buttonDefaultBG: colors.darkDefaultButton,
+ buttonHoveredBG: colors.darkDefaultButtonHover,
+ buttonPressedBG: colors.darkDefaultButtonPressed,
danger: colors.red,
dangerHover: colors.redHover,
dangerPressed: colors.redHover,
- warning: colors.yellow,
- success: colors.green,
+ warning: colors.yellow400,
+ success: colors.green400,
successHover: colors.greenHover,
successPressed: colors.greenPressed,
transparent: colors.transparent,
- midtone: colors.green700,
signInPage: colors.green800,
- dark: colors.midnight,
// Additional keys
- overlay: colors.greenHighlightBackground,
- inverse: colors.white,
+ overlay: colors.darkHighlightBackground,
+ inverse: colors.darkPrimaryText,
shadow: colors.black,
- componentBG: colors.greenAppBackground,
- hoverComponentBG: colors.greenHighlightBackground,
- activeComponentBG: colors.greenBorders,
+ componentBG: colors.darkAppBackground,
+ hoverComponentBG: colors.darkHighlightBackground,
+ activeComponentBG: colors.darkBorders,
signInSidebar: colors.green800,
- sidebar: colors.greenHighlightBackground,
- sidebarHover: colors.greenAppBackground,
- heading: colors.white,
- textLight: colors.white,
- textDark: colors.greenAppBackground,
- textReversed: colors.greenAppBackground,
- textBackground: colors.greenHighlightBackground,
- textMutedReversed: colors.greenIcons,
+ sidebar: colors.darkHighlightBackground,
+ sidebarHover: colors.darkAppBackground,
+ heading: colors.darkPrimaryText,
+ textLight: colors.darkPrimaryText,
+ textDark: colors.darkAppBackground,
+ textReversed: colors.lightPrimaryText,
+ textBackground: colors.darkHighlightBackground,
+ textMutedReversed: colors.darkIcons,
textError: colors.red,
- offline: colors.greenIcons,
- modalBackdrop: colors.greenHighlightBackground,
- modalBackground: colors.greenAppBackground,
- cardBG: colors.greenHighlightBackground,
- cardBorder: colors.greenHighlightBackground,
- spinner: colors.greenSupportingText,
- unreadIndicator: colors.green,
- placeholderText: colors.greenIcons,
- heroCard: colors.blue,
- uploadPreviewActivityIndicator: colors.greenHighlightBackground,
+ offline: colors.darkIcons,
+ modalBackdrop: colors.darkHighlightBackground,
+ modalBackground: colors.darkAppBackground,
+ cardBG: colors.darkHighlightBackground,
+ cardBorder: colors.darkHighlightBackground,
+ spinner: colors.darkSupportingText,
+ unreadIndicator: colors.green400,
+ placeholderText: colors.darkIcons,
+ heroCard: colors.blue400,
+ uploadPreviewActivityIndicator: colors.darkHighlightBackground,
dropUIBG: 'rgba(6,27,9,0.92)',
receiptDropUIBG: 'rgba(3, 212, 124, 0.84)',
- checkBox: colors.green,
- pickerOptionsTextColor: colors.white,
- imageCropBackgroundColor: colors.greenIcons,
+ checkBox: colors.green400,
+ pickerOptionsTextColor: colors.darkPrimaryText,
+ imageCropBackgroundColor: colors.darkIcons,
fallbackIconColor: colors.green700,
reactionActiveBackground: colors.green600,
reactionActiveText: colors.green100,
@@ -79,6 +76,11 @@ const darkTheme = {
mentionBG: colors.blue600,
ourMentionText: colors.green100,
ourMentionBG: colors.green600,
+ tooltipSupportingText: colors.lightSupportingText,
+ tooltipPrimaryText: colors.lightPrimaryText,
+ skeletonLHNIn: colors.darkBorders,
+ skeletonLHNOut: colors.darkDefaultButton,
+ QRLogo: colors.green400,
starDefaultBG: 'rgb(254, 228, 94)',
};
@@ -86,66 +88,9 @@ darkTheme.PAGE_BACKGROUND_COLORS = {
[SCREENS.HOME]: darkTheme.sidebar,
[SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
[SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
- [ROUTES.I_KNOW_A_TEACHER]: colors.orange800,
+ [ROUTES.SETTINGS_STATUS]: colors.green700,
+ [ROUTES.I_KNOW_A_TEACHER]: colors.tangerine800,
[ROUTES.SETTINGS_SECURITY]: colors.ice500,
};
-const oldTheme = {
- shadow: colors.black,
- link: colors.blue,
- linkHover: colors.blueHover,
- componentBG: colors.white,
- hoverComponentBG: colors.gray1,
- activeComponentBG: colors.gray2,
- appBG: colors.white,
- heading: colors.dark,
- sidebar: colors.gray1,
- sidebarHover: colors.white,
- border: colors.gray2,
- borderFocus: colors.blue,
- icon: colors.gray3,
- iconMenu: colors.gray3,
- iconHovered: colors.dark,
- iconSuccessFill: colors.green,
- iconReversed: colors.white,
- textSupporting: colors.gray4,
- text: colors.dark,
- textError: colors.oldRed,
- textBackground: colors.gray1,
- textReversed: colors.white,
- textMutedReversed: colors.gray3,
- buttonDefaultBG: colors.gray2,
- offline: colors.gray3,
- modalBackdrop: colors.gray3,
- modalBackground: colors.gray2,
- buttonDisabledBG: colors.gray2,
- buttonHoveredBG: colors.gray1,
- buttonPressedBG: colors.gray2,
- spinner: colors.gray4,
- unreadIndicator: colors.green,
- placeholderText: colors.gray3,
- heroCard: colors.blue,
- uploadPreviewActivityIndicator: colors.gray1,
- dropUIBG: 'rgba(6,27,9,0.92)',
- cardBG: colors.gray1,
- cardBorder: colors.gray1,
- checkBox: colors.blue,
- overlay: colors.gray1,
-
- // Merging new Keys for Dark Mode merge. Delete after new branding is implemented.
- highlightBG: colors.gray1,
- danger: colors.oldRed,
- dangerHover: colors.oldRedHover,
- dangerPressed: colors.oldRedHover,
- dangerDisabled: colors.oldRedDisabled,
- warning: colors.yellow,
- success: colors.green,
- successHover: colors.greenHover,
- successPressed: colors.greenPressed,
- transparent: colors.transparent,
- inverse: colors.dark,
- textLight: colors.white,
- textDark: colors.dark,
-};
-
export default darkTheme;
diff --git a/src/styles/themes/light.js b/src/styles/themes/light.js
new file mode 100644
index 000000000000..1a945cb84913
--- /dev/null
+++ b/src/styles/themes/light.js
@@ -0,0 +1,95 @@
+import colors from '../colors';
+import SCREENS from '../../SCREENS';
+import ROUTES from '../../ROUTES';
+
+const lightTheme = {
+ // Figma keys
+ appBG: colors.lightAppBackground,
+ splashBG: colors.green400,
+ highlightBG: colors.lightHighlightBackground,
+ border: colors.lightBorders,
+ borderLighter: colors.lightDefaultButtonPressed,
+ borderFocus: colors.green400,
+ icon: colors.lightIcons,
+ iconMenu: colors.green400,
+ iconHovered: colors.lightPrimaryText,
+ iconSuccessFill: colors.green400,
+ iconReversed: colors.lightAppBackground,
+ iconColorfulBackground: `${colors.ivory}cc`,
+ textColorfulBackground: colors.ivory,
+ textSupporting: colors.lightSupportingText,
+ text: colors.lightPrimaryText,
+ link: colors.blue600,
+ linkHover: colors.blue500,
+ buttonDefaultBG: colors.lightDefaultButton,
+ buttonHoveredBG: colors.lightDefaultButtonHover,
+ buttonPressedBG: colors.lightDefaultButtonPressed,
+ danger: colors.red,
+ dangerHover: colors.redHover,
+ dangerPressed: colors.redHover,
+ warning: colors.yellow400,
+ success: colors.green400,
+ successHover: colors.greenHover,
+ successPressed: colors.greenPressed,
+ transparent: colors.transparent,
+ signInPage: colors.green800,
+
+ // Additional keys
+ overlay: colors.lightHighlightBackground,
+ inverse: colors.lightPrimaryText,
+ shadow: colors.black,
+ componentBG: colors.lightAppBackground,
+ hoverComponentBG: colors.lightHighlightBackground,
+ activeComponentBG: colors.lightBorders,
+ signInSidebar: colors.green800,
+ sidebar: colors.lightHighlightBackground,
+ sidebarHover: colors.lightBorders,
+ heading: colors.lightPrimaryText,
+ textLight: colors.white,
+ textDark: colors.lightPrimaryText,
+ textReversed: colors.darkPrimaryText,
+ textBackground: colors.lightHighlightBackground,
+ textMutedReversed: colors.lightIcons,
+ textError: colors.red,
+ offline: colors.lightIcons,
+ modalBackdrop: colors.lightHighlightBackground,
+ modalBackground: colors.lightAppBackground,
+ cardBG: colors.lightHighlightBackground,
+ cardBorder: colors.lightHighlightBackground,
+ spinner: colors.lightSupportingText,
+ unreadIndicator: colors.green400,
+ placeholderText: colors.lightIcons,
+ heroCard: colors.blue400,
+ uploadPreviewActivityIndicator: colors.lightHighlightBackground,
+ dropUIBG: 'rgba(252, 251, 249, 0.92)',
+ dropTransparentOverlay: 'rgba(255,255,255,0)',
+ checkBox: colors.green400,
+ pickerOptionsTextColor: colors.lightPrimaryText,
+ imageCropBackgroundColor: colors.lightIcons,
+ fallbackIconColor: colors.green700,
+ reactionActiveBackground: colors.green100,
+ reactionActiveText: colors.green600,
+ badgeAdHoc: colors.pink600,
+ badgeAdHocHover: colors.pink700,
+ mentionText: colors.blue600,
+ mentionBG: colors.blue100,
+ ourMentionText: colors.green600,
+ ourMentionBG: colors.green100,
+ tooltipSupportingText: colors.darkSupportingText,
+ tooltipPrimaryText: colors.darkPrimaryText,
+ skeletonLHNIn: colors.lightBorders,
+ skeletonLHNOut: colors.lightDefaultButtonPressed,
+ QRLogo: colors.green400,
+ starDefaultBG: 'rgb(254, 228, 94)',
+};
+
+lightTheme.PAGE_BACKGROUND_COLORS = {
+ [SCREENS.HOME]: lightTheme.sidebar,
+ [SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
+ [SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
+ [ROUTES.SETTINGS_STATUS]: colors.green700,
+ [ROUTES.I_KNOW_A_TEACHER]: colors.tangerine800,
+ [ROUTES.SETTINGS_SECURITY]: colors.ice500,
+};
+
+export default lightTheme;
diff --git a/src/styles/utilities/spacing.ts b/src/styles/utilities/spacing.ts
index 7147b1f2b7d4..a3667f05ac06 100644
--- a/src/styles/utilities/spacing.ts
+++ b/src/styles/utilities/spacing.ts
@@ -373,6 +373,10 @@ export default {
paddingRight: 8,
},
+ pr3: {
+ paddingRight: 12,
+ },
+
pr4: {
paddingRight: 16,
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index eb182ab1eca0..17d056fbe6f4 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -133,6 +133,8 @@ export default {
signInLogoWidth: 120,
signInLogoWidthLargeScreen: 144,
signInLogoWidthPill: 132,
+ tabSelectorButtonHeight: 40,
+ tabSelectorButtonPadding: 12,
lhnLogoWidth: 108,
lhnLogoHeight: 28,
signInLogoWidthLargeScreenPill: 162,
diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts
index 02a96d4ce230..50b1503b90bd 100644
--- a/src/types/onyx/OnyxUpdatesFromServer.ts
+++ b/src/types/onyx/OnyxUpdatesFromServer.ts
@@ -2,13 +2,18 @@ import {OnyxUpdate} from 'react-native-onyx';
import Request from './Request';
import Response from './Response';
+type OnyxUpdateEvent = {
+ eventType: string;
+ data: OnyxUpdate[];
+};
+
type OnyxUpdatesFromServer = {
type: 'https' | 'pusher';
lastUpdateID: number | string;
previousUpdateID: number | string;
request?: Request;
response?: Response;
- updates?: OnyxUpdate[];
+ updates?: OnyxUpdateEvent[];
};
-export default OnyxUpdatesFromServer;
+export type {OnyxUpdatesFromServer, OnyxUpdateEvent};
diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts
index 1df20cfb28fe..94f14af0ddb3 100644
--- a/src/types/onyx/Request.ts
+++ b/src/types/onyx/Request.ts
@@ -1,7 +1,7 @@
import {OnyxUpdate} from 'react-native-onyx';
type Request = {
- command?: string;
+ command: string;
data?: Record;
type?: string;
shouldUseSecure?: boolean;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index a980e086aff5..a7bbaf848265 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -33,7 +33,7 @@ import ReimbursementAccountDraft from './ReimbursementAccountDraft';
import WalletTransfer from './WalletTransfer';
import ReceiptModal from './ReceiptModal';
import MapboxAccessToken from './MapboxAccessToken';
-import OnyxUpdatesFromServer from './OnyxUpdatesFromServer';
+import {OnyxUpdatesFromServer, OnyxUpdateEvent} from './OnyxUpdatesFromServer';
import Download from './Download';
import PolicyMember from './PolicyMember';
import Policy from './Policy';
@@ -97,6 +97,7 @@ export type {
Form,
AddDebitCardForm,
OnyxUpdatesFromServer,
+ OnyxUpdateEvent,
RecentWaypoints,
RecentlyUsedCategories,
RecentlyUsedTags,
diff --git a/tests/perf-test/SelectionList.perf-test.js b/tests/perf-test/SelectionList.perf-test.js
index d16875e31357..2d9cd4a87829 100644
--- a/tests/perf-test/SelectionList.perf-test.js
+++ b/tests/perf-test/SelectionList.perf-test.js
@@ -31,6 +31,7 @@ jest.mock('../../src/components/withKeyboardState', () => (Component) => (props)
jest.mock('@react-navigation/native', () => ({
useFocusEffect: () => {},
+ useIsFocused: () => true,
createNavigationContainerRef: jest.fn(),
}));