1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-30 20:39:46 +02:00

Compare commits

...

74 Commits

Author SHA1 Message Date
Laurent Cozic
f33088fbe0 Android 2.4.2 2021-09-22 18:12:29 +01:00
Laurent Cozic
31b6d06418 lock files 2021-09-22 17:38:35 +01:00
Laurent Cozic
06cd5ffa2d Server v2.4.9 2021-09-22 17:32:27 +01:00
Laurent Cozic
f3d4d8eaed Desktop release v2.4.8 2021-09-22 17:29:57 +01:00
Gen Neko
dc08e1ded5 All: Translation: Update ja_JP.po (#5488) 2021-09-22 05:57:58 -04:00
reportxx
31c3fec8d8 All: Translation: Update sv.po (#5484) 2021-09-22 05:57:22 -04:00
Laurent Cozic
4487cb85fc Tools: Trying to fix tests 2021-09-21 16:26:56 +01:00
Laurent Cozic
56cac1f729 Desktop: Fixes #5161: Improved plugin search and installing new plugins from China 2021-09-21 16:22:58 +01:00
Laurent Cozic
3ade7ed849 Doc: Clarified Joplin Cloud Education Discount 2021-09-21 13:29:29 +01:00
Laurent Cozic
a7eea9fc21 Fixed session swap handling on server 2021-09-21 12:31:53 +01:00
Laurent Cozic
7fac1941cd Server: Manage subscription entirely from Stripe 2021-09-21 10:51:10 +01:00
Laurent Cozic
061761f224 Server: Clear cookie when account has been deleted to allow viewing login page again 2021-09-21 10:49:41 +01:00
Laurent Cozic
63e88c05d9 Server: Add support for changing user own email 2021-09-20 19:48:17 +01:00
Laurent Cozic
a6b1cffd50 Server: Handle Joplin Cloud failed subscription payments 2021-09-20 17:04:09 +01:00
Laurent Cozic
8cc720963a Fixed typo 2021-09-20 17:04:08 +01:00
Laurent Cozic
da884752a8 Desktop: Fixed Sync Wizard logo images on Windows 2021-09-20 16:29:05 +01:00
Laurent Cozic
818c7d4640 Fixed CSRF handling when impersonating user 2021-09-20 15:38:26 +01:00
Laurent Cozic
4577c9c161 Server: Allow entering coupon code on Stripe checkout page 2021-09-20 15:25:59 +01:00
Laurent Cozic
03b4b6eb2d Server: Allow an admin to impersonate a user 2021-09-20 13:49:38 +01:00
Laurent Cozic
4d38397cd5 Server: Improved user list page 2021-09-20 12:20:18 +01:00
Laurent Cozic
37d446b970 Server: Rename admin button "Send reset password email" to more correct "Send account information email" 2021-09-20 11:53:35 +01:00
Laurent Cozic
c91d4bda3c Server: Redirect to user page after changing a user 2021-09-20 11:48:51 +01:00
Laurent Cozic
3e537967ee Tools: Added way to test creating Basic or Pro Joplin Server subscription 2021-09-20 11:46:24 +01:00
Laurent Cozic
0cbc261051 Chore: Remove last remnants of discontinued Nextcloud App 2021-09-19 19:15:10 +01:00
Laurent Cozic
542fdb496a Set default options 2021-09-19 18:54:14 +01:00
Laurent Cozic
d850eedd78 Server: Link to Joplin Cloud signup page on login page 2021-09-19 18:45:16 +01:00
Laurent Cozic
9429b51694 Chore: Fixed regression on mobile app 2021-09-19 15:27:23 +01:00
Laurent Cozic
72e58ee195 Tools: Trying to fix TaskService test on CI 2021-09-19 15:06:09 +01:00
Helmut K. C. Tessarek
56be4d59f4 Update translations 2021-09-19 08:37:08 -04:00
Laurent Cozic
bb740c75ec Desktop release v2.4.7 2021-09-19 13:04:59 +01:00
Laurent Cozic
4244f712e1 Merge branch 'dev' into release-2.4 2021-09-19 13:04:36 +01:00
Laurent Cozic
e447acc076 Desktop: Resolves #5440: Do not escape content when copying from Rich Text editor 2021-09-19 13:00:06 +01:00
Laurent Cozic
87f83236cf Desktop: Fixes #5480: Underline was not applied when using Cmd+U in Rich Text editor 2021-09-19 12:35:06 +01:00
Laurent Cozic
6d981864ef Desktop: Fixes #5461: Editor max width was not always applied in Rich Text editor 2021-09-19 12:04:23 +01:00
Nikhil Gautam
0d40026d8b Desktop: Resolves #5299: Display 0/0 when no search results are found in editor (#5360) 2021-09-19 11:37:33 +01:00
Caleb John
7a9ec627ee Desktop: Resolves #5233: Fire resize event whenever the layout changes (#5344)
* Fire resize event whenever the layout changes

This solves an issue where the markdown editor was changing size
physically, but the refresh function wasn't being called so the
editor would lose track of it's size and place the cursor wrongly.
The editor was able to correctly resize when the window resize event
was fired, but this didn't happen when the sidebars were toggled.
The solution implemented here is to hook in to the function where
layout props are changed, and emit a resize event there.
This means that anytime the layout changes (whether or not it affects
sizing), the resize event will be fired.
2021-09-19 11:36:23 +01:00
Marph
2d72d1435e Desktop: Support for user-data-dir flag (#5467)
This flag is passed by chromedriver.
2021-09-19 11:34:04 +01:00
Helmut K. C. Tessarek
12ec7b0c1d macOS: Added Cmd+Backspace shortcut to delete line (#5478) 2021-09-19 11:32:58 +01:00
JackGruber
afe1cf747d All: Fixes #5444: Misinterpreted search term after filter in quotation marks (#5445) 2021-09-19 11:31:38 +01:00
a1346054
c99aba0dff Desktop: Linux: Installer: properly quote variables (#5476) 2021-09-18 21:59:26 -04:00
Laurent Cozic
43c594b6b2 Server, Desktop: Sync deleted items first to allow fixing oversized accounts 2021-09-18 15:02:24 +01:00
Laurent Cozic
024967ce60 Server: Fixed calculating total item size after an item has been deleted 2021-09-18 14:46:10 +01:00
Laurent Cozic
cd877f64cd Server: Improved support for background tasks and added admin UI to view them 2021-09-18 11:29:24 +01:00
Laurent Cozic
f91b4edb30 Tools: Tweak to stress test script 2021-09-17 18:27:25 +01:00
Laurent Cozic
b56177a4e3 Tools: Added tools to stress test Joplin Server 2021-09-17 10:59:10 +01:00
Laurent Cozic
4e70ca6fd0 Server: Exclude certain queries from slow log 2021-09-16 17:36:06 +01:00
Laurent Cozic
2e04656b54 Server v2.4.8 2021-09-15 23:17:14 +01:00
Laurent Cozic
5e8b7420ff Server: Added support for app level slow SQL query log 2021-09-15 23:14:14 +01:00
Laurent Cozic
8ae4e30fd2 Server v2.4.7 2021-09-15 16:58:59 +01:00
Laurent Cozic
3ce947e82c Server: Fixed handling of brute force limiter by getting correct user IP 2021-09-15 16:57:18 +01:00
Laurent Cozic
c2298213d7 Server: Improve flag logic 2021-09-15 12:06:01 +01:00
Abdunnasir Saeed
9679f03cfa All: Translation: Update ar.po (#5464)
A complete Arabic translation
2021-09-15 06:39:50 -04:00
Laurent Cozic
3cddac3931 Server v2.4.6 2021-09-14 16:02:51 +01:00
Laurent Cozic
41c1e3bec9 Server: Fix transaction deadlock logging 2021-09-14 15:59:01 +01:00
Laurent Cozic
25c5892e74 Server v2.4.5 2021-09-14 13:02:56 +01:00
Laurent Cozic
a661a73511 Revert "Server: Enable multi platform builds (amd64, armv7 and arm64) (#5338)"
This reverts commit ab134807ea.

Does not build:

https://github.com/laurent22/joplin/runs/3597996286?check_suite_focus=true#step:8:388
2021-09-14 13:01:33 +01:00
Laurent Cozic
b00959e143 Server v2.4.4 2021-09-14 12:16:47 +01:00
Laurent Cozic
f6f5d6808d Merge branch 'dev' into release-2.4 2021-09-14 12:07:04 +01:00
Laurent Cozic
01b653fc34 Server: Add transaction info to debug deadlock issues 2021-09-14 12:05:29 +01:00
Laurent Cozic
4e7fe66883 Server: Add link to Stripe subscription page to manage payment details 2021-09-13 12:30:36 +01:00
mrkaato
cd99e675d9 All: Translation: Update fi_FI.po (#5452) 2021-09-12 13:00:30 -04:00
Laurent Cozic
a7130ce17a Tools: Added script to compile SASS files 2021-09-12 16:35:08 +01:00
Laurent Cozic
20f8743079 Tools: Upgrade back package-lock files to v2 2021-09-12 16:34:03 +01:00
Laurent Cozic
660b53575e Doc: Update sponsors 2021-09-12 13:08:02 +01:00
Kenichi Kobayashi
6c43b78496 All: Fixes #5447: Plugin onNoteSelectionChange() is triggered twice after a search (#5449) 2021-09-12 11:40:14 +01:00
Helmut K. C. Tessarek
9d5d891fe3 Desktop, Mobile: Resolves #5295: Update Mermaid 8.10.2 -> 8.12.1 and fix gitGraph crash (#5448) 2021-09-11 19:47:01 +01:00
Laurent Cozic
96ac12b460 Chore: Converted encryption config screens to React Hooks to share logic between desktop and mobile 2021-09-10 19:05:47 +01:00
Laurent Cozic
4b93664240 Desktop release v2.4.6 2021-09-09 19:25:54 +01:00
Laurent Cozic
a2c6461af8 Desktop: Fix handling of disabled master keys when enabling E2EE 2021-09-09 19:24:52 +01:00
Laurent Cozic
d33b99cffb Desktop: Fix handling of disabled master keys when enabling E2EE 2021-09-09 18:46:58 +01:00
Laurent Cozic
267c32143b Desktop: Fix handling of disabled master keys when enabling E2EE 2021-09-09 18:42:00 +01:00
Laurent Cozic
9260b2a9ab Plugins: Add support for enabledConditions when creating menu item from command 2021-09-09 14:44:16 +01:00
Xavi Ivars
0a54854f54 All: Translation: Update ca.po (#5432)
* Update ca.po

* Update ca.po

A ton of improvements
2021-09-08 16:33:30 -04:00
xnumad
496039f15c Doc: Update doc links (#5425) 2021-09-08 14:18:01 +01:00
237 changed files with 21881 additions and 19479 deletions

View File

@@ -76,6 +76,9 @@ packages/app-cli/app/command-e2ee.js.map
packages/app-cli/app/command-settingschema.d.ts
packages/app-cli/app/command-settingschema.js
packages/app-cli/app/command-settingschema.js.map
packages/app-cli/app/command-testing.d.ts
packages/app-cli/app/command-testing.js
packages/app-cli/app/command-testing.js.map
packages/app-cli/app/services/plugins/PluginRunner.d.ts
packages/app-cli/app/services/plugins/PluginRunner.js
packages/app-cli/app/services/plugins/PluginRunner.js.map
@@ -109,6 +112,9 @@ packages/app-cli/tests/services/plugins/sandboxProxy.js.map
packages/app-cli/tests/testUtils.d.ts
packages/app-cli/tests/testUtils.js
packages/app-cli/tests/testUtils.js.map
packages/app-cli/tools/populateDatabase.d.ts
packages/app-cli/tools/populateDatabase.js
packages/app-cli/tools/populateDatabase.js.map
packages/app-desktop/ElectronAppWrapper.d.ts
packages/app-desktop/ElectronAppWrapper.js
packages/app-desktop/ElectronAppWrapper.js.map
@@ -208,9 +214,9 @@ packages/app-desktop/gui/DialogTitle.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
@@ -931,9 +937,9 @@ packages/lib/commands/index.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
packages/lib/components/shared/encryption-config-shared.d.ts
packages/lib/components/shared/encryption-config-shared.js
packages/lib/components/shared/encryption-config-shared.js.map
packages/lib/components/EncryptionConfigScreen/utils.d.ts
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/EncryptionConfigScreen/utils.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map

View File

@@ -35,14 +35,6 @@ jobs:
sudo apt-get update || true
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
# the next line enables multi-architecture support for docker, it basically makes it use qemu for non native platforms
# See https://hub.docker.com/r/tonistiigi/binfmt for more info
docker run --privileged --rm tonistiigi/binfmt --install all
# this just prints the info about what platforms are supported in the builder (can help debugging if something isn't working right)
# and also proves the above worked properly
sudo docker buildx ls
- uses: actions/checkout@v2
- uses: olegtarasov/get-tag@v2.1
- uses: actions/setup-node@v2

18
.gitignore vendored
View File

@@ -61,6 +61,9 @@ packages/app-cli/app/command-e2ee.js.map
packages/app-cli/app/command-settingschema.d.ts
packages/app-cli/app/command-settingschema.js
packages/app-cli/app/command-settingschema.js.map
packages/app-cli/app/command-testing.d.ts
packages/app-cli/app/command-testing.js
packages/app-cli/app/command-testing.js.map
packages/app-cli/app/services/plugins/PluginRunner.d.ts
packages/app-cli/app/services/plugins/PluginRunner.js
packages/app-cli/app/services/plugins/PluginRunner.js.map
@@ -94,6 +97,9 @@ packages/app-cli/tests/services/plugins/sandboxProxy.js.map
packages/app-cli/tests/testUtils.d.ts
packages/app-cli/tests/testUtils.js
packages/app-cli/tests/testUtils.js.map
packages/app-cli/tools/populateDatabase.d.ts
packages/app-cli/tools/populateDatabase.js
packages/app-cli/tools/populateDatabase.js.map
packages/app-desktop/ElectronAppWrapper.d.ts
packages/app-desktop/ElectronAppWrapper.js
packages/app-desktop/ElectronAppWrapper.js.map
@@ -193,9 +199,9 @@ packages/app-desktop/gui/DialogTitle.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen.js.map
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.d.ts
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js.map
packages/app-desktop/gui/ErrorBoundary.d.ts
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ErrorBoundary.js.map
@@ -916,9 +922,9 @@ packages/lib/commands/index.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
packages/lib/components/shared/encryption-config-shared.d.ts
packages/lib/components/shared/encryption-config-shared.js
packages/lib/components/shared/encryption-config-shared.js.map
packages/lib/components/EncryptionConfigScreen/utils.d.ts
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/EncryptionConfigScreen/utils.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map

View File

@@ -137,8 +137,8 @@ fi
#-----------------------------------------------------
print 'Downloading Joplin...'
TEMP_DIR=$(mktemp -d)
wget -O ${TEMP_DIR}/Joplin.AppImage https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage
wget -O ${TEMP_DIR}/joplin.png https://joplinapp.org/images/Icon512.png
wget -O "${TEMP_DIR}/Joplin.AppImage" "https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage"
wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
#-----------------------------------------------------
print 'Installing Joplin...'
@@ -149,7 +149,7 @@ rm -f ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop ~/.joplin/
mkdir -p ~/.joplin/
# Download the latest version
mv ${TEMP_DIR}/Joplin.AppImage ~/.joplin/Joplin.AppImage
mv "${TEMP_DIR}/Joplin.AppImage" ~/.joplin/Joplin.AppImage
# Gives execution privileges
chmod +x ~/.joplin/Joplin.AppImage
@@ -159,7 +159,7 @@ print "${COLOR_GREEN}OK${COLOR_RESET}"
#-----------------------------------------------------
print 'Installing icon...'
mkdir -p ~/.local/share/icons/hicolor/512x512/apps
mv ${TEMP_DIR}/joplin.png ~/.local/share/icons/hicolor/512x512/apps/joplin.png
mv "${TEMP_DIR}/joplin.png" ~/.local/share/icons/hicolor/512x512/apps/joplin.png
print "${COLOR_GREEN}OK${COLOR_RESET}"
# Detect desktop environment
@@ -222,7 +222,7 @@ fi
print "${COLOR_GREEN}Joplin version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
# Record version
echo $RELEASE_VERSION > ~/.joplin/VERSION
echo "$RELEASE_VERSION" > ~/.joplin/VERSION
#-----------------------------------------------------
if [[ "$SHOW_CHANGELOG" == true ]]; then
@@ -232,5 +232,5 @@ fi
#-----------------------------------------------------
print "Cleaning up..."
rm -rf $TEMP_DIR
rm -rf "$TEMP_DIR"
print "${COLOR_GREEN}OK${COLOR_RESET}"

View File

@@ -504,47 +504,47 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 99%
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 28%
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 71%
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 56%
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 55%
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Michal Stanke](mailto:michal@stanke.cz) | 94%
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [marph91](mailto:martin.d@andix.de) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 54%
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Mora](mailto:francisco.m.collao@gmail.com) | 95%
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 31%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 96%
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 36%
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 84%
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 87%
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Albano Battistella](mailto:albano_battistella@hotmail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 83%
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 86%
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | Alexander Dawson | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 67%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Nicolas Suzuki](mailto:nicolas.suzuki@pm.me) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 63%
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 91%
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 62%
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 90%
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 42%
<img src="https://joplinapp.org/images/flags/country-4x3/vi.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 96%
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 92%
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 89%
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 81%
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 80%
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [南宫小骏](mailto:jackytsu@vip.qq.com) | 99%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Po-Chiang Chao](mailto:BobChao%29%20%28bobchao@gmail.com) | 94%
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [SiderealArt](mailto:nelson22768384@gmail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 95%
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 94%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Contributors

View File

@@ -23,4 +23,6 @@ tests/support/dropbox-auth.txt
tests/support/nextcloud-auth.json
tests/support/onedrive-auth.txt
build/
patches/
patches/
createUsers-*.txt
tools/temp/

View File

@@ -89,7 +89,7 @@ cliUtils.makeCommandArgs = function(cmd, argv) {
flags = cliUtils.parseFlags(flags);
if (!flags.arg) {
booleanFlags.push(flags.short);
if (flags.short) booleanFlags.push(flags.short);
if (flags.long) booleanFlags.push(flags.long);
}

View File

@@ -0,0 +1,95 @@
const { BaseCommand } = require('./base-command.js');
import { reg } from '@joplin/lib/registry';
import Note from '@joplin/lib/models/Note';
import uuid from '@joplin/lib/uuid';
import populateDatabase from '@joplin/lib/services/debug/populateDatabase';
function randomElement(array: any[]): any {
if (!array.length) return null;
return array[Math.floor(Math.random() * array.length)];
}
function itemCount(args: any) {
const count = Number(args.arg0);
if (!count || isNaN(count)) throw new Error('Note count must be specified');
return count;
}
class Command extends BaseCommand {
usage() {
return 'testing <command> [arg0]';
}
description() {
return 'testing';
}
enabled() {
return false;
}
options(): any[] {
return [
['--folder-count <count>', 'Folders to create'],
['--note-count <count>', 'Notes to create'],
['--tag-count <count>', 'Tags to create'],
['--tags-per-note <count>', 'Tags per note'],
['--silent', 'Silent'],
];
}
async action(args: any) {
const { command, options } = args;
if (command === 'populate') {
await populateDatabase(reg.db(), {
folderCount: options['folder-count'],
noteCount: options['note-count'],
tagCount: options['tag-count'],
tagsPerNote: options['tags-per-note'],
silent: options['silent'],
});
}
const promises: any[] = [];
if (command === 'createRandomNotes') {
const noteCount = itemCount(args);
for (let i = 0; i < noteCount; i++) {
promises.push(Note.save({
title: `Note ${uuid.createNano()}`,
}));
}
}
if (command === 'updateRandomNotes') {
const noteCount = itemCount(args);
const noteIds = await Note.allIds();
for (let i = 0; i < noteCount; i++) {
const noteId = randomElement(noteIds);
promises.push(Note.save({
id: noteId,
title: `Note ${uuid.createNano()}`,
}));
}
}
if (command === 'deleteRandomNotes') {
const noteCount = itemCount(args);
const noteIds = await Note.allIds();
for (let i = 0; i < noteCount; i++) {
const noteId = randomElement(noteIds);
promises.push(Note.delete(noteId));
}
}
await Promise.all(promises);
}
}
module.exports = Command;

52
packages/app-cli/createUsers.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
# Start the server with:
#
# JOPLIN_IS_TESTING=1 npm run start-dev
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# curl --data '{"action": "clearDatabase"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
# SMALL
# curl --data '{"action": "createTestUsers", "count": 400, "fromNum": 1}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
NUM=398
while [ "$NUM" -lt 400 ]; do
NUM=$(( NUM + 1 ))
echo "User $NUM"
CMD_FILE="$SCRIPT_DIR/createUsers-$NUM.txt"
PROFILE_DIR=~/.config/joplindev-testing-$NUM
USER_EMAIL="user$NUM@example.com"
rm -rf "$CMD_FILE" "$PROFILE_DIR"
touch "$CMD_FILE"
FLAG_FOLDER_COUNT=100
FLAG_NOTE_COUNT=1000
FLAG_TAG_COUNT=20
if [ "$NUM" -gt 300 ]; then
FLAG_FOLDER_COUNT=2000
FLAG_NOTE_COUNT=10000
FLAG_TAG_COUNT=200
fi
if [ "$NUM" -gt 399 ]; then
FLAG_FOLDER_COUNT=10000
FLAG_NOTE_COUNT=150000
FLAG_TAG_COUNT=2000
fi
echo "testing populate --silent --folder-count $FLAG_FOLDER_COUNT --note-count $FLAG_NOTE_COUNT --tag-count $FLAG_TAG_COUNT" >> "$CMD_FILE"
echo "config keychain.supported 0" >> "$CMD_FILE"
echo "config sync.target 10" >> "$CMD_FILE"
echo "config sync.10.username $USER_EMAIL" >> "$CMD_FILE"
echo "config sync.10.password hunter1hunter2hunter3" >> "$CMD_FILE"
echo "sync" >> "$CMD_FILE"
npm start -- --profile "$PROFILE_DIR" batch "$CMD_FILE"
done

View File

@@ -10,6 +10,7 @@
"test-ci": "jest --config=jest.config.js --forceExit",
"build": "gulp build",
"start": "gulp build -L && node \"build/main.js\" --stack-trace-enabled --log-level debug --env dev",
"start-no-build": "node \"build/main.js\" --stack-trace-enabled --log-level debug --env dev",
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json"
},

View File

@@ -84,4 +84,10 @@ describe('HtmlToMd', function() {
}
}));
it('should allow disabling escape', async () => {
const htmlToMd = new HtmlToMd();
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: true })).toBe('https://test.com/1_2_3.pdf');
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: false })).toBe('https://test.com/1\\_2\\_3.pdf');
});
});

