1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-02-10 08:14:27 +02:00

Compare commits

...

118 Commits
v3.6.1 ... oidc

Author SHA1 Message Date
Laurent Cozic
6c383996fa update 2026-02-08 19:57:45 +00:00
Laurent Cozic
01b15d58dd update 2026-02-08 18:15:04 +00:00
Laurent Cozic
fa07eb3db0 update 2026-02-08 16:06:40 +00:00
Laurent Cozic
f439835281 update 2026-02-08 15:46:55 +00:00
Laurent Cozic
740c87a817 update 2026-02-08 15:43:42 +00:00
Laurent Cozic
8b3835eb04 update 2026-02-08 15:32:37 +00:00
Laurent Cozic
a5318099c5 update 2026-02-08 15:22:06 +00:00
Laurent Cozic
b39628a963 update 2026-02-08 15:14:14 +00:00
Laurent Cozic
0386028803 update 2026-02-08 15:04:42 +00:00
Laurent Cozic
ed242f736c update 2026-02-07 13:41:07 +00:00
Laurent Cozic
8cd39e3b40 update 2026-02-07 12:49:16 +00:00
Laurent Cozic
8d4632d9dd update 2026-02-07 12:40:56 +00:00
Laurent Cozic
97a18f722d Chore: Convert WebDavAPI.js to TypeScript (#14291) 2026-02-06 13:28:35 +00:00
luzpaz
554e6efaab Doc: fixed a few dev-facing typos (#14279) 2026-02-05 00:11:51 +00:00
Joplin Bot
cd7af20bc1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-02-04 19:02:12 +00:00
renovate[bot]
4346616cae Update dependency sharp to v0.34.5 (#14264)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 18:02:14 +00:00
Laurent Cozic
ef646adafa Doc: Added mobile performance analysis report 2026-02-04 15:21:52 +00:00
Laurent Cozic
4ce47807b1 iOS 13.6.1 2026-02-04 15:13:08 +00:00
Laurent Cozic
9b0bc4d600 Chore: Limit build cleaning function to just Android 2026-02-04 15:13:08 +00:00
renovate[bot]
91b8e4d34d Update dependency style-to-js to v1.1.19 (#14244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 14:51:04 +00:00
renovate[bot]
9e9bd662dc Update dependency nan to v2.23.1 (#14263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 14:48:21 +00:00
Laurent Cozic
defbbd5d72 Android 3.6.12 2026-02-04 14:45:02 +00:00
Laurent Cozic
b2c9dd40dc Chore: Fixed Android release script 2026-02-04 14:43:56 +00:00
Laurent Cozic
a9049111e4 Chore: Add a marker to tell when the application is ready on mobile 2026-02-04 12:40:51 +00:00
renovate[bot]
55f642c625 Update dependency @types/serviceworker to v0.0.165 (#14250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 11:29:26 +00:00
Henry Heino
dee9ec3495 Chore: Testing: Give the plugin panel test more time to pass (#14199) 2026-02-04 10:10:01 +00:00
Henry Heino
97bf020150 Chore: Mobile: Migrate folder screen to TypeScript (#14200) 2026-02-04 10:09:35 +00:00
Henry Heino
2cb2680a5a All: Sync: Make resource processing in read-only shares more reliable (#14204) 2026-02-04 10:08:29 +00:00
Henry Heino
242c6ec3b8 Chore: Sync fuzzer: Don't attempt to publish read-only notes (#14205) 2026-02-04 10:08:14 +00:00
Henry Heino
ed0b1ae390 Desktop: Fixes #14216: Fix undo/redo menu items in the Rich Text and Markdown editors (#14218) 2026-02-04 10:07:34 +00:00
Henry Heino
de29e4ff92 Docs: Update SECURITY.md to suggest using GitHub private vulnerability reporting (#14221) 2026-02-04 10:03:36 +00:00
Henry Heino
9d96e31b83 Desktop: Fixes #14210: OneNote importer: Skip importing ink when ID lookup fails (#14230) 2026-02-04 10:03:28 +00:00
Henry Heino
aad460e9a1 Chore: Fixes #14222: Retry failing editor test in CI (#14231) 2026-02-04 10:03:11 +00:00
Henry Heino
00248a9177 Mobile: Upgrade to React Native 0.81 (#14232) 2026-02-04 10:03:02 +00:00
Henry Heino
af2926b634 Desktop: OneNote import: Fix onepkg import stops after the first section fails to import (#14246)
Co-authored-by: Himanshu <h-jangra@users.noreply.github.com>
2026-02-04 09:59:17 +00:00
Henry Heino
916ed9bbfb Chore: Desktop: Fix warning logged when opening the note viewer (#14247) 2026-02-04 09:59:01 +00:00
Laurent Cozic
b32015864e All: Add support for FrontMatter block rendering in notes (#14256) 2026-02-04 09:58:12 +00:00
Henry Heino
8939ef1c19 Desktop,Mobile: Resolves #9745: Markdown: Allow specifying the start/end of audio, videos, and PDFs (#14257) 2026-02-04 09:56:05 +00:00
Henry Heino
31bba39ae9 Chore: Fix HTML to Markdown conversion fails for certain rendered code block HTML (#14259) 2026-02-04 09:55:50 +00:00
Henry Heino
9dc49f0c24 Chore: Sync fuzzer: Fix "moveFolderToToplevel" command (#14261) 2026-02-04 09:55:03 +00:00
Henry Heino
02dfef11aa Desktop: Fixes #13540: Improve context menu handling in secondary windows (#14262) 2026-02-04 09:53:09 +00:00
renovate[bot]
c278b45c78 Update dependency nodejs to v24.8.0 (#14229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 23:54:22 +00:00
renovate[bot]
0dafd21db0 Update dependency electron-updater to v6.6.8 (#14239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 23:53:58 +00:00
Sebastian
490d35919c All: Translation: Update de_DE.po (#14242) 2026-02-01 06:24:59 -05:00
Nick
4c1ca5480d All: Translation: Update sv.po (#14241) 2026-02-01 06:20:55 -05:00
Joplin Bot
d414c6354a Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-02-01 02:36:23 +00:00
rnbastos
7651d8e3c4 All: Translation: Update pt_BR.po (#14238) 2026-01-31 17:23:25 -05:00
renovate[bot]
d5c72c13cb Update dependency @types/serviceworker to v0.0.164 (#14237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 09:49:32 +00:00
renovate[bot]
4377634e7b Update dependency esbuild to v0.25.12 (#14236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 04:59:05 +00:00
renovate[bot]
69ec5c7f86 Update dependency @types/serviceworker to v0.0.163 (#14234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 17:50:22 +00:00
renovate[bot]
f02b0f48d8 Update dependency react-refresh to v0.18.0 (#14233)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 14:33:32 +01:00
renovate[bot]
4d77c1385f Update dependency sass to v1.93.3 (#14228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:51:56 +00:00
renovate[bot]
c83f9ddeac Update dependency dayjs to v1.11.19 (#14227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 14:49:50 +00:00
renovate[bot]
1b9c11df7b Update dependency @types/serviceworker to v0.0.162 (#14225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 09:28:36 +00:00
Nick
333a8723e8 All: Translation: Update sv.po (#14220) 2026-01-28 18:14:28 -05:00
Laurent Cozic
e030c8271d Chore: Try to fix app-desktop tests on local 2026-01-28 12:55:08 +00:00
renovate[bot]
560bc31445 Update dependency gettext-extractor to v4.0.1 (#14217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 01:45:26 +00:00
renovate[bot]
c71aeb74b2 Update dependency gettext-extractor to v4 (#14213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 23:36:41 +00:00
renovate[bot]
ffaf2acb66 Update dependency @rollup/plugin-replace to v6.0.3 (#14212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 18:16:02 +00:00
renovate[bot]
f442f1fb23 Update dependency @types/serviceworker to v0.0.161 (#14206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 11:12:24 +00:00
renovate[bot]
81a1451820 Update dependency react-native-safe-area-context to v5.6.2 (#14202)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 02:16:37 +00:00
renovate[bot]
b3a3d71461 Update dependency @react-native-community/datetimepicker to v8.4.7 (#14191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 22:47:00 +00:00
bwat47
1db38c3232 Desktop, Mobile: Fixes #13933: Markdown editor: Scroll jumps in notes with many inline images (#13955) 2026-01-26 15:21:08 +00:00
Fardin96
42e645eb70 Mobile: Fixes #13243: Align tag search-input-clear behavior across input methods (#14042) 2026-01-26 15:12:06 +00:00
mrjo118
3860f44d06 Mobile: Fixes #14153: Prevent the back button sometimes disappearing when switching between editors (#14164) 2026-01-26 15:09:37 +00:00
Henry Heino
4df0f8668d Desktop,Mobile: Resolves #14158: Markdown Editor: Make code block highlighting closer to the viewer (#14168) 2026-01-26 15:06:37 +00:00
Henry Heino
306d0fddd8 Desktop: OneNote import: Import invalid attachments as empty attachments (#14177) 2026-01-26 15:06:24 +00:00
Henry Heino
56d12b28f2 All: Unlinked resource deletion: Fix resources attached only via reference links are auto-deleted (#14178) 2026-01-26 15:06:15 +00:00
Henry Heino
6c5ea4872a Desktop,Mobile: Markdown editor: Fix error logged in "hide markdown" mode for certain markup (#14179) 2026-01-26 15:06:06 +00:00
Henry Heino
9856e8ae93 Chore: Sync fuzzer: Test adding, removing resources from notes (#14185) 2026-01-26 15:05:50 +00:00
Henry Heino
5712da4c0f Desktop,Mobile: Fixes #14009: Markdown editor: Upgrade most CodeMirror dependencies (#14186) 2026-01-26 15:04:35 +00:00
Henry Heino
4f7ee56444 Desktop: Fixes #13793: Make conflicts caused by resource duplication less likely (#14188) 2026-01-26 15:04:26 +00:00
Laurent Cozic
8e2b6ca296 Chore: Improved performance log consistency, and log to console on Android production too 2026-01-26 14:47:19 +00:00
Laurent Cozic
0172bb0ad8 Chore: Fix error in release-android script when the main apk is not built 2026-01-26 14:46:33 +00:00
Laurent Cozic
1d38e443ba Chore: Do not create GitHub release when there is no APK to publish 2026-01-26 13:58:57 +00:00
Laurent Cozic
5ad19b7261 Chore: Make Android app profileable 2026-01-26 13:46:27 +00:00
Arda Kılıçdağı
70293478a2 All: Translation: Update tr_TR.po (#14193) 2026-01-25 18:14:39 -05:00
custiq
3aaa20254f All: Translation: Update fi_FI.po (#14189) 2026-01-24 23:46:32 -05:00
renovate[bot]
42c248f7ca Update dependency @types/serviceworker to v0.0.160 (#14190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 09:11:54 +00:00
Joplin Bot
ac1e94a8df Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-23 06:47:06 +00:00
renovate[bot]
daff4496cf Update dependency turndown to v7.2.2 (#14181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 02:15:02 +00:00
Joplin Bot
1e00078228 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-23 01:56:25 +00:00
renovate[bot]
03a1de9370 Update dependency @rollup/plugin-commonjs to v28.0.9 (#14175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 17:46:44 +00:00
renovate[bot]
55ef256c65 Update dependency rate-limiter-flexible to v7.4.0 (#14174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 17:46:38 +00:00
renovate[bot]
6d115db16f Update dependency @types/yargs to v17.0.34 (#14173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 16:25:50 +00:00
renovate[bot]
5853031fde Update dependency @types/serviceworker to v0.0.159 (#14172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 09:05:01 +00:00
renovate[bot]
47db2ae962 Update dependency @types/nodemailer to v6.4.21 (#14171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 09:02:54 +00:00
Laurent Cozic
b960a2a8b0 Doc: Updated JSB contact link 2026-01-21 09:27:55 +00:00
renovate[bot]
fcaa7d2a98 Update dependency lint-staged to v16.2.6 (#14165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 18:18:59 +00:00
renovate[bot]
99284ae135 Update dependency lint-staged to v16.2.0 (#14162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 13:20:03 +00:00
Joplin Bot
66ae58c81b Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-19 18:43:09 +00:00
Laurent Cozic
484d6a866d Doc: Remove "(Pre-release)" marker from Android changelog since all versions are pre-releases 2026-01-19 18:03:05 +00:00
Laurent Cozic
b45fd09e38 Merge branch 'release-3.5' into dev 2026-01-19 16:44:41 +00:00
Laurent Cozic
903a369c13 Android 3.5.9 2026-01-19 16:43:41 +00:00
Laurent Cozic
1fb79315e4 Chore: lock files 2026-01-19 16:13:04 +00:00
Henry Heino
4dc021b523 Android: Remove unnecessary READ_PHONE_STATE permission (#14157) 2026-01-19 16:04:56 +00:00
Joplin Bot
bbb4b46dd9 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-19 02:01:31 +00:00
renovate[bot]
063dc46f50 Update dependency dotenv to v17.2.3 (#14155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 00:02:36 +00:00
renovate[bot]
aa400b52be Update dependency short-uuid to v5 (#14156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 00:02:26 +00:00
renovate[bot]
be7de2f08a Update dependency dotenv to v17.2.2 (#14145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 22:01:23 +00:00
renovate[bot]
f8a129e4dc Update dependency npm-package-json-lint to v9 (#14146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 22:01:11 +00:00
Laurent Cozic
c5d9646908 Desktop release v3.6.2 2026-01-18 11:33:16 +00:00
Henry Heino
876ec80911 Desktop: Fixes #14084: .onepkg import: Fix Unicode issues, support Linux and MacOS (#14094)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:31:48 +00:00
mrjo118
4051f88ce7 Chore: Fix intermittent Synchronizer.revisions test failure (#14096)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:31:42 +00:00
Laurent Cozic
f194c111e4 All: Fixes #14144: Application crashes when profile database has been analyzed 2026-01-18 11:30:05 +00:00
Henry Heino
e386246bc9 Chore: Sync fuzzer: Improve error logging (#14108)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:29:32 +00:00
Henry Heino
292b269f1d Desktop: Resolves #14086: Accessibility: Include accessibility information in exported PDFs (#14111)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:29:25 +00:00
renovate[bot]
b2fc43da2b Update dependency short-uuid to v4.2.2 (#14114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:29:17 +00:00
Henry Heino
4a23a1ed3e Desktop: Fixes #14092: Built-in plugins: Upgrade Freehand Drawing to v4.3.0 (#14123)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:29:07 +00:00
Henry Heino
c8878a18bf Desktop, Mobile: Editor: Inline rendering: Render inline HTML (colorized text, superscript, subscript, strikethrough) (#14133) 2026-01-18 11:28:15 +00:00
Henry Heino
340fba7af5 Server: Fixes #14107: Fix warning when unsharing folder (#14134) 2026-01-18 11:25:52 +00:00
Henry Heino
271c4f4a2a Server: Fixes #14131: Allow changing the password for the admin account when SAML is enabled (#14135) 2026-01-18 11:25:38 +00:00
Henry Heino
c9dba20f59 Chore: Sync fuzzer: Allow specifying a set of initial actions (#14136)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:25:07 +00:00
renovate[bot]
b474cc206a Update dependency dotenv to v17 (#14138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 11:24:46 +00:00
Milo Ivir
9d4df8cc6e All: Translation: Update hr_HR.po (#14140) 2026-01-17 20:57:39 -05:00
Joplin Bot
a4ddfe1f58 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-17 18:38:35 +00:00
renovate[bot]
7d15215e66 Update dependency react-native-device-info to v14.1.1 (#14132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 14:23:02 +00:00
Laurent Cozic
449555c8e9 Desktop release v3.5.12 2026-01-17 11:21:02 +00:00
820 changed files with 11240 additions and 6379 deletions

View File

@@ -92,6 +92,7 @@ readme/
packages/react-native-vosk/lib/
packages/lib/countable/Countable.js
packages/onenote-converter/renderer/pkg/*
packages/whisper-voice-typing/lib/
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
packages/app-cli/app/LinkSelector.js
@@ -458,6 +459,7 @@ packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
packages/app-desktop/gui/WebDavOidcLoginScreen.js
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js
@@ -468,6 +470,8 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalRedo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalUndo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
@@ -511,6 +515,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderR
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/utils/canUseNativeUndo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/showFolderPicker.js
@@ -691,6 +696,7 @@ packages/app-mobile/components/FeedbackBanner.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/IconButton.js
packages/app-mobile/components/KeyboardAvoidingView.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/ModalDialog.js
packages/app-mobile/components/NestableFlatList.js
@@ -869,8 +875,10 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/dropbox-login.js
packages/app-mobile/components/screens/encryption-config.test.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/folder.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/screens/tags.js
packages/app-mobile/components/screens/webdav-oidc-login.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/testing/TestProviderStack.js
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
@@ -969,6 +977,7 @@ packages/app-mobile/utils/hooks/useSafeAreaPadding.js
packages/app-mobile/utils/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
packages/app-mobile/utils/initReact.js
packages/app-mobile/utils/initializeCommandService.js
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
@@ -1045,6 +1054,8 @@ packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
packages/editor/CodeMirror/extensions/links/utils/openLink.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.js
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.js
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
@@ -1061,6 +1072,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
packages/editor/CodeMirror/extensions/rendering/types.js
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
@@ -1101,6 +1114,7 @@ packages/editor/CodeMirror/utils/getSearchState.js
packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/htmlNodeInfo.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
@@ -1230,6 +1244,7 @@ packages/lib/JoplinError.js
packages/lib/JoplinServerApi.js
packages/lib/ObjectUtils.test.js
packages/lib/ObjectUtils.js
packages/lib/OidcApi.js
packages/lib/PerformanceLogger.test.js
packages/lib/PerformanceLogger.js
packages/lib/PoorManIntervals.js
@@ -1239,13 +1254,17 @@ packages/lib/SyncTargetFilesystem.js
packages/lib/SyncTargetJoplinCloud.js
packages/lib/SyncTargetJoplinServer.js
packages/lib/SyncTargetJoplinServerSAML.js
packages/lib/SyncTargetNextcloud.js
packages/lib/SyncTargetNone.js
packages/lib/SyncTargetOneDrive.js
packages/lib/SyncTargetRegistry.js
packages/lib/SyncTargetWebDAV.js
packages/lib/Synchronizer.js
packages/lib/TaskQueue.js
packages/lib/WebDavApi.js
packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/base-oauth-node-utils.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
@@ -1395,6 +1414,8 @@ packages/lib/models/utils/userData.test.js
packages/lib/models/utils/userData.js
packages/lib/net-utils.js
packages/lib/ntp.js
packages/lib/oidc-api-node-utils.js
packages/lib/onedrive-api-node-utils.js
packages/lib/onedrive-api.test.js
packages/lib/onedrive-api.js
packages/lib/path-utils.js
@@ -1704,6 +1725,7 @@ packages/lib/testing/share/mockShareService.js
packages/lib/testing/syncTargetUtils.js
packages/lib/testing/test-utils-synchronizer.js
packages/lib/testing/test-utils.js
packages/lib/testing/waitFor.js
packages/lib/theme.js
packages/lib/themes/aritimDark.js
packages/lib/themes/dark.js
@@ -1808,6 +1830,8 @@ packages/renderer/MdToHtml/rules/code_inline.js
packages/renderer/MdToHtml/rules/externalEmbed.js
packages/renderer/MdToHtml/rules/fence.js
packages/renderer/MdToHtml/rules/fountain.js
packages/renderer/MdToHtml/rules/frontmatter.test.js
packages/renderer/MdToHtml/rules/frontmatter.js
packages/renderer/MdToHtml/rules/highlight_keywords.js
packages/renderer/MdToHtml/rules/html_image.js
packages/renderer/MdToHtml/rules/image.js
@@ -1841,22 +1865,29 @@ packages/tools/checkIgnoredFiles.js
packages/tools/checkLibPaths.test.js
packages/tools/checkLibPaths.js
packages/tools/convertThemesToCss.js
packages/tools/fuzzer/ActionRunner.js
packages/tools/fuzzer/ActionTracker.js
packages/tools/fuzzer/Client.js
packages/tools/fuzzer/ClientPool.js
packages/tools/fuzzer/Server.js
packages/tools/fuzzer/constants.js
packages/tools/fuzzer/doRandomAction.js
packages/tools/fuzzer/model/FolderRecord.js
packages/tools/fuzzer/model/ResourceRecord.js
packages/tools/fuzzer/sync-fuzzer.js
packages/tools/fuzzer/types.js
packages/tools/fuzzer/utils/ProgressBar.js
packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
packages/tools/fuzzer/utils/diffSortedStringArrays.js
packages/tools/fuzzer/utils/extractResourceIds.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js
packages/tools/fuzzer/utils/hangingIndent.js
packages/tools/fuzzer/utils/logDiffDebug.js
packages/tools/fuzzer/utils/openDebugSession.js
packages/tools/fuzzer/utils/randomId.test.js
packages/tools/fuzzer/utils/randomId.js
packages/tools/fuzzer/utils/randomString.js
packages/tools/fuzzer/utils/retryWithCount.js
packages/tools/generate-database-types.js
@@ -1928,4 +1959,6 @@ packages/tools/website/utils/pressCarousel.js
packages/tools/website/utils/processTranslations.js
packages/tools/website/utils/render.js
packages/tools/website/utils/types.js
packages/whisper-voice-typing/src/index.js
packages/whisper-voice-typing/src/specs/Whisper.nitro.js
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

@@ -314,7 +314,7 @@ module.exports = {
selector: 'interface',
format: null,
'filter': {
'regex': '^(RSA|RSAKeyPair|iOS.*)$',
'regex': '^(RSA|RSAKeyPair|iOS.*|OAuth.*)$',
'match': true,
},
},

34
.gitignore vendored
View File

@@ -432,6 +432,7 @@ packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
packages/app-desktop/gui/WebDavOidcLoginScreen.js
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js
@@ -442,6 +443,8 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalRedo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalUndo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
@@ -485,6 +488,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderR
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/utils/canUseNativeUndo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/showFolderPicker.js
@@ -665,6 +669,7 @@ packages/app-mobile/components/FeedbackBanner.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/IconButton.js
packages/app-mobile/components/KeyboardAvoidingView.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/ModalDialog.js
packages/app-mobile/components/NestableFlatList.js
@@ -843,8 +848,10 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/dropbox-login.js
packages/app-mobile/components/screens/encryption-config.test.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/folder.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/screens/tags.js
packages/app-mobile/components/screens/webdav-oidc-login.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/testing/TestProviderStack.js
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
@@ -943,6 +950,7 @@ packages/app-mobile/utils/hooks/useSafeAreaPadding.js
packages/app-mobile/utils/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
packages/app-mobile/utils/initReact.js
packages/app-mobile/utils/initializeCommandService.js
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
@@ -1019,6 +1027,8 @@ packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
packages/editor/CodeMirror/extensions/links/utils/openLink.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.js
packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.js
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
@@ -1035,6 +1045,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
packages/editor/CodeMirror/extensions/rendering/types.js
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
@@ -1075,6 +1087,7 @@ packages/editor/CodeMirror/utils/getSearchState.js
packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/htmlNodeInfo.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
@@ -1204,6 +1217,7 @@ packages/lib/JoplinError.js
packages/lib/JoplinServerApi.js
packages/lib/ObjectUtils.test.js
packages/lib/ObjectUtils.js
packages/lib/OidcApi.js
packages/lib/PerformanceLogger.test.js
packages/lib/PerformanceLogger.js
packages/lib/PoorManIntervals.js
@@ -1213,13 +1227,17 @@ packages/lib/SyncTargetFilesystem.js
packages/lib/SyncTargetJoplinCloud.js
packages/lib/SyncTargetJoplinServer.js
packages/lib/SyncTargetJoplinServerSAML.js
packages/lib/SyncTargetNextcloud.js
packages/lib/SyncTargetNone.js
packages/lib/SyncTargetOneDrive.js
packages/lib/SyncTargetRegistry.js
packages/lib/SyncTargetWebDAV.js
packages/lib/Synchronizer.js
packages/lib/TaskQueue.js
packages/lib/WebDavApi.js
packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/base-oauth-node-utils.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
@@ -1369,6 +1387,8 @@ packages/lib/models/utils/userData.test.js
packages/lib/models/utils/userData.js
packages/lib/net-utils.js
packages/lib/ntp.js
packages/lib/oidc-api-node-utils.js
packages/lib/onedrive-api-node-utils.js
packages/lib/onedrive-api.test.js
packages/lib/onedrive-api.js
packages/lib/path-utils.js
@@ -1678,6 +1698,7 @@ packages/lib/testing/share/mockShareService.js
packages/lib/testing/syncTargetUtils.js
packages/lib/testing/test-utils-synchronizer.js
packages/lib/testing/test-utils.js
packages/lib/testing/waitFor.js
packages/lib/theme.js
packages/lib/themes/aritimDark.js
packages/lib/themes/dark.js
@@ -1782,6 +1803,8 @@ packages/renderer/MdToHtml/rules/code_inline.js
packages/renderer/MdToHtml/rules/externalEmbed.js
packages/renderer/MdToHtml/rules/fence.js
packages/renderer/MdToHtml/rules/fountain.js
packages/renderer/MdToHtml/rules/frontmatter.test.js
packages/renderer/MdToHtml/rules/frontmatter.js
packages/renderer/MdToHtml/rules/highlight_keywords.js
packages/renderer/MdToHtml/rules/html_image.js
packages/renderer/MdToHtml/rules/image.js
@@ -1815,22 +1838,29 @@ packages/tools/checkIgnoredFiles.js
packages/tools/checkLibPaths.test.js
packages/tools/checkLibPaths.js
packages/tools/convertThemesToCss.js
packages/tools/fuzzer/ActionRunner.js
packages/tools/fuzzer/ActionTracker.js
packages/tools/fuzzer/Client.js
packages/tools/fuzzer/ClientPool.js
packages/tools/fuzzer/Server.js
packages/tools/fuzzer/constants.js
packages/tools/fuzzer/doRandomAction.js
packages/tools/fuzzer/model/FolderRecord.js
packages/tools/fuzzer/model/ResourceRecord.js
packages/tools/fuzzer/sync-fuzzer.js
packages/tools/fuzzer/types.js
packages/tools/fuzzer/utils/ProgressBar.js
packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
packages/tools/fuzzer/utils/diffSortedStringArrays.js
packages/tools/fuzzer/utils/extractResourceIds.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js
packages/tools/fuzzer/utils/hangingIndent.js
packages/tools/fuzzer/utils/logDiffDebug.js
packages/tools/fuzzer/utils/openDebugSession.js
packages/tools/fuzzer/utils/randomId.test.js
packages/tools/fuzzer/utils/randomId.js
packages/tools/fuzzer/utils/randomString.js
packages/tools/fuzzer/utils/retryWithCount.js
packages/tools/generate-database-types.js
@@ -1902,5 +1932,7 @@ packages/tools/website/utils/pressCarousel.js
packages/tools/website/utils/processTranslations.js
packages/tools/website/utils/render.js
packages/tools/website/utils/types.js
packages/whisper-voice-typing/src/index.js
packages/whisper-voice-typing/src/specs/Whisper.nitro.js
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

@@ -5,6 +5,7 @@
"exceptions": [
"@joplin/editor",
"@joplin/fork-htmlparser2",
"@joplin/whisper-voice-typing",
"@joplin/fork-sax",
"@joplin/fork-uslug",
"@joplin/htmlpack",

View File

@@ -0,0 +1,21 @@
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
# permission from being added.
# See:
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
diff --git a/android/build.gradle b/android/build.gradle
index a16b4ad6d1871cf5cf73ef7ebeaf8bd4d662b134..9871afb5fbf8e687370e08f54d884ecd7dde7e7c 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,6 +37,10 @@ android {
}
compileSdkVersion safeExtGet('compileSdkVersion', 31)
+
+ defaultConfig {
+ minSdkVersion safeExtGet('minSdkVersion', 24)
+ }
}
dependencies {

View File

@@ -0,0 +1,21 @@
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
# permission from being added.
# See:
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
diff --git a/android/build.gradle b/android/build.gradle
index d42bd23123644cc324051e9c7ec4635de286315a..640996df60fe7769f69b30b35f771eb9cf0b75d4 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,6 +37,10 @@ android {
}
compileSdkVersion safeExtGet('compileSdkVersion', 31)
+
+ defaultConfig {
+ minSdkVersion safeExtGet('minSdkVersion', 24)
+ }
}
dependencies {

View File

@@ -0,0 +1,21 @@
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
# permission from being added.
# See:
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
diff --git a/android/build.gradle b/android/build.gradle
index 170ec0ff9befe0f9155aaf5e1b84133cfd87be99..e6a0ab4a019ee67c5af7761ae8bb35f18b05c590 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,6 +37,10 @@ android {
}
compileSdkVersion safeExtGet('compileSdkVersion', 31)
+
+ defaultConfig {
+ minSdkVersion safeExtGet('minSdkVersion', 24)
+ }
}
dependencies {

View File

@@ -0,0 +1,21 @@
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
# permission from being added.
# See:
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
diff --git a/android/build.gradle b/android/build.gradle
index 3b22f9de66795ee01dbaa29655727ee7ddba3cc8..325daa88d33f066b3826e5031ce281793710af2d 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,6 +37,10 @@ android {
}
compileSdkVersion safeExtGet('compileSdkVersion', 31)
+
+ defaultConfig {
+ minSdkVersion safeExtGet('minSdkVersion', 24)
+ }
}
dependencies {

View File

@@ -6,7 +6,7 @@ Only the latest version is supported with security updates.
## Reporting a Vulnerability
Please [contact support](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/AdresseSupport.png) **with a proof of concept** that shows the security vulnerability. Please do not contact us without this proof of concept, as we cannot fix anything without this.
Please report vulnerabilities [through private vulnerability reporting](https://github.com/laurent22/joplin/security/advisories/new) **with a proof of concept** that shows the security vulnerability. Please do not contact us without this proof of concept, as we cannot fix anything without this.
For general opinions on what makes an app more or less secure, please use the forum.

View File

@@ -33,7 +33,7 @@
"/packages/app-desktop/build/",
"/packages/app-desktop/utils/checkForUpdatesUtilsTestData.ts",
"/packages/app-desktop/vendor/",
"/packages/app-mobile/android/vendor/",
"/packages/whisper-voice-typing/vendor/",
"/packages/app-mobile/ios/Pods/",
"/packages/app-mobile/lib/rnInjectedJs",
"/packages/app-mobile/pluginAssets",

View File

@@ -9,7 +9,7 @@
"vips.dev": {
"platforms": ["aarch64-darwin"],
},
"nodejs": "24.5.0",
"nodejs": "24.8.0",
"pkg-config": "latest",
"python": "3.13.3",
"bat": "latest",

23
docker-compose-oidc.yml Normal file
View File

@@ -0,0 +1,23 @@
services:
ocis:
image: owncloud/ocis:latest
container_name: ocis
entrypoint: /bin/sh
command: ["-c", "ocis init --insecure true || true; ocis server"]
environment:
OCIS_URL: https://localhost:9200
OCIS_INSECURE: "true"
PROXY_ENABLE_BASIC_AUTH: "false"
IDM_ADMIN_PASSWORD: admin
OCIS_LOG_LEVEL: warn
# Allow Joplin's redirect URIs
IDP_INSECURE: "true"
IDP_IDENTIFIER_REGISTRATION_CONF: /etc/ocis/clients.yaml
ports:
- "9200:9200"
volumes:
- ocis_data:/var/lib/ocis
- ./ocis-clients.yaml:/etc/ocis/clients.yaml:ro
volumes:
ocis_data:

12
ocis-clients.yaml Normal file
View File

@@ -0,0 +1,12 @@
clients:
- id: joplin
name: Joplin
application_type: native
redirect_uris:
- http://localhost:9968
- http://localhost:8968
- http://localhost:8868
- http://127.0.0.1:9968
- http://127.0.0.1:8968
- http://127.0.0.1:8868
- joplin://oidc-callback

View File

@@ -86,9 +86,9 @@
"gulp": "4.0.2",
"husky": "9.1.7",
"lerna": "3.22.1",
"lint-staged": "16.1.6",
"lint-staged": "16.2.6",
"madge": "8.0.0",
"npm-package-json-lint": "8.0.0",
"npm-package-json-lint": "9.0.0",
"typescript": "5.8.3"
},
"dependencies": {

View File

@@ -1,6 +1,7 @@
import ShareService from '@joplin/lib/services/share/ShareService';
import mockShareService from '@joplin/lib/testing/share/mockShareService';
import { createFolderTree, setupDatabaseAndSynchronizer, switchClient, waitFor } from '@joplin/lib/testing/test-utils';
import { createFolderTree, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import waitFor from '@joplin/lib/testing/waitFor';
import { setupApplication, setupCommandForTesting } from './utils/testUtils';
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';

View File

@@ -8,7 +8,7 @@ import { masterKeysWithoutPassword } from '@joplin/lib/services/e2ee/utils';
import { appTypeToLockType } from '@joplin/lib/services/synchronizer/LockHandler';
const BaseCommand = require('./base-command').default;
import app from './app';
const { OneDriveApiNodeUtils } = require('@joplin/lib/onedrive-api-node-utils.js');
import OneDriveApiNodeUtils from '@joplin/lib/onedrive-api-node-utils';
import { reg } from '@joplin/lib/registry';
const { cliUtils } = require('./cli-utils.js');
const md5 = require('md5');

View File

@@ -1,6 +1,7 @@
import ShareService from '@joplin/lib/services/share/ShareService';
import mockShareService from '@joplin/lib/testing/share/mockShareService';
import { setupDatabaseAndSynchronizer, switchClient, waitFor } from '@joplin/lib/testing/test-utils';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import waitFor from '@joplin/lib/testing/waitFor';
import { setupApplication, setupCommandForTesting } from './utils/testUtils';
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';

View File

@@ -57,7 +57,7 @@
"proper-lockfile": "4.1.2",
"redux": "4.2.1",
"server-destroy": "1.0.1",
"sharp": "0.34.4",
"sharp": "0.34.5",
"sprintf-js": "1.1.3",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",

View File

@@ -12,6 +12,7 @@ function newTestMdToHtml(options: any = null) {
ResourceModel: {
isResourceUrl: isResourceUrl,
urlToId: resourceUrlToId,
fullPath: () => '/some/path/here',
},
fsDriver: shim.fsDriver(),
...options,
@@ -56,6 +57,21 @@ describe('MdToHtml', () => {
mdToHtmlOptions.mapsToLine = true;
} else if (mdFilename.startsWith('resource_')) {
mdToHtmlOptions.resources = {};
} else if (mdFilename.startsWith('pdf_')) {
mdToHtmlOptions.resources = {
'00000000000000000000000000000001': {
item: { mime: 'application/pdf' },
localState: { },
},
};
mdToHtmlOptions.pdfViewerEnabled = true;
} else if (mdFilename.startsWith('video_')) {
mdToHtmlOptions.resources = {
'00000000000000000000000000000001': {
item: { mime: 'video/mp4' },
localState: { },
},
};
}
const markdown = await shim.fsDriver().readFile(mdFilePath);
@@ -86,7 +102,7 @@ describe('MdToHtml', () => {
// eslint-disable-next-line no-console
console.info(msg.join('\n'));
expect(false).toBe(true);
expect(actualHtml).toBe(expectedHtml);
// return;
} else {
expect(true).toBe(true);

View File

@@ -0,0 +1,11 @@
<div class="joplin-editable">
<!-- Regression test: Historically, text nodes before the first "joplin-source" block caused
conversion to fail. -->
A text node!
<pre class="joplin-source" data-joplin-language="test" data-joplin-source-open="```&#10;" data-joplin-source-close="&#10;```">
Test!
</pre>
<div class="joplin-rendered">
<p>Test content</p>
</div>
</div>

View File

@@ -0,0 +1,4 @@
```
Test!
```

View File

@@ -0,0 +1,4 @@
<p>Embed without starting page:</p>
<p><a data-from-md data-resource-id='00000000000000000000000000000001' type='application/pdf' href='#' onclick='postMessage(&quot;joplin://00000000000000000000000000000001&quot;, { resourceId: &quot;00000000000000000000000000000001&quot; }); return false;'><span class="resource-icon fa-file-pdf"></span>pdf</a><object data="file:///some/path/here" class="media-player media-pdf" type="application/pdf"></object></p>
<p>Embed with starting page:</p>
<p><a data-from-md data-resource-id='00000000000000000000000000000001' type='application/pdf' href='#' onclick='postMessage(&quot;joplin://00000000000000000000000000000001#page=1&quot;, { resourceId: &quot;00000000000000000000000000000001&quot; }); return false;'><span class="resource-icon fa-file-pdf"></span>pdf</a><object data="file:///some/path/here#page=1" class="media-player media-pdf" type="application/pdf"></object></p>

View File

@@ -0,0 +1,8 @@
Embed without starting page:
[pdf](:/00000000000000000000000000000001)
Embed with starting page:
[pdf](:/00000000000000000000000000000001#page=1)

View File

@@ -0,0 +1,10 @@
<p><a data-from-md data-resource-id='00000000000000000000000000000001' type='video/mp4' href='#' onclick='postMessage(&quot;joplin://00000000000000000000000000000001#t=1,2&quot;, { resourceId: &quot;00000000000000000000000000000001&quot; }); return false;'><span class="resource-icon fa-file-video"></span>video, with start/end time</a>
<video class="media-player media-video" controls>
<source src="file:///some/path/here#t=1,2" type="video/mp4">
</video>
</p>
<p><a data-from-md data-resource-id='00000000000000000000000000000001' type='video/mp4' href='#' onclick='postMessage(&quot;joplin://00000000000000000000000000000001&quot;, { resourceId: &quot;00000000000000000000000000000001&quot; }); return false;'><span class="resource-icon fa-file-video"></span>video, without start/end time</a>
<video class="media-player media-video" controls>
<source src="file:///some/path/here" type="video/mp4">
</video>
</p>

View File

@@ -0,0 +1,4 @@
[video, with start/end time](:/00000000000000000000000000000001#t=1,2)
[video, without start/end time](:/00000000000000000000000000000001)

Binary file not shown.

View File

@@ -95,6 +95,9 @@ export default class InteropServiceHelper {
// Allows users to override the CSS page size.
// See https://github.com/laurent22/joplin/issues/13096
preferCSSPageSize: true,
// Include accessibility information in the output:
generateTaggedPDF: true,
});
resolve(data);
} catch (error) {

View File

@@ -76,6 +76,19 @@ class ConfigScreenComponent extends React.Component<any, any> {
});
}
}
// Check if WebDAV with OIDC authentication needs login
if (this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('webdav') &&
this.state.settings['sync.6.authType'] === 'oidc') {
const isAuthenticated = await reg.syncTarget().isAuthenticated();
if (!isAuthenticated) {
return this.props.dispatch({
type: 'NAV_GO',
routeName: 'WebDavOidcLogin',
});
}
}
await shared.checkSyncConfig(this, this.state.settings);
}
@@ -115,6 +128,13 @@ class ConfigScreenComponent extends React.Component<any, any> {
type: 'DIALOG_OPEN',
name: 'syncWizard',
});
} else if (key === 'sync.6.oidcLogin') {
// Save current settings before navigating to login
await shared.saveSettings(this);
this.props.dispatch({
type: 'NAV_GO',
routeName: 'WebDavOidcLogin',
});
} else {
throw new Error(`Unhandled key: ${key}`);
}

View File

@@ -693,17 +693,8 @@ function useMenu(props: Props) {
menuItemDic.pasteAsText,
menuItemDic.textSelectAll,
separator(),
// Using the generic "undo"/"redo" roles mean the menu
// item will work in every text fields, whether it's the
// editor or a regular text field.
{
role: 'undo',
label: _('Undo'),
},
{
role: 'redo',
label: _('Redo'),
},
menuItemDic.globalUndo,
menuItemDic.globalRedo,
separator(),
menuItemDic.textBold,
menuItemDic.textItalic,

View File

@@ -1,6 +1,6 @@
import { ContextMenuParams, Event } from 'electron';
import { useEffect, RefObject } from 'react';
import { useEffect, RefObject, useContext } from 'react';
import { _ } from '@joplin/lib/locale';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { EditContextMenuFilterObject, MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
@@ -11,6 +11,7 @@ import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'
import eventManager from '@joplin/lib/eventManager';
import bridge from '../../../../../services/bridge';
import Setting from '@joplin/lib/models/Setting';
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@@ -29,6 +30,7 @@ interface ContextMenuProps {
const useContextMenu = (props: ContextMenuProps) => {
const editorRef = props.editorRef;
const windowId = useContext(WindowIdContext);
// The below code adds support for spellchecking when it is enabled
// It might be buggy, refer to the below issue
@@ -156,7 +158,7 @@ const useContextMenu = (props: ContextMenuProps) => {
// Prepend the event listener so that it gets called before
// the listener that shows the default menu.
const targetWindow = bridge().activeWindow();
const targetWindow = bridge().windowById(windowId);
targetWindow.webContents.prependListener('context-menu', onContextMenu);
return () => {
@@ -167,6 +169,7 @@ const useContextMenu = (props: ContextMenuProps) => {
}, [
props.plugins, props.editorClassName, editorRef, props.containerRef,
props.editorCutText, props.editorCopyText, props.editorPaste,
windowId,
]);
};

View File

@@ -294,6 +294,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
window.requestAnimationFrame(() => editor.undoManager.add());
},
pasteAsText: () => editor.fire(TinyMceEditorEvents.PasteAsText),
'editor.undo': () => {
editor.undoManager.undo();
},
'editor.redo': () => {
editor.undoManager.redo();
},
};
if (additionalCommands[cmd.name]) {

View File

@@ -1,7 +1,7 @@
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import { useEffect } from 'react';
import { useContext, useEffect } from 'react';
import bridge from '../../../../../services/bridge';
import { ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenuUtils';
import { menuItems } from '../../../utils/contextMenu';
@@ -18,6 +18,7 @@ import { Dispatch } from 'redux';
import { _ } from '@joplin/lib/locale';
import type { MenuItem as MenuItemType } from 'electron';
import isItemId from '@joplin/lib/models/utils/isItemId';
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@@ -30,11 +31,12 @@ interface ContextMenuActionOptions {
const contextMenuActionOptions: ContextMenuActionOptions = { current: null };
export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatch, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler, editDialog: EditDialogControl) {
const windowId = useContext(WindowIdContext);
useEffect(() => {
if (!editor) return () => {};
const contextMenuItems = menuItems(dispatch);
const targetWindow = bridge().activeWindow();
const targetWindow = bridge().windowById(windowId);
const makeMainMenuItems = (element: Element) => {
let itemType: ContextMenuItemType = ContextMenuItemType.None;
@@ -175,5 +177,5 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
targetWindow.webContents.off('context-menu', onElectronContextMenu);
}
};
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog]);
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog, windowId]);
}

View File

@@ -51,6 +51,15 @@ function newBlockSource(language = '', content = '', previousSource: SourceInfo
} else {
fence = '$$';
}
} else if (language === 'frontmatter') {
// Frontmatter uses --- delimiters instead of code fences
return {
openCharacters: '---\n',
closeCharacters: '\n---\n',
content: content,
node: null,
language: language,
};
}
const fenceLanguage = language === 'katex' ? '' : language;

View File

@@ -58,6 +58,17 @@ const usePluginMessageResponder = (webviewRef: RefObject<HTMLIFrameElement>) =>
}, [webviewRef, windowId]);
};
const useAllowAttribute = () => {
// Specifies what content in the note viewer can do. See
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/allow
// allow=fullscreen: Required to allow the user to fullscreen videos.
return [
'clipboard-write', 'fullscreen', 'autoplay', 'local-fonts', 'encrypted-media',
].map(
attr => `${attr} joplin-content://note-viewer/`,
).join('; ');
};
const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerControl>) => {
const [webview, setWebview] = useState<HTMLIFrameElement|null>(null);
const webviewRef = useRef<HTMLIFrameElement|null>(null);
@@ -233,14 +244,13 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerCon
return { border: 'none', ...props.viewerStyle };
}, [props.viewerStyle]);
// allow=fullscreen: Required to allow the user to fullscreen videos.
const allow = useAllowAttribute();
return (
<iframe
className="noteTextViewer"
ref={setWebview}
style={viewerStyle}
allow='clipboard-write=(self) fullscreen=(self) autoplay=(self) local-fonts=(self) encrypted-media=(self)'
allowFullScreen={true}
allow={allow}
aria-label={_('Note viewer')}
src={`joplin-content://note-viewer/${toForwardSlashes(getAssetPath('gui/note-viewer/index.html'))}`}
></iframe>

View File

@@ -7,7 +7,7 @@ import { reg } from '@joplin/lib/registry';
import Setting from '@joplin/lib/models/Setting';
import bridge from '../services/bridge';
const { themeStyle } = require('@joplin/lib/theme');
const { OneDriveApiNodeUtils } = require('@joplin/lib/onedrive-api-node-utils.js');
import OneDriveApiNodeUtils from '@joplin/lib/onedrive-api-node-utils';
interface Props {
themeId: string;

View File

@@ -6,6 +6,7 @@ import ConfigScreen from './ConfigScreen/ConfigScreen';
import StatusScreen from './StatusScreen/StatusScreen';
import OneDriveLoginScreen from './OneDriveLoginScreen';
import DropboxLoginScreen from './DropboxLoginScreen';
import WebDavOidcLoginScreen from './WebDavOidcLoginScreen';
import ErrorBoundary from './ErrorBoundary';
import { themeStyle } from '@joplin/lib/theme';
import MenuBar from './MenuBar';
@@ -163,6 +164,7 @@ class RootComponent extends React.Component<Props, any> {
DropboxLogin: { screen: DropboxLoginScreen, title: () => _('Dropbox Login') },
JoplinCloudLogin: { screen: JoplinCloudLoginScreen, title: () => _('Joplin Cloud Login') },
JoplinServerSamlLogin: { screen: SsoLoginScreen(new SamlShared()), title: () => _('Joplin Server Login') },
WebDavOidcLogin: { screen: WebDavOidcLoginScreen, title: () => _('WebDAV OIDC Login') },
Import: { screen: ImportScreen, title: () => _('Import') },
Config: { screen: ConfigScreen, title: () => _('Options') },
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },

View File

@@ -0,0 +1,33 @@
.webdav-oidc-login-screen {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--joplin-background-color);
> .content {
padding: var(--joplin-config-screen-padding);
flex: 1;
color: var(--joplin-color);
> .title {
font-size: var(--joplin-h1-font-size);
font-weight: bold;
margin-bottom: 1em;
}
> .logentry {
font-size: var(--joplin-font-size);
margin: 0;
}
> .loglink {
color: var(--joplin-url-color);
font-size: var(--joplin-font-size);
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
}

View File

@@ -0,0 +1,111 @@
import * as React from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import ButtonBar from './ConfigScreen/ButtonBar';
import { _ } from '@joplin/lib/locale';
import { reg } from '@joplin/lib/registry';
import Setting from '@joplin/lib/models/Setting';
import bridge from '../services/bridge';
import { OidcApiNodeUtils } from '@joplin/lib/oidc-api-node-utils';
import OidcApi from '@joplin/lib/OidcApi';
interface LogEntry {
key: string;
text: string;
}
const WebDavOidcLoginScreen: React.FC = () => {
const [authLog, setAuthLog] = useState<LogEntry[]>([]);
const oidcApiUtilsRef = useRef<OidcApiNodeUtils | null>(null);
const dispatch = useDispatch();
const log = useCallback((s: string) => {
setAuthLog(prevLog => [
...prevLog,
{ key: `${Date.now()}-${Math.random()}`, text: s },
]);
}, []);
useEffect(() => {
const performAuth = async () => {
const syncTargetId = Setting.value('sync.target');
const oidcApi = new OidcApi({
issuerUrl: Setting.value('sync.6.oidcIssuerUrl'),
clientId: Setting.value('sync.6.oidcClientId'),
clientSecret: Setting.value('sync.6.oidcClientSecret'),
ignoreTlsErrors: Setting.value('net.ignoreTlsErrors'),
});
oidcApiUtilsRef.current = new OidcApiNodeUtils(oidcApi);
try {
const auth = await oidcApiUtilsRef.current.oauthDance({
log: (s: string) => log(s),
});
Setting.setValue(`sync.${syncTargetId}.oidcAuth`, auth ? JSON.stringify(auth) : '');
const syncTarget = reg.syncTarget(syncTargetId);
if (syncTarget.api && syncTarget.api()) {
syncTarget.api().setAuth(auth);
}
if (!auth) {
log(_('Authentication was not completed (did not receive an authentication token).'));
} else {
log(_('Authentication successful! You can now close this screen.'));
void reg.scheduleSync(0);
}
} catch (error) {
log(_('Authentication failed: %s', (error as Error).message));
}
};
void performAuth();
return () => {
if (oidcApiUtilsRef.current) {
oidcApiUtilsRef.current.cancelOAuthDance();
}
};
}, [log]);
const handleCancelClick = useCallback(() => {
dispatch({ type: 'NAV_BACK' });
}, [dispatch]);
const handleLinkClick = useCallback((url: string) => {
void bridge().openExternal(url);
}, []);
const renderLogEntries = () => {
return authLog.map(entry => {
if (entry.text.indexOf('http:') === 0 || entry.text.indexOf('https://') === 0) {
return (
<a
key={entry.key}
className="loglink"
href="#"
onClick={() => handleLinkClick(entry.text)}
>
{entry.text}
</a>
);
}
return <p key={entry.key} className="logentry">{entry.text}</p>;
});
};
return (
<div className="webdav-oidc-login-screen">
<div className="content">
<h1 className="title">{_('WebDAV OIDC Authentication')}</h1>
{renderLogEntries()}
</div>
<ButtonBar onCancelClick={handleCancelClick} />
</div>
);
};
export default WebDavOidcLoginScreen;

View File

@@ -0,0 +1,24 @@
import CommandService, { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { WindowControl } from '../utils/useWindowControl';
import bridge from '../../../services/bridge';
import canUseNativeUndo from './utils/canUseNativeUndo';
export const declaration: CommandDeclaration = {
name: 'globalRedo',
label: () => _('Redo'),
};
export const runtime = (control: WindowControl): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
if (canUseNativeUndo(control)) {
bridge().activeWindow().webContents.redo();
} else {
await CommandService.instance().execute('editor.redo');
}
},
enabledCondition: '',
};
};

View File

@@ -0,0 +1,27 @@
import CommandService, { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { WindowControl } from '../utils/useWindowControl';
import bridge from '../../../services/bridge';
import canUseNativeUndo from './utils/canUseNativeUndo';
export const declaration: CommandDeclaration = {
name: 'globalUndo',
label: () => _('Undo'),
};
export const runtime = (control: WindowControl): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
// As of January 2026, webContents.undo() doesn't work properly in more complex
// edit controls (e.g. CodeMirror or TinyMCE). Only use it when a more simple input
// has focus:
if (canUseNativeUndo(control)) {
bridge().activeWindow().webContents.undo();
} else {
await CommandService.instance().execute('editor.undo');
}
},
enabledCondition: '',
};
};

View File

@@ -5,6 +5,8 @@ import * as deleteFolder from './deleteFolder';
import * as duplicateNote from './duplicateNote';
import * as editAlarm from './editAlarm';
import * as exportPdf from './exportPdf';
import * as globalRedo from './globalRedo';
import * as globalUndo from './globalUndo';
import * as gotoAnything from './gotoAnything';
import * as hideModalMessage from './hideModalMessage';
import * as importFrom from './importFrom';
@@ -54,6 +56,8 @@ const index: any[] = [
duplicateNote,
editAlarm,
exportPdf,
globalRedo,
globalUndo,
gotoAnything,
hideModalMessage,
importFrom,

View File

@@ -0,0 +1,11 @@
import { WindowControl } from '../../utils/useWindowControl';
// CodeMirror and TinyMCE both have trouble with native Electron
// undo/redo.
// See https://github.com/laurent22/joplin/issues/14216
const canUseNativeUndo = (control: WindowControl) => {
const dom = control.getFocusedDocument();
return !dom.activeElement.closest('.CodeMirror, div.joplin-tinymce');
};
export default canUseNativeUndo;

View File

@@ -21,7 +21,7 @@ const useWindowCommands = ({ documentRef, customCss, plugins, editorNoteStatuses
editorNoteStatuses: editorNoteStatuses,
plugins: plugins,
});
const windowControl = useWindowControl(setDialogState, onPrintCallback);
const windowControl = useWindowControl(setDialogState, onPrintCallback, documentRef);
// This effect needs to run as soon as possible. Certain components may fail to load if window
// commands are not registered on their first render.

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useMemo, useRef } from 'react';
import { RefObject, useMemo, useRef } from 'react';
import { DialogState } from '../types';
import { PrintCallback } from './usePrintToCallback';
import { _ } from '@joplin/lib/locale';
@@ -23,10 +23,11 @@ export interface WindowControl {
showPrompt: <T>(options: PromptOptions<T>)=> Promise<T>;
printTo: PrintCallback;
announcePanelVisibility(panelName: string, visible: boolean): void;
getFocusedDocument(): Document;
}
export type OnSetDialogState = React.Dispatch<React.SetStateAction<DialogState>>;
const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallback) => {
const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallback, windowDomRef: RefObject<Document>) => {
// Use refs to avoid reloading the output where possible -- reloading the window control
// may mean reloading all main window commands.
const onPrintRef = useRef(onPrint);
@@ -67,9 +68,12 @@ const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallba
});
});
},
getFocusedDocument: () => {
return windowDomRef.current;
},
};
return control;
}, [setDialogState]);
}, [setDialogState, windowDomRef]);
};
export default useWindowControl;

View File

@@ -50,13 +50,16 @@ export default function() {
'editor.duplicateLine',
'openSecondaryAppInstance',
'openPrimaryAppInstance',
// We cannot put the undo/redo commands in the menu because they are
// editor-specific commands. If we put them there it will break the
// undo/redo in regular text fields.
// https://github.com/laurent22/joplin/issues/6214
// 'editor.undo',
// 'editor.redo',
// We cannot put the editor.undo/editor.redo commands in the menu because they are
// editor-specific commands. If we put them there it will break the undo/redo in
// regular text fields (https://github.com/laurent22/joplin/issues/6214).
// However, the native Electron undo/redo doesn't work well in TinyMCE/CodeMirror.
// As a workaround, use these commands that switch between editor.undo and native Electron
// undo/redo depending on the type of selected editor:
'globalUndo',
'globalRedo',
'editor.indentLess',
'editor.indentMore',
'editor.toggleComment',

View File

@@ -381,5 +381,24 @@ test.describe('markdownEditor', () => {
await goToAnything.runCommand(electronApp, 'textPaste');
await noteEditor.expectToHaveText(/^Test \(new content!\)[\n]+/);
});
test('the undo and redo menu items should work', async ({ mainWindow, electronApp }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.waitFor();
await mainScreen.createNewNote('Test undo/redo');
const noteEditor = mainScreen.noteEditor;
await noteEditor.focusCodeMirrorEditor();
await mainWindow.keyboard.type('A');
await noteEditor.expectToHaveText('A');
await activateMainMenuItem(electronApp, 'Undo');
await noteEditor.expectToHaveText('\n');
await activateMainMenuItem(electronApp, 'Redo');
await noteEditor.expectToHaveText('A');
});
});

View File

@@ -23,7 +23,7 @@ const waitFor = async (condition) => {
setTimeout(() => resolve(), 100);
});
};
for (let i = 0; i < 100; i++) {
for (let i = 0; i < 500; i++) {
if (await condition()) {
return;
}

View File

@@ -131,7 +131,9 @@ module.exports = {
testEnvironment: 'jsdom',
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
testEnvironmentOptions: {
customExportConditions: ['node', 'require'],
},
// Adds a location field to test results
// testLocationInResults: false,

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.6.1",
"version": "3.6.2",
"description": "Joplin for Desktop",
"main": "main.bundle.js",
"private": true,
@@ -169,7 +169,7 @@
"debounce": "1.2.1",
"electron": "39.2.3",
"electron-builder": "24.13.3",
"electron-updater": "6.6.2",
"electron-updater": "6.6.8",
"electron-window-state": "5.0.3",
"esbuild": "^0.25.3",
"formatcoords": "1.1.3",
@@ -185,7 +185,7 @@
"md5": "2.3.0",
"moment": "2.30.1",
"mustache": "4.2.0",
"nan": "2.23.0",
"nan": "2.23.1",
"node-notifier": "10.0.1",
"node-rsa": "1.1.1",
"pdfjs-dist": "3.11.174",

View File

@@ -8,6 +8,7 @@
@use 'gui/NoteList/style.scss' as note-list;
@use 'gui/SsoLoginScreen/SsoLoginScreen.scss' as sso-login-screen;
@use 'gui/JoplinCloudLoginScreen.scss' as joplin-cloud-login-screen;
@use 'gui/WebDavOidcLoginScreen.scss' as webdav-oidc-login-screen;
@use 'gui/NoteListHeader/style.scss' as note-list-header;
@use 'gui/UpdateNotification/style.scss' as update-notification;
@use 'gui/Sidebar/style.scss' as sidebar-styles;

View File

@@ -0,0 +1,90 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = notarizeFile;
const fs_1 = require('fs');
const notarize_1 = require('@electron/notarize');
const execCommand = require('./execCommand');
const child_process_1 = require('child_process');
const util_1 = require('util');
const execAsync = (0, util_1.promisify)(child_process_1.exec);
// Same appId in electron-builder.
const appId = 'net.cozic.joplin-desktop';
function isDesktopAppTag(tagName) {
if (!tagName) { return false; }
return tagName[0] === 'v';
}
async function notarizeFile(filePath) {
if (process.platform !== 'darwin') { return; }
console.info(`Checking if notarization should be done on: ${filePath}`);
if (!process.env.IS_CONTINUOUS_INTEGRATION || !isDesktopAppTag(process.env.GIT_TAG_NAME)) {
console.info(`Either not running in CI or not processing a desktop app tag - skipping notarization. process.env.IS_CONTINUOUS_INTEGRATION = ${process.env.IS_CONTINUOUS_INTEGRATION}; process.env.GIT_TAG_NAME = ${process.env.GIT_TAG_NAME}`);
return;
}
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
console.warn('Environment variables APPLE_ID and APPLE_ID_PASSWORD not found - notarization will NOT be done.');
return;
}
if (!(0, fs_1.existsSync)(filePath)) {
throw new Error(`Cannot find file at: ${filePath}`);
}
// Every x seconds we print something to stdout, otherwise CI may timeout
// the task after 10 minutes, and Apple notarization can take more time.
const waitingIntervalId = setInterval(() => {
console.info('.');
}, 60000);
const isPkg = filePath.endsWith('.pkg');
console.info(`Notarizing ${filePath}`);
try {
if (isPkg) {
await execAsync(`xcrun notarytool submit "${filePath}" ` +
`--apple-id "${process.env.APPLE_ID}" ` +
`--password "${process.env.APPLE_ID_PASSWORD}" ` +
`--team-id "${process.env.APPLE_ASC_PROVIDER}" ` +
'--wait', { maxBuffer: 1024 * 1024 });
} else {
await (0, notarize_1.notarize)({
appBundleId: appId,
appPath: filePath,
// Apple Developer email address
appleId: process.env.APPLE_ID,
// App-specific password: https://support.apple.com/en-us/HT204397
appleIdPassword: process.env.APPLE_ID_PASSWORD,
// When Apple ID is attached to multiple providers (eg if the
// account has been used to build multiple apps for different
// companies), in that case the provider "Team Short Name" (also
// known as "ProviderShortname") must be provided.
//
// Use this to get it:
//
// xcrun altool --list-providers -u APPLE_ID -p APPLE_ID_PASSWORD
// ascProvider: process.env.APPLE_ASC_PROVIDER,
// In our case, the team ID is the same as the legacy ASC_PROVIDER
teamId: process.env.APPLE_ASC_PROVIDER,
tool: 'notarytool',
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
});
}
} catch (error) {
console.error(error);
process.exit(1);
}
clearInterval(waitingIntervalId);
// It appears that electron-notarize doesn't staple the app, but without
// this we were still getting the malware warning when launching the app.
// Stapling the app means attaching the notarization ticket to it, so that
// if the user is offline, macOS can still check if the app was notarized.
// So it seems to be more or less optional, but at least in our case it
// wasn't.
console.info('Stapling notarization ticket to the file...');
const staplerCmd = `xcrun stapler staple "${filePath}"`;
console.info(`> ${staplerCmd}`);
console.info(await execCommand(staplerCmd));
console.info(`Validating stapled file: ${filePath}`);
try {
await execAsync(`spctl -a -vv -t install "${filePath}"`);
} catch (error) {
console.error(`Failed validating stapled file: ${filePath}:`, error);
}
console.info(`Done notarizing ${filePath}`);
}
// # sourceMappingURL=notarizeFile.js.map

View File

@@ -2,11 +2,24 @@ apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
// (Disabled) Use Expo CLI to bundle the app, this ensures the Metro config
// works correctly with Expo projects.
// cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
// bundleCommand = "export:embed"
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../..")
@@ -55,31 +68,12 @@ react {
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
android {
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
@@ -89,21 +83,17 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097788
versionName "3.6.0"
versionCode 2097800
versionName "3.6.12"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
// Needed to fix: The number of method references in a .dex file cannot exceed 64K
multiDexEnabled true
externalNativeBuild {
cmake {
cppFlags '-DCMAKE_BUILD_TYPE=Release'
// For 16 KB pages. This should be removable after upgrading to NDK r28
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
}
}
}
signingConfigs {
debug {
@@ -129,19 +119,25 @@ android {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.release
minifyEnabled enableProguardInReleaseBuilds
minifyEnabled enableMinifyInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
profileable {
// Release-like build that allows profiling with Android Studio Profiler
initWith release
signingConfig signingConfigs.debug
// Required for Android Studio Profiler to attach
debuggable false
// Keeps symbols for better stack traces in profiler
minifyEnabled false
// Use release variants of dependencies that don't have profileable
matchingFallbacks = ['release']
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
implementation("com.facebook.react:hermes-android")
}

View File

@@ -8,3 +8,7 @@
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# Keep classes referenced by JNI
# (see https://developer.android.com/topic/performance/app-optimization/add-keep-rules)
-keep class com.margelo.nitro.whispervoicetyping.AudioRecorder

View File

@@ -44,8 +44,12 @@
android:requestLegacyExternalStorage="true"
android:resizeableActivity="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="${usesCleartextTraffic}"
android:supportsRtl="true">
<!-- Enable profiling in release builds (Android 10+) -->
<profileable android:shell="true" />
<!--
2018-12-16: Changed android:launchMode from "singleInstance" to "singleTop" for Firebase notification
Previously singleInstance was necessary to prevent multiple instance of the RN app from running at the same time, but maybe no longer needed.

View File

@@ -1,65 +0,0 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("joplin")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
whisperWrapper.cpp
utils/WhisperSession.cpp
utils/findLongestSilence.cpp
utils/findLongestSilence_test.cpp
)
set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../../vendor/whisper.cpp)
# Based on the Whisper.cpp Android example:
set(SHARED_FLAGS "-O3 ")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SHARED_FLAGS} ")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SHARED_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections")
# Whisper: See https://stackoverflow.com/a/76290722
add_subdirectory(${WHISPER_LIB_DIR} ./whisper)
# Directories for header files
target_include_directories(
${CMAKE_PROJECT_NAME}
PUBLIC
${PROJECT_BASE_DIR}/shared
${WHISPER_LIB_DIR}/include
)
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
whisper
# List libraries link to the target library
android
log
)

View File

@@ -1,151 +0,0 @@
// Write C++ code here.
//
// Do not forget to dynamically load the C++ library into your application.
//
// For instance,
//
// In MainActivity.java:
// static {
// System.loadLibrary("joplin");
// }
//
// Or, in MainActivity.kt:
// companion object {
// init {
// System.loadLibrary("joplin")
// }
// }
#include <jni.h>
#include <memory>
#include <string>
#include <sstream>
#include <android/log.h>
#include "whisper.h"
#include "utils/WhisperSession.h"
#include "utils/androidUtil.h"
#include "utils/findLongestSilence_test.h"
void log_android(enum ggml_log_level level, const char* message, void* user_data) {
android_LogPriority priority = level == 4 ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO;
__android_log_print(priority, "Whisper::JNI::cpp", "%s", message);
}
jstring stringToJava(JNIEnv *env, const std::string& source) {
return env->NewStringUTF(source.c_str());
}
std::string stringToCXX(JNIEnv *env, jstring jString) {
const char *jStringChars = env->GetStringUTFChars(jString, nullptr);
std::string result { jStringChars };
env->ReleaseStringUTFChars(jString, jStringChars);
return result;
}
void throwException(JNIEnv *env, const std::string& message) {
jclass errorClass = env->FindClass("java/lang/Exception");
env->ThrowNew(errorClass, message.c_str());
}
extern "C"
JNIEXPORT jlong JNICALL
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_init(
JNIEnv *env,
jobject thiz,
jstring modelPath,
jstring language,
jstring prompt,
jboolean useShortAudioContext
) {
whisper_log_set(log_android, nullptr);
try {
auto *pSession = new WhisperSession(
stringToCXX(env, modelPath), stringToCXX(env, language), stringToCXX(env, prompt), useShortAudioContext
);
return (jlong) pSession;
} catch (const std::exception& exception) {
LOGW("Failed to init whisper: %s", exception.what());
throwException(env, exception.what());
return 0;
}
}
extern "C"
JNIEXPORT void JNICALL
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_free(JNIEnv *env, jobject thiz,
jlong pointer) {
delete reinterpret_cast<WhisperSession *>(pointer);
}
extern "C"
JNIEXPORT void JNICALL
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_addAudio(JNIEnv *env,
jobject thiz,
jlong pointer,
jfloatArray audio_data) {
auto *pSession = reinterpret_cast<WhisperSession *> (pointer);
jfloat *pAudioData = env->GetFloatArrayElements(audio_data, nullptr);
jsize lenAudioData = env->GetArrayLength(audio_data);
std::string result;
try {
pSession->addAudio(pAudioData, lenAudioData);
} catch (const std::exception& exception) {
LOGW("Failed to add to audio buffer: %s", exception.what());
throwException(env, exception.what());
}
// JNI_ABORT: "free the buffer without copying back the possible changes", pass 0 to copy
// changes (there should be no changes)
env->ReleaseFloatArrayElements(audio_data, pAudioData, JNI_ABORT);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_transcribeNextChunk(JNIEnv *env,
jobject thiz,
jlong pointer) {
auto *pSession = reinterpret_cast<WhisperSession *> (pointer);
std::string result;
try {
result = pSession->transcribeNextChunk();
} catch (const std::exception& exception) {
LOGW("Failed to run whisper: %s", exception.what());
throwException(env, exception.what());
return nullptr;
}
return stringToJava(env, result);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_transcribeRemaining(JNIEnv *env,
jobject thiz,
jlong pointer) {
auto *pSession = reinterpret_cast<WhisperSession *> (pointer);
std::string result;
try {
result = pSession->transcribeAll();
} catch (const std::exception& exception) {
LOGW("Failed to run whisper: %s", exception.what());
throwException(env, exception.what());
return nullptr;
}
return stringToJava(env, result);
}
extern "C"
JNIEXPORT void JNICALL
Java_net_cozic_joplin_audio_NativeWhisperLib_00024Companion_runTests(JNIEnv *env, jobject thiz) {
try {
findLongestSilence_test();
} catch (const std::exception& exception) {
LOGW("Failed to run tests: %s", exception.what());
throwException(env, exception.what());
}
}

View File

@@ -6,37 +6,36 @@ import expo.modules.ReactNativeHostWrapper
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.common.ReleaseLevel
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import net.cozic.joplin.audio.SpeechToTextPackage
import net.cozic.joplin.versioninfo.SystemVersionInformationPackage
import net.cozic.joplin.share.SharePackage
import net.cozic.joplin.ssl.SslPackage
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
this,
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
add(SharePackage())
add(SslPackage())
add(SystemVersionInformationPackage())
add(SpeechToTextPackage())
}
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
})
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
})
override val reactHost: ReactHost
get() = ReactNativeHostWrapper.createReactHost(this.applicationContext, reactNativeHost)
@@ -44,16 +43,17 @@ class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
try {
DefaultNewArchitectureEntryPoint.releaseLevel = ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
} catch (e: IllegalArgumentException) {
DefaultNewArchitectureEntryPoint.releaseLevel = ReleaseLevel.STABLE
}
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
loadReactNative(this)
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}

View File

@@ -1,5 +0,0 @@
package net.cozic.joplin.audio
class InvalidSessionIdException(id: Int) : IllegalArgumentException("Invalid session ID $id") {
}

View File

@@ -1,64 +0,0 @@
package net.cozic.joplin.audio
import java.io.Closeable
class NativeWhisperLib(
modelPath: String,
languageCode: String,
prompt: String,
shortAudioContext: Boolean,
) : Closeable {
companion object {
init {
System.loadLibrary("joplin")
}
external fun runTests(): Unit;
// TODO: The example whisper.cpp project transfers pointers as Longs to the Kotlin code.
// This seems unsafe. Try changing how this is managed.
private external fun init(modelPath: String, languageCode: String, prompt: String, shortAudioContext: Boolean): Long;
private external fun free(pointer: Long): Unit;
private external fun addAudio(pointer: Long, audioData: FloatArray): Unit;
private external fun transcribeNextChunk(pointer: Long): String;
private external fun transcribeRemaining(pointer: Long): String;
}
private var closed = false
private val pointer: Long = init(modelPath, languageCode, prompt, shortAudioContext)
fun addAudio(audioData: FloatArray) {
if (closed) {
throw Exception("Cannot add audio data to a closed session")
}
Companion.addAudio(pointer, audioData)
}
fun transcribeNextChunk(): String {
if (closed) {
throw Exception("Cannot transcribe using a closed session")
}
return Companion.transcribeNextChunk(pointer)
}
fun transcribeRemaining(): String {
if (closed) {
throw Exception("Cannot transcribeAll using a closed session")
}
return Companion.transcribeRemaining(pointer)
}
override fun close() {
if (closed) {
throw Exception("Cannot close a whisper session twice")
}
closed = true
free(pointer)
}
}

View File

@@ -1,62 +0,0 @@
package net.cozic.joplin.audio
import android.content.Context
import android.util.Log
import java.io.Closeable
class SpeechToTextConverter(
modelPath: String,
locale: String,
prompt: String,
useShortAudioCtx: Boolean,
recorderFactory: AudioRecorderFactory,
context: Context,
) : Closeable {
private val recorder = recorderFactory(context)
private val languageCode = Regex("_.*").replace(locale, "")
private var whisper = NativeWhisperLib(
modelPath,
languageCode,
prompt,
useShortAudioCtx,
)
fun start() {
recorder.start()
}
private fun convert(data: FloatArray): String {
Log.d("Whisper", "Pre-transcribe data of size ${data.size}")
whisper.addAudio(data)
val result = whisper.transcribeNextChunk()
Log.d("Whisper", "Post transcribe. Got $result")
return result;
}
fun dropFirstSeconds(seconds: Double) {
Log.i("Whisper", "Drop first seconds $seconds")
recorder.dropFirstSeconds(seconds)
}
val bufferLengthSeconds: Double get() = recorder.bufferLengthSeconds
fun convertNext(seconds: Double): String {
val buffer = recorder.pullNextSeconds(seconds)
val result = convert(buffer)
dropFirstSeconds(seconds)
return result
}
// Converts as many seconds of buffered data as possible, without waiting
fun convertRemaining(): String {
val buffer = recorder.pullAvailable()
whisper.addAudio(buffer)
return whisper.transcribeRemaining()
}
override fun close() {
Log.d("Whisper", "Close")
recorder.close()
whisper.close()
}
}

View File

@@ -1,87 +0,0 @@
package net.cozic.joplin.audio
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.uimanager.ViewManager
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class SpeechToTextPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf<NativeModule>(SpeechToTextModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
class SpeechToTextModule(
private var context: ReactApplicationContext,
) : ReactContextBaseJavaModule(context), LifecycleEventListener {
private val executorService: ExecutorService = Executors.newFixedThreadPool(1)
private val sessionManager = SpeechToTextSessionManager(executorService)
override fun getName() = "SpeechToTextModule"
override fun onHostResume() { }
override fun onHostPause() { }
override fun onHostDestroy() { }
@ReactMethod
fun runTests(promise: Promise) {
try {
NativeWhisperLib.runTests()
promise.resolve(true)
} catch (exception: Throwable) {
promise.reject(exception)
}
}
@ReactMethod
fun openSession(modelPath: String, locale: String, prompt: String, useShortAudioCtx: Boolean, promise: Promise) {
val appContext = context.applicationContext
try {
val sessionId = sessionManager.openSession(modelPath, locale, prompt, useShortAudioCtx, appContext)
promise.resolve(sessionId)
} catch (exception: Throwable) {
promise.reject(exception)
}
}
@ReactMethod
fun startRecording(sessionId: Int, promise: Promise) {
sessionManager.startRecording(sessionId, promise)
}
@ReactMethod
fun getBufferLengthSeconds(sessionId: Int, promise: Promise) {
sessionManager.getBufferLengthSeconds(sessionId, promise)
}
@ReactMethod
fun dropFirstSeconds(sessionId: Int, duration: Double, promise: Promise) {
sessionManager.dropFirstSeconds(sessionId, duration, promise)
}
@ReactMethod
fun convertNext(sessionId: Int, duration: Double, promise: Promise) {
sessionManager.convertNext(sessionId, duration, promise)
}
@ReactMethod
fun convertAvailable(sessionId: Int, promise: Promise) {
sessionManager.convertAvailable(sessionId, promise)
}
@ReactMethod
fun closeSession(sessionId: Int, promise: Promise) {
sessionManager.closeSession(sessionId, promise)
}
}
}

View File

@@ -1,111 +0,0 @@
package net.cozic.joplin.audio
import android.content.Context
import com.facebook.react.bridge.Promise
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
class SpeechToTextSession (
val converter: SpeechToTextConverter
) {
val mutex = ReentrantLock()
}
class SpeechToTextSessionManager(
private var executor: Executor,
) {
private val sessions: MutableMap<Int, SpeechToTextSession> = mutableMapOf()
private var nextSessionId: Int = 0
fun openSession(
modelPath: String,
locale: String,
prompt: String,
useShortAudioCtx: Boolean,
context: Context,
): Int {
val sessionId = nextSessionId++
sessions[sessionId] = SpeechToTextSession(
SpeechToTextConverter(
modelPath, locale, prompt, useShortAudioCtx, recorderFactory = AudioRecorder.factory, context,
)
)
return sessionId
}
private fun getSession(id: Int): SpeechToTextSession {
return sessions[id] ?: throw InvalidSessionIdException(id)
}
private fun concurrentWithSession(
id: Int,
callback: (session: SpeechToTextSession)->Unit,
) {
executor.execute {
val session = getSession(id)
session.mutex.lock()
try {
callback(session)
} finally {
session.mutex.unlock()
}
}
}
private fun concurrentWithSession(
id: Int,
onError: (error: Throwable)->Unit,
callback: (session: SpeechToTextSession)->Unit,
) {
return concurrentWithSession(id) { session ->
try {
callback(session)
} catch (error: Throwable) {
onError(error)
}
}
}
fun startRecording(sessionId: Int, promise: Promise) {
this.concurrentWithSession(sessionId, promise::reject) { session ->
session.converter.start()
promise.resolve(null)
}
}
// Left-shifts the recording buffer by [duration] seconds
fun dropFirstSeconds(sessionId: Int, duration: Double, promise: Promise) {
this.concurrentWithSession(sessionId, promise::reject) { session ->
session.converter.dropFirstSeconds(duration)
promise.resolve(sessionId)
}
}
fun getBufferLengthSeconds(sessionId: Int, promise: Promise) {
this.concurrentWithSession(sessionId, promise::reject) { session ->
promise.resolve(session.converter.bufferLengthSeconds)
}
}
// Waits for the next [duration] seconds to become available, then converts
fun convertNext(sessionId: Int, duration: Double, promise: Promise) {
this.concurrentWithSession(sessionId, promise::reject) { session ->
val result = session.converter.convertNext(duration)
promise.resolve(result)
}
}
// Converts all available recorded data
fun convertAvailable(sessionId: Int, promise: Promise) {
this.concurrentWithSession(sessionId, promise::reject) { session ->
val result = session.converter.convertRemaining()
promise.resolve(result)
}
}
fun closeSession(sessionId: Int, promise: Promise) {
this.concurrentWithSession(sessionId) { session ->
session.converter.close()
promise.resolve(null)
}
}
}

View File

@@ -2,14 +2,14 @@
buildscript {
ext {
buildToolsVersion = "35.0.0"
buildToolsVersion = "36.0.0"
minSdkVersion = 24
compileSdkVersion = 35
targetSdkVersion = 35
compileSdkVersion = 36
targetSdkVersion = 36
ndkVersion = "27.1.12297006"
kotlinVersion = "2.0.21"
kotlinVersion = "2.1.20"
}
repositories {
google()

View File

@@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryEr
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
@@ -34,12 +34,17 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
newArchEnabled=true
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
# Use this property to enable edge-to-edge display support.
# This allows your app to draw behind system bars for an immersive UI.
# Note: Only works with ReactActivity and should not be used with custom Activity.
edgeToEdgeEnabled=true
# To fix this error:
#
# > Failed to transform bcprov-jdk15on-1.68.jar

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

View File

@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -1,9 +0,0 @@
whisper.cpp/.gitmodules
whisper.cpp/scripts/
whisper.cpp/samples/
whisper.cpp/tests/
whisper.cpp/models/
whisper.cpp/examples/
whisper.cpp/.*/
whisper.cpp/bindings/
whisper.cpp/**/*.Dockerfile

View File

@@ -1,4 +1,7 @@
{
"name": "Joplin",
"displayName": "Joplin"
}
"displayName": "Joplin",
"plugins": [
"@react-native-community/datetimepicker"
]
}

View File

@@ -44,7 +44,7 @@ const useStyles = (theme: ThemeStyle) => {
},
contentContainer: {
padding: 20,
paddingBottom: 14,
paddingBottom: 14 + safeAreaPadding.paddingBottom,
gap: 8,
flexDirection: 'row',
flexWrap: 'wrap',

View File

@@ -438,9 +438,8 @@ const useInputEventHandlers = ({
const onSubmit = useCallback(() => {
if (selectedResult) {
onItemSelected(selectedResult, selectedIndex);
setSearch('');
}
}, [onItemSelected, selectedResult, selectedIndex, setSearch]);
}, [onItemSelected, selectedResult, selectedIndex]);
// For now, onKeyPress only works on web.
// See https://github.com/react-native-community/discussions-and-proposals/issues/249

View File

@@ -13,6 +13,7 @@ import shim from '@joplin/lib/shim';
import Logger from '@joplin/utils/Logger';
import { Props, WebViewControl } from './types';
import useCss from './utils/useCss';
import { Platform } from 'react-native';
const logger = Logger.create('ExtendedWebView');
@@ -141,7 +142,8 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
onLoadEnd={props.onLoadEnd}
onContentProcessDidTerminate={refreshWebViewAfterCrash}
onRenderProcessGone={refreshWebViewAfterCrash}
decelerationRate='normal'
// See https://github.com/react-native-webview/react-native-webview/issues/3814
decelerationRate={Platform.OS === 'ios' ? 'normal' : undefined}
/>
);
};

View File

@@ -2,7 +2,8 @@ import * as React from 'react';
import { Store } from 'redux';
import { AppState } from '../utils/types';
import TestProviderStack from './testing/TestProviderStack';
import { switchClient, setupDatabase, mockMobilePlatform, mockFetch, waitFor } from '@joplin/lib/testing/test-utils';
import { switchClient, setupDatabase, mockMobilePlatform, mockFetch } from '@joplin/lib/testing/test-utils';
import waitFor from '@joplin/lib/testing/waitFor';
import createMockReduxStore from '../utils/testing/createMockReduxStore';
import setupGlobalStore from '../utils/testing/setupGlobalStore';
import { act, fireEvent, render, screen } from '@testing-library/react-native';

View File

@@ -0,0 +1,26 @@
import * as React from 'react';
import { KeyboardAvoidingViewProps, KeyboardAvoidingView as NativeKeyboardAvoidingView } from 'react-native';
import useKeyboardState from '../utils/hooks/useKeyboardState';
interface Props extends KeyboardAvoidingViewProps {}
const KeyboardAvoidingView: React.FC<Props> = ({ enabled, children, ...forwardedProps }) => {
const keyboardState = useKeyboardState();
enabled &&= (
// When the floating keyboard is enabled, the KeyboardAvoidingView can have a very small
// height. Don't use the KeyboardAvoidingView when the floating keyboard is enabled.
// See https://github.com/facebook/react-native/issues/29473
!keyboardState.isFloatingKeyboard
);
return <NativeKeyboardAvoidingView
behavior='padding'
{...forwardedProps}
enabled={enabled}
>
{children}
</NativeKeyboardAvoidingView>;
};
export default KeyboardAvoidingView;

View File

@@ -1,12 +1,13 @@
import * as React from 'react';
import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
import { GestureResponderEvent, KeyboardAvoidingView, Modal, ModalProps, Platform, Pressable, ScrollView, ScrollViewProps, StyleSheet, View, ViewStyle } from 'react-native';
import { GestureResponderEvent, Modal, ModalProps, Platform, Pressable, ScrollView, ScrollViewProps, StyleSheet, View, ViewStyle } from 'react-native';
import FocusControl from './accessibility/FocusControl/FocusControl';
import { msleep, Second } from '@joplin/utils/time';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { ModalState } from './accessibility/FocusControl/types';
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
import { _ } from '@joplin/lib/locale';
import KeyboardAvoidingView from './KeyboardAvoidingView';
export interface ModalElementProps extends ModalProps {
children: React.ReactNode;
@@ -175,7 +176,7 @@ const ModalElement: React.FC<ModalElementProps> = ({
{...modalProps}
>
{scrollOverflow ? (
<KeyboardAvoidingView behavior='padding' style={styles.keyboardAvoidingView}>
<KeyboardAvoidingView style={styles.keyboardAvoidingView} enabled={true}>
<ScrollView
{...extraScrollViewProps}
style={[styles.modalScrollView, extraScrollViewProps.style]}

View File

@@ -61,7 +61,7 @@ const ProfileListItem: React.FC<ProfileItemProps> = ({ profile, profileConfig, s
}
};
const switchProfileMessage = _('To switch the profile, the app is going to close and you will need to restart it.');
const switchProfileMessage = _('To switch the profile, the app is going to restart.');
if (shim.mobilePlatform() === 'web') {
if (confirm(switchProfileMessage)) {
void doIt();

View File

@@ -688,16 +688,23 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
const menuComp =
!menuOptions.length || !showContextMenuButton ? null : (
<Menu themeId={this.props.themeId} options={menuOptions}>
<View style={contextMenuStyle} accessibilityLabel={_('Actions')}>
<Icon name="ionicon ellipsis-vertical" style={this.styles().contextMenuTrigger} accessibilityLabel={null}/>
<View style={contextMenuStyle}>
<Icon name="ionicon ellipsis-vertical" style={this.styles().contextMenuTrigger} accessibilityLabel={_('Actions')}/>
</View>
</Menu>
);
// Updating the state of this component can result in the left most element becoming hidden, so add a dummy as the first element to prevent this
// See https://github.com/laurent22/joplin/issues/14153
const zeroWidthSpacer = (
<View style={{ width: 0 }} pointerEvents="none"/>
);
return (
<View style={this.styles().outerContainer}>
<View style={this.styles().aboveHeader}/>
<View style={this.styles().innerContainer}>
{zeroWidthSpacer}
{sideMenuComp}
{backButtonComp}
{renderUndoButton()}

View File

@@ -7,6 +7,7 @@ import AccessibleView from './accessibility/AccessibleView';
import { _ } from '@joplin/lib/locale';
import useReduceMotionEnabled from '../utils/hooks/useReduceMotionEnabled';
import { themeStyle } from './global-style';
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
export enum SideMenuPosition {
Left = 'left',
@@ -40,6 +41,8 @@ interface UseStylesProps {
const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStylesProps) => {
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
const safeAreaInsets = useSafeAreaPadding();
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
@@ -53,7 +56,7 @@ const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStyl
contentOuterWrapper: {
flexGrow: 1,
flexShrink: 1,
width: windowWidth,
width: '100%',
height: windowHeight,
transform: [{
translateX: menuOpenFraction.interpolate({
@@ -71,11 +74,18 @@ const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStyl
flexShrink: 1,
},
menuWrapper: {
backgroundColor: theme.backgroundColor,
position: 'absolute',
top: 0,
bottom: 0,
width: menuWidth,
paddingLeft: isLeftMenu ? safeAreaInsets.paddingLeft : 0,
paddingRight: isLeftMenu ? 0 : safeAreaInsets.paddingRight,
paddingTop: safeAreaInsets.paddingTop,
paddingBottom: safeAreaInsets.paddingBottom,
// In React Native, RTL replaces `left` with `right` and `right` with `left`.
// As such, we need to reverse the normal direction in RTL mode.
...(isLeftMenu === !I18nManager.isRTL ? {
@@ -107,7 +117,7 @@ const useStyles = ({ themeId, isLeftMenu, menuWidth, menuOpenFraction }: UseStyl
width: windowWidth,
},
});
}, [themeId, isLeftMenu, windowWidth, windowHeight, menuWidth, menuOpenFraction]);
}, [themeId, isLeftMenu, windowWidth, windowHeight, menuWidth, menuOpenFraction, safeAreaInsets]);
};
interface UseAnimationsProps {

View File

@@ -2,13 +2,16 @@ import * as React from 'react';
import { connect } from 'react-redux';
import NotesScreen from './screens/Notes/Notes';
import SearchScreen from './screens/SearchScreen';
import { KeyboardAvoidingView, Platform, View } from 'react-native';
import { Platform, View, StyleSheet } from 'react-native';
import { AppState } from '../utils/types';
import { themeStyle } from './global-style';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import useKeyboardState from '../utils/hooks/useKeyboardState';
import usePrevious from '@joplin/lib/hooks/usePrevious';
import FeedbackBanner from './FeedbackBanner';
import { Theme } from '@joplin/lib/themes/type';
import { useMemo } from 'react';
import KeyboardAvoidingView from './KeyboardAvoidingView';
interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -20,6 +23,15 @@ interface Props {
themeId: number;
}
const useStyles = (theme: Theme) => {
return useMemo(() => {
return StyleSheet.create({
keyboardAvoidingView: { flex: 1, backgroundColor: theme.backgroundColor },
});
}, [theme]);
};
const AppNavComponent: React.FC<Props> = (props) => {
const keyboardState = useKeyboardState();
const safeAreaPadding = useSafeAreaInsets();
@@ -50,20 +62,18 @@ const AppNavComponent: React.FC<Props> = (props) => {
const searchScreenLoaded = searchScreenVisible || (previousRouteName === 'Search' && route.routeName === 'Note');
const theme = themeStyle(props.themeId);
const style = { flex: 1, backgroundColor: theme.backgroundColor };
// When the floating keyboard is enabled, the KeyboardAvoidingView can have a very small
// height. Don't use the KeyboardAvoidingView when the floating keyboard is enabled.
// See https://github.com/facebook/react-native/issues/29473
const keyboardAvoidingViewEnabled = !keyboardState.isFloatingKeyboard;
const autocompletionBarPadding = Platform.OS === 'ios' && keyboardState.keyboardVisible ? safeAreaPadding.top : 0;
const styles = useStyles(theme);
const autocompletionBarPadding = keyboardState.keyboardVisible ? safeAreaPadding.top : 0;
return (
<KeyboardAvoidingView
enabled={keyboardAvoidingViewEnabled}
behavior={Platform.OS === 'ios' ? 'padding' : null}
style={style}
style={styles.keyboardAvoidingView}
enabled={
// Workaround: On Android 15 and 16, the main app content seems to auto-resize when the keyboard is shown.
// On earlier Android versions (and in modals), this does not seem to be the case.
(Platform.OS === 'android' && Platform.Version < 35)
|| Platform.OS === 'ios'
}
>
<NotesScreen visible={notesScreenVisible} />
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} />}

View File

@@ -16,6 +16,7 @@ import usePrevious from '@joplin/lib/hooks/usePrevious';
import PlatformImplementation from '../../services/plugins/PlatformImplementation';
import AccessibleView from '../accessibility/AccessibleView';
import useOnDevPluginsUpdated from './utils/useOnDevPluginsUpdated';
import { ViewStyle } from 'react-native';
const logger = Logger.create('PluginRunnerWebView');
@@ -98,6 +99,17 @@ interface Props {
themeId: number;
}
// The WebView needs to have a non-zero size to be rendered by
// newer React Native versions. This style makes it visually hidden.
const hiddenStyle: ViewStyle = {
width: 1,
height: 1,
opacity: 0,
position: 'absolute',
top: 0,
zIndex: -1,
};
const PluginRunnerWebViewComponent: React.FC<Props> = props => {
const webviewRef = useRef<WebViewControl>(null);
@@ -189,7 +201,7 @@ const PluginRunnerWebViewComponent: React.FC<Props> = props => {
};
return (
<AccessibleView style={{ display: 'none' }} inert={true}>
<AccessibleView style={hiddenStyle} inert={true}>
{renderWebView()}
</AccessibleView>
);

View File

@@ -739,6 +739,14 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
return false;
};
private handleSettingButtonPress = async (key: string) => {
if (key === 'sync.6.oidcLogin') {
// Save current settings before navigating to login
await shared.saveSettings(this);
await NavService.go('WebDavOidcLogin');
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public settingToComponent(key: string, value: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -755,6 +763,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
themeId={this.props.themeId}
updateSettingValue={updateSettingValue}
styles={this.styles()}
onSettingButtonPress={this.handleSettingButtonPress}
/>
);
}

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { UpdateSettingValueCallback } from './types';
import { View, Text } from 'react-native';
import { View, Text, Button } from 'react-native';
import Setting, { AppType } from '@joplin/lib/models/Setting';
import Dropdown from '../../Dropdown';
import { ConfigScreenStyles } from './configScreenStyles';
@@ -23,6 +23,7 @@ interface Props {
themeId: number;
updateSettingValue: UpdateSettingValueCallback;
onSettingButtonPress?: (key: string)=> void;
}
@@ -127,7 +128,21 @@ const SettingComponent: React.FunctionComponent<Props> = props => {
/>
);
} else if (md.type === Setting.TYPE_BUTTON) {
// TODO: Not yet supported
return (
<View key={props.settingId} style={containerStyles.outerContainer}>
<View style={containerStyles.innerContainer}>
<Button
title={md.label()}
onPress={() => {
if (props.onSettingButtonPress) {
props.onSettingButtonPress(props.settingId);
}
}}
/>
</View>
{descriptionComp}
</View>
);
} else if (Setting.value('env') === 'dev') {
throw new Error(`Unsupported setting type: ${md.type}`);
}

View File

@@ -5,7 +5,7 @@ import { act, fireEvent, render, screen, userEvent, waitFor } from '../../../uti
import NoteScreen from './Note';
import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, supportDir, synchronizerStart, resourceFetcher, runWithFakeTimers } from '@joplin/lib/testing/test-utils';
import { waitFor as waitForWithRealTimers } from '@joplin/lib/testing/test-utils';
import waitForWithRealTimers from '@joplin/lib/testing/waitFor';
import Note from '@joplin/lib/models/Note';
import { AppState } from '../../../utils/types';
import { Store } from 'redux';

View File

@@ -1,30 +1,43 @@
const React = require('react');
import * as React from 'react';
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;
const { ScreenHeader } = require('../ScreenHeader');
const { BaseScreenComponent } = require('../base-screen');
const shim = require('@joplin/lib/shim').default;
const { _ } = require('@joplin/lib/locale');
const { default: FolderPicker } = require('../FolderPicker');
const TextInput = require('../TextInput').default;
import { View, StyleSheet } from 'react-native';
import { connect } from 'react-redux';
import Folder from '@joplin/lib/models/Folder';
import BaseModel from '@joplin/lib/BaseModel';
import { ScreenHeader } from '../ScreenHeader';
import { BaseScreenComponent } from '../base-screen';
import shim from '@joplin/lib/shim';
import { _ } from '@joplin/lib/locale';
import FolderPicker from '../FolderPicker';
import TextInput from '../TextInput';
import { FolderEntity } from '@joplin/lib/services/database/types';
import { AppState } from '../../utils/types';
import { Dispatch } from 'redux';
class FolderScreenComponent extends BaseScreenComponent {
static navigationOptions() {
return { header: null };
}
interface Props {
folderId: string;
selectedFolderId: string;
themeId: number;
folders: FolderEntity[];
dispatch: Dispatch;
}
constructor() {
super();
interface State {
folder: FolderEntity;
lastSavedFolder: FolderEntity|null;
}
class FolderScreenComponent extends BaseScreenComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
folder: Folder.new(),
lastSavedFolder: null,
};
}
UNSAFE_componentWillMount() {
public override UNSAFE_componentWillMount() {
if (!this.props.folderId) {
const folder = Folder.new();
this.setState({
@@ -33,7 +46,7 @@ class FolderScreenComponent extends BaseScreenComponent {
});
} else {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
Folder.load(this.props.folderId).then(folder => {
void Folder.load(this.props.folderId).then(folder => {
this.setState({
folder: folder,
lastSavedFolder: { ...folder },
@@ -42,38 +55,40 @@ class FolderScreenComponent extends BaseScreenComponent {
}
}
isModified() {
private isModified() {
if (!this.state.folder || !this.state.lastSavedFolder) return false;
const diff = BaseModel.diffObjects(this.state.folder, this.state.lastSavedFolder);
delete diff.type_;
return !!Object.getOwnPropertyNames(diff).length;
}
folderComponent_change(propName, propValue) {
private folderComponent_change(propName: keyof FolderEntity, propValue: string) {
this.setState((prevState) => {
const folder = { ...prevState.folder };
folder[propName] = propValue;
const folder = {
...prevState.folder,
[propName]: propValue,
};
return { folder: folder };
});
}
title_changeText(text) {
private title_changeText(text: string) {
this.folderComponent_change('title', text);
}
parent_changeValue(parent) {
private parent_changeValue(parent: string) {
this.folderComponent_change('parent_id', parent);
}
async saveFolderButton_press() {
private async saveFolderButton_press() {
let folder = { ...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) {
shim.showErrorDialog(_('The notebook could not be saved: %s', error.message));
void shim.showErrorDialog(_('The notebook could not be saved: %s', error.message));
return;
}
@@ -89,7 +104,7 @@ class FolderScreenComponent extends BaseScreenComponent {
});
}
render() {
public override render() {
const saveButtonDisabled = !this.isModified() || !this.state.folder.title;
return (
@@ -101,7 +116,7 @@ class FolderScreenComponent extends BaseScreenComponent {
autoFocus={true}
value={this.state.folder.title}
onChangeText={text => this.title_changeText(text)}
disabled={this.state.folder.encryption_applied}
editable={!this.state.folder.encryption_applied}
/>
<View style={styles.folderPickerContainer}>
<FolderPicker
@@ -120,7 +135,7 @@ class FolderScreenComponent extends BaseScreenComponent {
}
}
const FolderScreen = connect(state => {
export default connect((state: AppState) => {
return {
folderId: state.selectedFolderId,
themeId: state.settings.theme,
@@ -138,4 +153,3 @@ const styles = StyleSheet.create({
},
});
module.exports = { FolderScreen };

View File

@@ -0,0 +1,158 @@
import * as React from 'react';
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { View, Button } from 'react-native';
import { WebView, WebViewNavigation } from 'react-native-webview';
import { useDispatch, useSelector } from 'react-redux';
import { ScreenHeader } from '../ScreenHeader';
import { reg } from '@joplin/lib/registry';
import { _ } from '@joplin/lib/locale';
import { themeStyle } from '../global-style';
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
import OidcApi from '@joplin/lib/OidcApi';
const parseUri = require('@joplin/lib/parseUri');
const WebDavOidcLoginScreen: React.FC = () => {
const dispatch = useDispatch();
const themeId = useSelector((state: { settings: { theme: number } }) => state.settings.theme);
const [webviewUrl, setWebviewUrl] = useState('');
const [oidcApi, setOidcApi] = useState<OidcApi | null>(null);
const [redirectUri, setRedirectUri] = useState('');
const [oauthState, setOauthState] = useState('');
const authCodeRef = useRef<string | null>(null);
const styles = useMemo(() => {
const theme = themeStyle(themeId);
return {
screen: {
flex: 1,
backgroundColor: theme.backgroundColor,
},
};
}, [themeId]);
useEffect(() => {
const initOidc = async () => {
const api = new OidcApi({
issuerUrl: Setting.value('sync.6.oidcIssuerUrl'),
clientId: Setting.value('sync.6.oidcClientId'),
clientSecret: Setting.value('sync.6.oidcClientSecret'),
ignoreTlsErrors: Setting.value('net.ignoreTlsErrors'),
});
// Use a custom redirect URI that the WebView can intercept
// This is a common pattern for mobile OAuth - using a non-http URI
const redirect = 'joplin://oidc-callback';
const state = Math.random().toString(36).substring(7);
const authCodeUrl = await api.authCodeUrl(redirect, state);
setOidcApi(api);
setRedirectUri(redirect);
setOauthState(state);
setWebviewUrl(authCodeUrl);
};
void initOidc();
}, []);
const handleWebviewLoad = useCallback(async (event: WebViewNavigation) => {
const url = event.url;
// Check if this is our callback URL
if (url.startsWith('joplin://oidc-callback')) {
const parsedUrl = parseUri(url);
const query = parsedUrl.queryKey;
if (query.error) {
const errorDesc = query.error_description || query.error;
alert(`${_('Authentication failed')}: ${errorDesc}`);
dispatch({ type: 'NAV_BACK' });
return;
}
if (!authCodeRef.current && query.code) {
// Verify state to prevent CSRF
if (query.state !== oauthState) {
alert(_('Authentication failed: Invalid state parameter'));
dispatch({ type: 'NAV_BACK' });
return;
}
authCodeRef.current = query.code;
try {
await oidcApi.execTokenRequest(authCodeRef.current, redirectUri);
const auth = oidcApi.auth();
const syncTargetId = Setting.value('sync.target');
Setting.setValue(`sync.${syncTargetId}.oidcAuth`, auth ? JSON.stringify(auth) : '');
// Update the sync target's API with the new auth
const syncTarget = reg.syncTarget(syncTargetId);
if (syncTarget.api && syncTarget.api()) {
syncTarget.api().setAuth(auth);
}
dispatch({ type: 'NAV_BACK' });
void reg.scheduleSync(0);
} catch (error) {
alert(`${_('Could not authenticate with OIDC provider. Please try again')}\n\n${(error as Error).message}`);
}
authCodeRef.current = null;
}
}
}, [dispatch, oidcApi, oauthState, redirectUri]);
const handleWebviewError = useCallback(() => {
alert(_('Could not load page. Please check your connection and try again.'));
}, []);
const handleRetryPress = useCallback(() => {
// Reload the page by setting a temporary URL then back to the auth URL
const authUrl = webviewUrl;
setWebviewUrl('about:blank');
shim.setTimeout(() => {
setWebviewUrl(authUrl);
}, 500);
}, [webviewUrl]);
const handleShouldStartLoadWithRequest = useCallback((request: { url: string }) => {
// Intercept the callback URL
if (request.url.startsWith('joplin://oidc-callback')) {
void handleWebviewLoad({ url: request.url } as WebViewNavigation);
return false;
}
return true;
}, [handleWebviewLoad]);
const source = useMemo(() => ({ uri: webviewUrl }), [webviewUrl]);
return (
<View style={styles.screen}>
<ScreenHeader title={_('WebDAV OIDC Login')} />
<WebView
source={source}
onNavigationStateChange={(event: WebViewNavigation) => {
void handleWebviewLoad(event);
}}
onError={handleWebviewError}
onHttpError={handleWebviewError}
// Allow the custom joplin:// scheme to be intercepted
originWhitelist={['*']}
onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest}
/>
<Button
title={_('Refresh')}
onPress={handleRetryPress}
/>
</View>
);
};
export default WebDavOidcLoginScreen;

View File

@@ -6,16 +6,14 @@
// So there's basically still a one way flux: React => SQLite => Redux => React
import './utils/initReact';
import './utils/polyfills';
import Root from './root';
import { LogBox } from 'react-native';
import { registerRootComponent } from 'expo';
// Allows loading image assets. See https://github.com/expo/expo/issues/31240
import 'expo-asset';
import shim from '@joplin/lib/shim';
shim.setReact(require('react'));
const Root = require('./root').default;
// Seems JavaScript developers love adding warnings everywhere, even when these warnings can't be fixed
// or don't really matter. Because we want important warnings to actually be fixed, we disable

View File

@@ -1,4 +1,5 @@
import './utils/polyfills';
import './utils/initReact';
import { AppRegistry } from 'react-native';
import Root from './root';
import Setting from '@joplin/lib/models/Setting';

View File

@@ -35,6 +35,8 @@
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>RCTNewArchEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>

View File

@@ -321,6 +321,8 @@
files = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env",
"$(SRCROOT)/.xcode.env.local",
);
name = "Bundle React Native code and images";
outputPaths = (
@@ -339,12 +341,12 @@
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ReactNativeFs/RNFS_PrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
@@ -358,12 +360,12 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNFS_PrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
@@ -407,11 +409,15 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
@@ -450,11 +456,16 @@
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env",
"$(SRCROOT)/.xcode.env.local",
"$(SRCROOT)/Joplin/Joplin.entitlements",
"$(SRCROOT)/Pods/Target Support Files/Pods-Joplin/expo-configure-project.sh",
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
"$(SRCROOT)/Pods/Target Support Files/Pods-Joplin/ExpoModulesProvider.swift",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -509,7 +520,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 149;
CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist;
@@ -518,7 +529,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 13.6.0;
MARKETING_VERSION = 13.6.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -544,7 +555,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 149;
CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
@@ -552,7 +563,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 13.6.0;
MARKETING_VERSION = 13.6.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -651,6 +662,7 @@
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
};
name = Debug;
@@ -727,6 +739,7 @@
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
@@ -745,7 +758,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 149;
CURRENT_PROJECT_VERSION = 150;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -756,7 +769,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 13.6.0;
MARKETING_VERSION = 13.6.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
@@ -788,7 +801,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 149;
CURRENT_PROJECT_VERSION = 150;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -799,7 +812,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 13.6.0;
MARKETING_VERSION = 13.6.1;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"$(inherited)",

View File

@@ -44,16 +44,15 @@
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
<!-- Left over from before upgrading from RN 0.71, 0.73 -->
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<key>api.joplincloud.local</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>api.joplincloud.local</key>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
@@ -62,18 +61,22 @@
</dict>
<key>NSCameraUsageDescription</key>
<string>To allow attaching a photo to a note</string>
<key>NSFaceIDUsageDescription</key>
<string>$(PRODUCT_NAME) requires FaceID access to secure access to the application</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>To add geo-location information to a note. Can be disabled in app.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>To add geo-location information to a note. Can be disabled in app.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>To add geo-location information to a note. Can be disabled in app.</string>
<key>NSMicrophoneUsageDescription</key>
<string>To allow attaching voice recordings to a note</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>The images will be displayed on your notes.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>To allow attaching images to a note</string>
<key>NSMicrophoneUsageDescription</key>
<string>To allow attaching voice recordings to a note</string>
<key>RCTNewArchEnabled</key>
<true/>
<key>UIAppFonts</key>
<array>
<string>AntDesign.ttf</string>
@@ -86,6 +89,10 @@
<string>MaterialDesignIcons.ttf</string>
<string>MaterialCommunityIcons.ttf</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
@@ -109,11 +116,5 @@
<string>Automatic</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSFaceIDUsageDescription</key>
<string>$(PRODUCT_NAME) requires FaceID access to secure access to the application</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</dict>
</plist>

View File

@@ -5,23 +5,33 @@ require File.join(File.dirname(`node --print "require.resolve('react-native/pack
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
def ccache_enabled?(podfile_properties)
# Environment variable takes precedence
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
# Fall back to Podfile properties
podfile_properties['apple.ccacheEnabled'] == 'true'
end
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
install! 'cocoapods',
:deterministic_uuids => false
prepare_react_native_project!
target 'Joplin' do
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] != '0'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'npx',
'node',
'--no-warnings',
'--eval',
'require(\'expo/bin/autolinking\')',
'expo-modules-autolinking',
'react-native-config',
'--json',
@@ -35,13 +45,14 @@ target 'Joplin' do
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
:app_path => "#{Pod::Config.instance.installation_root}/..",
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
pod 'JoplinRNShareExtension', :path => 'ShareExtension'
post_install do |installer|
@@ -50,7 +61,7 @@ target 'Joplin' do
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
# :ccache_enabled => true
:ccache_enabled => ccache_enabled?(podfile_properties),
)
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
{
"newArchEnabled": "false"
"newArchEnabled": "true"
}

View File

@@ -43,5 +43,7 @@
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
<key>RCTNewArchEnabled</key>
<true/>
</dict>
</plist>

View File

@@ -84,6 +84,7 @@ const emptyMockPackages = [
'@joplin/react-native-saf-x',
'expo-av',
'expo-av/build/Audio',
'expo-image-manipulator',
];
for (const packageName of emptyMockPackages) {
jest.doMock(packageName, () => {
@@ -130,7 +131,7 @@ mockIconLibrary('@react-native-vector-icons/fontawesome5', 'FontAwesome5');
// Use a temporary folder instead.
const tempDirectoryPath = path.join(tmpdir(), `appmobile-test-${uuid.createNano()}`);
jest.doMock('react-native-fs', () => {
jest.doMock('@dr.pogodin/react-native-fs', () => {
return {
CachesDirectoryPath: tempDirectoryPath,
};

View File

@@ -23,6 +23,7 @@ const localPackages = {
'@joplin/tools': path.resolve(__dirname, '../tools/'),
'@joplin/utils': path.resolve(__dirname, '../utils/'),
'@joplin/fork-htmlparser2': path.resolve(__dirname, '../fork-htmlparser2/'),
'@joplin/whisper-voice-typing': path.resolve(__dirname, '../whisper-voice-typing/'),
'@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/'),

View File

@@ -21,25 +21,26 @@
"postinstall": "jetify"
},
"dependencies": {
"@bam.tech/react-native-image-resizer": "3.0.11",
"@dr.pogodin/react-native-fs": "2.36.2",
"@joplin/editor": "~3.6",
"@joplin/lib": "~3.6",
"@joplin/react-native-alarm-notification": "~3.6",
"@joplin/react-native-saf-x": "~3.6",
"@joplin/renderer": "~3.6",
"@joplin/utils": "~3.6",
"@joplin/whisper-voice-typing": "~3.6",
"@js-draw/material-icons": "1.33.0",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/datetimepicker": "8.4.5",
"@react-native-community/datetimepicker": "8.4.7",
"@react-native-community/geolocation": "3.4.0",
"@react-native-community/netinfo": "11.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-documents/picker": "10.1.7",
"@react-native-vector-icons/fontawesome5": "12.3.0",
"@react-native-vector-icons/fontawesome5": "patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch",
"@react-native-vector-icons/get-image": "12.3.0",
"@react-native-vector-icons/ionicons": "12.3.0",
"@react-native-vector-icons/material-design-icons": "12.4.0",
"@react-native-vector-icons/material-icons": "12.4.0",
"@react-native-vector-icons/ionicons": "patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch",
"@react-native-vector-icons/material-design-icons": "patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch",
"@react-native-vector-icons/material-icons": "patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch",
"assert-browserify": "2.0.0",
"buffer": "6.0.3",
"color": "3.2.1",
@@ -47,40 +48,41 @@
"crypto-browserify": "3.12.1",
"deprecated-react-native-prop-types": "5.0.0",
"events": "3.3.0",
"expo": "53.0.23",
"expo-av": "15.1.7",
"expo-camera": "16.1.11",
"expo-local-authentication": "16.0.5",
"expo": "54.0.31",
"expo-av": "16.0.8",
"expo-camera": "17.0.10",
"expo-image-manipulator": "14.0.8",
"expo-local-authentication": "17.0.8",
"js-draw": "1.33.0",
"lodash": "4.17.21",
"md5": "2.3.0",
"path-browserify": "1.0.1",
"prop-types": "15.8.1",
"punycode": "2.3.1",
"react": "19.0.0",
"react-native": "0.79.2",
"react-native-device-info": "14.0.4",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-device-info": "14.1.1",
"react-native-dropdownalert": "5.2.0",
"react-native-exit-app": "2.0.0",
"react-native-file-viewer": "2.1.5",
"react-native-fs": "2.20.0",
"react-native-get-random-values": "1.11.0",
"react-native-image-picker": "8.2.1",
"react-native-localize": "3.5.4",
"react-native-modal-datetime-picker": "18.0.0",
"react-native-nitro-modules": "0.33.2",
"react-native-paper": "5.14.5",
"react-native-popup-menu": "0.17.0",
"react-native-quick-actions": "0.3.13",
"react-native-quick-base64": "2.2.2",
"react-native-quick-crypto": "0.7.17",
"react-native-rsa-native": "2.0.5",
"react-native-safe-area-context": "5.6.1",
"react-native-safe-area-context": "5.6.2",
"react-native-securerandom": "1.0.1",
"react-native-share": "12.2.0",
"react-native-sqlite-storage": "6.0.1",
"react-native-svg": "15.13.0",
"react-native-svg": "15.12.1",
"react-native-url-polyfill": "2.0.0",
"react-native-version-info": "1.1.1",
"react-native-webview": "13.16.0",
"react-native-webview": "13.15.0",
"react-native-zip-archive": "7.0.2",
"react-redux": "8.1.3",
"redux": "4.2.1",
@@ -101,26 +103,26 @@
"@joplin/turndown": "~4.0.80",
"@joplin/turndown-plugin-gfm": "~1.0.62",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@react-native-community/cli": "16.0.3",
"@react-native-community/cli-platform-android": "16.0.3",
"@react-native-community/cli-platform-ios": "16.0.3",
"@react-native/babel-preset": "0.80.1",
"@react-native/metro-config": "0.79.5",
"@react-native/typescript-config": "0.80.2",
"@react-native-community/cli": "20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/babel-preset": "0.81.5",
"@react-native/metro-config": "0.81.5",
"@react-native/typescript-config": "0.81.5",
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
"@testing-library/react-native": "13.2.0",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.14",
"@types/node": "18.19.130",
"@types/react": "19.0.14",
"@types/react": "19.1.10",
"@types/react-redux": "7.1.33",
"@types/serviceworker": "0.0.158",
"@types/serviceworker": "0.0.165",
"@types/tar-stream": "3.1.4",
"babel-jest": "29.7.0",
"babel-loader": "9.1.3",
"babel-plugin-module-resolver": "4.1.0",
"babel-plugin-react-native-web": "0.21.2",
"esbuild": "0.25.11",
"esbuild": "0.25.12",
"fast-deep-equal": "3.1.3",
"fs-extra": "11.3.2",
"gulp": "4.0.2",
@@ -130,11 +132,11 @@
"jsdom": "26.1.0",
"nodemon": "3.1.10",
"punycode": "2.3.1",
"react-dom": "19.0.0",
"react-dom": "19.1.0",
"react-native-web": "0.21.2",
"react-refresh": "0.17.0",
"react-test-renderer": "19.0.0",
"sharp": "0.34.4",
"react-refresh": "0.18.0",
"react-test-renderer": "19.1.0",
"sharp": "0.34.5",
"sqlite3": "5.1.6",
"timers-browserify": "2.0.12",
"ts-jest": "29.4.1",
@@ -147,7 +149,7 @@
"webpack-dev-server": "5.2.2"
},
"engines": {
"node": ">=18"
"node": ">=20"
},
"expo": {
"autolinking": {
@@ -157,7 +159,6 @@
},
"install": {
"exclude": [
"react-native@~0.76.6",
"react-native-reanimated@~3.16.1",
"react-native-gesture-handler@~2.20.0",
"react-native-screens@~4.4.0",

View File

@@ -1,8 +1,5 @@
import * as React from 'react';
import shim from '@joplin/lib/shim';
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
shim.setReact(React);
PerformanceLogger.onAppStartBegin();
import setupQuickActions from './setupQuickActions';
@@ -38,13 +35,14 @@ import Folder from '@joplin/lib/models/Folder';
import NotesScreen from './components/screens/Notes/Notes';
import TagsScreen from './components/screens/tags';
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
const { FolderScreen } = require('./components/screens/folder.js');
import FolderScreen from './components/screens/folder';
import LogScreen from './components/screens/LogScreen';
import StatusScreen from './components/screens/status';
import SearchScreen from './components/screens/SearchScreen';
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
import EncryptionConfigScreen from './components/screens/encryption-config';
import DropboxLoginScreen from './components/screens/dropbox-login.js';
import WebDavOidcLoginScreen from './components/screens/webdav-oidc-login';
import { MenuProvider } from 'react-native-popup-menu';
import SideMenu, { SideMenuPosition } from './components/SideMenu';
import SideMenuContent from './components/side-menu-content';
@@ -56,8 +54,8 @@ import SearchEngine from '@joplin/lib/services/search/SearchEngine';
import { themeStyle } from './components/global-style';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import SyncTargetFilesystem from '@joplin/lib/SyncTargetFilesystem';
const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js');
import SyncTargetNextcloud from '@joplin/lib/SyncTargetNextcloud';
import SyncTargetWebDAV from '@joplin/lib/SyncTargetWebDAV';
const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
import SyncTargetJoplinServerSAML from '@joplin/lib/SyncTargetJoplinServerSAML';
@@ -445,6 +443,10 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
state: 'ready',
});
setTimeout(() => {
perfLogger.mark('Application is ready');
}, 50);
// setTimeout(() => {
// this.props.dispatch({
// type: 'NAV_GO',
@@ -695,12 +697,12 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
let disableSideMenuGestures = this.props.disableSideMenuGestures;
if (this.props.routeName === 'Note') {
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
sideMenuContent = <SideMenuContentNote options={this.props.noteSideMenuOptions}/>;
menuPosition = SideMenuPosition.Right;
} else if (this.props.routeName === 'Config') {
disableSideMenuGestures = true;
} else {
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContent/></SafeAreaView>;
sideMenuContent = <SideMenuContent/>;
}
const appNavInit = {
@@ -711,6 +713,7 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
OneDriveLogin: { screen: OneDriveLoginScreen },
DropboxLogin: { screen: DropboxLoginScreen },
JoplinCloudLogin: { screen: JoplinCloudLoginScreen },
WebDavOidcLogin: { screen: WebDavOidcLoginScreen },
JoplinServerSamlLogin: { screen: SsoLoginScreen(new SamlShared()) },
EncryptionConfig: { screen: EncryptionConfigScreen },
UpgradeSyncTarget: { screen: UpgradeSyncTargetScreen },

View File

@@ -71,7 +71,10 @@ const crypto: Crypto = {
digest: async (algorithm: Digest, data: Uint8Array) => {
const hash = QuickCrypto.createHash(digestNameMap[algorithm]);
hash.update(data);
hash.update(
// Cast: hash.update accepts TypedArrays, despite its declared types
data as unknown as ArrayBuffer,
);
return hash.digest();
},

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