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