diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 02f4af653ca6..24f3b6aa9aeb 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -264,11 +264,11 @@ jobs:
- name: Deploy production to S3
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://expensify-cash/ && aws s3 cp --content-type 'application/json' --metadata-directive REPLACE s3://expensify-cash/.well-known/apple-app-site-association s3://expensify-cash/.well-known/apple-app-site-association
+ run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://expensify-cash/ && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://expensify-cash/.well-known/apple-app-site-association s3://expensify-cash/.well-known/apple-app-site-association
- name: Deploy staging to S3
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://staging-expensify-cash/ && aws s3 cp --content-type 'application/json' --metadata-directive REPLACE s3://staging-expensify-cash/.well-known/apple-app-site-association s3://staging-expensify-cash/.well-known/apple-app-site-association
+ run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://staging-expensify-cash/ && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://staging-expensify-cash/.well-known/apple-app-site-association s3://staging-expensify-cash/.well-known/apple-app-site-association
- name: Purge production Cloudflare cache
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index ef37d1266930..6b068c9f6f8e 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -290,6 +290,6 @@ jobs:
IOS: ${{ needs.iOS.result }}
WEB: ${{ needs.web.result }}
ANDROID_LINK: ${{fromJson(steps.get_android_path.outputs.android_paths).html_path}}
- DESKTOP_LINK: "https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/$PULL_REQUEST_NUMBER/NewExpensify.dmg"
+ DESKTOP_LINK: https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/${{ env.PULL_REQUEST_NUMBER }}/NewExpensify.dmg
IOS_LINK: ${{ fromJson(steps.get_ios_path.outputs.ios_paths).html_path }}
- WEB_LINK: "https://$PULL_REQUEST_NUMBER.pr-testing.expensify.com"
+ WEB_LINK: https://${{ env.PULL_REQUEST_NUMBER }}.pr-testing.expensify.com
diff --git a/android/app/build.gradle b/android/app/build.gradle
index a0f9f25292d2..fc33dbe46373 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -156,8 +156,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001028002
- versionName "1.2.80-2"
+ versionCode 1001028100
+ versionName "1.2.81-0"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) {
diff --git a/assets/images/add-reaction.svg b/assets/images/add-reaction.svg
new file mode 100644
index 000000000000..a576e2c84622
--- /dev/null
+++ b/assets/images/add-reaction.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 9f319b4fcb79..46cd23113d25 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.2.80
+ 1.2.81
CFBundleSignature
????
CFBundleURLTypes
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.2.80.2
+ 1.2.81.0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 419a9a37a8b4..fc7e0ae14189 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.2.80
+ 1.2.81
CFBundleSignature
????
CFBundleVersion
- 1.2.80.2
+ 1.2.81.0
diff --git a/package-lock.json b/package-lock.json
index 0b4897abca4f..e1b55c1a17e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.2.80-2",
+ "version": "1.2.81-0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.2.80-2",
+ "version": "1.2.81-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 4b46717518d2..5dcbbb04bc81 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.2.80-2",
+ "version": "1.2.81-0",
"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.",
diff --git a/src/CONST.js b/src/CONST.js
index b8c26f163376..80f8ed3ccd59 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -823,6 +823,10 @@ const CONST = {
EMOJIS: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu,
TAX_ID: /^\d{9}$/,
NON_NUMERIC: /\D/g,
+
+ // Extract attachment's source from the data's html string
+ ATTACHMENT_DATA: /(data-expensify-source|data-name)="([^"]+)"/g,
+
NON_NUMERIC_WITH_PLUS: /[^0-9+]/g,
EMOJI_NAME: /:[\w+-]+:/g,
EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/,
@@ -973,261 +977,293 @@ const CONST = {
MAKE_REQUEST_WITH_SIDE_EFFECTS: 'makeRequestWithSideEffects',
},
+ QUICK_REACTIONS: [
+ {
+ name: '+1',
+ code: '👍',
+ types: [
+ '👍🏿',
+ '👍🏾',
+ '👍🏽',
+ '👍🏼',
+ '👍🏻',
+ ],
+ },
+ {
+ name: 'heart',
+ code: '❤️',
+ },
+ {
+ name: 'joy',
+ code: '😂',
+ },
+ {
+ name: 'fire',
+ code: '🔥',
+ },
+ ],
+
TFA_CODE_LENGTH: 6,
CHAT_ATTACHMENT_TOKEN_KEY: 'X-Chat-Attachment-Token',
USA_COUNTRY_NAME,
- ALL_COUNTRIES: [
- 'Afghanistan',
- 'Aland Islands',
- 'Albania',
- 'Algeria',
- 'American Samoa',
- 'Andorra',
- 'Angola',
- 'Anguilla',
- 'Antarctica',
- 'Antigua and Barbuda',
- 'Argentina',
- 'Armenia',
- 'Aruba',
- 'Australia',
- 'Austria',
- 'Azerbaijan',
- 'Bahamas',
- 'Bahrain',
- 'Bangladesh',
- 'Barbados',
- 'Belarus',
- 'Belgium',
- 'Belize',
- 'Benin',
- 'Bermuda',
- 'Bhutan',
- 'Bolivia',
- 'Bonaire, Saint Eustatius and Saba ',
- 'Bosnia and Herzegovina',
- 'Botswana',
- 'Bouvet Island',
- 'Brazil',
- 'British Indian Ocean Territory',
- 'British Virgin Islands',
- 'Brunei',
- 'Bulgaria',
- 'Burkina Faso',
- 'Burundi',
- 'Cambodia',
- 'Cameroon',
- 'Canada',
- 'Cape Verde',
- 'Cayman Islands',
- 'Central African Republic',
- 'Chad',
- 'Chile',
- 'China',
- 'Christmas Island',
- 'Cocos Islands',
- 'Colombia',
- 'Comoros',
- 'Cook Islands',
- 'Costa Rica',
- 'Croatia',
- 'Cuba',
- 'Curacao',
- 'Cyprus',
- 'Czech Republic',
- 'Democratic Republic of the Congo',
- 'Denmark',
- 'Djibouti',
- 'Dominica',
- 'Dominican Republic',
- 'East Timor',
- 'Ecuador',
- 'Egypt',
- 'El Salvador',
- 'Equatorial Guinea',
- 'Eritrea',
- 'Estonia',
- 'Ethiopia',
- 'Falkland Islands',
- 'Faroe Islands',
- 'Fiji',
- 'Finland',
- 'France',
- 'French Guiana',
- 'French Polynesia',
- 'French Southern Territories',
- 'Gabon',
- 'Gambia',
- 'Georgia',
- 'Germany',
- 'Ghana',
- 'Gibraltar',
- 'Greece',
- 'Greenland',
- 'Grenada',
- 'Guadeloupe',
- 'Guam',
- 'Guatemala',
- 'Guernsey',
- 'Guinea',
- 'Guinea-Bissau',
- 'Guyana',
- 'Haiti',
- 'Heard Island and McDonald Islands',
- 'Honduras',
- 'Hong Kong',
- 'Hungary',
- 'Iceland',
- 'India',
- 'Indonesia',
- 'Iran',
- 'Iraq',
- 'Ireland',
- 'Isle of Man',
- 'Israel',
- 'Italy',
- 'Ivory Coast',
- 'Jamaica',
- 'Japan',
- 'Jersey',
- 'Jordan',
- 'Kazakhstan',
- 'Kenya',
- 'Kiribati',
- 'Kosovo',
- 'Kuwait',
- 'Kyrgyzstan',
- 'Laos',
- 'Latvia',
- 'Lebanon',
- 'Lesotho',
- 'Liberia',
- 'Libya',
- 'Liechtenstein',
- 'Lithuania',
- 'Luxembourg',
- 'Macao',
- 'Macedonia',
- 'Madagascar',
- 'Malawi',
- 'Malaysia',
- 'Maldives',
- 'Mali',
- 'Malta',
- 'Marshall Islands',
- 'Martinique',
- 'Mauritania',
- 'Mauritius',
- 'Mayotte',
- 'Mexico',
- 'Micronesia',
- 'Moldova',
- 'Monaco',
- 'Mongolia',
- 'Montenegro',
- 'Montserrat',
- 'Morocco',
- 'Mozambique',
- 'Myanmar',
- 'Namibia',
- 'Nauru',
- 'Nepal',
- 'Netherlands',
- 'New Caledonia',
- 'New Zealand',
- 'Nicaragua',
- 'Niger',
- 'Nigeria',
- 'Niue',
- 'Norfolk Island',
- 'North Korea',
- 'Northern Mariana Islands',
- 'Norway',
- 'Oman',
- 'Pakistan',
- 'Palau',
- 'Palestinian Territory',
- 'Panama',
- 'Papua New Guinea',
- 'Paraguay',
- 'Peru',
- 'Philippines',
- 'Pitcairn',
- 'Poland',
- 'Portugal',
- 'Puerto Rico',
- 'Qatar',
- 'Republic of the Congo',
- 'Reunion',
- 'Romania',
- 'Russia',
- 'Rwanda',
- 'Saint Barthelemy',
- 'Saint Helena',
- 'Saint Kitts and Nevis',
- 'Saint Lucia',
- 'Saint Martin',
- 'Saint Pierre and Miquelon',
- 'Saint Vincent and the Grenadines',
- 'Samoa',
- 'San Marino',
- 'Sao Tome and Principe',
- 'Saudi Arabia',
- 'Senegal',
- 'Serbia',
- 'Seychelles',
- 'Sierra Leone',
- 'Singapore',
- 'Sint Maarten',
- 'Slovakia',
- 'Slovenia',
- 'Solomon Islands',
- 'Somalia',
- 'South Africa',
- 'South Georgia and the South Sandwich Islands',
- 'South Korea',
- 'South Sudan',
- 'Spain',
- 'Sri Lanka',
- 'Sudan',
- 'Suriname',
- 'Svalbard and Jan Mayen',
- 'Swaziland',
- 'Sweden',
- 'Switzerland',
- 'Syria',
- 'Taiwan',
- 'Tajikistan',
- 'Tanzania',
- 'Thailand',
- 'Togo',
- 'Tokelau',
- 'Tonga',
- 'Trinidad and Tobago',
- 'Tunisia',
- 'Turkey',
- 'Turkmenistan',
- 'Turks and Caicos Islands',
- 'Tuvalu',
- 'U.S. Virgin Islands',
- 'Uganda',
- 'Ukraine',
- 'United Arab Emirates',
- 'United Kingdom',
- USA_COUNTRY_NAME,
- 'Uruguay',
- 'Uzbekistan',
- 'Vanuatu',
- 'Vatican',
- 'Venezuela',
- 'Vietnam',
- 'Wallis and Futuna',
- 'Western Sahara',
- 'Yemen',
- 'Zambia',
- 'Zimbabwe',
- ],
+ ALL_COUNTRIES: {
+ AC: 'Ascension Island',
+ AD: 'Andorra',
+ AE: 'United Arab Emirates',
+ AF: 'Afghanistan',
+ AG: 'Antigua & Barbuda',
+ AI: 'Anguilla',
+ AL: 'Albania',
+ AM: 'Armenia',
+ AO: 'Angola',
+ AQ: 'Antarctica',
+ AR: 'Argentina',
+ AS: 'American Samoa',
+ AT: 'Austria',
+ AU: 'Australia',
+ AW: 'Aruba',
+ AX: 'Åland Islands',
+ AZ: 'Azerbaijan',
+ BA: 'Bosnia & Herzegovina',
+ BB: 'Barbados',
+ BD: 'Bangladesh',
+ BE: 'Belgium',
+ BF: 'Burkina Faso',
+ BG: 'Bulgaria',
+ BH: 'Bahrain',
+ BI: 'Burundi',
+ BJ: 'Benin',
+ BL: 'St. Barthélemy',
+ BM: 'Bermuda',
+ BN: 'Brunei',
+ BO: 'Bolivia',
+ BQ: 'Caribbean Netherlands',
+ BR: 'Brazil',
+ BS: 'Bahamas',
+ BT: 'Bhutan',
+ BW: 'Botswana',
+ BY: 'Belarus',
+ BZ: 'Belize',
+ CA: 'Canada',
+ CC: 'Cocos (Keeling) Islands',
+ CD: 'Congo - Kinshasa',
+ CF: 'Central African Republic',
+ CG: 'Congo - Brazzaville',
+ CH: 'Switzerland',
+ CI: 'Côte d’Ivoire',
+ CK: 'Cook Islands',
+ CL: 'Chile',
+ CM: 'Cameroon',
+ CN: 'China',
+ CO: 'Colombia',
+ CR: 'Costa Rica',
+ CU: 'Cuba',
+ CV: 'Cape Verde',
+ CW: 'Curaçao',
+ CX: 'Christmas Island',
+ CY: 'Cyprus',
+ CZ: 'Czechia',
+ DE: 'Germany',
+ DG: 'Diego Garcia',
+ DJ: 'Djibouti',
+ DK: 'Denmark',
+ DM: 'Dominica',
+ DO: 'Dominican Republic',
+ DZ: 'Algeria',
+ EA: 'Ceuta & Melilla',
+ EC: 'Ecuador',
+ EE: 'Estonia',
+ EG: 'Egypt',
+ EH: 'Western Sahara',
+ ER: 'Eritrea',
+ ES: 'Spain',
+ ET: 'Ethiopia',
+ EZ: 'Eurozone',
+ FI: 'Finland',
+ FJ: 'Fiji',
+ FK: 'Falkland Islands',
+ FM: 'Micronesia',
+ FO: 'Faroe Islands',
+ FR: 'France',
+ GA: 'Gabon',
+ GB: 'United Kingdom',
+ GD: 'Grenada',
+ GE: 'Georgia',
+ GF: 'French Guiana',
+ GG: 'Guernsey',
+ GH: 'Ghana',
+ GI: 'Gibraltar',
+ GL: 'Greenland',
+ GM: 'Gambia',
+ GN: 'Guinea',
+ GP: 'Guadeloupe',
+ GQ: 'Equatorial Guinea',
+ GR: 'Greece',
+ GS: 'South Georgia & South Sandwich Islands',
+ GT: 'Guatemala',
+ GU: 'Guam',
+ GW: 'Guinea-Bissau',
+ GY: 'Guyana',
+ HK: 'Hong Kong',
+ HN: 'Honduras',
+ HR: 'Croatia',
+ HT: 'Haiti',
+ HU: 'Hungary',
+ IC: 'Canary Islands',
+ ID: 'Indonesia',
+ IE: 'Ireland',
+ IL: 'Israel',
+ IM: 'Isle of Man',
+ IN: 'India',
+ IO: 'British Indian Ocean Territory',
+ IQ: 'Iraq',
+ IR: 'Iran',
+ IS: 'Iceland',
+ IT: 'Italy',
+ JE: 'Jersey',
+ JM: 'Jamaica',
+ JO: 'Jordan',
+ JP: 'Japan',
+ KE: 'Kenya',
+ KG: 'Kyrgyzstan',
+ KH: 'Cambodia',
+ KI: 'Kiribati',
+ KM: 'Comoros',
+ KN: 'St. Kitts & Nevis',
+ KP: 'North Korea',
+ KR: 'South Korea',
+ KW: 'Kuwait',
+ KY: 'Cayman Islands',
+ KZ: 'Kazakhstan',
+ LA: 'Laos',
+ LB: 'Lebanon',
+ LC: 'St. Lucia',
+ LI: 'Liechtenstein',
+ LK: 'Sri Lanka',
+ LR: 'Liberia',
+ LS: 'Lesotho',
+ LT: 'Lithuania',
+ LU: 'Luxembourg',
+ LV: 'Latvia',
+ LY: 'Libya',
+ MA: 'Morocco',
+ MC: 'Monaco',
+ MD: 'Moldova',
+ ME: 'Montenegro',
+ MF: 'St. Martin',
+ MG: 'Madagascar',
+ MH: 'Marshall Islands',
+ MK: 'Macedonia',
+ ML: 'Mali',
+ MM: 'Myanmar (Burma)',
+ MN: 'Mongolia',
+ MO: 'Macau',
+ MP: 'Northern Mariana Islands',
+ MQ: 'Martinique',
+ MR: 'Mauritania',
+ MS: 'Montserrat',
+ MT: 'Malta',
+ MU: 'Mauritius',
+ MV: 'Maldives',
+ MW: 'Malawi',
+ MX: 'Mexico',
+ MY: 'Malaysia',
+ MZ: 'Mozambique',
+ NA: 'Namibia',
+ NC: 'New Caledonia',
+ NE: 'Niger',
+ NF: 'Norfolk Island',
+ NG: 'Nigeria',
+ NI: 'Nicaragua',
+ NL: 'Netherlands',
+ NO: 'Norway',
+ NP: 'Nepal',
+ NR: 'Nauru',
+ NU: 'Niue',
+ NZ: 'New Zealand',
+ OM: 'Oman',
+ PA: 'Panama',
+ PE: 'Peru',
+ PF: 'French Polynesia',
+ PG: 'Papua New Guinea',
+ PH: 'Philippines',
+ PK: 'Pakistan',
+ PL: 'Poland',
+ PM: 'St. Pierre & Miquelon',
+ PN: 'Pitcairn Islands',
+ PR: 'Puerto Rico',
+ PS: 'Palestinian Territories',
+ PT: 'Portugal',
+ PW: 'Palau',
+ PY: 'Paraguay',
+ QA: 'Qatar',
+ RE: 'Réunion',
+ RO: 'Romania',
+ RS: 'Serbia',
+ RU: 'Russia',
+ RW: 'Rwanda',
+ SA: 'Saudi Arabia',
+ SB: 'Solomon Islands',
+ SC: 'Seychelles',
+ SD: 'Sudan',
+ SE: 'Sweden',
+ SG: 'Singapore',
+ SH: 'St. Helena',
+ SI: 'Slovenia',
+ SJ: 'Svalbard & Jan Mayen',
+ SK: 'Slovakia',
+ SL: 'Sierra Leone',
+ SM: 'San Marino',
+ SN: 'Senegal',
+ SO: 'Somalia',
+ SR: 'Suriname',
+ SS: 'South Sudan',
+ ST: 'São Tomé & Príncipe',
+ SV: 'El Salvador',
+ SX: 'Sint Maarten',
+ SY: 'Syria',
+ SZ: 'Swaziland',
+ TA: 'Tristan da Cunha',
+ TC: 'Turks & Caicos Islands',
+ TD: 'Chad',
+ TF: 'French Southern Territories',
+ TG: 'Togo',
+ TH: 'Thailand',
+ TJ: 'Tajikistan',
+ TK: 'Tokelau',
+ TL: 'Timor-Leste',
+ TM: 'Turkmenistan',
+ TN: 'Tunisia',
+ TO: 'Tonga',
+ TR: 'Turkey',
+ TT: 'Trinidad & Tobago',
+ TV: 'Tuvalu',
+ TW: 'Taiwan',
+ TZ: 'Tanzania',
+ UA: 'Ukraine',
+ UG: 'Uganda',
+ UM: 'U.S. Outlying Islands',
+ UN: 'United Nations',
+ US: 'United States',
+ UY: 'Uruguay',
+ UZ: 'Uzbekistan',
+ VA: 'Vatican City',
+ VC: 'St. Vincent & Grenadines',
+ VE: 'Venezuela',
+ VG: 'British Virgin Islands',
+ VI: 'U.S. Virgin Islands',
+ VN: 'Vietnam',
+ VU: 'Vanuatu',
+ WF: 'Wallis & Futuna',
+ WS: 'Samoa',
+ XK: 'Kosovo',
+ YE: 'Yemen',
+ YT: 'Mayotte',
+ ZA: 'South Africa',
+ ZM: 'Zambia',
+ ZW: 'Zimbabwe',
+ },
// Values for checking if polyfill is required on a platform
POLYFILL_TEST: {
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index 9cb0d62731d1..42a4a2d651be 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -161,9 +161,6 @@ export default {
// Set when we are loading payment methods
IS_LOADING_PAYMENT_METHODS: 'isLoadingPaymentMethods',
- // The number of minutes a user has to wait for a call.
- INBOX_CALL_USER_WAIT_TIME: 'inboxCallUserWaitTime',
-
// Is report data loading?
IS_LOADING_REPORT_DATA: 'isLoadingReportData',
@@ -179,7 +176,6 @@ export default {
// List of Form ids
FORMS: {
ADD_DEBIT_CARD_FORM: 'addDebitCardForm',
- REQUEST_CALL_FORM: 'requestCallForm',
REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount',
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
CLOSE_ACCOUNT_FORM: 'closeAccount',
diff --git a/src/ROUTES.js b/src/ROUTES.js
index 74f3ba233bdf..0b70b92c1d6f 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -129,8 +129,6 @@ export default {
getWorkspaceInvoicesRoute: policyID => `workspace/${policyID}/invoices`,
getWorkspaceTravelRoute: policyID => `workspace/${policyID}/travel`,
getWorkspaceMembersRoute: policyID => `workspace/${policyID}/members`,
- getRequestCallRoute: taskID => `request-call/${taskID}`,
- REQUEST_CALL: 'request-call/:taskID',
/**
* @param {String} route
diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js
index 3ba622591546..dd88073e228e 100644
--- a/src/components/AddressSearch.js
+++ b/src/components/AddressSearch.js
@@ -117,7 +117,15 @@ const AddressSearch = (props) => {
sublocality: 'long_name',
postal_code: 'long_name',
administrative_area_level_1: 'short_name',
- country: 'long_name',
+ country: 'short_name',
+ });
+
+ // The state's iso code (short_name) is needed for the StatePicker component but we also
+ // need the state's full name (long_name) when we render the state in a TextInput.
+ const {
+ administrative_area_level_1: longStateName,
+ } = GooglePlacesUtils.getAddressComponents(addressComponents, {
+ administrative_area_level_1: 'long_name',
});
const values = {
@@ -128,6 +136,12 @@ const AddressSearch = (props) => {
country: '',
};
+ // If the address is not in the US, use the full length state name since we're displaying the address's
+ // state / province in a TextInput instead of in a picker.
+ if (country !== CONST.COUNTRY.US) {
+ values.state = longStateName;
+ }
+
const street = `${streetNumber} ${streetName}`.trim();
if (street && street.length >= values.street.length) {
// We are only passing the street number and name if the combined length is longer than the value
@@ -137,7 +151,8 @@ const AddressSearch = (props) => {
values.street = street;
}
- if (_.includes(CONST.ALL_COUNTRIES, country)) {
+ const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country);
+ if (isValidCountryCode) {
values.country = country;
}
diff --git a/src/components/AttachmentCarousel/CarouselActions/index.js b/src/components/AttachmentCarousel/CarouselActions/index.js
new file mode 100644
index 000000000000..26af8917a04a
--- /dev/null
+++ b/src/components/AttachmentCarousel/CarouselActions/index.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {Pressable} from 'react-native';
+
+const propTypes = {
+ /** Handles onPress events with a callback */
+ onPress: PropTypes.func.isRequired,
+
+ /** Callback to cycle through attachments */
+ onCycleThroughAttachments: PropTypes.func.isRequired,
+
+ /** Styles to be assigned to Carousel */
+ styles: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
+
+ /** Children to render */
+ children: PropTypes.oneOfType([
+ PropTypes.func,
+ PropTypes.node,
+ ]).isRequired,
+};
+
+class Carousel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleKeyPress = this.handleKeyPress.bind(this);
+ }
+
+ componentDidMount() {
+ document.addEventListener('keydown', this.handleKeyPress);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.handleKeyPress);
+ }
+
+ /**
+ * Listens for keyboard shortcuts and applies the action
+ *
+ * @param {Object} e
+ */
+ handleKeyPress(e) {
+ // prevents focus from highlighting around the modal
+ e.target.blur();
+ if (e.key === 'ArrowLeft') {
+ this.props.onCycleThroughAttachments(-1);
+ }
+ if (e.key === 'ArrowRight') {
+ this.props.onCycleThroughAttachments(1);
+ }
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+Carousel.propTypes = propTypes;
+
+export default Carousel;
diff --git a/src/components/AttachmentCarousel/CarouselActions/index.native.js b/src/components/AttachmentCarousel/CarouselActions/index.native.js
new file mode 100644
index 000000000000..ebc7b7768077
--- /dev/null
+++ b/src/components/AttachmentCarousel/CarouselActions/index.native.js
@@ -0,0 +1,79 @@
+import React, {Component} from 'react';
+import {PanResponder, Dimensions, Animated} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../../../styles/styles';
+
+const propTypes = {
+ /** Attachment that's rendered */
+ children: PropTypes.element.isRequired,
+
+ /** Callback to fire when swiping left or right */
+ onCycleThroughAttachments: PropTypes.func.isRequired,
+
+ /** Callback to handle a press event */
+ onPress: PropTypes.func.isRequired,
+
+ /** Boolean to prevent a left swipe action */
+ canSwipeLeft: PropTypes.bool.isRequired,
+
+ /** Boolean to prevent a right swipe action */
+ canSwipeRight: PropTypes.bool.isRequired,
+};
+
+class Carousel extends Component {
+ constructor(props) {
+ super(props);
+ this.pan = new Animated.Value(0);
+
+ this.panResponder = PanResponder.create({
+ onStartShouldSetPanResponder: () => true,
+
+ onPanResponderMove: (event, gestureState) => Animated.event([null, {
+ dx: this.pan,
+ }], {useNativeDriver: false})(event, gestureState),
+
+ onPanResponderRelease: (event, gestureState) => {
+ if (gestureState.dx === 0 && gestureState.dy === 0) {
+ return this.props.onPress();
+ }
+
+ const deltaSlide = gestureState.dx > 0 ? 1 : -1;
+ if (Math.abs(gestureState.vx) < 1 || (deltaSlide === 1 && !this.props.canSwipeLeft) || (deltaSlide === -1 && !this.props.canSwipeRight)) {
+ return Animated.spring(this.pan, {useNativeDriver: false, toValue: 0}).start();
+ }
+
+ const width = Dimensions.get('window').width;
+ const slideLength = deltaSlide * (width * 1.1);
+ Animated.timing(this.pan, {useNativeDriver: false, duration: 100, toValue: slideLength}).start(({finished}) => {
+ if (!finished) {
+ return;
+ }
+
+ this.props.onCycleThroughAttachments(-deltaSlide);
+ this.pan.setValue(-slideLength);
+ Animated.timing(this.pan, {useNativeDriver: false, duration: 100, toValue: 0}).start();
+ });
+ },
+ });
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+Carousel.propTypes = propTypes;
+
+export default Carousel;
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
new file mode 100644
index 000000000000..144d0aaa0874
--- /dev/null
+++ b/src/components/AttachmentCarousel/index.js
@@ -0,0 +1,215 @@
+import React from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
+import * as Expensicons from '../Icon/Expensicons';
+import styles from '../../styles/styles';
+import themeColors from '../../styles/themes/default';
+import CarouselActions from './CarouselActions';
+import Button from '../Button';
+import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
+import AttachmentView from '../AttachmentView';
+import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL';
+import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
+import CONST from '../../CONST';
+import ONYXKEYS from '../../ONYXKEYS';
+import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
+import tryResolveUrlFromApiRoot from '../../libs/tryResolveUrlFromApiRoot';
+
+const propTypes = {
+ /** source is used to determine the starting index in the array of attachments */
+ source: PropTypes.string,
+
+ /** Callback to update the parent modal's state with a source and name from the attachments array */
+ onNavigate: PropTypes.func,
+
+ /** Object of report actions for this report */
+ reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+};
+
+const defaultProps = {
+ source: '',
+ reportActions: {},
+ onNavigate: () => {},
+};
+
+class AttachmentCarousel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
+ this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
+
+ this.state = {
+ source: this.props.source,
+ shouldShowArrow: this.canUseTouchScreen,
+ isForwardDisabled: true,
+ isBackDisabled: true,
+ };
+ }
+
+ componentDidMount() {
+ this.makeStateWithReports();
+ }
+
+ componentDidUpdate(prevProps) {
+ const previousReportActionsCount = _.size(prevProps.reportActions);
+ const currentReportActionsCount = _.size(this.props.reportActions);
+ if (previousReportActionsCount === currentReportActionsCount) {
+ return;
+ }
+ this.makeStateWithReports();
+ }
+
+ /**
+ * Helps to navigate between next/previous attachments
+ * @param {Object} attachmentItem
+ * @returns {Object}
+ */
+ getAttachment(attachmentItem) {
+ const source = _.get(attachmentItem, 'source', '');
+ const file = _.get(attachmentItem, 'file', {name: ''});
+ this.props.onNavigate({source: addEncryptedAuthTokenToURL(source), file});
+
+ return {
+ source,
+ file,
+ };
+ }
+
+ /**
+ * Toggles the visibility of the arrows
+ * @param {Boolean} shouldShowArrow
+ */
+ toggleArrowsVisibility(shouldShowArrow) {
+ this.setState({shouldShowArrow});
+ }
+
+ /**
+ * This is called when there are new reports to set the state
+ */
+ makeStateWithReports() {
+ let page;
+ const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions), true);
+
+ /**
+ * Looping to filter out attachments and retrieve the src URL and name of attachments.
+ */
+ const attachments = [];
+ _.forEach(actions, ({originalMessage, message}) => {
+ // Check for attachment which hasn't been deleted
+ if (!originalMessage || !originalMessage.html || _.some(message, m => m.isEdited)) {
+ return;
+ }
+ const matches = [...originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA)];
+
+ // matchAll captured both source url and name of the attachment
+ if (matches.length === 2) {
+ const [originalSource, name] = _.map(matches, m => m[2]);
+
+ // Update the image URL so the images can be accessed depending on the config environment.
+ // Eg: while using Ngrok the image path is from an Ngrok URL and not an Expensify URL.
+ const source = tryResolveUrlFromApiRoot(originalSource);
+ if (source === this.state.source) {
+ page = attachments.length;
+ }
+
+ attachments.push({source, file: {name}});
+ }
+ });
+
+ const {file} = this.getAttachment(attachments[page]);
+ this.setState({
+ page,
+ attachments,
+ file,
+ isForwardDisabled: page === 0,
+ isBackDisabled: page === attachments.length - 1,
+ });
+ }
+
+ /**
+ * Increments or decrements the index to get another selected item
+ * @param {Number} deltaSlide
+ */
+ cycleThroughAttachments(deltaSlide) {
+ if ((deltaSlide > 0 && this.state.isForwardDisabled) || (deltaSlide < 0 && this.state.isBackDisabled)) {
+ return;
+ }
+
+ this.setState(({attachments, page}) => {
+ const nextIndex = page - deltaSlide;
+ const {source, file} = this.getAttachment(attachments[nextIndex]);
+ return {
+ page: nextIndex,
+ source,
+ file,
+ isBackDisabled: nextIndex === attachments.length - 1,
+ isForwardDisabled: nextIndex === 0,
+ };
+ });
+ }
+
+ render() {
+ const isPageSet = Number.isInteger(this.state.page);
+ const authSource = addEncryptedAuthTokenToURL(this.state.source);
+ return (
+ this.toggleArrowsVisibility(true)}
+ onMouseLeave={() => this.toggleArrowsVisibility(false)}
+ >
+ {(isPageSet && this.state.shouldShowArrow) && (
+ <>
+ {!this.state.isBackDisabled && (
+
+ );
+ }
+}
+
+AttachmentCarousel.propTypes = propTypes;
+AttachmentCarousel.defaultProps = defaultProps;
+
+export default withOnyx({
+ reportActions: {
+ key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ canEvict: false,
+ },
+})(AttachmentCarousel);
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index baa459e01640..034821a8af56 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -6,13 +6,14 @@ import lodashGet from 'lodash/get';
import lodashExtend from 'lodash/extend';
import _ from 'underscore';
import CONST from '../CONST';
+import Navigation from '../libs/Navigation/Navigation';
import Modal from './Modal';
import AttachmentView from './AttachmentView';
+import AttachmentCarousel from './AttachmentCarousel';
import styles from '../styles/styles';
import * as StyleUtils from '../styles/StyleUtils';
import * as FileUtils from '../libs/fileDownload/FileUtils';
import themeColors from '../styles/themes/default';
-import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
import compose from '../libs/compose';
import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
import Button from './Button';
@@ -61,7 +62,7 @@ const propTypes = {
const defaultProps = {
source: '',
onConfirm: null,
- originalFileName: null,
+ originalFileName: '',
isAuthTokenRequired: false,
allowDownload: false,
headerTitle: null,
@@ -74,11 +75,11 @@ class AttachmentModal extends PureComponent {
this.state = {
isModalOpen: false,
+ reportID: null,
shouldLoadAttachment: false,
isAttachmentInvalid: false,
attachmentInvalidReasonTitle: null,
attachmentInvalidReason: null,
- file: null,
source: props.source,
modalType: CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE,
isConfirmButtonDisabled: false,
@@ -87,10 +88,20 @@ class AttachmentModal extends PureComponent {
this.submitAndClose = this.submitAndClose.bind(this);
this.closeConfirmModal = this.closeConfirmModal.bind(this);
+ this.onNavigate = this.onNavigate.bind(this);
this.validateAndDisplayFileToUpload = this.validateAndDisplayFileToUpload.bind(this);
this.updateConfirmButtonVisibility = this.updateConfirmButtonVisibility.bind(this);
}
+ /**
+ * Helps to navigate between next/previous attachments
+ * by setting sourceURL and file in state
+ * @param {Object} attachmentData
+ */
+ onNavigate(attachmentData) {
+ this.setState(attachmentData);
+ }
+
/**
* If our attachment is a PDF, return the unswipeable Modal type.
* @param {String} sourceURL
@@ -116,7 +127,8 @@ class AttachmentModal extends PureComponent {
* @param {String} sourceURL
*/
downloadAttachment(sourceURL) {
- fileDownload(this.props.isAuthTokenRequired ? addEncryptedAuthTokenToURL(sourceURL) : sourceURL, this.props.originalFileName);
+ const originalFileName = lodashGet(this.state, 'file.name') || this.props.originalFileName;
+ fileDownload(sourceURL, originalFileName);
// At ios, if the keyboard is open while opening the attachment, then after downloading
// the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard.
@@ -230,9 +242,7 @@ class AttachmentModal extends PureComponent {
}
render() {
- // If source is a URL, add auth token to get access
const source = this.state.source;
-
return (
<>
this.downloadAttachment(source)}
onCloseButtonPress={() => this.setState({isModalOpen: false})}
/>
-
- {this.state.source && this.state.shouldLoadAttachment && (
+ {this.state.reportID ? (
+
+ ) : this.state.source && this.state.shouldLoadAttachment && (
)}
-
{/* If we have an onConfirm method show a confirmation button */}
{this.props.onConfirm && (
@@ -302,7 +317,12 @@ class AttachmentModal extends PureComponent {
{this.props.children({
displayFileInModal: this.validateAndDisplayFileToUpload,
show: () => {
- this.setState({isModalOpen: true});
+ const route = Navigation.getActiveRoute();
+ let reportID = null;
+ if (route.includes('/r/')) {
+ reportID = route.replace('/r/', '');
+ }
+ this.setState({isModalOpen: true, reportID});
},
})}
>
diff --git a/src/components/AttachmentView.js b/src/components/AttachmentView.js
index 3381a35f0d39..5e5221482350 100755
--- a/src/components/AttachmentView.js
+++ b/src/components/AttachmentView.js
@@ -34,6 +34,9 @@ const propTypes = {
/** Flag to show the loading indicator */
shouldShowLoadingSpinnerIcon: PropTypes.bool,
+ /** Function for handle on press */
+ onPress: PropTypes.func,
+
/** Notify parent that the UI should be modified to accommodate keyboard */
onToggleKeyboard: PropTypes.func,
@@ -47,6 +50,7 @@ const defaultProps = {
},
shouldShowDownloadIcon: false,
shouldShowLoadingSpinnerIcon: false,
+ onPress: () => {},
onToggleKeyboard: () => {},
};
@@ -67,6 +71,7 @@ const AttachmentView = (props) => {
: props.source;
return (
{
// both PDFs and images will appear as images when pasted into the the text field
if (Str.isImage(props.source) || (props.file && Str.isImage(props.file.name))) {
return (
-
+
);
}
diff --git a/src/components/BaseMiniContextMenuItem.js b/src/components/BaseMiniContextMenuItem.js
new file mode 100644
index 000000000000..c91df470ba34
--- /dev/null
+++ b/src/components/BaseMiniContextMenuItem.js
@@ -0,0 +1,77 @@
+import {Pressable, View} from 'react-native';
+import React from 'react';
+import PropTypes from 'prop-types';
+import _ from 'underscore';
+import styles from '../styles/styles';
+import * as StyleUtils from '../styles/StyleUtils';
+import getButtonState from '../libs/getButtonState';
+import variables from '../styles/variables';
+import Tooltip from './Tooltip';
+
+const propTypes = {
+ /**
+ * Text to display when hovering the menu item
+ */
+ tooltipText: PropTypes.string.isRequired,
+
+ /**
+ * Callback to fire on press
+ */
+ onPress: PropTypes.func.isRequired,
+
+ /**
+ * The children to display within the menu item
+ */
+ children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired,
+
+ /**
+ * Whether the button should be in the active state
+ */
+ isDelayButtonStateComplete: PropTypes.bool,
+
+ /**
+ * A ref to forward to the Pressable
+ */
+ innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+};
+
+const defaultProps = {
+ isDelayButtonStateComplete: true,
+ innerRef: () => {},
+};
+
+/**
+ * Component that renders a mini context menu item with a
+ * pressable. Also renders a tooltip when hovering the item.
+ * @param {Object} props
+ * @returns {JSX.Element}
+ */
+const BaseMiniContextMenuItem = props => (
+
+ [
+ styles.reportActionContextMenuMiniButton,
+ StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, props.isDelayButtonStateComplete)),
+ ]
+ }
+ >
+ {pressableState => (
+
+ {_.isFunction(props.children) ? props.children(pressableState) : props.children}
+
+ )}
+
+
+);
+
+BaseMiniContextMenuItem.propTypes = propTypes;
+BaseMiniContextMenuItem.defaultProps = defaultProps;
+BaseMiniContextMenuItem.displayName = 'BaseMiniContextMenuItem';
+
+// eslint-disable-next-line react/jsx-props-no-spreading
+export default React.forwardRef((props, ref) => );
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index c87fc5e9b4c3..9dace75bec03 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -1,14 +1,12 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
-import {Pressable, View} from 'react-native';
import MenuItem from './MenuItem';
-import Tooltip from './Tooltip';
import Icon from './Icon';
import styles from '../styles/styles';
import * as StyleUtils from '../styles/StyleUtils';
import getButtonState from '../libs/getButtonState';
import withDelayToggleButtonState, {withDelayToggleButtonStatePropTypes} from './withDelayToggleButtonState';
-import variables from '../styles/variables';
+import BaseMiniContextMenuItem from './BaseMiniContextMenuItem';
const propTypes = {
/** Icon Component */
@@ -75,29 +73,19 @@ class ContextMenuItem extends Component {
return (
this.props.isMini
? (
-
- [
- styles.reportActionContextMenuMiniButton,
- StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, this.props.isDelayButtonStateComplete)),
- ]
- }
- >
- {({hovered, pressed}) => (
-
-
-
- )}
-
-
+
+ {({hovered, pressed}) => (
+
+ )}
+
) : (