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 => (
-
+ {menuItems.map(item => (
+
+
+ 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();
});
}