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/android/app/build.gradle b/android/app/build.gradle
index 1867a8cf85d2..c6a9c3147118 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 1001037007
+ versionName "1.3.70-7"
}
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..03dcc7770df0 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.70.2
+ 1.3.70.7
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index fd93684a1da3..941d232244e1 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.70.2
+ 1.3.70.7
diff --git a/package-lock.json b/package-lock.json
index 0ba372b22745..382dcf45f55e 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-7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.70-2",
+ "version": "1.3.70-7",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -52,7 +52,6 @@
"domhandler": "^4.3.0",
"expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086",
"fbjs": "^3.0.2",
- "focus-trap-react": "^10.2.1",
"htmlparser2": "^7.2.0",
"idb-keyval": "^6.2.1",
"jest-when": "^3.5.2",
@@ -90,10 +89,10 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.76",
+ "react-native-onyx": "1.0.77",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.7.1",
- "react-native-performance": "^4.0.0",
+ "react-native-performance": "^5.1.0",
"react-native-permissions": "^3.0.1",
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2",
"react-native-plaid-link-sdk": "^10.0.0",
@@ -103,7 +102,7 @@
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.4.1",
"react-native-screens": "3.21.0",
- "react-native-svg": "^13.9.0",
+ "react-native-svg": "^13.13.0",
"react-native-tab-view": "^3.5.2",
"react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "^3.6.0",
@@ -28295,28 +28294,6 @@
"readable-stream": "^2.3.6"
}
},
- "node_modules/focus-trap": {
- "version": "7.5.2",
- "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz",
- "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==",
- "dependencies": {
- "tabbable": "^6.2.0"
- }
- },
- "node_modules/focus-trap-react": {
- "version": "10.2.1",
- "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.1.tgz",
- "integrity": "sha512-UrAKOn52lvfHF6lkUMfFhlQxFgahyNW5i6FpHWkDxAeD4FSk3iwx9n4UEA4Sims0G5WiGIi0fAyoq3/UVeNCYA==",
- "dependencies": {
- "focus-trap": "^7.5.2",
- "tabbable": "^6.2.0"
- },
- "peerDependencies": {
- "prop-types": "^15.8.1",
- "react": ">=16.3.0",
- "react-dom": ">=16.3.0"
- }
- },
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
@@ -40567,9 +40544,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "1.0.76",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.76.tgz",
- "integrity": "sha512-mcMlYQCo1B/kom+4hu7CQKKLwvPFjQAJsVIzV2s9aa8XKNlcnYiJbfuM6RSJ1fFmSIeud4Y66rhv4/oWUkSl5A==",
+ "version": "1.0.77",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.77.tgz",
+ "integrity": "sha512-HmeS1Pz/BkKNbYuhWULC9I0VRBDt8yadG0ZFIW6wuZ+VajhjD960qh7Il1+XzEBI6Vb4d7BZkPcad87ad1IEOQ==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
@@ -40583,7 +40560,7 @@
"idb-keyval": "^6.2.1",
"react": ">=18.1.0",
"react-native-device-info": "^10.3.0",
- "react-native-performance": "^4.0.0",
+ "react-native-performance": "^5.1.0",
"react-native-quick-sqlite": "^8.0.0-beta.2"
},
"peerDependenciesMeta": {
@@ -40625,8 +40602,9 @@
}
},
"node_modules/react-native-performance": {
- "version": "4.0.0",
- "license": "MIT",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/react-native-performance/-/react-native-performance-5.1.0.tgz",
+ "integrity": "sha512-rq/YBf0/GptSOM/Lj64/1yRq8uN2YE0psFB16wFbYBbTcIEp/0rrgN2HyS5lhvfBOFgKoDRWQ53jHSCb+QJ5eA==",
"peerDependencies": {
"react-native": "*"
}
@@ -40783,8 +40761,9 @@
}
},
"node_modules/react-native-svg": {
- "version": "13.9.0",
- "license": "MIT",
+ "version": "13.13.0",
+ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.13.0.tgz",
+ "integrity": "sha512-L8y8uEiMG0Tr++Nb2+24wlMuv18+bmq/CMoFFtTUlEqVvGCoK2ea8WamPl/9bV8gjL+Rngg5NqEBvKS23sbYoA==",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3"
@@ -44692,11 +44671,6 @@
"dev": true,
"license": "BSD-3-Clause"
},
- "node_modules/tabbable": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
- "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
- },
"node_modules/table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
@@ -67708,23 +67682,6 @@
"readable-stream": "^2.3.6"
}
},
- "focus-trap": {
- "version": "7.5.2",
- "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz",
- "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==",
- "requires": {
- "tabbable": "^6.2.0"
- }
- },
- "focus-trap-react": {
- "version": "10.2.1",
- "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.1.tgz",
- "integrity": "sha512-UrAKOn52lvfHF6lkUMfFhlQxFgahyNW5i6FpHWkDxAeD4FSk3iwx9n4UEA4Sims0G5WiGIi0fAyoq3/UVeNCYA==",
- "requires": {
- "focus-trap": "^7.5.2",
- "tabbable": "^6.2.0"
- }
- },
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
@@ -76116,9 +76073,9 @@
}
},
"react-native-onyx": {
- "version": "1.0.76",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.76.tgz",
- "integrity": "sha512-mcMlYQCo1B/kom+4hu7CQKKLwvPFjQAJsVIzV2s9aa8XKNlcnYiJbfuM6RSJ1fFmSIeud4Y66rhv4/oWUkSl5A==",
+ "version": "1.0.77",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.77.tgz",
+ "integrity": "sha512-HmeS1Pz/BkKNbYuhWULC9I0VRBDt8yadG0ZFIW6wuZ+VajhjD960qh7Il1+XzEBI6Vb4d7BZkPcad87ad1IEOQ==",
"requires": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
@@ -76141,7 +76098,9 @@
}
},
"react-native-performance": {
- "version": "4.0.0",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/react-native-performance/-/react-native-performance-5.1.0.tgz",
+ "integrity": "sha512-rq/YBf0/GptSOM/Lj64/1yRq8uN2YE0psFB16wFbYBbTcIEp/0rrgN2HyS5lhvfBOFgKoDRWQ53jHSCb+QJ5eA==",
"requires": {}
},
"react-native-performance-flipper-reporter": {
@@ -76238,7 +76197,9 @@
}
},
"react-native-svg": {
- "version": "13.9.0",
+ "version": "13.13.0",
+ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.13.0.tgz",
+ "integrity": "sha512-L8y8uEiMG0Tr++Nb2+24wlMuv18+bmq/CMoFFtTUlEqVvGCoK2ea8WamPl/9bV8gjL+Rngg5NqEBvKS23sbYoA==",
"requires": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3"
@@ -78848,11 +78809,6 @@
"version": "2.0.15",
"dev": true
},
- "tabbable": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
- "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
- },
"table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
diff --git a/package.json b/package.json
index 97621503eb6f..0073dedb741c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.70-2",
+ "version": "1.3.70-7",
"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.",
@@ -94,7 +94,6 @@
"domhandler": "^4.3.0",
"expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086",
"fbjs": "^3.0.2",
- "focus-trap-react": "^10.2.1",
"htmlparser2": "^7.2.0",
"idb-keyval": "^6.2.1",
"jest-when": "^3.5.2",
@@ -132,10 +131,10 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.76",
+ "react-native-onyx": "1.0.77",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.7.1",
- "react-native-performance": "^4.0.0",
+ "react-native-performance": "^5.1.0",
"react-native-permissions": "^3.0.1",
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2",
"react-native-plaid-link-sdk": "^10.0.0",
@@ -145,7 +144,7 @@
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.4.1",
"react-native-screens": "3.21.0",
- "react-native-svg": "^13.9.0",
+ "react-native-svg": "^13.13.0",
"react-native-tab-view": "^3.5.2",
"react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "^3.6.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index 1ef2f3e83246..762186439cec 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: {
@@ -1170,6 +1170,7 @@ const CONST = {
SMALL_NORMAL: 'small-normal',
},
EXPENSIFY_CARD: {
+ BANK: 'Expensify Card',
FRAUD_TYPES: {
DOMAIN: 'domain',
INDIVIDUAL: 'individal',
@@ -1342,6 +1343,7 @@ const CONST = {
SETTINGS: 'settings',
LEAVE_ROOM: 'leaveRoom',
WELCOME_MESSAGE: 'welcomeMessage',
+ PRIVATE_NOTES: 'privateNotes',
},
EDIT_REQUEST_FIELD: {
AMOUNT: 'amount',
@@ -2624,6 +2626,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/Expensify.js b/src/Expensify.js
index 1086bd32cff9..fba65e42c06c 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -30,7 +30,6 @@ import KeyboardShortcutsModal from './components/KeyboardShortcutsModal';
import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper';
import EmojiPicker from './components/EmojiPicker/EmojiPicker';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
-import DownloadAppModal from './components/DownloadAppModal';
import DeeplinkWrapper from './components/DeeplinkWrapper';
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
@@ -193,7 +192,6 @@ function Expensify(props) {
{shouldInit && (
<>
-
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index eb649053c93f..f16f8129e86c 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -87,9 +87,6 @@ const ONYXKEYS = {
SESSION: 'session',
BETAS: 'betas',
- /** Denotes if the Download App Banner has been dismissed */
- SHOW_DOWNLOAD_APP_BANNER: 'showDownloadAppBanner',
-
/** NVP keys
* Contains the user's payPalMe data */
PAYPAL: 'paypal',
@@ -291,6 +288,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',
},
@@ -307,7 +305,6 @@ type OnyxValues = {
[ONYXKEYS.ACTIVE_CLIENTS]: string[];
[ONYXKEYS.DEVICE_ID]: string;
[ONYXKEYS.IS_SIDEBAR_LOADED]: boolean;
- [ONYXKEYS.SHOW_DOWNLOAD_APP_BANNER]: boolean;
[ONYXKEYS.PERSISTED_REQUESTS]: OnyxTypes.Request[];
[ONYXKEYS.QUEUED_ONYX_UPDATES]: OnyxTypes.QueuedOnyxUpdates;
[ONYXKEYS.CURRENT_DATE]: string;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 1133dcec8e9a..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,8 +66,9 @@ 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?',
@@ -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
@@ -174,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/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/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
index d5da25c89576..8a623a44709f 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js
@@ -59,6 +59,7 @@ function extractAttachmentsFromReport(report, reportActions) {
isAuthTokenRequired: true,
file: {name: transaction.filename},
isReceipt: true,
+ transactionID,
});
return;
}
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 5c731a0ccfee..574cb496d02f 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -19,6 +19,7 @@ import BlockingView from '../../BlockingViews/BlockingView';
import * as Illustrations from '../../Icon/Illustrations';
import variables from '../../../styles/variables';
import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
+import * as ReportActionsUtils from '../../../libs/ReportActionsUtils';
const viewabilityConfig = {
// To facilitate paging through the attachments, we want to consider an item "viewable" when it is
@@ -38,13 +39,25 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl
const [activeSource, setActiveSource] = useState(source);
const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
+ const compareImage = useCallback(
+ (attachment) => {
+ if (attachment.isReceipt) {
+ const action = ReportActionsUtils.getParentReportAction(report);
+ const transactionID = _.get(action, ['originalMessage', 'IOUTransactionID']);
+ return attachment.transactionID === transactionID;
+ }
+ return attachment.source === source;
+ },
+ [source, report],
+ );
+
useEffect(() => {
const attachmentsFromReport = extractAttachmentsFromReport(report, reportActions);
- const initialPage = _.findIndex(attachmentsFromReport, (a) => a.source === source);
+ const initialPage = _.findIndex(attachmentsFromReport, compareImage);
// Dismiss the modal when deleting an attachment during its display in preview.
- if (initialPage === -1 && _.find(attachments, (a) => a.source === source)) {
+ if (initialPage === -1 && _.find(attachments, compareImage)) {
Navigation.dismissModal();
} else {
setPage(initialPage);
@@ -57,7 +70,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl
if (!_.isUndefined(attachmentsFromReport[initialPage])) onNavigate(attachmentsFromReport[initialPage]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [report, reportActions, source]);
+ }, [reportActions, compareImage]);
/**
* Updates the page state when the user navigates between attachments
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index 95cda7c2f5c9..a7a2f35a2ccc 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -16,6 +16,7 @@ import * as Illustrations from '../../Icon/Illustrations';
import variables from '../../../styles/variables';
import compose from '../../../libs/compose';
import withLocalize from '../../withLocalize';
+import * as ReportActionsUtils from '../../../libs/ReportActionsUtils';
function AttachmentCarousel({report, reportActions, source, onNavigate, onClose, setDownloadButtonVisibility, translate}) {
const pagerRef = useRef(null);
@@ -27,13 +28,25 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose,
const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true);
const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
+ const compareImage = useCallback(
+ (attachment) => {
+ if (attachment.isReceipt) {
+ const action = ReportActionsUtils.getParentReportAction(report);
+ const transactionID = _.get(action, ['originalMessage', 'IOUTransactionID']);
+ return attachment.transactionID === transactionID;
+ }
+ return attachment.source === source;
+ },
+ [source, report],
+ );
+
useEffect(() => {
const attachmentsFromReport = extractAttachmentsFromReport(report, reportActions);
- const initialPage = _.findIndex(attachmentsFromReport, (a) => a.source === source);
+ const initialPage = _.findIndex(attachmentsFromReport, compareImage);
// Dismiss the modal when deleting an attachment during its display in preview.
- if (initialPage === -1 && _.find(attachments, (a) => a.source === source)) {
+ if (initialPage === -1 && _.find(attachments, compareImage)) {
Navigation.dismissModal();
} else {
setPage(initialPage);
@@ -46,7 +59,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose,
if (!_.isUndefined(attachmentsFromReport[initialPage])) onNavigate(attachmentsFromReport[initialPage]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [report, reportActions, source]);
+ }, [reportActions, compareImage]);
/**
* Updates the page state when the user navigates between attachments
diff --git a/src/components/Button/index.js b/src/components/Button/index.js
index bfde528a4750..c16860344837 100644
--- a/src/components/Button/index.js
+++ b/src/components/Button/index.js
@@ -218,6 +218,7 @@ class Button extends Component {
this.props.icon && styles.textAlignLeft,
...this.props.textStyles,
]}
+ dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{this.props.text}
diff --git a/src/components/CollapsibleSection/index.js b/src/components/CollapsibleSection/index.js
index e9c3a90a7b30..7009d1905e1d 100644
--- a/src/components/CollapsibleSection/index.js
+++ b/src/components/CollapsibleSection/index.js
@@ -51,6 +51,7 @@ class CollapsibleSection extends React.Component {
{this.props.title}
diff --git a/src/components/ConfirmContent.js b/src/components/ConfirmContent.js
index 9a72d4e7d584..ab3e23d6b1c1 100644
--- a/src/components/ConfirmContent.js
+++ b/src/components/ConfirmContent.js
@@ -100,8 +100,8 @@ function ConfirmContent(props) {
diff --git a/src/components/DownloadAppModal.js b/src/components/DownloadAppModal.js
deleted file mode 100644
index c96c6b3d28c0..000000000000
--- a/src/components/DownloadAppModal.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import React, {useState} from 'react';
-import PropTypes from 'prop-types';
-import {withOnyx} from 'react-native-onyx';
-import ONYXKEYS from '../ONYXKEYS';
-import styles from '../styles/styles';
-import CONST from '../CONST';
-import AppIcon from '../../assets/images/expensify-app-icon.svg';
-import useLocalize from '../hooks/useLocalize';
-import * as Link from '../libs/actions/Link';
-import * as Browser from '../libs/Browser';
-import getOperatingSystem from '../libs/getOperatingSystem';
-import setShowDownloadAppModal from '../libs/actions/DownloadAppModal';
-import ConfirmModal from './ConfirmModal';
-
-const propTypes = {
- /** ONYX PROP to hide banner for a user that has dismissed it */
- // eslint-disable-next-line react/forbid-prop-types
- showDownloadAppBanner: PropTypes.bool,
-
- /** Whether the user is logged in */
- isAuthenticated: PropTypes.bool.isRequired,
-};
-
-const defaultProps = {
- showDownloadAppBanner: true,
-};
-
-function DownloadAppModal({isAuthenticated, showDownloadAppBanner}) {
- const [shouldShowBanner, setShouldShowBanner] = useState(Browser.isMobile() && isAuthenticated && showDownloadAppBanner);
-
- const {translate} = useLocalize();
-
- const handleCloseBanner = () => {
- setShowDownloadAppModal(false);
- setShouldShowBanner(false);
- };
-
- let link = '';
-
- if (getOperatingSystem() === CONST.OS.IOS) {
- link = CONST.APP_DOWNLOAD_LINKS.IOS;
- } else if (getOperatingSystem() === CONST.OS.ANDROID) {
- link = CONST.APP_DOWNLOAD_LINKS.ANDROID;
- }
-
- const handleOpenAppStore = () => {
- setShowDownloadAppModal(false);
- setShouldShowBanner(false);
- Link.openExternalLink(link, true);
- };
-
- return (
-
- );
-}
-
-DownloadAppModal.displayName = 'DownloadAppModal';
-DownloadAppModal.propTypes = propTypes;
-DownloadAppModal.defaultProps = defaultProps;
-
-export default withOnyx({
- showDownloadAppBanner: {
- key: ONYXKEYS.SHOW_DOWNLOAD_APP_BANNER,
- },
-})(DownloadAppModal);
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/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/FocusTrapView/index.js b/src/components/FocusTrapView/index.js
deleted file mode 100644
index 2dcab7b9d998..000000000000
--- a/src/components/FocusTrapView/index.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * The FocusTrap is only used on web and desktop
- */
-import React, {useEffect, useRef} from 'react';
-import FocusTrap from 'focus-trap-react';
-import {View} from 'react-native';
-import {PropTypes} from 'prop-types';
-import {useIsFocused} from '@react-navigation/native';
-
-const propTypes = {
- /** Children to wrap with FocusTrap */
- children: PropTypes.node.isRequired,
-
- /** Whether to enable the FocusTrap */
- enabled: PropTypes.bool,
-
- /**
- * Whether to disable auto focus
- * It is used when the component inside the FocusTrap have their own auto focus logic
- */
- shouldEnableAutoFocus: PropTypes.bool,
-};
-
-const defaultProps = {
- enabled: true,
- shouldEnableAutoFocus: false,
-};
-
-function FocusTrapView({enabled, shouldEnableAutoFocus, ...props}) {
- const isFocused = useIsFocused();
-
- /**
- * Focus trap always needs a focusable element.
- * In case that we don't have any focusable elements in the modal,
- * the FocusTrap will use fallback View element using this ref.
- */
- const ref = useRef(null);
-
- /**
- * We have to set the 'tabindex' attribute to 0 to make the View focusable.
- * Currently, it is not possible to set this through props.
- * After the upgrade of 'react-native-web' to version 0.19 we can use 'tabIndex={0}' prop instead.
- */
- useEffect(() => {
- if (!ref.current) {
- return;
- }
- ref.current.setAttribute('tabindex', '0');
- }, []);
-
- return enabled ? (
- shouldEnableAutoFocus && ref.current,
- fallbackFocus: () => ref.current,
- clickOutsideDeactivates: true,
- }}
- >
-
-
- ) : (
- props.children
- );
-}
-
-FocusTrapView.displayName = 'FocusTrapView';
-FocusTrapView.propTypes = propTypes;
-FocusTrapView.defaultProps = defaultProps;
-
-export default FocusTrapView;
diff --git a/src/components/FocusTrapView/index.native.js b/src/components/FocusTrapView/index.native.js
deleted file mode 100644
index 5720601f5a2b..000000000000
--- a/src/components/FocusTrapView/index.native.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * The FocusTrap is only used on web and desktop
- */
-
-function FocusTrapView({children}) {
- return children;
-}
-
-FocusTrapView.displayName = 'FocusTrapView';
-
-export default FocusTrapView;
diff --git a/src/components/GrowlNotification/index.js b/src/components/GrowlNotification/index.js
index 70cadd5efd8e..a06185ac3320 100644
--- a/src/components/GrowlNotification/index.js
+++ b/src/components/GrowlNotification/index.js
@@ -10,6 +10,7 @@ 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]: {
@@ -59,7 +60,7 @@ function GrowlNotification(_, ref) {
Animated.spring(translateY, {
toValue: val,
duration: 80,
- useNativeDriver: true,
+ useNativeDriver,
}).start();
},
[translateY],
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.js
index d91510c3ec6a..262a4d1f178e 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.js
@@ -1,5 +1,6 @@
import _ from 'underscore';
import React from 'react';
+import CONST from '../../../CONST';
import htmlRendererPropTypes from './htmlRendererPropTypes';
import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
import Text from '../../Text';
@@ -28,6 +29,7 @@ function EditedRenderer(props) {
{' '}
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/MagicCodeInput.js b/src/components/MagicCodeInput.js
index b21a275a6597..454aacc8a03b 100644
--- a/src/components/MagicCodeInput.js
+++ b/src/components/MagicCodeInput.js
@@ -258,7 +258,13 @@ function MagicCodeInput(props) {
{/* Hide the input above the text. Cannot set opacity to 0 as it would break pasting on iOS Safari. */}
(inputRefs.current[index] = ref)}
+ ref={(ref) => {
+ inputRefs.current[index] = ref;
+ // Setting attribute type to "search" to prevent Password Manager from appearing in Mobile Chrome
+ if (ref && ref.setAttribute) {
+ ref.setAttribute('type', 'search');
+ }
+ }}
autoFocus={index === 0 && props.autoFocus}
inputMode="numeric"
textContentType="oneTimeCode"
diff --git a/src/components/MapView/responder/index.android.ts b/src/components/MapView/responder/index.android.ts
new file mode 100644
index 000000000000..a0fce71d8ef5
--- /dev/null
+++ b/src/components/MapView/responder/index.android.ts
@@ -0,0 +1,8 @@
+import {PanResponder} from 'react-native';
+
+const responder = PanResponder.create({
+ onStartShouldSetPanResponder: () => true,
+ onPanResponderTerminationRequest: () => false,
+});
+
+export default responder;
diff --git a/src/components/MapView/responder/index.ts b/src/components/MapView/responder/index.ts
index a0fce71d8ef5..422d6c1b4963 100644
--- a/src/components/MapView/responder/index.ts
+++ b/src/components/MapView/responder/index.ts
@@ -1,7 +1,7 @@
import {PanResponder} from 'react-native';
const responder = PanResponder.create({
- onStartShouldSetPanResponder: () => true,
+ onMoveShouldSetPanResponder: () => true,
onPanResponderTerminationRequest: () => false,
});
diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js
index c39c1d503258..88d3df3b7001 100644
--- a/src/components/MenuItem.js
+++ b/src/components/MenuItem.js
@@ -23,6 +23,7 @@ import variables from '../styles/variables';
import * as Session from '../libs/actions/Session';
import Hoverable from './Hoverable';
import useWindowDimensions from '../hooks/useWindowDimensions';
+import RenderHTML from './RenderHTML';
const propTypes = menuItemPropTypes;
@@ -73,6 +74,7 @@ const defaultProps = {
title: '',
numberOfLinesTitle: 1,
shouldGreyOutWhenDisabled: true,
+ shouldRenderAsHTML: false,
};
const MenuItem = React.forwardRef((props, ref) => {
@@ -220,10 +222,13 @@ const MenuItem = React.forwardRef((props, ref) => {
)}
- {Boolean(props.title) && (
+ {Boolean(props.title) && Boolean(props.shouldRenderAsHTML) && }
+
+ {Boolean(props.title) && !props.shouldRenderAsHTML && (
{convertToLTR(props.title)}
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/MultipleAvatars.js b/src/components/MultipleAvatars.js
index 4c6ba1307fb7..916646b5619a 100644
--- a/src/components/MultipleAvatars.js
+++ b/src/components/MultipleAvatars.js
@@ -221,6 +221,7 @@ function MultipleAvatars(props) {
{`+${avatars.length - props.maxAvatarsInRow}`}
@@ -278,6 +279,7 @@ function MultipleAvatars(props) {
{`+${props.icons.length - 1}`}
diff --git a/src/components/NewDatePicker/CalendarPicker/index.js b/src/components/NewDatePicker/CalendarPicker/index.js
index fe0c36d32e41..1e1ef3c3fad3 100644
--- a/src/components/NewDatePicker/CalendarPicker/index.js
+++ b/src/components/NewDatePicker/CalendarPicker/index.js
@@ -130,7 +130,10 @@ class CalendarPicker extends React.PureComponent {
return (
-
+
this.setState({isYearPickerVisible: true})}
style={[styles.alignItemsCenter, styles.flexRow, styles.flex1, styles.justifyContentStart]}
@@ -186,6 +189,7 @@ class CalendarPicker extends React.PureComponent {
{dayOfWeek[0]}
@@ -212,6 +216,7 @@ class CalendarPicker extends React.PureComponent {
accessibilityLabel={day ? day.toString() : undefined}
focusable={Boolean(day)}
accessible={Boolean(day)}
+ dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{({hovered, pressed}) => (
{},
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/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js
index 1f60dddef6ec..5f8151b385a2 100644
--- a/src/components/ReportActionItem/ReportPreview.js
+++ b/src/components/ReportActionItem/ReportPreview.js
@@ -177,7 +177,7 @@ function ReportPreview(props) {
onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)}
- style={[styles.flexRow, styles.justifyContentBetween]}
+ style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox]}
accessibilityRole="button"
accessibilityLabel={props.translate('iou.viewDetails')}
>
diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js
index 14b369d86a4c..4546e83ef769 100644
--- a/src/components/RoomNameInput/index.js
+++ b/src/components/RoomNameInput/index.js
@@ -6,7 +6,7 @@ import useLocalize from '../../hooks/useLocalize';
import * as roomNameInputPropTypes from './roomNameInputPropTypes';
import * as RoomNameInputUtils from '../../libs/RoomNameInputUtils';
-function RoomNameInput({autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange}) {
+function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) {
const {translate} = useLocalize();
const [selection, setSelection] = useState();
@@ -57,8 +57,9 @@ function RoomNameInput({autoFocus, disabled, errorText, forwardedRef, value, onB
onSelectionChange={(event) => setSelection(event.nativeEvent.selection)}
errorText={errorText}
autoCapitalize="none"
- onBlur={onBlur}
- autoFocus={autoFocus}
+ onBlur={() => isFocused && onBlur()}
+ shouldDelayFocus={shouldDelayFocus}
+ autoFocus={isFocused && autoFocus}
maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH}
spellCheck={false}
/>
diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js
index e263321dea0e..78500a8f0be2 100644
--- a/src/components/RoomNameInput/index.native.js
+++ b/src/components/RoomNameInput/index.native.js
@@ -7,7 +7,7 @@ import * as roomNameInputPropTypes from './roomNameInputPropTypes';
import * as RoomNameInputUtils from '../../libs/RoomNameInputUtils';
import getOperatingSystem from '../../libs/getOperatingSystem';
-function RoomNameInput({autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) {
+function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) {
const {translate} = useLocalize();
/**
@@ -41,8 +41,8 @@ function RoomNameInput({autoFocus, disabled, errorText, forwardedRef, value, onB
errorText={errorText}
maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH}
keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449
- onBlur={onBlur}
- autoFocus={autoFocus}
+ onBlur={() => isFocused && onBlur()}
+ autoFocus={isFocused && autoFocus}
autoCapitalize="none"
shouldDelayFocus={shouldDelayFocus}
/>
diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js
index 3eef833a1252..ab1ac37d32c8 100644
--- a/src/components/RoomNameInput/roomNameInputPropTypes.js
+++ b/src/components/RoomNameInput/roomNameInputPropTypes.js
@@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
+import {withNavigationFocusPropTypes} from '../withNavigationFocus';
const propTypes = {
/** Callback to execute when the text input is modified correctly */
@@ -27,6 +28,8 @@ const propTypes = {
/** Whether we should wait before focusing the TextInput, useful when using transitions on Android */
shouldDelayFocus: PropTypes.bool,
+
+ ...withNavigationFocusPropTypes,
};
const defaultProps = {
diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js
index f0f8b8a4b09b..f760e5d5aeb4 100644
--- a/src/components/ScreenWrapper/index.js
+++ b/src/components/ScreenWrapper/index.js
@@ -3,7 +3,6 @@ import React from 'react';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import {PickerAvoidingView} from 'react-native-picker-select';
-import FocusTrapView from '../FocusTrapView';
import KeyboardAvoidingView from '../KeyboardAvoidingView';
import CONST from '../../CONST';
import styles from '../../styles/styles';
@@ -125,26 +124,20 @@ class ScreenWrapper extends React.Component {
style={styles.flex1}
enabled={this.props.shouldEnablePickerAvoiding}
>
-
-
- {this.props.environment === CONST.ENVIRONMENT.DEV && }
- {this.props.environment === CONST.ENVIRONMENT.DEV && }
- {
- // If props.children is a function, call it to provide the insets to the children.
- _.isFunction(this.props.children)
- ? this.props.children({
- insets,
- safeAreaPaddingBottomStyle,
- didScreenTransitionEnd: this.state.didScreenTransitionEnd,
- })
- : this.props.children
- }
- {this.props.isSmallScreenWidth && this.props.shouldShowOfflineIndicator && }
-
+
+ {this.props.environment === CONST.ENVIRONMENT.DEV && }
+ {this.props.environment === CONST.ENVIRONMENT.DEV && }
+ {
+ // If props.children is a function, call it to provide the insets to the children.
+ _.isFunction(this.props.children)
+ ? this.props.children({
+ insets,
+ safeAreaPaddingBottomStyle,
+ didScreenTransitionEnd: this.state.didScreenTransitionEnd,
+ })
+ : this.props.children
+ }
+ {this.props.isSmallScreenWidth && this.props.shouldShowOfflineIndicator && }
diff --git a/src/components/ScreenWrapper/propTypes.js b/src/components/ScreenWrapper/propTypes.js
index c3538b3c026d..83033d9e97b7 100644
--- a/src/components/ScreenWrapper/propTypes.js
+++ b/src/components/ScreenWrapper/propTypes.js
@@ -48,12 +48,6 @@ const propTypes = {
/** Styles for the offline indicator */
offlineIndicatorStyle: stylePropTypes,
-
- /** Whether to disable the focus trap */
- shouldDisableFocusTrap: PropTypes.bool,
-
- /** Whether to disable auto focus of the focus trap */
- shouldEnableAutoFocus: PropTypes.bool,
};
const defaultProps = {
@@ -69,8 +63,6 @@ const defaultProps = {
shouldShowOfflineIndicator: true,
offlineIndicatorStyle: [],
headerGapStyles: [],
- shouldDisableFocusTrap: false,
- shouldEnableAutoFocus: false,
};
export {propTypes, defaultProps};
diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js
index e54d64fe56e8..e57f00e1849c 100644
--- a/src/components/SelectionList/BaseSelectionList.js
+++ b/src/components/SelectionList/BaseSelectionList.js
@@ -2,7 +2,7 @@ import React, {useCallback, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import lodashGet from 'lodash/get';
-import {useFocusEffect} from '@react-navigation/native';
+import {useFocusEffect, useIsFocused} from '@react-navigation/native';
import SectionList from '../SectionList';
import Text from '../Text';
import styles from '../../styles/styles';
@@ -61,6 +61,7 @@ function BaseSelectionList({
const shouldShowTextInput = Boolean(textInputLabel);
const shouldShowSelectAll = Boolean(onSelectAll);
const activeElement = useActiveElement();
+ const isFocused = useIsFocused();
/**
* Iterates through the sections and items inside each section, and builds 3 arrays along the way:
@@ -244,7 +245,7 @@ function BaseSelectionList({
const renderItem = ({item, index, section}) => {
const normalizedIndex = index + lodashGet(section, 'indexOffset', 0);
const isDisabled = section.isDisabled;
- const isFocused = !isDisabled && focusedIndex === normalizedIndex;
+ const isItemFocused = !isDisabled && focusedIndex === normalizedIndex;
// We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade.
const showTooltip = normalizedIndex < 10;
@@ -252,7 +253,7 @@ function BaseSelectionList({
return (
selectRow(item, index)}
onDismissError={onDismissError}
showTooltip={showTooltip}
@@ -263,7 +264,7 @@ function BaseSelectionList({
return (
selectRow(item, index)}
/>
@@ -289,14 +290,14 @@ function BaseSelectionList({
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, {
captureOnInputs: true,
shouldBubble: () => !flattenedSections.allOptions[focusedIndex],
- isActive: !activeElement,
+ isActive: !activeElement && isFocused,
});
/** Calls confirm action when pressing CTRL (CMD) + Enter */
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, {
captureOnInputs: true,
shouldBubble: () => !flattenedSections.allOptions[focusedIndex],
- isActive: Boolean(onConfirm),
+ isActive: Boolean(onConfirm) && isFocused,
});
return (
@@ -347,6 +348,7 @@ function BaseSelectionList({
accessibilityRole="button"
accessibilityState={{checked: flattenedSections.allSelected}}
disabled={flattenedSections.allOptions.length === flattenedSections.disabledOptionsIndexes.length}
+ dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
diff --git a/src/components/SelectionList/UserListItem.js b/src/components/SelectionList/UserListItem.js
index a3c25f09af3b..014e0cf879a5 100644
--- a/src/components/SelectionList/UserListItem.js
+++ b/src/components/SelectionList/UserListItem.js
@@ -63,6 +63,7 @@ function UserListItem({item, isFocused = false, showTooltip, onSelectRow, onDism
hoverDimmingValue={1}
hoverStyle={styles.hoveredComponentBG}
focusStyle={styles.hoveredComponentBG}
+ dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{
+const getIconAndTitle = (route, translate) => {
switch (route) {
+ case CONST.TAB.MANUAL:
+ return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')};
case CONST.TAB.SCAN:
- return Expensicons.Receipt;
+ return {icon: Expensicons.Receipt, title: translate('tabSelector.scan')};
+ case CONST.TAB.NEW_CHAT:
+ return {icon: Expensicons.User, title: translate('tabSelector.chat')};
+ case CONST.TAB.NEW_ROOM:
+ return {icon: Expensicons.Hashtag, title: translate('tabSelector.room')};
case CONST.TAB.DISTANCE:
- return Expensicons.Car;
+ return {icon: Expensicons.Car, title: translate('common.distance')};
default:
- return Expensicons.Pencil;
- }
-};
-
-const getTitle = (route, translate) => {
- switch (route) {
- case CONST.TAB.SCAN:
- return translate('tabSelector.scan');
- case CONST.TAB.DISTANCE:
- return translate('common.distance');
- default:
- return translate('tabSelector.manual');
+ throw new Error(`Route ${route} has no icon nor title set.`);
}
};
@@ -94,8 +89,8 @@ function TabSelector({state, navigation, onTabPress, position}) {
const activeOpacity = getOpacity(position, state.routes.length, index, true);
const inactiveOpacity = getOpacity(position, state.routes.length, index, false);
const backgroundColor = getBackgroundColor(position, state.routes.length, index);
-
const isFocused = index === state.index;
+ const {icon, title} = getIconAndTitle(route.name, translate);
const onPress = () => {
if (isFocused) {
@@ -119,8 +114,8 @@ function TabSelector({state, navigation, onTabPress, position}) {
return (
!prevPasswordHidden);
}, []);
- const storePrefixLayoutDimensions = useCallback((event) => {
- setPrefixWidth(Math.abs(event.nativeEvent.layout.width));
- }, []);
+ // When adding a new prefix character, adjust this method to add expected character width.
+ // This is because character width isn't known before it's rendered to the screen, and once it's rendered,
+ // it's too late to calculate it's width because the change in padding would cause a visible jump.
+ // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size
+ // also have an impact on the width of the character, but as long as there's only one font-family and one font-size,
+ // this method will produce reliable results.
+ const getCharacterPadding = (prefix) => {
+ switch (prefix) {
+ case CONST.POLICY.ROOM_PREFIX:
+ return 10;
+ default:
+ throw new Error(`Prefix ${prefix} has no padding assigned.`);
+ }
+ };
// eslint-disable-next-line react/forbid-foreign-prop-types
const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes));
@@ -295,7 +306,7 @@ function BaseTextInput(props) {
pointerEvents="none"
selectable={false}
style={[styles.textInputPrefix, !hasLabel && styles.pv0]}
- onLayout={storePrefixLayoutDimensions}
+ dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{props.prefixCharacter}
@@ -322,7 +333,7 @@ function BaseTextInput(props) {
styles.w100,
props.inputStyle,
(!hasLabel || isMultiline) && styles.pv0,
- props.prefixCharacter && StyleUtils.getPaddingLeft(prefixWidth + styles.pl1.paddingLeft),
+ props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft),
props.secureTextEntry && styles.secureInput,
// Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear
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 (
true,
- addListener: () => () => {},
- removeListener: () => () => {},
- }}
- >
-
-
- );
- }
+ function WithNavigationFallback(props) {
+ const context = useContext(NavigationContext);
- return (
+ const navigationContextValue = useMemo(() => ({isFocused: () => true, addListener: () => () => {}, removeListener: () => () => {}}), []);
+
+ return context ? (
+
+ ) : (
+
- );
- }
+
+ );
}
- WithNavigationFallback.contextType = NavigationContext;
WithNavigationFallback.displayName = `WithNavigationFocusWithFallback(${getComponentDisplayName(WrappedComponent)})`;
WithNavigationFallback.propTypes = {
forwardedRef: refPropTypes,
@@ -41,7 +32,8 @@ export default function (WrappedComponent) {
WithNavigationFallback.defaultProps = {
forwardedRef: undefined,
};
- return React.forwardRef((props, ref) => (
+
+ return forwardRef((props, ref) => (
variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint;
const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 03adaa1e66b1..c31e39319a98 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -344,11 +344,6 @@ export default {
`It's always great to see a new face around here! Please enter the magic code sent to ${login}. It should arrive within a minute or two.`,
welcomeEnterMagicCode: ({login}: WelcomeEnterMagicCodeParams) => `Please enter the magic code sent to ${login}. It should arrive within a minute or two.`,
},
- DownloadAppModal: {
- downloadTheApp: 'Download the app',
- keepTheConversationGoing: 'Keep the conversation going in New Expensify, download the app for an enhanced experience.',
- noThanks: 'No thanks',
- },
login: {
hero: {
header: 'Split bills, request payments, and chat with friends.',
@@ -460,13 +455,10 @@ export default {
},
},
sidebarScreen: {
- fabAction: 'New chat',
- newChat: 'New chat',
- newGroup: 'New group',
- newRoom: 'New room',
buttonSearch: 'Search',
buttonMySettings: 'My settings',
- fabNewChat: 'New chat (Floating action)',
+ fabNewChat: 'Send message',
+ fabNewChatExplained: 'Send message (Floating action)',
chatPinned: 'Chat pinned',
draftedMessage: 'Drafted message',
listOfChatMessages: 'List of chat messages',
@@ -474,6 +466,8 @@ export default {
saveTheWorld: 'Save the world',
},
tabSelector: {
+ chat: 'Chat',
+ room: 'Room',
manual: 'Manual',
scan: 'Scan',
},
@@ -500,9 +494,10 @@ export default {
approved: 'Approved',
cash: 'Cash',
split: 'Split',
+ addToSplit: 'Add to split',
+ splitBill: 'Split bill',
request: 'Request',
participants: 'Participants',
- splitBill: 'Split bill',
requestMoney: 'Request money',
sendMoney: 'Send money',
pay: 'Pay',
@@ -754,6 +749,13 @@ export default {
passwordUpdated: 'Password updated!',
allSet: 'You’re all set. Keep your new password safe.',
},
+ privateNotes: {
+ title: 'Private notes',
+ personalNoteMessage: 'Keep notes about this chat here. You are the only person who can add, edit or view these notes.',
+ sharedNoteMessage: 'Keep notes about this chat here. Expensify employees and other users on the team.expensify.com domain can view these notes.',
+ notesUnavailable: 'No notes found for the user',
+ composerLabel: 'Notes',
+ },
addPayPalMePage: {
enterYourUsernameToGetPaidViaPayPal: 'Get paid back via PayPal.',
payPalMe: 'PayPal.me/',
@@ -988,7 +990,9 @@ export default {
localTime: 'Local time',
},
newChatPage: {
+ createChat: 'Create chat',
createGroup: 'Create group',
+ addToGroup: 'Add to group',
},
yearPickerPage: {
year: 'Year',
@@ -1538,7 +1542,7 @@ export default {
openShortcutDialog: 'Opens the keyboard shortcuts dialog',
escape: 'Escape dialogs',
search: 'Open search dialog',
- newGroup: 'New group screen',
+ newChat: 'New chat screen',
copy: 'Copy comment',
},
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 7315c42cebab..d83104ff85e0 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -335,11 +335,6 @@ export default {
`¡Siempre es genial ver una cara nueva por aquÃ! Por favor ingresa el código mágico enviado a ${login}. DeberÃa llegar en un par de minutos.`,
welcomeEnterMagicCode: ({login}: WelcomeEnterMagicCodeParams) => `Por favor, introduce el código mágico enviado a ${login}. DeberÃa llegar en un par de minutos.`,
},
- DownloadAppModal: {
- downloadTheApp: 'Descarga la aplicación',
- keepTheConversationGoing: 'Mantén la conversación en New Expensify, descarga la aplicación para una experiencia mejorada.',
- noThanks: 'No, gracias',
- },
login: {
hero: {
header: 'Divida las facturas, solicite pagos y chatee con sus amigos.',
@@ -452,13 +447,10 @@ export default {
},
},
sidebarScreen: {
- fabAction: 'Nuevo chat',
- newChat: 'Nuevo chat',
- newGroup: 'Nuevo grupo',
- newRoom: 'Nueva sala de chat',
buttonSearch: 'Buscar',
buttonMySettings: 'Mi configuración',
- fabNewChat: 'Nuevo chat',
+ fabNewChat: 'Enviar mensaje',
+ fabNewChatExplained: 'Enviar mensaje',
chatPinned: 'Chat fijado',
draftedMessage: 'Mensaje borrador',
listOfChatMessages: 'Lista de mensajes del chat',
@@ -466,6 +458,8 @@ export default {
saveTheWorld: 'Salvar el mundo',
},
tabSelector: {
+ chat: 'Chat',
+ room: 'Sala',
manual: 'Manual',
scan: 'Escanear',
},
@@ -492,9 +486,10 @@ export default {
approved: 'Aprobado',
cash: 'Efectivo',
split: 'Dividir',
+ addToSplit: 'Añadir para dividir',
+ splitBill: 'Dividir factura',
request: 'Solicitar',
participants: 'Participantes',
- splitBill: 'Dividir factura',
requestMoney: 'Pedir dinero',
sendMoney: 'Enviar dinero',
pay: 'Pagar',
@@ -749,6 +744,13 @@ export default {
passwordUpdated: 'Contraseña actualizada!',
allSet: 'Todo está listo. Guarda tu contraseña en un lugar seguro.',
},
+ privateNotes: {
+ title: 'Notas privadas',
+ personalNoteMessage: 'Guarda notas sobre este chat aquÃ. Usted es la única persona que puede añadir, editar o ver estas notas.',
+ sharedNoteMessage: 'Guarda notas sobre este chat aquÃ. Los empleados de Expensify y otros usuarios del dominio team.expensify.com pueden ver estas notas.',
+ notesUnavailable: 'No se han encontrado notas para el usuario',
+ composerLabel: 'Notas',
+ },
addPayPalMePage: {
enterYourUsernameToGetPaidViaPayPal: 'Recibe pagos vÃa PayPal.',
payPalMe: 'PayPal.me/',
@@ -986,7 +988,9 @@ export default {
localTime: 'Hora local',
},
newChatPage: {
+ createChat: 'Crear chat',
createGroup: 'Crear grupo',
+ addToGroup: 'Añadir al grupo',
},
yearPickerPage: {
year: 'Año',
@@ -1560,7 +1564,7 @@ export default {
openShortcutDialog: 'Abre el cuadro de diálogo de métodos abreviados de teclado',
escape: 'Diálogos de escape',
search: 'Abrir diálogo de búsqueda',
- newGroup: 'Nueva pantalla de grupo',
+ newChat: 'Nueva pantalla de chat',
copy: 'Copiar comentario',
},
},
diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts
index bbb938a666ac..beb0ea800091 100644
--- a/src/libs/CardUtils.ts
+++ b/src/libs/CardUtils.ts
@@ -1,3 +1,6 @@
+import {Card} from '../types/onyx';
+import CONST from '../CONST';
+
/**
* @returns string with a month in MM format
*/
@@ -15,4 +18,11 @@ function getYearFromExpirationDateString(expirationDateString: string) {
return cardYear.length === 2 ? `20${cardYear}` : cardYear;
}
-export {getMonthFromExpirationDateString, getYearFromExpirationDateString};
+function getCompanyCards(cardList: {string: Card}) {
+ if (!cardList) {
+ return [];
+ }
+ return Object.values(cardList).filter((card) => card.bank !== CONST.EXPENSIFY_CARD.BANK);
+}
+
+export {getMonthFromExpirationDateString, getYearFromExpirationDateString, getCompanyCards};
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 6b83fe2dcbb8..c5bb02354641 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -273,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',
},
@@ -679,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;
@@ -777,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: () => {
@@ -798,7 +805,6 @@ export {
ReportWelcomeMessageModalStackNavigator,
ReportParticipantsModalStackNavigator,
SearchModalStackNavigator,
- NewGroupModalStackNavigator,
NewChatModalStackNavigator,
NewTaskModalStackNavigator,
SettingsModalStackNavigator,
@@ -808,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 a15daf992836..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: {
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/App.js b/src/libs/actions/App.js
index fffe43b88ee9..a9c3f8775cba 100644
--- a/src/libs/actions/App.js
+++ b/src/libs/actions/App.js
@@ -438,6 +438,15 @@ function beginDeepLinkRedirect(shouldAuthenticateWithCurrentAccount = true) {
// eslint-disable-next-line rulesdir/no-api-side-effects-method
API.makeRequestWithSideEffects('OpenOldDotLink', {shouldRetry: false}, {}).then((response) => {
+ if (!response) {
+ Log.alert(
+ 'Trying to redirect via deep link, but the response is empty. User likely not authenticated.',
+ {response, shouldAuthenticateWithCurrentAccount, currentUserAccountID},
+ true,
+ );
+ return;
+ }
+
Browser.openRouteInDesktopApp(response.shortLivedAuthToken, currentUserEmail);
});
}
diff --git a/src/libs/actions/Device/getDeviceInfo/getBaseInfo.js b/src/libs/actions/Device/getDeviceInfo/getBaseInfo.js
index a02178cbc0a3..bb66f3fe7a9b 100644
--- a/src/libs/actions/Device/getDeviceInfo/getBaseInfo.js
+++ b/src/libs/actions/Device/getDeviceInfo/getBaseInfo.js
@@ -1,8 +1,8 @@
-import {version} from '../../../../../package.json';
+import packageConfig from '../../../../../package.json';
export default function getBaseInfo() {
return {
- app_version: version,
+ app_version: packageConfig.version,
timestamp: new Date().toISOString().slice(0, 19),
};
}
diff --git a/src/libs/actions/DownloadAppModal.js b/src/libs/actions/DownloadAppModal.js
deleted file mode 100644
index 5dc2d3fdca22..000000000000
--- a/src/libs/actions/DownloadAppModal.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import Onyx from 'react-native-onyx';
-import ONYXKEYS from '../../ONYXKEYS';
-
-/**
- * @param {Boolean} shouldShowBanner
- */
-function setShowDownloadAppModal(shouldShowBanner) {
- Onyx.set(ONYXKEYS.SHOW_DOWNLOAD_APP_BANNER, shouldShowBanner);
-}
-
-export default setShowDownloadAppModal;
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 c5c6b03f3d44..3cefcd00ed60 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -89,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,
@@ -1950,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/OnyxUpdateManager.js b/src/libs/actions/OnyxUpdateManager.js
index f0051b85f302..e0f3f8fd4622 100644
--- a/src/libs/actions/OnyxUpdateManager.js
+++ b/src/libs/actions/OnyxUpdateManager.js
@@ -1,9 +1,11 @@
import Onyx from 'react-native-onyx';
+import _ from 'underscore';
import ONYXKEYS from '../../ONYXKEYS';
import Log from '../Log';
import * as SequentialQueue from '../Network/SequentialQueue';
import * as App from './App';
import * as OnyxUpdates from './OnyxUpdates';
+import CONST from '../../CONST';
// This file is in charge of looking at the updateIDs coming from the server and comparing them to the last updateID that the client has.
// If the client is behind the server, then we need to
@@ -35,6 +37,19 @@ export default () => {
return;
}
+ // Since we used the same key that used to store another object, let's confirm that the current object is
+ // following the new format before we proceed. If it isn't, then let's clear the object in Onyx.
+ if (
+ !_.isObject(val) ||
+ !_.has(val, 'type') ||
+ (!(val.type === CONST.ONYX_UPDATE_TYPES.HTTPS && _.has(val, 'request') && _.has(val, 'response')) && !(val.type === CONST.ONYX_UPDATE_TYPES.PUSHER && _.has(val, 'updates')))
+ ) {
+ console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue');
+ Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null);
+ SequentialQueue.unpause();
+ return;
+ }
+
const updateParams = val;
const lastUpdateIDFromServer = val.lastUpdateID;
const previousUpdateIDFromServer = val.previousUpdateID;
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 2542e65aa20a..5b4c1ea56bd3 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -1959,6 +1959,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,
@@ -2006,4 +2136,8 @@ export {
setLastOpenedPublicRoom,
flagComment,
openLastOpenedPublicRoom,
+ updatePrivateNotes,
+ getReportPrivateNote,
+ clearPrivateNotesError,
+ hasErrorInPrivateNotes,
};
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/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/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..19f2b1fdc0c6 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,8 +141,10 @@ function ProfilePage(props) {
const navigateBackTo = lodashGet(props.route, 'params.backTo', '');
+ const chatReportWithCurrentUser = !isCurrentUser && !Session.isAnonymousUser() ? ReportUtils.getChatByParticipants([accountID]) : 0;
+
return (
-
+
Navigation.goBack(navigateBackTo)}
@@ -235,6 +238,17 @@ function ProfilePage(props) {
shouldShowRightIcon
/>
)}
+ {!_.isEmpty(chatReportWithCurrentUser) && (
+