Compare commits

..

83 Commits

Author SHA1 Message Date
Abdullah Atta
006a239680 web: bump version to 2.6.9 2023-11-18 15:13:40 +05:00
Abdullah Atta
c6ed4bcab1 web: allow restoring backups when logged out 2023-11-18 15:13:05 +05:00
Abdullah Atta
b0fe967087 desktop: destroy system tray on toggle off setting 2023-11-18 14:53:14 +05:00
Abdullah Atta
3f32673b0a desktop: permanently disable menu bar 2023-11-18 14:45:17 +05:00
Ammar Ahmed
0525cecaee mobile: bump version code on android 2023-11-18 14:38:37 +05:00
Ammar Ahmed
7d84a9582b mobile: allow restoring backups when logged out 2023-11-18 14:38:37 +05:00
Abdullah Atta
289acd8ab6 web: add cross icon for removing tags 2023-11-18 14:35:57 +05:00
Abdullah Atta
bb6d9f604b web: wait 1s for math to render on pdf print 2023-11-18 14:23:57 +05:00
Abdullah Atta
c20a8232cd core: safe check for IS_DESKTOP_APP 2023-11-18 14:23:57 +05:00
Ammar Ahmed
c886212e89 core: add padding to math block in exports 2023-11-18 14:23:57 +05:00
Abdullah Atta
799ae111ae editor: disable resizing of floating images 2023-11-18 13:05:00 +05:00
Abdullah Atta
2a990979a0 editor: fix drag/drop of task items 2023-11-18 13:04:45 +05:00
Ammar Ahmed
2d1071117b mobile: release v2.6.9 2023-11-18 12:17:27 +05:00
Ammar Ahmed
aefce1ee67 mobile: add support /now /time in title 2023-11-18 12:17:27 +05:00
Ammar Ahmed
64d10f5050 mobile: reupload if file size is zero 2023-11-18 12:17:11 +05:00
Abdullah Atta
95b9edca62 web: check if attachment really exists on insertion 2023-11-18 12:17:11 +05:00
Abdullah Atta
06c8422cbe core: prevent duplicate attachments on backup restore 2023-11-18 12:17:11 +05:00
Abdullah Atta
177d8b5112 web: remove rogue console.log 2023-11-18 10:18:53 +05:00
Abdullah Atta
39afa98fce web: lazily load fs module 2023-11-18 10:17:23 +05:00
Abdullah Atta
5bc1ad4ba5 editor: fix url is undefined error in loading images 2023-11-18 10:16:59 +05:00
Abdullah Atta
0b1223976c web: fix builds 2023-11-18 10:03:14 +05:00
Muhammad Ali
3873758fe5 clipper(web): add fallback height & width for web clip(#3766) 2023-11-18 10:03:14 +05:00
Abdullah Atta
3c876cb1d8 global: update lockfiles 2023-11-18 10:03:13 +05:00
Abdullah Atta
0a1a16c197 editor: show placeholder when downloading image 2023-11-18 10:03:13 +05:00
Ammar Ahmed
3c56325fa6 mobile: fix image download 2023-11-18 10:03:13 +05:00
Abdullah Atta
e5db3353ae editor: improve image loading 2023-11-18 10:03:13 +05:00
Muhammad Ali
3416ba4eaa editor: removed a useState method 2023-11-18 10:03:13 +05:00
Muhammad Ali
cdb69bf553 editor: removed updateImage command from useEffect 2023-11-18 10:03:13 +05:00
Abdullah Atta
1aff6d0d55 editor: sync task list state at the very end 2023-11-18 10:03:13 +05:00
Ammar Ahmed
67b7bdcea3 mobile: update iap library to latest 2023-11-18 10:02:45 +05:00
Abdullah Atta
9b6a6de415 editor: update snapshots 2023-11-17 12:50:43 +05:00
Abdullah Atta
8325246a56 web: reduce network check retries to 3 2023-11-17 11:49:52 +05:00
Abdullah Atta
3d11fb5e3f web: specify if session has expired during login 2023-11-17 08:44:17 +05:00
Abdullah Atta
4627a58a52 web: show tags in readonly mode 2023-11-17 08:34:16 +05:00
Muhammad Ali
e557047af1 web: fix pressing enter to add a new tag from list of suggestion fails (#3548)
* web: added ref to control resturn key func

* web: click should open autosuggestions menu

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-11-17 08:30:13 +05:00
Muhammad Ali
8b4cea7339 editor: make table header row immovable (#3459) 2023-11-16 15:30:32 +05:00
Muhammad Ali
ba5e35ec5c editor: fix resize for floating images (#3643)
* editor: removed maxWidth to make image float resizeable

* editor: fix resize for floating images

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-11-16 15:26:25 +05:00
aryan katiyar
f53817e94d web: add link to source code in Settings (#3683) 2023-11-16 15:18:11 +05:00
Muhammad Ali
846efc5351 editor: use prosemirror plugin for managing task list state (#3252)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-11-16 15:12:19 +05:00
Abdullah Atta
0149d00d64 web: add support for storing attachments in opfs or cache storage 2023-11-16 15:01:48 +05:00
Abdullah Atta
275f9730f1 web: allow downloading unuploading attachments 2023-11-16 15:01:48 +05:00
Abdullah Atta
43bc954989 core: allow uploading attachments that are in 0 notes 2023-11-16 15:01:48 +05:00
Abdullah Atta
c88bc722e1 fs: get rid of localforage and use generic interface 2023-11-16 15:01:48 +05:00
Muhammad Ali
ec6fc6839e editor: support datetime shortcuts in task list title & note title (#3746)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-11-16 14:14:43 +05:00
Abdullah Atta
8383c93292 desktop: only set openAtLogin if not already set 2023-11-16 13:07:21 +05:00
Muhammad Ali
37d8d85935 clipper: delete import css rules after iteration (#3720)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-11-16 13:06:51 +05:00
Muhammad Ali
95db7c6329 desktop: fix minimize to system tray not working immediately after enabling (#3734)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-11-16 13:01:22 +05:00
Muhammad Ali
636783335f desktop: add setting to disable menu bar (#3728)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-11-16 09:51:01 +05:00
Abdullah Atta
f88431ded4 core: handle case where headline is undefined 2023-11-03 08:28:41 +05:00
Abdullah Atta
91b0de0580 web: bump version to 2.6.8 2023-11-02 13:23:00 +05:00
Abdullah Atta
a5704ad75c desktop: do not show auto update toggle for flatpak users 2023-11-02 13:20:51 +05:00
Abdullah Atta
ba8c09a7d9 desktop: update electron to 25.9.3 2023-11-02 13:07:15 +05:00
Abdullah Atta
dbe4015655 web: preserve note edited time when exporting 2023-11-02 12:59:47 +05:00
Abdullah Atta
76237ad7dd web: fix sync progress in account recovery 2023-11-02 10:35:06 +05:00
Abdullah Atta
dbfb1168fb core: fix restored notes not syncing (closes #3472) 2023-11-02 10:34:21 +05:00
Abdullah Atta
830d28fbe5 editor: fix pasting link replaces selected text
fixes #3520
2023-10-31 12:33:49 +05:00
Abdullah Atta
c6def2a8fe web: fix crash on opening editor properties
(fixes #3585)
2023-10-31 12:03:45 +05:00
Abdullah Atta
ebc95fcea7 web: make duplicate filenames in attachments unique on download (#3678)
* web: make duplicate filenames in attachments unique

* Update apps/web/src/utils/streams/attachment-stream.ts

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

* web: make attachment filename case insensitive

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
2023-10-31 11:35:01 +05:00
Abdullah Atta
f78cfe59e8 web: hide note title immediately on setting change 2023-10-31 11:12:11 +05:00
Johnny Avila
a2cbc93d34 web: add setting to allow hiding note title from window title (#3480)
Signed-off-by: Johnny Avila <avila.johnny11@gmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-10-31 11:08:14 +05:00
Muhammad Ali
4728f14e8c core: add year when grouping by month (#3630) 2023-10-31 10:57:52 +05:00
Abdullah Atta
e94b431d58 web: change pwa theme color according to current theme (#3621)
* web: change pwa theme color according to current theme

* web: use background color as pwa theme-color
2023-10-31 10:56:37 +05:00
Muhammad Ali
0ac5e2b995 web: hide unnecessary items from status bar in focus mode (#3577)
* web: used app-store to hide sync-status suring focus mode

* web: hide useless items from status bar in focus mode

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-10-31 10:54:57 +05:00
Muhammad Ali
365764e3e3 web: update vault setting immediately after vault creation (#3542)
* web: added onStateChange function to vault-settings.

* web: fix font style of checkbox in password dialog

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-10-31 10:50:38 +05:00
Ammar Ahmed
8d2b5aa3de mobile: release v2.6.8 2023-10-30 15:24:30 +05:00
Ammar Ahmed
9a031a4a43 mobile: fix selection state causing delete to fail 2023-10-30 14:25:14 +05:00
Ammar Ahmed
d412679fd7 mobile: fix relogin in session expired mode 2023-10-30 11:56:52 +05:00
Ammar Ahmed
22c52d4432 core: reset lastSynced if new login 2023-10-30 11:55:43 +05:00
Ammar Ahmed
9e5bdf9e33 core: fix access token not saved 2023-10-30 11:55:14 +05:00
Abdullah Atta
7253eca5c0 core: do not allow saving empty token response 2023-10-30 10:55:39 +05:00
Abdullah Atta
b1f3dc29c5 core: use single sse command for logging the user out 2023-10-30 10:55:39 +05:00
Abdullah Atta
8e3df8d41b editor: disallow links in inline code 2023-10-30 10:41:04 +05:00
Ammar Ahmed
efad22c00a mobile: fix 24 hour reminder time 2023-10-30 10:26:15 +05:00
Ammar Ahmed
b7690f13b1 mobile: hide pinned notification subtitle 2023-10-30 09:58:52 +05:00
Ammar Ahmed
6e1739da2e mobile: fix note not added to default notebook in widget 2023-10-24 17:35:53 +05:00
Ammar Ahmed
77ab423041 mobile: fix keep in view in editor (#3612) 2023-10-24 16:54:00 +05:00
Ammar Ahmed
96a8e885d3 Fix session expiry dialog not presented when user encryption key is not found (#3625)
* core: fix session expired dialog not shown when
user encryption key does not
exist

* core: add watch command
2023-10-24 15:58:26 +05:00
Ammar Ahmed
2b917c7eca mobile: fix pin input hide behind keyboard on session expiry sheet (#3624) 2023-10-24 15:57:20 +05:00
Ammar Ahmed
942d2f7a4e mobile: fix system bar colors (#3611) 2023-10-24 12:44:41 +05:00
Ammar Ahmed
75142ff1b2 mobile: fix session id mismatch causing editor to get stuck.(#3613) 2023-10-24 09:18:55 +05:00
Abdullah Atta
74172dc3fa docs: update organizing notes using notebooks doc 2023-10-03 14:42:54 +05:00
Abdullah Atta
848fdda8d7 docs: update outdated documentation 2023-10-03 13:44:21 +05:00
Abdullah Atta
3895569a0c setup: make flatpak source generation more robust 2023-10-03 12:02:27 +05:00
162 changed files with 7714 additions and 36543 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "2.6.7",
"version": "2.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "2.6.7",
"version": "2.6.9",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -24,7 +24,7 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "25.8.1",
"electron": "25.9.3",
"electron-builder": "^24.6.4",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
@@ -216,6 +216,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz",
"integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^2.2.0",
@@ -236,6 +237,7 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
@@ -249,6 +251,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
@@ -257,6 +260,7 @@
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -265,6 +269,7 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"engines": {
"node": ">= 4.0.0"
}
@@ -798,6 +803,7 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -809,6 +815,7 @@
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dev": true,
"dependencies": {
"defer-to-connect": "^2.0.0"
},
@@ -853,6 +860,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dev": true,
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
@@ -881,12 +889,14 @@
"node_modules/@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==",
"dev": true
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@@ -900,7 +910,8 @@
"node_modules/@types/node": {
"version": "18.16.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.1.tgz",
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA=="
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==",
"devOptional": true
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.1",
@@ -922,6 +933,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@@ -951,6 +963,7 @@
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
"dev": true,
"optional": true,
"dependencies": {
"@types/node": "*"
@@ -1191,6 +1204,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"dev": true,
"optional": true
},
"node_modules/brace-expansion": {
@@ -1242,6 +1256,7 @@
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"engines": {
"node": "*"
}
@@ -1316,6 +1331,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"dev": true,
"engines": {
"node": ">=10.6.0"
}
@@ -1324,6 +1340,7 @@
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"dev": true,
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
@@ -1443,6 +1460,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"dev": true,
"dependencies": {
"mimic-response": "^1.0.0"
},
@@ -1586,6 +1604,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"dependencies": {
"mimic-response": "^3.1.0"
},
@@ -1600,6 +1619,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -1611,6 +1631,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"dev": true,
"engines": {
"node": ">=10"
}
@@ -1619,6 +1640,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
"integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
"dev": true,
"optional": true,
"dependencies": {
"has-property-descriptors": "^1.0.0",
@@ -1644,6 +1666,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"dev": true,
"optional": true
},
"node_modules/dir-compare": {
@@ -1751,9 +1774,10 @@
}
},
"node_modules/electron": {
"version": "25.8.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.1.tgz",
"integrity": "sha512-GtcP1nMrROZfFg0+mhyj1hamrHvukfF6of2B/pcWxmWkd5FVY1NJib0tlhiorFZRzQN5Z+APLPr7aMolt7i2AQ==",
"version": "25.9.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.3.tgz",
"integrity": "sha512-dacaHg/PuwVcFRgPDCM5j7UDzqGJWOsbBRdS5wPKLNS/ejPeccIjuNUT1cqcrpvCJKAFW8swHWg9kdizNSEDHQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@electron/get": "^2.0.0",
@@ -1920,6 +1944,7 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"dependencies": {
"once": "^1.4.0"
}
@@ -1928,6 +1953,7 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -1951,6 +1977,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"dev": true,
"optional": true
},
"node_modules/esbuild": {
@@ -2002,6 +2029,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"optional": true,
"engines": {
"node": ">=10"
@@ -2027,6 +2055,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
@@ -2067,6 +2096,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"dependencies": {
"pend": "~1.2.0"
}
@@ -2183,7 +2213,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"devOptional": true
"dev": true
},
"node_modules/get-caller-file": {
"version": "2.0.5",
@@ -2197,6 +2227,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"dev": true,
"optional": true,
"dependencies": {
"function-bind": "^1.1.1",
@@ -2212,6 +2243,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"dependencies": {
"pump": "^3.0.0"
},
@@ -2280,6 +2312,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"dev": true,
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
@@ -2297,6 +2330,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
"integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
"dev": true,
"optional": true,
"dependencies": {
"define-properties": "^1.1.3"
@@ -2312,6 +2346,7 @@
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"dev": true,
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
@@ -2341,7 +2376,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"devOptional": true,
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
@@ -2362,6 +2397,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
"integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
"dev": true,
"optional": true,
"dependencies": {
"get-intrinsic": "^1.1.1"
@@ -2374,6 +2410,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"dev": true,
"optional": true,
"engines": {
"node": ">= 0.4"
@@ -2386,6 +2423,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"optional": true,
"engines": {
"node": ">= 0.4"
@@ -2409,7 +2447,8 @@
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
@@ -2429,6 +2468,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"dev": true,
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
@@ -2691,7 +2731,8 @@
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
@@ -2709,6 +2750,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true,
"optional": true
},
"node_modules/json5": {
@@ -2738,6 +2780,7 @@
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
"integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
"dev": true,
"dependencies": {
"json-buffer": "3.0.1"
}
@@ -2773,6 +2816,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -2792,6 +2836,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"dev": true,
"optional": true,
"dependencies": {
"escape-string-regexp": "^4.0.0"
@@ -2837,6 +2882,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -2959,6 +3005,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -2970,6 +3017,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"optional": true,
"engines": {
"node": ">= 0.4"
@@ -2979,6 +3027,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -2987,6 +3036,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -3066,7 +3116,8 @@
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -3105,6 +3156,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@@ -3126,6 +3178,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@@ -3144,6 +3197,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -3338,12 +3392,14 @@
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dev": true,
"dependencies": {
"lowercase-keys": "^2.0.0"
},
@@ -3379,6 +3435,7 @@
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"dev": true,
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
@@ -3458,12 +3515,14 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true,
"optional": true
},
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"dev": true,
"optional": true,
"dependencies": {
"type-fest": "^0.13.1"
@@ -3479,6 +3538,7 @@
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=10"
@@ -3599,6 +3659,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
"dev": true,
"optional": true
},
"node_modules/stat-mode": {
@@ -3671,6 +3732,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"dependencies": {
"debug": "^4.1.0"
},
@@ -3940,7 +4002,8 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/xmlbuilder": {
"version": "15.1.1",
@@ -3993,6 +4056,7 @@
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.6.7",
"version": "2.6.9",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
@@ -25,7 +25,7 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "25.8.1",
"electron": "25.9.3",
"electron-builder": "^24.6.4",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",

View File

@@ -29,6 +29,8 @@ import { dirname } from "path";
import { resolvePath } from "../utils/resolve-path";
import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
const t = initTRPC.create();
@@ -44,6 +46,8 @@ const NotificationOptions = z.object({
});
export const osIntegrationRouter = t.router({
isFlatpak: t.procedure.query(() => isFlatpak()),
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
globalThis.window?.webContents.setZoomFactor(factor);
@@ -79,6 +83,7 @@ export const osIntegrationRouter = t.router({
AutoLaunch.disable();
}
config.desktopSettings = settings;
setupDesktopIntegration(settings);
}),
selectDirectory: t.procedure

View File

@@ -24,7 +24,6 @@ import { configureAutoUpdater } from "./utils/autoupdater";
import { getBackgroundColor, getTheme, setTheme } from "./utils/theme";
import { setupMenu } from "./utils/menu";
import { WindowState } from "./utils/window-state";
import { AutoLaunch } from "./utils/autolaunch";
import { setupJumplist } from "./utils/jumplist";
import { setupTray } from "./utils/tray";
import { CLIOptions, parseArguments } from "./cli";
@@ -35,6 +34,7 @@ import { config } from "./utils/config";
import path from "path";
import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -77,7 +77,7 @@ async function createWindow() {
darkTheme: getTheme() === "dark",
backgroundColor: getBackgroundColor(),
opacity: 0,
autoHideMenuBar: true,
autoHideMenuBar: false,
icon: AssetManager.appIcon({
size: 512,
format: process.platform === "win32" ? "ico" : "png"
@@ -93,6 +93,7 @@ async function createWindow() {
createIPCHandler({ router, windows: [mainWindow] });
globalThis.window = mainWindow;
mainWindow.setMenuBarVisibility(false);
mainWindowState.manage(mainWindow);
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
@@ -106,7 +107,7 @@ async function createWindow() {
}
await AssetManager.loadIcons();
setupDesktopIntegration();
setupDesktopIntegration(config.desktopSettings);
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
@@ -176,49 +177,3 @@ function createURL(options: CLIOptions, path = "/") {
return url;
}
function setupDesktopIntegration() {
const desktopIntegration = config.desktopSettings;
if (
desktopIntegration.closeToSystemTray ||
desktopIntegration.minimizeToSystemTray
) {
setupTray();
}
// when close to system tray is enabled, it becomes nigh impossible
// to "quit" the app. This is necessary in order to fix that.
if (desktopIntegration.closeToSystemTray) {
app.on("before-quit", () => app.exit(0));
}
globalThis.window?.on("close", (e) => {
if (config.desktopSettings.closeToSystemTray) {
e.preventDefault();
if (process.platform == "darwin") {
// on macOS window cannot be minimized/hidden if it is already fullscreen
// so we just close it.
if (globalThis.window?.isFullScreen()) app.exit(0);
else app.hide();
} else {
globalThis.window?.minimize();
globalThis.window?.hide();
}
}
});
globalThis.window?.on("minimize", () => {
if (config.desktopSettings.minimizeToSystemTray) {
if (process.platform == "darwin") {
app.hide();
} else {
globalThis.window?.hide();
}
}
});
if (desktopIntegration.autoStart) {
AutoLaunch.enable(!!desktopIntegration.startMinimized);
}
}

View File

@@ -35,6 +35,8 @@ const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
"autostart"
);
const STARTUP_ARGS = ["--hidden"];
export class AutoLaunch {
static enable(hidden: boolean) {
if (process.platform === "linux") {
@@ -47,10 +49,14 @@ export class AutoLaunch {
LINUX_DESKTOP_ENTRY(hidden)
);
} else {
const loginItemSettings = app.getLoginItemSettings({
args: STARTUP_ARGS
});
if (loginItemSettings.openAtLogin) return;
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: hidden,
args: ["--hidden"]
args: STARTUP_ARGS
});
}
}

View File

@@ -0,0 +1,74 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { app } from "electron";
import { DesktopIntegration, config } from "./config";
import { setupTray, destroyTray } from "./tray";
import { AutoLaunch } from "./autolaunch";
export function setupDesktopIntegration(
desktopIntegration: DesktopIntegration
) {
if (
desktopIntegration.closeToSystemTray ||
desktopIntegration.minimizeToSystemTray
) {
setupTray();
} else {
destroyTray();
}
// when close to system tray is enabled, it becomes nigh impossible
// to "quit" the app. This is necessary in order to fix that.
app.on("before-quit", () =>
desktopIntegration.closeToSystemTray ? app.exit(0) : null
);
globalThis.window?.on("close", (e) => {
if (config.desktopSettings.closeToSystemTray) {
e.preventDefault();
if (process.platform == "darwin") {
// on macOS window cannot be minimized/hidden if it is already fullscreen
// so we just close it.
if (globalThis.window?.isFullScreen()) app.exit(0);
else app.hide();
} else {
try {
globalThis.window?.minimize();
globalThis.window?.hide();
} catch (error) {
console.error(error);
}
}
}
});
globalThis.window?.on("minimize", () => {
if (config.desktopSettings.minimizeToSystemTray) {
if (process.platform == "darwin") {
app.hide();
} else {
globalThis.window?.hide();
}
}
});
if (desktopIntegration.autoStart) {
AutoLaunch.enable(!!desktopIntegration.startMinimized);
}
}

View File

@@ -96,16 +96,22 @@ export async function downloadFile(filename, data, cancelToken) {
}
export async function getUploadedFileSize(hash) {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.user.tokenManager.getAccessToken();
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.user.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(attachmentInfo.headers?.get("content-length"));
return isNaN(contentLength) ? 0 : contentLength;
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
return 0;
}
}
export async function checkAttachment(hash) {

View File

@@ -91,6 +91,7 @@ const AuthModal = () => {
style={{
width: "100%"
}}
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
{currentAuthMode !== AuthMode.login ? (

View File

@@ -69,7 +69,8 @@ export const SessionExpired = () => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
}
},
true
);
const logout = async () => {
@@ -142,9 +143,9 @@ export const SessionExpired = () => {
passwordInputRef.current?.focus();
setFocused(true);
}}
enableSheetKeyboardHandler={true}
visible={true}
>
<SheetProvider context="two_factor_verify" />
<View
style={{
width: focused ? "100%" : "99.9%",
@@ -237,6 +238,8 @@ export const SessionExpired = () => {
</View>
<Toast context="local" />
<Dialog context="session_expiry" />
<SheetProvider context="two_factor_verify" />
</BaseDialog>
)
);

View File

@@ -33,7 +33,7 @@ export const LoginSteps = {
passwordAuth: 3
};
export const useLogin = (onFinishLogin) => {
export const useLogin = (onFinishLogin, sessionExpired = false) => {
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
@@ -69,6 +69,7 @@ export const useLogin = (onFinishLogin) => {
switch (step) {
case LoginSteps.emailAuth: {
const mfaInfo = await db.user.authenticateEmail(email.current);
console.log("email auth", mfaInfo);
if (mfaInfo) {
TwoFactorVerification.present(async (mfa, callback) => {
try {
@@ -101,7 +102,12 @@ export const useLogin = (onFinishLogin) => {
break;
}
case LoginSteps.passwordAuth: {
await db.user.authenticatePassword(email.current, password.current);
await db.user.authenticatePassword(
email.current,
password.current,
null,
sessionExpired
);
finishLogin();
break;
}

View File

@@ -51,7 +51,8 @@ const BaseDialog = ({
bounce = true,
closeOnTouch = true,
useSafeArea = true,
avoidKeyboardResize = false
avoidKeyboardResize = false,
enableSheetKeyboardHandler = false
}) => {
const floating = useIsFloatingKeyboard();
const appState = useAppState();
@@ -103,7 +104,9 @@ const BaseDialog = ({
if (lockEvents.current) return;
if (onShow) {
onShow();
useSettingStore.getState().setSheetKeyboardHandler(false);
if (!enableSheetKeyboardHandler) {
useSettingStore.getState().setSheetKeyboardHandler(false);
}
}
}}
animationType={animation}

View File

@@ -43,6 +43,9 @@ const SelectionWrapper = ({
const onLongPress = () => {
if (!useSelectionStore.getState().selectionMode) {
useSelectionStore.setState({
selectedItemsList: []
});
useSelectionStore.getState().setSelectionMode(true);
}
useSelectionStore.getState().setSelectedItem(item);

View File

@@ -165,7 +165,12 @@ export const Component = ({ close, promo }) => {
}}
size={SIZE.md}
>
({pricing?.product?.localizedPrice} / mo)
(
{Platform.OS === "android"
? pricing.product?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0].formattedPrice
: pricing.product?.localizedPrice}{" "}
/ mo)
</Paragraph>
)}

View File

@@ -18,13 +18,27 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { View } from "react-native";
import { Platform, View } from "react-native";
import { SIZE } from "../../utils/size";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import RNIap from "react-native-iap";
export const PricingItem = ({ product, onPress, compact }) => {
export const PricingItem = ({
product,
onPress,
compact
}: {
product: {
type: "yearly" | "monthly";
data?: RNIap.Subscription;
info: string;
offerType?: "yearly" | "monthly";
};
onPress?: () => void;
compact?: boolean;
}) => {
return (
<PressableButton
onPress={onPress}
@@ -54,7 +68,13 @@ export const PricingItem = ({ product, onPress, compact }) => {
<View>
<Paragraph size={SIZE.sm}>
<Heading size={SIZE.lg - 2}>{product?.data?.localizedPrice}/</Heading>
<Heading size={SIZE.lg - 2}>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0].formattedPrice
: (product.data as RNIap.SubscriptionIOS)?.localizedPrice}
</Heading>
{product?.type === "yearly" || product?.offerType === "yearly"
? "/year"
: "/month"}

View File

@@ -47,11 +47,15 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import { PricingItem } from "./pricing-item";
import { useSettingStore } from "../../stores/use-setting-store";
const promoCyclesMonthly = {
1: "first month",
2: "first 2 months",
3: "first 3 months"
3: "first 3 months",
4: "first 4 months",
5: "first 5 months",
6: "first 3 months"
};
const promoCyclesYearly = {
@@ -65,10 +69,24 @@ export const PricingPlans = ({
marginTop,
heading = true,
compact = false
}: {
promo?: {
promoCode: string;
};
marginTop?: any;
heading?: boolean;
compact?: boolean;
}) => {
const { colors } = useThemeColors();
const user = useUserStore((state) => state.user);
const [product, setProduct] = useState(null);
const [product, setProduct] = useState<{
type: string;
offerType: "monthly" | "yearly";
data: RNIap.Subscription;
cycleText: string;
info: string;
}>();
const [buying, setBuying] = useState(false);
const [loading, setLoading] = useState(false);
const userCanRequestTrial =
@@ -90,27 +108,40 @@ export const PricingPlans = ({
}
}, [promo?.promoCode]);
const getPromo = async (code) => {
const getPromo = async (code: string) => {
try {
let productId;
let skuId: string;
if (code.startsWith("com.streetwriters.notesnook")) {
productId = code;
skuId = code;
} else {
productId = await db.offers.getCode(code.split(":")[0], Platform.OS);
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
}
let products = await PremiumService.getProducts();
let product = products.find((p) => p.productId === productId);
const products = await PremiumService.getProducts();
const product = products.find((p) => p.productId === skuId);
if (!product) return false;
let isMonthly = product.productId.indexOf(".mo") > -1;
let cycleText = isMonthly
const isMonthly = product.productId.indexOf(".mo") > -1;
const cycleText = isMonthly
? promoCyclesMonthly[
product.introductoryPriceCyclesAndroid ||
product.introductoryPriceNumberOfPeriodsIOS
(Platform.OS === "android"
? (product as RNIap.SubscriptionAndroid)
.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0].billingCycleCount
: parseInt(
(product as RNIap.SubscriptionIOS)
.introductoryPriceNumberOfPeriodsIOS as string
)) as keyof typeof promoCyclesMonthly
]
: promoCyclesYearly[
product.introductoryPriceCyclesAndroid ||
product.introductoryPriceNumberOfPeriodsIOS
(Platform.OS === "android"
? (product as RNIap.SubscriptionAndroid)
.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0].billingCycleCount
: parseInt(
(product as RNIap.SubscriptionIOS)
.introductoryPriceNumberOfPeriodsIOS as string
)) as keyof typeof promoCyclesYearly
];
setProduct({
@@ -118,7 +149,7 @@ export const PricingPlans = ({
offerType: isMonthly ? "monthly" : "yearly",
data: product,
cycleText: cycleText,
info: "Pay monthly, cancel anytime"
info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
});
return true;
} catch (e) {
@@ -131,22 +162,37 @@ export const PricingPlans = ({
getSkus();
}, [getSkus]);
const buySubscription = async (product) => {
if (buying) return;
const buySubscription = async (product: RNIap.Subscription) => {
if (buying || !product) return;
setBuying(true);
try {
if (!user) {
setBuying(false);
return;
}
await RNIap.requestSubscription(
product?.productId,
false,
null,
-1,
user.id,
user.id
);
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
const androidOfferToken =
Platform.OS === "android"
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
.offerToken
: null;
await RNIap.requestSubscription({
sku: product?.productId,
obfuscatedAccountIdAndroid: user.id,
obfuscatedProfileIdAndroid: user.id,
appAccountToken: user.id,
andDangerouslyFinishTransactionAutomaticallyIOS: false,
subscriptionOffers: androidOfferToken
? [
{
offerToken: androidOfferToken,
sku: product?.productId
}
]
: undefined
});
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
setBuying(false);
eSendEvent(eCloseSheet);
eSendEvent(eClosePremiumDialog);
@@ -198,7 +244,14 @@ export const PricingPlans = ({
}}
size={SIZE.lg}
>
{PremiumService.getMontlySub().localizedPrice} / mo
{(Platform.OS === "android"
? (monthlyPlan?.product as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0].formattedPrice
: (monthlyPlan?.product as RNIap.SubscriptionIOS)
.localizedPrice) ||
(PremiumService.getMontlySub() as any).localizedPrice}
/ mo
</Paragraph>
<Button
onPress={() => {
@@ -218,7 +271,7 @@ export const PricingPlans = ({
<Button
onPress={async () => {
try {
await db.user.activateTrial();
await db.user?.activateTrial();
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseSheet);
await sleep(300);
@@ -247,7 +300,11 @@ export const PricingPlans = ({
}}
size={SIZE.lg - 4}
>
{product.data.introductoryPrice}
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0].formattedPrice
: (product.data as RNIap.SubscriptionIOS)?.introductoryPrice}
<Paragraph
style={{
textDecorationLine: "line-through",
@@ -255,7 +312,11 @@ export const PricingPlans = ({
}}
size={SIZE.sm}
>
({product.data.localizedPrice})
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[1].pricingPhases
.pricingPhaseList?.[1].formattedPrice
: (product.data as RNIap.SubscriptionIOS)?.localizedPrice}
</Paragraph>{" "}
for {product.cycleText}
</Heading>
@@ -263,9 +324,9 @@ export const PricingPlans = ({
{user && !product ? (
<>
{heading || monthlyPlan?.info?.discount > 0 ? (
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
<>
{monthlyPlan && monthlyPlan?.info?.discount > 0 ? (
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
<View
style={{
alignSelf: "center",
@@ -300,7 +361,10 @@ export const PricingPlans = ({
}}
>
<PricingItem
onPress={() => buySubscription(monthlyPlan?.product)}
onPress={() => {
if (!monthlyPlan?.product) return;
buySubscription(monthlyPlan?.product);
}}
compact={compact}
product={{
type: "monthly",
@@ -319,7 +383,10 @@ export const PricingPlans = ({
)}
<PricingItem
onPress={() => buySubscription(yearlyPlan?.product)}
onPress={() => {
if (!yearlyPlan?.product) return;
buySubscription(yearlyPlan?.product);
}}
compact={compact}
product={{
type: "yearly",
@@ -346,7 +413,7 @@ export const PricingPlans = ({
eSendEvent(eCloseSimpleDialog);
setBuying(true);
try {
if (!(await getPromo(value)))
if (!(await getPromo(value as string)))
throw new Error("Error applying promo code");
ToastEvent.show({
heading: "Discount applied!",
@@ -358,7 +425,7 @@ export const PricingPlans = ({
setBuying(false);
ToastEvent.show({
heading: "Promo code invalid or expired",
message: e.message,
message: (e as Error).message,
type: "error",
context: "local"
});
@@ -427,7 +494,10 @@ export const PricingPlans = ({
) : (
<>
<Button
onPress={() => buySubscription(product.data)}
onPress={() => {
if (!product?.data) return;
buySubscription(product.data);
}}
height={40}
width="50%"
type="accent"
@@ -436,7 +506,7 @@ export const PricingPlans = ({
<Button
onPress={() => {
setProduct(null);
setProduct(undefined);
}}
style={{
marginTop: 5
@@ -523,7 +593,7 @@ export const PricingPlans = ({
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos", colors)
openLinkInBrowser("https://notesnook.com/tos")
.catch(() => {})
.then(() => {});
}}
@@ -538,7 +608,7 @@ export const PricingPlans = ({
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy", colors)
openLinkInBrowser("https://notesnook.com/privacy")
.catch(() => {})
.then(() => {});
}}

View File

@@ -531,9 +531,7 @@ export default function ReminderSheet({
androidVariant="nativeAndroid"
is24hourSource="locale"
locale={
db.settings?.getTimeFormat() === "24-hour"
? "en_GB.UTF8"
: "en_US.UTF8"
db.settings?.getTimeFormat() === "24-hour" ? "en_GB" : "en_US"
}
mode={reminderMode === ReminderModes.Repeat ? "time" : "datetime"}
/>

View File

@@ -188,7 +188,7 @@ export const useActions = ({ close = () => null, item }) => {
Notifications.displayNotification({
title: item.title,
message: item.headline || text,
subtitle: item.headline || text,
subtitle: "",
bigText: html,
ongoing: true,
actions: ["UNPIN"],

View File

@@ -229,12 +229,25 @@ async function saveEditorState() {
MMKV.setString("appState", state);
}
}
/**
*
* @param {RNIap.Purchase} subscription
*/
const onSuccessfulSubscription = async (subscription) => {
console.log(
"Subscription success!",
subscription.transactionId,
subscription.obfuscatedAccountIdAndroid,
subscription.obfuscatedProfileIdAndroid
);
await PremiumService.subscriptions.set(subscription);
await PremiumService.subscriptions.verify(subscription);
};
/**
*
* @param {RNIap.PurchaseError} error
*/
const onSubscriptionError = async (error) => {
ToastEvent.show({
heading: "Failed to subscribe",
@@ -490,9 +503,9 @@ export const useAppEvents = () => {
);
const subscribeToIAPListeners = useCallback(async () => {
await RNIap.initConnection()
.catch(() => null)
.then(async () => {
RNIap.flushFailedPurchasesCachedAsPendingAndroid()
.catch(() => {})
.then(() => {
refValues.current.subsriptionSuccessListener =
RNIap.purchaseUpdatedListener(onSuccessfulSubscription);
refValues.current.subsriptionErrorListener =

View File

@@ -384,7 +384,16 @@ const _TabsHolder = () => {
}, []);
useEffect(() => {
changeNavigationBarColor(colors.primary.background, isDark, true);
function updateSystemBars() {
changeNavigationBarColor(colors.primary.background, isDark, true);
StatusBar.setBackgroundColor("transparent");
StatusBar.setTranslucent(true);
StatusBar.setBarStyle(isDark ? "light-content" : "dark-content");
}
updateSystemBars();
setTimeout(() => {
updateSystemBars();
}, 1000);
}, [colors.primary.background, isDark]);
return (
@@ -406,11 +415,7 @@ const _TabsHolder = () => {
: 0
}}
>
<StatusBar
barStyle={isDark ? "light-content" : "dark-content"}
translucent={true}
backgroundColor="transparent"
/>
<StatusBar translucent={true} backgroundColor="transparent" />
{!introCompleted ? (
<NavigationStack />

View File

@@ -115,7 +115,7 @@ const Editor = React.memo(
} else {
editor.commands.updateImage({
hash: hash,
src: src
dataurl: src
});
}
},
@@ -167,7 +167,7 @@ const Editor = React.memo(
globalThis.noToolbar=${noToolbar};
globalThis.noHeader=${noHeader};
`}
injectedJavaScript={`globalThis.sessionId="${editor.sessionId}";`}
useSharedProcessPool={false}
javaScriptEnabled={true}
focusable={true}
onContentProcessDidTerminate={onError}

View File

@@ -113,7 +113,7 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
};
setSessionId = async (id: string | null) =>
await this.doAsync(`globalThis.sessionId = "${id}"`);
await this.doAsync(`globalThis.sessionId = "${id}";`);
setStatus = async (date: string | undefined, saved: string) =>
await this.doAsync(
@@ -211,9 +211,20 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
);
};
insertImage = async (image: ImageAttributes) => {
insertImage = async (
image: Omit<ImageAttributes, "bloburl"> & {
dataurl: string;
}
) => {
await this.doAsync(
`editor && editor.commands.insertImage(${JSON.stringify(image)})`
`const image = toBlobURL("${image.dataurl}", "${image.hash}");
editor && editor.commands.insertImage({
...${JSON.stringify({
...image,
dataurl: undefined
})},
bloburl: image
})`
);
};
@@ -225,11 +236,20 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
);
};
updateImage = async ({ src, hash }: Partial<ImageAttributes>) => {
updateImage = async ({
hash,
dataurl
}: Partial<Omit<ImageAttributes, "bloburl">> & {
dataurl: string;
}) => {
await this.doAsync(
`editor && editor.commands.updateImage(${JSON.stringify({
`const image = toBlobURL("${dataurl}", "${hash}");
editor && editor.commands.updateImage(${JSON.stringify({
hash
})},${JSON.stringify({ dataurl: src, hash, preventUpdate: true })})`
})}, {
...${JSON.stringify({ hash, preventUpdate: true })},
bloburl: image
})`
);
};

View File

@@ -35,6 +35,7 @@ import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
import { eCloseSheet } from "../../../utils/events";
import { editorController, editorState } from "./utils";
import { useSettingStore } from "../../../stores/use-setting-store";
import filesystem from "../../../common/filesystem";
const showEncryptionSheet = (file) => {
presentSheet({
@@ -230,6 +231,16 @@ const handleImageResponse = async (response, options) => {
});
let fileName = image.originalFileName || image.fileName;
editorController.current?.commands.insertImage({
hash: hash,
mime: image.type,
title: fileName,
dataurl: b64,
size: image.fileSize,
filename: fileName
});
if (!(await attachFile(uri, hash, image.type, fileName, options))) return;
const isPng = /(png)/g.test(image.type);
const isJpeg = /(jpeg|jpg)/g.test(image.type);
@@ -243,15 +254,6 @@ const handleImageResponse = async (response, options) => {
}
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
editorController.current?.commands.insertImage({
hash: hash,
mime: image.type,
title: fileName,
dataurl: b64,
size: image.fileSize,
filename: fileName
});
};
export async function attachFile(uri, hash, type, filename, options) {
@@ -269,6 +271,10 @@ export async function attachFile(uri, hash, type, filename, options) {
return false;
}
if (!options.reupload && exists) {
options.reupload = (await filesystem.getUploadedFileSize(hash)) <= 0;
}
if (!exists || options?.reupload) {
let key = await db.attachments.generateKey();
encryptionInfo = await Sodium.encryptFile(key, {

View File

@@ -205,7 +205,6 @@ export const useEditorEvents = (
fullscreen,
isPremium,
readonly,
editor.sessionId,
editor.loading,
deviceMode,
tools,
@@ -229,6 +228,8 @@ export const useEditorEvents = (
logger.info("editor handled back event", editorHandledBack);
return;
}
editorState().currentlyEditing = false;
editor.reset();
setTimeout(async () => {
if (deviceMode !== "mobile" && fullscreen) {
if (fullscreen) {
@@ -241,14 +242,10 @@ export const useEditorEvents = (
editorState().movedAway = true;
tabBarRef.current?.goToPage(0);
}
setImmediate(() => {
useEditorStore.getState().setCurrentlyEditingNote(null);
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 500);
});
editorState().currentlyEditing = false;
editor.reset();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 500);
}, 1);
}, [editor, deviceMode, fullscreen]);
@@ -344,8 +341,12 @@ export const useEditorEvents = (
});
}
if (editorMessage.type === EventTypes.back) {
return onBackPress();
}
if (
editorMessage.sessionId !== editor.sessionId &&
editorMessage.sessionId !== editor.sessionId.current &&
editorMessage.type !== EditorEvents.status
) {
return;
@@ -431,9 +432,6 @@ export const useEditorEvents = (
editorState().isFullscreen = true;
eSendEvent(eOpenFullscreenEditor);
break;
case EventTypes.back:
onBackPress();
break;
case EventTypes.link:
openLinkInBrowser(editorMessage.value as string);
break;

View File

@@ -63,8 +63,7 @@ export const useEditor = (
const theme = useThemeEngineStore((state) => state.theme);
const [loading, setLoading] = useState(false);
const [sessionId, setSessionId] = useState<string>(makeSessionId());
const sessionIdRef = useRef(sessionId);
const sessionIdRef = useRef(makeSessionId());
const editorRef = useRef<WebView>(null);
const currentNote = useRef<NoteType | null>();
const currentContent = useRef<Content | null>();
@@ -99,10 +98,6 @@ export const useEditor = (
postMessage(EditorEvents.theme, theme);
}, [theme, postMessage]);
useEffect(() => {
sessionIdRef.current = sessionId;
}, [sessionId]);
useEffect(() => {
commands.setTags(currentNote.current);
}, [commands, tags]);
@@ -142,6 +137,7 @@ export const useEditor = (
clearTimeout(timers.current["loading-images"]);
sessionHistoryId.current = undefined;
saveCount.current = 0;
loadingState.current = undefined;
lock.current = false;
useEditorStore.getState().setReadonly(false);
resetContent && postMessage(EditorEvents.title, "");
@@ -151,8 +147,8 @@ export const useEditor = (
if (resetState) {
const newSessionId = makeSessionId();
sessionIdRef.current = newSessionId;
await commands.setSessionId(newSessionId);
setSessionId(newSessionId);
isDefaultEditor &&
useEditorStore.getState().setCurrentlyEditingNote(null);
@@ -363,7 +359,6 @@ export const useEditor = (
if (item && item.type === "new") {
currentNote.current && (await reset());
const nextSessionId = makeSessionId(item as NoteType);
setSessionId(nextSessionId);
sessionIdRef.current = nextSessionId;
sessionHistoryId.current = Date.now();
await commands.setSessionId(nextSessionId);
@@ -405,12 +400,10 @@ export const useEditor = (
}
lastContentChangeTime.current = item.dateEdited;
const nextSessionId = makeSessionId(item as NoteType);
sessionIdRef.current = nextSessionId;
lockedSessionId.current = nextSessionId;
sessionHistoryId.current = Date.now();
setSessionId(nextSessionId);
commands.setSessionId(nextSessionId);
sessionIdRef.current = nextSessionId;
await commands.setSessionId(nextSessionId);
currentNote.current = item as NoteType;
await commands.setStatus(getFormattedDate(item.dateEdited), "Saved");
await postMessage(EditorEvents.title, item.title);
@@ -638,7 +631,7 @@ export const useEditor = (
useEffect(() => {
state.current.saveCount = 0;
}, [sessionId, loading]);
}, [loading]);
const onReady = useCallback(async () => {
if (!(await isEditorLoaded(editorRef, sessionIdRef.current))) {
@@ -690,8 +683,7 @@ export const useEditor = (
loading,
setLoading,
state,
sessionId,
setSessionId,
sessionId: sessionIdRef,
note: currentNote,
onReady,
saveContent,

View File

@@ -88,8 +88,8 @@ export const settingsGroups: SettingSection[] = [
return isBasic || !user.subscription?.type
? "Subscribe to Pro"
: isTrial
? "Your free trial has started"
: "Subscription details";
? "Your free trial has started"
: "Subscription details";
},
type: "component",
component: "subscription",
@@ -111,16 +111,16 @@ export const settingsGroups: SettingSection[] = [
return user.subscription?.type === 2
? "You signed up on " + startDate
: user.subscription?.type === 1
? "Your free trial will end on " + expiryDate
: user.subscription?.type === 6
? subscriptionDaysLeft.time < -3
? "Your subscription has ended"
: "Your account will be downgraded to Basic in 3 days"
: user.subscription?.type === 7
? `Your subscription will end on ${expiryDate}.`
: user.subscription?.type === 5
? `Your subscription will renew on ${expiryDate}.`
: "Never hesitate to choose privacy";
? "Your free trial will end on " + expiryDate
: user.subscription?.type === 6
? subscriptionDaysLeft.time < -3
? "Your subscription has ended"
: "Your account will be downgraded to Basic in 3 days"
: user.subscription?.type === 7
? `Your subscription will end on ${expiryDate}.`
: user.subscription?.type === 5
? `Your subscription will renew on ${expiryDate}.`
: "Never hesitate to choose privacy";
}
},
{
@@ -892,16 +892,6 @@ export const settingsGroups: SettingSection[] = [
name: "Restore backup",
description: "Restore backup from phone storage.",
modifer: () => {
const user = useUserStore.getState().user;
if (!user || !user?.email) {
ToastEvent.show({
heading: "Login required",
message: "Please log in to your account to restore backup",
type: "error",
context: "global"
});
return;
}
eSendEvent(eOpenRestoreDialog);
}
},

View File

@@ -88,6 +88,13 @@ export const Subscription = () => {
}
};
function getPrice() {
return Platform.OS === "android"
? monthlyPlan?.product?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0].formattedPrice
: monthlyPlan?.product?.localizedPrice;
}
return (
<View>
{isNotPro ? (
@@ -114,12 +121,8 @@ export const Subscription = () => {
Config.GITHUB_RELEASE !== "true"
? "Resubscribe from Google Playstore"
: user.subscription?.type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED
? `Resubscribe to Pro (${
monthlyPlan?.product?.localizedPrice || "$4.49"
} / mo)`
: `Get Pro (${
monthlyPlan?.product?.localizedPrice || "$4.49"
} / mo)`
? `Resubscribe to Pro (${getPrice() || "$4.49"} / mo)`
: `Get Pro (${getPrice() || "$4.49"} / mo)`
}
/>
) : null}

View File

@@ -39,6 +39,10 @@ import { eSendEvent, presentSheet, ToastEvent } from "./event-manager";
import SettingsService from "./settings";
let premiumStatus = 0;
/**
* @type {RNIap.Subscription[]}
*/
let products = [];
let user = null;
@@ -68,7 +72,9 @@ async function setPremiumStatus() {
}
try {
await RNIap.initConnection();
products = await RNIap.getSubscriptions(itemSkus);
products = await RNIap.getSubscriptions({
skus: itemSkus
});
} catch (e) {
console.log("subscriptions: ", e);
}
@@ -223,12 +229,21 @@ const showVerifyEmailDialog = () => {
};
const subscriptions = {
/**
*
* @returns {RNIap.Purchase} subscription
*/
get: async () => {
if (Platform.OS === "android") return;
let _subscriptions = MMKV.getString("subscriptionsIOS");
if (!_subscriptions) return [];
return JSON.parse(_subscriptions);
},
/**
*
* @param {RNIap.Purchase} subscription
* @returns
*/
set: async (subscription) => {
if (Platform.OS === "android") return;
let _subscriptions = MMKV.getString("subscriptionsIOS");
@@ -263,6 +278,10 @@ const subscriptions = {
MMKV.setString("subscriptionsIOS", JSON.stringify(_subscriptions));
}
},
/**
*
* @param {RNIap.Purchase} subscription
*/
verify: async (subscription) => {
if (Platform.OS === "android") return;

View File

@@ -67,15 +67,10 @@ export const deleteItems = async (item, context) => {
});
return;
}
if (
item &&
item.id &&
useSelectionStore.getState().selectedItemsList.length === 0
) {
useSelectionStore.getState().setSelectedItem(item);
}
const { selectedItemsList } = useSelectionStore.getState();
const selectedItemsList = item
? [item]
: useSelectionStore.getState().selectedItemsList;
let notes = selectedItemsList.filter((i) => i.type === "note");
let notebooks = selectedItemsList.filter((i) => i.type === "notebook");
@@ -179,7 +174,9 @@ export const deleteItems = async (item, context) => {
});
}
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().clearSelection();
if (!item) {
useSelectionStore.getState().clearSelection();
}
useMenuStore.getState().setMenuPins();
useMenuStore.getState().setColorNotes();
SearchService.updateAndSearch();

View File

@@ -66,17 +66,37 @@ async function createNotes(bundle) {
const sessionId = bundle.note.sessionId;
const id = await db.notes.add(bundle.note);
for (const item of bundle.notebooks) {
if (item.type === "notebook") {
db.relations.add(item, { id, type: "note" });
} else {
db.notes.addToNotebook(
{
id: item.notebookId,
topic: item.id
},
id
);
if (!bundle.notebooks || !bundle.notebooks.length) {
const defaultNotebook = db.settings?.getDefaultNotebook();
if (defaultNotebook) {
if (!defaultNotebook.topic) {
db.relations.add(
{ type: "notebook", id: defaultNotebook.id },
{ id, type: "note" }
);
} else {
db.notes.addToNotebook(
{
id: defaultNotebook?.id,
topic: defaultNotebook.topic
},
id
);
}
}
} else {
for (const item of bundle.notebooks) {
if (item.type === "notebook") {
db.relations.add(item, { id, type: "note" });
} else {
db.notes.addToNotebook(
{
id: item.notebookId,
topic: item.id
},
id
);
}
}
}

View File

@@ -111,7 +111,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 2065
versionCode 2069
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -8,6 +8,10 @@ import com.ammarahmed.mmkv.MMKV;
public class OnClearFromRecentService extends Service {
static {
System.loadLibrary("rnmmkv");
}
@Override
public IBinder onBind(Intent intent) {
return null;
@@ -15,7 +19,6 @@ public class OnClearFromRecentService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
}
@@ -31,8 +34,10 @@ public class OnClearFromRecentService extends Service {
MMKV mmkv = MMKV.mmkvWithID("default",MMKV.SINGLE_PROCESS_MODE);
mmkv.removeValueForKey("appState");
stopSelf();
} catch (Exception e) {
} catch (UnsatisfiedLinkError e) {
} catch (Exception e) {
}
//System.exit(0);
}

View File

@@ -7,7 +7,7 @@ buildscript {
minSdkVersion = 21
compileSdkVersion = 33
targetSdkVersion = 33
ext.kotlinVersion = '1.6.10'
ext.kotlinVersion = '1.8.0'
androidXAnnotation = "1.1.0"
androidXCoreVersion = "1.7.0"
androidXCore = "1.7.0"

View File

@@ -1,3 +1,7 @@
- Fixed HTML & PDF exports
- Improved task lists in editor
- Improved loading images
- Allow restoring backups when logged out
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -997,7 +997,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2058;
CURRENT_PROJECT_VERSION = 2060;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1071,7 +1071,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.9;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1102,7 +1102,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2058;
CURRENT_PROJECT_VERSION = 2060;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1176,7 +1176,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.9;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1335,7 +1335,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2058;
CURRENT_PROJECT_VERSION = 2060;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1347,7 +1347,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.9;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1378,7 +1378,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2058;
CURRENT_PROJECT_VERSION = 2060;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1391,7 +1391,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.9;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1421,7 +1421,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2058;
CURRENT_PROJECT_VERSION = 2060;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1495,7 +1495,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.9;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1526,7 +1526,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2058;
CURRENT_PROJECT_VERSION = 2060;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1601,7 +1601,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.7;
MARKETING_VERSION = 2.6.9;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -477,7 +477,7 @@ PODS:
- React-Core
- RNGestureHandler (2.12.0):
- React-Core
- RNIap (7.5.6):
- RNIap (12.11.0):
- React-Core
- RNKeychain (4.0.5):
- React
@@ -911,7 +911,7 @@ SPEC CHECKSUMS:
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
RNFlashList: 399bf6a0db68f594ad2c86aaff3ea39564f39f8a
RNGestureHandler: dec4645026e7401a0899f2846d864403478ff6a5
RNIap: d248609d1b8937e63bd904e865c318e9b1457eff
RNIap: fc9af04ee706894a80c9d8f979bae930b0dee191
RNKeychain: 840f8e6f13be0576202aefcdffd26a4f54bfe7b5
RNNotifee: 2ae3c18196e6f307fa62ae5c8e5305dea03ff147
RNPrivacySnapshot: 8eaf571478a353f2e5184f5c803164f22428b023

View File

@@ -40,7 +40,7 @@
"react-native-get-random-values": "^1.7.0",
"react-native-gzip": "1.1.0",
"react-native-html-to-pdf-lite": "^0.9.1",
"react-native-iap": "7.5.6",
"react-native-iap": "12.11.0",
"react-native-image-picker": "4.1.2",
"react-native-in-app-review": "4.3.3",
"react-native-keychain": "4.0.5",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "2.6.7",
"version": "2.6.9",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -41,6 +41,7 @@
"@trpc/react-query": "10.38.3",
"@trpc/server": "10.38.3",
"react": "18.2.0",
"react-native": "0.72.0"
"react-native": "0.72.0",
"react-native-iap": "12.11.0"
}
}

View File

@@ -21,7 +21,7 @@ import ShareExtension from "@ammarahmed/react-native-share-extension";
import { getPreviewData } from "@flyerhq/react-native-link-preview";
import { formatBytes } from "@notesnook/common";
import { isImage } from "@notesnook/core/dist/utils/filename";
import { parseHTML } from "@notesnook/core/dist/utils/html-parser";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
@@ -53,10 +53,9 @@ import { getElevationStyle } from "../app/utils/elevation";
import { eOnLoadNote } from "../app/utils/events";
import { NoteBundle } from "../app/utils/note-bundle";
import { Editor } from "./editor";
import { HtmlLoadingWebViewAgent, fetchHandle } from "./fetch-webview";
import { Search } from "./search";
import { initDatabase, useShareStore } from "./store";
import { useThemeColors } from "@notesnook/theme";
import { HtmlLoadingWebViewAgent, fetchHandle } from "./fetch-webview";
const getLinkPreview = (url) => {
return getPreviewData(url, 5000);

View File

@@ -15,7 +15,9 @@
"@notesnook/theme": ["../../packages/theme"],
"@types/react": ["./node_modules/@types/react"]
},
"incremental": true
"incremental": true,
"maxNodeModuleJsDepth": 5,
"downlevelIteration": true
},
"exclude": ["native"]
}

View File

@@ -1377,7 +1377,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "2.6.5",
"version": "2.6.8",
"license": "GPL-3.0-or-later",
"dependencies": {
"@aws-sdk/util-base64-browser": "^3.208.0",
@@ -1446,6 +1446,7 @@
"devDependencies": {
"@babel/core": "^7.22.5",
"@playwright/test": "^1.36.2",
"@swc/core": "1.3.61",
"@trpc/server": "10.38.3",
"@types/babel__core": "^7.20.1",
"@types/file-saver": "^2.0.5",
@@ -1456,7 +1457,8 @@
"@types/react-dom": "17.0.2",
"@types/react-modal": "3.13.1",
"@types/tinycolor2": "^1.4.3",
"@vitejs/plugin-react-swc": "^3.3.2",
"@types/wicg-file-system-access": "^2020.9.6",
"@vitejs/plugin-react-swc": "3.3.2",
"autoprefixer": "^10.4.14",
"buffer": "^6.0.3",
"chalk": "^4.1.0",
@@ -1470,14 +1472,14 @@
"otplib": "^12.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"rollup": "^3.27.0",
"rollup": "^3.29.4",
"rollup-plugin-visualizer": "^5.9.2",
"swc-plugin-react-remove-properties": "^0.1.4",
"vite": "^4.3.9",
"vite": "^4.5.0",
"vite-plugin-env-compatible": "^1.1.1",
"vite-plugin-pwa": "^0.16.3",
"vite-plugin-svgr": "^3.2.0",
"vitest": "^0.32.0",
"vitest": "^0.34.6",
"workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-precaching": "^7.0.0",
@@ -2406,15 +2408,6 @@
"@trpc/server": "10.38.3"
}
},
"node_modules/@trpc/server": {
"version": "10.38.3",
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.38.3.tgz",
"integrity": "sha512-9s8/kwo2IDB5hwB2SKZZrfevRhdb1f9fdXtIYd3lbQuf2jQaC/LyQuHaIQjDQoUx9updBfsHXcFFPiCP1DL6pg==",
"funding": [
"https://trpc.io/sponsor"
],
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.1",
"dev": true,

38767
apps/web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "2.6.7",
"version": "2.6.9",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -74,6 +74,7 @@
"devDependencies": {
"@babel/core": "^7.22.5",
"@playwright/test": "^1.36.2",
"@swc/core": "1.3.61",
"@trpc/server": "10.38.3",
"@types/babel__core": "^7.20.1",
"@types/file-saver": "^2.0.5",
@@ -84,7 +85,8 @@
"@types/react-dom": "17.0.2",
"@types/react-modal": "3.13.1",
"@types/tinycolor2": "^1.4.3",
"@vitejs/plugin-react-swc": "^3.3.2",
"@types/wicg-file-system-access": "^2020.9.6",
"@vitejs/plugin-react-swc": "3.3.2",
"autoprefixer": "^10.4.14",
"buffer": "^6.0.3",
"chalk": "^4.1.0",
@@ -98,14 +100,14 @@
"otplib": "^12.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"rollup": "^3.27.0",
"rollup": "^3.29.4",
"rollup-plugin-visualizer": "^5.9.2",
"swc-plugin-react-remove-properties": "^0.1.4",
"vite": "^4.3.9",
"vite": "^4.5.0",
"vite-plugin-env-compatible": "^1.1.1",
"vite-plugin-pwa": "^0.16.3",
"vite-plugin-svgr": "^3.2.0",
"vitest": "^0.32.0",
"vitest": "^0.34.6",
"workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-precaching": "^7.0.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -129,7 +129,6 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
) {
const [key, status] = getProcessingStatusFromType(type);
console.log("handleDownloadUploadProgresss", key, status, current, total);
if (current === total) {
removeStatus(key);
} else {

View File

@@ -24,6 +24,7 @@ import Config from "./utils/config";
import { initalizeLogger, logger } from "./utils/logger";
import { AuthProps } from "./views/auth";
import { initializeFeatureChecks } from "./utils/feature-check";
type Route<TProps = null> = {
component: () => Promise<{
@@ -142,6 +143,8 @@ function isSessionExpired(path: Routes): RouteWithPath<AuthProps> | null {
export async function init() {
await initalizeLogger();
await initializeFeatureChecks();
const { path, route } = getRoute();
return { ...route, path };
}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import FS from "../interfaces/fs";
import { lazify } from "../utils/lazify";
import { db } from "./db";
async function download(hash: string) {
@@ -42,13 +42,15 @@ export async function saveAttachment(hash: string) {
if (!response) return;
const { attachment, key } = response;
await FS.saveFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
name: attachment.metadata.filename,
type: attachment.metadata.type,
isUploaded: !!attachment.dateUploaded
});
await lazify(import("../interfaces/fs"), ({ default: FS }) =>
FS.saveFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
name: attachment.metadata.filename,
type: attachment.metadata.type,
isUploaded: !!attachment.dateUploaded
})
);
}
type OutputTypeToReturnType = {
@@ -67,13 +69,15 @@ export async function downloadAttachment<
if (type === "base64" || type === "text")
return (await db.attachments?.read(hash, type)) as TOutputType;
const blob = await FS.decryptFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
name: attachment.metadata.filename,
type: attachment.metadata.type,
isUploaded: !!attachment.dateUploaded
});
const blob = await lazify(import("../interfaces/fs"), ({ default: FS }) =>
FS.decryptFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
name: attachment.metadata.filename,
type: attachment.metadata.type,
isUploaded: !!attachment.dateUploaded
})
);
if (!blob) return;
return blob as TOutputType;
@@ -84,7 +88,9 @@ export async function checkAttachment(hash: string) {
if (!attachment) return { failed: "Attachment not found." };
try {
const size = await FS.getUploadedFileSize(hash);
const size = await lazify(import("../interfaces/fs"), ({ default: FS }) =>
FS.getUploadedFileSize(hash)
);
if (size <= 0) return { failed: "File length is 0." };
} catch (e) {
return { failed: e instanceof Error ? e.message : "Unknown error." };

View File

@@ -21,6 +21,7 @@ import { EventSourcePolyfill as EventSource } from "event-source-polyfill";
import { DatabasePersistence, NNStorage } from "../interfaces/storage";
import { logger } from "../utils/logger";
import type Database from "@notesnook/core/dist/api";
import { showMigrationDialog } from "./dialog-controller";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@@ -70,7 +71,6 @@ async function initializeDatabase(persistence: DatabasePersistence) {
logger.measure("Database initialization");
if (db.migrations?.required()) {
const { showMigrationDialog } = await import("./dialog-controller");
await showMigrationDialog();
}

View File

@@ -41,12 +41,21 @@ export async function exportToPDF(
iframe.style.width = "0";
iframe.style.height = "0";
iframe.style.border = "0";
iframe.loading = "eager";
iframe.onload = () => {
iframe.onload = async () => {
if (!iframe.contentWindow) return;
if (iframe.contentDocument) iframe.contentDocument.title = title;
iframe.contentWindow.onbeforeunload = () => closePrint(false);
iframe.contentWindow.onafterprint = () => closePrint(true);
if (
iframe.contentDocument &&
!!iframe.contentDocument.querySelector(".math-block,.math-inline")
) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
iframe.contentWindow.print();
};
@@ -119,7 +128,11 @@ export async function exportNotes(
return await exportToPDF(note.title, exported);
}
files.push({ filename: note.title, content: exported });
files.push({
filename: note.title,
content: exported,
date: note.dateEdited
});
}
if (!files.length) return false;

View File

@@ -290,7 +290,6 @@ const AttachmentMenuItems: (
key: "download",
title: status?.type === "download" ? "Cancel download" : "Download",
icon: Download.path,
isDisabled: !attachment.dateUploaded,
onClick: async () => {
const isDownloading = status?.type === "download";
if (isDownloading) {

View File

@@ -26,6 +26,7 @@ import IconTag from "../icon-tag";
import { db } from "../../common/db";
import { useMenuTrigger } from "../../hooks/use-menu";
import { MenuItem } from "@notesnook/ui";
import { navigate } from "../../navigation";
type HeaderProps = { readonly: boolean };
function Header(props: HeaderProps) {
@@ -39,7 +40,7 @@ function Header(props: HeaderProps) {
return (
<>
{!readonly && id && (
{id && (
<Flex
sx={{ lineHeight: 2.5, alignItems: "center", flexWrap: "wrap" }}
data-test-id="tags"
@@ -50,24 +51,34 @@ function Header(props: HeaderProps) {
key={tag}
text={db.tags?.alias(tag)}
icon={Tag}
title={`Click to remove`}
onClick={() => setTag(tag)}
title={tag}
onClick={() => {
const tagItem = db.tags?.tag(tag);
if (!tagItem) {
setTag(tag);
return;
}
navigate(`/tags/${tagItem.id}`);
}}
onDismiss={readonly ? undefined : () => setTag(tag)}
styles={{ container: { mr: 1 }, text: { fontSize: "body" } }}
/>
))}
<Autosuggest
sessionId={id}
filter={(query) =>
db.lookup?.tags(filterableTags, query).slice(0, 10) || []
}
onAdd={(value) => setTag(value)}
onSelect={(item) => setTag(item.title)}
onRemove={() => {
if (tags.length <= 0) return;
setTag(tags[tags.length - 1]);
}}
defaultItems={filterableTags?.slice(0, 10) || []}
/>
{!readonly && (
<Autosuggest
sessionId={id}
filter={(query) =>
db.lookup?.tags(filterableTags, query).slice(0, 10) || []
}
onAdd={(value) => setTag(value)}
onSelect={(item) => setTag(item.title)}
onRemove={() => {
if (tags.length <= 0) return;
setTag(tags[tags.length - 1]);
}}
defaultItems={filterableTags?.slice(0, 10) || []}
/>
)}
</Flex>
)}
</>
@@ -86,6 +97,7 @@ type AutosuggestProps = {
export function Autosuggest(props: AutosuggestProps) {
const { sessionId, filter, onRemove, onSelect, onAdd, defaultItems } = props;
const inputRef = useRef<HTMLInputElement>(null);
const arrowDown = useRef<boolean>();
const filteredItems = useRef<any[]>([]);
const { openMenu, closeMenu, isOpen } = useMenuTrigger();
const clearInput = useCallback(() => {
@@ -178,8 +190,13 @@ export function Autosuggest(props: AutosuggestProps) {
data-test-id="editor-tag-input"
onFocus={() => {
const text = getInputValue();
console.log(defaultItems);
if (!text) onOpenMenu(defaultItems.slice());
else onOpenMenu([]);
}}
onClick={() => {
const text = getInputValue();
if (!text) onOpenMenu(defaultItems.slice());
else onOpenMenu([]);
}}
onChange={(e) => {
const { value } = e.target;
@@ -191,22 +208,17 @@ export function Autosuggest(props: AutosuggestProps) {
}}
onKeyDown={(e) => {
const text = getInputValue();
if (
e.key === "Enter" &&
!!text &&
isOpen &&
filteredItems.current.length <= 0
) {
if (e.key === "Enter" && !!text && isOpen && !arrowDown.current) {
onAction("add", text);
} else if (e.key === "Enter" && !!text && isOpen) {
onAction("select", filteredItems.current[0]);
} else if (!text && e.key === "Backspace") {
onRemove();
closeMenu();
} else if (e.key === "Escape") {
arrowDown.current = false;
closeMenu();
e.stopPropagation();
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
arrowDown.current = true;
if (e.key === "ArrowDown" && !text) onOpenMenu(defaultItems.slice());
e.preventDefault();

View File

@@ -23,7 +23,6 @@ import { db } from "../../common/db";
import { showBuyDialog } from "../../common/dialog-controller";
import { TaskManager } from "../../common/task-manager";
import { isUserPremium } from "../../hooks/use-is-user-premium";
import fs from "../../interfaces/fs";
import { showToast } from "../../utils/toast";
import { showFilePicker } from "../../utils/file-picker";
@@ -151,25 +150,29 @@ async function addAttachment(
dataurl: string | undefined,
options: AddAttachmentOptions = {}
): Promise<Attachment> {
const { expectedFileHash, forceWrite, showProgress = true } = options;
const { default: FS } = await import("../../interfaces/fs");
const { expectedFileHash, showProgress = true } = options;
let forceWrite = options.forceWrite;
const action = async () => {
const reader: ReadableStreamReader<Uint8Array> = (
file.stream() as unknown as ReadableStream<Uint8Array>
).getReader();
const { hash, type: hashType } = await fs.hashStream(reader);
const reader = file.stream().getReader();
const { hash, type: hashType } = await FS.hashStream(reader);
reader.releaseLock();
if (expectedFileHash && hash !== expectedFileHash)
throw new Error(
`Please select the same file for reuploading. Expected hash ${expectedFileHash} but got ${hash}.`
);
const exists = db.attachments?.exists(hash);
if (!forceWrite && exists) {
forceWrite = (await FS.getUploadedFileSize(hash)) <= 0;
}
if (forceWrite || !exists) {
const key: SerializedKey = await getEncryptionKey();
const output = await fs.writeEncryptedFile(file, key, hash);
const output = await FS.writeEncryptedFile(file, key, hash);
if (!output) throw new Error("Could not encrypt file.");
if (forceWrite && exists) await db.attachments?.reset(hash);

View File

@@ -57,6 +57,7 @@ import { store as editorstore } from "../../stores/editor-store";
import { ScopedThemeProvider } from "../theme-provider";
import { writeText } from "clipboard-polyfill";
import { useStore as useThemeStore } from "../../stores/theme-store";
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
type OnChangeHandler = (
id: string | undefined,
@@ -446,7 +447,7 @@ function toIEditor(editor: Editor): IEditor {
if (file.dataurl) {
editor.current?.commands.insertImage({
...file,
dataurl: file.dataurl
bloburl: toBlobURL(file.dataurl, file.hash)
});
} else editor.current?.commands.insertAttachment(file);
},
@@ -455,7 +456,7 @@ function toIEditor(editor: Editor): IEditor {
loadImage: (hash, dataurl) =>
editor.current?.commands.updateImage(
{ hash },
{ hash, dataurl, preventUpdate: true }
{ hash, bloburl: toBlobURL(dataurl, hash), preventUpdate: true }
),
sendAttachmentProgress: (hash, type, progress) =>
editor.current?.commands.setAttachmentProgress({

View File

@@ -26,6 +26,8 @@ import useTablet from "../../hooks/use-tablet";
import { useEditorConfig } from "./context";
import { getFontById } from "@notesnook/editor";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { replaceDateTime } from "@notesnook/editor/dist/extensions/date-time";
import { useStore as useSettingsStore } from "../../stores/setting-store";
type TitleBoxProps = {
readonly: boolean;
@@ -38,6 +40,9 @@ function TitleBox(props: TitleBoxProps) {
const isMobile = useMobile();
const isTablet = useTablet();
const { editorConfig } = useEditorConfig();
const dateFormat = useSettingsStore((store) => store.dateFormat);
const timeFormat = useSettingsStore((store) => store.timeFormat);
const fontFamily = useMemo(
() => getFontById(editorConfig.fontFamily)?.font || "heading",
[editorConfig.fontFamily]
@@ -111,6 +116,11 @@ function TitleBox(props: TitleBoxProps) {
}}
onChange={(e) => {
const { sessionId, id } = store.get().session;
e.target.value = replaceDateTime(
e.target.value,
dateFormat,
timeFormat
);
debouncedOnTitleChange(sessionId, id, e.target.value);
updateFontSize(e.target.value.length);
}}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Flex, Text } from "@theme-ui/components";
import { ThemeUICSSObject } from "@theme-ui/core";
import { Icon } from "../icons";
import { Close, Icon } from "../icons";
type IconTagProps = {
text: string;
@@ -33,10 +33,20 @@ type IconTagProps = {
};
testId?: string;
highlight?: boolean;
onDismiss?: () => void;
};
function IconTag(props: IconTagProps) {
const { icon: Icon, text, title, onClick, styles, testId, highlight } = props;
const {
icon: Icon,
text,
title,
onClick,
onDismiss,
styles,
testId,
highlight
} = props;
return (
<Flex
@@ -92,6 +102,22 @@ function IconTag(props: IconTagProps) {
>
{text}
</Text>
{onDismiss && (
<Close
size={12}
title="Remove"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDismiss();
}}
sx={{
ml: 1,
":hover": { bg: "background-error" },
":hover path": { fill: "var(--icon-error) !important" }
}}
/>
)}
</Flex>
);
}

View File

@@ -110,10 +110,11 @@ function Properties(props) {
"reminder"
);
const allNotebooks = useMemo(
() => [
...notebooks.map((ref) => db.notebooks.notebook(ref.id)?.data),
...db.relations.to({ id: sessionId, type: "note" }, "notebook")
],
() =>
[
...notebooks.map((ref) => db.notebooks.notebook(ref.id)?.data),
...db.relations.to({ id: sessionId, type: "note" }, "notebook")
].filter(Boolean),
[sessionId, notebooks]
);

View File

@@ -50,6 +50,7 @@ function StatusBar() {
const isLoggedIn = useUserStore((state) => state.isLoggedIn);
const statuses = useStatus();
const updateStatus = useAutoUpdater();
const isFocusMode = useAppStore((state) => state.isFocusMode);
return (
<ScopedThemeProvider
@@ -63,124 +64,130 @@ function StatusBar() {
}}
px={2}
>
<Flex>
{isLoggedIn ? (
<>
{isFocusMode ? (
<Flex />
) : (
<Flex>
{isLoggedIn ? (
<>
<Button
onClick={() =>
user?.isEmailConfirmed
? navigate("/settings")
: hashNavigate("/email/verify")
}
variant="statusitem"
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
<Circle
size={7}
color={
user?.isEmailConfirmed
? "var(--icon-success)"
: "var(--icon-error)"
}
/>
<Text
className="selectable"
variant="subBody"
ml={1}
sx={{ color: "paragraph" }}
>
{user?.email}
{user?.isEmailConfirmed ? "" : " (not verified)"}
</Text>
</Button>
<SyncStatus />
</>
) : isLoggedIn === false ? (
<Button
onClick={() =>
user?.isEmailConfirmed
? navigate("/settings")
: hashNavigate("/email/verify")
}
variant="statusitem"
onClick={() => hardNavigate("/login")}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
data-test-id="not-logged-in"
>
<Circle
size={7}
color={
user?.isEmailConfirmed
? "var(--icon-success)"
: "var(--icon-error)"
}
/>
<Text
className="selectable"
variant="subBody"
ml={1}
sx={{ color: "paragraph" }}
>
{user?.email}
{user?.isEmailConfirmed ? "" : " (not verified)"}
<Circle size={7} color="var(--icon-error)" />
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
Not logged in
</Text>
</Button>
<SyncStatus />
</>
) : isLoggedIn === false ? (
) : null}
<Button
variant="statusitem"
onClick={() => hardNavigate("/login")}
onClick={() => showIssueDialog()}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
data-test-id="not-logged-in"
title="Facing an issue? Click here to create a bug report."
>
<Circle size={7} color="var(--icon-error)" />
<Issue size={12} />
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
Not logged in
Report an issue
</Text>
</Button>
) : null}
<Button
variant="statusitem"
onClick={() => showIssueDialog()}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
title="Facing an issue? Click here to create a bug report."
>
<Issue size={12} />
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
Report an issue
</Text>
</Button>
{statuses?.map((status) => {
const { key, icon: Icon } = status;
return (
<Flex
key={key}
ml={1}
sx={{ alignItems: "center", justifyContent: "center" }}
{statuses?.map((status) => {
const { key, icon: Icon } = status;
return (
<Flex
key={key}
ml={1}
sx={{ alignItems: "center", justifyContent: "center" }}
>
{Icon ? <Icon size={12} /> : <Loading size={12} />}
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
{statusToString(status)}
</Text>
</Flex>
);
})}
{updateStatus && updateStatus.type !== "updated" && (
<Button
variant="statusitem"
onClick={async () => {
if (updateStatus.type === "available") {
await showUpdateAvailableNotice(updateStatus);
} else if (updateStatus.type === "completed") {
installUpdate();
} else {
checkForUpdate();
}
}}
sx={{
ml: 1,
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
{Icon ? <Icon size={12} /> : <Loading size={12} />}
<Update
rotate={
updateStatus.type !== "completed" &&
updateStatus.type !== "available"
}
color={
updateStatus.type === "available" ? "accent" : "paragraph"
}
size={12}
/>
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
{statusToString(status)}
{statusToInfoText(updateStatus)}
</Text>
</Flex>
);
})}
{updateStatus && updateStatus.type !== "updated" && (
<Button
variant="statusitem"
onClick={async () => {
if (updateStatus.type === "available") {
await showUpdateAvailableNotice(updateStatus);
} else if (updateStatus.type === "completed") {
installUpdate();
} else {
checkForUpdate();
}
}}
sx={{
ml: 1,
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
<Update
rotate={
updateStatus.type !== "completed" &&
updateStatus.type !== "available"
}
color={updateStatus.type === "available" ? "accent" : "paragraph"}
size={12}
/>
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
{statusToInfoText(updateStatus)}
</Text>
</Button>
)}
</Flex>
</Button>
)}
</Flex>
)}
<EditorFooter />
</ScopedThemeProvider>
);

View File

@@ -59,6 +59,16 @@ export function BaseThemeProvider(
useEffect(() => {
useThemeEngineStore.getState().setTheme(theme);
const themeColorElement = document.head.querySelector(
"meta[name='theme-color']"
);
if (themeColorElement) {
themeColorElement.setAttribute(
"content",
theme.scopes.base.primary.background
);
}
}, [theme]);
return (

View File

@@ -25,7 +25,6 @@ import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { db } from "../common/db";
import { useStore } from "../stores/reminder-store";
import { useStore as useSettingStore } from "../stores/setting-store";
import { showToast } from "../utils/toast";
import { useIsUserPremium } from "../hooks/use-is-user-premium";
import { Pro } from "../components/icons";
@@ -57,9 +56,7 @@ const RecurringModes = {
DAY: "day"
} as const;
let WEEK_DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const WEEK_DAYS_MON = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const WEEK_DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const modes = [
{
id: Modes.ONCE,
@@ -119,7 +116,6 @@ export default function AddReminderDialog(props: AddReminderDialogProps) {
const [title, setTitle] = useState<string>();
const [description, setDescription] = useState<string>();
const refresh = useStore((state) => state.refresh);
const weekFromat = useSettingStore((state) => state.weekFormat);
const isUserPremium = useIsUserPremium();
useEffect(() => {
@@ -146,10 +142,6 @@ export default function AddReminderDialog(props: AddReminderDialogProps) {
setDescription(note.headline);
}, [noteId]);
useEffect(() => {
if (weekFromat === "Mon") WEEK_DAYS = WEEK_DAYS_MON;
}, [weekFromat]);
const repeatsDaily =
(selectedDays.length === 7 && recurringMode === RecurringModes.WEEK) ||
(selectedDays.length === 31 && recurringMode === RecurringModes.MONTH) ||

View File

@@ -89,7 +89,17 @@ const features: Record<FeatureKeys, Feature> = {
)
}
]
: [],
: [
{
title: "Date time shortcuts in note title",
subtitle:
"You can now use date time shortcuts (/now, /date & /time) in note & task list titles."
},
{
title: "Restore backups without an account",
subtitle: "You can now restore backups without logging in."
}
],
cta: {
title: "Got it",
icon: Checkmark,

View File

@@ -105,7 +105,11 @@ function PasswordDialog(props) {
<Label
key={check.key}
mt={2}
sx={{ fontSize: "title", alignItems: "center" }}
sx={{
fontSize: "body",
fontFamily: "body",
alignItems: "center"
}}
>
<Checkbox id={check.key} name={check.key} />
{check.title}

View File

@@ -62,7 +62,6 @@ export const BackupExportSettings: SettingsGroup[] = [
key: "restore-backup",
title: "Restore backup",
description: "Restore a backup file from your disk drive.",
isHidden: () => !useUserStore.getState().isLoggedIn && !IS_TESTING,
components: [
{
type: "button",

View File

@@ -99,27 +99,6 @@ export const BehaviourSettings: SettingsGroup[] = [
]
}
]
},
{
key: "week-format",
title: "Week format",
description:
"This will set first day of the week for weekly reminders.",
keywords: [],
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.weekFormat, listener),
components: [
{
type: "dropdown",
onSelectionChanged: (value) =>
useSettingStore.getState().setWeekFormat(value),
selectedOption: () => useSettingStore.getState().weekFormat,
options: [
{ value: "Sun", title: "Sunday" },
{ value: "Mon", title: "Monday" }
]
}
]
}
]
},
@@ -157,9 +136,10 @@ export const BehaviourSettings: SettingsGroup[] = [
]
},
{
key: "updates",
key: "desktop-app",
section: "behaviour",
header: "Updates",
header: "Desktop app",
isHidden: () => !IS_DESKTOP_APP,
settings: [
{
key: "auto-updates",
@@ -168,6 +148,7 @@ export const BehaviourSettings: SettingsGroup[] = [
"Automatically download & install updates in the background without prompting first.",
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.autoUpdates, listener),
isHidden: () => useSettingStore.getState().isFlatpak,
components: [
{
type: "toggle",

View File

@@ -72,6 +72,24 @@ export const AboutSettings: SettingsGroup[] = [
];
}
},
{
key: "source-code",
title: "Source code",
description: "All the source code for Notesnook is available & open for everyone on GitHub.",
components: [
{
type: "button",
action: () => {
window.open(
"https://github.com/streetwriters/notesnook",
"_blank"
);
},
title: "View source code",
variant: "secondary"
}
]
},
{
key: "roadmap",
title: "Roadmap",

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { SettingsGroup } from "./types";
import { useStore as useSettingStore } from "../../stores/setting-store";
import { useStore as useUserStore } from "../../stores/user-store";
import { getPlatform, isDesktop } from "../../utils/platform";
import { getPlatform } from "../../utils/platform";
import { db } from "../../common/db";
import { showPromptDialog } from "../../common/dialog-controller";
import Config from "../../utils/config";
@@ -74,6 +74,20 @@ What data is collected & when?`,
}
]
},
{
key: "hide-note-title",
title: "Hide note title",
description: "Prevent note title from appearing in tab/window title.",
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.hideNoteTitle, listener),
components: [
{
type: "toggle",
isToggled: () => useSettingStore.getState().hideNoteTitle,
toggle: () => useSettingStore.getState().toggleHideTitle()
}
]
},
{
key: "privacy-mode",
title: "Privacy mode",

View File

@@ -58,6 +58,8 @@ export const VaultSettings: SettingsGroup[] = [
title: "Change vault password",
description: "Set a new password for your vault",
isHidden: () => !useAppStore.getState().isVaultCreated,
onStateChange: (listener) =>
useAppStore.subscribe((s) => s.isVaultCreated, listener),
components: [
{
type: "button",
@@ -72,6 +74,8 @@ export const VaultSettings: SettingsGroup[] = [
title: "Clear vault",
description: "Unlock all locked notes and clear vault.",
isHidden: () => !useAppStore.getState().isVaultCreated,
onStateChange: (listener) =>
useAppStore.subscribe((s) => s.isVaultCreated, listener),
components: [
{
type: "button",
@@ -91,6 +95,8 @@ export const VaultSettings: SettingsGroup[] = [
title: "Delete vault",
description: "Delete vault including all locked notes.",
isHidden: () => !useAppStore.getState().isVaultCreated,
onStateChange: (listener) =>
useAppStore.subscribe((s) => s.isVaultCreated, listener),
components: [
{
type: "button",

View File

@@ -3,16 +3,9 @@
<head>
<meta charset="utf-8" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAHUUExURUxpcf////////////////////////////////////////////////////////////////////////////////////39/f39/f////////////////////////////////z8/O3s7P7+/vTz8wAAAPz7++/v7/Py8u/u7vDv7+7t7dfX1/79/ezr66Kiovv6+vf29vX09OTj4w0NDfv7++Ti4vr6+vT09Ovq6vn5+f39/fb19dnX1/b29tvb29/f3xMTE/j4+Pn4+O7s7MnJyerq6uno6M/Pz8jIyN3d3dnZ2d7e3kZGRvj39/38/Pf39+zs7PDw8OPi4ubl5cvLy+rp6czMzPLy8vPz887Ozujo6MrKyuDf3+fn59PT0+bm5uXl5c3NzfX19evr6wsLC+/t7ePj4/Ty8u3r6/Lx8eXk5OPh4dDQ0Nza2tjX19vZ2fHw8Nva2tLR0djW1tbW1tra2tLS0uHh4evp6eLi4tXV1djY2L+/vysrK7q6utHR0aGhoa6urrm5uQICAigoKCEhIZCQkFxcXBgYGFRUVBISEm1tbQ4ODklJSbu7uyAgID09PZqamkVFRXh4eLGxsTg4OOTk5FFRUQEBAQoKCnx8fBcXF7/+rP8AAAAfdFJOUwBOqO2h6KT9Mtyr5+w3Str+6TTZOjXb31M24O8BOd0PapSsAAABxElEQVQ4y2NgYOBgYWWXxwLYBdmEGICAjx9Z1NJJVQfB42QE6hdG0RaoaaCqhKSCl4EF1VxNVAXyTAzcaArUUBXwMLBjKDBBViDAII+uwBZFgTy6AjU1awIKNKz1aa0g1VBfB58CDS08CuIb40AKzLAqsJKXN2tXacChwKS+rTtB3tJHRU9ewxmbFZ6tKiqTrSAKdIO1LDEUaPeoqMzsUwAp0NIt8Uj0ijJXQFVgnDJVZcK0XrACXcXY0sjoHM8AfyUkBbOn9KvMmgRSYFEW4hUVqJagHeaShqTAwX7ijJQusAkW5VXRHu6eruFqaigKkqarqAAVKFbYxWiYq1XGedijuMHBWz6xo7m2Rl7RwtvdJaw6NjzCFl2BVVNdshJQgbqBsqtdpIu7nasjXIFXS2cyRL2CW6i6JTCgFCLitUPs4QrM7cDmKZi7FRYVm6o5QeLbDGICLNEqJAVk59rk2djY5Me4FThawQOKFSzrrxmaZWpqBAamQUGmwRaakNTNzMAm7+ekrKmlqOjsrIgAGcAA1ci0NVPiYhAV8zVURgbp6nBg7isuwsDAKKfkp4AEVOHASklKApS9JZmY5bECaS5ZGQYA3vqpy6NoYh0AAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII="
/>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<!-- <link rel="manifest" href="/site.webmanifest" /> -->
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#01c352" />
<meta name="msapplication-TileColor" content="#01c352" />
<meta name="theme-color" content="#01c352" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@@ -24,6 +24,7 @@ import { loadDatabase } from "./hooks/use-database";
import { AppEventManager, AppEvents } from "./common/app-events";
import { BaseThemeProvider } from "./components/theme-provider";
import { register } from "./utils/stream-saver/mitm";
import { getServiceWorkerVersion } from "./utils/version";
renderApp();
@@ -60,7 +61,6 @@ async function initializeServiceWorker() {
serviceWorker.register({
onUpdate: async (registration: ServiceWorkerRegistration) => {
if (!registration.waiting) return;
const { getServiceWorkerVersion } = await import("./utils/version");
const { formatted } = await getServiceWorkerVersion(
registration.waiting
);

View File

@@ -0,0 +1,161 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { IFileStorage } from "@notesnook/streamable-fs/dist/src/interfaces";
import { File } from "@notesnook/streamable-fs/dist/src/types";
import { IndexedDBKVStore } from "./key-value";
import OriginPrivateFileStoreWorker from "./opfs.worker?worker";
import { OriginPrivateFileStoreWorkerType } from "./opfs.worker";
import { transfer, wrap } from "comlink";
export class IndexedDBFileStore implements IFileStorage {
storage: IndexedDBKVStore;
constructor(name: string) {
this.storage = new IndexedDBKVStore(name, name);
}
clear(): Promise<void> {
return this.storage.clear();
}
setMetadata(filename: string, metadata: File): Promise<void> {
return this.storage.set(filename, metadata);
}
getMetadata(filename: string): Promise<File | undefined> {
return this.storage.get(filename);
}
deleteMetadata(filename: string): Promise<void> {
return this.storage.delete(filename);
}
writeChunk(chunkName: string, data: Uint8Array): Promise<void> {
return this.storage.set(chunkName, data);
}
deleteChunk(chunkName: string): Promise<void> {
return this.storage.delete(chunkName);
}
readChunk(chunkName: string): Promise<Uint8Array | undefined> {
return this.storage.get(chunkName);
}
}
export class CacheStorageFileStore implements IFileStorage {
storage: IndexedDBKVStore;
constructor(private readonly name: string) {
this.storage = new IndexedDBKVStore(name, name);
console.log("USING CACHE FILE STORE!");
}
private getCache() {
return window.caches.open(this.name);
}
async clear(): Promise<void> {
const cache = await this.getCache();
for (const req of await cache.keys()) {
await cache.delete(req);
}
return this.storage.clear();
}
setMetadata(filename: string, metadata: File): Promise<void> {
return this.storage.set(filename, metadata);
}
getMetadata(filename: string): Promise<File | undefined> {
return this.storage.get(filename);
}
deleteMetadata(filename: string): Promise<void> {
return this.storage.delete(filename);
}
async writeChunk(chunkName: string, data: Uint8Array): Promise<void> {
const cache = await this.getCache();
await cache.put(
this.toURL(chunkName),
new Response(data, {
headers: new Headers({
"Content-Length": data.length.toString(),
"Content-Type": "application/encrypted-octet-stream"
})
})
);
}
async deleteChunk(chunkName: string): Promise<void> {
const cache = await this.getCache();
await cache.delete(this.toURL(chunkName));
}
async readChunk(chunkName: string): Promise<Uint8Array | undefined> {
const cache = await this.getCache();
const response = await cache.match(this.toURL(chunkName));
return response ? new Uint8Array(await response.arrayBuffer()) : undefined;
}
private toURL(chunkName: string) {
return `/${chunkName}`;
}
}
export class OriginPrivateFileSystem implements IFileStorage {
private readonly worker = wrap<OriginPrivateFileStoreWorkerType>(
new OriginPrivateFileStoreWorker()
);
private created = false;
constructor(private readonly name: string) {
console.log("using origin private file store");
}
private async create() {
if (this.created) return;
await this.worker.create(this.name, this.name);
this.created = true;
}
async clear(): Promise<void> {
await this.create();
await this.worker.clear(this.name);
}
async setMetadata(filename: string, metadata: File): Promise<void> {
await this.create();
await this.worker.setMetadata(this.name, filename, metadata);
}
async getMetadata(filename: string): Promise<File | undefined> {
await this.create();
return this.worker.getMetadata(this.name, filename);
}
async deleteMetadata(filename: string): Promise<void> {
await this.create();
return this.worker.deleteMetadata(this.name, filename);
}
async writeChunk(chunkName: string, data: Uint8Array): Promise<void> {
await this.create();
return this.worker.writeChunk(
this.name,
chunkName,
transfer(data.buffer, [data.buffer])
);
}
async deleteChunk(chunkName: string): Promise<void> {
await this.create();
return this.worker.deleteChunk(this.name, chunkName);
}
async readChunk(chunkName: string): Promise<Uint8Array | undefined> {
await this.create();
return this.worker.readChunk(this.name, chunkName);
}
}

View File

@@ -37,6 +37,12 @@ import { Cipher, DataFormat, SerializedKey } from "@notesnook/crypto";
import { IDataType } from "hash-wasm/dist/lib/util";
import { IndexedDBKVStore } from "./key-value";
import FileHandle from "@notesnook/streamable-fs/dist/src/filehandle";
import {
CacheStorageFileStore,
IndexedDBFileStore,
OriginPrivateFileSystem
} from "./file-store";
import { isFeatureSupported } from "../utils/feature-check";
const ABYTES = 17;
const CHUNK_SIZE = 512 * 1024;
@@ -45,7 +51,13 @@ const UPLOAD_PART_REQUIRED_CHUNKS = Math.ceil(
(5 * 1024 * 1024) / ENCRYPTED_CHUNK_SIZE
);
const MINIMUM_MULTIPART_FILE_SIZE = 25 * 1024 * 1024;
const streamablefs = new StreamableFS("streamable-fs");
const streamablefs = new StreamableFS(
isFeatureSupported("opfs")
? new OriginPrivateFileSystem("streamable-fs")
: isFeatureSupported("cache")
? new CacheStorageFileStore("streamable-fs")
: new IndexedDBFileStore("streamable-fs")
);
async function writeEncryptedFile(
file: File,
@@ -69,7 +81,12 @@ async function writeEncryptedFile(
const { iv, stream } = await NNCrypto.createEncryptionStream(key);
await file
.stream()
.pipeThrough(new ChunkedStream(CHUNK_SIZE))
.pipeThrough(
new ChunkedStream(
CHUNK_SIZE,
isFeatureSupported("opfs") ? "copy" : "nocopy"
)
)
.pipeThrough(new IntoChunks(file.size))
.pipeThrough(stream)
.pipeThrough(
@@ -500,11 +517,17 @@ async function downloadFile(filename: string, requestOptions: RequestOptions) {
);
})
)
.pipeThrough(new ChunkedStream(chunkSize + ABYTES))
.pipeThrough(
new ChunkedStream(
chunkSize + ABYTES,
isFeatureSupported("opfs") ? "copy" : "nocopy"
)
)
.pipeTo(fileHandle.writeable);
return true;
} catch (e) {
console.error(e);
showError(toS3Error(e), "Could not download file");
reportProgress(undefined, { type: "download", hash: filename });
return false;
@@ -542,8 +565,6 @@ export async function decryptFile(
}
async function saveFile(filename: string, fileMetadata: FileMetadata) {
if (!fileMetadata) return false;
const { name, type, isUploaded } = fileMetadata;
const decrypted = await decryptFile(filename, fileMetadata);

View File

@@ -21,4 +21,7 @@ import { INNCrypto } from "@notesnook/crypto";
import CryptoWorker from "./nncrypto.worker?worker";
import { wrap } from "comlink";
export const NNCrypto = wrap<INNCrypto>(new CryptoWorker()) as INNCrypto;
export const NNCrypto =
IS_DESKTOP_APP && window.NativeNNCrypto
? new window.NativeNNCrypto()
: (wrap<INNCrypto>(new CryptoWorker()) as INNCrypto);

View File

@@ -0,0 +1,119 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { IFileStorage } from "@notesnook/streamable-fs/dist/src/interfaces";
import { File } from "@notesnook/streamable-fs/dist/src/types";
import { IndexedDBKVStore } from "./key-value";
import { expose, transfer } from "comlink";
class OriginPrivateFileStore implements IFileStorage {
private storage: IndexedDBKVStore;
constructor(
name: string,
private readonly directory: FileSystemDirectoryHandle
) {
this.storage = new IndexedDBKVStore(name, name);
}
async clear(): Promise<void> {
for await (const [name] of this.directory) {
await this.directory.removeEntry(name, { recursive: true });
}
await this.storage.clear();
}
setMetadata(filename: string, metadata: File): Promise<void> {
return this.storage.set(filename, metadata);
}
getMetadata(filename: string): Promise<File | undefined> {
return this.storage.get(filename);
}
deleteMetadata(filename: string): Promise<void> {
return this.storage.delete(filename);
}
async writeChunk(chunkName: string, data: Uint8Array): Promise<void> {
const file = await this.directory.getFileHandle(chunkName, {
create: true
});
const syncHandle = await file.createSyncAccessHandle();
syncHandle.write(data);
syncHandle.close();
}
async deleteChunk(chunkName: string) {
try {
await this.directory.removeEntry(chunkName);
} catch (e) {
console.error("Failed to delete chunk", e);
}
}
async readChunk(chunkName: string): Promise<Uint8Array | undefined> {
try {
const file = await this.directory.getFileHandle(chunkName);
const syncHandle = await file.createSyncAccessHandle();
const buffer = new Uint8Array(syncHandle.getSize());
syncHandle.read(buffer);
syncHandle.close();
return buffer;
} catch (e) {
console.error("Failed to read chunk", e);
return;
}
}
}
const fileStores: Map<string, OriginPrivateFileStore> = new Map();
const workerModule = {
async create(name: string, directoryName: string) {
const root = await navigator.storage.getDirectory();
const directoryHandle = await root.getDirectoryHandle(directoryName, {
create: true
});
fileStores.set(
directoryName,
new OriginPrivateFileStore(name, directoryHandle)
);
},
clear(directoryName: string) {
return fileStores.get(directoryName)?.clear();
},
setMetadata(directoryName: string, filename: string, metadata: File) {
return fileStores.get(directoryName)?.setMetadata(filename, metadata);
},
getMetadata(directoryName: string, filename: string) {
return fileStores.get(directoryName)?.getMetadata(filename);
},
deleteMetadata(directoryName: string, filename: string) {
return fileStores.get(directoryName)?.deleteMetadata(filename);
},
writeChunk(directoryName: string, chunkName: string, data: ArrayBuffer) {
return fileStores
.get(directoryName)
?.writeChunk(chunkName, new Uint8Array(data));
},
deleteChunk(directoryName: string, chunkName: string) {
return fileStores.get(directoryName)?.deleteChunk(chunkName);
},
async readChunk(directoryName: string, chunkName: string) {
const chunk = await fileStores.get(directoryName)?.readChunk(chunkName);
return chunk ? transfer(chunk, [chunk.buffer]) : undefined;
}
};
expose(workerModule);
export type OriginPrivateFileStoreWorkerType = typeof workerModule;

View File

@@ -22,6 +22,7 @@ import { store as noteStore } from "./note-store";
import { store as attachmentStore } from "./attachment-store";
import { store as appStore } from "./app-store";
import { store as tagStore } from "./tag-store";
import { store as settingStore } from "./setting-store";
import { db } from "../common/db";
import BaseStore from ".";
import { EV, EVENTS } from "@notesnook/core/dist/common";
@@ -151,7 +152,7 @@ class EditorStore extends BaseStore {
if (!note) return;
noteStore.setSelectedNote(note.id);
setDocumentTitle(note.title);
setDocumentTitle(settingStore.get().hideNoteTitle ? undefined : note.title);
if (note.locked)
return hashNavigate(`/notes/${noteId}/unlock`, { replace: true });
@@ -247,7 +248,9 @@ class EditorStore extends BaseStore {
state.session.dateEdited = note.dateEdited;
state.session.attachmentsLength = attachments.length;
});
setDocumentTitle(note.title);
setDocumentTitle(
settingStore.get().hideNoteTitle ? undefined : note.title
);
this.setSaveState(1);
} catch (err) {

View File

@@ -23,7 +23,9 @@ import { desktop } from "../common/desktop-bridge";
import createStore from "../common/store";
import Config from "../utils/config";
import BaseStore from "./index";
import { store as editorStore } from "./editor-store";
import { isTelemetryEnabled, setTelemetry } from "../utils/telemetry";
import { setDocumentTitle } from "../utils/dom";
/**
* @extends {BaseStore<SettingStore>}
@@ -40,6 +42,7 @@ class SettingStore extends BaseStore {
zoomFactor = 1.0;
privacyMode = false;
hideNoteTitle = Config.get("hideNoteTitle", false);
telemetry = isTelemetryEnabled();
/** @type {string} */
dateFormat = null;
@@ -49,21 +52,20 @@ class SettingStore extends BaseStore {
/** @type {number} */
trashCleanupInterval = 7;
homepage = Config.get("homepage", 0);
/**@type {"Sun" | "Mon"} */
weekFormat = null;
/**
* @type {DesktopIntegrationSettings | undefined}
*/
desktopIntegrationSettings = undefined;
autoUpdates = true;
isFlatpak = false;
refresh = async () => {
this.set({
dateFormat: db.settings.getDateFormat(),
timeFormat: db.settings.getTimeFormat(),
weekFormat: db.settings.getWeekFormat(),
titleFormat: db.settings.getTitleFormat(),
trashCleanupInterval: db.settings.getTrashCleanupInterval(),
isFlatpak: await desktop?.integration.isFlatpak.query(),
desktopIntegrationSettings:
await desktop?.integration.desktopIntegration.query(),
privacyMode: await desktop?.integration.privacyMode.query(),
@@ -82,11 +84,6 @@ class SettingStore extends BaseStore {
this.set({ timeFormat });
};
setWeekFormat = async (weekFormat) => {
await db.settings.setWeekFormat(weekFormat);
this.set({ weekFormat });
};
setTitleFormat = async (titleFormat) => {
await db.settings.setTitleFormat(titleFormat);
this.set({ titleFormat });
@@ -171,6 +168,15 @@ class SettingStore extends BaseStore {
await desktop?.integration.setPrivacyMode.mutate({ enabled: !privacyMode });
};
toggleHideTitle = async () => {
const { hideNoteTitle } = this.get();
this.set({ hideNoteTitle: !hideNoteTitle });
Config.set("hideNoteTitle", !hideNoteTitle);
setDocumentTitle(
!hideNoteTitle ? undefined : editorStore.get().session.title
);
};
toggleAutoUpdates = async () => {
const autoUpdates = this.get().autoUpdates;
this.set({ autoUpdates: !autoUpdates });

View File

@@ -112,7 +112,7 @@ class UserStore extends BaseStore {
});
};
login = async (form, skipInit = false) => {
login = async (form, skipInit = false, sessionExpired = false) => {
this.set((state) => (state.isLoggingIn = true));
const { email, password, code, method } = form;
@@ -120,7 +120,12 @@ class UserStore extends BaseStore {
if (code) {
return await db.user.authenticateMultiFactorCode(code, method);
} else if (password) {
await db.user.authenticatePassword(email, password, null);
await db.user.authenticatePassword(
email,
password,
null,
sessionExpired
);
Config.set("encryptBackups", true);
if (skipInit) return true;

View File

@@ -0,0 +1,60 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const FEATURE_CHECKS = {
opfs: false,
cache: false
};
async function isOPFSSupported() {
const hasGetDirectory =
"getDirectory" in window.navigator.storage &&
typeof window.navigator.storage.getDirectory === "function";
return (
hasGetDirectory &&
window.navigator.storage
.getDirectory()
.then(() => (FEATURE_CHECKS.opfs = true))
.catch(() => (FEATURE_CHECKS.opfs = false))
);
}
async function isCacheSupported() {
const hasCacheStorage =
"CacheStorage" in window &&
"caches" in window &&
window.caches instanceof CacheStorage;
return (
hasCacheStorage &&
window.caches
.has("something")
.then((f) => (FEATURE_CHECKS.cache = true))
.catch((a) => (FEATURE_CHECKS.cache = false))
);
}
export async function initializeFeatureChecks() {
await Promise.allSettled([isOPFSSupported(), isCacheSupported()]);
}
function isFeatureSupported(key: keyof typeof FEATURE_CHECKS) {
return FEATURE_CHECKS[key];
}
export { isFeatureSupported };

View File

@@ -18,8 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/db";
import FS from "../interfaces/fs";
import type { Note, Notebook } from "@notesnook-importer/core";
import {
ATTACHMENTS_DIRECTORY_NAME,
@@ -30,6 +28,7 @@ import { Reader, Entry } from "./zip-reader";
import { path } from "@notesnook-importer/core/dist/src/utils/path";
export async function* importFiles(zipFiles: File[]) {
const { default: FS } = await import("../interfaces/fs");
for (const zip of zipFiles) {
let count = 0;
let filesRead = 0;

View File

@@ -0,0 +1,26 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export async function lazify<T, R>(
loader: Promise<T>,
action: (module: T) => R | Promise<R>
): Promise<R> {
const module = await loader;
return await action(module);
}

View File

@@ -20,7 +20,7 @@ import { expose } from "comlink";
const module = {
async waitForInternet() {
let retries = 10;
let retries = 3;
while (retries-- > 0) {
try {
const response = await fetch("https://api.notesnook.com/health");

View File

@@ -129,5 +129,5 @@ export function isMac() {
}
export function isMacStoreApp() {
return window.os && window.os() === "mas";
return window.os ? window.os() === "mas" : false;
}

View File

@@ -17,8 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { decryptFile } from "../../interfaces/fs";
import { db } from "../../common/db";
import { lazify } from "../lazify";
import { ZipFile } from "./zip-stream";
export const METADATA_FILENAME = "metadata.json";
@@ -29,12 +29,13 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
signal?: AbortSignal,
onProgress?: (current: number) => void
) {
let index = 0;
const counters: Record<string, number> = {};
if (signal)
signal.onabort = async () => {
await db.fs?.cancel(GROUP_ID, "download");
};
let index = 0;
super({
start() {},
async pull(controller) {
@@ -54,18 +55,22 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
);
const key = await db.attachments?.decryptKey(attachment.key);
const file = await decryptFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
name: attachment.metadata.filename,
type: attachment.metadata.type,
isUploaded: !!attachment.dateUploaded
});
const file = await lazify(
import("../../interfaces/fs"),
({ decryptFile }) =>
decryptFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
name: attachment.metadata.filename,
type: attachment.metadata.type,
isUploaded: !!attachment.dateUploaded
})
);
if (file) {
const filePath = attachment.metadata.filename;
const filePath: string = attachment.metadata.filename;
controller.enqueue({
path: filePath,
path: makeUniqueFilename(filePath, counters),
data: new Uint8Array(await file.arrayBuffer())
});
} else {
@@ -79,3 +84,17 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
});
}
}
function makeUniqueFilename(
filePath: string,
counters: Record<string, number>
) {
filePath = filePath.toLowerCase();
counters[filePath] = (counters[filePath] || 0) + 1;
if (counters[filePath] === 1) return filePath;
const parts = filePath.split(".");
return `${parts.slice(0, -1).join(".")}-${counters[filePath]}.${
parts[parts.length - 1]
}`;
}

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Chunk } from "@notesnook/crypto";
export class ChunkedStream extends TransformStream<Uint8Array, Uint8Array> {
constructor(chunkSize: number) {
constructor(chunkSize: number, mode: "nocopy" | "copy") {
let backBuffer: Uint8Array | null = null;
super({
start() {},
@@ -38,7 +38,13 @@ export class ChunkedStream extends TransformStream<Uint8Array, Uint8Array> {
const start = backBuffer.length - remainingBytes;
const end = start + chunkSize;
controller.enqueue(backBuffer.subarray(start, end));
// TODO: find a way to support sending the chunked
// buffer to web workers without copying.
controller.enqueue(
mode === "copy"
? new Uint8Array(backBuffer.buffer.slice(start, end))
: backBuffer.subarray(start, end)
);
remainingBytes -= chunkSize;
}

View File

@@ -17,23 +17,25 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Unzipped } from "fflate";
import { AsyncZippable, zip as zipAsync } from "fflate";
import { sanitizeFilename } from "@notesnook/common";
const textEncoder = new TextEncoder();
type File = { filename: string; content: string };
type File = { filename: string; content: string; date: number };
async function zip(files: File[], format: string): Promise<Uint8Array> {
const obj: Unzipped = Object.create(null);
const obj: AsyncZippable = Object.create(null);
files.forEach((file) => {
const name = sanitizeFilename(file.filename, { replacement: "-" });
let counter = 0;
while (obj[makeFilename(name, format, counter)]) ++counter;
obj[makeFilename(name, format, counter)] = textEncoder.encode(file.content);
obj[makeFilename(name, format, counter)] = [
textEncoder.encode(file.content),
{ mtime: file.date }
];
});
const { zip } = await import("fflate");
return new Promise((resolve, reject) =>
zip(obj, (err, data) => (err ? reject(err) : resolve(data)))
zipAsync(obj, (err, data) => (err ? reject(err) : resolve(data)))
);
}
export { zip };

View File

@@ -278,10 +278,14 @@ function LoginPassword(props: BaseAuthComponentProps<"login:password">) {
subtitle: "Please wait while you are authenticated."
}}
onSubmit={async (form) => {
await userstore.login({
password: form.password,
email: formData.email
});
await userstore.login(
{
password: form.password,
email: formData.email
},
false,
Config.get("sessionExpired", false)
);
Config.set("sessionExpired", false);
openURL("/");
}}

View File

@@ -329,23 +329,14 @@ function RecoveryMethods(props: BaseRecoveryComponentProps<"methods">) {
function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
const { navigate } = props;
const [progress, setProgress] = useState("0");
const [progress, setProgress] = useState(0);
useEffect(() => {
db.eventManager.subscribe(
EVENTS.syncProgress,
({
type,
total,
current
}: {
type: string;
total: number;
current: number;
}) => {
if (total === current) return;
({ type, current }: { type: string; current: number }) => {
if (type === "download") {
setProgress(((current / total) * 100).toFixed());
setProgress(current);
}
}
);
@@ -358,11 +349,11 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
title="Recover your account"
subtitle={"Use a data recovery key to reset your account password."}
loading={{
title: `Downloading your data (${progress}%)`,
title: `Downloading your data (${progress})`,
subtitle: "Please wait while your data is downloaded & decrypted."
}}
onSubmit={async (form) => {
setProgress("0");
setProgress(0);
const user = await db.user?.getUser();
if (!user) throw new Error("User not authenticated");
@@ -479,14 +470,13 @@ function BackupData(props: BaseRecoveryComponentProps<"backup">) {
function NewPassword(props: BaseRecoveryComponentProps<"new">) {
const { navigate, formData } = props;
const [progress, setProgress] = useState("0");
const [progress, setProgress] = useState(0);
useEffect(() => {
db.eventManager.subscribe(
EVENTS.syncProgress,
({ total, current }: { total: number; current: number }) => {
if (total === current) return;
setProgress(((current / total) * 100).toFixed());
({ current }: { current: number }) => {
setProgress(current);
}
);
}, []);
@@ -500,11 +490,11 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
"Notesnook is E2E encrypted — your password never leaves this device."
}
loading={{
title: `Resetting account password (${progress}%)`,
title: `Resetting account password (${progress})`,
subtitle: "Please wait while we reset your account password."
}}
onSubmit={async (form) => {
setProgress("0");
setProgress(0);
if (form.password !== form.confirmPassword)
throw new Error("Passwords do not match.");

View File

@@ -8,6 +8,6 @@ Unfortunately, we cannot find what you are looking for.
Here are some other pages you might be interested in:
1. [Installing Notesnook](installation)
1. [Installing Notesnook](organizing-notes/organize-notes-using-notebooks)
2. [Creating your first note](create-a-note-in-notesnook)
3. [How is my data encrypted?](how-is-my-data-encrypted)

View File

@@ -11,4 +11,6 @@ Notesnook is our **proof** that privacy does _not_ (always) have to come at the
And with that convenience in mind, we believe that it is equally important to put together a simple & useful help website about everything Notesnook. Our help is a complete walkthrough of the Notesnook app and it's features.
So let's get started with [installing Notesnook](/installation) on all your devices.
Before we get started, let's [download & install Notesnook](https://notesnook.com/downloads) on all your devices.
Once that's done, let's learn how to [create your first note](/create-a-note-in-notesnook) in Notesnook.

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -163,6 +163,10 @@ nav li {
line-height: 24px;
}
li.nested {
min-height: 38px !important;
}
nav li li:not(li.nested):hover {
background-color: var(--hover-bg);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -5,41 +5,58 @@ description: Notesnook allows you to backup all your notes data to a single back
# Backup your notes
It is always a good practice to take a backup of your data so in case you lose access to your account, you don't lose your notes. Currently backups are local only.
It is always a good practice to take regular backups of your data so you can easily recover your data in case of data corruption or losing access to your account. All backups are stored locally encrypted (unless you turn off backup encryption) on your device.
> warning
> error Store your password & recovery key safely
>
> Since all your data is end-to-end encrypted, we do not have the access to restore your account if you forgot your account password and recovery key. Therefore we recommend that you store your password safely and make sure that you have saved your account recovery.
> Since all your data is end-to-end encrypted, we have no way to restore your account data if you forget your account password and lose your account recovery key. That's why we recommend that you store your password & recovery key in a password manager or some other safe place.
# [Desktop/Web](#/tab/web)
1. Go to Settings from Sidebar
2. Scroll down to `Backup & Restore` section
3. Click on `Backup Data` to download the `.nnbackup` file
1. Go to Settings
2. Scroll down in the Settings navigation menu and click on `Backup & export` section
3. Click on `Create backup` under `Backup now` heading to create a new `.nnbackupz` file
![](/create-backup-web.png)
# [Mobile](#/tab/mobile)
1. Go to Settings from Sidebar
2. Scroll down to `Backup and Restore`
3. Go to `Backups`
4. Press on `Backup now` to save `.nnbackup` file
3. Tap on `Backups`
4. Press on `Backup now` to create a new `.nnbackupz` file
> info
>
> On **Android** when you take a backup first time, you will be asked to select a folder on your phone where you would like to store backup files. You can always change your backup files location from `Backups > Change backup location`.
> On **Android** when you take a backup for the first time, you will be asked to select a folder where you want to store all your backup files. You can always change your backup files location from `Backups > Select backup directory`.
>
> Regardless of the folder you select, Notesnook will create a folder "Notesnook/backups" inside it and store backup files there.
> Regardless of the folder you select, Notesnook will create a folder "Notesnook/backups" inside it and store all backup files there.
---
## Automatic Backups
On mobile and desktop apps you can enable automatic daily, weekly or monthly backups of your notes. Your data will backup automatically and backup file will be stored locally.
For maximum safety against potential data loss, you can enable daily, weekly or monthly backups of your notes. Enabling automatic backups will ensure that all your data is safely backed up locally after a regular interval.
# [Desktop/Web](#/tab/web)
# [Desktop](#/tab/desktop)
1. Go to Settings > Backup & Restore
2. Select automatic backup frequency to enable automatic backups
1. Go to Settings
2. Scroll down in the Settings navigation menu and click on `Backup & export` section
3. Select the Automatic backups interval from the dropdown
![](/auto-backups-desktop.png)
# [Web](#/tab/web)
> info
>
> On the **web** app there is no way to automatically save backups to a folder, that is why Notesnook only reminds the users when it's time to create a new backup.
1. Go to Settings
2. Scroll down in the Settings navigation menu and click on `Backup & export` section
3. Select the Backup reminders interval from the dropdown
![](/auto-backups-web.png)
# [Mobile](#/tab/mobile)
@@ -51,41 +68,44 @@ On mobile and desktop apps you can enable automatic daily, weekly or monthly bac
## Encrypted Backups (Recommended)
To keep your backups secure & private, it is recommended that you enable encryption on your backup files instead of storing them as plain json data.
To keep your backups secure & private, it is recommended that you enable encryption on your backup files instead of storing them as plaintext data. **Starting from v2.6.0, encrypted backups are enabled by default for all users, free or Pro.**
# [Desktop/Web](#/tab/web)
1. Go to Settings > Backup & Restore
2. Enable encrypted backups option
1. Go to Settings
2. Scroll down in the Settings navigation menu and click on `Backup & export` section
3. Click on the toggle next to `Backup encryption` to enable/disable encrypted backups
# [Mobile](#/tab/mobile)
1. Go to Settings > Backup & Restore
2. Press Backups
3. Enable encrypted backups option
1. Go to `Settings` > `Backup & Restore`
2. Tap on `Backups`
3. Tap on the toggle next to `Backup encryption` to enable/disable encrypted backups
---
> info
>
> Backups are encrypted with your account password.
> Backups are always encrypted with your account password.
# Restore a backup
To restore a backup, you must be logged in to your Notesnook account.
At any point in time, you can restore a backup to recover lost data. However, **to restore a backup, you must be logged in to your Notesnook account.** Backups created on one account can be restored on another Notesnook account.
# [Desktop/Web](#/tab/web)
1. Go to Settings from Sidebar
2. Scroll down to `Backup & Restore` section
3. Click on `Restore backup`
4. Select the `.nnbackup` file from your PC that you want to restore.
1. Go to Settings
2. Scroll down in the Settings navigation menu and click on `Backup & export` section
3. Click on `Restore` button next to `Restore backup` heading
4. Select the `.nnbackupz` or `.nnbackup` file from your PC that you want to restore.
# [Mobile](#/tab/mobile)
1. Go to Settings from Sidebar
2. Scroll down to `Backup & Restore` section
3. Press on `Restore backup`
4. From `Restore backup` sheet, select the backup you want to restore. If your backup file is located in some other location, click on `Open file manager` on top right corner of the sheet then select the backup file.
3. Tap on `Restore backup`
4. From `Restore backup` sheet, select the backup you want to restore. If your backup file is located in some other location, click on `Restore from files` on top right corner of the sheet then select the backup file.
<img src="/restore-backup-mobile.png" height="700px">
---

View File

@@ -5,33 +5,34 @@ description: Notesnook let's you create unlimited notes for free. Learn how you
# Creating your first note
You are in a notes app, the first thing you want to do is make a note.
You are in a note taking app, the first thing you'd want to do is create a note.
# [Desktop/Web](#/tab/web)
1. Click on the `+` button on top right corner or click anywhere inside the editor to focus it.
2. Start typing in the editor and your note will be saved.
2. Start typing in the editor and a new note will be automatically created.
3. As you type, your note is saved automatically whenever you stop for a few seconds.
![First note web](/first-note-desktop.png)
> info
>
> The bottom right corner of the app will show the no. of words of current note & the last saved time.
> The bottom right corner of the app will show the number of words of current note & the last saved time.
>
> ![Status bar desktop](/editor-status-bar-desktop.png)
# [Mobile](#/tab/mobile)
1. On mobile, just like desktop, regardless of what screen you have opened, you can always swipe from right to left and open the editor.
2. You can also press the `+` button on bottom right corner to open the editor when you are in notes, a topic or tag.
3. As you type, your note is saved automatically whenever you stop for a few seconds.
1. On mobile regardless of what screen you are on, you can swipe from right to left to open the editor.
2. You can also press the `+` button on bottom right corner to open the editor (note: the `+` button is visible only on some screens).
3. Start typing in the editor and a new note will be automatically created.
4. As you type, your note is saved automatically whenever you stop for a few seconds.
![First note mobile](/first-note-mobile.png)
> info
>
> Below the note title is the editor status bar. It shows you the no. of words in the note and last saved time.
> Below the note title is the editor status bar. It shows you the number of words in the note and last saved time.
---

View File

@@ -12,7 +12,7 @@ Notesnook allows you to delete your accounts and your data without any questions
1. Open Notesnook web or desktop app
2. Make sure you are logged in
3. Go to Settings
4. Scroll to the very end & click on `Delete account`
4. Click on `Delete account` button next to **Account removal** heading
## [Mobile](#/tab/mobile)
@@ -20,7 +20,7 @@ Notesnook allows you to delete your accounts and your data without any questions
2. Make sure you are logged in
3. Go to Settings
4. Go to `Account Settings`
5. Click on `Delete account`
5. Tap on `Delete account`
---
@@ -32,4 +32,4 @@ All your notes, notebooks, attachments, and any other data that you created with
### Is there any way to recover deleted data?
No. Once you delete your account, it's gone for good and there is no way for us or anyone else to recover it. If your data is important to you, make sure to take a [backup](/backup-and-restore-notes-in-notesnook) before deleting your account or [exporting your notes](/export-notes-from-notesnook).
No. Once you delete your account, it's gone for good and there is no way for us or anyone else to recover it. If your data is important to you, make sure to take a [backup](/backup-and-restore-notes-in-notesnook) or [export your notes](/export-notes-from-notesnook) before deleting your account.

View File

@@ -4,6 +4,8 @@ Notesnook supports quick actions from the "jumplist" menu (dock menu on macOS):
## [Windows](#/tab/windows)
![](/static/desktop-integration/jumplist-menu-windows.png)
## [Linux](#/tab/linux)
![](/static/desktop-integration/jumplist-menu-linux.png)
@@ -16,9 +18,8 @@ Notesnook supports quick actions from the "jumplist" menu (dock menu on macOS):
## How it works?
The jumplist menu doesn't work if you already have Notesnook opened. They are responsible for opening Notesnook at the specified page. For example:
Jumplist menu items are responsible for opening Notesnook at the specified page. For example:
1. Close Notesnook if you already have it opened
2. Right click on the Notesnook icon in your taskbar/dock
3. Click on "New notebook"
4. Notice how the new notebook dialog is already opened after the app starts.
1. Right click on the Notesnook icon in your taskbar/dock
2. Click on "New notebook"
3. Notice how the new notebook dialog is opened after the Notesnook app is focused

View File

@@ -5,23 +5,23 @@ description: Notesnook is zero lock-in. You can always export and backup your no
# Exporting notes
You can export your notes as PDF, HTML, Markdown and Plain text files on desktop and mobile apps.
You can export some or all your notes as PDF, HTML, Markdown and Plain text files on all Notesnook apps.
## Exporting a single note
# [Desktop/Web](#/tab/web)
1. Right click on a note
2. Click on 'Export'
3. Select the desired format to export your note
2. Click on `Export as`
3. Select the desired format
4. Wait a few moments while your note is exported
5. Save the note in desired location
5. Save the note at your desired location
# [Mobile](#/tab/mobile)
1. Press the ![Three dot button](/three-dot-button.png) button on a note
2. Press on 'Export'
3. Select the desired format to export your note
1. Tap the ![Three dot button](/three-dot-button.png) button on a note
2. Tap on `Export`
3. Select the desired format
4. Wait a few moments while your note is exported
5. Exported notes are stored in `Notesnook/exported` folder.
@@ -31,20 +31,19 @@ You can export your notes as PDF, HTML, Markdown and Plain text files on desktop
# [Desktop/Web](#/tab/web)
1. Hold `Ctrl` & Click on a note to enter multi-selection mode
2. Select the notes you want to export then right click
3. Click on "Export as" & select your desired export format
1. Hold `Ctrl` & click on all the notes you want to export
2. Right click & click on `Export as`
3. Select your desired export format
4. Wait a few moments while your notes are exported
5. Save the note in desired location
5. Save the `.zip` file at your desired location
# [Mobile](#/tab/mobile)
1. Long press on a note item
2. Press on "Select" on the action bar to enter selection mode
3. Select the notes you want to export
4. Press on the export icon on top right corner
5. Select the desired format to export your note
6. Exported notes are stored in `Notesnook/exported` folder
1. Long press on a note to enter multi selection mode
2. Tap on all the notes you want to export
3. Press on the Export button on top right corner
4. Select the desired format
5. Exported notes are stored in `Notesnook/exported` folder
---
@@ -52,14 +51,19 @@ You can export your notes as PDF, HTML, Markdown and Plain text files on desktop
# [Desktop/Web](#/tab/web)
1. Go to Settings from Sidebar
2. Scroll down to `Backup & Restore` section
3. Click on `Backup Data` to download the `.nnbackup` file
1. Go to Settings
2. Scroll down to `Import & export` section
3. Click on `Backup & export`
4. Click on `Select format` dropdown next to `Export all notes` heading
5. Select the desired format
6. Enter account password for authentication
7. Save the `.zip` file at your desired location
# [Mobile](#/tab/mobile)
1. Go to Settings from Sidebar
2. Scroll down to `Backup and Restore`
3. Select "Export all notes"
4. Select the desired format to export your notes
5. Exported notes are stored in `Notesnook/exported` folder as a single .zip file
3. Tap on `Export all notes`
4. Select the desired format
5. Enter account password for authentication
6. Exported notes are stored in `Notesnook/exported` folder as a single .zip file

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