Compare commits
131 Commits
webpack_el
...
v2.11.6
Author | SHA1 | Date | |
---|---|---|---|
|
7a9afd4aff | ||
|
e647775608 | ||
|
577aa519e0 | ||
|
03cfef6a8d | ||
|
1219a30dff | ||
|
801719e3ba | ||
|
02098dbd79 | ||
|
e149e4b5fd | ||
|
b19f1a1023 | ||
|
192bfb5555 | ||
|
fd578d1c36 | ||
|
230e7f6914 | ||
|
12bba9da29 | ||
|
03424f76ea | ||
|
745849023d | ||
|
3e3b1764b7 | ||
|
902f0c3bf7 | ||
|
0f51249a76 | ||
|
3621e252d1 | ||
|
5669b1f04f | ||
|
91c99960ba | ||
|
b309504ffb | ||
|
22ab4b7473 | ||
|
04ea9343b0 | ||
|
b93f91078b | ||
|
b82e4be5c5 | ||
|
77a6e6f617 | ||
|
959ae59af0 | ||
|
98dd926cb9 | ||
|
8197c7aa7a | ||
|
1687adf015 | ||
|
7542ca907c | ||
|
1652b06e8c | ||
|
1a835ef094 | ||
|
575f55b22c | ||
|
d105387ece | ||
|
587eceb7c0 | ||
|
c90d7756c0 | ||
|
9e90d9016d | ||
|
ccec93eaa3 | ||
|
7bf823e0df | ||
|
b533d8d164 | ||
|
97477bb751 | ||
|
9cf863979b | ||
|
9ffe990c0b | ||
|
2c57e949b9 | ||
|
a7e185eb11 | ||
|
091eff9bc2 | ||
|
a04be2b28a | ||
|
caf66068bf | ||
|
8deba24d7d | ||
|
84b130e0cb | ||
|
e6209f449e | ||
|
faa4c5b9fc | ||
|
df9bfc7635 | ||
|
a46648f1ee | ||
|
215434bb4f | ||
|
40e0037d50 | ||
|
6afd839ae8 | ||
|
63595d2469 | ||
|
d715e1ba6b | ||
|
fb8a0c9ea9 | ||
|
ad20eba65e | ||
|
ea73e20115 | ||
|
1b8e9271a6 | ||
|
3a8a7a592d | ||
|
9d5c63de4f | ||
|
e37c95b9e5 | ||
|
201c7d893e | ||
|
c12108d1ac | ||
|
6ffd1046bd | ||
|
d4f49d6a02 | ||
|
cff24af099 | ||
|
b99ba85acd | ||
|
a0b707cbda | ||
|
1d7ffe358e | ||
|
9160f0e2a2 | ||
|
92272533e5 | ||
|
648e091523 | ||
|
b023f58497 | ||
|
c639791d4f | ||
|
6d52288e28 | ||
|
3e2f4b163b | ||
|
e0dbd198d8 | ||
|
a76c5c8746 | ||
|
20b43ce56e | ||
|
8215ce14c6 | ||
|
3fead0a8a7 | ||
|
c8bf3e8583 | ||
|
42dee6c275 | ||
|
6f3f866f78 | ||
|
adf2e7322d | ||
|
0df170926a | ||
|
20a26732a9 | ||
|
0da3e91a29 | ||
|
9ad56dc373 | ||
|
29dab26dce | ||
|
10e8fbcdab | ||
|
6c4f566765 | ||
|
78df302e86 | ||
|
f52dd4f098 | ||
|
dbab786c7b | ||
|
3eb44d27b2 | ||
|
52bea115ac | ||
|
19bdda25c6 | ||
|
b26bc9ed5f | ||
|
865cedc24f | ||
|
33f0811ad2 | ||
|
8cedf27fea | ||
|
052a829167 | ||
|
5371c97ccd | ||
|
c53b957293 | ||
|
e6f8dc96df | ||
|
21648b1b1b | ||
|
83db6f6596 | ||
|
3adfa574c0 | ||
|
4d0ffc5beb | ||
|
69f9b160dd | ||
|
c17b02cfb5 | ||
|
6dd57b63a6 | ||
|
248c8014c8 | ||
|
5fe2766a6b | ||
|
000e0ad517 | ||
|
c047375143 | ||
|
bd9e62cbd2 | ||
|
5ecae17538 | ||
|
35037e2dc9 | ||
|
059202be09 | ||
|
6672f63981 | ||
|
f390eca4de | ||
|
edc5e33559 |
@@ -363,6 +363,7 @@ packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
@@ -403,7 +404,6 @@ packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ScreenHeader.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
@@ -425,8 +425,8 @@ packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.dummy.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
@@ -483,6 +483,7 @@ packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
packages/lib/Synchronizer.js
|
||||
packages/lib/TaskQueue.js
|
||||
packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
@@ -518,6 +519,7 @@ packages/lib/import-enex-md-gen.js
|
||||
packages/lib/import-enex-md-gen.test.js
|
||||
packages/lib/import-enex.js
|
||||
packages/lib/locale.js
|
||||
packages/lib/locale.test.js
|
||||
packages/lib/markdownUtils.js
|
||||
packages/lib/markdownUtils.test.js
|
||||
packages/lib/markdownUtils2.test.js
|
||||
@@ -811,7 +813,6 @@ packages/plugins/ToggleSidebars/api/index.js
|
||||
packages/plugins/ToggleSidebars/api/types.js
|
||||
packages/plugins/ToggleSidebars/src/index.js
|
||||
packages/react-native-saf-x/src/index.js
|
||||
packages/react-native-vosk/src/index.js
|
||||
packages/renderer/HtmlToHtml.js
|
||||
packages/renderer/InMemoryCache.js
|
||||
packages/renderer/MarkupToHtml.js
|
||||
@@ -845,6 +846,7 @@ packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/build-welcome.js
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/bundleDefaultPlugins.js
|
||||
|
@@ -42,6 +42,8 @@ module.exports = {
|
||||
'zxcvbn': 'readonly',
|
||||
|
||||
'tinymce': 'readonly',
|
||||
|
||||
'JSX': 'readonly',
|
||||
},
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 2018,
|
||||
|
24
.github/scripts/run_ci.sh
vendored
@@ -183,12 +183,32 @@ if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
# cd "$ROOT_DIR/packages/tools"
|
||||
# node bundleDefaultPlugins.js
|
||||
cd "$ROOT_DIR/packages/app-desktop"
|
||||
USE_HARD_LINKS=false yarn run dist
|
||||
|
||||
if [ "$IS_MACOS" == "1" ]; then
|
||||
# This is to fix this error:
|
||||
#
|
||||
# Exit code: ENOENT. spawn /usr/bin/python ENOENT
|
||||
#
|
||||
# Ref: https://github.com/electron-userland/electron-builder/issues/6767#issuecomment-1096589528
|
||||
#
|
||||
# It can be removed once we upgrade to electron-builder@23, however we
|
||||
# cannot currently do this due to this error:
|
||||
# https://github.com/laurent22/joplin/issues/8149
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn run dist
|
||||
else
|
||||
USE_HARD_LINKS=false yarn run dist
|
||||
fi
|
||||
elif [[ $IS_LINUX = 1 ]] && [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
|
||||
echo "Step: Building Docker Image..."
|
||||
cd "$ROOT_DIR"
|
||||
yarn run buildServerDocker --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
|
||||
else
|
||||
echo "Step: Building but *not* publishing desktop application..."
|
||||
USE_HARD_LINKS=false yarn run dist --publish=never
|
||||
|
||||
if [ "$IS_MACOS" == "1" ]; then
|
||||
# See above why we need to specify Python
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn run dist --publish=never
|
||||
else
|
||||
USE_HARD_LINKS=false yarn run dist --publish=never
|
||||
fi
|
||||
fi
|
||||
|
3
.github/workflows/build-android.yml
vendored
@@ -17,9 +17,8 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
|
||||
BuildAndroidDebug:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
if: github.repository == 'laurent22/joplin' && needs.pre_job.outputs.should_skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Linux dependencies
|
||||
|
10
.gitignore
vendored
@@ -349,6 +349,7 @@ packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
@@ -389,7 +390,6 @@ packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ScreenHeader.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
@@ -411,8 +411,8 @@ packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.dummy.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
@@ -469,6 +469,7 @@ packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
packages/lib/Synchronizer.js
|
||||
packages/lib/TaskQueue.js
|
||||
packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
@@ -504,6 +505,7 @@ packages/lib/import-enex-md-gen.js
|
||||
packages/lib/import-enex-md-gen.test.js
|
||||
packages/lib/import-enex.js
|
||||
packages/lib/locale.js
|
||||
packages/lib/locale.test.js
|
||||
packages/lib/markdownUtils.js
|
||||
packages/lib/markdownUtils.test.js
|
||||
packages/lib/markdownUtils2.test.js
|
||||
@@ -797,7 +799,6 @@ packages/plugins/ToggleSidebars/api/index.js
|
||||
packages/plugins/ToggleSidebars/api/types.js
|
||||
packages/plugins/ToggleSidebars/src/index.js
|
||||
packages/react-native-saf-x/src/index.js
|
||||
packages/react-native-vosk/src/index.js
|
||||
packages/renderer/HtmlToHtml.js
|
||||
packages/renderer/InMemoryCache.js
|
||||
packages/renderer/MarkupToHtml.js
|
||||
@@ -831,6 +832,7 @@ packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/build-welcome.js
|
||||
packages/tools/buildServerDocker.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/bundleDefaultPlugins.js
|
||||
|
@@ -15,7 +15,6 @@
|
||||
"@joplin/tools",
|
||||
"@joplin/react-native-saf-x",
|
||||
"@joplin/react-native-alarm-notification",
|
||||
"@joplin/react-native-vosk",
|
||||
"@joplin/utils"
|
||||
]
|
||||
}
|
||||
|
@@ -105,6 +105,70 @@ index 441e41cc402cca3a60b34978ef4fea976076259c..a173acebb4b314402550442ad471e0f7
|
||||
unload: () => void;
|
||||
onResult: (onResult: (e: VoskEvent) => void) => EventSubscription;
|
||||
onFinalResult: (onFinalResult: (e: VoskEvent) => void) => EventSubscription;
|
||||
diff --git a/package.json b/package.json
|
||||
index 707eddb8d68007f93071ac659c5b087c935c5f01..90ebe20f224eeec472c377df1fef9b15f2ff8200 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -11,12 +11,9 @@
|
||||
"src",
|
||||
"lib",
|
||||
"android",
|
||||
- "ios",
|
||||
"cpp",
|
||||
- "react-native-vosk.podspec",
|
||||
"!lib/typescript/example",
|
||||
"!android/build",
|
||||
- "!ios/build",
|
||||
"!**/__tests__",
|
||||
"!**/__fixtures__",
|
||||
"!**/__mocks__"
|
||||
diff --git a/react-native-vosk.podspec b/react-native-vosk.podspec
|
||||
deleted file mode 100644
|
||||
index e3d41b90c5eef890c7a5108aaf16ac07d34a698b..0000000000000000000000000000000000000000
|
||||
--- a/react-native-vosk.podspec
|
||||
+++ /dev/null
|
||||
@@ -1,41 +0,0 @@
|
||||
-require "json"
|
||||
-
|
||||
-package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
||||
-folly_version = '2021.06.28.00-v2'
|
||||
-folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
||||
-
|
||||
-Pod::Spec.new do |s|
|
||||
- s.name = "react-native-vosk"
|
||||
- s.version = package["version"]
|
||||
- s.summary = package["description"]
|
||||
- s.homepage = package["homepage"]
|
||||
- s.license = package["license"]
|
||||
- s.authors = package["author"]
|
||||
-
|
||||
- s.platforms = { :ios => "10.0" }
|
||||
- s.source = { :git => "https://github.com/riderodd/react-native-vosk.git", :tag => "#{s.version}" }
|
||||
-
|
||||
- s.source_files = "ios/**/*.{h,m,mm,swift}"
|
||||
- s.resource_bundles = { 'Vosk' => ['ios/Vosk/*'] }
|
||||
-
|
||||
- s.dependency "React-Core"
|
||||
- s.frameworks = "Accelerate"
|
||||
- s.library = "c++"
|
||||
- s.vendored_frameworks = "ios/libvosk.xcframework"
|
||||
- s.requires_arc = true
|
||||
-
|
||||
- # Don't install the dependencies when we run `pod install` in the old architecture.
|
||||
- if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
|
||||
- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
||||
- s.pod_target_xcconfig = {
|
||||
- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
||||
- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
||||
- }
|
||||
-
|
||||
- s.dependency "React-Codegen"
|
||||
- s.dependency "RCT-Folly", folly_version
|
||||
- s.dependency "RCTRequired"
|
||||
- s.dependency "RCTTypeSafety"
|
||||
- s.dependency "ReactCommon/turbomodule/core"
|
||||
- end
|
||||
-end
|
||||
diff --git a/src/index.tsx b/src/index.tsx
|
||||
index d9f90c921d89b1b4d85e145443ed3376546a368a..29e4068dbd7500828a73145bd25497a52c9bf638 100644
|
||||
--- a/src/index.tsx
|
||||
|
78
Assets/ImageSources/JoplinIconForeground.svg
Normal file
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 108 108"
|
||||
height="108"
|
||||
width="108"
|
||||
xml:space="preserve"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
sodipodi:docname="JoplinLetter.svg"
|
||||
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
|
||||
id="metadata23">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
|
||||
|
||||
|
||||
</cc:Work>
|
||||
|
||||
</rdf:RDF>
|
||||
|
||||
</metadata>
|
||||
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
inkscape:document-rotation="0"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1094"
|
||||
id="namedview21"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.8"
|
||||
inkscape:cx="-2.1428571"
|
||||
inkscape:cy="73.214286"
|
||||
inkscape:window-x="-11"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
|
||||
|
||||
<defs
|
||||
id="defs6" />
|
||||
|
||||
|
||||
<g
|
||||
id="g865"
|
||||
transform="translate(44, 40), scale(0.33,0.33)"
|
||||
style="fill:none;fill-opacity:1"
|
||||
inkscape:label="Letter container"><path
|
||||
id="path30"
|
||||
fill="#ffffff"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:1.17263;stroke-dasharray:none;stroke-opacity:1;stop-opacity:1"
|
||||
d="M 92.634508,-48.8373 H 28.444063 c -0.606678,0 -1.097079,0.490859 -1.097079,1.096592 v 20.383573 c 0,0.686004 0.555444,1.241912 1.241922,1.241912 h 8.800852 c 3.559682,0 6.44623,2.738013 6.755314,6.217395 v 13.9751869 7.78593 74.1585111 0.52545 h -0.0016 c 0.02333,0.77689 -0.0096,1.54086 -0.08442,2.29329 -0.0096,0.10981 -0.02333,0.21821 -0.03653,0.32709 -0.05078,0.43965 -0.107028,0.87699 -0.192454,1.30511 -0.04661,0.24405 -0.112627,0.48164 -0.171063,0.72199 -0.07882,0.3197 -0.142579,0.64448 -0.240789,0.95727 -0.580372,1.85779 -1.49796,3.58087 -2.767117,5.09773 -0.04647,0.054 -0.104299,0.10381 -0.150278,0.15732 -0.387038,0.44843 -0.793477,0.88531 -1.243283,1.2945 -0.453957,0.4129 -0.934667,0.79673 -1.439823,1.15335 -3.591033,2.54379 -8.424421,3.66298 -13.824789,3.14629 -6.885879,-0.64724 -13.717305,-3.77002 -19.2390127,-8.79395 -5.52077603,-5.02346 -8.953577,-11.23901 -9.664036,-17.50346 -0.638943,-5.60429 1.040305,-10.53595 4.72500997,-13.88846 0.0096,-0.007 0.01866,-0.0142 0.0258,-0.0222 0.142742,-0.12779 0.299405,-0.23989 0.446104,-0.36307 2.63699903,-2.209329 6.07439703,-3.548119 9.99711673,-3.91719 0.04421,-0.0042 0.08683,-0.0112 0.128653,-0.0147 0.412878,-0.03644 0.833625,-0.05307 1.258052,-0.06875 0.221801,-0.0075 0.441028,-0.02309 0.666165,-0.02489 0.07465,-10e-4 0.145635,-0.0089 0.220634,-0.0089 0.133761,0 0.272634,0.0175 0.406421,0.01936 0.618197,0.0098 1.240993,0.03506 1.876236,0.08951 0.08027,0.0065 0.156833,0.0037 0.239457,0.0112 0.05865,0.0061 0.116802,0.0189 0.175239,0.02494 3.598402,0.360764 7.161287,1.43521 10.542386,3.06325 0.0705,0.003 0.152191,0.0249 0.254215,0.0812 1.030611,0.56929 1.228062,-0.0415 1.258503,-0.44519 V 33.605817 20.558368 c 0,-0.878838 -0.611246,-1.656187 -1.468421,-1.844874 -18.2116207,-4.008068 -36.079989,0.163761 -48.68826,11.641302 -11.014353,10.01925 -16.342292,24.477454 -14.615976,39.665024 1.5404,13.5277 8.484385,26.52393 19.555018,36.599 10.7901423,9.81673 24.5286593,15.9557 38.700372,17.29085 1.952829,0.18076 3.914888,0.27393 5.829429,0.27393 13.534593,0 26.093507,-4.64092 35.363086,-13.07322 8.781041,-7.99493 14.040701,-18.97328 14.812501,-30.91676 l 0.0714,-78.3303471 h 0.006 V -19.079901 h 0.002 v -0.365378 c 0.0793,-3.696201 3.09325,-6.669963 6.80976,-6.669963 h 8.80085 c 0.68555,0 1.24192,-0.555907 1.24192,-1.241909 v -20.383571 c 0,-0.605733 -0.4904,-1.096592 -1.09705,-1.096592"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:label="Letter" /></g>
|
||||
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
1
Assets/ImageSources/JoplinNotificationIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="#fff" d="M5.841 0C2.63 0 0 2.628 0 5.841V18.16C0 21.372 2.629 24 5.841 24H18.16C21.372 24 24 21.372 24 18.159V5.84C24 2.628 21.372 0 18.159 0Zm6.207 3.21h6.522a.111.111 0 0 1 .111.111v2.071c0 .07-.056.127-.126.127h-.894a.692.692 0 0 0-.692.677v2.165h-.001l-.007 7.96a4.628 4.628 0 0 1-1.505 3.14c-.942.858-2.218 1.33-3.594 1.33-.194 0-.393-.01-.592-.029a6.843 6.843 0 0 1-3.932-1.757c-1.125-1.023-1.83-2.344-1.987-3.718-.175-1.544.366-3.013 1.485-4.03 1.281-1.167 3.097-1.59 4.947-1.183.087.019.15.098.15.187v3.117c-.004.041-.024.104-.128.046a.059.059 0 0 0-.026-.009 3.22 3.22 0 0 0-1.072-.31c-.005-.001-.011-.003-.017-.003-.009-.001-.016 0-.025-.002a2.753 2.753 0 0 0-.19-.009l-.042-.002-.022.001c-.023 0-.045.002-.068.003a2.53 2.53 0 0 0-.127.007l-.014.001c-.398.038-.747.174-1.015.398l-.046.037a.02.02 0 0 1-.002.002c-.375.341-.545.842-.48 1.412.072.636.42 1.268.981 1.778.562.51 1.256.828 1.955.894.55.052 1.04-.062 1.405-.32a1.52 1.52 0 0 0 .146-.117c.046-.042.087-.086.127-.132l.015-.016c.129-.154.222-.329.281-.518.01-.031.017-.064.025-.097l.017-.073c.009-.044.014-.088.02-.133l.003-.033a1.81 1.81 0 0 0 .009-.233v-9.8a.69.69 0 0 0-.686-.631h-.895a.126.126 0 0 1-.126-.127v-2.07a.111.111 0 0 1 .112-.112Z" style="stroke-width:.015625"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/TranioOverseasProperty.jpg
Normal file
After Width: | Height: | Size: 6.1 KiB |
@@ -9,7 +9,9 @@ function getOs() {
|
||||
function getFilename(path) {
|
||||
if (!path) return '';
|
||||
const s = path.split('/');
|
||||
return s.pop();
|
||||
const urlWithParams = s.pop();
|
||||
const s2 = urlWithParams.split('?');
|
||||
return s2[0];
|
||||
}
|
||||
|
||||
function getMobileOs() {
|
||||
|
@@ -114,6 +114,20 @@ elif [[ $ARCHITECTURE =~ .*i386.*|.*i686.* ]] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#-----------------------------------------------------
|
||||
print "Checking dependencies..."
|
||||
## Check if libfuse2 is present.
|
||||
if [[ $(command -v ldconfig) ]]; then
|
||||
LIBFUSE=$(ldconfig -p | grep "libfuse.so.2" || echo '')
|
||||
else
|
||||
LIBFUSE=$(find /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib -name "libfuse.so.2" 2>/dev/null | grep "libfuse.so.2" || echo '')
|
||||
fi
|
||||
if [[ $LIBFUSE == "" ]] ; then
|
||||
print "${COLOR_RED}Error: Can't get libfuse2 on system, please install libfuse2${COLOR_RESET}"
|
||||
print "See https://joplinapp.org/faq/#desktop-application-will-not-launch-on-linux and https://github.com/AppImage/AppImageKit/wiki/FUSE for more information"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#-----------------------------------------------------
|
||||
# Download Joplin
|
||||
#-----------------------------------------------------
|
||||
@@ -134,10 +148,16 @@ else
|
||||
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION:-no version} installed."
|
||||
fi
|
||||
|
||||
# Check if it's an update or a new install
|
||||
DOWNLOAD_TYPE="New"
|
||||
if [[ -f ~/.joplin/Joplin.AppImage ]]; then
|
||||
DOWNLOAD_TYPE="Update"
|
||||
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.AppImage" "https://objects.joplinusercontent.com/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage?source=LinuxInstallScript&type=$DOWNLOAD_TYPE"
|
||||
wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
|
||||
|
||||
#-----------------------------------------------------
|
||||
|
14
README.md
@@ -22,21 +22,23 @@ Three types of applications are available: for **desktop** (Windows, macOS and L
|
||||
|
||||
Operating System | Download
|
||||
---|---
|
||||
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v2.9.17/Joplin-Setup-2.9.17.exe'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v2.9.17/Joplin-2.9.17.dmg'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.9.17/Joplin-2.9.17.AppImage'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
|
||||
Windows (32 and 64-bit) | <a href='https://objects.joplinusercontent.com/v2.10.19/Joplin-Setup-2.10.19.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
|
||||
macOS | <a href='https://objects.joplinusercontent.com/v2.10.19/Joplin-2.10.19.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
|
||||
Linux | <a href='https://objects.joplinusercontent.com/v2.10.19/Joplin-2.10.19.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
|
||||
|
||||
**On Windows**, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v2.9.17/JoplinPortable.exe'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v2.10.19/JoplinPortable.exe?source=JoplinWebsite&type=New'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
|
||||
**On Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:
|
||||
|
||||
<pre><code style="word-break: break-all">wget -O - https://raw.githubusercontent.com/laurent22/joplin/dev/Joplin_install_and_update.sh | bash</code></pre>
|
||||
|
||||
The install and update script supports the [following flags](https://github.com/laurent22/joplin/blob/dev/Joplin_install_and_update.sh#L50) (around line 50 at the time of this writing).
|
||||
|
||||
## Mobile applications
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
---|---|---
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.9.8/joplin-v2.9.8.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.9.8/joplin-v2.9.8-32bit.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://objects.joplinusercontent.com/android-v2.9.8/joplin-v2.9.8.apk?source=JoplinWebsite&type=New) [32-bit](https://objects.joplinusercontent.com/android-v2.9.8/joplin-v2.9.8-32bit.apk?source=JoplinWebsite&type=New)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
@@ -64,7 +66,7 @@ A community maintained list of these distributions can be found here: [Unofficia
|
||||
# Sponsors
|
||||
|
||||
<!-- SPONSORS-ORG -->
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://residence-greece.com/"><img title="Greece Golden Visa" width="256" src="https://joplinapp.org/images/sponsors/ResidenceGreece.jpg"/></a> <a href="https://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.png"/></a>
|
||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://residence-greece.com/"><img title="Greece Golden Visa" width="256" src="https://joplinapp.org/images/sponsors/ResidenceGreece.jpg"/></a> <a href="https://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.png"/></a> <a href="https://tranio.com/spain/"><img title="Property for sale in Spain" width="256" src="https://joplinapp.org/images/sponsors/TranioOverseasProperty.jpg"/></a>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
@@ -107,7 +107,10 @@
|
||||
".eslintignore": true,
|
||||
".gitignore": true,
|
||||
".vscode/*": true,
|
||||
".yarn": true,
|
||||
".yarn/cache": true,
|
||||
".yarn/install-state.gz": true,
|
||||
".yarn/plugins": true,
|
||||
".yarn/releases": true,
|
||||
"*.sublime-workspace": true,
|
||||
"**/_mydocs": true,
|
||||
"**/_mydocs/EnexSamples/*.enex": true,
|
||||
|
@@ -81,9 +81,10 @@
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "13.2.1",
|
||||
"lint-staged": "13.2.2",
|
||||
"madge": "6.0.0",
|
||||
"npm-package-json-lint": "6.4.0",
|
||||
"svg2vectordrawable": "^2.9.1",
|
||||
"typedoc": "0.17.8",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
|
@@ -57,7 +57,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"read-chunk": "2.1.0",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.32.0",
|
||||
"sharp": "0.32.1",
|
||||
"sprintf-js": "1.1.2",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
|
@@ -0,0 +1,19 @@
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:left">Left</td>
|
||||
<td style="text-align:center">Centered</td>
|
||||
<td style="text-align:right">Right</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:left">Left</td>
|
||||
<td style="text-align:center">Centered</td>
|
||||
<td style="text-align:right">Right</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:left">Left</td>
|
||||
<td style="text-align:center">Centered</td>
|
||||
<td style="text-align:right">Right</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@@ -0,0 +1,5 @@
|
||||
| | | |
|
||||
| :--- | :---: | ---: |
|
||||
| Left | Centered | Right |
|
||||
| Left | Centered | Right |
|
||||
| Left | Centered | Right |
|
@@ -0,0 +1,26 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left">Left-aligned Column</th>
|
||||
<th style="text-align:center">Center-aligned Column</th>
|
||||
<th style="text-align:right">Right-aligned Column</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:left">Left</td>
|
||||
<td style="text-align:center">Centered</td>
|
||||
<td style="text-align:right">Right</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:left">Left</td>
|
||||
<td style="text-align:center">Centered</td>
|
||||
<td style="text-align:right">Right</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:left">Left</td>
|
||||
<td style="text-align:center">Centered</td>
|
||||
<td style="text-align:right">Right</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@@ -0,0 +1,5 @@
|
||||
| Left-aligned Column | Center-aligned Column | Right-aligned Column |
|
||||
| :--- | :---: | ---: |
|
||||
| Left | Centered | Right |
|
||||
| Left | Centered | Right |
|
||||
| Left | Centered | Right |
|
@@ -0,0 +1,14 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">abc</th>
|
||||
<th align="right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">bar</td>
|
||||
<td align="right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@@ -0,0 +1,3 @@
|
||||
| abc | defghi |
|
||||
| :---: | ---: |
|
||||
| bar | baz |
|
@@ -0,0 +1,29 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left">Left-aligned Column</th>
|
||||
<th>This header cell's text is unaligned, but a majority of the text in this column is center-aligned so the
|
||||
column will be center-aligned</th>
|
||||
<th style="text-align:right">Right-aligned Column</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:left">Left</td>
|
||||
<td style="text-align:center">Centered</td>
|
||||
<td style="text-align:right">Right</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:right">This is the only right-aligned cell in this column. This is possible if a user
|
||||
edits a cell's alignment using the cell properties dialog.</td>
|
||||
<td style="text-align:center">Centered</td>
|
||||
<td style="text-align:right">Right</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:center">This is the only center-aligned cell in this column. This is possible if a
|
||||
user edits a cell's alignment using the cell properties dialog.</td>
|
||||
<td style="text-align:center">Centered</td>
|
||||
<td style="text-align:right">Right</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@@ -0,0 +1,5 @@
|
||||
| Left-aligned Column | This header cell's text is unaligned, but a majority of the text in this column is center-aligned so the column will be center-aligned | Right-aligned Column |
|
||||
| :--- | :---: | ---: |
|
||||
| Left | Centered | Right |
|
||||
| This is the only right-aligned cell in this column. This is possible if a user edits a cell's alignment using the cell properties dialog. | Centered | Right |
|
||||
| This is the only center-aligned cell in this column. This is possible if a user edits a cell's alignment using the cell properties dialog. | Centered | Right |
|
1
packages/app-cli/tests/md_to_html/sanitize_13.html
Normal file
@@ -0,0 +1 @@
|
||||
<div class="jop-noMdConv">
|
1
packages/app-cli/tests/md_to_html/sanitize_13.md
Normal file
@@ -0,0 +1 @@
|
||||
<div><svg><style></svg><iframe srcdoc="<script>top.require('child_process').execSync('calc')</script>"></iframe></div>
|
1
packages/app-cli/tests/md_to_html/sanitize_14.html
Normal file
@@ -0,0 +1 @@
|
||||
<a href="#" class="jop-noMdConv">XSS</a>
|
1
packages/app-cli/tests/md_to_html/sanitize_14.md
Normal file
@@ -0,0 +1 @@
|
||||
<a data-from-md="" href="javascript:top.require('child_process').execSync('open -a Calculator')">XSS</a>
|
1
packages/app-cli/tests/md_to_html/sanitize_15.html
Normal file
@@ -0,0 +1 @@
|
||||
<use href="#" class="jop-noMdConv">
|
1
packages/app-cli/tests/md_to_html/sanitize_15.md
Normal file
@@ -0,0 +1 @@
|
||||
<svg><use href="data:image/svg+xml,<svg id='x' xmlns='http://www.w3.org/2000/svg'><image href='asdf' onerror='top.require(`child_process`).execSync(`calc.exe`)' /></svg>#x" />
|
After Width: | Height: | Size: 193 B |
1
packages/app-cli/tests/md_to_html/sanitize_16.html
Normal file
@@ -0,0 +1 @@
|
||||
<map name="test" class="jop-noMdConv"><area coords="0,0,1000,1000" href="#" class="jop-noMdConv"/></map><img usemap="#test" src="https://github.com/Ry0taK.png" class="jop-noMdConv"/>
|
1
packages/app-cli/tests/md_to_html/sanitize_16.md
Normal file
@@ -0,0 +1 @@
|
||||
<map name="test"><area coords="0,0,1000,1000" href="javascript:top.require(`child_process`).execSync(`calc.exe`)"></map><img usemap="#test" src="https://github.com/Ry0taK.png">
|
@@ -68,6 +68,10 @@ export class Bridge {
|
||||
return process.argv;
|
||||
}
|
||||
|
||||
public getLocale = () => {
|
||||
return this.electronApp().electronApp().getLocale();
|
||||
};
|
||||
|
||||
// Applies to electron-context-menu@3:
|
||||
//
|
||||
// For now we have to disable spell checking in non-editor text
|
||||
|
@@ -97,7 +97,8 @@ async function fetchLatestRelease(options: CheckForUpdateOptions) {
|
||||
}
|
||||
|
||||
if (found) {
|
||||
downloadUrl = asset.browser_download_url;
|
||||
downloadUrl = asset.browser_download_url.replace('github.com/laurent22/joplin/releases/download', 'objects.joplinusercontent.com');
|
||||
downloadUrl.concat('?source=DesktopApp&type=Update');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -866,6 +866,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const syncInfo = localSyncInfoFromState(state);
|
||||
const showNeedUpgradingEnabledMasterKeyMessage = !!EncryptionService.instance().masterKeysThatNeedUpgrading(syncInfo.masterKeys.filter((k) => !!k.enabled)).length;
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
@@ -873,7 +874,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||
hasDisabledEncryptionItems: state.hasDisabledEncryptionItems,
|
||||
showMissingMasterKeyMessage: showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys),
|
||||
showNeedUpgradingMasterKeyMessage: !!EncryptionService.instance().masterKeysThatNeedUpgrading(syncInfo.masterKeys).length,
|
||||
showNeedUpgradingMasterKeyMessage: showNeedUpgradingEnabledMasterKeyMessage,
|
||||
showShouldReencryptMessage: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
|
||||
pluginsLegacy: state.pluginsLegacy,
|
||||
|
@@ -26,11 +26,15 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
private focusInput_: boolean;
|
||||
private styles_: any;
|
||||
private styleKey_: string;
|
||||
private menuIsOpened_: boolean = false;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.answerInput_ = React.createRef();
|
||||
|
||||
this.select_menuOpen = this.select_menuOpen.bind(this);
|
||||
this.select_menuClose = this.select_menuClose.bind(this);
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillMount() {
|
||||
@@ -39,6 +43,7 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
answer: this.props.defaultValue ? this.props.defaultValue : '',
|
||||
});
|
||||
this.focusInput_ = true;
|
||||
this.menuIsOpened_ = false;
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
@@ -52,6 +57,14 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
}
|
||||
}
|
||||
|
||||
private select_menuOpen() {
|
||||
this.menuIsOpened_ = true;
|
||||
}
|
||||
|
||||
private select_menuClose() {
|
||||
this.menuIsOpened_ = false;
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (this.focusInput_ && this.answerInput_.current) this.answerInput_.current.focus();
|
||||
this.focusInput_ = false;
|
||||
@@ -224,16 +237,14 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
|
||||
const onKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (this.props.inputType === 'tags' || this.props.inputType === 'dropdown') {
|
||||
// If the dropdown is open, we don't close the dialog - instead
|
||||
// the currently item will be selcted. If it is closed however
|
||||
// we confirm the dialog.
|
||||
if ((this.props.inputType === 'tags' || this.props.inputType === 'dropdown') && this.menuIsOpened_) {
|
||||
// Do nothing
|
||||
} else {
|
||||
onClose(true);
|
||||
}
|
||||
|
||||
// } else if (this.answerInput_.current && !this.answerInput_.current.state.menuIsOpen) {
|
||||
// // The menu will be open if the user is selecting a new item
|
||||
// onClose(true);
|
||||
// }
|
||||
} else if (event.key === 'Escape') {
|
||||
onClose(false);
|
||||
}
|
||||
@@ -246,9 +257,9 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
if (this.props.inputType === 'datetime') {
|
||||
inputComp = <Datetime className="datetime-picker" value={this.state.answer} inputProps={{ style: styles.input }} dateFormat={time.dateFormat()} timeFormat={time.timeFormat()} onChange={(momentObject: any) => onDateTimeChange(momentObject)} />;
|
||||
} else if (this.props.inputType === 'tags') {
|
||||
inputComp = <CreatableSelect className="tag-selector" styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} value={this.state.answer} placeholder="" components={makeAnimated()} isMulti={true} isClearable={false} backspaceRemovesValue={true} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
inputComp = <CreatableSelect className="tag-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} value={this.state.answer} placeholder="" components={makeAnimated()} isMulti={true} isClearable={false} backspaceRemovesValue={true} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
} else if (this.props.inputType === 'dropdown') {
|
||||
inputComp = <Select className="item-selector" styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} components={makeAnimated()} value={this.props.answer} defaultValue={this.props.defaultValue} isClearable={false} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
inputComp = <Select className="item-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} components={makeAnimated()} value={this.props.answer} defaultValue={this.props.defaultValue} isClearable={false} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
} else {
|
||||
inputComp = <input style={styles.input} ref={this.answerInput_} value={this.state.answer} type="text" onChange={event => onChange(event)} onKeyDown={event => onKeyDown(event)} />;
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@ import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
import ImportScreen from './ImportScreen';
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
import Navigator from './Navigator';
|
||||
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
|
||||
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
||||
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
|
||||
@@ -141,7 +141,7 @@ class RootComponent extends React.Component<Props, any> {
|
||||
});
|
||||
}
|
||||
|
||||
await WelcomeUtils.install(this.props.dispatch);
|
||||
await WelcomeUtils.install(Setting.value('locale'), this.props.dispatch);
|
||||
}
|
||||
|
||||
private renderModalMessage(props: ModalDialogProps) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { StyledRoot, StyledAddButton, StyledShareIcon, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
|
||||
import { ButtonLevel } from '../Button/Button';
|
||||
@@ -40,24 +40,15 @@ const { clipboard } = require('electron');
|
||||
|
||||
const logger = Logger.create('Sidebar');
|
||||
|
||||
const StyledFoldersHolder = styled.div`
|
||||
// linux bug: https://github.com/laurent22/joplin/issues/7506#issuecomment-1447101057
|
||||
& a.list-item {
|
||||
${shim.isLinux() && {
|
||||
opacity: 1,
|
||||
}}
|
||||
}
|
||||
`;
|
||||
const TagsHolder = styled.div`
|
||||
// linux bug: https://github.com/laurent22/joplin/issues/8000
|
||||
// solution ref: https://github.com/laurent22/joplin/issues/7506#issuecomment-1447101057
|
||||
& a.list-item {
|
||||
${shim.isLinux() && {
|
||||
opacity: 1,
|
||||
}}
|
||||
}
|
||||
// Workaround sidebar rendering bug on Linux Intel GPU.
|
||||
// https://github.com/laurent22/joplin/issues/7506
|
||||
const StyledSpanFix = styled.span`
|
||||
${shim.isLinux() && css`
|
||||
position: relative;
|
||||
`}
|
||||
`;
|
||||
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
@@ -138,7 +129,7 @@ function FolderItem(props: any) {
|
||||
}}
|
||||
onDoubleClick={onFolderToggleClick_}
|
||||
>
|
||||
{showFolderIcon ? renderFolderIcon(folderIcon) : null}<span className="title" style={{ lineHeight: 0 }}>{folderTitle}</span>
|
||||
{showFolderIcon ? renderFolderIcon(folderIcon) : null}<StyledSpanFix className="title" style={{ lineHeight: 0 }}>{folderTitle}</StyledSpanFix>
|
||||
{shareIcon} {noteCountComp}
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
@@ -573,7 +564,7 @@ const SidebarComponent = (props: Props) => {
|
||||
tagItem_click(tag);
|
||||
}}
|
||||
>
|
||||
<span className="tag-label">{Tag.displayTitle(tag)}</span>
|
||||
<StyledSpanFix className="tag-label">{Tag.displayTitle(tag)}</StyledSpanFix>
|
||||
{noteCount}
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
@@ -725,13 +716,13 @@ const SidebarComponent = (props: Props) => {
|
||||
const folderItems = [renderAllNotesItem(theme, allNotesSelected)].concat(result.items);
|
||||
folderItemsOrder_.current = result.order;
|
||||
items.push(
|
||||
<StyledFoldersHolder
|
||||
<div
|
||||
className={`folders ${props.folderHeaderIsExpanded ? 'expanded' : ''}`}
|
||||
key="folder_items"
|
||||
style={foldersStyle}
|
||||
>
|
||||
{folderItems}
|
||||
</StyledFoldersHolder>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -747,9 +738,9 @@ const SidebarComponent = (props: Props) => {
|
||||
tagItemsOrder_.current = result.order;
|
||||
|
||||
items.push(
|
||||
<TagsHolder className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
|
||||
<div className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
|
||||
{tagItems}
|
||||
</TagsHolder>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.11.1",
|
||||
"version": "2.11.6",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -12,7 +12,7 @@
|
||||
"electronRebuild": "gulp electronRebuild",
|
||||
"tsc": "tsc --project tsconfig.json",
|
||||
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
|
||||
"start": "gulp build && electron . --env dev --log-level debug --no-welcome --open-dev-tools",
|
||||
"start": "gulp build && electron . --env dev --log-level debug --open-dev-tools",
|
||||
"test": "jest",
|
||||
"test-ci": "yarn test"
|
||||
},
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
"build": {
|
||||
"appId": "net.cozic.joplin-desktop",
|
||||
"compression": "maximum",
|
||||
"compression": "normal",
|
||||
"productName": "Joplin",
|
||||
"npmRebuild": false,
|
||||
"afterSign": "./tools/notarizeMacApp.js",
|
||||
@@ -116,7 +116,7 @@
|
||||
"@types/react-redux": "7.1.25",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"electron": "19.1.4",
|
||||
"electron-builder": "23.6.0",
|
||||
"electron-builder": "22.11.7",
|
||||
"electron-notarize": "1.2.2",
|
||||
"electron-rebuild": "3.2.9",
|
||||
"glob": "8.1.0",
|
||||
@@ -163,7 +163,7 @@
|
||||
"react-datetime": "3.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "8.0.5",
|
||||
"react-select": "5.7.2",
|
||||
"react-select": "5.7.3",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
"redux": "4.2.1",
|
||||
@@ -171,7 +171,7 @@
|
||||
"roboto-fontface": "0.10.0",
|
||||
"smalltalk": "2.5.1",
|
||||
"sqlite3": "5.1.6",
|
||||
"styled-components": "5.3.9",
|
||||
"styled-components": "5.3.10",
|
||||
"styled-system": "5.1.5",
|
||||
"taboverride": "4.0.3",
|
||||
"tinymce": "5.10.6"
|
||||
|
@@ -79,9 +79,13 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
||||
*/
|
||||
|
||||
project.ext.react = [
|
||||
// 2023/05/07: Leave that to `false` for now because Hermes is rubbish at
|
||||
// reporting errors, which it makes it impossible to investigate crashes.
|
||||
enableHermes: false, // clean and rebuild if changing
|
||||
// 2023-05-09: This seems to be optional, but it's not. Without it, the app
|
||||
// will crash on certain devices with this error:
|
||||
//
|
||||
// > java.lang.UnsatisfiedLinkError: couldn't find DSO to load: libhermes.so"
|
||||
//
|
||||
// https://github.com/laurent22/joplin/issues/8144#issuecomment-1539629812
|
||||
enableHermes: true, // clean and rebuild if changing
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
@@ -152,8 +156,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097695
|
||||
versionName "2.11.10"
|
||||
versionCode 2097707
|
||||
versionName "2.11.22"
|
||||
// ndk {
|
||||
// abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
// }
|
||||
@@ -300,8 +304,11 @@ dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
// implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation ("com.facebook.react:react-native") version {
|
||||
strictly "0.70.6" // pass in your react-native version
|
||||
}
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
|
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M5.84 0C2.63 0 0 2.63 0 5.84V18.16C0 21.37 2.63 24 5.84 24H18.16C21.37 24 24 21.37 24 18.16V5.84C24 2.63 21.37 0 18.16 0Zm6.21 3.21h6.52a0.11 0.11 0 0 1 0.11 0.11v2.07c0 0.07-0.06 0.13-0.12 0.13h-0.9a0.69 0.69 0 0 0-0.69 0.68v2.16h0l-0.01 7.96a4.63 4.63 0 0 1-1.5 3.14c-0.94 0.86-2.22 1.33-3.6 1.33-0.19 0-0.39-0.01-0.59-0.03a6.84 6.84 0 0 1-3.93-1.75c-1.13-1.02-1.83-2.34-1.99-3.72-0.17-1.54 0.37-3.01 1.49-4.03 1.28-1.17 3.1-1.59 4.94-1.19 0.09 0.02 0.15 0.1 0.15 0.19v3.12c0 0.04-0.02 0.1-0.12 0.04a0.06 0.06 0 0 0-0.03 0 3.22 3.22 0 0 0-1.07-0.31c-0.01 0-0.01 0-0.02-0.01-0.01 0-0.02 0-0.02 0a2.75 2.75 0 0 0-0.19-0.01l-0.05 0-0.02 0c-0.02 0-0.04 0-0.07 0a2.53 2.53 0 0 0-0.12 0.01l-0.02 0c-0.4 0.04-0.75 0.17-1.01 0.4l-0.05 0.04a0.02 0.02 0 0 1 0 0c-0.38 0.34-0.55 0.84-0.48 1.41 0.07 0.64 0.42 1.27 0.98 1.78 0.56 0.51 1.26 0.83 1.96 0.89 0.55 0.05 1.04-0.06 1.4-0.32a1.52 1.52 0 0 0 0.15-0.11c0.05-0.04 0.09-0.09 0.12-0.14l0.02-0.01c0.13-0.15 0.22-0.33 0.28-0.52 0.01-0.03 0.02-0.06 0.02-0.1l0.02-0.07c0.01-0.04 0.01-0.09 0.02-0.13l0-0.04a1.81 1.81 0 0 0 0.01-0.23v-9.8a0.69 0.69 0 0 0-0.68-0.63h-0.9a0.13 0.13 0 0 1-0.12-0.13v-2.07a0.11 0.11 0 0 1 0.11-0.11Z"/>
|
||||
</vector>
|
After Width: | Height: | Size: 568 B |
After Width: | Height: | Size: 421 B |
After Width: | Height: | Size: 697 B |
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group
|
||||
android:scaleX="0.33"
|
||||
android:scaleY="0.33"
|
||||
android:translateX="44"
|
||||
android:translateY="40">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M92.63-48.84H28.44c-0.61 0-1.1 0.49-1.09 1.1v20.38c0 0.69 0.56 1.24 1.24 1.24h8.8c3.56 0 6.45 2.74 6.76 6.22v13.98 7.78 74.16 0.53h-0.01c0.02 0.78-0.01 1.54-0.08 2.29-0.01 0.11-0.02 0.22-0.04 0.33-0.05 0.44-0.11 0.88-0.19 1.3-0.05 0.24-0.11 0.48-0.17 0.72-0.08 0.32-0.14 0.64-0.24 0.96-0.58 1.86-1.5 3.58-2.77 5.1-0.05 0.05-0.1 0.1-0.15 0.16-0.39 0.45-0.79 0.89-1.24 1.29-0.45 0.41-0.93 0.8-1.44 1.15-3.59 2.54-8.42 3.66-13.83 3.15-6.89-0.65-13.72-3.77-19.24-8.79-5.52-5.02-8.95-11.24-9.66-17.51-0.64-5.6 1.04-10.54 4.72-13.88 0.01-0.01 0.02-0.01 0.03-0.03 0.14-0.13 0.3-0.24 0.45-0.36 2.64-2.21 6.07-3.55 9.99-3.92 0.04 0 0.09-0.01 0.13-0.01 0.41-0.04 0.83-0.05 1.26-0.07 0.22-0.01 0.44-0.02 0.67-0.03 0.07 0 0.15-0.01 0.22 0 0.13 0 0.27 0.02 0.4 0.01 0.62 0.01 1.24 0.04 1.88 0.09 0.08 0.01 0.16 0 0.24 0.02 0.06 0.01 0.12 0.02 0.17 0.02 3.6 0.36 7.16 1.44 10.55 3.06 0.07 0 0.15 0.02 0.25 0.09 1.03 0.57 1.23-0.04 1.26-0.45V33.61 20.56c0-0.88-0.61-1.66-1.47-1.85-18.21-4.01-36.08 0.16-48.69 11.64-11.01 10.02-16.34 24.48-14.61 39.67 1.54 13.53 8.48 26.52 19.55 36.6 10.79 9.82 24.53 15.96 38.7 17.29 1.95 0.18 3.91 0.27 5.83 0.27 13.53 0 26.09-4.64 35.37-13.07 8.78-7.99 14.04-18.97 14.81-30.92l0.07-78.33h0.01V-19.08h0v-0.37c0.08-3.7 3.09-6.67 6.81-6.67h8.8c0.69 0 1.24-0.56 1.24-1.24v-20.38c0-0.61-0.49-1.1-1.1-1.1"/>
|
||||
</group>
|
||||
</vector>
|
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 725 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,3 +1,4 @@
|
||||
module.exports = {
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
plugins: ['react-native-reanimated/plugin'],
|
||||
};
|
||||
|
90
packages/app-mobile/components/FolderPicker.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
const React = require('react');
|
||||
|
||||
import { FunctionComponent } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
|
||||
const { themeStyle } = require('./global-style.js');
|
||||
import Dropdown, { DropdownListItem, OnValueChangedListener } from './Dropdown';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
interface FolderPickerProps {
|
||||
disabled?: boolean;
|
||||
selectedFolderId?: string;
|
||||
onValueChange?: OnValueChangedListener;
|
||||
mustSelect?: boolean;
|
||||
folders: FolderEntity[];
|
||||
placeholder?: string;
|
||||
darkText?: boolean;
|
||||
themeId?: string;
|
||||
}
|
||||
|
||||
|
||||
const FolderPicker: FunctionComponent<FolderPickerProps> = ({
|
||||
disabled,
|
||||
selectedFolderId,
|
||||
onValueChange,
|
||||
mustSelect,
|
||||
folders,
|
||||
placeholder,
|
||||
darkText,
|
||||
themeId,
|
||||
}) => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const addFolderChildren = (
|
||||
folders: FolderEntityWithChildren[], pickerItems: DropdownListItem[], indent: number
|
||||
) => {
|
||||
folders.sort((a, b) => {
|
||||
const aTitle = a && a.title ? a.title : '';
|
||||
const bTitle = b && b.title ? b.title : '';
|
||||
return aTitle.toLowerCase() < bTitle.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const f = folders[i];
|
||||
const icon = Folder.unserializeIcon(f.icon);
|
||||
const iconString = icon ? `${icon.emoji} ` : '';
|
||||
pickerItems.push({ label: `${' '.repeat(indent)} ${iconString + Folder.displayTitle(f)}`, value: f.id });
|
||||
pickerItems = addFolderChildren(f.children, pickerItems, indent + 1);
|
||||
}
|
||||
|
||||
return pickerItems;
|
||||
};
|
||||
|
||||
const titlePickerItems = (mustSelect: boolean) => {
|
||||
const folderList = folders.filter(f => f.id !== Folder.conflictFolderId());
|
||||
let output = [];
|
||||
if (mustSelect) output.push({ label: placeholder || _('Move to notebook...'), value: '' });
|
||||
const folderTree = Folder.buildTree(folderList);
|
||||
output = addFolderChildren(folderTree, output, 0);
|
||||
return output;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={titlePickerItems(!!mustSelect)}
|
||||
disabled={disabled}
|
||||
labelTransform="trim"
|
||||
selectedValue={selectedFolderId || ''}
|
||||
itemListStyle={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}}
|
||||
headerStyle={{
|
||||
color: darkText ? theme.colorFaded : theme.colorBright2,
|
||||
fontSize: theme.fontSize,
|
||||
opacity: disabled ? theme.disabledOpacity : 1,
|
||||
}}
|
||||
itemStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onValueChange={(folderId) => {
|
||||
if (onValueChange) {
|
||||
onValueChange(folderId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderPicker;
|
@@ -10,9 +10,9 @@ import { Menu, MenuOptions, MenuOption, MenuTrigger } from 'react-native-popup-m
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
const { themeStyle } = require('./global-style.js');
|
||||
import Dropdown, { DropdownListItem, OnValueChangedListener } from './Dropdown';
|
||||
import { OnValueChangedListener } from './Dropdown';
|
||||
const { dialogs } = require('../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
@@ -20,6 +20,7 @@ import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
import CustomButton from './CustomButton';
|
||||
import FolderPicker from './FolderPicker';
|
||||
|
||||
// We need this to suppress the useless warning
|
||||
// https://github.com/oblador/react-native-vector-icons/issues/1465
|
||||
@@ -494,58 +495,14 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
}
|
||||
|
||||
const createTitleComponent = (disabled: boolean) => {
|
||||
const themeId = Setting.value('theme');
|
||||
const theme = themeStyle(themeId);
|
||||
const folderPickerOptions = this.props.folderPickerOptions;
|
||||
|
||||
if (folderPickerOptions && folderPickerOptions.enabled) {
|
||||
const addFolderChildren = (
|
||||
folders: FolderEntityWithChildren[], pickerItems: DropdownListItem[], indent: number
|
||||
) => {
|
||||
folders.sort((a, b) => {
|
||||
const aTitle = a && a.title ? a.title : '';
|
||||
const bTitle = b && b.title ? b.title : '';
|
||||
return aTitle.toLowerCase() < bTitle.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const f = folders[i];
|
||||
const icon = Folder.unserializeIcon(f.icon);
|
||||
const iconString = icon ? `${icon.emoji} ` : '';
|
||||
pickerItems.push({ label: `${' '.repeat(indent)} ${iconString + Folder.displayTitle(f)}`, value: f.id });
|
||||
pickerItems = addFolderChildren(f.children, pickerItems, indent + 1);
|
||||
}
|
||||
|
||||
return pickerItems;
|
||||
};
|
||||
|
||||
const titlePickerItems = (mustSelect: boolean) => {
|
||||
const folders = this.props.folders.filter(f => f.id !== Folder.conflictFolderId());
|
||||
let output = [];
|
||||
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
|
||||
const folderTree = Folder.buildTree(folders);
|
||||
output = addFolderChildren(folderTree, output, 0);
|
||||
return output;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={titlePickerItems(!!folderPickerOptions.mustSelect)}
|
||||
<FolderPicker
|
||||
themeId={themeId}
|
||||
disabled={disabled}
|
||||
labelTransform="trim"
|
||||
selectedValue={'selectedFolderId' in folderPickerOptions ? folderPickerOptions.selectedFolderId : null}
|
||||
itemListStyle={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}}
|
||||
headerStyle={{
|
||||
color: theme.colorBright2,
|
||||
fontSize: theme.fontSize,
|
||||
opacity: disabled ? theme.disabledOpacity : 1,
|
||||
}}
|
||||
itemStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
selectedFolderId={'selectedFolderId' in folderPickerOptions ? folderPickerOptions.selectedFolderId : null}
|
||||
onValueChange={async (folderId) => {
|
||||
// If onValueChange is specified, use this as a callback, otherwise do the default
|
||||
// which is to take the selectedNoteIds from the state and move them to the
|
||||
@@ -570,6 +527,8 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
await Note.moveToFolder(noteIds[i], folderId);
|
||||
}
|
||||
}}
|
||||
mustSelect={!!folderPickerOptions.mustSelect}
|
||||
folders={this.props.folders}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@@ -1,22 +0,0 @@
|
||||
const { connect } = require('react-redux');
|
||||
const SideMenu_ = require('react-native-side-menu-updated').default;
|
||||
import { Dimensions } from 'react-native';
|
||||
import { State } from '@joplin/lib/reducer';
|
||||
|
||||
class SideMenuComponent extends SideMenu_ {
|
||||
public onLayoutChange(e: any) {
|
||||
const { width, height } = e.nativeEvent.layout;
|
||||
const openMenuOffsetPercentage = this.props.openMenuOffset / Dimensions.get('window').width;
|
||||
const openMenuOffset = width * openMenuOffsetPercentage;
|
||||
const hiddenMenuOffset = width * this.state.hiddenMenuOffsetPercentage;
|
||||
this.setState({ width, height, openMenuOffset, hiddenMenuOffset });
|
||||
}
|
||||
}
|
||||
|
||||
const SideMenu = connect((state: State) => {
|
||||
return {
|
||||
isOpen: state.showSideMenu,
|
||||
};
|
||||
})(SideMenuComponent);
|
||||
|
||||
export default SideMenu;
|
@@ -98,6 +98,8 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
};
|
||||
|
||||
this.saveButton_press = this.saveButton_press.bind(this);
|
||||
|
||||
this.syncStatusButtonPress_ = () => {
|
||||
void NavService.go('Status');
|
||||
};
|
||||
@@ -199,6 +201,8 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
this.logButtonPress_ = () => {
|
||||
void NavService.go('Log');
|
||||
};
|
||||
|
||||
this.handleSetting = this.handleSetting.bind(this);
|
||||
}
|
||||
|
||||
public async checkFilesystemPermission() {
|
||||
@@ -482,8 +486,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
if (key === 'security.biometricsEnabled' && !!value) {
|
||||
try {
|
||||
await biometricAuthenticate();
|
||||
shared.updateSettingValue(this, key, value);
|
||||
await this.saveButton_press();
|
||||
shared.updateSettingValue(this, key, value, async () => await this.saveButton_press());
|
||||
} catch (error) {
|
||||
shared.updateSettingValue(this, key, false);
|
||||
Alert.alert(error.message);
|
||||
@@ -492,8 +495,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
if (key === 'security.biometricsEnabled' && !value) {
|
||||
shared.updateSettingValue(this, key, value);
|
||||
await this.saveButton_press();
|
||||
shared.updateSettingValue(this, key, value, async () => await this.saveButton_press());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -751,6 +753,12 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
</View>
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="version_info_hermes" style={this.styles().settingContainer}>
|
||||
<Text style={this.styles().settingText}>{_('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0)}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.themeId).root}>
|
||||
<ScreenHeader title={_('Configuration')} showSaveButton={true} showSearchButton={false} showSideMenuButton={false} saveButtonDisabled={!this.state.changedSettingKeys.length} onSaveButtonPress={this.saveButton_press} />
|
||||
|
@@ -1,6 +1,6 @@
|
||||
const React = require('react');
|
||||
|
||||
const { View } = require('react-native');
|
||||
const { View, StyleSheet } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
@@ -8,6 +8,7 @@ const { ScreenHeader } = require('../ScreenHeader');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
const { default: FolderPicker } = require('../FolderPicker');
|
||||
const TextInput = require('../TextInput').default;
|
||||
|
||||
class FolderScreenComponent extends BaseScreenComponent {
|
||||
@@ -60,10 +61,16 @@ class FolderScreenComponent extends BaseScreenComponent {
|
||||
this.folderComponent_change('title', text);
|
||||
}
|
||||
|
||||
parent_changeValue(parent) {
|
||||
this.folderComponent_change('parent_id', parent);
|
||||
}
|
||||
|
||||
|
||||
async saveFolderButton_press() {
|
||||
let folder = Object.assign({}, this.state.folder);
|
||||
|
||||
try {
|
||||
if (folder.id && !(await Folder.canNestUnder(folder.id, folder.parent_id))) throw new Error(_('Cannot move notebook to this location'));
|
||||
folder = await Folder.save(folder, { userSideValidation: true });
|
||||
} catch (error) {
|
||||
dialogs.error(this, _('The notebook could not be saved: %s', error.message));
|
||||
@@ -83,7 +90,7 @@ class FolderScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const saveButtonDisabled = !this.isModified();
|
||||
const saveButtonDisabled = !this.isModified() || !this.state.folder.title;
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.themeId).root}>
|
||||
@@ -94,7 +101,20 @@ class FolderScreenComponent extends BaseScreenComponent {
|
||||
autoFocus={true}
|
||||
value={this.state.folder.title}
|
||||
onChangeText={text => this.title_changeText(text)}
|
||||
disabled={this.state.folder.encryption_applied}
|
||||
/>
|
||||
<View style={styles.folderPickerContainer}>
|
||||
<FolderPicker
|
||||
themeId={this.props.themeId}
|
||||
placeholder={_('Select parent notebook')}
|
||||
folders={this.props.folders}
|
||||
selectedFolderId={this.state.folder.parent_id}
|
||||
onValueChange={newValue => this.parent_changeValue(newValue)}
|
||||
mustSelect
|
||||
darkText
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1 }} />
|
||||
<dialogs.DialogBox
|
||||
ref={dialogbox => {
|
||||
this.dialogbox = dialogbox;
|
||||
@@ -109,7 +129,18 @@ const FolderScreen = connect(state => {
|
||||
return {
|
||||
folderId: state.selectedFolderId,
|
||||
themeId: state.settings.theme,
|
||||
folders: state.folders.filter((folder) => folder.id !== state.selectedFolderId),
|
||||
};
|
||||
})(FolderScreenComponent);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
folderPickerContainer: {
|
||||
height: 46,
|
||||
paddingLeft: 14,
|
||||
paddingRight: 14,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = { FolderScreen };
|
||||
|
@@ -140,13 +140,8 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
_('Notebook: %s', folder.title),
|
||||
[
|
||||
{
|
||||
text: _('Rename'),
|
||||
text: _('Edit'),
|
||||
onPress: () => {
|
||||
if (folder.encryption_applied) {
|
||||
alert(_('Encrypted notebooks cannot be renamed'));
|
||||
return;
|
||||
}
|
||||
|
||||
props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
props.dispatch({
|
||||
|
@@ -10,6 +10,9 @@
|
||||
import 'react-native-get-random-values';
|
||||
import 'react-native-url-polyfill/auto';
|
||||
|
||||
// Set up required for react-native-drawer-layout (See: https://reactnavigation.org/docs/drawer-layout/ v6.x)
|
||||
import 'react-native-gesture-handler';
|
||||
|
||||
import { LogBox, AppRegistry } from 'react-native';
|
||||
const Root = require('./root').default;
|
||||
|
||||
|
@@ -420,7 +420,6 @@
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-vosk/Vosk.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
@@ -441,7 +440,6 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Vosk.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
@@ -517,13 +515,13 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 92;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.11.0;
|
||||
MARKETING_VERSION = 12.11.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -546,12 +544,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 92;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.11.0;
|
||||
MARKETING_VERSION = 12.11.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -698,14 +696,14 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 92;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.11.0;
|
||||
MARKETING_VERSION = 12.11.3;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -729,14 +727,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 89;
|
||||
CURRENT_PROJECT_VERSION = 92;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.11.0;
|
||||
MARKETING_VERSION = 12.11.3;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@@ -310,13 +310,13 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-image-resizer (1.4.5):
|
||||
- React-Core
|
||||
- react-native-netinfo (9.3.9):
|
||||
- react-native-netinfo (9.3.10):
|
||||
- React-Core
|
||||
- react-native-rsa-native (2.0.5):
|
||||
- React
|
||||
- react-native-saf-x (2.11.0):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.5.1):
|
||||
- react-native-safe-area-context (4.5.2):
|
||||
- RCT-Folly
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
@@ -328,8 +328,6 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-vosk (0.1.12):
|
||||
- React-Core
|
||||
- react-native-webview (11.26.1):
|
||||
- React-Core
|
||||
- React-perflogger (0.70.6)
|
||||
@@ -402,7 +400,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNCClipboard (1.5.1):
|
||||
- React-Core
|
||||
- RNCPushNotificationIOS (1.10.1):
|
||||
- RNCPushNotificationIOS (1.11.0):
|
||||
- React-Core
|
||||
- RNDateTimePicker (6.7.5):
|
||||
- React-Core
|
||||
@@ -412,8 +410,37 @@ PODS:
|
||||
- React-Core
|
||||
- RNFS (2.20.0):
|
||||
- React-Core
|
||||
- RNGestureHandler (2.9.0):
|
||||
- React-Core
|
||||
- RNQuickAction (0.3.13):
|
||||
- React
|
||||
- RNReanimated (3.0.2):
|
||||
- DoubleConversion
|
||||
- FBLazyVector
|
||||
- FBReactNativeSpec
|
||||
- glog
|
||||
- RCT-Folly
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-callinvoker
|
||||
- React-Core
|
||||
- React-Core/DevSupport
|
||||
- React-Core/RCTWebSocket
|
||||
- React-CoreModules
|
||||
- React-cxxreact
|
||||
- React-jsi
|
||||
- React-jsiexecutor
|
||||
- React-jsinspector
|
||||
- React-RCTActionSheet
|
||||
- React-RCTAnimation
|
||||
- React-RCTBlob
|
||||
- React-RCTImage
|
||||
- React-RCTLinking
|
||||
- React-RCTNetwork
|
||||
- React-RCTSettings
|
||||
- React-RCTText
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNSecureRandom (1.0.1):
|
||||
- React
|
||||
- RNShare (8.2.2):
|
||||
@@ -486,7 +513,6 @@ DEPENDENCIES:
|
||||
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
|
||||
- react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`)
|
||||
- react-native-version-info (from `../node_modules/react-native-version-info`)
|
||||
- react-native-vosk (from `../node_modules/react-native-vosk`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||
@@ -507,7 +533,9 @@ DEPENDENCIES:
|
||||
- RNExitApp (from `../node_modules/react-native-exit-app`)
|
||||
- RNFileViewer (from `../node_modules/react-native-file-viewer`)
|
||||
- RNFS (from `../node_modules/react-native-fs`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNQuickAction (from `../node_modules/react-native-quick-actions`)
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
|
||||
- RNShare (from `../node_modules/react-native-share`)
|
||||
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||
@@ -604,8 +632,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-sqlite-storage"
|
||||
react-native-version-info:
|
||||
:path: "../node_modules/react-native-version-info"
|
||||
react-native-vosk:
|
||||
:path: "../node_modules/react-native-vosk"
|
||||
react-native-webview:
|
||||
:path: "../node_modules/react-native-webview"
|
||||
React-perflogger:
|
||||
@@ -646,8 +672,12 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-file-viewer"
|
||||
RNFS:
|
||||
:path: "../node_modules/react-native-fs"
|
||||
RNGestureHandler:
|
||||
:path: "../node_modules/react-native-gesture-handler"
|
||||
RNQuickAction:
|
||||
:path: "../node_modules/react-native-quick-actions"
|
||||
RNReanimated:
|
||||
:path: "../node_modules/react-native-reanimated"
|
||||
RNSecureRandom:
|
||||
:path: "../node_modules/react-native-securerandom"
|
||||
RNShare:
|
||||
@@ -700,14 +730,13 @@ SPEC CHECKSUMS:
|
||||
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
|
||||
react-native-image-picker: ec9b713e248760bfa0f879f0715391de4651a7cb
|
||||
react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f
|
||||
react-native-netinfo: 22c082970cbd99071a4e5aa7a612ac20d66b08f0
|
||||
react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
react-native-saf-x: 9bd5238d3b43d76bbec64aa82c173ac20a4bce9f
|
||||
react-native-safe-area-context: f5549f36508b1b7497434baa0cd97d7e470920d4
|
||||
react-native-safe-area-context: 1d596539b05a78f2b346e954e7c577f6f9be7544
|
||||
react-native-slider: 33b8d190b59d4f67a541061bb91775d53d617d9d
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||
react-native-vosk: 33b8e82a46cc56f31bb4847a40efa2d160270e2e
|
||||
react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
|
||||
React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595
|
||||
React-RCTActionSheet: 7316773acabb374642b926c19aef1c115df5c466
|
||||
@@ -723,12 +752,14 @@ SPEC CHECKSUMS:
|
||||
ReactCommon: 349be31adeecffc7986a0de875d7fb0dcf4e251c
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCPushNotificationIOS: 87b8d16d3ede4532745e05b03c42cff33a36cc45
|
||||
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
||||
RNDateTimePicker: 65e1d202799460b286ff5e741d8baf54695e8abd
|
||||
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNReanimated: 0a5f87ec1da472cca3e835333fdebe51d983c411
|
||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||
RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
|
@@ -24,7 +24,6 @@ const localPackages = {
|
||||
'@joplin/fork-uslug': path.resolve(__dirname, '../fork-uslug/'),
|
||||
'@joplin/react-native-saf-x': path.resolve(__dirname, '../react-native-saf-x/'),
|
||||
'@joplin/react-native-alarm-notification': path.resolve(__dirname, '../react-native-alarm-notification/'),
|
||||
'@joplin/react-native-vosk': path.resolve(__dirname, '../react-native-vosk/'),
|
||||
};
|
||||
|
||||
const watchedFolders = [];
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"version": "2.11.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "react-native start --reset-cache",
|
||||
"start": "BROWSERSLIST_IGNORE_OLD_DATA=true react-native start --reset-cache",
|
||||
"android": "react-native run-android",
|
||||
"build": "gulp build",
|
||||
"tsc": "tsc --project tsconfig.json",
|
||||
@@ -25,8 +25,8 @@
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/datetimepicker": "6.7.5",
|
||||
"@react-native-community/geolocation": "2.1.0",
|
||||
"@react-native-community/netinfo": "9.3.9",
|
||||
"@react-native-community/push-notification-ios": "1.10.1",
|
||||
"@react-native-community/netinfo": "9.3.10",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-community/slider": "4.4.2",
|
||||
"assert-browserify": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
@@ -45,23 +45,25 @@
|
||||
"react-native-camera": "4.2.1",
|
||||
"react-native-dialogbox": "0.6.10",
|
||||
"react-native-document-picker": "8.2.0",
|
||||
"react-native-drawer-layout": "3.2.0",
|
||||
"react-native-dropdownalert": "4.5.1",
|
||||
"react-native-exit-app": "1.1.0",
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
"react-native-fingerprint-scanner": "6.0.0",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-gesture-handler": "2.9.0",
|
||||
"react-native-get-random-values": "1.8.0",
|
||||
"react-native-image-picker": "5.3.1",
|
||||
"react-native-image-resizer": "1.4.5",
|
||||
"react-native-modal-datetime-picker": "14.0.1",
|
||||
"react-native-paper": "5.5.2",
|
||||
"react-native-paper": "5.8.0",
|
||||
"react-native-popup-menu": "0.16.1",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-reanimated": "3.1.0",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "4.5.1",
|
||||
"react-native-safe-area-context": "4.5.2",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "8.2.2",
|
||||
"react-native-side-menu-updated": "1.3.2",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
"react-native-vector-icons": "9.2.0",
|
||||
@@ -106,7 +108,7 @@
|
||||
"jest": "29.4.3",
|
||||
"jest-environment-jsdom": "29.4.3",
|
||||
"jetifier": "2.0.0",
|
||||
"jsdom": "21.1.1",
|
||||
"jsdom": "21.1.2",
|
||||
"md5-file": "5.0.0",
|
||||
"metro-react-native-babel-preset": "0.72.3",
|
||||
"nodemon": "2.0.22",
|
||||
|
@@ -28,7 +28,7 @@ import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
|
||||
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
|
||||
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
|
||||
const VersionInfo = require('react-native-version-info').default;
|
||||
const { Keyboard, NativeModules, BackHandler, Animated, View, StatusBar, Platform, Dimensions } = require('react-native');
|
||||
const { Keyboard, NativeModules, BackHandler, View, StatusBar, Platform, Dimensions } = require('react-native');
|
||||
import { AppState as RNAppState, EmitterSubscription, Linking, NativeEventSubscription } from 'react-native';
|
||||
import getResponsiveValue from './components/getResponsiveValue';
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
@@ -67,7 +67,7 @@ const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js'
|
||||
import EncryptionConfigScreen from './components/screens/encryption-config';
|
||||
const { DropboxLoginScreen } = require('./components/screens/dropbox-login.js');
|
||||
const { MenuContext } = require('react-native-popup-menu');
|
||||
import SideMenu from './components/SideMenu';
|
||||
import { Drawer } from 'react-native-drawer-layout';
|
||||
import SideMenuContent from './components/side-menu-content';
|
||||
const { SideMenuContentNote } = require('./components/side-menu-content-note.js');
|
||||
const { DatabaseDriverReactNative } = require('./utils/database-driver-react-native');
|
||||
@@ -76,7 +76,7 @@ const { defaultState } = require('@joplin/lib/reducer');
|
||||
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local');
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
|
||||
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
|
||||
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
||||
const { themeStyle } = require('./components/global-style.js');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
const SyncTargetFilesystem = require('@joplin/lib/SyncTargetFilesystem.js');
|
||||
@@ -116,6 +116,9 @@ import ProfileEditor from './components/ProfileSwitcher/ProfileEditor';
|
||||
import sensorInfo from './components/biometrics/sensorInfo';
|
||||
import { getCurrentProfile } from '@joplin/lib/services/profileConfig';
|
||||
import { getDatabaseName, getProfilesRootDir, getResourceDir, setDispatch } from './services/profiles';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type SideMenuPosition = 'left' | 'right';
|
||||
|
||||
const logger = Logger.create('root');
|
||||
|
||||
@@ -142,7 +145,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
|
||||
if (action.type === 'NAV_GO') Keyboard.dismiss();
|
||||
|
||||
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
||||
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] }, true);
|
||||
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(1000, { syncSteps: ['update_remote', 'delete_remote'] }, true);
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
}
|
||||
|
||||
@@ -659,7 +662,7 @@ async function initialize(dispatch: Function) {
|
||||
// doWifiConnectionCheck set to true so initial sync
|
||||
// doesn't happen on mobile data
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
void reg.scheduleSync(1000, null, true).then(() => {
|
||||
void reg.scheduleSync(100, null, true).then(() => {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
void AlarmService.updateAllNotifications();
|
||||
@@ -667,7 +670,7 @@ async function initialize(dispatch: Function) {
|
||||
void DecryptionWorker.instance().scheduleStart();
|
||||
});
|
||||
|
||||
await WelcomeUtils.install(dispatch);
|
||||
await WelcomeUtils.install(Setting.value('locale'), dispatch);
|
||||
|
||||
// Collect revisions more frequently on mobile because it doesn't auto-save
|
||||
// and it cannot collect anything when the app is not active.
|
||||
@@ -713,7 +716,6 @@ class AppComponent extends React.Component {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
sideMenuContentOpacity: new Animated.Value(0),
|
||||
sideMenuWidth: this.getSideMenuWidth(),
|
||||
sensorInfo: null,
|
||||
};
|
||||
@@ -864,14 +866,7 @@ class AppComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: any) {
|
||||
if (this.props.showSideMenu !== prevProps.showSideMenu) {
|
||||
Animated.timing(this.state.sideMenuContentOpacity, {
|
||||
toValue: this.props.showSideMenu ? 0.5 : 0,
|
||||
duration: 600,
|
||||
}).start();
|
||||
}
|
||||
|
||||
public async componentDidUpdate(prevProps: any) {
|
||||
if (this.props.biometricsDone !== prevProps.biometricsDone && this.props.biometricsDone) {
|
||||
logger.info('Sharing: componentDidUpdate: biometricsDone');
|
||||
void this.handleShareData();
|
||||
@@ -950,8 +945,8 @@ class AppComponent extends React.Component {
|
||||
if (this.props.appState !== 'ready') return null;
|
||||
const theme: Theme = themeStyle(this.props.themeId);
|
||||
|
||||
let sideMenuContent = null;
|
||||
let menuPosition = 'left';
|
||||
let sideMenuContent: ReactNode = null;
|
||||
let menuPosition: SideMenuPosition = 'left';
|
||||
|
||||
if (this.props.routeName === 'Note') {
|
||||
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
|
||||
@@ -984,24 +979,27 @@ class AppComponent extends React.Component {
|
||||
const biometricIsEnabled = !!this.state.sensorInfo && this.state.sensorInfo.enabled;
|
||||
const shouldShowMainContent = !biometricIsEnabled || this.props.biometricsDone;
|
||||
|
||||
logger.info('root.biometrics: biometricsDone', this.props.biometricsDone);
|
||||
logger.info('root.biometrics: biometricIsEnabled', biometricIsEnabled);
|
||||
logger.info('root.biometrics: shouldShowMainContent', shouldShowMainContent);
|
||||
logger.info('root.biometrics: this.state.sensorInfo', this.state.sensorInfo);
|
||||
|
||||
const mainContent = (
|
||||
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
|
||||
<SideMenu
|
||||
menu={sideMenuContent}
|
||||
edgeHitWidth={5}
|
||||
openMenuOffset={this.state.sideMenuWidth}
|
||||
menuPosition={menuPosition}
|
||||
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
|
||||
onSliding={(percent: number) => {
|
||||
this.props.dispatch({
|
||||
type: 'SIDE_MENU_OPEN_PERCENT',
|
||||
value: percent,
|
||||
});
|
||||
<Drawer
|
||||
// Need to reset the key here based on menu position, otherwise
|
||||
// the drawer will flash open on screen and close every time the
|
||||
// drawer position switches (i.e. when opening or closing a note)
|
||||
key={`main-drawer-${menuPosition}`}
|
||||
open={this.props.showSideMenu}
|
||||
onOpen={() => this.sideMenu_change(true)}
|
||||
onClose={() => this.sideMenu_change(false)}
|
||||
drawerPosition={menuPosition}
|
||||
swipeEdgeWidth={15}
|
||||
drawerStyle={{
|
||||
width: this.state.sideMenuWidth,
|
||||
}}
|
||||
renderDrawerContent={() => sideMenuContent}
|
||||
>
|
||||
<StatusBar barStyle={statusBarStyle} />
|
||||
<MenuContext style={{ flex: 1 }}>
|
||||
@@ -1011,15 +1009,14 @@ class AppComponent extends React.Component {
|
||||
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
|
||||
</View>
|
||||
<DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
|
||||
<Animated.View pointerEvents='none' style={{ position: 'absolute', backgroundColor: 'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '120%' }}/>
|
||||
{ this.state.sensorInfo && <BiometricPopup
|
||||
{ !shouldShowMainContent && <BiometricPopup
|
||||
dispatch={this.props.dispatch}
|
||||
themeId={this.props.themeId}
|
||||
sensorInfo={this.state.sensorInfo}
|
||||
/> }
|
||||
</SafeAreaView>
|
||||
</MenuContext>
|
||||
</SideMenu>
|
||||
</Drawer>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1058,6 +1055,7 @@ const mapStateToProps = (state: any) => {
|
||||
themeId: state.settings.theme,
|
||||
noteSideMenuOptions: state.noteSideMenuOptions,
|
||||
biometricsDone: state.biometricsDone,
|
||||
biometricsEnabled: state.settings['security.biometricsEnabled'],
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -53,8 +53,7 @@ export default class AlarmServiceDriver {
|
||||
title: notification.title,
|
||||
message: notification.body ? notification.body : '-', // Required
|
||||
channel: 'net.cozic.joplin.notification',
|
||||
small_icon: 'ic_launcher_foreground', // Android requires the icon to be transparent
|
||||
color: 'blue',
|
||||
small_icon: 'ic_notification',
|
||||
data: {
|
||||
joplinNotificationId: notification.id,
|
||||
noteId: notification.noteId,
|
||||
|
@@ -1,3 +1,5 @@
|
||||
// Currently disabled on iOS
|
||||
|
||||
type Vosk = any;
|
||||
|
||||
export { Vosk };
|
@@ -13,6 +13,7 @@
|
||||
"tools/*.ts",
|
||||
],
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "node"]
|
||||
"types": ["jest", "node"],
|
||||
"moduleSuffixes": [".ios", ".android", ".native", ""]
|
||||
}
|
||||
}
|
||||
|
@@ -840,13 +840,8 @@ export default class BaseApplication {
|
||||
}
|
||||
|
||||
if (Setting.value('firstStart')) {
|
||||
// If it's a sub-profile, the locale must come from the root
|
||||
// profile.
|
||||
if (!Setting.value('isSubProfile')) {
|
||||
const locale = shim.detectAndSetLocale(Setting);
|
||||
reg.logger().info(`First start: detected locale as ${locale}`);
|
||||
}
|
||||
|
||||
const locale = shim.detectAndSetLocale(Setting);
|
||||
reg.logger().info(`First start: detected locale as ${locale}`);
|
||||
Setting.skipDefaultMigrations();
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
|
@@ -45,12 +45,13 @@ export default class JoplinServerApi {
|
||||
private options_: Options;
|
||||
private session_: Session;
|
||||
private debugRequests_: boolean = false;
|
||||
private debugRequestsShowPasswords_: boolean = false;
|
||||
|
||||
public constructor(options: Options) {
|
||||
this.options_ = options;
|
||||
|
||||
if (options.env === Env.Dev) {
|
||||
// this.debugRequests_ = true;
|
||||
if (options.env !== Env.Dev) {
|
||||
this.debugRequestsShowPasswords_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,15 +98,15 @@ export default class JoplinServerApi {
|
||||
try {
|
||||
const output = JSON.parse(o);
|
||||
if (!output) return o;
|
||||
if (output.password) output.password = '******';
|
||||
if (output.password && !this.debugRequestsShowPasswords_) output.password = '******';
|
||||
return JSON.stringify(output);
|
||||
} catch (error) {
|
||||
return o;
|
||||
}
|
||||
} else {
|
||||
const output = { ...o };
|
||||
if (output.password) output.password = '******';
|
||||
if (output['X-API-AUTH']) output['X-API-AUTH'] = '******';
|
||||
if (output.password && !this.debugRequestsShowPasswords_) output.password = '******';
|
||||
if (output['X-API-AUTH'] && !this.debugRequestsShowPasswords_) output['X-API-AUTH'] = '******';
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
@@ -1,80 +0,0 @@
|
||||
const welcomeAssets = require('./welcomeAssets');
|
||||
const Note = require('./models/Note').default;
|
||||
const Setting = require('./models/Setting').default;
|
||||
const Folder = require('./models/Folder').default;
|
||||
const Tag = require('./models/Tag').default;
|
||||
const shim = require('./shim').default;
|
||||
const uuid = require('./uuid').default;
|
||||
const { fileExtension, basename } = require('./path-utils');
|
||||
const { pregQuote } = require('./string-utils');
|
||||
|
||||
class WelcomeUtils {
|
||||
static async createWelcomeItems() {
|
||||
const output = {
|
||||
defaultFolderId: null,
|
||||
};
|
||||
|
||||
const folderAssets = welcomeAssets.folders;
|
||||
const tempDir = Setting.value('resourceDir');
|
||||
|
||||
for (let i = 0; i < folderAssets.length; i++) {
|
||||
const folderAsset = folderAssets[i];
|
||||
const folder = await Folder.save({ title: `${folderAsset.title} (${Setting.appTypeToLabel(Setting.value('appType'))})` });
|
||||
if (!output.defaultFolderId) output.defaultFolderId = folder.id;
|
||||
}
|
||||
|
||||
const noteAssets = welcomeAssets.notes;
|
||||
|
||||
for (let i = noteAssets.length - 1; i >= 0; i--) {
|
||||
const noteAsset = noteAssets[i];
|
||||
|
||||
let noteBody = noteAsset.body;
|
||||
|
||||
for (const resourceUrl in noteAsset.resources) {
|
||||
if (!noteAsset.resources.hasOwnProperty(resourceUrl)) continue;
|
||||
const resourceAsset = noteAsset.resources[resourceUrl];
|
||||
const ext = fileExtension(resourceUrl);
|
||||
const tempFilePath = `${tempDir}/${uuid.create()}.tmp.${ext}`;
|
||||
await shim.fsDriver().writeFile(tempFilePath, resourceAsset.body, 'base64');
|
||||
const resource = await shim.createResourceFromPath(tempFilePath, {
|
||||
title: basename(resourceUrl),
|
||||
});
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
|
||||
const regex = new RegExp(pregQuote(`(${resourceUrl})`), 'g');
|
||||
noteBody = noteBody.replace(regex, `(:/${resource.id})`);
|
||||
}
|
||||
|
||||
const note = await Note.save({
|
||||
parent_id: output.defaultFolderId,
|
||||
title: noteAsset.title,
|
||||
body: noteBody,
|
||||
});
|
||||
|
||||
if (noteAsset.tags) await Tag.setNoteTagsByTitles(note.id, noteAsset.tags);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async install(dispatch) {
|
||||
if (!Setting.value('welcome.enabled')) {
|
||||
Setting.setValue('welcome.wasBuilt', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Setting.value('welcome.wasBuilt')) {
|
||||
const result = await WelcomeUtils.createWelcomeItems();
|
||||
Setting.setValue('welcome.wasBuilt', true);
|
||||
|
||||
dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: result.defaultFolderId,
|
||||
});
|
||||
|
||||
Setting.setValue('activeFolderId', result.defaultFolderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WelcomeUtils;
|
122
packages/lib/WelcomeUtils.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
const welcomeAssetsAny = require('./welcomeAssets');
|
||||
import Note from './models/Note';
|
||||
import Setting from './models/Setting';
|
||||
import Folder from './models/Folder';
|
||||
import shim from './shim';
|
||||
import uuid from './uuid';
|
||||
import { fileExtension, basename } from './path-utils';
|
||||
import { _ } from './locale';
|
||||
const { pregQuote } = require('./string-utils');
|
||||
|
||||
export interface ItemMetadatum {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type ItemMetadata = Record<string, ItemMetadatum>;
|
||||
|
||||
export interface CreateWelcomeItemsResult {
|
||||
defaultFolderId: string;
|
||||
}
|
||||
|
||||
export interface WelcomeAssetResource {
|
||||
id: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface WelcomeAssetNote {
|
||||
id: string;
|
||||
parent_id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
resources: Record<string, WelcomeAssetResource>;
|
||||
}
|
||||
|
||||
export interface WelcomeAssetFolder {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface AssetContent {
|
||||
notes: WelcomeAssetNote[];
|
||||
folders: WelcomeAssetFolder[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type WelcomeAssets = Record<string, AssetContent>;
|
||||
|
||||
class WelcomeUtils {
|
||||
|
||||
public static async createWelcomeItems(locale: string): Promise<CreateWelcomeItemsResult> {
|
||||
const output: CreateWelcomeItemsResult = {
|
||||
defaultFolderId: null,
|
||||
};
|
||||
|
||||
const allWelcomeAssets = welcomeAssetsAny as WelcomeAssets;
|
||||
const welcomeAssets = locale in allWelcomeAssets ? allWelcomeAssets[locale] : allWelcomeAssets['en_GB'];
|
||||
const enGbWelcomeAssets = allWelcomeAssets['en_GB'];
|
||||
|
||||
const folderAssets = welcomeAssets.folders;
|
||||
const tempDir = Setting.value('resourceDir');
|
||||
|
||||
// Actually we don't really support mutiple folders at this point, because not needed
|
||||
for (let i = 0; i < folderAssets.length; i++) {
|
||||
const folder = await Folder.save({ title: _('Welcome!') });
|
||||
if (!output.defaultFolderId) output.defaultFolderId = folder.id;
|
||||
}
|
||||
|
||||
const noteAssets = welcomeAssets.notes;
|
||||
|
||||
for (let i = noteAssets.length - 1; i >= 0; i--) {
|
||||
const noteAsset = noteAssets[i];
|
||||
const enGbNoteAsset = enGbWelcomeAssets.notes[i];
|
||||
|
||||
let noteBody = noteAsset.body;
|
||||
|
||||
for (const resourceUrl in enGbNoteAsset.resources) {
|
||||
if (!enGbNoteAsset.resources.hasOwnProperty(resourceUrl)) continue;
|
||||
const resourceAsset = enGbNoteAsset.resources[resourceUrl];
|
||||
const ext = fileExtension(resourceUrl);
|
||||
const tempFilePath = `${tempDir}/${uuid.create()}.tmp.${ext}`;
|
||||
await shim.fsDriver().writeFile(tempFilePath, resourceAsset.body, 'base64');
|
||||
const resource = await shim.createResourceFromPath(tempFilePath, {
|
||||
title: basename(resourceUrl),
|
||||
});
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
|
||||
const regex = new RegExp(pregQuote(`(${resourceUrl})`), 'g');
|
||||
noteBody = noteBody.replace(regex, `(:/${resource.id})`);
|
||||
}
|
||||
|
||||
await Note.save({
|
||||
parent_id: output.defaultFolderId,
|
||||
title: noteAsset.title,
|
||||
body: noteBody,
|
||||
});
|
||||
|
||||
// if (noteAsset.tags) await Tag.setNoteTagsByTitles(note.id, noteAsset.tags);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public static async install(locale: string, dispatch: Function) {
|
||||
if (!Setting.value('welcome.enabled')) {
|
||||
Setting.setValue('welcome.wasBuilt', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Setting.value('welcome.wasBuilt')) {
|
||||
const result = await WelcomeUtils.createWelcomeItems(locale);
|
||||
Setting.setValue('welcome.wasBuilt', true);
|
||||
|
||||
dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: result.defaultFolderId,
|
||||
});
|
||||
|
||||
Setting.setValue('activeFolderId', result.defaultFolderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default WelcomeUtils;
|
@@ -75,7 +75,9 @@ shared.checkSyncConfigMessages = function(comp) {
|
||||
return output;
|
||||
};
|
||||
|
||||
shared.updateSettingValue = function(comp, key, value) {
|
||||
shared.updateSettingValue = function(comp, key, value, callback = null) {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
comp.setState(state => {
|
||||
// @react-native-community/slider (4.4.0) will emit a valueChanged event
|
||||
// when the component is mounted, even though the value hasn't changed.
|
||||
@@ -99,7 +101,7 @@ shared.updateSettingValue = function(comp, key, value) {
|
||||
settings: settings,
|
||||
changedSettingKeys: changedSettingKeys,
|
||||
};
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
|
||||
shared.scheduleSaveSettings = function(comp) {
|
||||
|
@@ -175,6 +175,10 @@ export default class FileApiDriverJoplinServer {
|
||||
// they can have names such as ".resources/xxxxxxxxxx'
|
||||
}
|
||||
|
||||
private isRejectedBySyncTargetError(error: any) {
|
||||
return error.code === 413 || error.code === 409 || error.httpCode === 413 || error.httpCode === 409;
|
||||
}
|
||||
|
||||
public async put(path: string, content: any, options: any = null) {
|
||||
try {
|
||||
const output = await this.api().exec('PUT', `${this.apiFilePath_(path)}/content`, options && options.shareId ? { share_id: options.shareId } : null, content, {
|
||||
@@ -182,7 +186,7 @@ export default class FileApiDriverJoplinServer {
|
||||
}, options);
|
||||
return output;
|
||||
} catch (error) {
|
||||
if (error.code === 413) {
|
||||
if (this.isRejectedBySyncTargetError(error)) {
|
||||
throw new JoplinError(error.message, 'rejectedByTarget');
|
||||
}
|
||||
throw error;
|
||||
@@ -190,7 +194,15 @@ export default class FileApiDriverJoplinServer {
|
||||
}
|
||||
|
||||
public async multiPut(items: MultiPutItem[], options: any = null) {
|
||||
return this.api().exec('PUT', 'api/batch_items', null, { items: items }, null, options);
|
||||
const output = await this.api().exec('PUT', 'api/batch_items', null, { items: items }, null, options);
|
||||
|
||||
for (const [, response] of Object.entries<any>(output.items)) {
|
||||
if (response.error && this.isRejectedBySyncTargetError(response.error)) {
|
||||
response.error.code = 'rejectedByTarget';
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async delete(path: string) {
|
||||
|
18
packages/lib/locale.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { closestSupportedLocale } from './locale';
|
||||
|
||||
describe('locale', () => {
|
||||
|
||||
it('should find the closest matching locale', () => {
|
||||
const testCases: [string, string[], string][] = [
|
||||
['fr', ['fr_FR', 'en_GB'], 'fr_FR'],
|
||||
['pt-br', ['fr_FR', 'en_GB', 'pt_BR'], 'pt_BR'],
|
||||
['ro', ['fr_FR', 'en_GB', 'pt_BR'], 'en_GB'],
|
||||
];
|
||||
|
||||
for (const [input, locales, expected] of testCases) {
|
||||
const actual = closestSupportedLocale(input, true, locales);
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
@@ -584,14 +584,7 @@ function localesFromLanguageCode(languageCode: string, locales: string[]): strin
|
||||
}
|
||||
|
||||
function _(s: string, ...args: any[]): string {
|
||||
const strings = localeStrings(currentLocale_);
|
||||
let result = strings[s];
|
||||
if (result === '' || result === undefined) result = s;
|
||||
try {
|
||||
return sprintf(result, ...args);
|
||||
} catch (error) {
|
||||
return `${result} ${args.join(', ')} (Translation error: ${error.message})`;
|
||||
}
|
||||
return stringByLocale(currentLocale_, s, ...args);
|
||||
}
|
||||
|
||||
function _n(singular: string, plural: string, n: number, ...args: any[]) {
|
||||
@@ -599,4 +592,15 @@ function _n(singular: string, plural: string, n: number, ...args: any[]) {
|
||||
return _(singular, ...args);
|
||||
}
|
||||
|
||||
const stringByLocale = (locale: string, s: string, ...args: any[]): string => {
|
||||
const strings = localeStrings(locale);
|
||||
let result = strings[s];
|
||||
if (result === '' || result === undefined) result = s;
|
||||
try {
|
||||
return sprintf(result, ...args);
|
||||
} catch (error) {
|
||||
return `${result} ${args.join(', ')} (Translation error: ${error.message})`;
|
||||
}
|
||||
};
|
||||
|
||||
export { _, _n, supportedLocales, currentLocale, localesFromLanguageCode, languageCodeOnly, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Setting, { SettingSectionSource, SettingStorage } from '../models/Setting';
|
||||
import Setting, { SettingItemType, SettingSectionSource, SettingStorage } from '../models/Setting';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow, msleep } from '../testing/test-utils';
|
||||
import { readFile, stat, mkdirp, writeFile, pathExists, readdir } from 'fs-extra';
|
||||
import Logger from '../Logger';
|
||||
@@ -61,11 +61,11 @@ describe('models/Setting', () => {
|
||||
await Setting.reset();
|
||||
|
||||
await expectNotThrow(async () => Setting.load());
|
||||
await expectThrow(async () => Setting.value('itsgone'));
|
||||
await expectThrow(async () => Setting.value('itsgone'), 'unknown_key');
|
||||
}));
|
||||
|
||||
it('should allow registering new settings dynamically', (async () => {
|
||||
await expectThrow(async () => Setting.setValue('myCustom', '123'));
|
||||
await expectThrow(async () => Setting.setValue('myCustom', '123'), 'unknown_key');
|
||||
|
||||
await Setting.registerSetting('myCustom', {
|
||||
public: true,
|
||||
@@ -297,12 +297,21 @@ describe('models/Setting', () => {
|
||||
expect(Setting.isSet('spellChecker.languages')).toBe(false);
|
||||
}));
|
||||
|
||||
it('should load sub-profile settings - 1', async () => {
|
||||
it('should load sub-profile settings', async () => {
|
||||
await Setting.reset();
|
||||
|
||||
await Setting.registerSetting('non_builtin', {
|
||||
public: true,
|
||||
storage: SettingStorage.File,
|
||||
isGlobal: true,
|
||||
type: SettingItemType.Bool,
|
||||
value: false,
|
||||
});
|
||||
|
||||
Setting.setValue('locale', 'fr_FR'); // Global setting
|
||||
Setting.setValue('theme', Setting.THEME_DARK); // Global setting
|
||||
Setting.setValue('sync.target', 9); // Local setting
|
||||
Setting.setValue('non_builtin', true); // Local setting
|
||||
await Setting.saveAll();
|
||||
|
||||
await switchToSubProfileSettings();
|
||||
@@ -311,6 +320,9 @@ describe('models/Setting', () => {
|
||||
expect(Setting.value('theme')).toBe(Setting.THEME_DARK); // Should come from the root profile
|
||||
expect(Setting.value('sync.target')).toBe(0); // Should come from the local profile
|
||||
|
||||
// Non-built-in variables are not copied
|
||||
expect(() => Setting.value('non_builtin')).toThrow();
|
||||
|
||||
// Also check that the special loadOne() function works as expected
|
||||
|
||||
expect((await Setting.loadOne('locale')).value).toBe('fr_FR');
|
||||
@@ -318,7 +330,7 @@ describe('models/Setting', () => {
|
||||
expect((await Setting.loadOne('sync.target')).value).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should save sub-profile settings - 2', async () => {
|
||||
it('should save sub-profile settings', async () => {
|
||||
await Setting.reset();
|
||||
Setting.setValue('locale', 'fr_FR'); // Global setting
|
||||
Setting.setValue('theme', Setting.THEME_DARK); // Global setting
|
||||
|
@@ -9,6 +9,7 @@ import FileHandler, { SettingValues } from './settings/FileHandler';
|
||||
import Logger from '../Logger';
|
||||
import mergeGlobalAndLocalSettings from '../services/profileConfig/mergeGlobalAndLocalSettings';
|
||||
import splitGlobalAndLocalSettings from '../services/profileConfig/splitGlobalAndLocalSettings';
|
||||
import JoplinError from '../JoplinError';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const ObjectUtils = require('../ObjectUtils');
|
||||
const { toTitleCase } = require('../string-utils.js');
|
||||
@@ -312,6 +313,7 @@ class Setting extends BaseModel {
|
||||
private static fileHandler_: FileHandler = null;
|
||||
private static rootFileHandler_: FileHandler = null;
|
||||
private static settingFilename_: string = 'settings.json';
|
||||
private static buildInMetadata_: SettingItems = null;
|
||||
|
||||
public static tableName() {
|
||||
return 'settings';
|
||||
@@ -406,7 +408,7 @@ class Setting extends BaseModel {
|
||||
return output;
|
||||
};
|
||||
|
||||
this.metadata_ = {
|
||||
this.buildInMetadata_ = {
|
||||
'clientId': {
|
||||
value: '',
|
||||
type: SettingItemType.String,
|
||||
@@ -1696,6 +1698,8 @@ class Setting extends BaseModel {
|
||||
|
||||
};
|
||||
|
||||
this.metadata_ = { ...this.buildInMetadata_ };
|
||||
|
||||
this.metadata_ = Object.assign(this.metadata_, this.customMetadata_);
|
||||
|
||||
if (this.constants_.env === Env.Dev) this.validateMetadata(this.metadata_);
|
||||
@@ -1709,6 +1713,10 @@ class Setting extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
public static isBuiltinKey(key: string): boolean {
|
||||
return key in this.buildInMetadata_;
|
||||
}
|
||||
|
||||
public static customCssFilePath(filename: string): string {
|
||||
return `${this.value('rootProfileDir')}/${filename}`;
|
||||
}
|
||||
@@ -1810,7 +1818,7 @@ class Setting extends BaseModel {
|
||||
|
||||
public static settingMetadata(key: string): SettingItem {
|
||||
const metadata = this.metadata();
|
||||
if (!(key in metadata)) throw new Error(`Unknown key: ${key}`);
|
||||
if (!(key in metadata)) throw new JoplinError(`Unknown key: ${key}`, 'unknown_key');
|
||||
const output = Object.assign({}, metadata[key]);
|
||||
output.key = key;
|
||||
return output;
|
||||
@@ -2255,7 +2263,7 @@ class Setting extends BaseModel {
|
||||
|
||||
public static enumOptions(key: string) {
|
||||
const metadata = this.metadata();
|
||||
if (!metadata[key]) throw new Error(`Unknown key: ${key}`);
|
||||
if (!metadata[key]) throw new JoplinError(`Unknown key: ${key}`, 'unknown_key');
|
||||
if (!metadata[key].options) throw new Error(`No options for: ${key}`);
|
||||
return metadata[key].options();
|
||||
}
|
||||
|
@@ -2,19 +2,29 @@ import shim from './shim';
|
||||
import time from './time';
|
||||
const ntpClient_ = require('./vendor/ntp-client');
|
||||
|
||||
const server = {
|
||||
domain: 'pool.ntp.org',
|
||||
port: 123,
|
||||
};
|
||||
interface NtpServer {
|
||||
domain: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
function ntpClient() {
|
||||
ntpClient_.dgram = shim.dgram();
|
||||
return ntpClient_;
|
||||
}
|
||||
|
||||
export async function getNetworkTime(): Promise<Date> {
|
||||
const parseNtpServer = (ntpServer: string): NtpServer => {
|
||||
const s = ntpServer.split(':');
|
||||
if (s.length !== 2) throw new Error('NTP server URL must be in format `domain:port`');
|
||||
return {
|
||||
domain: s[0],
|
||||
port: Number(s[1]),
|
||||
};
|
||||
};
|
||||
|
||||
export async function getNetworkTime(ntpServer: string): Promise<Date> {
|
||||
return new Promise((resolve: Function, reject: Function) => {
|
||||
ntpClient().getNetworkTime(server.domain, server.port, (error: any, date: Date) => {
|
||||
const s = parseNtpServer(ntpServer);
|
||||
ntpClient().getNetworkTime(s.domain, s.port, (error: any, date: Date) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
@@ -25,7 +35,7 @@ export async function getNetworkTime(): Promise<Date> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDeviceTimeDrift(): Promise<number> {
|
||||
export async function getDeviceTimeDrift(ntpServer: string): Promise<number> {
|
||||
const maxTries = 3;
|
||||
let tryCount = 0;
|
||||
|
||||
@@ -34,12 +44,12 @@ export async function getDeviceTimeDrift(): Promise<number> {
|
||||
while (true) {
|
||||
tryCount++;
|
||||
try {
|
||||
ntpTime = await getNetworkTime();
|
||||
ntpTime = await getNetworkTime(ntpServer);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (tryCount >= maxTries) {
|
||||
const newError = typeof error === 'string' ? new Error(error) : error;
|
||||
newError.message = `Cannot retrieve the network time from ${server.domain}:${server.port}: ${newError.message}`;
|
||||
newError.message = `Cannot retrieve the network time from ${ntpServer}: ${newError.message}`;
|
||||
throw newError;
|
||||
} else {
|
||||
await time.msleep(tryCount * 1000);
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"@types/uuid": "^9.0.0",
|
||||
"clean-html": "1.5.0",
|
||||
"jest": "29.4.3",
|
||||
"sharp": "0.32.0",
|
||||
"sharp": "0.32.1",
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -86,7 +86,7 @@
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
"string-to-stream": "3.0.1",
|
||||
"tar": "6.1.13",
|
||||
"tar": "6.1.14",
|
||||
"tcp-port-used": "1.0.2",
|
||||
"uglifycss": "0.0.29",
|
||||
"url-parse": "1.5.10",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow, expectThrow, kvStore } from '../../testing/test-utils';
|
||||
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, encryptionService, expectNotThrow, expectThrow, kvStore, msleep } from '../../testing/test-utils';
|
||||
import MasterKey from '../../models/MasterKey';
|
||||
import { migrateMasterPassword, resetMasterPassword, showMissingMasterKeyMessage, updateMasterPassword } from './utils';
|
||||
import { activeMasterKeySanityCheck, migrateMasterPassword, resetMasterPassword, showMissingMasterKeyMessage, updateMasterPassword } from './utils';
|
||||
import { localSyncInfo, masterKeyById, masterKeyEnabled, setActiveMasterKeyId, setMasterKeyEnabled, setPpk } from '../synchronizer/syncInfoUtils';
|
||||
import Setting from '../../models/Setting';
|
||||
import { generateKeyPair, ppkPasswordIsValid } from './ppk';
|
||||
@@ -145,6 +145,43 @@ describe('e2ee/utils', () => {
|
||||
expect(localSyncInfo().ppk.id).not.toBe(previousPpk.id);
|
||||
expect(localSyncInfo().ppk.privateKey.ciphertext).not.toBe(previousPpk.privateKey.ciphertext);
|
||||
expect(localSyncInfo().ppk.publicKey).not.toBe(previousPpk.publicKey);
|
||||
|
||||
// Also check that a new master key has been created, that it is active and enabled
|
||||
expect(localSyncInfo().masterKeys.length).toBe(3);
|
||||
expect(localSyncInfo().activeMasterKeyId).toBe(localSyncInfo().masterKeys[2].id);
|
||||
expect(masterKeyEnabled(localSyncInfo().masterKeys[2])).toBe(true);
|
||||
});
|
||||
|
||||
it('should fix active key selection issues - 1', async () => {
|
||||
const masterPassword1 = '111111';
|
||||
Setting.setValue('encryption.masterPassword', masterPassword1);
|
||||
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||
await msleep(1);
|
||||
await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||
await msleep(1);
|
||||
const mk3 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||
setActiveMasterKeyId(mk1.id);
|
||||
setMasterKeyEnabled(mk1.id, false);
|
||||
|
||||
activeMasterKeySanityCheck();
|
||||
|
||||
const syncInfo = localSyncInfo();
|
||||
expect(syncInfo.activeMasterKeyId).toBe(mk3.id);
|
||||
});
|
||||
|
||||
it('should fix active key selection issues - 2', async () => {
|
||||
// Should not do anything if the active key is already an enabled one.
|
||||
const masterPassword1 = '111111';
|
||||
Setting.setValue('encryption.masterPassword', masterPassword1);
|
||||
const mk1 = await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||
await msleep(1);
|
||||
await MasterKey.save(await encryptionService().generateMasterKey(masterPassword1));
|
||||
setActiveMasterKeyId(mk1.id);
|
||||
|
||||
activeMasterKeySanityCheck();
|
||||
|
||||
const syncInfo = localSyncInfo();
|
||||
expect(syncInfo.activeMasterKeyId).toBe(mk1.id);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@ import MasterKey from '../../models/MasterKey';
|
||||
import Setting from '../../models/Setting';
|
||||
import { MasterKeyEntity } from './types';
|
||||
import EncryptionService from './EncryptionService';
|
||||
import { getActiveMasterKey, getActiveMasterKeyId, localSyncInfo, masterKeyEnabled, saveLocalSyncInfo, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
|
||||
import { getActiveMasterKey, getActiveMasterKeyId, localSyncInfo, masterKeyEnabled, saveLocalSyncInfo, setActiveMasterKeyId, setEncryptionEnabled, SyncInfo } from '../synchronizer/syncInfoUtils';
|
||||
import JoplinError from '../../JoplinError';
|
||||
import { generateKeyPair, pkReencryptPrivateKey, ppkPasswordIsValid } from './ppk';
|
||||
import KvStore from '../KvStore';
|
||||
@@ -131,6 +131,8 @@ export async function findMasterKeyPassword(service: EncryptionService, masterKe
|
||||
}
|
||||
|
||||
export async function loadMasterKeysFromSettings(service: EncryptionService) {
|
||||
activeMasterKeySanityCheck();
|
||||
|
||||
const masterKeys = await MasterKey.all();
|
||||
const activeMasterKeyId = getActiveMasterKeyId();
|
||||
|
||||
@@ -153,6 +155,35 @@ export async function loadMasterKeysFromSettings(service: EncryptionService) {
|
||||
logger.info(`Loaded master keys: ${service.loadedMasterKeysCount()}`);
|
||||
}
|
||||
|
||||
// In some rare cases (normally should no longer be possible), a disabled master
|
||||
// key end up being the active one (the one used to encrypt data). This sanity
|
||||
// check resolves this by making an enabled key the active one.
|
||||
export const activeMasterKeySanityCheck = () => {
|
||||
const syncInfo = localSyncInfo();
|
||||
const activeMasterKeyId = syncInfo.activeMasterKeyId;
|
||||
const enabledMasterKeys = syncInfo.masterKeys.filter(mk => masterKeyEnabled(mk));
|
||||
if (!enabledMasterKeys.length) return;
|
||||
|
||||
if (enabledMasterKeys.find(mk => mk.id === activeMasterKeyId)) {
|
||||
logger.info('activeMasterKeySanityCheck: Active key is an enabled key - nothing to do');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('activeMasterKeySanityCheck: Active key is **not** an enabled key - selecting a different key as the active key...');
|
||||
|
||||
const latestMasterKey = enabledMasterKeys.reduce((acc: MasterKeyEntity, current: MasterKeyEntity) => {
|
||||
if (current.created_time > acc.created_time) {
|
||||
return current;
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('activeMasterKeySanityCheck: Selected new active key:', latestMasterKey);
|
||||
|
||||
setActiveMasterKeyId(latestMasterKey.id);
|
||||
};
|
||||
|
||||
export function showMissingMasterKeyMessage(syncInfo: SyncInfo, notLoadedMasterKeys: string[]) {
|
||||
if (!syncInfo.masterKeys.length) return false;
|
||||
|
||||
@@ -280,6 +311,10 @@ export async function resetMasterPassword(encryptionService: EncryptionService,
|
||||
}
|
||||
|
||||
Setting.setValue('encryption.masterPassword', newPassword);
|
||||
|
||||
const masterKey = await encryptionService.generateMasterKey(newPassword);
|
||||
await MasterKey.save(masterKey);
|
||||
await loadMasterKeysFromSettings(encryptionService);
|
||||
}
|
||||
|
||||
export enum MasterPasswordStatus {
|
||||
|
@@ -1,20 +1,52 @@
|
||||
import Logger from '../../Logger';
|
||||
import Setting from '../../models/Setting';
|
||||
|
||||
const logger = Logger.create('mergeGlobalAndLocalSettings');
|
||||
|
||||
export default (rootSettings: Record<string, any>, subProfileSettings: Record<string, any>) => {
|
||||
const output: Record<string, any> = { ...subProfileSettings };
|
||||
|
||||
for (const k of Object.keys(output)) {
|
||||
const md = Setting.settingMetadata(k);
|
||||
if (md.isGlobal) {
|
||||
delete output[k];
|
||||
if (k in rootSettings) output[k] = rootSettings[k];
|
||||
try {
|
||||
const md = Setting.settingMetadata(k);
|
||||
if (md.isGlobal) {
|
||||
delete output[k];
|
||||
if (k in rootSettings) output[k] = rootSettings[k];
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'unknown_key') {
|
||||
// The root settings may contain plugin parameters, but the
|
||||
// sub-profile won't necessarily have these plugins. In that
|
||||
// case, the app will throw an error, but we can ignore it since
|
||||
// we don't need this particular setting.
|
||||
// https://github.com/laurent22/joplin/issues/8143
|
||||
logger.info(`Ignoring unknown key in root settings: ${k}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const k of Object.keys(rootSettings)) {
|
||||
const md = Setting.settingMetadata(k);
|
||||
if (md.isGlobal) {
|
||||
output[k] = rootSettings[k];
|
||||
// We only copy built-in key and not, for example, plugin keys, because
|
||||
// those are plugin-specific
|
||||
if (!Setting.isBuiltinKey(k)) {
|
||||
logger.info(`Skipping non-built-in key: ${k}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const md = Setting.settingMetadata(k);
|
||||
if (md.isGlobal) {
|
||||
output[k] = rootSettings[k];
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'unknown_key') {
|
||||
// The root settings may contain plugin parameters, but the
|
||||
// sub-profile won't necessarily have these plugins. In that
|
||||
// case, the app will throw an error, but we can ignore it since
|
||||
// we don't need this particular setting.
|
||||
// https://github.com/laurent22/joplin/issues/8143
|
||||
logger.info(`Ignoring unknown key in root settings: ${k}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -159,7 +159,7 @@ describe('synchronizer/ItemUploader', () => {
|
||||
await itemUploader.preUploadItems(notes);
|
||||
|
||||
await expectNotThrow(async () => itemUploader.serializeAndUploadItem(Note, BaseItem.systemPath(notes[0]), notes[0]));
|
||||
await expectThrow(async () => itemUploader.serializeAndUploadItem(Note, BaseItem.systemPath(notes[1]), notes[1]));
|
||||
await expectThrow(async () => itemUploader.serializeAndUploadItem(Note, BaseItem.systemPath(notes[1]), notes[1]), null);
|
||||
await expectNotThrow(async () => itemUploader.serializeAndUploadItem(Note, BaseItem.systemPath(notes[2]), notes[2]));
|
||||
}));
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { ModelType } from '../../BaseModel';
|
||||
import { FileApi, MultiPutItem } from '../../file-api';
|
||||
import JoplinError from '../../JoplinError';
|
||||
import Logger from '../../Logger';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
import { BaseItemEntity } from '../database/types';
|
||||
@@ -45,7 +46,8 @@ export default class ItemUploader {
|
||||
// the regular upload.
|
||||
logger.warn(`Pre-uploaded item updated_time has changed. It is going to be re-uploaded again: ${path} (From ${this.preUploadedItemUpdatedTimes_[path]} to ${local.updated_time})`);
|
||||
} else {
|
||||
if (preUploadItem.error) throw new Error(preUploadItem.error.message ? preUploadItem.error.message : 'Unknown pre-upload error');
|
||||
const error = preUploadItem.error;
|
||||
if (error) throw new JoplinError(error.message ? error.message : 'Unknown pre-upload error', error.code);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import { syncTargetName, afterAllCleanUp, synchronizerStart, setupDatabaseAndSyn
|
||||
import Folder from '../../models/Folder';
|
||||
import Note from '../../models/Note';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
const WelcomeUtils = require('../../WelcomeUtils');
|
||||
import WelcomeUtils from '../../WelcomeUtils';
|
||||
|
||||
describe('Synchronizer.basics', () => {
|
||||
|
||||
@@ -332,12 +332,12 @@ describe('Synchronizer.basics', () => {
|
||||
it('should create a new Welcome notebook on each client', (async () => {
|
||||
// Create the Welcome items on two separate clients
|
||||
|
||||
await WelcomeUtils.createWelcomeItems();
|
||||
await WelcomeUtils.createWelcomeItems('en_GB');
|
||||
await synchronizerStart();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await WelcomeUtils.createWelcomeItems();
|
||||
await WelcomeUtils.createWelcomeItems('en_GB');
|
||||
const beforeFolderCount = (await Folder.all()).length;
|
||||
const beforeNoteCount = (await Note.all()).length;
|
||||
expect(beforeFolderCount === 1).toBe(true);
|
||||
|
@@ -120,4 +120,38 @@ describe('syncInfoUtils', () => {
|
||||
expect(mergeSyncInfos(syncInfo1, syncInfo2).activeMasterKeyId).toBe('2');
|
||||
});
|
||||
|
||||
it('should merge sync target info, but should not make a disabled key the active one', async () => {
|
||||
const syncInfo1 = new SyncInfo();
|
||||
syncInfo1.masterKeys = [{
|
||||
id: '1',
|
||||
content: 'content1',
|
||||
hasBeenUsed: true,
|
||||
enabled: 0,
|
||||
}];
|
||||
syncInfo1.activeMasterKeyId = '1';
|
||||
|
||||
await msleep(1);
|
||||
|
||||
const syncInfo2 = new SyncInfo();
|
||||
syncInfo2.masterKeys = [{
|
||||
id: '2',
|
||||
content: 'content2',
|
||||
enabled: 1,
|
||||
hasBeenUsed: false,
|
||||
}];
|
||||
syncInfo2.activeMasterKeyId = '2';
|
||||
|
||||
// Normally, if one master key has been used (1) and the other not (2),
|
||||
// it should select the one that's been used regardless of timestamps.
|
||||
// **However**, if the key 1 has been disabled by user, it should
|
||||
// **not** be picked as the active one. Instead it should use key 2,
|
||||
// because it's still enabled.
|
||||
expect(mergeSyncInfos(syncInfo1, syncInfo2).activeMasterKeyId).toBe('2');
|
||||
|
||||
// If both key are disabled, we go back to the original logic, where we
|
||||
// select the key that's been used.
|
||||
syncInfo2.masterKeys[0].enabled = 0;
|
||||
expect(mergeSyncInfos(syncInfo1, syncInfo2).activeMasterKeyId).toBe('1');
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -115,13 +115,20 @@ export function localSyncInfoFromState(state: State): SyncInfo {
|
||||
// has already been used to encrypt data. In this case, at the moment we compare
|
||||
// local and remote sync info (before synchronising the data), key1.hasBeenUsed
|
||||
// is true, but key2.hasBeenUsed is false.
|
||||
//
|
||||
// 2023-05-30: Additionally, if one key is enabled and the other is not, we
|
||||
// always pick the enabled one regardless of usage.
|
||||
const mergeActiveMasterKeys = (s1: SyncInfo, s2: SyncInfo, output: SyncInfo) => {
|
||||
const activeMasterKey1 = getActiveMasterKey(s1);
|
||||
const activeMasterKey2 = getActiveMasterKey(s2);
|
||||
let doDefaultAction = false;
|
||||
|
||||
if (activeMasterKey1 && activeMasterKey2) {
|
||||
if (activeMasterKey1.hasBeenUsed && !activeMasterKey2.hasBeenUsed) {
|
||||
if (masterKeyEnabled(activeMasterKey1) && !masterKeyEnabled(activeMasterKey2)) {
|
||||
output.setWithTimestamp(s1, 'activeMasterKeyId');
|
||||
} else if (!masterKeyEnabled(activeMasterKey1) && masterKeyEnabled(activeMasterKey2)) {
|
||||
output.setWithTimestamp(s2, 'activeMasterKeyId');
|
||||
} else if (activeMasterKey1.hasBeenUsed && !activeMasterKey2.hasBeenUsed) {
|
||||
output.setWithTimestamp(s1, 'activeMasterKeyId');
|
||||
} else if (!activeMasterKey1.hasBeenUsed && activeMasterKey2.hasBeenUsed) {
|
||||
output.setWithTimestamp(s2, 'activeMasterKeyId');
|
||||
|
@@ -139,7 +139,7 @@ function shimInit(options = null) {
|
||||
};
|
||||
|
||||
shim.detectAndSetLocale = function(Setting) {
|
||||
let locale = process.env.LANG;
|
||||
let locale = shim.isElectron() ? shim.electronBridge().getLocale() : process.env.LANG;
|
||||
if (!locale) locale = defaultLocale();
|
||||
locale = locale.split('.');
|
||||
locale = locale[0];
|
||||
|
@@ -24,9 +24,9 @@ describe('checkProviderIsSupported', () => {
|
||||
|
||||
expect(() => checkProviderIsSupported('https://api.pcloud.com')).toThrowError('The WebDAV implementation of pcloud is incompatible with Joplin, and as such is no longer supported. Please use a different sync method.');
|
||||
|
||||
expect(() => checkProviderIsSupported('https://api-pcloud-test.com')).toThrowError('The WebDAV implementation of pcloud is incompatible with Joplin, and as such is no longer supported. Please use a different sync method.');
|
||||
// expect(() => checkProviderIsSupported('https://api-pcloud-test.com')).toThrowError('The WebDAV implementation of pcloud is incompatible with Joplin, and as such is no longer supported. Please use a different sync method.');
|
||||
});
|
||||
expect(() => checkProviderIsSupported('?param=pcloud')).toThrowError('The WebDAV implementation of pcloud is incompatible with Joplin, and as such is no longer supported. Please use a different sync method.');
|
||||
// expect(() => checkProviderIsSupported('?param=pcloud')).toThrowError('The WebDAV implementation of pcloud is incompatible with Joplin, and as such is no longer supported. Please use a different sync method.');
|
||||
});
|
||||
|
||||
describe('when an unsupported provider is already configured', () => {
|
||||
|
@@ -1,18 +1,30 @@
|
||||
import { _ } from '../locale';
|
||||
import Setting from '../models/Setting';
|
||||
import { URL } from 'url';
|
||||
|
||||
const pathContainsUnsupportedProvider = (path: string, unsupportedProviders: string[]) => {
|
||||
try {
|
||||
const url = new URL(path.toLowerCase());
|
||||
const splitted = url.host.split('.');
|
||||
|
||||
for (const s of splitted) {
|
||||
if (unsupportedProviders.includes(s)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
// The URL is probably invalid, but it's not here that we should handle
|
||||
// this.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const checkProviderIsSupported = (path: string): void => {
|
||||
if (Setting.value('sync.allowUnsupportedProviders') === 1) return;
|
||||
|
||||
const unsupportedProviders = ['pcloud', 'jianguoyun'];
|
||||
for (const p of unsupportedProviders) {
|
||||
// For a provider named abc, this regex will match the provider's name if enclosed by either '/', '.', '-', '=' or end of string.
|
||||
// E.g: https://abc.com, https://api.abc.com, https://api-abc-test.com, https://api/test?param=abc
|
||||
//
|
||||
// It will not match a provider which name happens to contain an unsupported provider (i.e a substring).
|
||||
// E.g: https://fooabc.com
|
||||
const pattern = `(?<=[-/.=])${p}(?=[-/.=]|$)`;
|
||||
if (path.search(new RegExp(pattern)) !== -1) {
|
||||
if (pathContainsUnsupportedProvider(path, unsupportedProviders)) {
|
||||
throw new Error(_('The WebDAV implementation of %s is incompatible with Joplin, and as such is no longer supported. Please use a different sync method.', p));
|
||||
}
|
||||
}
|
||||
|
@@ -44,6 +44,6 @@
|
||||
"pdfjs-dist": "2.16.105",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"styled-components": "5.3.9"
|
||||
"styled-components": "5.3.10"
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"gh-release-assets": "2.0.1",
|
||||
"node-fetch": "2.6.7",
|
||||
"source-map-support": "0.5.21",
|
||||
"yargs": "17.7.1"
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "9.0.13",
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.emekalites.react.alarm.notification">
|
||||
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
@@ -369,7 +369,7 @@ public class AlarmModel implements Serializable {
|
||||
alarm.setActive(1);
|
||||
alarm.setAutoCancel(bundle.getBoolean("auto_cancel", true));
|
||||
alarm.setChannel(bundle.getString("channel", "my_channel_id"));
|
||||
alarm.setColor(bundle.getString("color", "red"));
|
||||
alarm.setColor(bundle.getString("color", ""));
|
||||
|
||||
Bundle data = bundle.getBundle("data");
|
||||
alarm.setData(data);
|
||||
@@ -380,7 +380,7 @@ public class AlarmModel implements Serializable {
|
||||
alarm.setMessage(bundle.getString("message", "My Notification Message"));
|
||||
alarm.setPlaySound(bundle.getBoolean("play_sound", true));
|
||||
alarm.setScheduleType(bundle.getString("schedule_type", "once"));
|
||||
alarm.setSmallIcon(bundle.getString("small_icon", "ic_launcher"));
|
||||
alarm.setSmallIcon(bundle.getString("small_icon", "ic_notification"));
|
||||
alarm.setSnoozeInterval((int) bundle.getDouble("snooze_interval", 1.0));
|
||||
alarm.setSoundName(bundle.getString("sound_name", null));
|
||||
alarm.setSoundNames(bundle.getString("sound_names", null));
|
||||
|
@@ -9,6 +9,7 @@ import android.util.Log;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AlarmReceiver extends BroadcastReceiver {
|
||||
|
||||
@@ -27,7 +28,7 @@ public class AlarmReceiver extends BroadcastReceiver {
|
||||
alarmUtil.sendNotification(alarm);
|
||||
alarmUtil.setBootReceiver();
|
||||
|
||||
ArrayList<AlarmModel> alarms = alarmDB.getAlarmList(1);
|
||||
List<AlarmModel> alarms = alarmDB.getAlarmList(1);
|
||||
Log.d(Constants.TAG, "alarm start: " + alarm.toString() + ", alarms left: " + alarms.size());
|
||||
} catch (Exception e) {
|
||||
Log.e(Constants.TAG, "Failed to add alarm", e);
|
||||
|