View File

@@ -1 +1 @@
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <s>Strike</s>
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <span style="text-decoration: underline;">Insert alt</span> <s>Strike</s>

View File

@@ -1 +1 @@
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> ~~Strike~~
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <ins>Insert alt</ins> ~~Strike~~

View File

@@ -0,0 +1,108 @@
// This script can be used to simulate a running production environment, by
// having multiple users in parallel changing notes and synchronising.
//
// To get it working:
//
// - Run the Postgres database -- `sudo docker-compose --file docker-compose.db-dev.yml up`
// - Update the DB parameters in ~/joplin-credentials/server.env to use the dev
// database
// - Run the server - `JOPLIN_IS_TESTING=1 npm run start-dev`
// - Then run this script - `node populateDatabase.js`
//
// Currently it doesn't actually create the users, so that should be done using:
//
// curl --data '{"action": "createTestUsers", "count": 400, "fromNum": 1}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
//
// That will create n users with email `user<n>@example.com`
import * as fs from 'fs-extra';
import { homedir } from 'os';
import { execCommand2 } from '@joplin/tools/tool-utils';
import { chdir } from 'process';
const minUserNum = 1;
const maxUserNum = 400;
const cliDir = `${__dirname}/..`;
const tempDir = `${__dirname}/temp`;
function randomInt(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const processing_: Record<number, boolean> = {};
const processUser = async (userNum: number) => {
if (processing_[userNum]) {
console.info(`User already being processed: ${userNum} - skipping`);
return;
}
processing_[userNum] = true;
try {
const userEmail = `user${userNum}@example.com`;
const userPassword = 'hunter1hunter2hunter3';
const commandFile = `${tempDir}/populateDatabase-${userNum}.txt`;
const profileDir = `${homedir()}/.config/joplindev-populate/joplindev-testing-${userNum}`;
const commands: string[] = [];
const jackpot = Math.random() >= 0.95 ? 100 : 1;
commands.push(`testing createRandomNotes ${randomInt(1, 500 * jackpot)}`);
commands.push(`testing updateRandomNotes ${randomInt(1, 1500 * jackpot)}`);
commands.push(`testing deleteRandomNotes ${randomInt(1, 200 * jackpot)}`);
commands.push('config keychain.supported 0');
commands.push('config sync.target 10');
commands.push(`config sync.10.username ${userEmail}`);
commands.push(`config sync.10.password ${userPassword}`);
commands.push('sync');
await fs.writeFile(commandFile, commands.join('\n'), 'utf8');
await chdir(cliDir);
await execCommand2(['npm', 'run', 'start-no-build', '--', '--profile', profileDir, 'batch', commandFile]);
} catch (error) {
console.error(`Could not process user ${userNum}:`, error);
} finally {
delete processing_[userNum];
}
};
const waitForProcessing = (count: number) => {
return new Promise((resolve) => {
const iid = setInterval(() => {
if (Object.keys(processing_).length <= count) {
clearInterval(iid);
resolve(null);
}
}, 100);
});
};
const main = async () => {
await fs.mkdirp(tempDir);
// Build the app once before starting, because we'll use start-no-build to
// run the scripts (faster)
await execCommand2(['npm', 'run', 'build']);
const focusUserNum = 400;
while (true) {
let userNum = randomInt(minUserNum, maxUserNum);
if (Math.random() >= .7) userNum = focusUserNum;
void processUser(userNum);
await waitForProcessing(10);
}
};
main().catch((error) => {
console.error('Fatal error', error);
process.exit(1);
});

View File

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 441 B

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -6,7 +6,7 @@ import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge';
import Setting, { AppType, SyncStartupOperation } from '@joplin/lib/models/Setting';
import control_PluginsStates from './controls/plugins/PluginsStates';
import EncryptionConfigScreen from '../EncryptionConfigScreen';
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
@@ -42,9 +42,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.sidebar_selectionChange = this.sidebar_selectionChange.bind(this);
this.checkSyncConfig_ = this.checkSyncConfig_.bind(this);
// this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this);
this.showLogButton_click = this.showLogButton_click.bind(this);
this.nextcloudAppHelpLink_click = this.nextcloudAppHelpLink_click.bind(this);
this.onCancelClick = this.onCancelClick.bind(this);
this.onSaveClick = this.onSaveClick.bind(this);
this.onApplyClick = this.onApplyClick.bind(this);
@@ -58,19 +55,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
await shared.checkSyncConfig(this, this.state.settings);
}
// async checkNextcloudAppButton_click() {
// this.setState({ showNextcloudAppLog: true });
// await shared.checkNextcloudApp(this, this.state.settings);
// }
showLogButton_click() {
this.setState({ showNextcloudAppLog: true });
}
nextcloudAppHelpLink_click() {
bridge().openExternal('https://joplinapp.org/nextcloud_app');
}
UNSAFE_componentWillMount() {
this.setState({ settings: this.props.settings });
}

