diff --git a/.github/actions/isStagingDeployLocked/index.js b/.github/actions/isStagingDeployLocked/index.js index 113f35ac3dc1..39a47d553a54 100644 --- a/.github/actions/isStagingDeployLocked/index.js +++ b/.github/actions/isStagingDeployLocked/index.js @@ -18,7 +18,10 @@ const run = function () { const githubUtils = new GithubUtils(octokit); return githubUtils.getStagingDeployCash() - .then(({labels}) => core.setOutput('IS_LOCKED', _.contains(_.pluck(labels, 'name'), '🔐 LockCashDeploys 🔐'))) + .then(({labels}) => { + console.log(`Found StagingDeployCash with labels: ${_.pluck(labels, 'name')}`); + core.setOutput('IS_LOCKED', _.contains(_.pluck(labels, 'name'), '🔐 LockCashDeploys 🔐')); + }) .catch((err) => { console.warn('No open StagingDeployCash found, continuing...', err); core.setOutput('IS_LOCKED', false); diff --git a/.github/actions/isStagingDeployLocked/isStagingDeployLocked.js b/.github/actions/isStagingDeployLocked/isStagingDeployLocked.js index 372fa10c8e5d..fd3464f8d6da 100644 --- a/.github/actions/isStagingDeployLocked/isStagingDeployLocked.js +++ b/.github/actions/isStagingDeployLocked/isStagingDeployLocked.js @@ -8,7 +8,10 @@ const run = function () { const githubUtils = new GithubUtils(octokit); return githubUtils.getStagingDeployCash() - .then(({labels}) => core.setOutput('IS_LOCKED', _.contains(_.pluck(labels, 'name'), '🔐 LockCashDeploys 🔐'))) + .then(({labels}) => { + console.log(`Found StagingDeployCash with labels: ${_.pluck(labels, 'name')}`); + core.setOutput('IS_LOCKED', _.contains(_.pluck(labels, 'name'), '🔐 LockCashDeploys 🔐')); + }) .catch((err) => { console.warn('No open StagingDeployCash found, continuing...', err); core.setOutput('IS_LOCKED', false); diff --git a/.github/actions/markPullRequestsAsDeployed/index.js b/.github/actions/markPullRequestsAsDeployed/index.js index fffae169fbbc..16f50a2e15ec 100644 --- a/.github/actions/markPullRequestsAsDeployed/index.js +++ b/.github/actions/markPullRequestsAsDeployed/index.js @@ -31,10 +31,10 @@ const githubUtils = new GithubUtils(octokit); prList.forEach((pr) => { githubUtils.createComment(github.context.repo.repo, pr, message, octokit) .then(() => { - console.log(`Comment created on #${pr} successfully`); + console.log(`Comment created on #${pr} successfully 🎉`); }) .catch((err) => { - console.log(`Unable to write comment on #${pr}`); + console.log(`Unable to write comment on #${pr} 😞`); core.setFailed(err.message); }); }); diff --git a/.github/actions/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js b/.github/actions/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js index 11d5e98d88c1..46ed3c18df80 100644 --- a/.github/actions/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js +++ b/.github/actions/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js @@ -21,10 +21,10 @@ const githubUtils = new GithubUtils(octokit); prList.forEach((pr) => { githubUtils.createComment(github.context.repo.repo, pr, message, octokit) .then(() => { - console.log(`Comment created on #${pr} successfully`); + console.log(`Comment created on #${pr} successfully 🎉`); }) .catch((err) => { - console.log(`Unable to write comment on #${pr}`); + console.log(`Unable to write comment on #${pr} 😞`); core.setFailed(err.message); }); }); diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 7eac9e34cc87..bf12fb745cad 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -8,9 +8,9 @@ on: status: {} jobs: - automerge: + master: runs-on: ubuntu-latest - + if: github.event.pull_request.base.ref == 'master' && github.actor == 'OSBotify' && github.event.label.name == 'automerge' steps: - name: Export Files Changed id: changed @@ -24,14 +24,14 @@ jobs: uses: hmarr/auto-approve-action@7782c7e2bdf62b4d79bdcded8332808fd2f179cd with: github-token: ${{ secrets.GITHUB_TOKEN }} - if: github.event.pull_request.mergeable && github.event.label.name == 'automerge' && github.actor == 'OSBotify' && steps.changed.outputs.files_updated == 'android/app/build.gradle ios/ExpensifyCash/Info.plist ios/ExpensifyCashTests/Info.plist package-lock.json package.json' && steps.changed.outputs.files_created == '' && steps.changed.outputs.files_deleted == '' + if: github.event.pull_request.mergeable && steps.changed.outputs.files_updated == 'android/app/build.gradle ios/ExpensifyCash/Info.plist ios/ExpensifyCashTests/Info.plist package-lock.json package.json' && steps.changed.outputs.files_created == '' && steps.changed.outputs.files_deleted == '' - name: Check for an auto merge # Version: 0.12.0 uses: pascalgn/automerge-action@c9bd1823770819dc8fb8a5db2d11a3a95fbe9b07 if: github.event.pull_request.mergeable && github.event.pull_request.mergeable_state == 'clean' env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} # This Slack step is duplicated in all workflows, if you make a change to this step, make sure to update all # the other workflows with the same change @@ -53,3 +53,85 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + staging: + runs-on: ubuntu-latest + if: github.event.pull_request.base.ref == 'staging' && github.actor == 'OSBotify' && github.event.label.name == 'automerge' + steps: + - name: Check for an auto approve + # Version: 2.0.0 + uses: hmarr/auto-approve-action@7782c7e2bdf62b4d79bdcded8332808fd2f179cd + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + if: github.event.pull_request.mergeable && github.event.pull_request.head.ref == 'master' + + - name: Check PR mergable states + run: echo "Mergeable - ${{ github.event.pull_request.mergeable }} Clean - ${{ github.event.pull_request.mergeable_state }}" + + # TODO: make sure PR is in "clean" mergeable_state + - name: Check for an auto merge + # Version: 0.12.0 + uses: pascalgn/automerge-action@c9bd1823770819dc8fb8a5db2d11a3a95fbe9b07 + if: github.event.pull_request.mergeable + env: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + + # This Slack step is duplicated in all workflows, if you make a change to this step, make sure to update all + # the other workflows with the same change + - uses: 8398a7/action-slack@v3 + name: Job failed Slack notification + if: ${{ failure() }} + with: + status: custom + fields: workflow, repo + custom_payload: | + { + channel: '#announce', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 ${process.env.AS_REPO} failed on ${process.env.AS_WORKFLOW} workflow 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + production: + runs-on: ubuntu-latest + if: github.event.pull_request.base.ref == 'production' && github.actor == 'OSBotify' && github.event.label.name == 'automerge' + steps: + - name: Check for an auto approve + # Version: 2.0.0 + uses: hmarr/auto-approve-action@7782c7e2bdf62b4d79bdcded8332808fd2f179cd + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + if: github.event.pull_request.mergeable && github.event.pull_request.head.ref == 'staging' + + - name: Check for an auto merge + # Version: 0.12.0 + uses: pascalgn/automerge-action@c9bd1823770819dc8fb8a5db2d11a3a95fbe9b07 + if: github.event.pull_request.mergeable && github.event.pull_request.mergeable_state == 'clean' + env: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + + # This Slack step is duplicated in all workflows, if you make a change to this step, make sure to update all + # the other workflows with the same change + - uses: 8398a7/action-slack@v3 + name: Job failed Slack notification + if: ${{ failure() }} + with: + status: custom + fields: workflow, repo + custom_payload: | + { + channel: '#announce', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 ${process.env.AS_REPO} failed on ${process.env.AS_WORKFLOW} workflow 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 1ab328213886..ee98cee44ae8 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -10,8 +10,12 @@ jobs: CLA: runs-on: ubuntu-latest # This job only runs for pull request comments or pull request target events (not issue comments) - if: github.event.issue.pull_request || github.event_name == 'pull_request_target' + # It does not run for pull requests created by OSBotify + # TODO: Fix this if so that it doesn't run CLA for pull requests created by OSBotify + if: ${{ github.event.issue.pull_request || (github.event_name == 'pull_request_target' && github.event.pull_request.user.login != 'OSBotify') }} steps: + # TODO: remove this first run step + - run: echo ${{ github.event.pull_request.user.login }} - uses: actions-ecosystem/action-regex-match@9c35fe9ac1840239939c59e5db8839422eed8a73 id: sign with: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000000..4bf63e49e95a --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,57 @@ +name: Deploy code to staging or production + +on: + push: + branches: [staging, production] + +jobs: + validate: + runs-on: ubuntu-latest + outputs: + isAutomergePR: ${{ steps.isAutomergePR.outputs.IS_AUTOMERGE_PR }} + + steps: + - name: Get merged pull request + id: getMergedPullRequest + uses: actions-ecosystem/action-get-merged-pull-request@59afe90821bb0b555082ce8ff1e36b03f91553d9 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if merged pull request was an automatic version bump PR + id: isAutomergePR + run: echo "::set-output name=IS_AUTOMERGE_PR::${{ contains(steps.getMergedPullRequest.outputs.labels, 'automerge') && github.actor == 'OSBotify' }}" + + deployStaging: + runs-on: ubuntu-latest + needs: validate + if: ${{ needs.validate.outputs.isAutomergePR == 'true' && github.ref == 'refs/heads/staging' }} + + steps: + - name: Checkout staging + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + with: + ref: staging + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Tag version + run: git tag $(npm run print-version --silent) + + - name: 🚀 Push tags to trigger staging deploy 🚀 + run: git push --tags + + deployProduction: + runs-on: ubuntu-latest + needs: validate + if: ${{ needs.validate.outputs.isAutomergePR == 'true' && github.ref == 'refs/heads/production' }} + + steps: + - name: Checkout production + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + with: + ref: production + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: 🚀 Create release to trigger production deploy 🚀 + run: echo "Create release with version $(npm run print-version --silent)" + + diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 1c0afae35981..523b17acac0f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -3,6 +3,7 @@ name: E2E iOS Tests on: pull_request: types: [opened, synchronize] + branches-ignore: [staging, production] env: DEVELOPER_DIR: /Applications/Xcode_12.3.app/Contents/Developer diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bf0016694f7a..9fee19b8a69c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,6 +3,7 @@ name: Lint JavaScript on: pull_request: types: [opened, synchronize] + branches-ignore: [staging, production] jobs: lint: diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index f2cf30a95819..a341cf5a04d0 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -10,6 +10,7 @@ jobs: outputs: mergedPullRequest: ${{ steps.getMergedPullRequest.outputs.number }} isStagingDeployLocked: ${{ steps.isStagingDeployLocked.outputs.IS_LOCKED }} + isVersionBumpPR: ${{ steps.isVersionBumpPR.outputs.IS_VERSION_BUMP_PR }} steps: # Version: 2.3.4 @@ -30,10 +31,14 @@ jobs: with: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + - name: Check if merged pull request was an automatic version bump PR + id: isVersionBumpPR + run: echo "::set-output name=IS_VERSION_BUMP_PR::${{ contains(steps.getMergedPullRequest.outputs.labels, 'automerge') && github.actor == 'OSBotify' }}" + skipDeploy: runs-on: ubuntu-latest needs: chooseDeployActions - if: ${{ needs.chooseDeployActions.outputs.isStagingDeployLocked == 'true' }} + if: ${{ needs.chooseDeployActions.outputs.isStagingDeployLocked == 'true' && needs.chooseDeployActions.outputs.isVersionBumpPR == 'false' }} steps: - name: Comment on deferred PR @@ -47,7 +52,9 @@ jobs: version: runs-on: macos-latest needs: chooseDeployActions - if: ${{ needs.chooseDeployActions.outputs.isStagingDeployLocked == 'false' }} + if: ${{ needs.chooseDeployActions.outputs.isVersionBumpPR == 'false' }} + outputs: + newVersion: ${{ steps.bumpVersion.outputs.NEW_VERSION }} steps: # Version: 2.3.4 @@ -79,7 +86,7 @@ jobs: with: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - - name: Commit and tag new version + - name: Commit new version run: | git add \ ./package.json \ @@ -88,10 +95,6 @@ jobs: ./ios/ExpensifyCash/Info.plist \ ./ios/ExpensifyCashTests/Info.plist git commit -m "Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }}" - git tag ${{ steps.bumpVersion.outputs.NEW_VERSION }} - - - name: Push tags - run: git push --tags - name: Create Pull Request (master) uses: peter-evans/create-pull-request@09b9ac155b0d5ad7d8d157ed32158c1b73689109 @@ -131,3 +134,31 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + updateStaging: + runs-on: ubuntu-latest + needs: chooseDeployActions + if: ${{ needs.chooseDeployActions.outputs.isVersionBumpPR == 'true' && needs.chooseDeployActions.outputs.isStagingDeployLocked == 'false' }} + + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + with: + fetch-depth: 0 + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Checkout master branch + run: git checkout master + + - name: Set Staging Version + run: echo "STAGING_VERSION=$(npm run print-version --silent)" >> $GITHUB_ENV + + - name: Create Pull Request + # Version: 2.4.3 + uses: repo-sync/pull-request@33777245b1aace1a58c87a29c90321aa7a74bd7d + with: + source_branch: master + destination_branch: staging + pr_label: automerge + github_token: ${{ secrets.OS_BOTIFY_TOKEN }} + pr_title: Update version to ${{ env.STAGING_VERSION }} on staging + pr_body: Update version to ${{ env.STAGING_VERSION }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2f14b32ab963..7addc4e18c98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ name: Jest Unit Tests on: pull_request: types: [opened, synchronize] + branches-ignore: [staging, production] jobs: test: diff --git a/.github/workflows/verifyGithubActionBuilds.yml b/.github/workflows/verifyGithubActionBuilds.yml index 70e2cb9e7dba..21fa3213259b 100644 --- a/.github/workflows/verifyGithubActionBuilds.yml +++ b/.github/workflows/verifyGithubActionBuilds.yml @@ -3,6 +3,7 @@ name: Verify Github Action Builds on: pull_request: types: [opened, synchronize] + branches-ignore: [staging, production] jobs: verify: diff --git a/.github/workflows/verifyPodfile.yml b/.github/workflows/verifyPodfile.yml index 9727d77f3849..2cb062700dd9 100644 --- a/.github/workflows/verifyPodfile.yml +++ b/.github/workflows/verifyPodfile.yml @@ -3,6 +3,7 @@ name: Verify Podfile on: pull_request: types: [opened, synchronize] + branches-ignore: [staging, production] jobs: verify: diff --git a/android/app/build.gradle b/android/app/build.gradle index 5363ccb0e7e8..698134ee62db 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -148,8 +148,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001000253 - versionName "1.0.2-53" + versionCode 1001000268 + versionName "1.0.2-68" } splits { abi { diff --git a/desktop/main.js b/desktop/main.js index 7a25730a5da7..820c381a059f 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -71,22 +71,36 @@ for (let i = 0; i < process.argv.length; i++) { // Add the listeners and variables required to ensure that auto-updating // happens correctly. let hasUpdate = false; +let downloadedVersion; -const quitAndInstallWithUpdate = (version) => { +const quitAndInstallWithUpdate = () => { + if (!downloadedVersion) { + return; + } app.relaunch({ - args: [`${EXPECTED_UPDATE_VERSION_FLAG}=${version}`], + args: [`${EXPECTED_UPDATE_VERSION_FLAG}=${downloadedVersion}`], }); hasUpdate = true; autoUpdater.quitAndInstall(); }; +// Defines the system-level menu item for manually triggering an update after +const updateAppMenuItem = new MenuItem({ + label: 'Update Expensify.cash', + enabled: false, + click: quitAndInstallWithUpdate, +}); + +// Actual auto-update listeners const electronUpdater = browserWindow => ({ init: () => { autoUpdater.on('update-downloaded', (info) => { + downloadedVersion = info.version; + updateAppMenuItem.enabled = true; if (browserWindow.isVisible()) { browserWindow.webContents.send('update-downloaded', info.version); } else { - quitAndInstallWithUpdate(info.version); + quitAndInstallWithUpdate(); } }); @@ -134,6 +148,9 @@ const mainWindow = (() => { }], })); + const appMenu = systemMenu.items.find(item => item.role === 'appmenu'); + appMenu.submenu.insert(1, updateAppMenuItem); + // On mac, pressing cmd++ actually sends a cmd+=. cmd++ is generally the zoom in shortcut, but this is // not properly listened for by electron. Adding in an invisible cmd+= listener fixes this. const viewWindow = systemMenu.items.find(item => item.role === 'viewmenu'); diff --git a/ios/ExpensifyCash/Info.plist b/ios/ExpensifyCash/Info.plist index 90bb9088fa17..9258a05646b0 100644 --- a/ios/ExpensifyCash/Info.plist +++ b/ios/ExpensifyCash/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 1.0.2.53 + 1.0.2.68 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/ExpensifyCashTests/Info.plist b/ios/ExpensifyCashTests/Info.plist index b119ea23e0f8..6c7e5406998e 100644 --- a/ios/ExpensifyCashTests/Info.plist +++ b/ios/ExpensifyCashTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.0.2.53 + 1.0.2.68 diff --git a/package-lock.json b/package-lock.json index 4ed789ca0054..0ef7913eb30e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "expensify.cash", - "version": "1.0.2-53", + "version": "1.0.2-68", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -11074,15 +11074,14 @@ } }, "expensify-common": { - "version": "git+https://github.com/Expensify/expensify-common.git#58ed3e9e813a9bc1cf0dd4626ce90ef35110369c", - "from": "git+https://github.com/Expensify/expensify-common.git#58ed3e9e813a9bc1cf0dd4626ce90ef35110369c", + "version": "git+https://github.com/Expensify/expensify-common.git#c7becaa79e10c10da521261ebb83dd3871847e84", + "from": "git+https://github.com/Expensify/expensify-common.git#c7becaa79e10c10da521261ebb83dd3871847e84", "requires": { "classnames": "2.2.5", "clipboard": "2.0.4", "html-entities": "^1.3.1", "jquery": "3.3.1", - "lodash.get": "4.4.2", - "lodash.has": "4.5.2", + "lodash": "4.17.21", "prop-types": "15.7.2", "react": "16.12.0", "react-dom": "16.12.0", @@ -11091,6 +11090,11 @@ "underscore": "1.9.1" }, "dependencies": { + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "react": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", diff --git a/package.json b/package.json index c664663607f1..4010192b1a06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "expensify.cash", - "version": "1.0.2-53", + "version": "1.0.2-68", "author": "Expensify, Inc.", "homepage": "https://expensify.cash", "description": "Expensify.cash is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -47,7 +47,7 @@ "electron-log": "^4.2.4", "electron-serve": "^1.0.0", "electron-updater": "^4.3.4", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#58ed3e9e813a9bc1cf0dd4626ce90ef35110369c", + "expensify-common": "git+https://github.com/Expensify/expensify-common.git#c7becaa79e10c10da521261ebb83dd3871847e84", "file-loader": "^6.0.0", "html-entities": "^1.3.1", "lodash.get": "^4.4.2", diff --git a/src/Expensify.js b/src/Expensify.js index 053d9dc09af2..0fa0da2e4104 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -42,12 +42,15 @@ const propTypes = { // Session info for the currently logged in user. session: PropTypes.shape({ + // Currently logged in user authToken authToken: PropTypes.string, + + // Currently logged in user accountID accountID: PropTypes.number, }), - // Version of newly downloaded update. - version: PropTypes.string, + // Whether a new update is available and ready to install. + updateAvailable: PropTypes.bool, }; const defaultProps = { @@ -55,7 +58,7 @@ const defaultProps = { authToken: null, accountID: null, }, - version: '', + updateAvailable: false, }; class Expensify extends PureComponent { @@ -98,7 +101,7 @@ class Expensify extends PureComponent { return ( <> {/* We include the modal for showing a new update at the top level so the option is always present. */} - {this.props.version ? : null} + {this.props.updateAvailable ? : null} ); @@ -111,8 +114,8 @@ export default withOnyx({ session: { key: ONYXKEYS.SESSION, }, - version: { - key: ONYXKEYS.UPDATE_VERSION, + updateAvailable: { + key: ONYXKEYS.UPDATE_AVAILABLE, initWithStoredValues: false, }, })(Expensify); diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index b9057c941645..8edd302fb8c6 100644 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -38,8 +38,8 @@ export default { // Contains the user preference for the LHN priority mode PRIORITY_MODE: 'priorityMode', - // Contains the version of the update that has newly been downloaded. - UPDATE_VERSION: 'updateVersion', + // Indicates whether an update is available and ready to beinstalled. + UPDATE_AVAILABLE: 'updateAvailable', // Saves the current country code which is displayed when the user types a phone number without // an international code diff --git a/src/ROUTES.js b/src/ROUTES.js index 664ac7f1ac08..5f4c4337c95e 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -2,22 +2,23 @@ * This is a file containing constants for all of the routes we want to be able to go to */ export default { - HOME: '/', - SETTINGS: '/settings', - SETTINGS_PROFILE: '/settings/profile', - SETTINGS_PREFERENCES: '/settings/preferences', - SETTINGS_PASSWORD: '/settings/password', - SETTINGS_PAYMENTS: '/settings/payments', - NEW_GROUP: '/new/group', - NEW_CHAT: '/new/chat', - REPORT: '/r', - IOU_REQUEST: '/iou/request', - IOU_BILL: '/iou/split', - getReportRoute: reportID => `/r/${reportID}`, - SEARCH: '/search', - SET_PASSWORD: '/setpassword/:validateCode', - SIGNIN: '/signin', - NOT_FOUND: '/404', - PROFILE: '/profile/:login', - getProfileRoute: login => `/profile/${login}`, + HOME: '', + SETTINGS: 'settings', + SETTINGS_PROFILE: 'settings/profile', + SETTINGS_PREFERENCES: 'settings/preferences', + SETTINGS_PASSWORD: 'settings/password', + SETTINGS_PAYMENTS: 'settings/payments', + NEW_GROUP: 'new/group', + NEW_CHAT: 'new/chat', + REPORT: 'r', + REPORT_WITH_ID: 'r/:reportID', + getReportRoute: reportID => `r/${reportID}`, + IOU_REQUEST: 'iou/request', + IOU_BILL: 'iou/split', + SEARCH: 'search', + SIGNIN: 'signin', + SET_PASSWORD_WITH_VALIDATE_CODE: 'setpassword/:validateCode', + PROFILE: 'profile', + PROFILE_WITH_LOGIN: 'profile/:login', + getProfileRoute: login => `profile/${login}`, }; diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index b78b9fe21308..14b2de88e10b 100644 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -105,6 +105,7 @@ class AttachmentModal extends PureComponent { > fileDownload(sourceURL)} onCloseButtonPress={() => this.setState({isModalOpen: false})} /> diff --git a/src/components/FAB.js b/src/components/FAB.js index c526b66e40af..b79f070ae41e 100644 --- a/src/components/FAB.js +++ b/src/components/FAB.js @@ -55,12 +55,12 @@ class FAB extends PureComponent { const backgroundColor = this.animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [themeColors.buttonSuccessBG, themeColors.sidebar], + outputRange: [themeColors.buttonSuccessBG, themeColors.buttonDefaultBG], }); const fill = this.animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [themeColors.componentBG, themeColors.icon], + outputRange: [themeColors.componentBG, themeColors.heading], }); return ( diff --git a/src/components/HeaderWithCloseButton.js b/src/components/HeaderWithCloseButton.js index f90cfc57d34c..6a281565a1b0 100644 --- a/src/components/HeaderWithCloseButton.js +++ b/src/components/HeaderWithCloseButton.js @@ -37,8 +37,8 @@ const defaultProps = { onCloseButtonPress: () => {}, onBackButtonPress: () => {}, shouldShowBackButton: false, - textSize: 'default', - shouldShowBorderBottom: true, + textSize: 'large', + shouldShowBorderBottom: false, }; const HeaderWithCloseButton = props => ( diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js index 35a0a52cd4a8..51884d6fdd6b 100644 --- a/src/components/Modal/BaseModal.js +++ b/src/components/Modal/BaseModal.js @@ -14,12 +14,22 @@ class BaseModal extends PureComponent { constructor(props) { super(props); + this.hideModalAndRemoveEventListeners = this.hideModalAndRemoveEventListeners.bind(this); this.subscribeToKeyEvents = this.subscribeToKeyEvents.bind(this); this.unsubscribeFromKeyEvents = this.unsubscribeFromKeyEvents.bind(this); } componentWillUnmount() { + this.hideModalAndRemoveEventListeners(); + } + + /** + * Hides modal and unsubscribes from key event listeners + */ + hideModalAndRemoveEventListeners() { this.unsubscribeFromKeyEvents(); + setModalVisibility(false); + this.props.onModalHide(); } /** @@ -65,11 +75,7 @@ class BaseModal extends PureComponent { this.subscribeToKeyEvents(); setModalVisibility(true); }} - onModalHide={() => { - this.unsubscribeFromKeyEvents(); - setModalVisibility(false); - this.props.onModalHide(); - }} + onModalHide={this.hideModalAndRemoveEventListeners} onSwipeComplete={this.props.onClose} swipeDirection={swipeDirection} isVisible={this.props.isVisible} diff --git a/src/components/ScreenWrapper.js b/src/components/ScreenWrapper.js index 441023b39d48..2575d6cb9453 100644 --- a/src/components/ScreenWrapper.js +++ b/src/components/ScreenWrapper.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import React from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; @@ -9,8 +10,11 @@ const propTypes = { // Array of additional styles to add style: PropTypes.arrayOf(PropTypes.object), - // Returns a function as a child to pass insets to - children: PropTypes.func.isRequired, + // Returns a function as a child to pass insets to or a node to render without insets + children: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.func, + ]).isRequired, // Whether to include padding bottom includePaddingBottom: PropTypes.bool, @@ -47,7 +51,11 @@ const ScreenWrapper = props => ( ]} > - {props.children(insets)} + {// If props.children is a function, call it to provide the insets to the children. + _.isFunction(props.children) + ? props.children(insets) + : props.children + } ); }} diff --git a/src/components/UpdateAppModal/UpdateAppModalPropTypes.js b/src/components/UpdateAppModal/UpdateAppModalPropTypes.js index 476cfb15a104..c55209dfc1a2 100644 --- a/src/components/UpdateAppModal/UpdateAppModalPropTypes.js +++ b/src/components/UpdateAppModal/UpdateAppModalPropTypes.js @@ -3,15 +3,10 @@ import PropTypes from 'prop-types'; const propTypes = { // Callback to fire when we want to trigger the update. onSubmit: PropTypes.func, - - // Version string for the app to update to. - // eslint-disable-next-line react/no-unused-prop-types - version: PropTypes.string, }; const defaultProps = { onSubmit: null, - version: '', }; export {propTypes, defaultProps}; diff --git a/src/components/UpdateAppModal/index.desktop.js b/src/components/UpdateAppModal/index.desktop.js index 3bb08fc5ff58..2b7de124e406 100644 --- a/src/components/UpdateAppModal/index.desktop.js +++ b/src/components/UpdateAppModal/index.desktop.js @@ -8,7 +8,7 @@ const UpdateAppModal = (props) => { if (props.onSubmit) { props.onSubmit(); } - ipcRenderer.sendSync('start-update', props.version); + ipcRenderer.sendSync('start-update'); }; return ; }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 68ebc28c6d27..5003ab5ba411 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -1,6 +1,7 @@ import React from 'react'; import styles from '../../../styles/styles'; +import ROUTES from '../../../ROUTES'; import { SettingsModalStack, NewChatModalStack, @@ -29,7 +30,7 @@ const defaultSubRouteOptions = { const IOUBillStackNavigator = () => ( ( const IOURequestModalStackNavigator = () => ( ( const ProfileModalStackNavigator = () => ( ( const SearchModalStackNavigator = () => ( ( const NewGroupModalStackNavigator = () => ( ( const NewChatModalStackNavigator = () => ( ( const SettingsModalStackNavigator = () => ( Navigation.dismissModal()} - > - {currentViewDescriptor.render()} - - ); - } +/** + * Returns the current descriptor for the focused screen in this navigators state. The descriptor has a function + * called render() that we must call each time this navigator updates. It's important to use this method to render + * a screen, otherwise any child navigators won't be connected to the navigation tree properly. + * + * @param {Object} props + * @returns {Object} + */ +function getCurrentViewDescriptor(props) { + const currentRoute = props.state.routes[props.state.index]; + const currentRouteKey = currentRoute.key; + const currentDescriptor = props.descriptors[currentRouteKey]; + return currentDescriptor; } +const ResponsiveView = props => ( + + {getCurrentViewDescriptor(props).render()} + +); + ResponsiveView.propTypes = propTypes; ResponsiveView.defaultProps = defaultProps; +ResponsiveView.displayName = 'ResponsiveView'; const ResponsiveViewWithHOCs = compose( withWindowDimensions, diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index bd5cc6f1ee92..fe3194f84689 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -32,11 +32,7 @@ function goBack() { * Main navigation method for redirecting to a route. * @param {String} route */ -function navigate(route) { - if (!route) { - return; - } - +function navigate(route = ROUTES.HOME) { if (route === ROUTES.HOME) { openDrawer(); return; diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index 6eca02216268..a7e781abecb4 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -7,7 +7,7 @@ import { getPathFromState, NavigationContainer, } from '@react-navigation/native'; -import Onyx, {withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import {navigationRef} from './Navigation'; import linkingConfig from './linkingConfig'; import AppNavigator from './AppNavigator'; @@ -16,9 +16,14 @@ import ONYXKEYS from '../../ONYXKEYS'; import ROUTES from '../../ROUTES'; import styles from '../../styles/styles'; import themeColors from '../../styles/themes/default'; +import {updateCurrentlyViewedReportID} from '../actions/Report'; +import {setCurrentURL} from '../actions/App'; const propTypes = { + // Whether the current user is logged in with an authToken authenticated: PropTypes.bool.isRequired, + + // The current reportID that we are navigated to or should show in the ReportScreen currentlyViewedReportID: PropTypes.string, }; @@ -43,11 +48,11 @@ class NavigationRoot extends Component { // hooked up const path = getPathName(initialUrl); let initialState = getStateFromPath(path, linkingConfig.config); - Onyx.set(ONYXKEYS.CURRENT_URL, path); + setCurrentURL(path); // If we are landing on something other than the report screen or site root then we MUST set the // initial route to the currently viewed report so there some history to navigate back from - if (path !== ROUTES.HOME && !path.includes(ROUTES.REPORT)) { + if (path !== `/${ROUTES.HOME}` && !path.includes(`/${ROUTES.REPORT}`)) { const homeRoute = { name: 'Home', }; @@ -99,11 +104,11 @@ class NavigationRoot extends Component { if (path.includes(ROUTES.REPORT)) { const reportID = Number(_.last(path.split('/'))); if (reportID && !_.isNaN(reportID)) { - Onyx.merge(ONYXKEYS.CURRENTLY_VIEWED_REPORTID, String(reportID)); + updateCurrentlyViewedReportID(reportID); } } - Onyx.merge(ONYXKEYS.CURRENT_URL, path); + setCurrentURL(path); }} ref={navigationRef} linking={linkingConfig} diff --git a/src/libs/Navigation/linkTo.js b/src/libs/Navigation/linkTo.js index 0253a1422d44..f98f3a672a39 100644 --- a/src/libs/Navigation/linkTo.js +++ b/src/libs/Navigation/linkTo.js @@ -5,36 +5,33 @@ import { import linkingConfig from './linkingConfig'; export default function linkTo(navigation, path) { - if (!path.startsWith('/')) { - throw new Error(`The path must start with '/' (${path}).`); - } - + const normalizedPath = !path.startsWith('/') ? `/${path}` : path; if (navigation === undefined) { throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); } - const state = linkingConfig?.getStateFromPath - ? linkingConfig.getStateFromPath(path, linkingConfig.config) - : getStateFromPath(path, linkingConfig?.config); + const state = linkingConfig.getStateFromPath + ? linkingConfig.getStateFromPath(normalizedPath, linkingConfig.config) + : getStateFromPath(normalizedPath, linkingConfig.config); - if (state) { - let root = navigation; - let current; + if (!state) { + throw new Error('Failed to parse the path to a navigation state.'); + } - // Traverse up to get the root navigation - // eslint-disable-next-line no-cond-assign - while ((current = root.dangerouslyGetParent())) { - root = current; - } + let root = navigation; + let current; - const action = getActionFromState(state, linkingConfig?.config); + // Traverse up to get the root navigation + // eslint-disable-next-line no-cond-assign + while ((current = root.dangerouslyGetParent())) { + root = current; + } + + const action = getActionFromState(state, linkingConfig.config); - if (action !== undefined) { - root.dispatch(action); - } else { - root.reset(state); - } + if (action !== undefined) { + root.dispatch(action); } else { - throw new Error('Failed to parse the path to a navigation state.'); + root.reset(state); } } diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 8dbb061527cb..a7ad1099204a 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -1,3 +1,5 @@ +import ROUTES from '../../ROUTES'; + export default { prefixes: ['expensify-cash://', 'https://expensify.cash', 'https://www.expensify.cash', 'http://localhost'], config: { @@ -7,79 +9,79 @@ export default { initialRouteName: 'Report', screens: { // Report route - Report: 'r/:reportID', + Report: ROUTES.REPORT_WITH_ID, }, }, // Public Routes - SignIn: 'signin', - SetPassword: 'setpassword/:validateCode', + SignIn: ROUTES.SIGNIN, + SetPassword: ROUTES.SET_PASSWORD_WITH_VALIDATE_CODE, // Modal Screens Settings: { - path: 'settings', + path: ROUTES.SETTINGS, initialRouteName: 'Settings_Root', screens: { Settings_Root: { path: '', }, Settings_Preferences: { - path: 'preferences', + path: ROUTES.SETTINGS_PREFERENCES, + exact: true, }, Settings_Password: { - path: 'password', + path: ROUTES.SETTINGS_PASSWORD, + exact: true, }, Settings_Payments: { - path: 'payments', + path: ROUTES.SETTINGS_PAYMENTS, + exact: true, }, Settings_Profile: { - path: 'profile', + path: ROUTES.SETTINGS_PROFILE, + exact: true, }, }, }, NewGroup: { - path: 'new/group', + path: ROUTES.NEW_GROUP, initialRouteName: 'NewGroup_Root', screens: { - NewGroup_Root: { - path: '', - }, + NewGroup_Root: '', }, }, NewChat: { - path: 'new/chat', + path: ROUTES.NEW_CHAT, initialRouteName: 'NewChat_Root', screens: { - NewChat_Root: { - path: '', - }, + NewChat_Root: '', }, }, Search: { - path: 'search', + path: ROUTES.SEARCH, initialRouteName: 'Search_Root', screens: { - Search_Root: { - path: '', - }, + Search_Root: '', }, }, Profile: { initialRouteName: 'Profile_Root', screens: { - Profile_Root: 'profile/:login', + Profile_Root: ROUTES.PROFILE_WITH_LOGIN, }, }, IOU_Request: { + path: ROUTES.IOU_REQUEST, initialRouteName: 'IOU_Request_Root', screens: { - IOU_Request_Root: 'iou/request', + IOU_Request_Root: '', }, }, IOU_Bill: { + path: ROUTES.IOU_BILL, initialRouteName: 'IOU_Bill_Root', screens: { - IOU_Bill_Root: 'iou/split', + IOU_Bill_Root: '', }, }, }, diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.js index 2fb6932e8232..bb29b96e41f8 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.js +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.js @@ -122,17 +122,14 @@ export default { /** * Create a notification to indicate that an update is available. - * - * @param {Object} params - * @param {String} params.version */ - pushUpdateAvailableNotification({version}) { + pushUpdateAvailableNotification() { push({ title: 'Update available', body: 'A new version of Expensify.cash is available!', delay: 0, onClick: () => { - Onyx.merge(ONYXKEYS.UPDATE_VERSION, version); + Onyx.merge(ONYXKEYS.UPDATE_AVAILABLE, true); }, }); }, diff --git a/src/libs/Notification/LocalNotification/index.js b/src/libs/Notification/LocalNotification/index.js index 9dfdd62b3b86..9564b0ef7f26 100644 --- a/src/libs/Notification/LocalNotification/index.js +++ b/src/libs/Notification/LocalNotification/index.js @@ -4,8 +4,8 @@ function showCommentNotification({reportAction, onClick}) { BrowserNotifications.pushReportCommentNotification({reportAction, onClick}); } -function showUpdateAvailableNotification({version}) { - BrowserNotifications.pushUpdateAvailableNotification({version}); +function showUpdateAvailableNotification() { + BrowserNotifications.pushUpdateAvailableNotification(); } export default { diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js new file mode 100644 index 000000000000..66c52a79a360 --- /dev/null +++ b/src/libs/actions/App.js @@ -0,0 +1,14 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../ONYXKEYS'; + +/** + * @param {String} url + */ +function setCurrentURL(url) { + Onyx.set(ONYXKEYS.CURRENT_URL, url); +} + +export { + // eslint-disable-next-line import/prefer-default-export + setCurrentURL, +}; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index ef053c8e9ca9..11988ae99b21 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -238,6 +238,7 @@ function removeOptimisticActions(reportID) { function updateReportWithNewAction(reportID, reportAction) { const newMaxSequenceNumber = reportAction.sequenceNumber; const isFromCurrentUser = reportAction.actorAccountID === currentUserAccountID; + const lastReadSequenceNumber = lastReadSequenceNumbers[reportID] || 0; // When handling an action from the current users we can assume that their // last read actionID has been updated in the server but not necessarily reflected @@ -251,14 +252,21 @@ function updateReportWithNewAction(reportID, reportAction) { // Always merge the reportID into Onyx // If the report doesn't exist in Onyx yet, then all the rest of the data will be filled out // by handleReportChanged - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { + const updatedReportObject = { reportID, - unreadActionCount: newMaxSequenceNumber - (lastReadSequenceNumbers[reportID] || 0), + unreadActionCount: newMaxSequenceNumber - lastReadSequenceNumber, maxSequenceNumber: reportAction.sequenceNumber, - lastMessageTimestamp: reportAction.timestamp, - lastMessageText: messageText, - lastActorEmail: reportAction.actorEmail, - }); + }; + + // If the report action from pusher is a higher sequence number than we know about (meaning it has come from + // a chat participant in another application), then the last message text and author needs to be updated as well + if (newMaxSequenceNumber > lastReadSequenceNumber) { + updatedReportObject.lastMessageTimestamp = reportAction.timestamp; + updatedReportObject.lastMessageText = messageText; + updatedReportObject.lastActorEmail = reportAction.actorEmail; + } + + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, updatedReportObject); const reportActionsToMerge = {}; if (reportAction.clientID) { @@ -570,17 +578,24 @@ function fetchAll(shouldRedirectToReport = true, shouldRecordHomePageTiming = fa function addAction(reportID, text, file) { // Convert the comment from MD into HTML because that's how it is stored in the database const parser = new ExpensiMark(); - const htmlComment = parser.replace(text); + const commentText = parser.replace(text); const isAttachment = _.isEmpty(text) && file !== undefined; // The new sequence number will be one higher than the highest const highestSequenceNumber = reportMaxSequenceNumbers[reportID] || 0; const newSequenceNumber = highestSequenceNumber + 1; + const htmlForNewComment = isAttachment ? 'Uploading Attachment...' : commentText; + + // Remove HTML from text when applying optimistic offline comment + const textForNewComment = isAttachment ? '[Attachment]' + : htmlForNewComment.replace(/<[^>]*>?/gm, ''); // Update the report in Onyx to have the new sequence number Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { maxSequenceNumber: newSequenceNumber, lastMessageTimestamp: moment().unix(), + lastMessageText: textForNewComment, + lastActorEmail: currentUserEmail, }); // Generate a clientID so we can save the optimistic action to storage with the clientID as key. Later, we will @@ -623,11 +638,8 @@ function addAction(reportID, text, file) { message: [ { type: 'COMMENT', - html: isAttachment ? 'Uploading Attachment...' : htmlComment, - - // Remove HTML from text when applying optimistic offline comment - text: isAttachment ? '[Attachment]' - : htmlComment.replace(/<[^>]*>?/gm, ''), + html: htmlForNewComment, + text: textForNewComment, }, ], isFirstItem: false, @@ -639,7 +651,7 @@ function addAction(reportID, text, file) { API.Report_AddComment({ reportID, - reportComment: htmlComment, + reportComment: htmlForNewComment, file, clientID: optimisticReportActionID, @@ -735,6 +747,13 @@ function handleReportChanged(report) { optimisticReportActionIDs[report.reportID] = report.optimisticReportActionIDs; } +/** + * @param {Number} reportID + */ +function updateCurrentlyViewedReportID(reportID) { + Onyx.merge(ONYXKEYS.CURRENTLY_VIEWED_REPORTID, String(reportID)); +} + Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: handleReportChanged, @@ -757,4 +776,5 @@ export { saveReportComment, broadcastUserIsTyping, togglePinnedState, + updateCurrentlyViewedReportID, }; diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index 47218268fa8c..9a428e7d71b6 100644 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -112,42 +112,38 @@ class NewChatPage extends Component { return ( - {() => ( - <> - Navigation.dismissModal()} - /> - - { - const { - personalDetails, - userToInvite, - } = getNewChatOptions( - this.props.reports, - this.props.personalDetails, - searchValue, - ); - this.setState({ - searchValue, - userToInvite, - personalDetails, - }); - }} - headerMessage={headerMessage} - hideSectionHeaders - disableArrowKeysActions - hideAdditionalOptionStates - forceTextUnreadStyle - /> - - - - )} + Navigation.dismissModal()} + /> + + { + const { + personalDetails, + userToInvite, + } = getNewChatOptions( + this.props.reports, + this.props.personalDetails, + searchValue, + ); + this.setState({ + searchValue, + userToInvite, + personalDetails, + }); + }} + headerMessage={headerMessage} + hideSectionHeaders + disableArrowKeysActions + hideAdditionalOptionStates + forceTextUnreadStyle + /> + + ); } diff --git a/src/pages/NewGroupPage.js b/src/pages/NewGroupPage.js index 665117775e53..1bee4f70ba61 100644 --- a/src/pages/NewGroupPage.js +++ b/src/pages/NewGroupPage.js @@ -180,63 +180,59 @@ class NewGroupPage extends Component { ); return ( - {() => ( - <> - Navigation.dismissModal()} - /> - - { - const { - recentReports, - personalDetails, - userToInvite, - } = getNewGroupOptions( - this.props.reports, - this.props.personalDetails, - searchValue, - [], - ); - this.setState({ - searchValue, - userToInvite, - recentReports, - personalDetails, - }); - }} - headerMessage={headerMessage} - disableArrowKeysActions - hideAdditionalOptionStates - forceTextUnreadStyle - /> - {this.state.selectedOptions?.length > 0 && ( - - [ - styles.button, - styles.buttonSuccess, - styles.w100, - hovered && styles.buttonSuccessHovered, - ]} - > - - Create Group - - - - )} + Navigation.dismissModal()} + /> + + { + const { + recentReports, + personalDetails, + userToInvite, + } = getNewGroupOptions( + this.props.reports, + this.props.personalDetails, + searchValue, + [], + ); + this.setState({ + searchValue, + userToInvite, + recentReports, + personalDetails, + }); + }} + headerMessage={headerMessage} + disableArrowKeysActions + hideAdditionalOptionStates + forceTextUnreadStyle + /> + {this.state.selectedOptions?.length > 0 && ( + + [ + styles.button, + styles.buttonSuccess, + styles.w100, + hovered && styles.buttonSuccessHovered, + ]} + > + + Create Group + + - - - )} + )} + + ); } diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 6c46fa3e05ad..d4b4be1c0712 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -53,72 +53,68 @@ const ProfilePage = ({personalDetails, route}) => { const profileDetails = personalDetails[route.params.login]; return ( - {() => ( - <> - - - {profileDetails ? ( - - - - - - - {profileDetails.displayName - ? profileDetails.displayName - : null} + + + {profileDetails ? ( + + + + + + + {profileDetails.displayName + ? profileDetails.displayName + : null} + + {profileDetails.login ? ( + + + {Str.isSMSLogin(profileDetails.login) ? 'Phone Number' : 'Email'} + + + {Str.isSMSLogin(profileDetails.login) + ? Str.removeSMSDomain(profileDetails.login) + : profileDetails.login} - {profileDetails.login ? ( - - - {Str.isSMSLogin(profileDetails.login) ? 'Phone Number' : 'Email'} - - - {Str.isSMSLogin(profileDetails.login) - ? Str.removeSMSDomain(profileDetails.login) - : profileDetails.login} - - - ) : null} - {profileDetails.pronouns ? ( - - - Preferred Pronouns - - - {profileDetails.pronouns} - - - ) : null} - {profileDetails.timezone ? ( - - - Local Time - - - {moment().tz(profileDetails.timezone.selected).format('LT')} - - - ) : null} - - ) : null} + ) : null} + {profileDetails.pronouns ? ( + + + Preferred Pronouns + + + {profileDetails.pronouns} + + + ) : null} + {profileDetails.timezone ? ( + + + Local Time + + + {moment().tz(profileDetails.timezone.selected).format('LT')} + + + ) : null} + - - )} + ) : null} + ); }; diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index ecb92bd2a34f..76a06bf34e46 100644 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -136,42 +136,38 @@ class SearchPage extends Component { ); return ( - {() => ( - <> - Navigation.dismissModal()} - /> - - { - const { - recentReports, - personalDetails, - userToInvite, - } = getSearchOptions( - this.props.reports, - this.props.personalDetails, - searchValue, - ); - this.setState({ - searchValue, - userToInvite, - recentReports, - personalDetails, - }); - }} - headerMessage={headerMessage} - hideSectionHeaders - hideAdditionalOptionStates - /> - - - - )} + Navigation.dismissModal()} + /> + + { + const { + recentReports, + personalDetails, + userToInvite, + } = getSearchOptions( + this.props.reports, + this.props.personalDetails, + searchValue, + ); + this.setState({ + searchValue, + userToInvite, + recentReports, + personalDetails, + }); + }} + headerMessage={headerMessage} + hideSectionHeaders + hideAdditionalOptionStates + /> + + ); } diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index cd54ed789e2d..4cc437036309 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -16,11 +16,11 @@ const propTypes = { }; const defaultProps = { - currentlyViewedReportID: null, + currentlyViewedReportID: 0, }; const ReportScreen = (props) => { - const activeReportID = parseInt(props.currentlyViewedReportID || 0, 10); + const activeReportID = parseInt(props.currentlyViewedReportID, 10); if (!activeReportID) { return null; } @@ -33,19 +33,15 @@ const ReportScreen = (props) => { styles.flexColumn, ]} > - {() => ( - <> - Navigation.navigate(ROUTES.HOME)} - /> - - - - - )} + Navigation.navigate(ROUTES.HOME)} + /> + + + ); }; diff --git a/src/pages/home/sidebar/OptionRow.js b/src/pages/home/sidebar/OptionRow.js index a556b858d7d2..641109bc45c6 100644 --- a/src/pages/home/sidebar/OptionRow.js +++ b/src/pages/home/sidebar/OptionRow.js @@ -68,7 +68,9 @@ const OptionRow = ({ return ( {hovered => ( - onSelectRow(option)} + activeOpacity={0.8} style={[ styles.flexRow, styles.alignItemsCenter, @@ -79,9 +81,7 @@ const OptionRow = ({ hovered && !optionIsFocused ? hoverStyle : null, ]} > - onSelectRow(option)} - activeOpacity={0.8} + )} - + {!hideAdditionalOptionStates && ( {option.hasDraftComment && ( @@ -140,7 +140,7 @@ const OptionRow = ({ )} )} - + )} ); diff --git a/src/pages/settings/InitialPage.js b/src/pages/settings/InitialPage.js index b5a828395920..ee343ea59c62 100644 --- a/src/pages/settings/InitialPage.js +++ b/src/pages/settings/InitialPage.js @@ -88,66 +88,62 @@ const InitialSettingsPage = ({ } return ( - {() => ( - <> - Navigation.dismissModal()} - /> - - - + Navigation.dismissModal()} + /> + + + - - - - - {myPersonalDetails.displayName - ? myPersonalDetails.displayName - : Str.removeSMSDomain(session.email)} - - {myPersonalDetails.displayName && ( - - {Str.removeSMSDomain(session.email)} - - )} - - {menuItems.map(item => ( - Navigation.navigate(item.route)} - shouldShowRightArrow - /> - ))} - - - - Sign Out - - - + + - - v - {version} + + {myPersonalDetails.displayName + ? myPersonalDetails.displayName + : Str.removeSMSDomain(session.email)} + {myPersonalDetails.displayName && ( + + {Str.removeSMSDomain(session.email)} + + )} + + {menuItems.map(item => ( + Navigation.navigate(item.route)} + shouldShowRightArrow + /> + ))} + + + + Sign Out + + - - )} + + + v + {version} + + ); }; diff --git a/src/pages/settings/PasswordPage.js b/src/pages/settings/PasswordPage.js index 64f48b95207f..7ed238f3596c 100644 --- a/src/pages/settings/PasswordPage.js +++ b/src/pages/settings/PasswordPage.js @@ -6,17 +6,12 @@ import ScreenWrapper from '../../components/ScreenWrapper'; const PasswordPage = () => ( - {() => ( - <> - Navigation.navigate(ROUTES.SETTINGS)} - onCloseButtonPress={() => Navigation.dismissModal()} - /> - - - )} + Navigation.navigate(ROUTES.SETTINGS)} + onCloseButtonPress={() => Navigation.dismissModal()} + /> ); diff --git a/src/pages/settings/PaymentsPage.js b/src/pages/settings/PaymentsPage.js index 565ebb89ba79..0ef4afe61973 100644 --- a/src/pages/settings/PaymentsPage.js +++ b/src/pages/settings/PaymentsPage.js @@ -53,46 +53,41 @@ class PaymentsPage extends React.Component { render() { return ( - {() => ( - <> - Navigation.navigate(ROUTES.SETTINGS)} - onCloseButtonPress={() => Navigation.dismissModal()} + Navigation.navigate(ROUTES.SETTINGS)} + onCloseButtonPress={() => Navigation.dismissModal()} + /> + + + + Enter your username to get paid back via PayPal. + + + PayPal.me/ + + this.setState({payPalMeUsername: text})} /> - - - - Enter your username to get paid back via PayPal. - - - PayPal.me/ - - this.setState({payPalMeUsername: text})} - /> - - [ - styles.button, - styles.buttonSuccess, - styles.mt3, - hovered && styles.buttonSuccessHovered, - ]} - > - - Add PayPal Account - - - - - - )} + + [ + styles.button, + styles.buttonSuccess, + styles.mt3, + hovered && styles.buttonSuccessHovered, + ]} + > + + Add PayPal Account + + + ); } diff --git a/src/pages/settings/PreferencesPage.js b/src/pages/settings/PreferencesPage.js index 6558a372cee8..b964cb75dceb 100644 --- a/src/pages/settings/PreferencesPage.js +++ b/src/pages/settings/PreferencesPage.js @@ -40,41 +40,37 @@ const priorityModes = { const PreferencesPage = ({priorityMode}) => ( - {() => ( - <> - Navigation.navigate(ROUTES.SETTINGS)} - onCloseButtonPress={() => Navigation.dismissModal()} - /> - - - - Priority Mode - - - {/* empty object in placeholder below to prevent default */} - {/* placeholder from appearing as a selection option. */} - NameValuePair.set(CONST.NVP.PRIORITY_MODE, mode, ONYXKEYS.PRIORITY_MODE) - } - items={Object.values(priorityModes)} - style={styles.picker} - useNativeAndroidPickerStyle={false} - placeholder={{}} - value={priorityMode} - Icon={() => } - /> - - - {priorityModes[priorityMode].description} - - + Navigation.navigate(ROUTES.SETTINGS)} + onCloseButtonPress={() => Navigation.dismissModal()} + /> + + + + Priority Mode + + + {/* empty object in placeholder below to prevent default */} + {/* placeholder from appearing as a selection option. */} + NameValuePair.set(CONST.NVP.PRIORITY_MODE, mode, ONYXKEYS.PRIORITY_MODE) + } + items={Object.values(priorityModes)} + style={styles.picker} + useNativeAndroidPickerStyle={false} + placeholder={{}} + value={priorityMode} + Icon={() => } + /> - - )} + + {priorityModes[priorityMode].description} + + + ); diff --git a/src/pages/settings/ProfilePage.js b/src/pages/settings/ProfilePage.js index 70cb60dedab6..93e284f709fc 100644 --- a/src/pages/settings/ProfilePage.js +++ b/src/pages/settings/ProfilePage.js @@ -6,16 +6,12 @@ import ROUTES from '../../ROUTES'; const ProfilePage = () => ( - {() => ( - <> - Navigation.navigate(ROUTES.SETTINGS)} - onCloseButtonPress={() => Navigation.dismissModal()} - /> - - )} + Navigation.navigate(ROUTES.SETTINGS)} + onCloseButtonPress={() => Navigation.dismissModal()} + /> ); diff --git a/src/setup/index.desktop.js b/src/setup/index.desktop.js index 8864e3c6b396..f1c9364e7ab3 100644 --- a/src/setup/index.desktop.js +++ b/src/setup/index.desktop.js @@ -9,7 +9,7 @@ export default function () { rootTag: document.getElementById('root'), }); - ipcRenderer.on('update-downloaded', (_, version) => { - LocalNotification.showUpdateAvailableNotification({version}); + ipcRenderer.on('update-downloaded', () => { + LocalNotification.showUpdateAvailableNotification(); }); }