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/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/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..70cadd5efd8e 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';
@@ -14,15 +14,15 @@ import * as Pressables from '../Pressable';
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,
},
};
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/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 a78a6cf35471..9b20ee0168aa 100644
--- a/src/libs/ReportActionsUtils.js
+++ b/src/libs/ReportActionsUtils.js
@@ -620,6 +620,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,
@@ -657,4 +668,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/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/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/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) && (
+