View File

@@ -9,6 +9,7 @@ import PluginBox, { InstallState } from './PluginBox';
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import { _ } from '@joplin/lib/locale';
import useOnInstallHandler from './useOnInstallHandler';
import { themeStyle } from '@joplin/lib/theme';
const Root = styled.div`
`;
@@ -100,6 +101,13 @@ export default function(props: Props) {
}
}
const renderContentSourceInfo = () => {
if (props.repoApi().isUsingDefaultContentUrl) return null;
const theme = themeStyle(props.themeId);
const url = new URL(props.repoApi().contentBaseUrl);
return <div style={{ ...theme.textStyleMinor, marginTop: 5, fontSize: theme.fontSize }}>{_('Content provided by %s', url.hostname)}</div>;
};
return (
<Root>
<div style={{ marginBottom: 10, width: props.maxWidth }}>
@@ -112,6 +120,7 @@ export default function(props: Props) {
placeholder={props.disabled ? _('Please wait...') : _('Search for plugins...')}
disabled={props.disabled}
/>
{renderContentSourceInfo()}
</div>
<ResultsRoot>

View File

@@ -1,398 +0,0 @@
const React = require('react');
const { connect } = require('react-redux');
import Setting from '@joplin/lib/models/Setting';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import { State } from '@joplin/lib/reducer';
import shim from '@joplin/lib/shim';
import dialogs from './dialogs';
import bridge from '../services/bridge';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import MasterKey from '@joplin/lib/models/MasterKey';
import StyledInput from './style/StyledInput';
import Button, { ButtonLevel } from './Button/Button';
import styled from 'styled-components';
const MasterPasswordInput = styled(StyledInput)`
min-width: 300px;
align-items: center;
`;
interface Props {}
class EncryptionConfigScreenComponent extends React.Component<Props> {
constructor(props: Props) {
super(props);
shared.initialize(this, props);
}
componentWillUnmount() {
this.isMounted_ = false;
shared.componentWillUnmount();
}
componentDidMount() {
this.isMounted_ = true;
shared.componentDidMount(this);
}
componentDidUpdate(prevProps: Props) {
shared.componentDidUpdate(this, prevProps);
}
async checkPasswords() {
return shared.checkPasswords(this);
}
private renderMasterKey(mk: MasterKeyEntity, _isDefault: boolean) {
const theme = themeStyle(this.props.themeId);
const onToggleEnabledClick = () => {
return shared.onToggleEnabledClick(this, mk);
};
const passwordStyle = {
color: theme.color,
backgroundColor: theme.backgroundColor,
border: '1px solid',
borderColor: theme.dividerColor,
};
const onSaveClick = () => {
return shared.onSavePasswordClick(this, mk);
};
const onPasswordChange = (event: any) => {
return shared.onPasswordChange(this, mk, event.target.value);
};
const renderPasswordInput = (masterKeyId: string) => {
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
return (
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
({_('Master password')})
</td>
);
} else {
return (
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onPasswordChange(event)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSaveClick()}>
{_('Save')}
</button>
</td>
);
}
};
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const isActive = this.props.activeMasterKeyId === mk.id;
const activeIcon = isActive ? '✔' : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
return (
<tr key={mk.id}>
<td style={theme.textStyle}>{activeIcon}</td>
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
{renderPasswordInput(mk.id)}
<td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}>
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick()}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
</td>
</tr>
);
}
renderNeedUpgradeSection() {
if (!shim.isElectron()) return null;
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(this.props.masterKeys);
if (!needUpgradeMasterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const rows = [];
const comp = this;
for (const mk of needUpgradeMasterKeys) {
rows.push(
<tr key={mk.id}>
<td style={theme.textStyle}>{mk.id}</td>
<td><button onClick={() => shared.upgradeMasterKey(comp, mk)} style={theme.buttonStyle}>Upgrade</button></td>
</tr>
);
}
return (
<div>
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Upgrade')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
}
renderReencryptData() {
if (!shim.isElectron()) return null;
if (!this.props.shouldReencrypt) return null;
const theme = themeStyle(this.props.themeId);
const buttonLabel = _('Re-encrypt data');
const intro = this.props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
t = t.replace(/\n\n/g, '</p><p>');
t = t.replace(/\n/g, '<br>');
t = `<p>${t}</p>`;
return (
<div>
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
<span style={{ marginRight: 10 }}>
<button onClick={() => shared.reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
</span>
{ !this.props.shouldReencrypt ? null : <button onClick={() => shared.dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
</div>
);
}
private renderMasterKeySection(masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) {
const theme = themeStyle(this.props.themeId);
const mkComps = [];
const showTable = isEnabledMasterKeys || this.state.showDisabledMasterKeys;
const latestMasterKey = MasterKey.latest();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(mk, isEnabledMasterKeys && latestMasterKey && mk.id === latestMasterKey.id));
}
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => shared.toggleShowDisabledMasterKeys(this) } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
const tableComp = !showTable ? null : (
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('Active')}</th>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Date')}</th>
<th style={theme.textStyle}>{_('Password')}</th>
<th style={theme.textStyle}>{_('Valid')}</th>
<th style={theme.textStyle}>{_('Actions')}</th>
</tr>
{mkComps}
</tbody>
</table>
);
if (mkComps.length) {
return (
<div>
{headerComp}
{tableComp}
{infoComp}
</div>
);
}
return null;
}
private renderMasterPassword() {
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const onMasterPasswordSave = async () => {
shared.onMasterPasswordSave(this);
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
alert('Password is invalid. Please try again.');
}
};
if (this.state.passwordChecks['master']) {
return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span style={theme.textStyle}>{_('Master password:')}</span>&nbsp;&nbsp;
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}> {_('Loaded')}</span>
</div>
);
} else {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={theme.textStyle}> {'The master password is not set or is invalid. Please type it below:'}</span>
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={this.state.masterPasswordInput} onChange={(event: any) => shared.onMasterPasswordChange(this, event.target.value)} />{' '}
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
</div>
</div>
);
}
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys: MasterKeyEntity[] = this.props.masterKeys;
const containerStyle = Object.assign({}, theme.containerStyle, {
padding: theme.configScreenPadding,
overflow: 'auto',
backgroundColor: theme.backgroundColor3,
});
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
const isEnabled = getEncryptionEnabled();
let masterKey = getDefaultMasterKey();
// If the user has explicitly disabled the master key, we generate a
// new one. Needed for one the password has been forgotten.
if (!masterKey.enabled) masterKey = null;
let answer = null;
if (isEnabled) {
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
} else {
const msg = shared.enableEncryptionConfirmationMessages(masterKey);
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
}
if (!answer) return;
try {
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
} catch (error) {
await dialogs.alert(error.message);
}
};
const decryptedItemsInfo = <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p>;
const toggleButton = (
<button
style={theme.buttonStyle}
onClick={() => {
void onToggleButtonClick();
}}
>
{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
</button>
);
const needUpgradeSection = this.renderNeedUpgradeSection();
const reencryptDataSection = this.renderReencryptData();
const enabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => masterKeyEnabled(mk)), true);
const disabledMasterKeySection = this.renderMasterKeySection(masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<tr key={id}>
<td style={theme.textStyle}>{id}</td>
</tr>
);
}
nonExistingMasterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
}
return (
<div>
<div style={containerStyle}>
{
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
<p style={theme.textStyle}>
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
<a
onClick={() => {
bridge().openExternal('https://joplinapp.org/e2ee/');
}}
href="#"
style={theme.urlStyle}
>
https://joplinapp.org/e2ee/
</a>
</p>
</div>
}
<h1 style={theme.h1Style}>{_('Status')}</h1>
<p style={theme.textStyle}>
{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p>
{this.renderMasterPassword()}
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
{this.props.shouldReencrypt ? reencryptDataSection : null}
{enabledMasterKeySection}
{disabledMasterKeySection}
{nonExistingMasterKeySection}
{!this.props.shouldReencrypt ? reencryptDataSection : null}
</div>
</div>
);
}
}
const mapStateToProps = (state: State) => {
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
return {
themeId: state.settings.theme,
masterKeys: syncInfo.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: syncInfo.e2ee,
activeMasterKeyId: syncInfo.activeMasterKeyId,
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
};
const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent);
export default EncryptionConfigScreen;

View File

