Compare commits

...
Author SHA1 Message Date
StandardNotes CI 6e6a511771 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2023-05-03 14:15:57 +00:00
Mo 44e91b5ede chore: copy and style improvements 2023-05-03 08:41:25 -05:00
StandardNotes CI 4aad554c1a chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected].1
2023-05-03 13:28:45 +00:00
Mo 9f937f217b chore: remove phased out storage encryption policy (#2323) 2023-05-03 07:56:01 -05:00
StandardNotes CI e3f8b81139 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2023-05-03 12:44:33 +00:00
Mo b117271d61 feat: Added Export option for Authenticator entries 2023-05-03 07:11:17 -05:00
StandardNotes CI e635e06ca0 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected].1
2023-05-02 21:21:11 +00:00
Mo 75ecf4c393 fix: automatically convert Super notes to Markdown for the Plaintext Backups feature 2023-05-02 15:46:24 -05:00
Aman Harwara 2eddc3a1c6 chore: fix prefs open by default 2023-05-03 01:43:42 +05:30
StandardNotes CI 90d1ead9e3 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].0
2023-05-02 16:42:21 +00:00
Mo 7e3db49322 feat: Automatic plaintext backup option in Preferences > Backups will backup your notes and tags into plaintext, unencrypted folders on your computer. In addition, automatic encrypted text backups preference management has moved from the top-level menu in the desktop app to Preferences > Backups. (#2322) 2023-05-02 11:05:10 -05:00
Aman Harwara 3df23cdb5c chore: ios testflight 2023-05-02 13:27:47 +05:30
StandardNotes CI e739795396 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
2023-05-01 20:36:40 +00:00
Aman Harwara b5f31c3ca3 chore: manual ios testflight deploy 2023-05-02 01:35:43 +05:30
StandardNotes CI 053aaa5c9f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].1
2023-05-01 16:23:02 +00:00
Aman Harwara 9e0e176bac fix: Fixed the issue where imported notes would have incorrect "Created" date and time 2023-05-01 21:23:41 +05:30
Aman Harwara b1e211f91f chore: changelog 2023-05-01 21:21:49 +05:30
Aman Harwara 880cceecd5 Revert "feat: Swipe gestures on mobile are now enabled by default and have been improved. You can disable them from Preferences > General > Labs (#2319)"
This reverts commit 6d326e2db4.
2023-05-01 21:21:00 +05:30
StandardNotes CI 30c160f4b8 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2023-05-01 14:14:26 +00:00
Aman Harwara 2b8d7e65ff chore: fix linking input 2023-05-01 19:16:35 +05:30
Aman Harwara 6d326e2db4 feat: Swipe gestures on mobile are now enabled by default and have been improved. You can disable them from Preferences > General > Labs (#2319) 2023-05-01 18:59:05 +05:30
StandardNotes CI 9fe88c6c10 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2023-04-28 19:27:37 +00:00
Aman Harwara 17dc74aaef chore: fix table view layout on tablet-size screens 2023-04-29 00:29:33 +05:30
Aman Harwara 9c8837ecdb Merge branch 'main' of github.com:standardnotes/app 2023-04-29 00:10:26 +05:30
Aman Harwara e1a47ea884 chore: display options menu keyboard nav 2023-04-29 00:10:13 +05:30
StandardNotes CI 06d6f3b481 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2023-04-28 11:30:21 +00:00
Aman Harwara 0ab9006c52 chore: update collapsible plugin 2023-04-28 16:26:02 +05:30
Aman Harwara 4d9c9d597b chore: make collapsible arrow key behavior same as lexical 2023-04-28 16:24:38 +05:30
Aman Harwara 40179bba06 fix: Fixed issue where removing a collapsible block would unwrap the contents in reverse order 2023-04-28 16:20:46 +05:30
Aman Harwara 8c548d40c4 chore: fix rename input theming 2023-04-28 15:56:17 +05:30
StandardNotes CI a4cfd603d2 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2023-04-28 08:16:09 +00:00
Aman Harwara 27ab5449f6 chore: upgrade lexical 2023-04-28 13:15:45 +05:30
Aman Harwara 4740e43d1c chore: fix default editor dropdown 2023-04-28 13:01:50 +05:30
StandardNotes CI b8f78eb4fd chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2023-04-27 15:51:05 +00:00
Aman Harwara 7c1ded30c7 Merge branch 'main' of github.com:standardnotes/app 2023-04-27 20:46:24 +05:30
Aman Harwara 247ff62422 fix(desktop): Fixed issue on Mac desktop app where multiple files could not be downloaded 2023-04-27 20:46:18 +05:30
StandardNotes CI 420cb4d945 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2023-04-27 15:12:31 +00:00
Aman Harwara 7b6e5f95d5 chore: ios ci 2023-04-27 20:07:39 +05:30
Aman Harwara beddb3cc6c Merge branch 'main' of github.com:standardnotes/app 2023-04-27 19:53:47 +05:30
Aman Harwara 725b49568a chore: ios ci 2023-04-27 19:53:37 +05:30
StandardNotes CI 6b89a6615f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].9
2023-04-27 13:19:37 +00:00
Aman Harwara 13d56082d8 chore: fix menu closing 2023-04-27 18:19:25 +05:30
Aman Harwara 91b3978e14 Merge branch 'main' of github.com:standardnotes/app 2023-04-27 18:12:29 +05:30
Aman Harwara b34ab6ab28 chore: fix error when converting empty super note 2023-04-27 18:12:23 +05:30
StandardNotes CI 07ec2be5ee chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
2023-04-27 12:37:47 +00:00
Aman Harwara d1009e368a chore: fix mobile large font size 2023-04-27 18:06:20 +05:30
Aman Harwara b0cf179a90 Merge branch 'main' of github.com:standardnotes/app 2023-04-27 17:38:57 +05:30
Aman Harwara 2c4b574bd4 chore: ios ci 2023-04-27 17:38:48 +05:30
Aman Harwara d658769531 chore: tooltips 2023-04-27 17:37:31 +05:30
StandardNotes CI a10d5a6728 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
2023-04-27 11:08:06 +00:00
Aman Harwara 67d3f149b2 chore: ios ci 2023-04-27 16:02:50 +05:30
Aman Harwara 0baf500616 Merge branch 'main' of github.com:standardnotes/app 2023-04-27 15:58:06 +05:30
Aman Harwara ef27a21fb1 fix(desktop): Fixed issue where preferences header & tooltips are overlapped by custom titlebar 2023-04-27 15:57:56 +05:30
Aman Harwara 76bd995c9a chore: tooltips 2023-04-27 15:13:54 +05:30
StandardNotes CI 3ada748522 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
2023-04-27 09:19:43 +00:00
Aman Harwara f45a639a72 chore: close file menu on download 2023-04-27 14:29:59 +05:30
Aman Harwara 8bb5bb6f9f Merge branch 'main' of github.com:standardnotes/app 2023-04-27 14:21:34 +05:30
Aman Harwara 1925d8a9c2 chore: trigger build 2023-04-27 14:21:21 +05:30
StandardNotes CI 93a1402088 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
2023-04-27 08:33:18 +00:00
Aman Harwara cb15d183ca chore: update fastlane 2023-04-27 13:29:42 +05:30
StandardNotes CI e686aa29e8 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
2023-04-27 07:07:21 +00:00
Aman Harwara 96905fe980 Merge branch 'main' of github.com:standardnotes/app 2023-04-27 12:09:42 +05:30
Aman Harwara bd3fabcaa7 chore: ios ci 2023-04-27 12:09:33 +05:30
StandardNotes CI c6dcbf2cc9 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
2023-04-26 18:40:15 +00:00
Aman Harwara a5d24fa526 Merge branch 'main' of github.com:standardnotes/app 2023-04-26 23:39:12 +05:30
Aman Harwara aa16a287fa chore: trigger build 2023-04-26 23:39:04 +05:30
StandardNotes CI c41f686fd0 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
2023-04-26 18:01:06 +00:00
Aman Harwara ec358d6b07 chore: show android system webview update prompt if app fails to load (#2318) 2023-04-26 22:58:59 +05:30
Aman Harwara af1e18b47e chore: use latest macos runner 2023-04-26 22:25:06 +05:30
173 changed files with 2959 additions and 1698 deletions
+57
View File
@@ -0,0 +1,57 @@
name: iOS Testflight
on: workflow_dispatch
jobs:
ios:
defaults:
run:
working-directory: packages/mobile
runs-on: macos-latest
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Export version from package.json
run:
echo "PACKAGE_VERSION=$(grep '"version"' ../web/package.json | cut -d '"' -f 4 | cut -d "-" -f 1)" >> $GITHUB_ENV
- name: Install dependencies
run: yarn install --immutable && yarn install:pods
- run: yarn build:mobile
- name: Set ssh connection to Github
uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Export Apple AppStore Connect API key
run: echo "${{ secrets.APPSTORE_CONNECT_KEY }}" > ios/Authkey.p8
- name: Ruby Setup for Fastlane
uses: ruby/setup-ruby@v1
with:
working-directory: 'packages/mobile'
- name: Setup fastlane
uses: maierj/[email protected]
env:
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
with:
lane: 'ios setup'
subdirectory: 'packages/mobile'
- name: fastlane
uses: maierj/[email protected]
env:
APPLE_APP_ID: ${{ secrets.APP_APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPSTORE_CONNECT_KEY_ID: ${{ secrets.APPSTORE_CONNECT_KEY_ID }}
APPSTORE_CONNECT_KEY_ISSUER_ID: ${{ secrets.APPSTORE_CONNECT_KEY_ISSUER_ID }}
BUILD_NUMBER: ${{ github.run_number }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }}
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
MATCH_CERTIFICATES_URL: ${{ secrets.MATCH_CERTIFICATES_URL }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}
with:
lane: 'ios testflight'
subdirectory: 'packages/mobile'
+1 -3
View File
@@ -66,12 +66,10 @@ jobs:
packages/mobile/android/app/build/outputs/apk/prod/release/app-prod-release.apk
ios:
env:
ITMSTRANSPORTER_FORCE_ITMS_PACKAGE_UPLOAD: true
defaults:
run:
working-directory: packages/mobile
runs-on: macos-11
runs-on: macos-latest
timeout-minutes: 90
steps:
- name: Checkout code
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+72
View File
@@ -3,6 +3,78 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.41](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.40](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.39](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.38](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.37](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.36](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.35](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.34](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.33](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.30](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.29](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.28](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/clipper
## [1.0.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/clipper
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/clipper",
"description": "Web clipper browser extension for Standard Notes",
"version": "1.0.24",
"version": "1.0.42",
"private": true,
"scripts": {
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
+4 -4
View File
@@ -1,4 +1,4 @@
PURCHASE_URL=https://website-dev.standardnotes.com/purchase
PLANS_URL=https://website-dev.standardnotes.com/plans
DASHBOARD_URL=https://website-dev.standardnotes.com/dashboard
DEFAULT_SYNC_SERVER=https://api-dev.standardnotes.com
PURCHASE_URL=https://standardnotes.com/purchase
PLANS_URL=https://standardnotes.com/plans
DASHBOARD_URL=https://standardnotes.com/dashboard
DEFAULT_SYNC_SERVER=https://api.standardnotes.com
+76
View File
@@ -3,6 +3,82 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.106.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/desktop
## [3.106.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
**Note:** Version bump only for package @standardnotes/desktop
# [3.106.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
### Features
* Automatic plaintext backup option in Preferences > Backups will backup your notes and tags into plaintext, unencrypted folders on your computer. In addition, automatic encrypted text backups preference management has moved from the top-level menu in the desktop app to Preferences > Backups. ([#2322](https://github.com/standardnotes/app/issues/2322)) ([7e3db49](https://github.com/standardnotes/app/commit/7e3db49322034aa644c3c99ccf9a5f1b508583aa))
## [3.105.86](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.85](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.84](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.83](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.82](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.81](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.80](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.79](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.78](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.77](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
### Bug Fixes
* **desktop:** Fixed issue where preferences header & tooltips are overlapped by custom titlebar ([ef27a21](https://github.com/standardnotes/app/commit/ef27a21fb141c4ef1a760cf6fb550e4b713f8f98))
## [3.105.76](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.75](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.74](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/desktop
## [3.105.73](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/desktop
+6 -48
View File
@@ -1,51 +1,17 @@
# Standard Notes
<div align="center">
[![Twitter Follow](https://img.shields.io/badge/follow-%40standardnotes-blue.svg?style=flat&logo=twitter)](https://twitter.com/standardnotes)
</div>
This application makes use of the core JS/CSS/HTML code found in the [web repo](https://github.com/standardnotes/app). For issues related to the actual app experience, please post issues in the web repo.
# Standard Notes Desktop App
## Running Locally
Make sure [Yarn](https://classic.yarnpkg.com/en/) is installed on your system.
Most commands below hog up a terminal process and must be conducted in different tabs. Be sure to quit any production version of the app running on your system first.
```bash
yarn install
yarn build:web # Or `yarn dev:web`
yarn dev
# In another terminal
yarn start
cd packages/snjs && yarn start # optional to watch snjs changes
cd packages/web && yarn watch # optional to watch web changes
yarn dev # to start compilation watch process for electron-related code
yarn start # to start dev app
```
We use [commitlint](https://github.com/conventional-changelog/commitlint) to validate commit messages.
Before making a pull request, make sure to check the output of the following commands:
```bash
yarn lint
yarn test # Make sure to start `yarn dev` before running the tests, and quit any running Standard Notes applications so they don't conflict.
```
Pull requests should target the `develop` branch.
### Installing dependencies
To determine where to install a dependency:
- If it is only required for building, install it in `package.json`'s `devDependencies`
- If it is required at runtime but can be packaged by webpack, install it in `package.json`'s `dependencies`.
- If it must be distributed as a node module (not packaged by webpack), install it in `app/package.json`'s `dependencies`
- Also make sure to declare it as an external commonjs dependency in `webpack.common.js`.
## Building
Build for all platforms:
- `yarn release`
## Building natively on arm64
Building arm64 releases on amd64 systems is only possible with AppImage, Debian and universal "dir" targets.
@@ -63,14 +29,6 @@ and making sure `$GEM_HOME/bin` is added to `$PATH`.
Snap releases also require a working snapcraft / `snapd` installation.
Building can then be done by running:
- `yarn install`
Followed by
- `node scripts/build.mjs deb-arm64`
## Installation
On Linux, download the latest AppImage from the [Releases](https://github.com/standardnotes/app/releases/latest) page, and give it executable permission:
-11
View File
@@ -1,4 +1,3 @@
import { action, makeObservable, observable } from 'mobx'
import { MessageType } from '../test/TestIpcMessage'
import { Store } from './javascripts/Main/Store/Store'
import { StoreKeys } from './javascripts/Main/Store/StoreKeys'
@@ -14,7 +13,6 @@ export class AppState {
readonly startUrl = Urls.indexHtml
readonly isPrimaryInstance: boolean
public willQuitApp = false
public lastBackupDate: number | null = null
public windowState?: WindowState
public deepLinkUrl?: string
public readonly updates: UpdateState
@@ -28,11 +26,6 @@ export class AppState {
this.lastRunVersion = this.store.get(StoreKeys.LastRunVersion) || 'unknown'
this.store.set(StoreKeys.LastRunVersion, this.version)
makeObservable(this, {
lastBackupDate: observable,
setBackupCreationDate: action,
})
this.updates = new UpdateState(this)
if (isTesting()) {
@@ -45,8 +38,4 @@ export class AppState {
public isRunningVersionForFirstTime(): boolean {
return this.lastRunVersion !== this.version
}
setBackupCreationDate(date: number | null): void {
this.lastBackupDate = date
}
}
+21
View File
@@ -0,0 +1,21 @@
import { isDev } from './javascripts/Main/Utils/Utils'
import { log as utilsLog } from '@standardnotes/utils'
export const isDevMode = isDev()
export enum LoggingDomain {
Backups,
}
const LoggingStatus: Record<LoggingDomain, boolean> = {
[LoggingDomain.Backups]: true,
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function log(domain: LoggingDomain, ...args: any[]): void {
if (!isDevMode || !LoggingStatus[domain]) {
return
}
utilsLog(LoggingDomain[domain], ...args)
}
+4
View File
@@ -29,6 +29,10 @@ export function initializeApplication(args: { app: Electron.App; ipcMain: Electr
if (isDev()) {
/** Expose the app's state as a global variable. Useful for debugging */
;(global as any).appState = state
setTimeout(() => {
state.windowState?.window.webContents.openDevTools()
}, 500)
}
}
+1 -1
View File
@@ -120,7 +120,7 @@ function migrateSnapStorage() {
error?.message ?? error,
)
}
store.set(StoreKeys.BackupsLocation, newLocation)
store.set(StoreKeys.LegacyTextBackupsLocation, newLocation)
console.log('Migration: finished moving backups directory.')
}
}
@@ -1,245 +0,0 @@
import { dialog, shell, WebContents } from 'electron'
import { promises as fs } from 'fs'
import path from 'path'
import { AppMessageType, MessageType } from '../../../../test/TestIpcMessage'
import { AppState } from '../../../AppState'
import { MessageToWebApp } from '../../Shared/IpcMessages'
import { StoreKeys } from '../Store/StoreKeys'
import { backups as str } from '../Strings'
import { Paths } from '../Types/Paths'
import {
deleteDir,
deleteDirContents,
ensureDirectoryExists,
FileDoesNotExist,
moveFiles,
openDirectoryPicker,
} from '../Utils/FileUtils'
import { handleTestMessage, send } from '../Utils/Testing'
import { isTesting, last } from '../Utils/Utils'
import { BackupsManagerInterface } from './BackupsManagerInterface'
function log(...message: any) {
console.log('BackupsManager:', ...message)
}
function logError(...message: any) {
console.error('BackupsManager:', ...message)
}
export const enum EnsureRecentBackupExists {
Success = 0,
BackupsAreDisabled = 1,
FailedToCreateBackup = 2,
}
export const BackupsDirectoryName = 'Standard Notes Backups'
const BackupFileExtension = '.txt'
function backupFileNameToDate(string: string): number {
string = path.basename(string, '.txt')
const dateTimeDelimiter = string.indexOf('T')
const date = string.slice(0, dateTimeDelimiter)
const time = string.slice(dateTimeDelimiter + 1).replace(/-/g, ':')
return Date.parse(date + 'T' + time)
}
function dateToSafeFilename(date: Date) {
return date.toISOString().replace(/:/g, '-')
}
async function copyDecryptScript(location: string) {
try {
await ensureDirectoryExists(location)
await fs.copyFile(Paths.decryptScript, path.join(location, path.basename(Paths.decryptScript)))
} catch (error) {
console.error(error)
}
}
export function createBackupsManager(webContents: WebContents, appState: AppState): BackupsManagerInterface {
let backupsLocation = appState.store.get(StoreKeys.BackupsLocation)
let backupsDisabled = appState.store.get(StoreKeys.BackupsDisabled)
let needsBackup = false
if (!backupsDisabled) {
void copyDecryptScript(backupsLocation)
}
determineLastBackupDate(backupsLocation)
.then((date) => appState.setBackupCreationDate(date))
.catch(console.error)
async function setBackupsLocation(location: string) {
const previousLocation = backupsLocation
if (previousLocation === location) {
return
}
const newLocation = path.join(location, BackupsDirectoryName)
let previousLocationFiles = await fs.readdir(previousLocation)
const backupFiles = previousLocationFiles
.filter((fileName) => fileName.endsWith(BackupFileExtension))
.map((fileName) => path.join(previousLocation, fileName))
await moveFiles(backupFiles, newLocation)
await copyDecryptScript(newLocation)
previousLocationFiles = await fs.readdir(previousLocation)
if (previousLocationFiles.length === 0 || previousLocationFiles[0] === path.basename(Paths.decryptScript)) {
await deleteDir(previousLocation)
}
/** Wait for the operation to be successful before saving new location */
backupsLocation = newLocation
appState.store.set(StoreKeys.BackupsLocation, backupsLocation)
}
async function saveBackupData(data: any) {
if (backupsDisabled) {
return
}
let success: boolean
let name: string | undefined
try {
name = await writeDataToFile(data)
log(`Data backup successfully saved: ${name}`)
success = true
appState.setBackupCreationDate(Date.now())
} catch (err) {
success = false
logError('An error occurred saving backup file', err)
}
webContents.send(MessageToWebApp.FinishedSavingBackup, { success })
if (isTesting()) {
send(AppMessageType.SavedBackup)
}
return name
}
function performBackup() {
if (backupsDisabled) {
return
}
webContents.send(MessageToWebApp.PerformAutomatedBackup)
}
async function writeDataToFile(data: any): Promise<string> {
await ensureDirectoryExists(backupsLocation)
const name = dateToSafeFilename(new Date()) + BackupFileExtension
const filePath = path.join(backupsLocation, name)
await fs.writeFile(filePath, data)
return name
}
let interval: NodeJS.Timeout | undefined
function beginBackups() {
if (interval) {
clearInterval(interval)
}
needsBackup = true
const hoursInterval = 12
const seconds = hoursInterval * 60 * 60
const milliseconds = seconds * 1000
interval = setInterval(performBackup, milliseconds)
}
function toggleBackupsStatus() {
backupsDisabled = !backupsDisabled
appState.store.set(StoreKeys.BackupsDisabled, backupsDisabled)
/** Create a backup on reactivation. */
if (!backupsDisabled) {
performBackup()
}
}
if (isTesting()) {
handleTestMessage(MessageType.DataArchive, (data: any) => saveBackupData(data))
handleTestMessage(MessageType.BackupsAreEnabled, () => !backupsDisabled)
handleTestMessage(MessageType.ToggleBackupsEnabled, toggleBackupsStatus)
handleTestMessage(MessageType.BackupsLocation, () => backupsLocation)
handleTestMessage(MessageType.PerformBackup, performBackup)
handleTestMessage(MessageType.CopyDecryptScript, copyDecryptScript)
handleTestMessage(MessageType.ChangeBackupsLocation, setBackupsLocation)
}
return {
get backupsAreEnabled() {
return !backupsDisabled
},
get backupsLocation() {
return backupsLocation
},
saveBackupData,
performBackup,
beginBackups,
toggleBackupsStatus,
async backupsCount(): Promise<number> {
let files = await fs.readdir(backupsLocation)
files = files.filter((fileName) => fileName.endsWith(BackupFileExtension))
return files.length
},
applicationDidBlur() {
if (needsBackup) {
needsBackup = false
performBackup()
}
},
viewBackups() {
void shell.openPath(backupsLocation)
},
async deleteBackups() {
await deleteDirContents(backupsLocation)
return copyDecryptScript(backupsLocation)
},
async changeBackupsLocation() {
const path = await openDirectoryPicker()
if (!path) {
return
}
try {
await setBackupsLocation(path)
performBackup()
} catch (e) {
logError(e)
void dialog.showMessageBox({
message: str().errorChangingDirectory(e),
})
}
},
}
}
async function determineLastBackupDate(backupsLocation: string): Promise<number | null> {
try {
const files = (await fs.readdir(backupsLocation))
.filter((filename) => filename.endsWith(BackupFileExtension) && !Number.isNaN(backupFileNameToDate(filename)))
.sort()
const lastBackupFileName = last(files)
if (!lastBackupFileName) {
return null
}
const backupDate = backupFileNameToDate(lastBackupFileName)
if (Number.isNaN(backupDate)) {
return null
}
return backupDate
} catch (error: any) {
if (error.code !== FileDoesNotExist) {
console.error(error)
}
return null
}
}
@@ -1,13 +0,0 @@
export interface BackupsManagerInterface {
backupsAreEnabled: boolean
toggleBackupsStatus(): void
backupsLocation: string
backupsCount(): Promise<number>
applicationDidBlur(): void
changeBackupsLocation(): void
beginBackups(): void
performBackup(): void
deleteBackups(): Promise<void>
viewBackups(): void
saveBackupData(data: unknown): void
}
@@ -1,18 +1,22 @@
import { LoggingDomain, log } from './../../../Logging'
import {
FileBackupRecord,
FileBackupsDevice,
FileBackupsMapping,
FileBackupReadToken,
FileBackupReadChunkResponse,
PlaintextBackupsMapping,
DesktopWatchedDirectoriesChange,
} from '@web/Application/Device/DesktopSnjsExports'
import { AppState } from 'app/AppState'
import { shell } from 'electron'
import { promises as fs } from 'fs'
import { WebContents, shell } from 'electron'
import { StoreKeys } from '../Store/StoreKeys'
import path from 'path'
import {
deleteFile,
deleteFileIfExists,
ensureDirectoryExists,
moveDirContents,
moveFile,
openDirectoryPicker,
readJSONFile,
writeFile,
@@ -20,6 +24,10 @@ import {
} from '../Utils/FileUtils'
import { FileDownloader } from './FileDownloader'
import { FileReadOperation } from './FileReadOperation'
import { Paths } from '../Types/Paths'
import { MessageToWebApp } from '../../Shared/IpcMessages'
const TextBackupFileExtension = '.txt'
export const FileBackupsConstantsV1 = {
Version: '1.0.0',
@@ -29,107 +37,112 @@ export const FileBackupsConstantsV1 = {
export class FilesBackupManager implements FileBackupsDevice {
private readOperations: Map<string, FileReadOperation> = new Map()
private plaintextMappingCache?: PlaintextBackupsMapping
constructor(private appState: AppState) {}
constructor(private appState: AppState, private webContents: WebContents) {}
public isFilesBackupsEnabled(): Promise<boolean> {
return Promise.resolve(this.appState.store.get(StoreKeys.FileBackupsEnabled))
}
private async findUuidForPlaintextBackupFileName(
backupsDirectory: string,
targetFilename: string,
): Promise<string | undefined> {
const mapping = await this.getPlaintextBackupsMappingFile(backupsDirectory)
public async enableFilesBackups(): Promise<void> {
const currentLocation = await this.getFilesBackupsLocation()
if (!currentLocation) {
const result = await this.changeFilesBackupsLocation()
if (!result) {
return
const uuid = Object.keys(mapping.files).find((uuid) => {
const entries = mapping.files[uuid]
for (const entry of entries) {
const filePath = entry.path
const filename = path.basename(filePath)
if (filename === targetFilename) {
return true
}
}
}
return false
})
this.appState.store.set(StoreKeys.FileBackupsEnabled, true)
const mapping = this.getMappingFileFromDisk()
if (!mapping) {
await this.saveFilesBackupsMappingFile(this.defaultMappingFileValue())
}
return uuid
}
public disableFilesBackups(): Promise<void> {
this.appState.store.set(StoreKeys.FileBackupsEnabled, false)
public async migrateLegacyFileBackupsToNewStructure(newLocation: string): Promise<void> {
const legacyLocation = await this.getLegacyFilesBackupsLocation()
if (!legacyLocation) {
return
}
return Promise.resolve()
await ensureDirectoryExists(newLocation)
const legacyMappingLocation = `${legacyLocation}/info.json`
const newMappingLocation = this.getFileBackupsMappingFilePath(newLocation)
await ensureDirectoryExists(path.dirname(newMappingLocation))
await moveFile(legacyMappingLocation, newMappingLocation)
await moveDirContents(legacyLocation, newLocation)
}
public async changeFilesBackupsLocation(): Promise<string | undefined> {
const newPath = await openDirectoryPicker()
public async isLegacyFilesBackupsEnabled(): Promise<boolean> {
return this.appState.store.get(StoreKeys.LegacyFileBackupsEnabled)
}
if (!newPath) {
async wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean> {
const value = this.appState.store.get(StoreKeys.LegacyTextBackupsDisabled)
return value === true
}
async getUserDocumentsDirectory(): Promise<string> {
return Paths.documentsDir
}
public async getLegacyFilesBackupsLocation(): Promise<string | undefined> {
return this.appState.store.get(StoreKeys.LegacyFileBackupsLocation)
}
async getLegacyTextBackupsLocation(): Promise<string | undefined> {
const savedLocation = this.appState.store.get(StoreKeys.LegacyTextBackupsLocation)
if (savedLocation) {
return savedLocation
}
const LegacyTextBackupsDirectory = 'Standard Notes Backups'
return `${Paths.homeDir}/${LegacyTextBackupsDirectory}`
}
public async presentDirectoryPickerForLocationChangeAndTransferOld(
appendPath: string,
oldLocation?: string,
): Promise<string | undefined> {
const selectedDirectory = await openDirectoryPicker('Select')
if (!selectedDirectory) {
return undefined
}
const oldPath = await this.getFilesBackupsLocation()
const newPath = path.join(selectedDirectory, path.normalize(appendPath))
if (oldPath) {
await this.transferFilesBackupsToNewLocation(oldPath, newPath)
} else {
this.appState.store.set(StoreKeys.FileBackupsLocation, newPath)
await ensureDirectoryExists(newPath)
if (oldLocation) {
await moveDirContents(path.normalize(oldLocation), newPath)
}
return newPath
}
private async transferFilesBackupsToNewLocation(oldPath: string, newPath: string): Promise<void> {
const mapping = await this.getMappingFileFromDisk()
if (!mapping) {
return
}
const entries = Object.values(mapping.files)
for (const entry of entries) {
const sourcePath = path.join(oldPath, entry.relativePath)
const destinationPath = path.join(newPath, entry.relativePath)
await moveDirContents(sourcePath, destinationPath)
}
for (const entry of entries) {
entry.absolutePath = path.join(newPath, entry.relativePath)
}
const oldMappingFileLocation = this.getMappingFileLocation()
this.appState.store.set(StoreKeys.FileBackupsLocation, newPath)
const result = await this.saveFilesBackupsMappingFile(mapping)
if (result === 'success') {
await deleteFile(oldMappingFileLocation)
}
private getFileBackupsMappingFilePath(backupsLocation: string): string {
return `${backupsLocation}/.settings/info.json`
}
public getFilesBackupsLocation(): Promise<string> {
return Promise.resolve(this.appState.store.get(StoreKeys.FileBackupsLocation))
private async getFileBackupsMappingFileFromDisk(backupsLocation: string): Promise<FileBackupsMapping | undefined> {
return readJSONFile<FileBackupsMapping>(this.getFileBackupsMappingFilePath(backupsLocation))
}
private getMappingFileLocation(): string {
const base = this.appState.store.get(StoreKeys.FileBackupsLocation)
return `${base}/info.json`
}
private async getMappingFileFromDisk(): Promise<FileBackupsMapping | undefined> {
return readJSONFile<FileBackupsMapping>(this.getMappingFileLocation())
}
private defaultMappingFileValue(): FileBackupsMapping {
private defaulFileBackupstMappingFileValue(): FileBackupsMapping {
return { version: FileBackupsConstantsV1.Version, files: {} }
}
async getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
const data = await this.getMappingFileFromDisk()
async getFilesBackupsMappingFile(backupsLocation: string): Promise<FileBackupsMapping> {
const data = await this.getFileBackupsMappingFileFromDisk(backupsLocation)
if (!data) {
return this.defaultMappingFileValue()
return this.defaulFileBackupstMappingFileValue()
}
for (const entry of Object.values(data.files)) {
@@ -139,23 +152,18 @@ export class FilesBackupManager implements FileBackupsDevice {
return data
}
async openFilesBackupsLocation(): Promise<void> {
const location = await this.getFilesBackupsLocation()
async openLocation(location: string): Promise<void> {
void shell.openPath(location)
}
async openFileBackup(record: FileBackupRecord): Promise<void> {
void shell.openPath(record.absolutePath)
}
async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> {
await writeJSONFile(this.getMappingFileLocation(), file)
private async saveFilesBackupsMappingFile(location: string, file: FileBackupsMapping): Promise<'success' | 'failed'> {
await writeJSONFile(this.getFileBackupsMappingFilePath(location), file)
return 'success'
}
async saveFilesBackupsFile(
location: string,
uuid: string,
metaFile: string,
downloadRequest: {
@@ -164,9 +172,7 @@ export class FilesBackupManager implements FileBackupsDevice {
url: string
},
): Promise<'success' | 'failed'> {
const backupsDir = await this.getFilesBackupsLocation()
const fileDir = `${backupsDir}/${uuid}`
const fileDir = `${location}/${uuid}`
const metaFilePath = `${fileDir}/${FileBackupsConstantsV1.MetadataFileName}`
const binaryPath = `${fileDir}/${FileBackupsConstantsV1.BinaryFileName}`
@@ -184,25 +190,24 @@ export class FilesBackupManager implements FileBackupsDevice {
const result = await downloader.run()
if (result === 'success') {
const mapping = await this.getFilesBackupsMappingFile()
const mapping = await this.getFilesBackupsMappingFile(location)
mapping.files[uuid] = {
backedUpOn: new Date(),
absolutePath: fileDir,
relativePath: uuid,
metadataFileName: FileBackupsConstantsV1.MetadataFileName,
binaryFileName: FileBackupsConstantsV1.BinaryFileName,
version: FileBackupsConstantsV1.Version,
}
await this.saveFilesBackupsMappingFile(mapping)
await this.saveFilesBackupsMappingFile(location, mapping)
}
return result
}
async getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
const operation = new FileReadOperation(record)
async getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken> {
const operation = new FileReadOperation(filePath)
this.readOperations.set(operation.token, operation)
@@ -224,4 +229,180 @@ export class FilesBackupManager implements FileBackupsDevice {
return result
}
async getTextBackupsCount(location: string): Promise<number> {
let files = await fs.readdir(location)
files = files.filter((fileName) => fileName.endsWith(TextBackupFileExtension))
return files.length
}
async saveTextBackupData(location: string, data: string): Promise<void> {
log(LoggingDomain.Backups, 'Saving text backup data', 'to', location)
let success: boolean
try {
await ensureDirectoryExists(location)
const name = `${new Date().toISOString().replace(/:/g, '-')}${TextBackupFileExtension}`
const filePath = path.join(location, name)
await fs.writeFile(filePath, data)
success = true
} catch (err) {
success = false
console.error('An error occurred saving backup file', err)
}
log(LoggingDomain.Backups, 'Finished saving text backup data', { success })
}
async copyDecryptScript(location: string) {
try {
await ensureDirectoryExists(location)
await fs.copyFile(Paths.decryptScript, path.join(location, path.basename(Paths.decryptScript)))
} catch (error) {
console.error(error)
}
}
private getPlaintextMappingFilePath(location: string): string {
return `${location}/.settings/info.json`
}
private async getPlaintextMappingFileFromDisk(location: string): Promise<PlaintextBackupsMapping | undefined> {
return readJSONFile<PlaintextBackupsMapping>(this.getPlaintextMappingFilePath(location))
}
private async savePlaintextBackupsMappingFile(
location: string,
file: PlaintextBackupsMapping,
): Promise<'success' | 'failed'> {
await writeJSONFile(this.getPlaintextMappingFilePath(location), file)
return 'success'
}
private defaultPlaintextMappingFileValue(): PlaintextBackupsMapping {
return { version: '1.0', files: {} }
}
async getPlaintextBackupsMappingFile(location: string): Promise<PlaintextBackupsMapping> {
if (this.plaintextMappingCache) {
return this.plaintextMappingCache
}
let data = await this.getPlaintextMappingFileFromDisk(location)
if (!data) {
data = this.defaultPlaintextMappingFileValue()
}
this.plaintextMappingCache = data
return data
}
async savePlaintextNoteBackup(
location: string,
uuid: string,
name: string,
tags: string[],
data: string,
): Promise<void> {
log(LoggingDomain.Backups, 'Saving plaintext note backup', uuid, 'to', location)
const mapping = await this.getPlaintextBackupsMappingFile(location)
if (!mapping.files[uuid]) {
mapping.files[uuid] = []
}
const removeNoteFromAllDirectories = async () => {
const records = mapping.files[uuid]
for (const record of records) {
const filePath = path.join(location, record.path)
await deleteFileIfExists(filePath)
}
mapping.files[uuid] = []
}
await removeNoteFromAllDirectories()
const writeFileToPath = async (absolutePath: string, filename: string, data: string, forTag?: string) => {
const findMappingRecord = (tag?: string) => {
const records = mapping.files[uuid]
return records.find((record) => record.tag === tag)
}
await ensureDirectoryExists(absolutePath)
const relativePath = forTag ?? ''
const filenameWithSlashesEscaped = filename.replace(/\//g, '\u2215')
const fileAbsolutePath = path.join(absolutePath, relativePath, filenameWithSlashesEscaped)
await writeFile(fileAbsolutePath, data)
const existingRecord = findMappingRecord(forTag)
if (!existingRecord) {
mapping.files[uuid].push({
tag: forTag,
path: path.join(relativePath, filename),
})
} else {
existingRecord.path = path.join(relativePath, filename)
existingRecord.tag = forTag
}
}
const uuidPart = uuid.split('-')[0]
const condensedUuidPart = uuidPart.substring(0, 4)
if (tags.length === 0) {
await writeFileToPath(location, `${name}-${condensedUuidPart}.txt`, data)
} else {
for (const tag of tags) {
await writeFileToPath(location, `${name}-${condensedUuidPart}.txt`, data, tag)
}
}
}
async persistPlaintextBackupsMappingFile(location: string): Promise<void> {
if (!this.plaintextMappingCache) {
return
}
await this.savePlaintextBackupsMappingFile(location, this.plaintextMappingCache)
}
async monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise<void> {
const FEATURE_ENABLED = false
if (!FEATURE_ENABLED) {
return
}
try {
const watcher = fs.watch(backupsDirectory, { recursive: true })
for await (const event of watcher) {
const { eventType, filename } = event
if (eventType !== 'change' && eventType !== 'rename') {
continue
}
const itemUuid = await this.findUuidForPlaintextBackupFileName(backupsDirectory, filename)
if (itemUuid) {
try {
const change: DesktopWatchedDirectoriesChange = {
itemUuid,
path: path.join(backupsDirectory, filename),
type: eventType,
content: await fs.readFile(path.join(backupsDirectory, filename), 'utf-8'),
}
this.webContents.send(MessageToWebApp.WatchedDirectoriesChanges, [change])
} catch (err) {
log(LoggingDomain.Backups, 'Error processing watched change', err)
continue
}
}
}
} catch (err) {
if ((err as Error).name === 'AbortError') {
return
}
throw err
}
}
}
@@ -1,6 +1,5 @@
import { FileBackupReadChunkResponse, FileBackupRecord } from '@web/Application/Device/DesktopSnjsExports'
import { FileBackupReadChunkResponse } from '@web/Application/Device/DesktopSnjsExports'
import fs from 'fs'
import path from 'path'
const ONE_MB = 1024 * 1024
const CHUNK_LIMIT = ONE_MB * 5
@@ -11,9 +10,9 @@ export class FileReadOperation {
private localFileId: number
private fileLength: number
constructor(backupRecord: FileBackupRecord) {
this.token = backupRecord.absolutePath
this.localFileId = fs.openSync(path.join(backupRecord.absolutePath, backupRecord.binaryFileName), 'r')
constructor(filePath: string) {
this.token = filePath
this.localFileId = fs.openSync(filePath, 'r')
this.fileLength = fs.fstatSync(this.localFileId).size
}
@@ -20,7 +20,6 @@ import { checkForUpdate, openChangelog, showUpdateInstallationDialog } from '../
import { handleTestMessage } from '../Utils/Testing'
import { isDev, isTesting } from '../Utils/Utils'
import { MessageType } from './../../../../test/TestIpcMessage'
import { BackupsManagerInterface } from './../Backups/BackupsManagerInterface'
import { SpellcheckerManager } from './../SpellcheckerManager'
import { MenuManagerInterface } from './MenuManagerInterface'
@@ -112,14 +111,12 @@ function suggestionsMenu(
export function createMenuManager({
window,
appState,
backupsManager,
trayManager,
store,
spellcheckerManager,
}: {
window: Electron.BrowserWindow
appState: AppState
backupsManager: BackupsManagerInterface
trayManager: TrayManager
store: Store
spellcheckerManager?: SpellcheckerManager
@@ -167,7 +164,6 @@ export function createMenuManager({
editMenu(spellcheckerManager, reload),
viewMenu(window, store, reload),
windowMenu(store, trayManager, reload),
backupsMenu(backupsManager, reload),
updateMenu(window, appState),
...(isLinux() ? [keyringMenu(window, store)] : []),
helpMenu(window, shell),
@@ -468,34 +464,6 @@ function minimizeToTrayItem(store: Store, trayManager: TrayManager, reload: () =
}
}
function backupsMenu(archiveManager: BackupsManagerInterface, reload: () => any) {
return {
label: str().backups,
submenu: [
{
label: archiveManager.backupsAreEnabled ? str().disableAutomaticBackups : str().enableAutomaticBackups,
click() {
archiveManager.toggleBackupsStatus()
reload()
},
},
Separator,
{
label: str().changeBackupsLocation,
click() {
archiveManager.changeBackupsLocation()
},
},
{
label: str().openBackupsLocation,
click() {
void shell.openPath(archiveManager.backupsLocation)
},
},
],
}
}
function updateMenu(window: BrowserWindow, appState: AppState) {
const updateState = appState.updates
let label
@@ -8,12 +8,11 @@ const rendererPath = path.join('file://', __dirname, '/renderer.js')
import {
FileBackupsDevice,
FileBackupsMapping,
FileBackupRecord,
FileBackupReadToken,
FileBackupReadChunkResponse,
PlaintextBackupsMapping,
} from '@web/Application/Device/DesktopSnjsExports'
import { app, BrowserWindow } from 'electron'
import { BackupsManagerInterface } from '../Backups/BackupsManagerInterface'
import { KeychainInterface } from '../Keychain/KeychainInterface'
import { MenuManagerInterface } from '../Menus/MenuManagerInterface'
import { Component, PackageManagerInterface } from '../Packages/PackageManagerInterface'
@@ -29,7 +28,6 @@ export class RemoteBridge implements CrossProcessBridge {
constructor(
private window: BrowserWindow,
private keychain: KeychainInterface,
private backups: BackupsManagerInterface,
private packages: PackageManagerInterface,
private search: SearchManagerInterface,
private data: RemoteDataInterface,
@@ -54,28 +52,30 @@ export class RemoteBridge implements CrossProcessBridge {
getKeychainValue: this.getKeychainValue.bind(this),
setKeychainValue: this.setKeychainValue.bind(this),
clearKeychainValue: this.clearKeychainValue.bind(this),
localBackupsCount: this.localBackupsCount.bind(this),
viewlocalBackups: this.viewlocalBackups.bind(this),
deleteLocalBackups: this.deleteLocalBackups.bind(this),
displayAppMenu: this.displayAppMenu.bind(this),
saveDataBackup: this.saveDataBackup.bind(this),
syncComponents: this.syncComponents.bind(this),
onMajorDataChange: this.onMajorDataChange.bind(this),
onSearch: this.onSearch.bind(this),
onInitialDataLoad: this.onInitialDataLoad.bind(this),
destroyAllData: this.destroyAllData.bind(this),
getFilesBackupsMappingFile: this.getFilesBackupsMappingFile.bind(this),
saveFilesBackupsFile: this.saveFilesBackupsFile.bind(this),
isFilesBackupsEnabled: this.isFilesBackupsEnabled.bind(this),
enableFilesBackups: this.enableFilesBackups.bind(this),
disableFilesBackups: this.disableFilesBackups.bind(this),
changeFilesBackupsLocation: this.changeFilesBackupsLocation.bind(this),
getFilesBackupsLocation: this.getFilesBackupsLocation.bind(this),
openFilesBackupsLocation: this.openFilesBackupsLocation.bind(this),
openFileBackup: this.openFileBackup.bind(this),
isLegacyFilesBackupsEnabled: this.isLegacyFilesBackupsEnabled.bind(this),
getLegacyFilesBackupsLocation: this.getLegacyFilesBackupsLocation.bind(this),
getFileBackupReadToken: this.getFileBackupReadToken.bind(this),
readNextChunk: this.readNextChunk.bind(this),
askForMediaAccess: this.askForMediaAccess.bind(this),
wasLegacyTextBackupsExplicitlyDisabled: this.wasLegacyTextBackupsExplicitlyDisabled.bind(this),
getLegacyTextBackupsLocation: this.getLegacyTextBackupsLocation.bind(this),
saveTextBackupData: this.saveTextBackupData.bind(this),
savePlaintextNoteBackup: this.savePlaintextNoteBackup.bind(this),
openLocation: this.openLocation.bind(this),
presentDirectoryPickerForLocationChangeAndTransferOld:
this.presentDirectoryPickerForLocationChangeAndTransferOld.bind(this),
getPlaintextBackupsMappingFile: this.getPlaintextBackupsMappingFile.bind(this),
persistPlaintextBackupsMappingFile: this.persistPlaintextBackupsMappingFile.bind(this),
getTextBackupsCount: this.getTextBackupsCount.bind(this),
migrateLegacyFileBackupsToNewStructure: this.migrateLegacyFileBackupsToNewStructure.bind(this),
getUserDocumentsDirectory: this.getUserDocumentsDirectory.bind(this),
monitorPlaintextBackupsLocationForChanges: this.monitorPlaintextBackupsLocationForChanges.bind(this),
}
}
@@ -135,51 +135,28 @@ export class RemoteBridge implements CrossProcessBridge {
return this.keychain.clearKeychainValue()
}
async localBackupsCount() {
return this.backups.backupsCount()
}
viewlocalBackups() {
this.backups.viewBackups()
}
async deleteLocalBackups() {
return this.backups.deleteBackups()
}
syncComponents(components: Component[]) {
void this.packages.syncComponents(components)
}
onMajorDataChange() {
this.backups.performBackup()
}
onSearch(text: string) {
this.search.findInPage(text)
}
onInitialDataLoad() {
this.backups.beginBackups()
}
destroyAllData() {
this.data.destroySensitiveDirectories()
}
saveDataBackup(data: unknown) {
this.backups.saveBackupData(data)
}
displayAppMenu() {
this.menus.popupMenu()
}
getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
return this.fileBackups.getFilesBackupsMappingFile()
getFilesBackupsMappingFile(location: string): Promise<FileBackupsMapping> {
return this.fileBackups.getFilesBackupsMappingFile(location)
}
saveFilesBackupsFile(
location: string,
uuid: string,
metaFile: string,
downloadRequest: {
@@ -188,43 +165,74 @@ export class RemoteBridge implements CrossProcessBridge {
url: string
},
): Promise<'success' | 'failed'> {
return this.fileBackups.saveFilesBackupsFile(uuid, metaFile, downloadRequest)
return this.fileBackups.saveFilesBackupsFile(location, uuid, metaFile, downloadRequest)
}
getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
return this.fileBackups.getFileBackupReadToken(record)
getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken> {
return this.fileBackups.getFileBackupReadToken(filePath)
}
readNextChunk(nextToken: string): Promise<FileBackupReadChunkResponse> {
return this.fileBackups.readNextChunk(nextToken)
}
public isFilesBackupsEnabled(): Promise<boolean> {
return this.fileBackups.isFilesBackupsEnabled()
public isLegacyFilesBackupsEnabled(): Promise<boolean> {
return this.fileBackups.isLegacyFilesBackupsEnabled()
}
public enableFilesBackups(): Promise<void> {
return this.fileBackups.enableFilesBackups()
public getLegacyFilesBackupsLocation(): Promise<string | undefined> {
return this.fileBackups.getLegacyFilesBackupsLocation()
}
public disableFilesBackups(): Promise<void> {
return this.fileBackups.disableFilesBackups()
wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean> {
return this.fileBackups.wasLegacyTextBackupsExplicitlyDisabled()
}
public changeFilesBackupsLocation(): Promise<string | undefined> {
return this.fileBackups.changeFilesBackupsLocation()
getLegacyTextBackupsLocation(): Promise<string | undefined> {
return this.fileBackups.getLegacyTextBackupsLocation()
}
public getFilesBackupsLocation(): Promise<string> {
return this.fileBackups.getFilesBackupsLocation()
saveTextBackupData(location: string, data: string): Promise<void> {
return this.fileBackups.saveTextBackupData(location, data)
}
public openFilesBackupsLocation(): Promise<void> {
return this.fileBackups.openFilesBackupsLocation()
savePlaintextNoteBackup(location: string, uuid: string, name: string, tags: string[], data: string): Promise<void> {
return this.fileBackups.savePlaintextNoteBackup(location, uuid, name, tags, data)
}
public openFileBackup(record: FileBackupRecord): Promise<void> {
return this.fileBackups.openFileBackup(record)
openLocation(path: string): Promise<void> {
return this.fileBackups.openLocation(path)
}
presentDirectoryPickerForLocationChangeAndTransferOld(
appendPath: string,
oldLocation?: string | undefined,
): Promise<string | undefined> {
return this.fileBackups.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation)
}
getPlaintextBackupsMappingFile(location: string): Promise<PlaintextBackupsMapping> {
return this.fileBackups.getPlaintextBackupsMappingFile(location)
}
persistPlaintextBackupsMappingFile(location: string): Promise<void> {
return this.fileBackups.persistPlaintextBackupsMappingFile(location)
}
getTextBackupsCount(location: string): Promise<number> {
return this.fileBackups.getTextBackupsCount(location)
}
migrateLegacyFileBackupsToNewStructure(newPath: string): Promise<void> {
return this.fileBackups.migrateLegacyFileBackupsToNewStructure(newPath)
}
getUserDocumentsDirectory(): Promise<string> {
return this.fileBackups.getUserDocumentsDirectory()
}
monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise<void> {
return this.fileBackups.monitorPlaintextBackupsLocationForChanges(backupsDirectory)
}
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean> {
@@ -156,7 +156,7 @@ export function createSpellcheckerManager(
const firstOutlier = session.availableSpellCheckerLanguages.find(
(language, index) => availableSpellCheckerLanguages[index] !== language,
)
throw new Error(`Found unsupported language code: ${firstOutlier}`)
console.error(`Found unsupported language code: ${firstOutlier}`)
}
setSpellcheckerLanguages()
@@ -4,30 +4,34 @@ export enum StoreKeys {
ExtServerHost = 'extServerHost',
UseSystemMenuBar = 'useSystemMenuBar',
MenuBarVisible = 'isMenuBarVisible',
BackupsLocation = 'backupsLocation',
BackupsDisabled = 'backupsDisabled',
MinimizeToTray = 'minimizeToTray',
EnableAutoUpdate = 'enableAutoUpdates',
ZoomFactor = 'zoomFactor',
SelectedSpellCheckerLanguageCodes = 'selectedSpellCheckerLanguageCodes',
UseNativeKeychain = 'useNativeKeychain',
FileBackupsEnabled = 'fileBackupsEnabled',
FileBackupsLocation = 'fileBackupsLocation',
LastRunVersion = 'LastRunVersion',
LegacyTextBackupsLocation = 'backupsLocation',
LegacyTextBackupsDisabled = 'backupsDisabled',
LegacyFileBackupsEnabled = 'fileBackupsEnabled',
LegacyFileBackupsLocation = 'fileBackupsLocation',
}
export interface StoreData {
[StoreKeys.ExtServerHost]: string
[StoreKeys.UseSystemMenuBar]: boolean
[StoreKeys.MenuBarVisible]: boolean
[StoreKeys.BackupsLocation]: string
[StoreKeys.BackupsDisabled]: boolean
[StoreKeys.MinimizeToTray]: boolean
[StoreKeys.EnableAutoUpdate]: boolean
[StoreKeys.UseNativeKeychain]: boolean | null
[StoreKeys.ZoomFactor]: number
[StoreKeys.SelectedSpellCheckerLanguageCodes]: Set<Language> | null
[StoreKeys.FileBackupsEnabled]: boolean
[StoreKeys.FileBackupsLocation]: string
[StoreKeys.LastRunVersion]: string
[StoreKeys.LegacyTextBackupsLocation]: string
[StoreKeys.LegacyTextBackupsDisabled]: boolean
[StoreKeys.LegacyFileBackupsEnabled]: boolean
[StoreKeys.LegacyFileBackupsLocation]: string
}
@@ -1,31 +1,30 @@
import fs from 'fs'
import path from 'path'
import { BackupsDirectoryName } from '../Backups/BackupsManager'
import { Language } from '../SpellcheckerManager'
import { FileDoesNotExist } from '../Utils/FileUtils'
import { ensureIsBoolean, isBoolean, isDev, isTesting } from '../Utils/Utils'
import { ensureIsBoolean, isBoolean } from '../Utils/Utils'
import { StoreData, StoreKeys } from './StoreKeys'
import { app, logError } from './Store'
import { logError } from './Store'
export function createSanitizedStoreData(data: any = {}): StoreData {
return {
[StoreKeys.MenuBarVisible]: ensureIsBoolean(data[StoreKeys.MenuBarVisible], true),
[StoreKeys.UseSystemMenuBar]: ensureIsBoolean(data[StoreKeys.UseSystemMenuBar], false),
[StoreKeys.BackupsDisabled]: ensureIsBoolean(data[StoreKeys.BackupsDisabled], false),
[StoreKeys.MinimizeToTray]: ensureIsBoolean(data[StoreKeys.MinimizeToTray], false),
[StoreKeys.EnableAutoUpdate]: ensureIsBoolean(data[StoreKeys.EnableAutoUpdate], true),
[StoreKeys.UseNativeKeychain]: isBoolean(data[StoreKeys.UseNativeKeychain])
? data[StoreKeys.UseNativeKeychain]
: null,
[StoreKeys.ExtServerHost]: data[StoreKeys.ExtServerHost],
[StoreKeys.BackupsLocation]: sanitizeBackupsLocation(data[StoreKeys.BackupsLocation]),
[StoreKeys.ZoomFactor]: sanitizeZoomFactor(data[StoreKeys.ZoomFactor]),
[StoreKeys.SelectedSpellCheckerLanguageCodes]: sanitizeSpellCheckerLanguageCodes(
data[StoreKeys.SelectedSpellCheckerLanguageCodes],
),
[StoreKeys.FileBackupsEnabled]: ensureIsBoolean(data[StoreKeys.FileBackupsEnabled], false),
[StoreKeys.FileBackupsLocation]: data[StoreKeys.FileBackupsLocation],
[StoreKeys.LastRunVersion]: data[StoreKeys.LastRunVersion],
[StoreKeys.LegacyTextBackupsLocation]: data[StoreKeys.LegacyTextBackupsLocation],
[StoreKeys.LegacyTextBackupsDisabled]: data[StoreKeys.LegacyTextBackupsDisabled],
[StoreKeys.LegacyFileBackupsEnabled]: data[StoreKeys.LegacyFileBackupsEnabled],
[StoreKeys.LegacyFileBackupsLocation]: data[StoreKeys.LegacyFileBackupsLocation],
}
}
function sanitizeZoomFactor(factor?: any): number {
@@ -35,29 +34,7 @@ function sanitizeZoomFactor(factor?: any): number {
return 1
}
}
function sanitizeBackupsLocation(location?: unknown): string {
const defaultPath = path.join(
isTesting() ? app.getPath('userData') : isDev() ? app.getPath('documents') : app.getPath('home'),
BackupsDirectoryName,
)
if (typeof location !== 'string') {
return defaultPath
}
try {
const stat = fs.lstatSync(location)
if (stat.isDirectory()) {
return location
}
/** Path points to something other than a directory */
return defaultPath
} catch (e) {
/** Path does not point to a valid directory */
logError(e)
return defaultPath
}
}
function sanitizeSpellCheckerLanguageCodes(languages?: unknown): Set<Language> | null {
if (!languages) {
return null
@@ -13,8 +13,6 @@ export function createEnglishStrings(): Strings {
automaticUpdatesDisabled: 'Automatic Updates Disabled',
disableAutomaticBackups: 'Disable Automatic Backups',
enableAutomaticBackups: 'Enable Automatic Backups',
changeBackupsLocation: 'Change Backups Location',
openBackupsLocation: 'Open Backups Location',
emailSupport: 'Email Support',
website: 'Website',
gitHub: 'GitHub',
@@ -146,15 +144,5 @@ export function createEnglishStrings(): Strings {
},
unknownVersionName: 'Unknown',
},
backups: {
errorChangingDirectory(error: any): string {
return (
'An error occurred while changing your backups directory. ' +
'If this issue persists, please contact support with the following ' +
'information: \n' +
JSON.stringify(error)
)
},
},
}
}
@@ -28,15 +28,5 @@ export function createFrenchStrings(): Strings {
},
extensions: fallback.extensions,
updates: fallback.updates,
backups: {
errorChangingDirectory(error: any): string {
return (
"Une erreur s'est produite lors du déplacement du dossier de " +
'sauvegardes. Si le problème est récurrent, contactez le support ' +
'technique (en anglais) avec les informations suivantes:\n' +
JSON.stringify(error)
)
},
},
}
}
@@ -53,10 +53,6 @@ export function updates() {
return str().updates
}
export function backups() {
return str().backups
}
function stringsForLocale(locale: string): Strings {
if (locale === 'en' || locale.startsWith('en-')) {
return createEnglishStrings()
@@ -4,7 +4,6 @@ export interface Strings {
tray: TrayStrings
extensions: ExtensionsStrings
updates: UpdateStrings
backups: BackupsStrings
}
interface AppMenuStrings {
@@ -18,8 +17,6 @@ interface AppMenuStrings {
automaticUpdatesDisabled: string
disableAutomaticBackups: string
enableAutomaticBackups: string
changeBackupsLocation: string
openBackupsLocation: string
emailSupport: string
website: string
gitHub: string
@@ -103,7 +100,3 @@ interface UpdateStrings {
}
unknownVersionName: string
}
interface BackupsStrings {
errorChangingDirectory(error: any): string
}
@@ -36,6 +36,9 @@ export const Paths = {
get userDataDir(): string {
return app.getPath('userData')
},
get homeDir(): string {
return app.getPath('home')
},
get documentsDir(): string {
return app.getPath('documents')
},
@@ -2,11 +2,10 @@ import { compareVersions } from 'compare-versions'
import { BrowserWindow, dialog, shell } from 'electron'
import electronLog from 'electron-log'
import { autoUpdater } from 'electron-updater'
import { action, autorun, computed, makeObservable, observable } from 'mobx'
import { action, computed, makeObservable, observable } from 'mobx'
import { MessageType } from '../../../test/TestIpcMessage'
import { AppState } from '../../AppState'
import { MessageToWebApp } from '../Shared/IpcMessages'
import { BackupsManagerInterface } from './Backups/BackupsManagerInterface'
import { StoreKeys } from './Store/StoreKeys'
import { updates as str } from './Strings'
import { autoUpdatingAvailable } from './Types/Constants'
@@ -84,7 +83,7 @@ export class UpdateState {
let updatesSetup = false
export function setupUpdates(window: BrowserWindow, appState: AppState, backupsManager: BackupsManagerInterface): void {
export function setupUpdates(window: BrowserWindow, appState: AppState): void {
if (!autoUpdatingAvailable) {
return
}
@@ -97,22 +96,6 @@ export function setupUpdates(window: BrowserWindow, appState: AppState, backupsM
const updateState = appState.updates
function checkUpdateSafety(): boolean {
let canUpdate: boolean
if (appState.store.get(StoreKeys.BackupsDisabled)) {
canUpdate = true
} else {
canUpdate = updateState.enableAutoUpdate && isLessThanOneHourFromNow(appState.lastBackupDate)
}
autoUpdater.autoInstallOnAppQuit = canUpdate
autoUpdater.autoDownload = canUpdate
return canUpdate
}
autorun(checkUpdateSafety)
const oneHour = 1 * 60 * 60 * 1000
setInterval(checkUpdateSafety, oneHour)
autoUpdater.on('update-downloaded', (info: { version?: string }) => {
window.webContents.send(MessageToWebApp.UpdateAvailable, null)
updateState.autoUpdateHasBeenDownloaded(info.version || null)
@@ -122,10 +105,9 @@ export function setupUpdates(window: BrowserWindow, appState: AppState, backupsM
autoUpdater.on(MessageToWebApp.UpdateAvailable, (info: { version?: string }) => {
updateState.checkedForUpdate(info.version || null)
if (updateState.enableAutoUpdate) {
const canUpdate = checkUpdateSafety()
if (!canUpdate) {
backupsManager.performBackup()
}
const canUpdate = updateState.enableAutoUpdate
autoUpdater.autoInstallOnAppQuit = canUpdate
autoUpdater.autoDownload = canUpdate
}
})
autoUpdater.on('update-not-available', (info: { version?: string }) => {
@@ -164,46 +146,21 @@ function quitAndInstall(window: BrowserWindow) {
}, 0)
}
function isLessThanOneHourFromNow(date: number | null) {
const now = Date.now()
const onHourMs = 1 * 60 * 60 * 1000
return now - (date ?? 0) < onHourMs
}
export async function showUpdateInstallationDialog(parentWindow: BrowserWindow, appState: AppState): Promise<void> {
if (!appState.updates.latestVersion) {
return
}
if (appState.lastBackupDate && isLessThanOneHourFromNow(appState.lastBackupDate)) {
const result = await dialog.showMessageBox(parentWindow, {
type: 'info',
title: str().updateReady.title,
message: str().updateReady.message(appState.updates.latestVersion),
buttons: [str().updateReady.installLater, str().updateReady.quitAndInstall],
cancelId: 0,
})
const result = await dialog.showMessageBox(parentWindow, {
type: 'info',
title: str().updateReady.title,
message: str().updateReady.message(appState.updates.latestVersion),
buttons: [str().updateReady.installLater, str().updateReady.quitAndInstall],
cancelId: 0,
})
const buttonIndex = result.response
if (buttonIndex === 1) {
quitAndInstall(parentWindow)
}
} else {
const cancelId = 0
const result = await dialog.showMessageBox({
type: 'warning',
title: str().updateReady.title,
message: str().updateReady.noRecentBackupMessage,
detail: str().updateReady.noRecentBackupDetail(appState.lastBackupDate),
checkboxLabel: str().updateReady.noRecentBackupChecbox,
checkboxChecked: false,
buttons: [str().updateReady.installLater, str().updateReady.quitAndInstall],
cancelId,
})
if (!result.checkboxChecked || result.response === cancelId) {
return
}
const buttonIndex = result.response
if (buttonIndex === 1) {
quitAndInstall(parentWindow)
}
}
@@ -27,9 +27,10 @@ export function debouncedJSONDiskWriter(durationMs: number, location: string, da
}, durationMs)
}
export async function openDirectoryPicker(): Promise<string | undefined> {
export async function openDirectoryPicker(buttonLabel?: string): Promise<string | undefined> {
const result = await dialog.showOpenDialog({
properties: ['openDirectory', 'showHiddenFiles', 'createDirectory'],
buttonLabel: buttonLabel,
})
return result.filePaths[0]
@@ -63,6 +64,7 @@ export function writeJSONFileSync(filepath: string, data: unknown): void {
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8')
}
/** Creates the directory if it doesn't exist. */
export async function ensureDirectoryExists(dirPath: string): Promise<void> {
try {
const stat = await fs.promises.lstat(dirPath)
@@ -251,7 +253,7 @@ export async function moveFiles(sources: string[], destDir: string): Promise<voi
return Promise.all(sources.map((fileName) => moveFile(fileName, path.join(destDir, path.basename(fileName)))))
}
async function moveFile(source: PathLike, destination: PathLike) {
export async function moveFile(source: PathLike, destination: PathLike) {
try {
await fs.promises.rename(source, destination)
} catch (_error) {
@@ -261,6 +263,14 @@ async function moveFile(source: PathLike, destination: PathLike) {
}
}
export async function deleteFileIfExists(filePath: PathLike): Promise<void> {
try {
await deleteFile(filePath)
} catch {
return
}
}
/** Deletes a file, handling EPERM and EBUSY errors on Windows. */
export async function deleteFile(filePath: PathLike): Promise<void> {
for (let i = 1, maxTries = 10; i < maxTries; i++) {
@@ -6,8 +6,6 @@ import path from 'path'
import { AppMessageType, MessageType } from '../../../test/TestIpcMessage'
import { AppState } from '../../AppState'
import { MessageToWebApp } from '../Shared/IpcMessages'
import { createBackupsManager } from './Backups/BackupsManager'
import { BackupsManagerInterface } from './Backups/BackupsManagerInterface'
import { FilesBackupManager } from './FileBackups/FileBackupsManager'
import { Keychain } from './Keychain/Keychain'
import { MediaManager } from './Media/MediaManager'
@@ -35,7 +33,6 @@ const WINDOW_MIN_HEIGHT = 400
export interface WindowState {
window: Electron.BrowserWindow
menuManager: MenuManagerInterface
backupsManager: BackupsManagerInterface
trayManager: TrayManager
}
@@ -64,7 +61,6 @@ export async function createWindowState({
;(global as any).RemoteBridge = new RemoteBridge(
window,
Keychain,
services.backupsManager,
services.packageManager,
services.searchManager,
{
@@ -93,7 +89,6 @@ export async function createWindowState({
window.on('blur', () => {
window.webContents.send(MessageToWebApp.WindowBlurred, null)
services.backupsManager.applicationDidBlur()
})
window.once('ready-to-show', () => {
@@ -201,8 +196,7 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
const searchManager = initializeSearchManager(window.webContents)
initializeZoomManager(window, appState.store)
const backupsManager = createBackupsManager(window.webContents, appState)
const updateManager = setupUpdates(window, appState, backupsManager)
const updateManager = setupUpdates(window, appState)
const trayManager = createTrayManager(window, appState.store)
const spellcheckerManager = createSpellcheckerManager(appState.store, window.webContents, appLocale)
const mediaManager = new MediaManager()
@@ -214,16 +208,14 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
const menuManager = createMenuManager({
appState,
window,
backupsManager,
trayManager,
store: appState.store,
spellcheckerManager,
})
const fileBackupsManager = new FilesBackupManager(appState)
const fileBackupsManager = new FilesBackupManager(appState, window.webContents)
return {
backupsManager,
updateManager,
trayManager,
spellcheckerManager,
@@ -3,52 +3,22 @@ import { Component } from '../Main/Packages/PackageManagerInterface'
export interface CrossProcessBridge extends FileBackupsDevice {
get extServerHost(): string
get useNativeKeychain(): boolean
get rendererPath(): string
get isMacOS(): boolean
get appVersion(): string
get useSystemMenuBar(): boolean
closeWindow(): void
minimizeWindow(): void
maximizeWindow(): void
unmaximizeWindow(): void
isWindowMaximized(): boolean
getKeychainValue(): Promise<unknown>
setKeychainValue: (value: unknown) => Promise<void>
clearKeychainValue(): Promise<boolean>
localBackupsCount(): Promise<number>
viewlocalBackups(): void
deleteLocalBackups(): Promise<void>
saveDataBackup(data: unknown): void
displayAppMenu(): void
syncComponents(components: Component[]): void
onMajorDataChange(): void
onSearch(text: string): void
onInitialDataLoad(): void
destroyAllData(): void
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
}
@@ -1,11 +1,11 @@
import {
DesktopDeviceInterface,
Environment,
FileBackupsMapping,
RawKeychainValue,
FileBackupRecord,
FileBackupReadToken,
FileBackupReadChunkResponse,
FileBackupsMapping,
PlaintextBackupsMapping,
} from '@web/Application/Device/DesktopSnjsExports'
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
import { Component } from '../Main/Packages/PackageManagerInterface'
@@ -25,6 +25,33 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
super(appVersion)
}
openLocation(path: string): Promise<void> {
return this.remoteBridge.openLocation(path)
}
presentDirectoryPickerForLocationChangeAndTransferOld(
appendPath: string,
oldLocation?: string | undefined,
): Promise<string | undefined> {
return this.remoteBridge.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation)
}
getFilesBackupsMappingFile(location: string): Promise<FileBackupsMapping> {
return this.remoteBridge.getFilesBackupsMappingFile(location)
}
getPlaintextBackupsMappingFile(location: string): Promise<PlaintextBackupsMapping> {
return this.remoteBridge.getPlaintextBackupsMappingFile(location)
}
persistPlaintextBackupsMappingFile(location: string): Promise<void> {
return this.remoteBridge.persistPlaintextBackupsMappingFile(location)
}
getTextBackupsCount(location: string): Promise<number> {
return this.remoteBridge.getTextBackupsCount(location)
}
async getKeychainValue() {
if (this.useNativeKeychain) {
const keychainValue = await this.remoteBridge.getKeychainValue()
@@ -57,18 +84,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
this.remoteBridge.syncComponents(components)
}
onMajorDataChange() {
this.remoteBridge.onMajorDataChange()
}
onSearch(text: string) {
this.remoteBridge.onSearch(text)
}
onInitialDataLoad() {
this.remoteBridge.onInitialDataLoad()
}
async clearAllDataFromDevice(workspaceIdentifiers: string[]): Promise<{ killsApplication: boolean }> {
await super.clearAllDataFromDevice(workspaceIdentifiers)
@@ -77,69 +96,36 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
return { killsApplication: true }
}
async downloadBackup() {
const receiver = window.webClient
receiver.didBeginBackup()
try {
const data = await receiver.requestBackupFile()
if (data) {
this.remoteBridge.saveDataBackup(data)
} else {
receiver.didFinishBackup(false)
}
} catch (error) {
console.error(error)
receiver.didFinishBackup(false)
}
public isLegacyFilesBackupsEnabled(): Promise<boolean> {
return this.remoteBridge.isLegacyFilesBackupsEnabled()
}
async localBackupsCount() {
return this.remoteBridge.localBackupsCount()
public getLegacyFilesBackupsLocation(): Promise<string | undefined> {
return this.remoteBridge.getLegacyFilesBackupsLocation()
}
viewlocalBackups() {
this.remoteBridge.viewlocalBackups()
wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean> {
return this.remoteBridge.wasLegacyTextBackupsExplicitlyDisabled()
}
async deleteLocalBackups() {
return this.remoteBridge.deleteLocalBackups()
getUserDocumentsDirectory(): Promise<string> {
return this.remoteBridge.getUserDocumentsDirectory()
}
public isFilesBackupsEnabled(): Promise<boolean> {
return this.remoteBridge.isFilesBackupsEnabled()
getLegacyTextBackupsLocation(): Promise<string | undefined> {
return this.remoteBridge.getLegacyTextBackupsLocation()
}
public enableFilesBackups(): Promise<void> {
return this.remoteBridge.enableFilesBackups()
saveTextBackupData(workspaceId: string, data: string): Promise<void> {
return this.remoteBridge.saveTextBackupData(workspaceId, data)
}
public disableFilesBackups(): Promise<void> {
return this.remoteBridge.disableFilesBackups()
}
public changeFilesBackupsLocation(): Promise<string | undefined> {
return this.remoteBridge.changeFilesBackupsLocation()
}
public getFilesBackupsLocation(): Promise<string> {
return this.remoteBridge.getFilesBackupsLocation()
}
async getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
return this.remoteBridge.getFilesBackupsMappingFile()
}
async openFilesBackupsLocation(): Promise<void> {
return this.remoteBridge.openFilesBackupsLocation()
}
openFileBackup(record: FileBackupRecord): Promise<void> {
return this.remoteBridge.openFileBackup(record)
savePlaintextNoteBackup(location: string, uuid: string, name: string, tags: string[], data: string): Promise<void> {
return this.remoteBridge.savePlaintextNoteBackup(location, uuid, name, tags, data)
}
async saveFilesBackupsFile(
location: string,
uuid: string,
metaFile: string,
downloadRequest: {
@@ -148,17 +134,25 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
url: string
},
): Promise<'success' | 'failed'> {
return this.remoteBridge.saveFilesBackupsFile(uuid, metaFile, downloadRequest)
return this.remoteBridge.saveFilesBackupsFile(location, uuid, metaFile, downloadRequest)
}
getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
return this.remoteBridge.getFileBackupReadToken(record)
getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken> {
return this.remoteBridge.getFileBackupReadToken(filePath)
}
migrateLegacyFileBackupsToNewStructure(newPath: string): Promise<void> {
return this.remoteBridge.migrateLegacyFileBackupsToNewStructure(newPath)
}
readNextChunk(token: string): Promise<FileBackupReadChunkResponse> {
return this.remoteBridge.readNextChunk(token)
}
monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise<void> {
return this.remoteBridge.monitorPlaintextBackupsLocationForChanges(backupsDirectory)
}
async performHardReset(): Promise<void> {
console.error('performHardReset is not yet implemented')
}
@@ -1,28 +1,25 @@
import { IpcRendererEvent } from 'electron/renderer'
import { MessageToWebApp } from '../Shared/IpcMessages'
import { ElectronMainEvents, MainEventHandler } from '../Shared/ElectronMainEvents'
const { ipcRenderer } = require('electron')
const RemoteBridge = require('@electron/remote').getGlobal('RemoteBridge')
const { contextBridge } = require('electron')
type MainEventCallback = (event: IpcRendererEvent, value: any) => void
process.once('loaded', function () {
contextBridge.exposeInMainWorld('electronRemoteBridge', RemoteBridge.exposableValue)
contextBridge.exposeInMainWorld('electronMainEvents', {
handleUpdateAvailable: (callback: MainEventCallback) => ipcRenderer.on(MessageToWebApp.UpdateAvailable, callback),
const mainEvents: ElectronMainEvents = {
setUpdateAvailableHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.UpdateAvailable, handler),
handlePerformAutomatedBackup: (callback: MainEventCallback) =>
ipcRenderer.on(MessageToWebApp.PerformAutomatedBackup, callback),
setWindowBlurredHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.WindowBlurred, handler),
handleFinishedSavingBackup: (callback: MainEventCallback) =>
ipcRenderer.on(MessageToWebApp.FinishedSavingBackup, callback),
setWindowFocusedHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.WindowFocused, handler),
handleWindowBlurred: (callback: MainEventCallback) => ipcRenderer.on(MessageToWebApp.WindowBlurred, callback),
setWatchedDirectoriesChangeHandler: (handler: MainEventHandler) =>
ipcRenderer.on(MessageToWebApp.WatchedDirectoriesChanges, handler),
handleWindowFocused: (callback: MainEventCallback) => ipcRenderer.on(MessageToWebApp.WindowFocused, callback),
setInstallComponentCompleteHandler: (handler: MainEventHandler) =>
ipcRenderer.on(MessageToWebApp.InstallComponentComplete, handler),
}
handleInstallComponentComplete: (callback: MainEventCallback) =>
ipcRenderer.on(MessageToWebApp.InstallComponentComplete, callback),
})
contextBridge.exposeInMainWorld('electronMainEvents', mainEvents)
})
@@ -1,8 +1,12 @@
import { DesktopClientRequiresWebMethods } from '@web/Application/Device/DesktopSnjsExports'
import {
DesktopClientRequiresWebMethods,
DesktopWatchedDirectoriesChanges,
} from '@web/Application/Device/DesktopSnjsExports'
import { StartApplication } from '@web/Application/Device/StartApplication'
import { IpcRendererEvent } from 'electron/renderer'
import { CrossProcessBridge } from './CrossProcessBridge'
import { DesktopDevice } from './DesktopDevice'
import { ElectronMainEvents } from '../Shared/ElectronMainEvents'
declare const DEFAULT_SYNC_SERVER: string
declare const WEBSOCKET_URL: string
@@ -23,7 +27,7 @@ declare global {
purchaseUrl: string
startApplication: StartApplication
zip: unknown
electronMainEvents: any
electronMainEvents: ElectronMainEvents
}
}
@@ -114,29 +118,36 @@ async function configureWindow(remoteBridge: CrossProcessBridge) {
if (isMacOS || useSystemMenuBar) {
// !important is important here because #desktop-title-bar has display: flex.
sheet.insertRule('#desktop-title-bar { display: none !important; }', sheet.cssRules.length)
} else {
/* Use custom title bar. Take the sn-titlebar-height off of
the app content height so its not overflowing */
sheet.insertRule(
'[data-dialog] { position: relative; height: calc(100vh - var(--sn-desktop-titlebar-height)) !important; margin-top: var(--sn-desktop-titlebar-height) !important; }',
sheet.cssRules.length,
)
sheet.insertRule(
'[data-mobile-popover] { padding-top: var(--sn-desktop-titlebar-height) !important; }',
sheet.cssRules.length,
)
}
}
window.electronMainEvents.handleUpdateAvailable(() => {
window.electronMainEvents.setUpdateAvailableHandler(() => {
window.webClient.updateAvailable()
})
window.electronMainEvents.handlePerformAutomatedBackup(() => {
void window.device.downloadBackup()
})
window.electronMainEvents.handleFinishedSavingBackup((_: IpcRendererEvent, data: { success: boolean }) => {
window.webClient.didFinishBackup(data.success)
})
window.electronMainEvents.handleWindowBlurred(() => {
window.electronMainEvents.setWindowBlurredHandler(() => {
window.webClient.windowLostFocus()
})
window.electronMainEvents.handleWindowFocused(() => {
window.electronMainEvents.setWindowFocusedHandler(() => {
window.webClient.windowGainedFocus()
})
window.electronMainEvents.handleInstallComponentComplete((_: IpcRendererEvent, data: any) => {
window.electronMainEvents.setInstallComponentCompleteHandler((_: IpcRendererEvent, data: any) => {
void window.webClient.onComponentInstallationComplete(data.component, undefined)
})
window.electronMainEvents.setWatchedDirectoriesChangeHandler((_: IpcRendererEvent, changes: unknown) => {
void window.webClient.handleWatchedDirectoriesChanges(changes as DesktopWatchedDirectoriesChanges)
})
@@ -0,0 +1,11 @@
import { IpcRendererEvent } from 'electron/renderer'
export type MainEventHandler = (event: IpcRendererEvent, value: unknown) => void
export interface ElectronMainEvents {
setUpdateAvailableHandler(handler: MainEventHandler): void
setWindowBlurredHandler(handler: MainEventHandler): void
setWindowFocusedHandler(handler: MainEventHandler): void
setInstallComponentCompleteHandler(handler: MainEventHandler): void
setWatchedDirectoriesChangeHandler(handler: MainEventHandler): void
}
@@ -1,10 +1,9 @@
export enum MessageToWebApp {
UpdateAvailable = 'update-available',
PerformAutomatedBackup = 'download-backup',
FinishedSavingBackup = 'finished-saving-backup',
WindowBlurred = 'window-blurred',
WindowFocused = 'window-focused',
InstallComponentComplete = 'install-component-complete',
WatchedDirectoriesChanges = 'watched-directories-changes',
}
export enum MessageToMainProcess {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.105.73",
"version": "3.106.4",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
@@ -23,7 +23,7 @@
"format": "prettier --write .",
"lint:eslint": "eslint app/index.ts app/application.ts app/javascripts/**/*.ts",
"lint:formatting": "prettier --check app",
"lint": "yarn lint:formatting && yarn lint:eslint app",
"lint": "yarn lint:formatting && yarn lint:eslint app && yarn tsc",
"tsc": "tsc --noEmit",
"release:mac": "node scripts/build.mjs mac",
"start": "electron ./app --enable-logging --icon _icon/icon.png",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.39](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.38](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.37](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.28.37",
"version": "1.28.39",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.15.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
### Bug Fixes
* automatically convert Super notes to Markdown for the Plaintext Backups feature ([75ecf4c](https://github.com/standardnotes/app/commit/75ecf4c393a6bc0ab2d6bf50dcbee21cef520746))
# [1.15.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
### Features
* Automatic plaintext backup option in Preferences > Backups will backup your notes and tags into plaintext, unencrypted folders on your computer. In addition, automatic encrypted text backups preference management has moved from the top-level menu in the desktop app to Preferences > Backups. ([#2322](https://github.com/standardnotes/app/issues/2322)) ([7e3db49](https://github.com/standardnotes/app/commit/7e3db49322034aa644c3c99ccf9a5f1b508583aa))
## [1.14.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.14.47",
"version": "1.15.1",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -0,0 +1,8 @@
export type DesktopWatchedDirectoriesChange = {
itemUuid: string
path: string
type: 'rename' | 'change'
content: string
}
export type DesktopWatchedDirectoriesChanges = DesktopWatchedDirectoriesChange[]
@@ -1,12 +1,44 @@
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
import { FileBackupRecord, FileBackupsMapping } from './FileBackupsMapping'
import { FileBackupsMapping } from './FileBackupsMapping'
type PlaintextNoteRecord = {
tag?: string
path: string
}
type UuidString = string
export type PlaintextBackupsMapping = {
version: string
files: Record<UuidString, PlaintextNoteRecord[]>
}
export interface FileBackupsDevice
extends FileBackupsMethods,
LegacyBackupsMethods,
PlaintextBackupsMethods,
TextBackupsMethods {
openLocation(path: string): Promise<void>
/**
* The reason we combine presenting a directory picker and transfering old files to the new location
* in one function is so we don't have to expose a general `transferDirectories` function to the web app,
* which would give it too much power.
* @param appendPath The path to append to the selected directory.
*/
presentDirectoryPickerForLocationChangeAndTransferOld(
appendPath: string,
oldLocation?: string,
): Promise<string | undefined>
monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise<void>
}
export type FileBackupReadToken = string
export type FileBackupReadChunkResponse = { chunk: Uint8Array; isLast: boolean; progress: FileDownloadProgress }
export interface FileBackupsDevice {
getFilesBackupsMappingFile(): Promise<FileBackupsMapping>
interface FileBackupsMethods {
getFilesBackupsMappingFile(location: string): Promise<FileBackupsMapping>
saveFilesBackupsFile(
location: string,
uuid: string,
metaFile: string,
downloadRequest: {
@@ -15,13 +47,26 @@ export interface FileBackupsDevice {
url: string
},
): Promise<'success' | 'failed'>
getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken>
getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken>
readNextChunk(token: string): Promise<FileBackupReadChunkResponse>
isFilesBackupsEnabled(): Promise<boolean>
enableFilesBackups(): Promise<void>
disableFilesBackups(): Promise<void>
changeFilesBackupsLocation(): Promise<string | undefined>
getFilesBackupsLocation(): Promise<string>
openFilesBackupsLocation(): Promise<void>
openFileBackup(record: FileBackupRecord): Promise<void>
}
interface PlaintextBackupsMethods {
getPlaintextBackupsMappingFile(location: string): Promise<PlaintextBackupsMapping>
persistPlaintextBackupsMappingFile(location: string): Promise<void>
savePlaintextNoteBackup(location: string, uuid: string, name: string, tags: string[], data: string): Promise<void>
}
interface TextBackupsMethods {
getTextBackupsCount(location: string): Promise<number>
saveTextBackupData(location: string, data: string): Promise<void>
getUserDocumentsDirectory(): Promise<string>
}
interface LegacyBackupsMethods {
migrateLegacyFileBackupsToNewStructure(newPath: string): Promise<void>
isLegacyFilesBackupsEnabled(): Promise<boolean>
getLegacyFilesBackupsLocation(): Promise<string | undefined>
wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean>
getLegacyTextBackupsLocation(): Promise<string | undefined>
}
@@ -2,7 +2,6 @@ import { FileBackupsConstantsV1 } from './FileBackupsConstantsV1'
export type FileBackupRecord = {
backedUpOn: Date
absolutePath: string
relativePath: string
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
@@ -1,14 +1,37 @@
import { OnChunkCallback } from '../Chunker/OnChunkCallback'
import { DesktopWatchedDirectoriesChanges } from '../Device/DesktopWatchedChanges'
import { FileBackupRecord } from '../Device/FileBackupsMapping'
import { SuperConverterServiceInterface } from './SuperConverterServiceInterface'
export interface BackupServiceInterface {
openAllDirectoriesContainingBackupFiles(): void
prependWorkspacePathForPath(path: string): string
importWatchedDirectoryChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void>
setSuperConverter(converter: SuperConverterServiceInterface): void
getFileBackupInfo(file: { uuid: string }): Promise<FileBackupRecord | undefined>
readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'>
isFilesBackupsEnabled(): Promise<boolean>
isFilesBackupsEnabled(): boolean
enableFilesBackups(): Promise<void>
disableFilesBackups(): Promise<void>
disableFilesBackups(): void
changeFilesBackupsLocation(): Promise<string | undefined>
getFilesBackupsLocation(): Promise<string>
getFilesBackupsLocation(): string | undefined
openFilesBackupsLocation(): Promise<void>
openFileBackup(record: FileBackupRecord): Promise<void>
getFileBackupAbsolutePath(record: FileBackupRecord): string
isTextBackupsEnabled(): boolean
enableTextBackups(): Promise<void>
disableTextBackups(): void
getTextBackupsLocation(): string | undefined
openTextBackupsLocation(): Promise<void>
changeTextBackupsLocation(): Promise<string | undefined>
saveTextBackupData(data: string): Promise<void>
isPlaintextBackupsEnabled(): boolean
enablePlaintextBackups(): Promise<void>
disablePlaintextBackups(): void
getPlaintextBackupsLocation(): string | undefined
openPlaintextBackupsLocation(): Promise<void>
changePlaintextBackupsLocation(): Promise<string | undefined>
}
@@ -0,0 +1,3 @@
export interface SuperConverterServiceInterface {
convertString: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => string
}
+9 -7
View File
@@ -1,30 +1,32 @@
export * from './Api/DirectoryHandle'
export * from './Api/FileHandleRead'
export * from './Api/FileHandleReadWrite'
export * from './Api/FilesApiInterface'
export * from './Api/FileSystemApi'
export * from './Api/FileSystemNoSelection'
export * from './Api/FileSystemResult'
export * from './Api/FilesApiInterface'
export * from './Cache/FileMemoryCache'
export * from './Chunker/ByteChunker'
export * from './Chunker/OnChunkCallback'
export * from './Chunker/OrderedByteChunker'
export * from './Device/DesktopWatchedChanges'
export * from './Device/FileBackupMetadataFile'
export * from './Device/FileBackupsConstantsV1'
export * from './Device/FileBackupsDevice'
export * from './Device/FileBackupsMapping'
export * from './Operations/DownloadAndDecrypt'
export * from './Operations/EncryptAndUpload'
export * from './Service/BackupServiceInterface'
export * from './Service/SuperConverterServiceInterface'
export * from './Service/FilesClientInterface'
export * from './Service/ReadAndDecryptBackupFileFileSystemAPI'
export * from './Service/ReadAndDecryptBackupFileUsingBackupService'
export * from './Operations/DownloadAndDecrypt'
export * from './Operations/EncryptAndUpload'
export * from './UseCase/FileDecryptor'
export * from './UseCase/FileUploader'
export * from './UseCase/FileEncryptor'
export * from './UseCase/FileDownloader'
export * from './Types/DecryptedBytes'
export * from './Types/EncryptedBytes'
export * from './Types/FileDownloadProgress'
export * from './Types/FileUploadProgress'
export * from './Types/FileUploadResult'
export * from './UseCase/FileDecryptor'
export * from './UseCase/FileDownloader'
export * from './UseCase/FileEncryptor'
export * from './UseCase/FileUploader'
+86
View File
@@ -3,6 +3,92 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.53.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/mobile
## [3.53.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
**Note:** Version bump only for package @standardnotes/mobile
# [3.53.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
### Features
* Automatic plaintext backup option in Preferences > Backups will backup your notes and tags into plaintext, unencrypted folders on your computer. In addition, automatic encrypted text backups preference management has moved from the top-level menu in the desktop app to Preferences > Backups. ([#2322](https://github.com/standardnotes/app/issues/2322)) ([7e3db49](https://github.com/standardnotes/app/commit/7e3db49322034aa644c3c99ccf9a5f1b508583aa))
## [3.52.36](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.35](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.34](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.33](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.30](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.29](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.28](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/mobile
## [3.52.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/mobile
+37 -37
View File
@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
CFPropertyList (3.0.6)
rexml
activesupport (6.1.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -9,28 +9,28 @@ GEM
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.603.0)
aws-sdk-core (3.131.2)
aws-partitions (1.755.0)
aws-sdk-core (3.171.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (1.63.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-s3 (1.121.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.0)
aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.0.3)
@@ -81,13 +81,13 @@ GEM
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
dotenv (2.8.1)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.92.3)
faraday (1.10.0)
excon (0.99.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -116,7 +116,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.207.0)
fastlane (2.212.2)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -160,9 +160,9 @@ GEM
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.24.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-core (0.7.0)
google-apis-androidpublisher_v3 (0.40.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -171,27 +171,27 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.13.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-playcustomapp_v1 (0.10.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-storage_v1 (0.17.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.37.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.2.0)
googleauth (1.5.2)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -204,11 +204,11 @@ GEM
httpclient (2.8.3)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
jmespath (1.6.1)
json (2.6.2)
jwt (2.4.1)
jmespath (1.6.2)
json (2.6.3)
jwt (2.7.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_magick (4.12.0)
mini_mime (1.1.2)
minitest (5.15.0)
molinillo (0.8.0)
@@ -220,7 +220,7 @@ GEM
netrc (0.11.0)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
plist (3.7.0)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.2.0)
@@ -239,7 +239,7 @@ GEM
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
@@ -259,7 +259,7 @@ GEM
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
webrick (1.8.1)
word_wrap (1.0.0)
xcode-install (2.8.0)
claide (>= 0.9.1, < 1.1.0)
@@ -291,4 +291,4 @@ RUBY VERSION
ruby 2.7.6p219
BUNDLED WITH
2.3.25
2.3.15
+7 -2
View File
@@ -34,7 +34,7 @@ platform :ios do
build_number
end
def deploy_ios(scheme)
def deploy_ios(scheme, is_testflight = false)
sign_ios
build_number = build_ios(scheme)
app_store_connect_api_key(
@@ -55,7 +55,7 @@ platform :ios do
changelog: 'Fixes and improvements.'
)
if is_prod
if is_prod && !is_testflight
upload_to_app_store(
build_number: build_number,
submit_for_review: true,
@@ -83,6 +83,11 @@ platform :ios do
deploy_ios 'StandardNotes'
end
desc 'Deploy prod app to tesflight'
lane :testflight do
deploy_ios 'StandardNotes', true
end
desc 'Setup iOS for running on CI'
lane :setup do
setup_project
@@ -16,7 +16,14 @@
window.plansUrl = "https://standardnotes.com/plans";
window.dashboardUrl = "https://standardnotes.com/dashboard";
</script>
<script>
window.onerror = function (message, source, lineno, colno, error) {
if (document.readyState === "complete") {
return
}
window.ReactNativeWebView.postMessage("appLoadError");
};
</script>
<script src="web-src/app.js"></script>
</head>
+2 -2
View File
@@ -637,7 +637,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 60195509584153283780abdac5569feffb8f08cc
@@ -658,7 +658,7 @@ SPEC CHECKSUMS:
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: bec48f07daf7bcdc2655a0cde84e07d24d2a9e2a
RCTTypeSafety: 171394eebacf71e1cfad79dbfae7ee8fc16ca80a
React: d7433ccb6a8c36e4cbed59a73c0700fc83c3e98a
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.52.20",
"version": "3.53.4",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+48 -1
View File
@@ -1,6 +1,6 @@
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Keyboard, Platform } from 'react-native'
import { Button, Keyboard, Platform, Text, View } from 'react-native'
import VersionInfo from 'react-native-version-info'
import { WebView, WebViewMessageEvent } from 'react-native-webview'
import { OnShouldStartLoadWithRequest } from 'react-native-webview/lib/WebViewTypes'
@@ -34,6 +34,8 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
[androidBackHandlerService, colorSchemeService, stateService],
)
const [showAndroidWebviewUpdatePrompt, setShowAndroidWebviewUpdatePrompt] = useState(false)
useEffect(() => {
const removeStateServiceListener = stateService.addEventObserver((event: ReactNativeToWebEvent) => {
webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' }))
@@ -228,6 +230,10 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
const onMessage = (event: WebViewMessageEvent) => {
const message = event.nativeEvent.data
if (message === 'appLoadError' && Platform.OS === 'android') {
setShowAndroidWebviewUpdatePrompt(true)
return
}
try {
const functionData = JSON.parse(message)
void onFunctionMessage(functionData.functionName, functionData.messageId, functionData.args)
@@ -277,6 +283,47 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
const requireInlineMediaPlaybackForMomentsFeature = true
const requireMediaUserInteractionForMomentsFeature = false
if (showAndroidWebviewUpdatePrompt) {
return (
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'black',
}}
>
<Text
style={{
color: 'white',
fontSize: 20,
fontWeight: 'bold',
marginBottom: 20,
}}
>
Could not load app
</Text>
<Text
style={{
color: 'white',
fontSize: 16,
marginBottom: 20,
textAlign: 'center',
}}
>
Please make sure your Android System Webview is updated to the latest version
</Text>
<Button
title={'Update'}
onPress={() => {
setShowAndroidWebviewUpdatePrompt(false)
device.openUrl('https://play.google.com/store/apps/details?id=com.google.android.webview')
}}
/>
</View>
)
}
return (
<WebView
ref={webViewRef}
+84
View File
@@ -3,6 +3,90 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.272](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.271](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.270](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.269](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.268](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.267](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.266](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.265](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-01)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.264](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.263](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.262](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-28)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.261](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.260](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.259](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.258](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.257](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.256](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.255](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.254](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-27)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.253](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.252](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.251](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.4.251",
"version": "1.4.272",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.59.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/services
## [1.59.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
### Bug Fixes
* automatically convert Super notes to Markdown for the Plaintext Backups feature ([75ecf4c](https://github.com/standardnotes/app/commit/75ecf4c393a6bc0ab2d6bf50dcbee21cef520746))
# [1.59.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
### Features
* Automatic plaintext backup option in Preferences > Backups will backup your notes and tags into plaintext, unencrypted folders on your computer. In addition, automatic encrypted text backups preference management has moved from the top-level menu in the desktop app to Preferences > Backups. ([#2322](https://github.com/standardnotes/app/issues/2322)) ([7e3db49](https://github.com/standardnotes/app/commit/7e3db49322034aa644c3c99ccf9a5f1b508583aa))
## [1.58.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/services
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.58.5",
"version": "1.59.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -20,6 +20,7 @@
"@standardnotes/common": "^1.46.4",
"@standardnotes/domain-core": "^1.12.0",
"@standardnotes/encryption": "workspace:^",
"@standardnotes/features": "workspace:^",
"@standardnotes/files": "workspace:^",
"@standardnotes/models": "workspace:^",
"@standardnotes/responses": "workspace:*",
@@ -1,6 +1,6 @@
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
import { FilesClientInterface } from '@standardnotes/files'
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
import { AlertService } from '../Alert/AlertService'
import { ComponentManagerInterface } from '../Component/ComponentManagerInterface'
@@ -50,6 +50,7 @@ export interface ApplicationInterface {
get user(): UserClientInterface
get files(): FilesClientInterface
get subscriptions(): SubscriptionClientInterface
get fileBackups(): BackupServiceInterface | undefined
readonly identifier: ApplicationIdentifier
readonly platform: Platform
deviceInterface: DeviceInterface
@@ -1,3 +1,7 @@
import { HistoryServiceInterface } from './../History/HistoryServiceInterface'
import { PayloadManagerInterface } from './../Payloads/PayloadManagerInterface'
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { StatusServiceInterface } from './../Status/StatusServiceInterface'
import { FilesBackupService } from './BackupService'
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
@@ -20,6 +24,10 @@ describe('backup service', () => {
let internalEventBus: InternalEventBusInterface
let backupService: FilesBackupService
let device: FileBackupsDevice
let session: SessionsClientInterface
let storage: StorageServiceInterface
let payloads: PayloadManagerInterface
let history: HistoryServiceInterface
beforeEach(() => {
apiService = {} as jest.Mocked<ApiServiceInterface>
@@ -41,6 +49,8 @@ describe('backup service', () => {
device.getFileBackupReadToken = jest.fn()
device.readNextChunk = jest.fn()
session = {} as jest.Mocked<SessionsClientInterface>
syncService = {} as jest.Mocked<SyncServiceInterface>
syncService.sync = jest.fn()
@@ -55,7 +65,25 @@ describe('backup service', () => {
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publish = jest.fn()
backupService = new FilesBackupService(itemManager, apiService, encryptor, device, status, crypto, internalEventBus)
payloads = {} as PayloadManagerInterface
history = {} as HistoryServiceInterface
storage = {} as StorageServiceInterface
storage.getValue = jest.fn().mockReturnValue('')
backupService = new FilesBackupService(
itemManager,
apiService,
encryptor,
device,
status,
crypto,
storage,
session,
payloads,
history,
internalEventBus,
)
crypto.xchacha20StreamInitDecryptor = jest.fn().mockReturnValue({
state: {},
@@ -1,6 +1,16 @@
import { NoteType } from '@standardnotes/features'
import { ApplicationStage } from './../Application/ApplicationStage'
import { ContentType } from '@standardnotes/common'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
import {
PayloadEmitSource,
FileItem,
CreateEncryptedBackupFileContextPayload,
SNNote,
SNTag,
isNote,
NoteContent,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import {
FilesApiInterface,
@@ -10,19 +20,34 @@ import {
FileBackupRecord,
OnChunkCallback,
BackupServiceInterface,
DesktopWatchedDirectoriesChanges,
SuperConverterServiceInterface,
} from '@standardnotes/files'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { AbstractService } from '../Service/AbstractService'
import { StatusServiceInterface } from '../Status/StatusServiceInterface'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { log, LoggingDomain } from '../Logging'
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import { StorageKey } from '../Storage/StorageKeys'
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { HistoryServiceInterface } from '../History/HistoryServiceInterface'
const PlaintextBackupsDirectoryName = 'Plaintext Backups'
export const TextBackupsDirectoryName = 'Text Backups'
export const FileBackupsDirectoryName = 'File Backups'
export class FilesBackupService extends AbstractService implements BackupServiceInterface {
private itemsObserverDisposer: () => void
private filesObserverDisposer: () => void
private notesObserverDisposer: () => void
private tagsObserverDisposer: () => void
private pendingFiles = new Set<string>()
private mappingCache?: FileBackupsMapping['files']
private markdownConverter!: SuperConverterServiceInterface
constructor(
private items: ItemManagerInterface,
private api: FilesApiInterface,
@@ -30,45 +55,263 @@ export class FilesBackupService extends AbstractService implements BackupService
private device: FileBackupsDevice,
private status: StatusServiceInterface,
private crypto: PureCryptoInterface,
private storage: StorageServiceInterface,
private session: SessionsClientInterface,
private payloads: PayloadManagerInterface,
private history: HistoryServiceInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
this.itemsObserverDisposer = items.addObserver<FileItem>(ContentType.File, ({ changed, inserted, source }) => {
this.filesObserverDisposer = items.addObserver<FileItem>(ContentType.File, ({ changed, inserted, source }) => {
const applicableSources = [
PayloadEmitSource.LocalDatabaseLoaded,
PayloadEmitSource.RemoteSaved,
PayloadEmitSource.RemoteRetrieved,
]
if (applicableSources.includes(source)) {
void this.handleChangedFiles([...changed, ...inserted])
}
})
const noteAndTagSources = [
PayloadEmitSource.RemoteSaved,
PayloadEmitSource.RemoteRetrieved,
PayloadEmitSource.OfflineSyncSaved,
]
this.notesObserverDisposer = items.addObserver<SNNote>(ContentType.Note, ({ changed, inserted, source }) => {
if (noteAndTagSources.includes(source)) {
void this.handleChangedNotes([...changed, ...inserted])
}
})
this.tagsObserverDisposer = items.addObserver<SNTag>(ContentType.Tag, ({ changed, inserted, source }) => {
if (noteAndTagSources.includes(source)) {
void this.handleChangedTags([...changed, ...inserted])
}
})
}
setSuperConverter(converter: SuperConverterServiceInterface): void {
this.markdownConverter = converter
}
async importWatchedDirectoryChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void> {
for (const change of changes) {
const existingItem = this.items.findItem(change.itemUuid)
if (!existingItem) {
continue
}
if (!isNote(existingItem)) {
continue
}
const newContent: NoteContent = {
...existingItem.payload.content,
preview_html: undefined,
preview_plain: undefined,
text: change.content,
}
const payloadCopy = existingItem.payload.copy({
content: newContent,
})
await this.payloads.importPayloads([payloadCopy], this.history.getHistoryMapCopy())
}
}
override deinit() {
super.deinit()
this.itemsObserverDisposer()
this.filesObserverDisposer()
this.notesObserverDisposer()
this.tagsObserverDisposer()
;(this.items as unknown) = undefined
;(this.api as unknown) = undefined
;(this.encryptor as unknown) = undefined
;(this.device as unknown) = undefined
;(this.status as unknown) = undefined
;(this.crypto as unknown) = undefined
;(this.storage as unknown) = undefined
;(this.session as unknown) = undefined
}
public isFilesBackupsEnabled(): Promise<boolean> {
return this.device.isFilesBackupsEnabled()
override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
if (stage === ApplicationStage.Launched_10) {
void this.automaticallyEnableTextBackupsIfPreferenceNotSet()
}
}
private async automaticallyEnableTextBackupsIfPreferenceNotSet(): Promise<void> {
if (this.storage.getValue(StorageKey.TextBackupsEnabled) == undefined) {
this.storage.setValue(StorageKey.TextBackupsEnabled, true)
const location = `${await this.device.getUserDocumentsDirectory()}/${this.prependWorkspacePathForPath(
TextBackupsDirectoryName,
)}`
this.storage.setValue(StorageKey.TextBackupsLocation, location)
}
}
openAllDirectoriesContainingBackupFiles(): void {
const fileBackupsLocation = this.getFilesBackupsLocation()
const plaintextBackupsLocation = this.getPlaintextBackupsLocation()
const textBackupsLocation = this.getTextBackupsLocation()
if (fileBackupsLocation) {
void this.device.openLocation(fileBackupsLocation)
}
if (plaintextBackupsLocation) {
void this.device.openLocation(plaintextBackupsLocation)
}
if (textBackupsLocation) {
void this.device.openLocation(textBackupsLocation)
}
}
isFilesBackupsEnabled(): boolean {
return this.storage.getValue(StorageKey.FileBackupsEnabled, undefined, false)
}
getFilesBackupsLocation(): string | undefined {
return this.storage.getValue(StorageKey.FileBackupsLocation)
}
isTextBackupsEnabled(): boolean {
return this.storage.getValue(StorageKey.TextBackupsEnabled, undefined, true)
}
prependWorkspacePathForPath(path: string): string {
const workspacePath = this.session.getWorkspaceDisplayIdentifier()
return `${workspacePath}/${path}`
}
async enableTextBackups(): Promise<void> {
let location = this.getTextBackupsLocation()
if (!location) {
location = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
this.prependWorkspacePathForPath(TextBackupsDirectoryName),
)
if (!location) {
return
}
}
this.storage.setValue(StorageKey.TextBackupsEnabled, true)
this.storage.setValue(StorageKey.TextBackupsLocation, location)
}
disableTextBackups(): void {
this.storage.setValue(StorageKey.TextBackupsEnabled, false)
}
getTextBackupsLocation(): string | undefined {
return this.storage.getValue(StorageKey.TextBackupsLocation)
}
async openTextBackupsLocation(): Promise<void> {
const location = this.getTextBackupsLocation()
if (location) {
void this.device.openLocation(location)
}
}
async changeTextBackupsLocation(): Promise<string | undefined> {
const oldLocation = this.getTextBackupsLocation()
const newLocation = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
this.prependWorkspacePathForPath(TextBackupsDirectoryName),
oldLocation,
)
if (!newLocation) {
return undefined
}
this.storage.setValue(StorageKey.TextBackupsLocation, newLocation)
return newLocation
}
async saveTextBackupData(data: string): Promise<void> {
const location = this.getTextBackupsLocation()
if (!location) {
return
}
return this.device.saveTextBackupData(location, data)
}
isPlaintextBackupsEnabled(): boolean {
return this.storage.getValue(StorageKey.PlaintextBackupsEnabled, undefined, false)
}
public async enablePlaintextBackups(): Promise<void> {
let location = this.getPlaintextBackupsLocation()
if (!location) {
location = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
this.prependWorkspacePathForPath(PlaintextBackupsDirectoryName),
)
if (!location) {
return
}
}
this.storage.setValue(StorageKey.PlaintextBackupsEnabled, true)
this.storage.setValue(StorageKey.PlaintextBackupsLocation, location)
void this.handleChangedNotes(this.items.getItems<SNNote>(ContentType.Note))
}
disablePlaintextBackups(): void {
this.storage.setValue(StorageKey.PlaintextBackupsEnabled, false)
this.storage.setValue(StorageKey.PlaintextBackupsLocation, undefined)
}
getPlaintextBackupsLocation(): string | undefined {
return this.storage.getValue(StorageKey.PlaintextBackupsLocation)
}
async openPlaintextBackupsLocation(): Promise<void> {
const location = this.getPlaintextBackupsLocation()
if (location) {
void this.device.openLocation(location)
}
}
async changePlaintextBackupsLocation(): Promise<string | undefined> {
const oldLocation = this.getPlaintextBackupsLocation()
const newLocation = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
this.prependWorkspacePathForPath(PlaintextBackupsDirectoryName),
oldLocation,
)
if (!newLocation) {
return undefined
}
this.storage.setValue(StorageKey.PlaintextBackupsLocation, newLocation)
return newLocation
}
public async enableFilesBackups(): Promise<void> {
await this.device.enableFilesBackups()
if (!(await this.isFilesBackupsEnabled())) {
return
let location = this.getFilesBackupsLocation()
if (!location) {
location = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
this.prependWorkspacePathForPath(FileBackupsDirectoryName),
)
if (!location) {
return
}
}
this.storage.setValue(StorageKey.FileBackupsEnabled, true)
this.storage.setValue(StorageKey.FileBackupsLocation, location)
this.backupAllFiles()
}
@@ -78,24 +321,39 @@ export class FilesBackupService extends AbstractService implements BackupService
void this.handleChangedFiles(files)
}
public disableFilesBackups(): Promise<void> {
return this.device.disableFilesBackups()
public disableFilesBackups(): void {
this.storage.setValue(StorageKey.FileBackupsEnabled, false)
}
public changeFilesBackupsLocation(): Promise<string | undefined> {
return this.device.changeFilesBackupsLocation()
public async changeFilesBackupsLocation(): Promise<string | undefined> {
const oldLocation = this.getFilesBackupsLocation()
const newLocation = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
this.prependWorkspacePathForPath(FileBackupsDirectoryName),
oldLocation,
)
if (!newLocation) {
return undefined
}
this.storage.setValue(StorageKey.FileBackupsLocation, newLocation)
return newLocation
}
public getFilesBackupsLocation(): Promise<string> {
return this.device.getFilesBackupsLocation()
public async openFilesBackupsLocation(): Promise<void> {
const location = this.getFilesBackupsLocation()
if (location) {
void this.device.openLocation(location)
}
}
public openFilesBackupsLocation(): Promise<void> {
return this.device.openFilesBackupsLocation()
}
private async getBackupsMappingFromDisk(): Promise<FileBackupsMapping['files'] | undefined> {
const location = this.getFilesBackupsLocation()
if (!location) {
return undefined
}
private async getBackupsMappingFromDisk(): Promise<FileBackupsMapping['files']> {
const result = (await this.device.getFilesBackupsMappingFile()).files
const result = (await this.device.getFilesBackupsMappingFile(location)).files
this.mappingCache = result
@@ -106,30 +364,39 @@ export class FilesBackupService extends AbstractService implements BackupService
this.mappingCache = undefined
}
private async getBackupsMappingFromCache(): Promise<FileBackupsMapping['files']> {
private async getBackupsMappingFromCache(): Promise<FileBackupsMapping['files'] | undefined> {
return this.mappingCache ?? (await this.getBackupsMappingFromDisk())
}
public async getFileBackupInfo(file: { uuid: string }): Promise<FileBackupRecord | undefined> {
const mapping = await this.getBackupsMappingFromCache()
if (!mapping) {
return undefined
}
const record = mapping[file.uuid]
return record
}
public getFileBackupAbsolutePath(record: FileBackupRecord): string {
const location = this.getFilesBackupsLocation()
return `${location}/${record.relativePath}`
}
public async openFileBackup(record: FileBackupRecord): Promise<void> {
await this.device.openFileBackup(record)
const location = this.getFileBackupAbsolutePath(record)
await this.device.openLocation(location)
}
private async handleChangedFiles(files: FileItem[]): Promise<void> {
if (files.length === 0) {
return
}
if (!(await this.isFilesBackupsEnabled())) {
if (files.length === 0 || !this.isFilesBackupsEnabled()) {
return
}
const mapping = await this.getBackupsMappingFromDisk()
if (!mapping) {
throw new ClientDisplayableError('No backups mapping found')
}
for (const file of files) {
if (this.pendingFiles.has(file.uuid)) {
@@ -150,6 +417,41 @@ export class FilesBackupService extends AbstractService implements BackupService
this.invalidateMappingCache()
}
private async handleChangedNotes(notes: SNNote[]): Promise<void> {
if (notes.length === 0 || !this.isPlaintextBackupsEnabled()) {
return
}
const location = this.getPlaintextBackupsLocation()
if (!location) {
throw new ClientDisplayableError('No plaintext backups location found')
}
if (!this.markdownConverter) {
throw 'Super markdown converter not initialized'
}
for (const note of notes) {
const tags = this.items.getSortedTagsForItem(note)
const tagNames = tags.map((tag) => this.items.getTagLongTitle(tag))
const text = note.noteType === NoteType.Super ? this.markdownConverter.convertString(note.text, 'md') : note.text
await this.device.savePlaintextNoteBackup(location, note.uuid, note.title, tagNames, text)
}
await this.device.persistPlaintextBackupsMappingFile(location)
}
private async handleChangedTags(tags: SNTag[]): Promise<void> {
if (tags.length === 0 || !this.isPlaintextBackupsEnabled()) {
return
}
for (const tag of tags) {
const notes = this.items.referencesForItem<SNNote>(tag, ContentType.Note)
await this.handleChangedNotes(notes)
}
}
async readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'> {
const fileBackup = await this.getFileBackupInfo({ uuid })
@@ -157,7 +459,8 @@ export class FilesBackupService extends AbstractService implements BackupService
return 'failed'
}
const token = await this.device.getFileBackupReadToken(fileBackup)
const path = `${this.getFilesBackupsLocation()}/${fileBackup.relativePath}/${fileBackup.binaryFileName}`
const token = await this.device.getFileBackupReadToken(path)
let readMore = true
let index = 0
@@ -176,7 +479,10 @@ export class FilesBackupService extends AbstractService implements BackupService
}
private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> {
log(LoggingDomain.FilesBackups, 'Backing up file locally', file.uuid)
const location = this.getFilesBackupsLocation()
if (!location) {
return 'failed'
}
const messageId = this.status.addMessage(`Backing up file ${file.name}...`)
@@ -189,6 +495,7 @@ export class FilesBackupService extends AbstractService implements BackupService
const itemsKey = this.items.getDisplayableItemsKeys().find((k) => k.uuid === encryptedFile.items_key_id)
if (!itemsKey) {
this.status.removeMessage(messageId)
return 'failed'
}
@@ -201,6 +508,7 @@ export class FilesBackupService extends AbstractService implements BackupService
const token = await this.api.createFileValetToken(file.remoteIdentifier, 'read')
if (token instanceof ClientDisplayableError) {
this.status.removeMessage(messageId)
return 'failed'
}
@@ -218,7 +526,7 @@ export class FilesBackupService extends AbstractService implements BackupService
const metaFileAsString = JSON.stringify(metaFile, null, 2)
const result = await this.device.saveFilesBackupsFile(file.uuid, metaFileAsString, {
const result = await this.device.saveFilesBackupsFile(location, file.uuid, metaFileAsString, {
chunkSizes: file.encryptedChunkSizes,
url: this.api.getFilesDownloadUrl(),
valetToken: token,
@@ -235,4 +543,18 @@ export class FilesBackupService extends AbstractService implements BackupService
return result
}
/**
* Not presently used or enabled. It works, but presently has the following edge cases:
* 1. Editing the note directly in SN triggers an immediate backup which triggers a file change which triggers the observer
* 2. Since changes are based on filenames, a note with the same title as another may not properly map to the correct uuid
* 3. Opening the file triggers a watch event from Node's watch API.
* 4. Gives web code ability to monitor arbitrary locations. Needs whitelisting mechanism.
*/
disabledExperimental_monitorPlaintextBackups(): void {
const location = this.getPlaintextBackupsLocation()
if (location) {
void this.device.monitorPlaintextBackupsLocationForChanges(location)
}
}
}
@@ -1,23 +1,11 @@
import { DecryptedTransferPayload } from '@standardnotes/models'
import { FileBackupsDevice } from '@standardnotes/files'
import { DesktopWatchedDirectoriesChanges, FileBackupsDevice } from '@standardnotes/files'
export interface WebClientRequiresDesktopMethods extends FileBackupsDevice {
localBackupsCount(): Promise<number>
viewlocalBackups(): void
deleteLocalBackups(): Promise<void>
syncComponents(payloads: unknown[]): void
onMajorDataChange(): void
onInitialDataLoad(): void
onSearch(text?: string): void
downloadBackup(): void | Promise<void>
get extensionsServerHost(): string
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
@@ -32,9 +20,5 @@ export interface DesktopClientRequiresWebMethods {
onComponentInstallationComplete(componentData: DecryptedTransferPayload, error: unknown): Promise<void>
requestBackupFile(): Promise<string | undefined>
didBeginBackup(): void
didFinishBackup(success: boolean): void
handleWatchedDirectoriesChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void>
}
@@ -0,0 +1,5 @@
import { HistoryMap } from '@standardnotes/models'
export interface HistoryServiceInterface {
getHistoryMapCopy(): HistoryMap
}
@@ -12,6 +12,7 @@ import {
ItemContent,
PredicateInterface,
DecryptedPayload,
SNTag,
} from '@standardnotes/models'
import { AbstractService } from '../Service/AbstractService'
@@ -96,4 +97,11 @@ export interface ItemManagerInterface extends AbstractService {
subItemsMatchingPredicates<T extends DecryptedItemInterface>(items: T[], predicates: PredicateInterface<T>[]): T[]
removeAllItemsFromMemory(): Promise<void>
getDirtyItems(): (DecryptedItemInterface | DeletedItemInterface)[]
getTagLongTitle(tag: SNTag): string
getSortedTagsForItem(item: DecryptedItemInterface<ItemContent>): SNTag[]
referencesForItem<I extends DecryptedItemInterface = DecryptedItemInterface>(
itemToLookupUuidFor: DecryptedItemInterface,
contentType?: ContentType,
): I[]
findItem<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: string): T | undefined
}
@@ -3,6 +3,8 @@ import {
EncryptedPayloadInterface,
FullyFormedPayloadInterface,
PayloadEmitSource,
DecryptedPayloadInterface,
HistoryMap,
} from '@standardnotes/models'
import { IntegrityPayload } from '@standardnotes/responses'
@@ -21,4 +23,6 @@ export interface PayloadManagerInterface {
* Returns a detached array of all items which are not deleted
*/
get nonDeletedItems(): FullyFormedPayloadInterface[]
importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
}
@@ -8,6 +8,7 @@ import { Base64String } from '@standardnotes/sncrypto-common'
import { SessionManagerResponse } from './SessionManagerResponse'
export interface SessionsClientInterface {
getWorkspaceDisplayIdentifier(): string
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
getUser(): User | undefined
isCurrentSessionReadOnly(): boolean | undefined
@@ -31,7 +31,6 @@ export enum StorageKey {
ProtectionExpirey = 'SessionExpiresAtKey',
ProtectionSessionLength = 'SessionLengthKey',
KeyRecoveryUndecryptableItems = 'key_recovery_undecryptable',
StorageEncryptionPolicy = 'storage_policy',
WebSocketUrl = 'webSocket_url',
UserRoles = 'user_roles',
OfflineUserRoles = 'offline_user_roles',
@@ -42,6 +41,12 @@ export enum StorageKey {
LaunchPriorityUuids = 'launch_priority_uuids',
LastReadChangelogVersion = 'last_read_changelog_version',
MomentsEnabled = 'moments_enabled',
TextBackupsEnabled = 'text_backups_enabled',
TextBackupsLocation = 'text_backups_location',
PlaintextBackupsEnabled = 'plaintext_backups_enabled',
PlaintextBackupsLocation = 'plaintext_backups_location',
FileBackupsEnabled = 'file_backups_enabled',
FileBackupsLocation = 'file_backups_location',
}
export enum NonwrappedStorageKey {
@@ -7,13 +7,8 @@ export enum StoragePersistencePolicies {
Ephemeral = 2,
}
export enum StorageEncryptionPolicy {
Default = 1,
Disabled = 2,
}
export enum StorageValueModes {
/** Stored inside wrapped encrpyed storage object */
/** Stored inside wrapped encrypted storage object */
Default = 1,
/** Stored outside storage object, unencrypted */
Nonwrapped = 2,
+12 -11
View File
@@ -4,19 +4,21 @@ export * from './Application/AppGroupManagedApplication'
export * from './Application/ApplicationInterface'
export * from './Application/ApplicationStage'
export * from './Application/DeinitCallback'
export * from './Application/DeinitSource'
export * from './Application/DeinitMode'
export * from './Application/DeinitSource'
export * from './Application/WebApplicationInterface'
export * from './Auth/AuthClientInterface'
export * from './Auth/AuthManager'
export * from './Authenticator/AuthenticatorClientInterface'
export * from './Authenticator/AuthenticatorManager'
export * from './User/UserClientInterface'
export * from './Application/WebApplicationInterface'
export * from './Backups/BackupService'
export * from './Challenge'
export * from './Component/ComponentManagerInterface'
export * from './Component/ComponentViewerError'
export * from './Component/ComponentViewerInterface'
export * from './Device/DatabaseItemMetadata'
export * from './Device/DatabaseLoadOptions'
export * from './Device/DatabaseLoadSorter'
export * from './Device/DesktopDeviceInterface'
export * from './Device/DesktopManagerInterface'
export * from './Device/DesktopWebCommunication'
@@ -24,9 +26,6 @@ export * from './Device/DeviceInterface'
export * from './Device/MobileDeviceInterface'
export * from './Device/TypeCheck'
export * from './Device/WebOrDesktopDeviceInterface'
export * from './Device/DatabaseLoadOptions'
export * from './Device/DatabaseItemMetadata'
export * from './Device/DatabaseLoadSorter'
export * from './Diagnostics/ServiceDiagnostics'
export * from './Encryption/BackupFileDecryptor'
export * from './Encryption/EncryptionService'
@@ -40,12 +39,13 @@ export * from './Event/EventObserver'
export * from './Event/SyncEvent'
export * from './Event/SyncEventReceiver'
export * from './Event/WebAppEvent'
export * from './Feature/FeatureStatus'
export * from './Feature/FeaturesClientInterface'
export * from './Feature/FeaturesEvent'
export * from './Feature/FeatureStatus'
export * from './Feature/OfflineSubscriptionEntitlements'
export * from './Feature/SetOfflineFeaturesFunctionResponse'
export * from './Files/FileService'
export * from './History/HistoryServiceInterface'
export * from './Integrity/IntegrityApiInterface'
export * from './Integrity/IntegrityEvent'
export * from './Integrity/IntegrityEventPayload'
@@ -59,9 +59,9 @@ export * from './Internal/InternalEventType'
export * from './Item/ItemCounter'
export * from './Item/ItemCounterInterface'
export * from './Item/ItemManagerInterface'
export * from './Item/ItemRelationshipDirection'
export * from './Item/ItemsClientInterface'
export * from './Item/ItemsServerInterface'
export * from './Item/ItemRelationshipDirection'
export * from './Mutator/MutatorClientInterface'
export * from './Payloads/PayloadManagerInterface'
export * from './Preferences/PreferenceServiceInterface'
@@ -76,21 +76,22 @@ export * from './Session/SessionManagerResponse'
export * from './Session/SessionsClientInterface'
export * from './Status/StatusService'
export * from './Status/StatusServiceInterface'
export * from './Storage/StorageKeys'
export * from './Storage/InMemoryStore'
export * from './Storage/KeyValueStoreInterface'
export * from './Storage/StorageKeys'
export * from './Storage/StorageServiceInterface'
export * from './Storage/StorageTypes'
export * from './Strings/InfoStrings'
export * from './Strings/Messages'
export * from './Subscription/SubscriptionClientInterface'
export * from './Subscription/SubscriptionManager'
export * from './Subscription/AppleIAPProductId'
export * from './Subscription/AppleIAPReceipt'
export * from './Subscription/SubscriptionClientInterface'
export * from './Subscription/SubscriptionManager'
export * from './Sync/SyncMode'
export * from './Sync/SyncOptions'
export * from './Sync/SyncQueueStrategy'
export * from './Sync/SyncServiceInterface'
export * from './Sync/SyncSource'
export * from './User/UserClientInterface'
export * from './User/UserClientInterface'
export * from './User/UserService'
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.168.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-03)
**Note:** Version bump only for package @standardnotes/snjs
## [2.168.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
**Note:** Version bump only for package @standardnotes/snjs
# [2.168.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-05-02)
### Features
* Automatic plaintext backup option in Preferences > Backups will backup your notes and tags into plaintext, unencrypted folders on your computer. In addition, automatic encrypted text backups preference management has moved from the top-level menu in the desktop app to Preferences > Backups. ([#2322](https://github.com/standardnotes/app/issues/2322)) ([7e3db49](https://github.com/standardnotes/app/commit/7e3db49322034aa644c3c99ccf9a5f1b508583aa))
## [2.167.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-04-26)
**Note:** Version bump only for package @standardnotes/snjs
@@ -1,145 +0,0 @@
/**
* @jest-environment jsdom
*/
import { SNLog } from './../Log'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { AlertService, DeviceInterface, namespacedKey, RawStorageKey } from '@standardnotes/services'
import { Environment, Platform } from '@standardnotes/models'
import { SNApplication } from './Application'
describe('application', () => {
// eslint-disable-next-line no-console
SNLog.onLog = console.log
SNLog.onError = console.error
let application: SNApplication
let device: DeviceInterface
let crypto: PureCryptoInterface
beforeEach(async () => {
const identifier = '123'
crypto = {} as jest.Mocked<PureCryptoInterface>
crypto.initialize = jest.fn()
device = {} as jest.Mocked<DeviceInterface>
device.openDatabase = jest.fn().mockResolvedValue(true)
device.getAllDatabaseEntries = jest.fn().mockReturnValue([])
device.setRawStorageValue = jest.fn()
device.getRawStorageValue = jest.fn().mockImplementation((key) => {
if (key === namespacedKey(identifier, RawStorageKey.SnjsVersion)) {
return '10.0.0'
}
return undefined
})
application = new SNApplication({
environment: Environment.Mobile,
platform: Platform.Ios,
deviceInterface: device,
crypto: crypto,
alertService: {} as jest.Mocked<AlertService>,
identifier: identifier,
defaultHost: 'localhost',
appVersion: '1.0',
})
await application.prepareForLaunch({ receiveChallenge: jest.fn() })
})
it('diagnostics', async () => {
const diagnostics = await application.getDiagnostics()
expect(diagnostics).toEqual(
expect.objectContaining({
application: expect.objectContaining({
appVersion: '1.0',
environment: 3,
platform: 1,
}),
payloads: {
integrityPayloads: [],
nonDeletedItemCount: 0,
invalidPayloadsCount: 0,
},
items: { allIds: [] },
storage: {
storagePersistable: false,
persistencePolicy: 'Default',
encryptionPolicy: 'Default',
needsPersist: false,
currentPersistPromise: false,
isStorageWrapped: false,
allRawPayloadsCount: 0,
},
encryption: expect.objectContaining({
getLatestVersion: '004',
hasAccount: false,
getUserVersion: undefined,
upgradeAvailable: false,
accountUpgradeAvailable: false,
passcodeUpgradeAvailable: false,
hasPasscode: false,
isPasscodeLocked: false,
itemsEncryption: expect.objectContaining({
itemsKeysIds: [],
}),
rootKeyEncryption: expect.objectContaining({
hasRootKey: false,
keyMode: 'RootKeyNone',
hasRootKeyWrapper: false,
hasAccount: false,
hasPasscode: false,
}),
}),
api: {
hasSession: false,
user: undefined,
registering: false,
authenticating: false,
changing: false,
refreshingSession: false,
filesHost: undefined,
host: 'localhost',
},
session: {
isSessionRenewChallengePresented: false,
online: false,
offline: true,
isSignedIn: false,
isSignedIntoFirstPartyServer: false,
},
sync: {
syncToken: undefined,
cursorToken: undefined,
lastSyncDate: undefined,
outOfSync: false,
completedOnlineDownloadFirstSync: false,
clientLocked: false,
databaseLoaded: false,
syncLock: false,
dealloced: false,
itemsNeedingSync: [],
itemsNeedingSyncCount: 0,
pendingRequestCount: 0,
},
protections: expect.objectContaining({
getLastSessionLength: undefined,
hasProtectionSources: false,
hasUnprotectedAccessSession: true,
hasBiometricsEnabled: false,
}),
keyRecovery: { queueLength: 0, isProcessingQueue: false },
features: {
roles: [],
features: [],
enabledExperimentalFeatures: [],
needsInitialFeaturesUpdate: true,
completedSuccessfulFeaturesRetrieval: false,
},
migrations: { activeMigrations: [] },
}),
)
})
})
+8 -12
View File
@@ -188,7 +188,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private declare _getRevision: GetRevision
private declare _deleteRevision: DeleteRevision
private internalEventBus!: ExternalServices.InternalEventBusInterface
public internalEventBus!: ExternalServices.InternalEventBusInterface
private eventHandlers: ApplicationObserver[] = []
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1079,15 +1079,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.userService.changePasscode(newPasscode, origination)
}
public getStorageEncryptionPolicy(): ExternalServices.StorageEncryptionPolicy {
return this.diskStorageService.getStorageEncryptionPolicy()
}
public setStorageEncryptionPolicy(encryptionPolicy: ExternalServices.StorageEncryptionPolicy): Promise<void> {
this.diskStorageService.setEncryptionPolicy(encryptionPolicy)
return this.protocolService.repersistAllItems()
}
public enableEphemeralPersistencePolicy(): Promise<void> {
return this.diskStorageService.setPersistencePolicy(ExternalServices.StoragePersistencePolicies.Ephemeral)
}
@@ -1184,13 +1175,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.createSettingsService()
this.createFeaturesService()
this.createComponentManager()
this.createMigrationService()
this.createMfaService()
this.createStatusService()
if (isDesktopDevice(this.deviceInterface)) {
this.createFilesBackupService(this.deviceInterface)
}
this.createMigrationService()
this.createFileService()
this.createIntegrityService()
@@ -1381,6 +1372,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
identifier: this.identifier,
internalEventBus: this.internalEventBus,
legacySessionStorageMapper: this.legacySessionStorageMapper,
backups: this.fileBackups,
})
this.services.push(this.migrationService)
}
@@ -1526,7 +1518,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.diskStorageService = new InternalServices.DiskStorageService(
this.deviceInterface,
this.identifier,
this.environment,
this.internalEventBus,
)
this.services.push(this.diskStorageService)
@@ -1584,6 +1575,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.httpService,
this.sessionStorageMapper,
this.legacySessionStorageMapper,
this.identifier,
this.internalEventBus,
)
this.serviceObservers.push(
@@ -1761,6 +1753,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
device,
this.statusService,
this.options.crypto,
this.storage,
this.sessions,
this.payloadManager,
this.historyManager,
this.internalEventBus,
)
this.services.push(this.filesBackupService)
@@ -1,6 +1,6 @@
import { BackupServiceInterface } from '@standardnotes/files'
import { Environment } from '@standardnotes/models'
import { DeviceInterface, InternalEventBusInterface, EncryptionService } from '@standardnotes/services'
import { SNSessionManager } from '../Services/Session/SessionManager'
import { ApplicationIdentifier } from '@standardnotes/common'
import { ItemManager } from '@Lib/Services/Items/ItemManager'
@@ -13,6 +13,7 @@ export type MigrationServices = {
storageService: DiskStorageService
challengeService: ChallengeService
sessionManager: SNSessionManager
backups?: BackupServiceInterface
itemManager: ItemManager
singletonManager: SNSingletonManager
featuresService: SNFeaturesService
@@ -0,0 +1,51 @@
import {
ApplicationStage,
FileBackupsDirectoryName,
StorageKey,
TextBackupsDirectoryName,
isDesktopDevice,
} from '@standardnotes/services'
import { Migration } from '@Lib/Migrations/Migration'
export class Migration2_167_6 extends Migration {
static override version(): string {
return '2.167.6'
}
protected registerStageHandlers(): void {
this.registerStageHandler(ApplicationStage.Launched_10, async () => {
await this.migrateStorageKeysForDesktopBackups()
this.markDone()
})
}
private async migrateStorageKeysForDesktopBackups(): Promise<void> {
const device = this.services.deviceInterface
if (!isDesktopDevice(device) || !this.services.backups) {
return
}
const fileBackupsEnabled = await device.isLegacyFilesBackupsEnabled()
this.services.storageService.setValue(StorageKey.FileBackupsEnabled, fileBackupsEnabled)
if (fileBackupsEnabled) {
const legacyLocation = await device.getLegacyFilesBackupsLocation()
const newLocation = `${legacyLocation}/${this.services.backups.prependWorkspacePathForPath(
FileBackupsDirectoryName,
)}`
await device.migrateLegacyFileBackupsToNewStructure(newLocation)
this.services.storageService.setValue(StorageKey.FileBackupsLocation, newLocation)
}
const wasLegacyDisabled = await device.wasLegacyTextBackupsExplicitlyDisabled()
if (wasLegacyDisabled) {
this.services.storageService.setValue(StorageKey.TextBackupsEnabled, false)
} else {
const newTextBackupsLocation = `${await device.getLegacyTextBackupsLocation()}/${this.services.backups.prependWorkspacePathForPath(
TextBackupsDirectoryName,
)}`
this.services.storageService.setValue(StorageKey.TextBackupsLocation, newTextBackupsLocation)
this.services.storageService.setValue(StorageKey.TextBackupsEnabled, true)
}
}
}
@@ -0,0 +1,5 @@
## To create a migration:
1. Create a new file inside versions specifiying the would-be version of SNJS that would result when publishing your migration. For example, if the current SNJS version is 1.0.0 in package.json, your migration version should be 1.0.1 to target users below this version.
2. **Important** Export your migration inside the index.ts file.
+10 -2
View File
@@ -3,7 +3,15 @@ import { Migration2_7_0 } from './2_7_0'
import { Migration2_20_0 } from './2_20_0'
import { Migration2_36_0 } from './2_36_0'
import { Migration2_42_0 } from './2_42_0'
import { Migration2_167_6 } from './2_167_6'
export const MigrationClasses = [Migration2_0_15, Migration2_7_0, Migration2_20_0, Migration2_36_0, Migration2_42_0]
export const MigrationClasses = [
Migration2_0_15,
Migration2_7_0,
Migration2_20_0,
Migration2_36_0,
Migration2_42_0,
Migration2_167_6,
]
export { Migration2_0_15, Migration2_7_0, Migration2_20_0, Migration2_36_0, Migration2_42_0 }
export { Migration2_0_15, Migration2_7_0, Migration2_20_0, Migration2_36_0, Migration2_42_0, Migration2_167_6 }
@@ -5,7 +5,12 @@ import { DiskStorageService } from '@Lib/Services/Storage/DiskStorageService'
import { UuidString } from '../../Types/UuidString'
import * as Models from '@standardnotes/models'
import { SNNote } from '@standardnotes/models'
import { AbstractService, DeviceInterface, InternalEventBusInterface } from '@standardnotes/services'
import {
AbstractService,
DeviceInterface,
HistoryServiceInterface,
InternalEventBusInterface,
} from '@standardnotes/services'
/** The amount of revisions per item above which should call for an optimization. */
const DefaultItemRevisionsThreshold = 20
@@ -25,7 +30,7 @@ const LargeEntryDeltaThreshold = 25
* 2. Remote server history. Entries are automatically added by the server and must be
* retrieved per item via an API call.
*/
export class SNHistoryManager extends AbstractService {
export class SNHistoryManager extends AbstractService implements HistoryServiceInterface {
private removeChangeObserver: () => void
/**

Some files were not shown because too many files have changed in this diff Show More