@@ -0,0 +1,366 @@
const React = require('react');
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shim from '@joplin/lib/shim';
import dialogs from '../dialogs';
import bridge from '../../services/bridge';
import { decryptedStatText, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import StyledInput from '../style/StyledInput';
import Button, { ButtonLevel } from '../Button/Button';
import styled from 'styled-components';
import { useCallback, useMemo } from 'react';
import { connect } from 'react-redux';
import { AppState } from '../../app.reducer';
import Setting from '@joplin/lib/models/Setting';
const MasterPasswordInput = styled(StyledInput)`
min-width: 300px;
align-items: center;
`;
interface Props {
themeId: any;
masterKeys: MasterKeyEntity[];
passwords: Record<string, string>;
notLoadedMasterKeys: string[];
encryptionEnabled: boolean;
shouldReencrypt: boolean;
activeMasterKeyId: string;
masterPassword: string;
}
const EncryptionConfigScreen = (props: Props) => {
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
const theme: any = useMemo(() => {
return themeStyle(props.themeId);
}, [props.themeId]);
const stats = useStats();
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
const onUpgradeMasterKey = useCallback((mk: MasterKeyEntity) => {
void upgradeMasterKey(mk, passwordChecks, props.passwords);
}, [passwordChecks, props.passwords]);
const renderNeedUpgradeSection = () => {
if (!shim.isElectron()) return null;
const needUpgradeMasterKeys = EncryptionService.instance().masterKeysThatNeedUpgrading(props.masterKeys);
if (!needUpgradeMasterKeys.length) return null;
const theme = themeStyle(props.themeId);
const rows = [];
for (const mk of needUpgradeMasterKeys) {
rows.push(
<tr key={mk.id}>
<td style={theme.textStyle}>{mk.id}</td>
<td><button onClick={() => onUpgradeMasterKey(mk)} style={theme.buttonStyle}>Upgrade</button></td>
</tr>
);
}
return (
<div>
<h1 style={theme.h1Style}>{_('Master keys that need upgrading')}</h1>
<p style={theme.textStyle}>{_('The following master keys use an out-dated encryption algorithm and it is recommended to upgrade them. The upgraded master key will still be able to decrypt and encrypt your data as usual.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Upgrade')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
};
const renderReencryptData = () => {
if (!shim.isElectron()) return null;
if (!props.shouldReencrypt) return null;
const theme = themeStyle(props.themeId);
const buttonLabel = _('Re-encrypt data');
const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
t = t.replace(/\n\n/g, '</p><p>');
t = t.replace(/\n/g, '<br>');
t = `<p>${t}</p>`;
return (
<div>
<h1 style={theme.h1Style}>{_('Re-encryption')}</h1>
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
<span style={{ marginRight: 10 }}>
<button onClick={() => void reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
</span>
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
</div>
);
};
const renderMasterKey = (mk: MasterKeyEntity) => {
const theme = themeStyle(props.themeId);
const passwordStyle = {
color: theme.color,
backgroundColor: theme.backgroundColor,
border: '1px solid',
borderColor: theme.dividerColor,
};
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
const isActive = props.activeMasterKeyId === mk.id;
const activeIcon = isActive ? '✔' : '';
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
const renderPasswordInput = (masterKeyId: string) => {
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
return (
<td style={{ ...theme.textStyle, color: theme.colorFaded, fontStyle: 'italic' }}>
({_('Master password')})
</td>
);
} else {
return (
<td style={theme.textStyle}>
<input type="password" style={passwordStyle} value={password} onChange={event => onInputPasswordChange(mk, event.target.value)} />{' '}
<button style={theme.buttonStyle} onClick={() => onSavePasswordClick(mk, props.passwords)}>
{_('Save')}
</button>
</td>
);
}
};
return (
<tr key={mk.id}>
<td style={theme.textStyle}>{activeIcon}</td>
<td style={theme.textStyle}>{mk.id}<br/>{_('Source: ')}{mk.source_application}</td>
<td style={theme.textStyle}>{_('Created: ')}{time.formatMsToLocal(mk.created_time)}<br/>{_('Updated: ')}{time.formatMsToLocal(mk.updated_time)}</td>
{renderPasswordInput(mk.id)}
<td style={theme.textStyle}>{passwordOk}</td>
<td style={theme.textStyle}>
<button style={theme.buttonStyle} onClick={() => onToggleEnabledClick(mk)}>{masterKeyEnabled(mk) ? _('Disable') : _('Enable')}</button>
</td>
</tr>
);
};
const renderMasterKeySection = (masterKeys: MasterKeyEntity[], isEnabledMasterKeys: boolean) => {
const theme = themeStyle(props.themeId);
const mkComps = [];
const showTable = isEnabledMasterKeys || showDisabledMasterKeys;
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(renderMasterKey(mk));
}
const headerComp = isEnabledMasterKeys ? <h1 style={theme.h1Style}>{_('Master Keys')}</h1> : <a onClick={() => toggleShowDisabledMasterKeys() } style={{ ...theme.urlStyle, display: 'inline-block', marginBottom: 10 }} href="#">{showTable ? _('Hide disabled master keys') : _('Show disabled master keys')}</a>;
const infoComp = isEnabledMasterKeys ? <p style={theme.textStyle}>{'Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.'}</p> : null;
const tableComp = !showTable ? null : (
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('Active')}</th>
<th style={theme.textStyle}>{_('ID')}</th>
<th style={theme.textStyle}>{_('Date')}</th>
<th style={theme.textStyle}>{_('Password')}</th>
<th style={theme.textStyle}>{_('Valid')}</th>
<th style={theme.textStyle}>{_('Actions')}</th>
</tr>
{mkComps}
</tbody>
</table>
);
if (mkComps.length) {
return (
<div>
{headerComp}
{tableComp}
{infoComp}
</div>
);
}
return null;
};
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
const renderMasterPassword = () => {
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
const theme = themeStyle(props.themeId);
if (passwordChecks['master']) {
return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span style={theme.textStyle}>{_('Master password:')}</span>&nbsp;&nbsp;
<span style={{ ...theme.textStyle, fontWeight: 'bold' }}> {_('Loaded')}</span>
</div>
);
} else {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span style={theme.textStyle}> {'The master password is not set or is invalid. Please type it below:'}</span>
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<MasterPasswordInput placeholder={_('Enter your master password')} type="password" value={inputMasterPassword} onChange={(event: any) => onMasterPasswordChange(event.target.value)} />{' '}
<Button ml="10px" level={ButtonLevel.Secondary} onClick={onMasterPasswordSave} title={_('Save')} />
</div>
</div>
);
}
};
const containerStyle = Object.assign({}, theme.containerStyle, {
padding: theme.configScreenPadding,
overflow: 'auto',
backgroundColor: theme.backgroundColor3,
});
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
for (let i = 0; i < props.masterKeys.length; i++) {
const mk = props.masterKeys[i];
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
const isEnabled = getEncryptionEnabled();
const masterKey = getDefaultMasterKey();
let answer = null;
if (isEnabled) {
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
} else {
const msg = enableEncryptionConfirmationMessages(masterKey);
answer = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
}
if (!answer) return;
try {
await toggleAndSetupEncryption(EncryptionService.instance(), !isEnabled, masterKey, answer);
} catch (error) {
await dialogs.alert(error.message);
}
};
const decryptedItemsInfo = <p style={theme.textStyle}>{decryptedStatText(stats)}</p>;
const toggleButton = (
<button
style={theme.buttonStyle}
onClick={() => {
void onToggleButtonClick();
}}
>
{props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}
</button>
);
const needUpgradeSection = renderNeedUpgradeSection();
const reencryptDataSection = renderReencryptData();
const enabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true);
const disabledMasterKeySection = renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false);
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<tr key={id}>
<td style={theme.textStyle}>{id}</td>
</tr>
);
}
nonExistingMasterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
</tr>
{rows}
</tbody>
</table>
</div>
);
}
return (
<div>
<div style={containerStyle}>
{
<div className="alert alert-warning" style={{ backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
<p style={theme.textStyle}>
<span>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</span>{' '}
<a
onClick={() => {
bridge().openExternal('https://joplinapp.org/e2ee/');
}}
href="#"
style={theme.urlStyle}
>
https://joplinapp.org/e2ee/
</a>
</p>
</div>
}
<h1 style={theme.h1Style}>{_('Status')}</h1>
<p style={theme.textStyle}>
{_('Encryption is:')} <strong>{props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong>
</p>
{renderMasterPassword()}
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
{props.shouldReencrypt ? reencryptDataSection : null}
{enabledMasterKeySection}
{disabledMasterKeySection}
{nonExistingMasterKeySection}
{!props.shouldReencrypt ? reencryptDataSection : null}
</div>
</div>
);
};
const mapStateToProps = (state: AppState) => {
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
return {
themeId: state.settings.theme,
masterKeys: syncInfo.masterKeys,
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: syncInfo.e2ee,
activeMasterKeyId: syncInfo.activeMasterKeyId,
shouldReencrypt: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
};
export default connect(mapStateToProps)(EncryptionConfigScreen);

View File

@@ -0,0 +1,5 @@
.encryption-config-test {
& > .item {
font-weight: bold;
}
}

View File

@@ -19,6 +19,11 @@ export const runtime = (): CommandRuntime => {
visible: !layoutItemProp(layout, 'noteList', 'visible'),
});
// Toggling the sidebar will affect the size of most other on-screen components.
// Dispatching a window resize event is a bit of a hack, but it ensures that any
// component that watches for resizes will be accurately notified
window.dispatchEvent(new Event('resize'));
context.dispatch({
type: 'MAIN_LAYOUT_SET',
value: newLayout,

View File

@@ -19,6 +19,11 @@ export const runtime = (): CommandRuntime => {
visible: !layoutItemProp(layout, 'sideBar', 'visible'),
});
// Toggling the sidebar will affect the size of most other on-screen components.
// Dispatching a window resize event is a bit of a hack, but it ensures that any
// component that watches for resizes will be accurately notified
window.dispatchEvent(new Event('resize'));
context.dispatch({
type: 'MAIN_LAYOUT_SET',
value: newLayout,

View File

@@ -6,7 +6,8 @@ import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
import { CommandValue } from '../../utils/types';
import { useScrollHandler, usePrevious, cursorPositionToTextOffset, useRootSize } from './utils';
import { useScrollHandler, usePrevious, cursorPositionToTextOffset } from './utils';
import useElementSize from '@joplin/lib/hooks/useElementSize';
import Toolbar from './Toolbar';
import styles_ from './styles';
import { RenderedBody, defaultRenderedBody } from './utils/types';
@@ -59,7 +60,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const props_onChangeRef = useRef<Function>(null);
props_onChangeRef.current = props.onChange;
const rootSize = useRootSize({ rootRef });
const rootSize = useElementSize(rootRef);
usePluginServiceRegistration(ref);

View File

@@ -1,4 +1,4 @@
import { useEffect, useCallback, useRef, useState } from 'react';
import { useEffect, useCallback, useRef } from 'react';
import shim from '@joplin/lib/shim';
export function cursorPositionToTextOffset(cursorPos: any, body: string) {
@@ -89,21 +89,3 @@ export function useScrollHandler(editorRef: any, webviewRef: any, onScroll: Func
return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll };
}
export function useRootSize(dependencies: any) {
const { rootRef } = dependencies;
const [rootSize, setRootSize] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!rootRef.current) return;
const { width, height } = rootRef.current.getBoundingClientRect();
if (rootSize.width !== width || rootSize.height !== height) {
setRootSize({ width: width, height: height });
}
});
return rootSize;
}

View File

@@ -171,6 +171,7 @@ export default function useKeymap(CodeMirror: any) {
'Cmd-Right': 'goLineRightSmart',
'Alt-Backspace': 'delGroupBefore',
'Alt-Delete': 'delGroupAfter',
'Cmd-Backspace': 'delWrappedLineLeft',
'fallthrough': 'basic',
};

View File

@@ -793,7 +793,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
};
}
await loadDocumentAssets(editor, await props.allAssets(props.contentMarkupLanguage));
await loadDocumentAssets(editor, await props.allAssets(props.contentMarkupLanguage, { contentMaxWidthTarget: '.mce-content-body' }));
dispatchDidUpdate(editor);
};

View File

@@ -14,7 +14,7 @@ import useMarkupToHtml from './utils/useMarkupToHtml';
import useFormNote, { OnLoadEvent } from './utils/useFormNote';
import useFolder from './utils/useFolder';
import styles_ from './styles';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions } from './utils/types';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
import CommandService from '@joplin/lib/services/CommandService';
import ToolbarButton from '../ToolbarButton/ToolbarButton';
@@ -151,7 +151,12 @@ function NoteEditor(props: NoteEditorProps) {
plugins: props.plugins,
});
const allAssets = useCallback(async (markupLanguage: number): Promise<any[]> => {
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {
options = {
contentMaxWidthTarget: '',
...options,
};
const theme = themeStyle(props.themeId);
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
@@ -159,7 +164,10 @@ function NoteEditor(props: NoteEditorProps) {
customCss: props.customCss,
});
return markupToHtml.allAssets(markupLanguage, theme, { contentMaxWidth: props.contentMaxWidth });
return markupToHtml.allAssets(markupLanguage, theme, {
contentMaxWidth: props.contentMaxWidth,
contentMaxWidthTarget: options.contentMaxWidthTarget,
});
}, [props.themeId, props.customCss, props.contentMaxWidth]);
const handleProvisionalFlag = useCallback(() => {

View File

@@ -69,8 +69,17 @@ export function htmlToClipboardData(html: string): ClipboardData {
// In that case we need to set both HTML and Text context, otherwise it
// won't be possible to paste the text in, for example, a text editor.
// https://github.com/laurent22/joplin/issues/4788
//
// Also we don't escape the content produced in HTML to MD conversion
// because it's not what would be expected. For example, if the content is
// `* something`, strictly speaking it would be correct to escape to `\*
// something`, however this is not what the user would expect when copying
// text. Likewise for URLs that contain "_". So the resulting Markdown might
// not be perfectly valid but would be closer to what a user would expect.
// If they want accurate MArkdown they can switch to the MD editor.
// https://github.com/laurent22/joplin/issues/5440
return {
text: htmlToMd().parse(copyableContent),
text: htmlToMd().parse(copyableContent, { disableEscapeContent: true }),
html: cleanUpCodeBlocks(copyableContent),
};
}

View File

@@ -6,6 +6,10 @@ import { MarkupLanguage } from '@joplin/renderer';
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
import { MarkupToHtmlOptions } from './useMarkupToHtml';
export interface AllAssetsOptions {
contentMaxWidthTarget?: string;
}
export interface ToolbarButtonInfos {
[key: string]: ToolbarButtonInfo;
}
@@ -55,7 +59,7 @@ export interface NoteBodyEditorProps {
onScroll(event: any): void;
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
htmlToMarkdown: Function;
allAssets: (markupLanguage: MarkupLanguage)=> Promise<RenderResultPluginAsset[]>;
allAssets: (markupLanguage: MarkupLanguage, options: AllAssetsOptions)=> Promise<RenderResultPluginAsset[]>;
disabled: boolean;
dispatch: Function;
noteToolbar: any;

View File

@@ -139,9 +139,10 @@ class NoteSearchBarComponent extends React.Component {
color: theme.colorFaded,
backgroundColor: theme.backgroundColor,
});
const matchesFoundString = (query.length > 0 && this.props.resultCount > 0) ? (
const matchesFoundString = (query.length > 0) ? (
<div style={textStyle}>
{`${this.props.selectedIndex + 1} / ${this.props.resultCount}`}
{`${this.props.resultCount === 0 ? 0 : this.props.selectedIndex + 1} / ${this.props.resultCount}`}
</div>
) : null;

View File

@@ -138,9 +138,9 @@ const syncTargetNames: string[] = [
const logosImageNames: Record<string, string> = {
'dropbox': 'Dropbox.svg',
'joplinCloud': 'JoplinCloud.svg',
'onedrive': 'OneDrive.svg',
'dropbox': 'SyncTarget_Dropbox.svg',
'joplinCloud': 'SyncTarget_JoplinCloud.svg',
'onedrive': 'SyncTarget_OneDrive.svg',
};
export default function(props: Props) {
@@ -274,7 +274,7 @@ export default function(props: Props) {
const height = info.name !== 'joplinCloud' ? descriptionHeight : null;
const logoImageName = logosImageNames[info.name];
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/syncTargetLogos/${logoImageName}` : '';
const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/${logoImageName}` : '';
const logo = logoImageSrc ? <SyncTargetLogo src={logoImageSrc}/> : null;
const descriptionComp = <SyncTargetDescription height={height} ref={info.name === 'joplinCloud' ? joplinCloudDescriptionRef : null}>{info.description}</SyncTargetDescription>;
const featuresComp = showJoplinCloudForm && info.name === 'joplinCloud' ? null : renderFeatures(info.name);

View File

@@ -1,5 +1,6 @@
const gulp = require('gulp');
const utils = require('@joplin/tools/gulp/utils');
const compileSass = require('@joplin/tools/compileSass');
const tasks = {
compileScripts: {
@@ -20,6 +21,14 @@ const tasks = {
tsc: require('@joplin/tools/gulp/tasks/tsc'),
updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
buildCommandIndex: require('@joplin/tools/gulp/tasks/buildCommandIndex'),
compileSass: {
fn: async () => {
const guiDir = `${__dirname}/gui`;
await compileSass([
`${guiDir}/EncryptionConfigScreen/style.scss`,
], `${__dirname}/style.min.css`);
},
},
};
utils.registerGulpTasks(gulp, tasks);
@@ -31,6 +40,7 @@ const buildParallel = [
'copyTinyMceLangs',
'updateIgnoredTypeScriptBuild',
'buildCommandIndex',
'compileSass',
];
gulp.task('build', gulp.parallel(...buildParallel));

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.5",
"version": "2.4.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/app-desktop",
"version": "2.4.5",
"version": "2.4.8",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.5",
"version": "2.4.8",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

5
packages/app-desktop/style.min.css vendored Normal file
View File

@@ -0,0 +1,5 @@
.encryption-config-test > .item {
font-weight: bold;
}
/*# sourceMappingURL=style.min.css.map */

View File

@@ -141,8 +141,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097649
versionName "2.4.1"
versionCode 2097650
versionName "2.4.2"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -2,68 +2,56 @@ const React = require('react');
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
const { BaseScreenComponent } = require('../base-screen.js');
const { themeStyle } = require('../global-style.js');
const DialogBox = require('react-native-dialogbox').default;
const { dialogs } = require('../../utils/dialogs.js');
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shared from '@joplin/lib/components/shared/encryption-config-shared';
import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { State } from '@joplin/lib/reducer';
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import { useMemo, useRef, useState } from 'react';
interface Props {}
interface Props {
themeId: any;
masterKeys: MasterKeyEntity[];
passwords: Record<string, string>;
notLoadedMasterKeys: string[];
encryptionEnabled: boolean;
shouldReencrypt: boolean;
activeMasterKeyId: string;
masterPassword: string;
}
class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
static navigationOptions(): any {
return { header: null };
}
const EncryptionConfigScreen = (props: Props) => {
const [passwordPromptShow, setPasswordPromptShow] = useState(false);
const [passwordPromptAnswer, setPasswordPromptAnswer] = useState('');
const [passwordPromptConfirmAnswer, setPasswordPromptConfirmAnswer] = useState('');
const stats = useStats();
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
const dialogBoxRef = useRef(null);
constructor(props: Props) {
super(props);
const mkComps = [];
this.state = {
passwordPromptShow: false,
passwordPromptAnswer: '',
passwordPromptConfirmAnswer: '',
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
const theme: any = useMemo(() => {
return themeStyle(props.themeId);
}, [props.themeId]);
const rootStyle = useMemo(() => {
return {
flex: 1,
backgroundColor: theme.backgroundColor,
};
}, [theme]);
shared.initialize(this, props);
this.styles_ = {};
}
componentWillUnmount() {
this.isMounted_ = false;
}
async refreshStats() {
return shared.refreshStats(this);
}
componentDidMount() {
this.isMounted_ = true;
shared.componentDidMount(this);
}
componentDidUpdate(prevProps: Props) {
shared.componentDidUpdate(this, prevProps);
}
async checkPasswords() {
return shared.checkPasswords(this);
}
styles() {
const themeId = this.props.themeId;
const theme = themeStyle(themeId);
if (this.styles_[themeId]) return this.styles_[themeId];
this.styles_ = {};
const styles = useMemo(() => {
const styles = {
titleText: {
flex: 1,
@@ -93,39 +81,32 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
},
};
this.styles_[themeId] = StyleSheet.create(styles);
return this.styles_[themeId];
}
return StyleSheet.create(styles);
}, [theme]);
renderMasterKey(_num: number, mk: MasterKeyEntity) {
const theme = themeStyle(this.props.themeId);
const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null;
const onSaveClick = () => {
return shared.onSavePasswordClick(this, mk);
};
const renderMasterKey = (_num: number, mk: MasterKeyEntity) => {
const theme = themeStyle(props.themeId);
const onPasswordChange = (text: string) => {
return shared.onPasswordChange(this, mk, text);
};
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌';
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;
const renderPasswordInput = (masterKeyId: string) => {
if (this.state.masterPasswordKeys[masterKeyId] || !this.state.passwordChecks['master']) {
if (masterPasswordKeys[masterKeyId] || !passwordChecks['master']) {
return (
<Text style={{ ...this.styles().normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
<Text style={{ ...styles.normalText, color: theme.colorFaded, fontStyle: 'italic' }}>({_('Master password')})</Text>
);
} else {
return (
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onPasswordChange(text)} style={inputStyle}></TextInput>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onInputPasswordChange(mk, text)} style={inputStyle}></TextInput>
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
<Button title={_('Save')} onPress={() => onSavePasswordClick(mk, props.passwords)}></Button>
</View>
);
}
@@ -133,69 +114,65 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
return (
<View key={mk.id}>
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<Text style={styles.titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={styles.normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
{renderPasswordInput(mk.id)}
</View>
</View>
);
}
};
passwordPromptComponent() {
const theme = themeStyle(this.props.themeId);
let masterKey = getDefaultMasterKey();
// If the user has explicitly disabled the master key, we generate a
// new one. Needed for one the password has been forgotten.
if (!masterKey.enabled) masterKey = null;
const renderPasswordPrompt = () => {
const theme = themeStyle(props.themeId);
const masterKey = getDefaultMasterKey();
const onEnableClick = async () => {
try {
const password = this.state.passwordPromptAnswer;
const password = passwordPromptAnswer;
if (!password) throw new Error(_('Password cannot be empty'));
const password2 = this.state.passwordPromptConfirmAnswer;
const password2 = passwordPromptConfirmAnswer;
if (!password2) throw new Error(_('Confirm password cannot be empty'));
if (password !== password2) throw new Error(_('Passwords do not match!'));
await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, password);
// await generateMasterKeyAndEnableEncryption(EncryptionService.instance(), password);
this.setState({ passwordPromptShow: false });
setPasswordPromptShow(false);
} catch (error) {
await dialogs.error(this, error.message);
alert(error.message);
}
};
const messages = shared.enableEncryptionConfirmationMessages(masterKey);
const messages = enableEncryptionConfirmationMessages(masterKey);
const messageComps = messages.map(msg => {
const messageComps = messages.map((msg: string) => {
return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>;
});
return (
<View style={{ flex: 1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
<View>{messageComps}</View>
<Text style={this.styles().normalText}>{_('Password:')}</Text>
<Text style={styles.normalText}>{_('Password:')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
style={this.styles().normalTextInput}
style={styles.normalTextInput}
secureTextEntry={true}
value={this.state.passwordPromptAnswer}
value={passwordPromptAnswer}
onChangeText={(text: string) => {
this.setState({ passwordPromptAnswer: text });
setPasswordPromptAnswer(text);
}}
></TextInput>
<Text style={this.styles().normalText}>{_('Confirm password:')}</Text>
<Text style={styles.normalText}>{_('Confirm password:')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
style={this.styles().normalTextInput}
style={styles.normalTextInput}
secureTextEntry={true}
value={this.state.passwordPromptConfirmAnswer}
value={passwordPromptConfirmAnswer}
onChangeText={(text: string) => {
this.setState({ passwordPromptConfirmAnswer: text });
setPasswordPromptConfirmAnswer(text);
}}
></TextInput>
<View style={{ flexDirection: 'row' }}>
@@ -211,156 +188,132 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent<Props> {
<Button
title={_('Cancel')}
onPress={() => {
this.setState({ passwordPromptShow: false });
setPasswordPromptShow(false);
}}
></Button>
</View>
</View>
</View>
);
}
};
private renderMasterPassword() {
if (!this.props.encryptionEnabled && !this.props.masterKeys.length) return null;
const theme = themeStyle(this.props.themeId);
const onMasterPasswordSave = async () => {
shared.onMasterPasswordSave(this);
if (!(await shared.masterPasswordIsValid(this, this.state.masterPasswordInput))) {
alert('Password is invalid. Please try again.');
}
};
const renderMasterPassword = () => {
if (!props.encryptionEnabled && !props.masterKeys.length) return null;
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
inputStyle.borderBottomWidth = 1;
inputStyle.borderBottomColor = theme.dividerColor;
if (this.state.passwordChecks['master']) {
if (passwordChecks['master']) {
return (
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ ...this.styles().normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
<Text style={{ ...this.styles().normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
<Text style={{ ...styles.normalText, flex: 0, marginRight: 5 }}>{_('Master password:')}</Text>
<Text style={{ ...styles.normalText, fontWeight: 'bold' }}>{_('Loaded')}</Text>
</View>
);
} else {
return (
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
<Text style={this.styles().normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
<Text style={styles.normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text>
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={this.state.masterPasswordInput} onChangeText={(text: string) => shared.onMasterPasswordChange(this, text)} style={inputStyle}></TextInput>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={inputMasterPassword} onChangeText={(text: string) => onMasterPasswordChange(text)} style={inputStyle}></TextInput>
<Button onPress={onMasterPasswordSave} title={_('Save')} />
</View>
</View>
);
}
};
for (let i = 0; i < props.masterKeys.length; i++) {
const mk = props.masterKeys[i];
mkComps.push(renderMasterKey(i + 1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
render() {
const theme = themeStyle(this.props.themeId);
const masterKeys = this.props.masterKeys;
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
const onToggleButtonClick = async () => {
if (props.encryptionEnabled) {
const ok = await dialogs.confirmRef(dialogBoxRef.current, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
if (!ok) return;
const mkComps = [];
const nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(i + 1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
try {
await setupAndDisableEncryption(EncryptionService.instance());
} catch (error) {
alert(error.message);
}
} else {
setPasswordPromptShow(true);
setPasswordPromptAnswer('');
setPasswordPromptConfirmAnswer('');
return;
}
};
const onToggleButtonClick = async () => {
if (this.props.encryptionEnabled) {
const ok = await dialogs.confirm(this, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
if (!ok) return;
let nonExistingMasterKeySection = null;
try {
await setupAndDisableEncryption(EncryptionService.instance());
} catch (error) {
await dialogs.error(this, error.message);
}
} else {
this.setState({
passwordPromptShow: true,
passwordPromptAnswer: '',
passwordPromptConfirmAnswer: '',
});
return;
}
};
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<Text style={this.styles().normalText} key={id}>
{id}
</Text>
);
}
nonExistingMasterKeySection = (
<View>
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
<View style={{ marginTop: 10 }}>{rows}</View>
</View>
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(
<Text style={styles.normalText} key={id}>
{id}
</Text>
);
}
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
const toggleButton = !this.state.passwordPromptShow ? (
<View style={{ marginTop: 10 }}>
<Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
</View>
) : null;
return (
<View style={this.rootStyle(this.props.themeId).root}>
<ScreenHeader title={_('Encryption Config')} />
<ScrollView style={this.styles().container}>
{
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
<TouchableOpacity
onPress={() => {
Linking.openURL('https://joplinapp.org/e2ee/');
}}
>
<Text>https://joplinapp.org/e2ee/</Text>
</TouchableOpacity>
</View>
}
<Text style={this.styles().titleText}>{_('Status')}</Text>
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo}
{this.renderMasterPassword()}
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{ flex: 1, height: 20 }}></View>
</ScrollView>
<DialogBox
ref={(dialogbox: any) => {
this.dialogbox = dialogbox;
}}
/>
nonExistingMasterKeySection = (
<View>
<Text style={styles.titleText}>{_('Missing Master Keys')}</Text>
<Text style={styles.normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
<View style={{ marginTop: 10 }}>{rows}</View>
</View>
);
}
}
const EncryptionConfigScreen = connect((state: State) => {
const passwordPromptComp = passwordPromptShow ? renderPasswordPrompt() : null;
const toggleButton = !passwordPromptShow ? (
<View style={{ marginTop: 10 }}>
<Button title={props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
</View>
) : null;
return (
<View style={rootStyle}>
<ScreenHeader title={_('Encryption Config')} />
<ScrollView style={styles.container}>
{
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
<TouchableOpacity
onPress={() => {
Linking.openURL('https://joplinapp.org/e2ee/');
}}
>
<Text>https://joplinapp.org/e2ee/</Text>
</TouchableOpacity>
</View>
}
<Text style={styles.titleText}>{_('Status')}</Text>
<Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo}
{renderMasterPassword()}
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{ flex: 1, height: 20 }}></View>
</ScrollView>
<DialogBox ref={dialogBoxRef}/>
</View>
);
};
export default connect((state: State) => {
const syncInfo = new SyncInfo(state.settings['syncInfoCache']);
return {
@@ -372,6 +325,4 @@ const EncryptionConfigScreen = connect((state: State) => {
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
};
})(EncryptionConfigScreenComponent);
export default EncryptionConfigScreen;
})(EncryptionConfigScreen);

View File

@@ -488,7 +488,7 @@ SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: d2f54de51f69366bd1f5c1fb9270698dce678f8d
FBReactNativeSpec: 6da2c8ff1ebe6b6cf4510fcca58c24c4d02b16fc
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
JoplinCommonShareExtension: 270b4f8eb4e22828eeda433a04ed689fc1fd09b5
JoplinRNShareExtension: 7137e9787374e1b0797ecbef9103d1588d90e403

View File

@@ -15,8 +15,8 @@
"postinstall": "jetify && npm run build"
},
"dependencies": {
"@joplin/lib": "^2.2.0",
"@joplin/renderer": "^2.2.0",
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@react-native-community/clipboard": "^1.5.0",
"@react-native-community/datetimepicker": "^3.0.3",
"@react-native-community/geolocation": "^2.0.2",
@@ -69,7 +69,7 @@
"@codemirror/lang-markdown": "^0.18.4",
"@codemirror/state": "^0.18.7",
"@codemirror/view": "^0.18.19",
"@joplin/tools": "^1.0.9",
"@joplin/tools": "~2.4",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/node": "^14.14.6",

View File

@@ -1 +1 @@
module.exports = `LyoKCkF0b20gT25lIERhcmsgV2l0aCBzdXBwb3J0IGZvciBSZWFzb25NTCBieSBHaWRpIE1vcnJpcywgYmFzZWQgb2ZmIHdvcmsgYnkgRGFuaWVsIEdhbWFnZQoKT3JpZ2luYWwgT25lIERhcmsgU3ludGF4IHRoZW1lIGZyb20gaHR0cHM6Ly9naXRodWIuY29tL2F0b20vb25lLWRhcmstc3ludGF4CgoqLwouaGxqcyB7CiAgZGlzcGxheTogYmxvY2s7CiAgb3ZlcmZsb3cteDogYXV0bzsKICBwYWRkaW5nOiAwLjVlbTsKICBjb2xvcjogI2FiYjJiZjsKICBiYWNrZ3JvdW5kOiAjMjgyYzM0Owp9Ci5obGpzLWtleXdvcmQsIC5obGpzLW9wZXJhdG9yIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIHsKICBjb2xvcjogI0Y5MjY3MjsKfQouaGxqcy1wYXR0ZXJuLW1hdGNoIC5obGpzLWNvbnN0cnVjdG9yIHsKICBjb2xvcjogIzYxYWVlZTsKfQouaGxqcy1mdW5jdGlvbiB7CiAgY29sb3I6ICM2MWFlZWU7Cn0KLmhsanMtZnVuY3Rpb24gLmhsanMtcGFyYW1zIHsKICBjb2xvcjogI0E2RTIyRTsKfQouaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5nIHsKICBjb2xvcjogI0ZEOTcxRjsKfQouaGxqcy1tb2R1bGUtYWNjZXNzIC5obGpzLW1vZHVsZSB7CiAgY29sb3I6ICM3ZTU3YzI7Cn0KLmhsanMtY29uc3RydWN0b3IgewogIGNvbG9yOiAjZTJiOTNkOwp9Ci5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZyB7CiAgY29sb3I6ICM5Q0NDNjU7Cn0KLmhsanMtY29tbWVudCwgLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYjE4ZWIxOwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQouaGxqcy1kb2N0YWcsIC5obGpzLWZvcm11bGEgewogIGNvbG9yOiAjYzY3OGRkOwp9Ci5obGpzLXNlY3Rpb24sIC5obGpzLW5hbWUsIC5obGpzLXNlbGVjdG9yLXRhZywgLmhsanMtZGVsZXRpb24sIC5obGpzLXN1YnN0IHsKICBjb2xvcjogI2UwNmM3NTsKfQouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzU2YjZjMjsKfQouaGxqcy1zdHJpbmcsIC5obGpzLXJlZ2V4cCwgLmhsanMtYWRkaXRpb24sIC5obGpzLWF0dHJpYnV0ZSwgLmhsanMtbWV0YS1zdHJpbmcgewogIGNvbG9yOiAjOThjMzc5Owp9Ci5obGpzLWJ1aWx0X2luLCAuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSB7CiAgY29sb3I6ICNlNmMwN2I7Cn0KLmhsanMtYXR0ciwgLmhsanMtdmFyaWFibGUsIC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLCAuaGxqcy10eXBlLCAuaGxqcy1zZWxlY3Rvci1jbGFzcywgLmhsanMtc2VsZWN0b3ItYXR0ciwgLmhsanMtc2VsZWN0b3ItcHNldWRvLCAuaGxqcy1udW1iZXIgewogIGNvbG9yOiAjZDE5YTY2Owp9Ci5obGpzLXN5bWJvbCwgLmhsanMtYnVsbGV0LCAuaGxqcy1saW5rLCAuaGxqcy1tZXRhLCAuaGxqcy1zZWxlY3Rvci1pZCwgLmhsanMtdGl0bGUgewogIGNvbG9yOiAjNjFhZWVlOwp9Ci5obGpzLWVtcGhhc2lzIHsKICBmb250LXN0eWxlOiBpdGFsaWM7Cn0KLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQouaGxqcy1saW5rIHsKICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsKfQo=`;
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiNhYmIyYmY7YmFja2dyb3VuZDojMjgyYzM0fS5obGpzLWtleXdvcmQsLmhsanMtb3BlcmF0b3IsLmhsanMtcGF0dGVybi1tYXRjaHtjb2xvcjojZjkyNjcyfS5obGpzLWZ1bmN0aW9uLC5obGpzLXBhdHRlcm4tbWF0Y2ggLmhsanMtY29uc3RydWN0b3J7Y29sb3I6IzYxYWVlZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXN7Y29sb3I6I2E2ZTIyZX0uaGxqcy1mdW5jdGlvbiAuaGxqcy1wYXJhbXMgLmhsanMtdHlwaW5ne2NvbG9yOiNmZDk3MWZ9LmhsanMtbW9kdWxlLWFjY2VzcyAuaGxqcy1tb2R1bGV7Y29sb3I6IzdlNTdjMn0uaGxqcy1jb25zdHJ1Y3Rvcntjb2xvcjojZTJiOTNkfS5obGpzLWNvbnN0cnVjdG9yIC5obGpzLXN0cmluZ3tjb2xvcjojOWNjYzY1fS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2IxOGViMTtmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYXtjb2xvcjojYzY3OGRkfS5obGpzLWRlbGV0aW9uLC5obGpzLW5hbWUsLmhsanMtc2VjdGlvbiwuaGxqcy1zZWxlY3Rvci10YWcsLmhsanMtc3Vic3R7Y29sb3I6I2UwNmM3NX0uaGxqcy1saXRlcmFse2NvbG9yOiM1NmI2YzJ9LmhsanMtYWRkaXRpb24sLmhsanMtYXR0cmlidXRlLC5obGpzLW1ldGEgLmhsanMtc3RyaW5nLC5obGpzLXJlZ2V4cCwuaGxqcy1zdHJpbmd7Y29sb3I6Izk4YzM3OX0uaGxqcy1idWlsdF9pbiwuaGxqcy1jbGFzcyAuaGxqcy10aXRsZSwuaGxqcy10aXRsZS5jbGFzc197Y29sb3I6I2U2YzA3Yn0uaGxqcy1hdHRyLC5obGpzLW51bWJlciwuaGxqcy1zZWxlY3Rvci1hdHRyLC5obGpzLXNlbGVjdG9yLWNsYXNzLC5obGpzLXNlbGVjdG9yLXBzZXVkbywuaGxqcy10ZW1wbGF0ZS12YXJpYWJsZSwuaGxqcy10eXBlLC5obGpzLXZhcmlhYmxle2NvbG9yOiNkMTlhNjZ9LmhsanMtYnVsbGV0LC5obGpzLWxpbmssLmhsanMtbWV0YSwuaGxqcy1zZWxlY3Rvci1pZCwuaGxqcy1zeW1ib2wsLmhsanMtdGl0bGV7Y29sb3I6IzYxYWVlZX0uaGxqcy1lbXBoYXNpc3tmb250LXN0eWxlOml0YWxpY30uaGxqcy1zdHJvbmd7Zm9udC13ZWlnaHQ6NzAwfS5obGpzLWxpbmt7dGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZX0=`;

View File

@@ -1 +1 @@
module.exports = `LyoKCkF0b20gT25lIExpZ2h0IGJ5IERhbmllbCBHYW1hZ2UKT3JpZ2luYWwgT25lIExpZ2h0IFN5bnRheCB0aGVtZSBmcm9tIGh0dHBzOi8vZ2l0aHViLmNvbS9hdG9tL29uZS1saWdodC1zeW50YXgKCmJhc2U6ICAgICNmYWZhZmEKbW9uby0xOiAgIzM4M2E0Mgptb25vLTI6ICAjNjg2Yjc3Cm1vbm8tMzogICNhMGExYTcKaHVlLTE6ICAgIzAxODRiYgpodWUtMjogICAjNDA3OGYyCmh1ZS0zOiAgICNhNjI2YTQKaHVlLTQ6ICAgIzUwYTE0ZgpodWUtNTogICAjZTQ1NjQ5Cmh1ZS01LTI6ICNjOTEyNDMKaHVlLTY6ICAgIzk4NjgwMQpodWUtNi0yOiAjYzE4NDAxCgoqLwoKLmhsanMgewogIGRpc3BsYXk6IGJsb2NrOwogIG92ZXJmbG93LXg6IGF1dG87CiAgcGFkZGluZzogMC41ZW07CiAgY29sb3I6ICMzODNhNDI7CiAgYmFja2dyb3VuZDogI2ZhZmFmYTsKfQoKLmhsanMtY29tbWVudCwKLmhsanMtcXVvdGUgewogIGNvbG9yOiAjYTBhMWE3OwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtZG9jdGFnLAouaGxqcy1rZXl3b3JkLAouaGxqcy1mb3JtdWxhIHsKICBjb2xvcjogI2E2MjZhNDsKfQoKLmhsanMtc2VjdGlvbiwKLmhsanMtbmFtZSwKLmhsanMtc2VsZWN0b3ItdGFnLAouaGxqcy1kZWxldGlvbiwKLmhsanMtc3Vic3QgewogIGNvbG9yOiAjZTQ1NjQ5Owp9CgouaGxqcy1saXRlcmFsIHsKICBjb2xvcjogIzAxODRiYjsKfQoKLmhsanMtc3RyaW5nLAouaGxqcy1yZWdleHAsCi5obGpzLWFkZGl0aW9uLAouaGxqcy1hdHRyaWJ1dGUsCi5obGpzLW1ldGEtc3RyaW5nIHsKICBjb2xvcjogIzUwYTE0ZjsKfQoKLmhsanMtYnVpbHRfaW4sCi5obGpzLWNsYXNzIC5obGpzLXRpdGxlIHsKICBjb2xvcjogI2MxODQwMTsKfQoKLmhsanMtYXR0ciwKLmhsanMtdmFyaWFibGUsCi5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLAouaGxqcy10eXBlLAouaGxqcy1zZWxlY3Rvci1jbGFzcywKLmhsanMtc2VsZWN0b3ItYXR0ciwKLmhsanMtc2VsZWN0b3ItcHNldWRvLAouaGxqcy1udW1iZXIgewogIGNvbG9yOiAjOTg2ODAxOwp9CgouaGxqcy1zeW1ib2wsCi5obGpzLWJ1bGxldCwKLmhsanMtbGluaywKLmhsanMtbWV0YSwKLmhsanMtc2VsZWN0b3ItaWQsCi5obGpzLXRpdGxlIHsKICBjb2xvcjogIzQwNzhmMjsKfQoKLmhsanMtZW1waGFzaXMgewogIGZvbnQtc3R5bGU6IGl0YWxpYzsKfQoKLmhsanMtc3Ryb25nIHsKICBmb250LXdlaWdodDogYm9sZDsKfQoKLmhsanMtbGluayB7CiAgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmU7Cn0K`;
module.exports = `cHJlIGNvZGUuaGxqc3tkaXNwbGF5OmJsb2NrO292ZXJmbG93LXg6YXV0bztwYWRkaW5nOjFlbX1jb2RlLmhsanN7cGFkZGluZzozcHggNXB4fS5obGpze2NvbG9yOiMzODNhNDI7YmFja2dyb3VuZDojZmFmYWZhfS5obGpzLWNvbW1lbnQsLmhsanMtcXVvdGV7Y29sb3I6I2EwYTFhNztmb250LXN0eWxlOml0YWxpY30uaGxqcy1kb2N0YWcsLmhsanMtZm9ybXVsYSwuaGxqcy1rZXl3b3Jke2NvbG9yOiNhNjI2YTR9LmhsanMtZGVsZXRpb24sLmhsanMtbmFtZSwuaGxqcy1zZWN0aW9uLC5obGpzLXNlbGVjdG9yLXRhZywuaGxqcy1zdWJzdHtjb2xvcjojZTQ1NjQ5fS5obGpzLWxpdGVyYWx7Y29sb3I6IzAxODRiYn0uaGxqcy1hZGRpdGlvbiwuaGxqcy1hdHRyaWJ1dGUsLmhsanMtbWV0YSAuaGxqcy1zdHJpbmcsLmhsanMtcmVnZXhwLC5obGpzLXN0cmluZ3tjb2xvcjojNTBhMTRmfS5obGpzLWF0dHIsLmhsanMtbnVtYmVyLC5obGpzLXNlbGVjdG9yLWF0dHIsLmhsanMtc2VsZWN0b3ItY2xhc3MsLmhsanMtc2VsZWN0b3ItcHNldWRvLC5obGpzLXRlbXBsYXRlLXZhcmlhYmxlLC5obGpzLXR5cGUsLmhsanMtdmFyaWFibGV7Y29sb3I6Izk4NjgwMX0uaGxqcy1idWxsZXQsLmhsanMtbGluaywuaGxqcy1tZXRhLC5obGpzLXNlbGVjdG9yLWlkLC5obGpzLXN5bWJvbCwuaGxqcy10aXRsZXtjb2xvcjojNDA3OGYyfS5obGpzLWJ1aWx0X2luLC5obGpzLWNsYXNzIC5obGpzLXRpdGxlLC5obGpzLXRpdGxlLmNsYXNzX3tjb2xvcjojYzE4NDAxfS5obGpzLWVtcGhhc2lze2ZvbnQtc3R5bGU6aXRhbGljfS5obGpzLXN0cm9uZ3tmb250LXdlaWdodDo3MDB9LmhsanMtbGlua3t0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lfQ==`;

View File

@@ -1,5 +1,5 @@
module.exports = {
hash:"6608023b8053b48e0eec248644475e33", files: {
hash:"de3871f000c87478973d7cd0913bd3ff", files: {
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },

File diff suppressed because one or more lines are too long

View File

@@ -7,14 +7,13 @@ const { Keyboard } = require('react-native');
const dialogs = {};
dialogs.confirm = (parentComponent, message) => {
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
dialogs.confirmRef = (ref, message) => {
if (!ref) throw new Error('ref is required');
return new Promise((resolve) => {
Keyboard.dismiss();
parentComponent.dialogbox.confirm({
ref.confirm({
content: message,
ok: {
@@ -32,6 +31,13 @@ dialogs.confirm = (parentComponent, message) => {
});
};
dialogs.confirm = (parentComponent, message) => {
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
return dialogs.confirmRef(parentComponent.dialogbox, message);
};
dialogs.pop = (parentComponent, message, buttons, options = null) => {
if (!parentComponent) throw new Error('parentComponent is required');
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');

View File

@@ -251,6 +251,12 @@ export default class BaseApplication {
continue;
}
if (arg.indexOf('--user-data-dir=') === 0) {
// Electron-specific flag. Allows users to run the app with chromedriver.
argv.splice(0, 1);
continue;
}
if (arg.length && arg[0] == '-') {
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
} else {

View File

@@ -6,6 +6,7 @@ export interface ParseOptions {
anchorNames?: string[];
preserveImageTagsWithSize?: boolean;
baseUrl?: string;
disableEscapeContent?: boolean;
}
export default class HtmlToMd {
@@ -20,6 +21,7 @@ export default class HtmlToMd {
emDelimiter: '*',
strongDelimiter: '**',
br: '',
disableEscapeContent: 'disableEscapeContent' in options ? options.disableEscapeContent : false,
});
turndown.use(turndownPluginGfm);
turndown.remove('script');

View File

@@ -479,6 +479,31 @@ export default class Synchronizer {
void this.cancel();
});
// ========================================================================
// 2. DELETE_REMOTE
// ------------------------------------------------------------------------
// Delete the remote items that have been deleted locally.
// ========================================================================
if (syncSteps.indexOf('delete_remote') >= 0) {
const deletedItems = await BaseItem.deletedItems(syncTargetId);
for (let i = 0; i < deletedItems.length; i++) {
if (this.cancelling()) break;
const item = deletedItems[i];
const path = BaseItem.systemPath(item.item_id);
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
await this.apiCall('delete', path);
if (item.item_type === BaseModel.TYPE_RESOURCE) {
const remoteContentPath = resourceRemotePath(item.item_id);
await this.apiCall('delete', remoteContentPath);
}
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
}
} // DELETE_REMOTE STEP
// ========================================================================
// 1. UPLOAD
// ------------------------------------------------------------------------
@@ -763,31 +788,6 @@ export default class Synchronizer {
}
} // UPLOAD STEP
// ========================================================================
// 2. DELETE_REMOTE
// ------------------------------------------------------------------------
// Delete the remote items that have been deleted locally.
// ========================================================================
if (syncSteps.indexOf('delete_remote') >= 0) {
const deletedItems = await BaseItem.deletedItems(syncTargetId);
for (let i = 0; i < deletedItems.length; i++) {
if (this.cancelling()) break;
const item = deletedItems[i];
const path = BaseItem.systemPath(item.item_id);
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
await this.apiCall('delete', path);
if (item.item_type === BaseModel.TYPE_RESOURCE) {
const remoteContentPath = resourceRemotePath(item.item_id);
await this.apiCall('delete', remoteContentPath);
}
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
}
} // DELETE_REMOTE STEP
// ------------------------------------------------------------------------
// 3. DELTA
// ------------------------------------------------------------------------

View File

@@ -0,0 +1,183 @@
import shim from '../../shim';
import { _ } from '../../locale';
import BaseItem, { EncryptedItemsStats } from '../../models/BaseItem';
import useAsyncEffect, { AsyncEffectEvent } from '../../hooks/useAsyncEffect';
import { MasterKeyEntity } from '../../services/e2ee/types';
import time from '../../time';
import { findMasterKeyPassword } from '../../services/e2ee/utils';
import EncryptionService from '../../services/e2ee/EncryptionService';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
import MasterKey from '../../models/MasterKey';
import { reg } from '../../registry';
import Setting from '../../models/Setting';
const { useCallback, useEffect, useState } = shim.react();
type PasswordChecks = Record<string, boolean>;
export const useStats = () => {
const [stats, setStats] = useState<EncryptedItemsStats>({ encrypted: null, total: null });
const [statsUpdateTime, setStatsUpdateTime] = useState<number>(0);
useAsyncEffect(async (event: AsyncEffectEvent) => {
const r = await BaseItem.encryptedItemsStats();
if (event.cancelled) return;
setStats(r);
}, [statsUpdateTime]);
useEffect(() => {
const iid = shim.setInterval(() => {
setStatsUpdateTime(Date.now());
}, 3000);
return () => {
clearInterval(iid);
};
}, []);
return stats;
};
export const decryptedStatText = (stats: EncryptedItemsStats) => {
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
const totalCount = stats.total !== null ? stats.total : '-';
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
return result;
};
export const enableEncryptionConfirmationMessages = (masterKey: MasterKeyEntity) => {
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
return msg;
};
const masterPasswordIsValid = async (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string = null) => {
const activeMasterKey = masterKeys.find((mk: MasterKeyEntity) => mk.id === activeMasterKeyId);
masterPassword = masterPassword === null ? masterPassword : masterPassword;
if (activeMasterKey && masterPassword) {
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
}
return false;
};
export const reencryptData = async () => {
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
if (!ok) return;
await BaseItem.forceSyncAll();
void reg.waitForSyncFinishedThenSync();
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
alert(_('Your data is going to be re-encrypted and synced again.'));
};
export const dontReencryptData = () => {
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
};
export const useToggleShowDisabledMasterKeys = () => {
const [showDisabledMasterKeys, setShowDisabledMasterKeys] = useState<boolean>(false);
const toggleShowDisabledMasterKeys = () => {
setShowDisabledMasterKeys((current) => !current);
};
return { showDisabledMasterKeys, toggleShowDisabledMasterKeys };
};
export const onToggleEnabledClick = (mk: MasterKeyEntity) => {
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
};
export const onSavePasswordClick = (mk: MasterKeyEntity, passwords: Record<string, string>) => {
const password = passwords[mk.id];
if (!password) {
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
} else {
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
}
};
export const onMasterPasswordSave = (masterPasswordInput: string) => {
Setting.setValue('encryption.masterPassword', masterPasswordInput);
};
export const useInputMasterPassword = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string) => {
const [inputMasterPassword, setInputMasterPassword] = useState<string>('');
const onMasterPasswordSave = useCallback(async () => {
Setting.setValue('encryption.masterPassword', inputMasterPassword);
if (!(await masterPasswordIsValid(masterKeys, activeMasterKeyId, inputMasterPassword))) {
alert('Password is invalid. Please try again.');
}
}, [inputMasterPassword]);
const onMasterPasswordChange = useCallback((password: string) => {
setInputMasterPassword(password);
}, []);
return { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange };
};
export const useInputPasswords = (propsPasswords: Record<string, string>) => {
const [inputPasswords, setInputPasswords] = useState<Record<string, string>>(propsPasswords);
useEffect(() => {
setInputPasswords(propsPasswords);
}, [propsPasswords]);
const onInputPasswordChange = useCallback((mk: MasterKeyEntity, password: string) => {
setInputPasswords(current => {
return {
...current,
[mk.id]: password,
};
});
}, []);
return { inputPasswords, onInputPasswordChange };
};
export const usePasswordChecker = (masterKeys: MasterKeyEntity[], activeMasterKeyId: string, masterPassword: string, passwords: Record<string, string>) => {
const [passwordChecks, setPasswordChecks] = useState<PasswordChecks>({});
const [masterPasswordKeys, setMasterPasswordKeys] = useState<PasswordChecks>({});
useAsyncEffect(async (event: AsyncEffectEvent) => {
const newPasswordChecks: PasswordChecks = {};
const newMasterPasswordKeys: PasswordChecks = {};
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
const password = await findMasterKeyPassword(EncryptionService.instance(), mk, passwords);
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
newPasswordChecks[mk.id] = ok;
newMasterPasswordKeys[mk.id] = password === masterPassword;
}
newPasswordChecks['master'] = await masterPasswordIsValid(masterKeys, activeMasterKeyId, masterPassword);
if (event.cancelled) return;
setPasswordChecks(newPasswordChecks);
setMasterPasswordKeys(newMasterPasswordKeys);
}, [masterKeys, masterPassword]);
return { passwordChecks, masterPasswordKeys };
};
export const upgradeMasterKey = async (masterKey: MasterKeyEntity, passwordChecks: PasswordChecks, passwords: Record<string, string>): Promise<string> => {
const passwordCheck = passwordChecks[masterKey.id];
if (!passwordCheck) {
return _('Please enter your password in the master key list below before upgrading the key.');
}
try {
const password = passwords[masterKey.id];
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
await MasterKey.save(newMasterKey);
void reg.waitForSyncFinishedThenSync();
return _('The master key has been upgraded successfully!');
} catch (error) {
return _('Could not upgrade master key: %s', error.message);
}
};

View File

@@ -9,10 +9,8 @@ const shared = {};
shared.init = function(comp) {
if (!comp.state) comp.state = {};
comp.state.checkSyncConfigResult = null;
comp.state.checkNextcloudAppResult = null;
comp.state.settings = {};
comp.state.changedSettingKeys = [];
comp.state.showNextcloudAppLog = false;
comp.state.showAdvancedSettings = false;
};
@@ -35,7 +33,6 @@ shared.checkSyncConfig = async function(comp, settings) {
comp.setState({ checkSyncConfigResult: result });
if (result.ok) {
// await shared.checkNextcloudApp(comp, settings);
// Users often expect config to be auto-saved at this point, if the config check was successful
shared.saveSettings(comp);
}

View File

@@ -1,196 +0,0 @@
import EncryptionService from '../../services/e2ee/EncryptionService';
import { _ } from '../../locale';
import BaseItem from '../../models/BaseItem';
import Setting from '../../models/Setting';
import MasterKey from '../../models/MasterKey';
import { reg } from '../../registry.js';
import shim from '../../shim';
import { MasterKeyEntity } from '../../services/e2ee/types';
import time from '../../time';
import { masterKeyEnabled, setMasterKeyEnabled } from '../../services/synchronizer/syncInfoUtils';
import { findMasterKeyPassword } from '../../services/e2ee/utils';
class Shared {
private refreshStatsIID_: any;
public initialize(comp: any, props: any) {
comp.state = {
passwordChecks: {},
// Master keys that can be decrypted with the master password
// (normally all of them, but for legacy support we need this).
masterPasswordKeys: {},
stats: {
encrypted: null,
total: null,
},
passwords: Object.assign({}, props.passwords),
showDisabledMasterKeys: false,
masterPasswordInput: '',
};
comp.isMounted_ = false;
this.refreshStatsIID_ = null;
}
public async refreshStats(comp: any) {
const stats = await BaseItem.encryptedItemsStats();
comp.setState({
stats: stats,
});
}
public async toggleShowDisabledMasterKeys(comp: any) {
comp.setState({ showDisabledMasterKeys: !comp.state.showDisabledMasterKeys });
}
public async reencryptData() {
const ok = confirm(_('Please confirm that you would like to re-encrypt your complete database.'));
if (!ok) return;
await BaseItem.forceSyncAll();
void reg.waitForSyncFinishedThenSync();
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
alert(_('Your data is going to be re-encrypted and synced again.'));
}
public dontReencryptData() {
Setting.setValue('encryption.shouldReencrypt', Setting.SHOULD_REENCRYPT_NO);
}
public async upgradeMasterKey(comp: any, masterKey: MasterKeyEntity) {
const passwordCheck = comp.state.passwordChecks[masterKey.id];
if (!passwordCheck) {
alert(_('Please enter your password in the master key list below before upgrading the key.'));
return;
}
try {
const password = comp.state.passwords[masterKey.id];
const newMasterKey = await EncryptionService.instance().upgradeMasterKey(masterKey, password);
await MasterKey.save(newMasterKey);
void reg.waitForSyncFinishedThenSync();
alert(_('The master key has been upgraded successfully!'));
} catch (error) {
alert(_('Could not upgrade master key: %s', error.message));
}
}
public componentDidMount(comp: any) {
this.componentDidUpdate(comp);
void this.refreshStats(comp);
if (this.refreshStatsIID_) {
shim.clearInterval(this.refreshStatsIID_);
this.refreshStatsIID_ = null;
}
this.refreshStatsIID_ = shim.setInterval(() => {
if (!comp.isMounted_) {
shim.clearInterval(this.refreshStatsIID_);
this.refreshStatsIID_ = null;
return;
}
void this.refreshStats(comp);
}, 3000);
}
public componentDidUpdate(comp: any, prevProps: any = null) {
if (prevProps && comp.props.passwords !== prevProps.passwords) {
comp.setState({ passwords: Object.assign({}, comp.props.passwords) });
}
if (!prevProps || comp.props.masterKeys !== prevProps.masterKeys || comp.props.passwords !== prevProps.passwords) {
comp.checkPasswords();
}
}
public componentWillUnmount() {
if (this.refreshStatsIID_) {
shim.clearInterval(this.refreshStatsIID_);
this.refreshStatsIID_ = null;
}
}
public async masterPasswordIsValid(comp: any, masterPassword: string = null) {
const activeMasterKey = comp.props.masterKeys.find((mk: MasterKeyEntity) => mk.id === comp.props.activeMasterKeyId);
masterPassword = masterPassword === null ? comp.props.masterPassword : masterPassword;
if (activeMasterKey && masterPassword) {
return EncryptionService.instance().checkMasterKeyPassword(activeMasterKey, masterPassword);
}
return false;
}
public async checkPasswords(comp: any) {
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
const masterPasswordKeys = Object.assign({}, comp.state.masterPasswordKeys);
for (let i = 0; i < comp.props.masterKeys.length; i++) {
const mk = comp.props.masterKeys[i];
const password = await findMasterKeyPassword(EncryptionService.instance(), mk);
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
passwordChecks[mk.id] = ok;
masterPasswordKeys[mk.id] = password === comp.props.masterPassword;
}
passwordChecks['master'] = await this.masterPasswordIsValid(comp);
comp.setState({ passwordChecks, masterPasswordKeys });
}
public masterPasswordStatus(comp: any) {
// Don't translate for now because that's temporary - later it should
// always be set and the label should be replaced by a "Change master
// password" button.
return comp.props.masterPassword ? 'Master password is set' : 'Master password is not set';
}
public decryptedStatText(comp: any) {
const stats = comp.state.stats;
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
const totalCount = stats.total !== null ? stats.total : '-';
const result = _('Decrypted items: %s / %s', doneCount, totalCount);
return result;
}
public onSavePasswordClick(comp: any, mk: MasterKeyEntity) {
const password = comp.state.passwords[mk.id];
if (!password) {
Setting.deleteObjectValue('encryption.passwordCache', mk.id);
} else {
Setting.setObjectValue('encryption.passwordCache', mk.id, password);
}
comp.checkPasswords();
}
public onMasterPasswordChange(comp: any, value: string) {
comp.setState({ masterPasswordInput: value });
}
public onMasterPasswordSave(comp: any) {
Setting.setValue('encryption.masterPassword', comp.state.masterPasswordInput);
}
public onPasswordChange(comp: any, mk: MasterKeyEntity, password: string) {
const passwords = Object.assign({}, comp.state.passwords);
passwords[mk.id] = password;
comp.setState({ passwords: passwords });
}
public onToggleEnabledClick(_comp: any, mk: MasterKeyEntity) {
setMasterKeyEnabled(mk.id, !masterKeyEnabled(mk));
}
public enableEncryptionConfirmationMessages(masterKey: MasterKeyEntity) {
const msg = [_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')];
if (masterKey) msg.push(_('Encryption will be enabled using the master key created on %s', time.unixMsToLocalDateTime(masterKey.created_time)));
return msg;
}
}
const shared = new Shared();
export default shared;

View File

@@ -25,6 +25,7 @@ function useEventListener(
const eventListener = (event: Event) => {
// eslint-disable-next-line no-extra-boolean-cast
if (!!savedHandler?.current) {
// @ts-ignore
savedHandler.current(event);
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -41,45 +41,45 @@ locales['uk_UA'] = require('./uk_UA.json');
locales['vi'] = require('./vi.json');
locales['zh_CN'] = require('./zh_CN.json');
locales['zh_TW'] = require('./zh_TW.json');
stats['ar'] = {"percentDone":91};
stats['ar'] = {"percentDone":99};
stats['eu'] = {"percentDone":28};
stats['bs_BA'] = {"percentDone":71};
stats['bg_BG'] = {"percentDone":56};
stats['ca'] = {"percentDone":95};
stats['hr_HR'] = {"percentDone":96};
stats['cs_CZ'] = {"percentDone":95};
stats['bg_BG'] = {"percentDone":55};
stats['ca'] = {"percentDone":99};
stats['hr_HR'] = {"percentDone":95};
stats['cs_CZ'] = {"percentDone":94};
stats['da_DK'] = {"percentDone":99};
stats['de_DE'] = {"percentDone":95};
stats['de_DE'] = {"percentDone":99};
stats['et_EE'] = {"percentDone":54};
stats['en_GB'] = {"percentDone":100};
stats['en_US'] = {"percentDone":100};
stats['es_ES'] = {"percentDone":95};
stats['eo'] = {"percentDone":31};
stats['fi_FI'] = {"percentDone":90};
stats['fi_FI'] = {"percentDone":99};
stats['fr_FR'] = {"percentDone":96};
stats['gl_ES'] = {"percentDone":36};
stats['id_ID'] = {"percentDone":96};
stats['it_IT'] = {"percentDone":96};
stats['hu_HU'] = {"percentDone":84};
stats['nl_BE'] = {"percentDone":87};
stats['id_ID'] = {"percentDone":95};
stats['it_IT'] = {"percentDone":95};
stats['hu_HU'] = {"percentDone":83};
stats['nl_BE'] = {"percentDone":86};
stats['nl_NL'] = {"percentDone":90};
stats['nb_NO'] = {"percentDone":96};
stats['fa'] = {"percentDone":67};
stats['pl_PL'] = {"percentDone":90};
stats['pl_PL'] = {"percentDone":89};
stats['pt_BR'] = {"percentDone":96};
stats['pt_PT'] = {"percentDone":90};
stats['ro'] = {"percentDone":63};
stats['sl_SI'] = {"percentDone":91};
stats['sv'] = {"percentDone":96};
stats['pt_PT'] = {"percentDone":89};
stats['ro'] = {"percentDone":62};
stats['sl_SI'] = {"percentDone":90};
stats['sv'] = {"percentDone":99};
stats['th_TH'] = {"percentDone":42};
stats['vi'] = {"percentDone":96};
stats['tr_TR'] = {"percentDone":99};
stats['uk_UA'] = {"percentDone":89};
stats['el_GR'] = {"percentDone":92};
stats['ru_RU'] = {"percentDone":89};
stats['sr_RS'] = {"percentDone":81};
stats['sr_RS'] = {"percentDone":80};
stats['zh_CN'] = {"percentDone":99};
stats['zh_TW'] = {"percentDone":94};
stats['zh_TW'] = {"percentDone":95};
stats['ja_JP'] = {"percentDone":95};
stats['ko'] = {"percentDone":95};
stats['ko'] = {"percentDone":94};
module.exports = { locales: locales, stats: stats };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -33,6 +33,11 @@ export interface ItemsThatNeedSyncResult {
neverSyncedItemIds: string[];
}
export interface EncryptedItemsStats {
encrypted: number;
total: number;
}
export default class BaseItem extends BaseModel {
public static encryptionService_: any = null;
@@ -513,7 +518,7 @@ export default class BaseItem extends BaseModel {
return output;
}
static async encryptedItemsStats() {
public static async encryptedItemsStats(): Promise<EncryptedItemsStats> {
const classNames = this.encryptableItemClassNames();
let encryptedCount = 0;
let totalCount = 0;

View File

@@ -67,6 +67,7 @@
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@types/react": "^17.0.20",
"clean-html": "^1.5.0",
"jest": "^26.6.3",
"sharp": "^0.26.2",
@@ -1073,6 +1074,29 @@
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"node_modules/@types/react": {
"version": "17.0.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"node_modules/@types/stack-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@@ -2253,6 +2277,12 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true
},
"node_modules/csstype": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
"dev": true
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -5639,6 +5669,7 @@
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
"integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.",
"optional": true,
"dependencies": {
"block-stream": "*",
@@ -9758,6 +9789,29 @@
"integrity": "sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==",
"dev": true
},
"@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"@types/react": {
"version": "17.0.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz",
"integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"@types/stack-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@@ -10725,6 +10779,12 @@
}
}
},
"csstype": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
"dev": true
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",

View File

@@ -19,6 +19,7 @@
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@types/react": "^17.0.20",
"clean-html": "^1.5.0",
"jest": "^26.6.3",
"sharp": "^0.26.2",

View File

@@ -589,4 +589,25 @@ describe('reducer', function() {
expect(state.selectedFolderId).toEqual(null);
expect(state.selectedNoteIds[0]).toEqual(notes[1].id);
});
// tests for NOTE_UPDATE_ALL about issue #5447
it('should not change selectedNoteIds object when selections are not changed', async () => {
const folders = await createNTestFolders(1);
const notes = await createNTestNotes(5, folders[0]);
{
// Case 1. Selected notes are changed when one of selected notes is deleted.
let state = initTestState(folders, 0, notes, [0, 2, 4]);
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes.slice(0, 4), notesSource: 'test' });
const expected = [notes[0].id, notes[2].id].sort();
expect([...state.selectedNoteIds].sort()).toEqual(expected);
}
{
// Case 2. Selected notes and object identity are unchanged when notes are not changed.
let state = initTestState(folders, 0, notes, [0, 2, 4]);
const expected = state.selectedNoteIds;
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, notesSource: 'test' });
// Object identity is checked. Don't use toEqual() or toStrictEqual() here.
expect(state.selectedNoteIds).toBe(expected);
}
});
});

View File

@@ -433,7 +433,7 @@ function updateSelectedNotesFromExistingNotes(draft: Draft<State>) {
}
}
}
if (JSON.stringify(draft.selectedNoteIds) === JSON.stringify(newSelectedNoteIds)) return;
draft.selectedNoteIds = newSelectedNoteIds;
}

View File

@@ -12,6 +12,7 @@ interface MenuItem {
click: Function;
role?: any;
accelerator?: string;
enabled: boolean;
}
interface MenuItems {
@@ -78,6 +79,7 @@ export default class MenuUtils {
id: command.declaration.name,
label: this.service.label(commandName),
click: () => onClick(command.declaration.name),
enabled: true,
};
if (command.declaration.role) item.role = command.declaration.role;
@@ -132,10 +134,13 @@ export default class MenuUtils {
public pluginContextMenuItems(plugins: PluginStates, location: MenuItemLocation): MenuItem[] {
const output: MenuItem[] = [];
const pluginViewInfos = pluginUtils.viewInfosByType(plugins, 'menuItem');
const whenClauseContext = this.service.currentWhenClauseContext();
for (const info of pluginViewInfos) {
if (info.view.location !== location) continue;
output.push(this.commandToStatefulMenuItem(info.view.commandName));
const menuItem = this.commandToStatefulMenuItem(info.view.commandName);
menuItem.enabled = this.service.isEnabled(info.view.commandName, whenClauseContext);
output.push(menuItem);
}
if (output.length) output.splice(0, 0, { type: 'separator' } as any);

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