Compare commits

...

73 Commits
v3.0.4 ... ai

Author SHA1 Message Date
Abdullah Atta
f751db673e web: benchmark different embedding models 2024-06-01 11:14:11 +05:00
Abdullah Atta
6d9af85e2a web: fix background color of count in nav item 2024-05-21 13:14:56 +05:00
Abdullah Atta
1d58cfea3c web: fix navigation bar in settings dialog not themed 2024-05-21 13:14:56 +05:00
Abdullah Atta
c1fa4d3400 web: fix wrong version saved on conflict resolve 2024-05-21 13:14:42 +05:00
Abdullah Atta
df179056d6 logger: fix logger in core not working 2024-05-21 12:19:39 +05:00
Abdullah Atta
26f9e08c23 web: fix sync not running on page focus 2024-05-21 10:47:25 +05:00
Abdullah Atta
f719bebe5a core: create triggers after resetting database 2024-05-21 09:59:20 +05:00
Abdullah Atta
9693c8ea54 desktop: run sqlite on another thread 2024-05-21 09:59:20 +05:00
Abdullah Atta
040a677895 core: add support for closing logger db 2024-05-21 09:59:20 +05:00
Ammar Ahmed
df2708147e mobile: release v3.0.8 2024-05-20 15:35:59 +05:00
Ammar Ahmed
faba5f6385 mobile: always recreate editor on changing note 2024-05-20 15:08:27 +05:00
Ammar Ahmed
eab5bccb99 mobile: use sqlite based logger 2024-05-20 15:05:18 +05:00
Abdullah Atta
ed399c0d0a web: use noop logger before initializing 2024-05-20 15:05:18 +05:00
Abdullah Atta
3c7e962fe0 logger: minor refactor 2024-05-20 15:05:18 +05:00
Abdullah Atta
56614ef19d core: use chunks and set date only once 2024-05-20 15:05:18 +05:00
Abdullah Atta
169318c549 core: remove attachments initialized log 2024-05-20 15:05:18 +05:00
Abdullah Atta
bd6a1b9bff web: add support for opening multiple sqlite databases 2024-05-20 15:05:18 +05:00
Abdullah Atta
443d6625f9 core: add support for deleting logs for a specific date 2024-05-20 15:05:18 +05:00
Abdullah Atta
b6f95ff7cd core: use sqlite for storing logs 2024-05-20 15:05:18 +05:00
Ammar Ahmed
1b13da54a1 mobile: fix image extension 2024-05-20 12:55:34 +05:00
Ammar Ahmed
adf0f132fd mobile: convert heic/heif images to jpeg 2024-05-20 12:55:34 +05:00
Ammar Ahmed
73ec04b42d mobile: fix error when writing user key 2024-05-20 10:45:02 +05:00
Abdullah Atta
54a68e4406 themebuilder: bump version to 1.2.0 2024-05-18 13:09:49 +05:00
Abdullah Atta
bd73ae9df4 theme: add titleBar to theme scopes 2024-05-18 13:09:49 +05:00
Abdullah Atta
fe41f7b687 themebuilder: make theme builder compile 2024-05-18 13:09:49 +05:00
Ammar Ahmed
c9adaa2442 mobile: release v3.0.7 2024-05-17 12:41:13 +05:00
Abdullah Atta
10be5771c6 web: bump version to 3.0.6 2024-05-17 12:31:22 +05:00
Abdullah Atta
43f4669360 core: fix notes with no conflict marked as conflicted 2024-05-17 12:29:58 +05:00
Ammar Ahmed
3b04925e54 mobile: update changelog 2024-05-17 09:58:43 +05:00
Ammar Ahmed
ba3959af9e mobile: release v3.0.6 2024-05-17 09:58:08 +05:00
Ammar Ahmed
552d86d9d2 mobile: fix android compilation on java 17 2024-05-17 09:56:51 +05:00
Ammar Ahmed
096602150e mobile: fix encryption key not found error on login 2024-05-17 09:56:51 +05:00
Abdullah Atta
e5795bcbb2 web: fix app asking password on auto backups with encryption off 2024-05-16 16:25:04 +05:00
Ammar Ahmed
9be52a69ad mobile: add changelog 2024-05-16 15:45:38 +05:00
Ammar Ahmed
5809384f62 mobile: release v3.0.5 2024-05-16 15:44:48 +05:00
Ammar Ahmed
fff70dce16 mobile: fix recovery key dialog ui 2024-05-16 15:14:14 +05:00
Ammar Ahmed
258140b1e0 mobile: fix share widget title issues
1. Do not change title of note when choosing append to note
2. Fix color of title in widget
2024-05-16 12:07:29 +05:00
Ammar Ahmed
0a00844e64 mobile: fix locked note title cannot be edited 2024-05-16 12:07:05 +05:00
Abdullah Atta
03cea85efc web: bump version to 3.0.5 2024-05-16 11:57:12 +05:00
Abdullah Atta
8a16ef9b85 web: fix unlock a note for editing test 2024-05-16 11:55:50 +05:00
Abdullah Atta
c82c30b530 editor: fix search replace 2024-05-16 11:34:10 +05:00
Abdullah Atta
46599f66da web: fix search not working in readonly editor 2024-05-16 11:33:53 +05:00
Abdullah Atta
d1b532a4ae editor: show (empty) toolbar in readonly mode 2024-05-16 11:33:53 +05:00
Abdullah Atta
0baf7afc25 web: allow locked note duplication without unlocking 2024-05-16 09:31:46 +05:00
Abdullah Atta
a0595a7d23 core: copy all note relations when duplicating 2024-05-16 09:31:46 +05:00
Abdullah Atta
1e6f6ab477 core: fix Cannot read properties of undefined (reading 'startsWith') error on export 2024-05-16 09:31:18 +05:00
Abdullah Atta
62d677f9ad web: fix web app not working on safari 2024-05-16 09:30:49 +05:00
Abdullah Atta
8de98d98ab core: fix merger tests 2024-05-16 09:30:28 +05:00
Ammar Ahmed
5b57614bfe mobile: fix app lock issues 2024-05-16 09:27:27 +05:00
Ammar Ahmed
ae7c538ad7 mobile: enable privacy screen with applock 2024-05-16 09:27:27 +05:00
Ammar Ahmed
c6394c9e37 mobile: allow removing app lock with account password 2024-05-16 09:27:27 +05:00
Ammar Ahmed
acf3740a68 mobile: fix pricing plans stuck at loading 2024-05-16 09:24:19 +05:00
Ammar Ahmed
8423543904 mobile: show diff in note history 2024-05-16 09:23:45 +05:00
Abdullah Atta
b90cb28637 web: handle corrupted+migrations: error 2024-05-16 08:40:03 +05:00
Abdullah Atta
780e1e9db0 web: reopen editor session if conflict is resolved/created 2024-05-15 12:37:50 +05:00
Abdullah Atta
ca6f8e5e4b web: fix real time sync ignored when resolved content is synced 2024-05-15 12:37:08 +05:00
Abdullah Atta
4c4cda1c39 core: create conflict if local item is unsynced
previously we were only creating a conflict when the local item was
edited after the remote item. This failed in cases where even though
the local item was edited before the remote item, it still contained
important information.

Now we only check if the local item is synced or not. If it isn't synced,
we most probably want to preserve the local changes, or at least want
to be notified.
2024-05-15 12:36:38 +05:00
Abdullah Atta
6fcbde772c core: fix notes marked as conflicted wrongly 2024-05-15 12:34:23 +05:00
Abdullah Atta
eac1750b1c core: add test for merge conflicts during sync 2024-05-14 15:21:00 +05:00
Abdullah Atta
5eea179a58 core: set conflicted property on notes after fetch is done 2024-05-14 15:21:00 +05:00
Abdullah Atta
fe1ea6dc27 core: do not update dateModified when updating content internally 2024-05-14 15:21:00 +05:00
Abdullah Atta
cf1b774255 core: filter out conflicted notes & content during sync 2024-05-14 15:21:00 +05:00
Abdullah Atta
198fa33fc0 core: do not update dateModified when setting synced=true 2024-05-14 15:21:00 +05:00
Abdullah Atta
e722866d62 web: better handling of database disk image is malformed error 2024-05-14 15:21:00 +05:00
Ammar Ahmed
58845e5508 mobile: release v3.0.4 2024-05-11 15:19:12 +05:00
Abdullah Atta
543c6f17ed web: enable sourcemaps for web app 2024-05-11 13:09:59 +05:00
Abdullah Atta
533f95fcff editor: fix editor crash due to TypeError: t.startsWith is not a function 2024-05-11 12:53:30 +05:00
Ammar Ahmed
c02f1cde16 mobile: disable template when pin note to notifications 2024-05-11 12:49:11 +05:00
Ammar Ahmed
043f55ead0 mobile: fix exporting note to text for share/copy 2024-05-11 12:49:11 +05:00
Ammar Ahmed
fcece06a1c mobile: fix update notification not showing on mobile app 2024-05-11 12:49:11 +05:00
Abdullah Atta
da7d79edc6 core: token without a scope is invalid 2024-05-11 12:36:23 +05:00
Ammar Ahmed
a2e7190a4a mobile: fix colors of error popup in editor 2024-05-11 11:26:15 +05:00
Ammar Ahmed
503f2b5876 mobile: fix cannot read property 'groupBy' of undefined 2024-05-11 11:22:59 +05:00
113 changed files with 4656 additions and 1248 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.0.4",
"version": "3.0.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.0.4",
"version": "3.0.6",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -29,6 +29,7 @@
"electron": "^29.3.1",
"electron-builder": "^24.13.3",
"esbuild": "^0.20.0",
"kysely": "^0.27.3",
"node-gyp-build": "^4.8.0",
"prebuildify": "^6.0.1",
"tree-kill": "^1.2.2",
@@ -112,7 +113,6 @@
"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",
@@ -133,7 +133,6 @@
"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",
@@ -147,7 +146,6 @@
"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"
}
@@ -156,7 +154,6 @@
"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"
}
@@ -165,7 +162,6 @@
"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"
}
@@ -821,7 +817,6 @@
"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"
},
@@ -833,7 +828,6 @@
"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"
},
@@ -878,7 +872,6 @@
"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",
@@ -907,14 +900,12 @@
"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==",
"dev": true
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
},
"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": "*"
}
@@ -928,8 +919,7 @@
"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==",
"devOptional": true
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA=="
},
"node_modules/@types/plist": {
"version": "3.0.2",
@@ -945,7 +935,6 @@
"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": "*"
}
@@ -975,7 +964,6 @@
"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": "*"
@@ -1120,6 +1108,80 @@
"node": ">=12.0.0"
}
},
"node_modules/archiver": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
"dev": true,
"peer": true,
"dependencies": {
"archiver-utils": "^2.1.0",
"async": "^3.2.4",
"buffer-crc32": "^0.2.1",
"readable-stream": "^3.6.0",
"readdir-glob": "^1.1.2",
"tar-stream": "^2.2.0",
"zip-stream": "^4.1.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/archiver-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
"peer": true,
"dependencies": {
"glob": "^7.1.4",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^2.0.0"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/archiver-utils/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/archiver-utils/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"peer": true
},
"node_modules/archiver-utils/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1259,7 +1321,6 @@
"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": {
@@ -1310,7 +1371,6 @@
"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": "*"
}
@@ -1386,7 +1446,6 @@
"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"
}
@@ -1395,7 +1454,6 @@
"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",
@@ -1515,7 +1573,6 @@
"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"
},
@@ -1569,6 +1626,22 @@
"node": ">=0.10.0"
}
},
"node_modules/compress-commons": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
"dev": true,
"peer": true,
"dependencies": {
"buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1635,7 +1708,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"optional": true
"devOptional": true
},
"node_modules/crc": {
"version": "3.8.0",
@@ -1646,6 +1719,33 @@
"buffer": "^5.1.0"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"dev": true,
"peer": true,
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc32-stream": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
"dev": true,
"peer": true,
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1738,7 +1838,6 @@
"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"
}
@@ -1747,7 +1846,6 @@
"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",
@@ -1781,7 +1879,6 @@
"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": {
@@ -1911,7 +2008,6 @@
"version": "29.3.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-29.3.1.tgz",
"integrity": "sha512-auge1/6RVqgUd6TgIq88wKdUCJi2cjESi3jy7d+6X4JzvBGprKBqMJ8JSSFpu/Px1YJrFUKAxfy6SC+TQf1uLw==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@electron/get": "^2.0.0",
@@ -1951,6 +2047,19 @@
"node": ">=14.0.0"
}
},
"node_modules/electron-builder-squirrel-windows": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz",
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
"dev": true,
"peer": true,
"dependencies": {
"app-builder-lib": "24.13.3",
"archiver": "^5.3.1",
"builder-util": "24.13.1",
"fs-extra": "^10.1.0"
}
},
"node_modules/electron-builder/node_modules/builder-util-runtime": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
@@ -2024,7 +2133,6 @@
"version": "20.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -2046,7 +2154,6 @@
"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"
}
@@ -2061,7 +2168,6 @@
"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": {
@@ -2114,7 +2220,6 @@
"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"
@@ -2135,7 +2240,6 @@
"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",
@@ -2176,7 +2280,6 @@
"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"
}
@@ -2319,7 +2422,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true,
"optional": true
},
"node_modules/get-caller-file": {
@@ -2334,7 +2436,6 @@
"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",
@@ -2350,7 +2451,6 @@
"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"
},
@@ -2424,7 +2524,6 @@
"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",
@@ -2442,7 +2541,6 @@
"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"
@@ -2458,7 +2556,6 @@
"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",
@@ -2488,7 +2585,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"optional": true,
"dependencies": {
"function-bind": "^1.1.1"
@@ -2510,7 +2606,6 @@
"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"
@@ -2523,7 +2618,6 @@
"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"
@@ -2536,7 +2630,6 @@
"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"
@@ -2560,8 +2653,7 @@
"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==",
"dev": true
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
@@ -2581,7 +2673,6 @@
"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"
@@ -2748,6 +2839,13 @@
"node": ">=0.12.0"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"peer": true
},
"node_modules/isbinaryfile": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz",
@@ -2843,8 +2941,7 @@
"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==",
"dev": true
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
@@ -2856,7 +2953,6 @@
"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": {
@@ -2886,37 +2982,125 @@
"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"
}
},
"node_modules/kysely": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.3.tgz",
"integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==",
"dev": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/lazy-val": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="
},
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"dev": true,
"peer": true,
"dependencies": {
"readable-stream": "^2.0.5"
},
"engines": {
"node": ">= 0.6.3"
}
},
"node_modules/lazystream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/lazystream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"peer": true
},
"node_modules/lazystream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true,
"peer": true
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true,
"peer": true
},
"node_modules/lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"dev": true,
"peer": true
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"peer": true
},
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
"dev": true,
"peer": true
},
"node_modules/lowercase-keys": {
"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"
}
@@ -2936,7 +3120,6 @@
"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"
@@ -2982,7 +3165,6 @@
"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"
}
@@ -3108,7 +3290,6 @@
"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"
},
@@ -3132,7 +3313,6 @@
"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"
@@ -3150,7 +3330,6 @@
"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"
}
@@ -3213,8 +3392,7 @@
"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==",
"dev": true
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -3291,11 +3469,17 @@
"prebuildify": "bin.js"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"peer": true
},
"node_modules/progress": {
"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"
}
@@ -3335,7 +3519,6 @@
"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"
},
@@ -3402,6 +3585,16 @@
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/readdir-glob": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"dev": true,
"peer": true,
"dependencies": {
"minimatch": "^5.1.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -3425,14 +3618,12 @@
"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==",
"dev": true
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
},
"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"
},
@@ -3453,7 +3644,6 @@
"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",
@@ -3533,14 +3723,12 @@
"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"
@@ -3556,7 +3744,6 @@
"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"
@@ -3709,7 +3896,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"dev": true,
"optional": true
},
"node_modules/stat-mode": {
@@ -3809,7 +3995,6 @@
"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"
},
@@ -4011,8 +4196,7 @@
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/universalify": {
"version": "2.0.0",
@@ -4161,12 +4345,48 @@
"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"
}
},
"node_modules/zip-stream": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
"dev": true,
"peer": true,
"dependencies": {
"archiver-utils": "^3.0.4",
"compress-commons": "^4.1.2",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/zip-stream/node_modules/archiver-utils": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
"dev": true,
"peer": true,
"dependencies": {
"glob": "^7.2.3",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/zod": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.4",
"version": "3.0.6",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
@@ -30,6 +30,7 @@
"electron": "^29.3.1",
"electron-builder": "^24.13.3",
"esbuild": "^0.20.0",
"kysely": "^0.27.3",
"node-gyp-build": "^4.8.0",
"prebuildify": "^6.0.1",
"tree-kill": "^1.2.2",

View File

@@ -0,0 +1,143 @@
/*
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 type { Database, Statement } from "better-sqlite3-multiple-ciphers";
import type { QueryResult } from "kysely";
type SQLiteCompatibleType =
| number
| string
| Uint8Array
| Array<number>
| bigint
| null;
export class SQLite {
sqlite?: Database;
initialized = false;
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
retryCounter: Record<string, number> = {};
constructor() {
console.log("new sqlite worker");
}
async open(filePath: string) {
if (this.sqlite) {
console.error("Database is already initialized");
return;
}
this.sqlite = require("better-sqlite3-multiple-ciphers")(
filePath
).unsafeMode(true);
}
/**
* Wrapper function for preparing SQL statements with caching
* to avoid unnecessary computations.
*/
async prepare(sql: string): Promise<Statement | undefined> {
if (!this.sqlite) throw new Error("Database is not initialized.");
try {
const cached = this.preparedStatements.get(sql);
if (cached !== undefined) return cached;
const prepared = this.sqlite.prepare(sql);
if (!prepared) return;
this.preparedStatements.set(sql, prepared);
// reset retry count on success
this.retryCounter[sql] = 0;
return prepared;
} catch (ex) {
console.error(ex);
// statement prepare process can be flaky so retry at least 5 times
// before giving up.
if (this.retryCounter[sql] < 5) {
this.retryCounter[sql] = (this.retryCounter[sql] || 0) + 1;
console.warn("Failed to prepare statement. Retrying:", sql);
return this.prepare(sql);
} else this.retryCounter[sql] = 0;
if (ex instanceof Error) ex.message += ` (query: ${sql})`;
throw ex;
}
}
async exec<R>(
sql: string,
parameters: SQLiteCompatibleType[] = []
): Promise<QueryResult<R>> {
const prepared = await this.prepare(sql);
if (!prepared) return { rows: [] };
try {
if (prepared.reader) {
return {
rows: prepared.all(parameters) as R[]
};
} else {
const { changes, lastInsertRowid } = prepared.run(parameters);
const numAffectedRows =
changes !== undefined && changes !== null && !isNaN(changes)
? BigInt(changes)
: undefined;
return {
numAffectedRows,
insertId:
lastInsertRowid !== undefined && lastInsertRowid !== null
? typeof lastInsertRowid === "bigint"
? lastInsertRowid
: BigInt(lastInsertRowid)
: undefined,
rows: [] as R[]
};
}
} catch (e) {
if (e instanceof Error) e.message += ` (query: ${sql})`;
throw e;
}
}
async run<R>(
sql: string,
parameters?: SQLiteCompatibleType[]
): Promise<QueryResult<R>> {
if (!this.sqlite) throw new Error("No database is not opened.");
return await this.exec(sql, parameters);
}
async close() {
if (!this.sqlite) return;
this.preparedStatements.clear();
this.sqlite.close();
this.sqlite = undefined;
}
async delete(filePath: string) {
await this.close();
await require("fs/promises").rm(filePath, {
force: true,
maxRetries: 5,
retryDelay: 500
});
}
}

View File

@@ -21,3 +21,4 @@ export * from "./constants";
export type { AppRouter } from "./api";
export { type UpdateInfo } from "builder-util-runtime";
export { type DesktopIntegration } from "./utils/config";
export { SQLite } from "./api/sqlite-kysely.js";

View File

@@ -100,6 +100,7 @@ async function createWindow() {
zoomFactor: config.zoomFactor,
nodeIntegration: true,
contextIsolation: false,
nodeIntegrationInWorker: true,
spellcheck: config.isSpellCheckerEnabled,
preload: __dirname + "/preload.js"
}

View File

@@ -23,13 +23,11 @@ import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
import type { NNCrypto } from "@notesnook/crypto";
import { ipcRenderer } from "electron";
import { platform } from "os";
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
declare global {
var os: () => "mas" | ReturnType<typeof platform>;
var electronTRPC: RendererGlobalElectronTRPC;
var NativeNNCrypto: (new () => NNCrypto) | undefined;
var createSQLite3Database: (filename: string) => Database;
}
process.once("loaded", async () => {
@@ -43,5 +41,4 @@ process.once("loaded", async () => {
});
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());

View File

@@ -16,15 +16,16 @@ 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 "react-native-gesture-handler";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { I18nManager, View } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { initializeLogger } from "./common/database/logger";
import AppLockedOverlay from "./components/app-lock-overlay";
import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
@@ -44,23 +45,28 @@ I18nManager.swapLeftAndRightInRTL(false);
const App = () => {
const init = useAppEvents();
useEffect(() => {
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
SettingsService.onFirstLaunch();
await Notifications.get();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(true);
}
TipManager.init();
}, 100);
initializeLogger()
.catch((e) => {
console.log(e);
})
.finally(() => {
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
SettingsService.onFirstLaunch();
await Notifications.get();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(true);
}
TipManager.init();
}, 100);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View File

@@ -25,6 +25,7 @@ import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { MMKV } from "./mmkv";
import { ToastManager } from "../../services/event-manager";
// Database key cipher is persisted across different user sessions hence it has
// it's independent storage which we will never clear. This is only used when application has
@@ -43,9 +44,8 @@ const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
@@ -117,9 +117,7 @@ export async function setAppLockVerificationCipher(appLockPassword) {
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = await encrypt(appLockCredentials, generatePassword());
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
DatabaseLogger.error(e);
@@ -135,10 +133,8 @@ export async function validateAppLockPassword(appLockPassword) {
try {
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const decrypted = await decrypt(
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
appLockCipher
);
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
@@ -214,7 +210,7 @@ export async function getDatabaseKey(appLockPassword) {
const userKeyCipher = await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_USER_KEY_SALT
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
);
@@ -227,6 +223,7 @@ export async function getDatabaseKey(appLockPassword) {
return DB_KEY;
} catch (e) {
ToastManager.error(e, "Error getting database key");
console.log(e, "error");
DatabaseLogger.error(e);
return null;
@@ -239,7 +236,7 @@ export async function deriveCryptoKey(data) {
const userKeyCipher = await encrypt(
{
key: await getDatabaseKey(),
salt: NOTESNOOK_USER_KEY_SALT
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key
);
@@ -264,13 +261,12 @@ export async function getCryptoKey(_name) {
const key = await decrypt(
{
key: await getDatabaseKey()
key: await getDatabaseKey(),
salt: keyCipher.salt
},
keyCipher
);
DatabaseLogger.info("User key decrypted: ", !!key);
return key;
} catch (e) {
console.log("getCryptoKey", e);

View File

@@ -85,4 +85,10 @@ export async function setupDatabase(password) {
}
export const db = database;
export const DatabaseLogger = dbLogger;
let DatabaseLogger = dbLogger.scope(Platform.OS);
const setLogger = () => {
DatabaseLogger = dbLogger.scope(Platform.OS);
};
export { DatabaseLogger, setLogger };

View File

@@ -16,14 +16,27 @@ 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 { MMKVLoader } from "react-native-mmkv-storage";
import { initialize } from "@notesnook/core/dist/logger";
import { KV } from "./storage";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import { Platform } from "react-native";
import { setLogger } from ".";
import { RNSqliteDriver } from "./sqlite.kysely";
const LoggerStorage = new MMKVLoader()
.withInstanceID("notesnook_logs")
.initialize();
const initializeLogger = async () => {
await initialize({
dialect: (name) => ({
createDriver: () => {
return new RNSqliteDriver({ async: true, dbName: name });
},
createAdapter: () => new SqliteAdapter(),
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler()
}),
tempStore: "memory",
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL"
});
setLogger();
};
initialize(new KV(LoggerStorage));
export { LoggerStorage };
export { initializeLogger };

View File

@@ -34,11 +34,13 @@ import {
import { MMKV } from "../../common/database/mmkv";
import { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import { NotesnookModule } from "../../utils/notesnook-module";
import { SIZE } from "../../utils/size";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
@@ -148,6 +150,12 @@ const AppLockedOverlay = () => {
lockApp(false);
enabled(false);
password.current = undefined;
} else {
ToastManager.show({
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
type: "error",
context: "local"
});
}
} catch (e) {
console.error(e);
@@ -188,11 +196,7 @@ const AppLockedOverlay = () => {
enabled(false);
} else {
SettingsService.appEnteredBackground();
if (
SettingsService.get().privacyScreen ||
SettingsService.getProperty("appLockEnabled")
) {
if (SettingsService.get().privacyScreen) {
enabled(true);
}
}
@@ -209,6 +213,7 @@ const AppLockedOverlay = () => {
justifyContent: "center"
}}
>
<Toast context="local" />
<View
style={{
flex: 1,

View File

@@ -112,6 +112,10 @@ export const SessionExpired = () => {
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
const key = await db.user.getEncryptionKey();
if (!key) throw new Error("No encryption key found.");
Sync.run("global", false, "full", async (complete) => {
if (!complete) {
let user = await db.user.getUser();
@@ -127,7 +131,6 @@ export const SessionExpired = () => {
setVisible(false);
});
} catch (e) {
console.log(e);
let user = await db.user.getUser();
if (!user) return;
email.current = user.email;

View File

@@ -44,6 +44,7 @@ type DialogInfo = {
// eslint-disable-next-line @typescript-eslint/ban-types
context: "global" | "local" | (string & {});
secureTextEntry?: boolean;
keyboardType?: string;
};
export function presentDialog(data: Partial<DialogInfo>): void {

View File

@@ -174,6 +174,7 @@ export const Dialog = ({ context = "global" }) => {
onSubmit={onPressPositive}
returnKeyLabel="Done"
returnKeyType="done"
keyboardType={dialogInfo.keyboardType || "default"}
placeholder={dialogInfo.inputPlaceholder}
/>
</View>

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { db } from "../../../common/database";
import {
clearAppLockVerificationCipher,
setAppLockVerificationCipher,
@@ -44,6 +45,7 @@ import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
@@ -66,6 +68,7 @@ export const AppLockPassword = () => {
confirmPassword?: string;
}>({});
const [secureTextEntry, setSecureTextEntry] = useState(true);
const [accountPass, setAccountPass] = useState(false);
useEffect(() => {
const subs = [
@@ -73,6 +76,7 @@ export const AppLockPassword = () => {
eOpenAppLockPasswordDialog,
(mode: "create" | "change" | "remove") => {
setMode(mode);
setAccountPass(false);
setVisible(true);
}
),
@@ -126,7 +130,9 @@ export const AppLockPassword = () => {
mode === "change"
? `Change app lock ${keyboardType}`
: mode === "remove"
? `Remove app lock ${keyboardType}`
? `Enter ${
accountPass ? "account password" : `app lock ${keyboardType}`
} to remove ${keyboardType}`
: `Set up a custom app lock ${keyboardType} to unlock the app`
}
icon="shield"
@@ -169,33 +175,39 @@ export const AppLockPassword = () => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.password}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
keyboardType={
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
}
autoComplete="password"
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
returnKeyType={mode !== "remove" ? "next" : "done"}
secureTextEntry={secureTextEntry}
buttonLeft={
<IconButton
name={keyboardType === "password" ? "numeric" : "keyboard"}
onPress={() => {
setKeyboardType(
keyboardType === "password" ? "pin" : "password"
);
setSecureTextEntry(false);
setImmediate(() => {
setSecureTextEntry(true);
});
}}
style={{
width: 25,
height: 25,
marginRight: 5
}}
size={SIZE.lg}
/>
accountPass ? null : (
<IconButton
name={keyboardType === "password" ? "numeric" : "keyboard"}
onPress={() => {
setKeyboardType(
keyboardType === "password" ? "pin" : "password"
);
setSecureTextEntry(false);
setImmediate(() => {
setSecureTextEntry(true);
});
}}
style={{
width: 25,
height: 25,
marginRight: 5
}}
size={SIZE.lg}
/>
)
}
placeholder={
mode === "change"
accountPass
? "Account password"
: mode === "change"
? `New ${keyboardType}`
: `${keyboardType === "pin" ? "Pin" : "Password"}`
}
@@ -222,6 +234,36 @@ export const AppLockPassword = () => {
placeholder={`Confirm ${keyboardType}`}
/>
) : null}
{mode === "remove" ? (
<>
<Button
icon={
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
}
onPress={() => {
setSecureTextEntry(false);
setAccountPass(!accountPass);
setTimeout(() => {
setSecureTextEntry(true);
});
}}
iconSize={SIZE.lg}
type="plain"
iconColor={
accountPass ? colors.primary.accent : colors.primary.icon
}
title="Use account password instead"
style={{
width: "100%",
alignSelf: "flex-start",
justifyContent: "flex-start",
paddingHorizontal: 0,
height: 30
}}
/>
</>
) : null}
</View>
<DialogButtons
@@ -249,7 +291,8 @@ export const AppLockPassword = () => {
);
return;
}
await setAppLockVerificationCipher(values.current.password);
const password = values.current.password;
setAppLockVerificationCipher(password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
} else if (mode === "change") {
if (
@@ -277,7 +320,6 @@ export const AppLockPassword = () => {
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.currentPassword
);
@@ -293,8 +335,10 @@ export const AppLockPassword = () => {
return;
}
const password = values.current.password;
await clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(values.current.password);
await setAppLockVerificationCipher(password);
} else if (mode === "remove") {
if (!values.current.password) {
ToastManager.error(
@@ -305,14 +349,18 @@ export const AppLockPassword = () => {
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.password
);
const isCurrentPasswordCorrect = accountPass
? await db.user.verifyPassword(values.current.password)
: await validateAppLockPassword(values.current.password);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
accountPass
? "Account password incorrect"
: `${
keyboardType === "pin" ? "Pin" : "Password"
} incorrect`
),
undefined,
"local"
@@ -342,7 +390,9 @@ export const AppLockPassword = () => {
close();
}}
positiveTitle="Save"
positiveTitle={
mode === "remove" ? "Remove" : mode === "change" ? "Change" : "Save"
}
negativeTitle="Cancel"
positiveType="transparent"
loading={false}

View File

@@ -586,7 +586,7 @@ export class VaultDialog extends Component {
}
async _copyNote(note) {
Clipboard.setString(await convertNoteToText(note));
Clipboard.setString((await convertNoteToText(note, true)) || "");
ToastManager.show({
heading: "Note copied",
type: "success",
@@ -602,7 +602,7 @@ export class VaultDialog extends Component {
await Share.open({
heading: "Share note",
failOnCancel: false,
message: await convertNoteToText(note)
message: (await convertNoteToText(note)) || ""
});
} catch (e) {
console.error(e);

View File

@@ -21,10 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import Editor from "../../screens/editor";
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { ToastManager, eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useTrashStore } from "../../stores/use-trash-store";
@@ -34,7 +34,7 @@ import DialogHeader from "../dialog/dialog-header";
import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
import { diff } from "diffblazer";
/**
*
@@ -121,12 +121,21 @@ export default function NotePreview({ session, content, note }) {
<ReadonlyEditor
editorId="historyPreview"
onLoad={async (loadContent) => {
if (content.data) {
const _note = note || (await db.notes.note(session?.noteId));
loadContent({
data: content.data,
id: _note.id
});
try {
if (content.data) {
const _note = note || (await db.notes.note(session?.noteId));
const currentContent = await db.content.get(_note.contentId);
loadContent({
data: diff(currentContent.data, content.data),
id: _note.id
});
}
} catch (e) {
ToastManager.error(
e,
"Failed to load history preview",
"local"
);
}
}}
/>

View File

@@ -86,16 +86,16 @@ const Sort = ({ type, screen }) => {
<Button
title={
groupOptions?.sortDirection === "asc"
? groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
? groupOptions?.groupBy === "abc" ||
groupOptions?.sortBy === "title"
? "A - Z"
: groupOptions.sortBy === "dueDate"
: groupOptions?.sortBy === "dueDate"
? "Earliest first"
: "Old - New"
: groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
: groupOptions?.groupBy === "abc" ||
groupOptions?.sortBy === "title"
? "Z - A"
: groupOptions.sortBy === "dueDate"
: groupOptions?.sortBy === "dueDate"
? "Latest first"
: "New - Old"
}
@@ -134,7 +134,7 @@ const Sort = ({ type, screen }) => {
alignItems: "center"
}}
>
{groupOptions.groupBy === "abc" ? (
{groupOptions?.groupBy === "abc" ? (
<Button
type="secondary"
title="Title"
@@ -154,18 +154,18 @@ const Sort = ({ type, screen }) => {
item === "dateEdited") ? null : (
<Button
key={item}
type={groupOptions.sortBy === item ? "selected" : "plain"}
type={groupOptions?.sortBy === item ? "selected" : "plain"}
title={SORT[item]}
height={40}
iconPosition="left"
icon={groupOptions.sortBy === item ? "check" : null}
icon={groupOptions?.sortBy === item ? "check" : null}
style={{
marginRight: 10,
paddingHorizontal: 8
}}
buttonType={{
text:
groupOptions.sortBy === item
groupOptions?.sortBy === item
? colors.primary.accent
: colors.secondary.paragraph
}}
@@ -213,11 +213,11 @@ const Sort = ({ type, screen }) => {
key={item}
testID={"btn-" + item}
type={
groupOptions.groupBy === GROUP[item] ? "selected" : "plain"
groupOptions?.groupBy === GROUP[item] ? "selected" : "plain"
}
buttonType={{
text:
groupOptions.groupBy === GROUP[item]
groupOptions?.groupBy === GROUP[item]
? colors.primary.accent
: colors.secondary.paragraph
}}
@@ -235,7 +235,7 @@ const Sort = ({ type, screen }) => {
updateGroupOptions(_groupOptions);
}}
height={40}
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
icon={groupOptions?.groupBy === GROUP[item] ? "check" : null}
title={
item.slice(0, 1).toUpperCase() + item.slice(1, item.length)
}

View File

@@ -600,20 +600,11 @@ export const useActions = ({
});
return;
}
const text = await convertNoteToText(item as Note, false);
if (!text) {
ToastManager.error(
new Error(Errors.export("text")),
undefined,
"local"
);
return;
}
const html = text.replace(/\n/g, "<br />");
const text = await convertNoteToText(item as Note, true);
const html = (text || "").replace(/\n/g, "<br />");
await Notifications.displayNotification({
title: item.title,
message: (item as Note).headline || text,
message: (item as Note).headline || text || "",
subtitle: "",
bigText: html,
ongoing: true,
@@ -684,15 +675,11 @@ export const useActions = ({
} else {
processingId.current = "shareNote";
const convertedText = await convertNoteToText(item);
if (!convertedText) {
ToastManager.error(new Error(Errors.export("text")));
return;
}
processingId.current = undefined;
Share.open({
title: "Share note to",
failOnCancel: false,
message: convertedText
message: convertedText || ""
});
}
}
@@ -772,11 +759,7 @@ export const useActions = ({
} else {
processingId.current = "copyContent";
const text = await convertNoteToText(item as Note, true);
if (!text) {
ToastManager.error(new Error(Errors.export("text")));
return;
}
Clipboard.setString(text);
Clipboard.setString(text || "");
processingId.current = undefined;
ToastManager.show({
heading: "Note copied to clipboard",

View File

@@ -22,20 +22,14 @@ import { Platform } from "react-native";
import { Subscription } from "react-native-iap";
import PremiumService from "../services/premium";
import { db } from "../common/database";
import { Product } from "@notesnook/core/dist/api/pricing";
type PurchaseInfo = {
country: string;
countryCode: string;
sku: string;
discount: number;
};
const skuInfos: { [name: string]: PurchaseInfo | undefined } = {};
const skuInfos: { [name: string]: Product | undefined } = {};
export const usePricing = (period: "monthly" | "yearly") => {
const [current, setCurrent] = useState<{
period: string;
info?: PurchaseInfo;
info?: Product;
product?: Subscription;
}>();
@@ -54,6 +48,7 @@ export const usePricing = (period: "monthly" | "yearly") => {
period
));
skuInfos[period] = skuInfo;
const products = (await PremiumService.getProducts()) as Subscription[];
let product = products.find((p) => p.productId === skuInfo?.sku);
if (!product)

View File

@@ -75,14 +75,16 @@ export function ReadonlyEditor(props: {
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
console.log("Readonly editor loaded.");
props.onLoad?.((content: { data: string; id: string }) => {
noteId.current = content.id;
editorRef.current?.postMessage(
JSON.stringify({
type: "native:html",
value: content.data
})
);
setLoading(false);
setTimeout(() => {
noteId.current = content.id;
editorRef.current?.postMessage(
JSON.stringify({
type: "native:html",
value: content.data
})
);
setLoading(false);
}, 300);
});
} else if (editorMessage.type === EventTypes.getAttachmentData) {
const attachment = (editorMessage.value as any).attachment as Attachment;

View File

@@ -185,7 +185,8 @@ const camera = async (options: PickerOptions) => {
cropping: false,
multiple: true,
maxFiles: 10,
writeTempFile: true
writeTempFile: true,
compressImageQuality: 1
})
.then((response) => {
handleImageResponse(
@@ -215,7 +216,8 @@ const gallery = async (options: PickerOptions) => {
mediaType: "photo",
maxFiles: 10,
cropping: false,
multiple: true
multiple: true,
compressImageQuality: 1
})
.then((response) =>
handleImageResponse(
@@ -266,7 +268,6 @@ const handleImageResponse = async (
response: Image[],
options: PickerOptions
) => {
console.log(response, "result-file-picker");
const result = await AttachImage.present(response, options.context);
if (!result) return;
const compress = result.compress;
@@ -302,10 +303,15 @@ const handleImageResponse = async (
type: "url"
});
const fileName = image.sourceURL
let fileName = image.sourceURL
? basename(image.sourceURL)
: image.filename || "image";
fileName =
image.mime === "image/jpeg"
? fileName.replace(/HEIC|HEIF/, "jpeg")
: fileName;
console.log("attaching image...", fileName);
console.log("attaching file...");

View File

@@ -351,9 +351,18 @@ export const useEditor = (
if (!unlocked)
throw new Error("Could not save note, vault is locked");
}
noteData.contentId = note?.contentId;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await db.vault?.save(noteData as any);
if (typeof noteData.title === "string") {
await db.notes.add({
title: noteData.title,
id: noteData.id
});
}
noteData.contentId = note?.contentId;
if (data) {
await db.vault?.save(noteData as any);
}
clearTimeout(saveTimer);
}
@@ -696,14 +705,13 @@ export const useEditor = (
);
}
console.log("readonly state changed...", note.readonly);
useTabStore.getState().updateTab(tabId, {
readonly: note.readonly
});
}
if (data.type === "tiptap" && note && !isLocal) {
if (lastContentChangeTime.current[noteId] >= data.dateEdited) {
if (lastContentChangeTime.current[noteId] >= data.dateModified) {
return;
}
@@ -728,7 +736,7 @@ export const useEditor = (
}
} else {
const _nextContent = data.data;
if (_nextContent === currentContents.current?.data) {
if (_nextContent === currentContents.current[note.id]?.data) {
return;
}
lastContentChangeTime.current[note.id] = note.dateEdited;

View File

@@ -122,7 +122,8 @@ export default function DebugLogs() {
colors.primary.paragraph,
colors.error.paragraph,
colors.static.black,
colors.static.orange
colors.static.orange,
colors.primary.border
]
);

View File

@@ -90,13 +90,26 @@ export async function verifyUserWithApplock() {
positiveText: "Verify",
secureTextEntry: true,
negativeText: "Cancel",
keyboardType: keyboardType,
positivePress: async (value) => {
try {
const verified = await validateAppLockPassword(value);
if (!verified) {
ToastManager.show({
heading: `Invalid ${
keyboardType === "numeric" ? "pin" : "password"
}`,
type: "error",
context: "local"
});
return false;
}
resolve(verified);
} catch (e) {
resolve(false);
return false;
}
return true;
}
});
} else {

View File

@@ -871,6 +871,9 @@ export const settingsGroups: SettingSection[] = [
type: "switch",
property: "appLockEnabled",
onChange: () => {
SettingsService.set({
privacyScreen: true
});
SettingsService.setPrivacyScreen(SettingsService.get());
},
onVerify: async () => {
@@ -973,7 +976,7 @@ export const settingsGroups: SettingSection[] = [
SettingsService.getProperty("applockKeyboardType") === "numeric"
? "pin"
: "password"
}, app lock will fallback to using account password to unlock the app`,
}, app lock will be disabled if no other security method is enabled.`,
hidden: () => {
return !SettingsService.getProperty("appLockHasPasswordSecurity");
},

View File

@@ -59,14 +59,14 @@ const recoveryKeyMessage = {
() => {
eSendEvent(eOpenRecoveryKeyDialog);
},
true,
false,
async () => {
SettingsService.set({
recoveryKeySaved: true
});
clearMessage();
},
"I have saved my key already"
"Cancel"
);
},
data: {},

View File

@@ -952,13 +952,13 @@ async function pinNote(id: string) {
const note = await db.notes.note(id as string);
if (!note) return;
let text = await convertNoteToText(note, false);
let text = await convertNoteToText(note, true);
if (!text) text = "";
const html = text.replace(/\n/g, "<br />");
Notifications.displayNotification({
title: note.title,
message: note.headline || text,
subtitle: note.headline || text,
subtitle: "",
bigText: html,
ongoing: true,
actions: ["UNPIN"],

View File

@@ -76,6 +76,17 @@ function migrateAppLock() {
DatabaseLogger.debug("App lock Migrated");
}
function migrateSettings(settings: SettingStore["settings"]) {
const version = settings.settingsVersion;
if (!version) {
settings.settingsVersion = 1;
settings.privacyScreen = settings.appLockEnabled
? true
: settings.privacyScreen;
MMKV.setString("appSettings", JSON.stringify(settings));
}
}
function init() {
scale.fontScale = 1;
const settingsJson = MMKV.getString("appSettings");
@@ -83,14 +94,17 @@ function init() {
if (!settingsJson) {
MMKV.setString("appSettings", JSON.stringify(settings));
} else {
const settingsParsed = JSON.parse(settingsJson);
migrateSettings(settingsParsed);
settings = {
...settings,
...JSON.parse(settingsJson)
...settingsParsed
};
}
if (settings.fontScale) {
scale.fontScale = settings.fontScale;
}
setTimeout(() => setPrivacyScreen(settings), 1);
updateSize();
useSettingStore.getState().setSettings({ ...settings });
@@ -98,7 +112,7 @@ function init() {
}
function setPrivacyScreen(settings: SettingStore["settings"]) {
if (settings.privacyScreen || settings.appLockEnabled) {
if (settings.privacyScreen) {
if (Platform.OS === "android") {
NotesnookModule.setSecureMode(true);
} else {

View File

@@ -81,6 +81,7 @@ export type Settings = {
biometricsAuthEnabled?: boolean;
backgroundSync?: boolean;
applockKeyboardType: "numeric" | "default";
settingsVersion?: number;
};
type DimensionsType = {
@@ -169,7 +170,8 @@ export const defaultSettings: SettingStore["settings"] = {
markdownShortcuts: true,
biometricsAuthEnabled: false,
appLockHasPasswordSecurity: false,
backgroundSync: true
backgroundSync: true,
settingsVersion: 0
};
export const useSettingStore = create<SettingStore>((set, get) => ({

View File

@@ -23,7 +23,7 @@ export const IOS_APPGROUPID = "group.org.streetwriters.notesnook";
export const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
export const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
export const BETA = true;
export const BETA = false;
export const STORE_LINK =
Platform.OS === "ios"

View File

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

View File

@@ -37,4 +37,10 @@ allprojects {
maven { url 'https://www.jitpack.io' }
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "11"
}
}
}

View File

@@ -1,4 +1,3 @@
- Added push/pull changes to troubleshoot sync issues in settings
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -1015,7 +1015,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2100;
CURRENT_PROJECT_VERSION = 2105;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1089,7 +1089,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.3;
MARKETING_VERSION = 3.0.8;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1120,7 +1120,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2100;
CURRENT_PROJECT_VERSION = 2105;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1194,7 +1194,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.3;
MARKETING_VERSION = 3.0.8;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1353,7 +1353,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2100;
CURRENT_PROJECT_VERSION = 2105;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1365,7 +1365,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.3;
MARKETING_VERSION = 3.0.8;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1396,7 +1396,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2100;
CURRENT_PROJECT_VERSION = 2105;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1409,7 +1409,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.3;
MARKETING_VERSION = 3.0.8;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1439,7 +1439,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2100;
CURRENT_PROJECT_VERSION = 2105;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1513,7 +1513,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.3;
MARKETING_VERSION = 3.0.8;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1544,7 +1544,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2100;
CURRENT_PROJECT_VERSION = 2105;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1619,7 +1619,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.3;
MARKETING_VERSION = 3.0.8;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -959,4 +959,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
COCOAPODS: 1.12.1
COCOAPODS: 1.15.2

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.0.2",
"version": "3.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.0.2",
"version": "3.0.4",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.3",
"version": "3.0.8",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -10,3 +10,19 @@ index 5de0845..1b158d8 100644
if (includeBase64) {
image.putString("data", getBase64StringFromFile(compressedImagePath));
diff --git a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
index 9f20973..5e14da8 100644
--- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
+++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
@@ -595,8 +595,10 @@ - (void)qb_imagePickerController:
NSString *mimeType = [self determineMimeTypeFromImageData:imageData];
Boolean isKnownMimeType = [mimeType length] > 0;
+ Boolean isHeicOrHeif = [mimeType isEqualToString:@"image/heic"] || [mimeType isEqualToString:@"image/heif"];
+
ImageResult *imageResult = [[ImageResult alloc] init];
- if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg) {
+ if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg && !isHeicOrHeif) {
// Use original, unmodified image
imageResult.data = imageData;
imageResult.width = @(imgT.size.width);

View File

@@ -311,8 +311,7 @@ const ShareView = () => {
type: "tiptap"
},
id: note.id,
sessionId: Date.now(),
title: noteTitle.current
sessionId: Date.now()
};
} else {
noteData = { ...note };
@@ -487,25 +486,42 @@ const ShareView = () => {
gap: 10
}}
>
<TextInput
placeholder="Enter note title"
ref={inputRef}
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "OpenSans-SemiBold",
fontSize: SIZE.lg,
paddingBottom: 0,
paddingTop: 0
}}
onChangeText={(value) => {
noteTitle.current = value;
}}
blurOnSubmit={false}
onSubmitEditing={() => {
editorRef.current.focus();
}}
/>
{appendNoteId ? (
<Heading
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "OpenSans-SemiBold",
fontSize: SIZE.lg,
paddingBottom: 0,
paddingTop: 0
}}
>
Save note
</Heading>
) : (
<TextInput
placeholder="Enter note title"
ref={inputRef}
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "OpenSans-SemiBold",
fontSize: SIZE.lg,
paddingBottom: 0,
paddingTop: 0,
color: colors.primary.heading
}}
onChangeText={(value) => {
noteTitle.current = value;
}}
defaultValue={noteTitle.current}
blurOnSubmit={false}
onSubmitEditing={() => {
editorRef.current.focus();
}}
/>
)}
<Button
title="Done"
type="accent"

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/theme-builder",
"version": "1.1.1",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/theme-builder",
"version": "1.1.1",
"version": "1.2.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"@emotion/react": "11.11.1",
@@ -18,17 +18,18 @@
"@theme-ui/components": "^0.14.7",
"@theme-ui/core": "^0.14.7",
"@trpc/client": "^10.38.3",
"@types/react-dom": "^18.3.0",
"clipboard-polyfill": "^4.0.0",
"file-saver": "^2.0.5",
"katex": "^0.16.2",
"react": "18.2.0",
"react-dom": "18.2.0"
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.22.5",
"@types/babel__core": "^7.20.1",
"@types/file-saver": "^2.0.5",
"@types/react": "^18.2.17",
"@types/react": "^18.2.39",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"buffer": "^6.0.3",
@@ -978,7 +979,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "3.0.0",
"version": "3.0.6",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -2026,6 +2027,15 @@
"@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,
@@ -2098,25 +2108,30 @@
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.2.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"dev": true
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
},
"node_modules/@types/styled-system": {
"version": "5.1.16",

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/theme-builder",
"description": "Your private note taking space",
"version": "1.1.1",
"version": "1.2.0",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -17,18 +17,18 @@
"@theme-ui/components": "^0.14.7",
"@theme-ui/core": "^0.14.7",
"@trpc/client": "^10.38.3",
"@types/react-dom": "^18.3.0",
"clipboard-polyfill": "^4.0.0",
"file-saver": "^2.0.5",
"katex": "^0.16.2",
"react": "18.2.0",
"react-dom": "18.2.0"
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.22.5",
"@types/babel__core": "^7.20.1",
"@types/file-saver": "^2.0.5",
"@types/react": "^18.2.39",
"@types/react": "^18.2.17",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"buffer": "^6.0.3",

View File

@@ -2,19 +2,10 @@
<html lang="en">
<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="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" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="theme-color" content="#01c352" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--
manifest.json provides metadata used when your web app is installed on a
@@ -49,13 +40,26 @@
/>
<meta name="twitter:card" content="summary_large_image" />
<title>Notesnook Theme Builder</title>
<style id="theme-colors">
#splash {
display: none !important;
}
</style>
<script type="module">
import { themeToCSS } from "@notesnook/theme";
<script nonce="7WIq8hRwApoXhctoGZZthMLYQLRNiprTwcPi6Azdf">
const colorScheme = JSON.parse(
window.localStorage.getItem("colorScheme") || '"light"'
);
const root = document.querySelector("html");
if (root) root.setAttribute("data-theme", colorScheme);
const theme = window.localStorage.getItem(`theme:${colorScheme}`);
if (theme) {
const css = themeToCSS(JSON.parse(theme));
const stylesheet = document.getElementById("theme-colors");
if (stylesheet) stylesheet.innerHTML = css;
}
</script>
<script type="module" src="/index.tsx"></script>
<style>
@@ -63,21 +67,6 @@
overscroll-behavior: none;
}
html[data-theme="dark"] {
--bg: #0f0f0f;
--fg: #fff;
--three-bars-bg: #494949;
--gradient-stop-color: #111111;
--n-letter-color: #e1e1e1;
}
html[data-theme="light"] {
--bg: #fff;
--three-bars-bg: #bebebe;
--gradient-stop-color: #fff9f9;
--n-letter-color: #000;
}
html[data-theme="light"] .react-loading-skeleton {
--base-color: var(--background-secondary);
--highlight-color: #var(--background-secondary);
@@ -88,6 +77,19 @@
--highlight-color: var(--background-secondary);
}
#splash {
background-color: var(--background);
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#splash svg {
transform: scale(1);
animation: pulse 2s infinite;
@@ -95,7 +97,7 @@
@keyframes pulse {
0% {
transform: scale(0.95);
transform: scale(0.9);
}
70% {
@@ -103,7 +105,7 @@
}
100% {
transform: scale(0.95);
transform: scale(0.9);
}
}
@@ -128,20 +130,9 @@
overflow: hidden;
}
@keyframes fadeUp {
0% {
transform: translateY(500px);
opacity: 0;
}
80% {
transform: translateY(0px);
opacity: 0.7;
}
100% {
opacity: 1;
}
#root {
display: flex;
flex-direction: column;
}
/* svg {
@@ -171,85 +162,40 @@
</style>
</head>
<body>
<body class="theme-scope-base">
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- <script src="https://cdn.jsdelivr.net/npm/highlightjs@9.16.2/highlight.pack.min.js"></script>
-->
<div id="root"></div>
<div
id="splash"
style="
background-color: var(--bg);
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
"
<svg
xmlns="http://www.w3.org/2000/svg"
style="height: 0; width: 0; z-index: -1; position: absolute"
>
<svg viewBox="0 0 339 339" style="width: 150px">
<defs />
<defs>
<linearGradient
xlink:href="#a"
id="b"
x1="188.61227"
x2="193.54405"
y1="165.2058"
y2="216.81519"
gradientTransform="rotate(5 4448 -4204) scale(2.93671)"
gradientUnits="userSpaceOnUse"
/>
<linearGradient id="a">
<stop offset="0" />
<stop
offset="1"
stop-color="var(--gradient-stop-color)"
stop-opacity="0"
/>
</linearGradient>
<linearGradient
id="c"
x1="167.8"
x2="270.6"
y1="76.9"
y2="64.2"
gradientTransform="rotate(5 465 -2050) scale(1.50082)"
gradientUnits="userSpaceOnUse"
xlink:href="#a"
/>
</defs>
<g transform="translate(0 42)">
<path fill="url(#b)" d="M160 205l154 42-141 44-155-42z" />
<path fill="url(#c)" d="M160-35v240l154 42 1-253z" />
<symbol id="full-logo" viewBox="0 0 1000 1000">
<rect
width="1000"
height="1000"
rx="30%"
ry="30%"
style="fill: var(--paragraph)"
/>
<g transform="translate(275,176.95254)">
<path
fill="none"
stroke-width="1.2"
d="M160 205V-35m0 240L18 249m142-44l154 41"
/>
<path
fill="var(--n-letter-color)"
d="M84 109l35 54V98l21-7v91l-27 9-35-54v65l-21 6v-91z"
/>
<rect
width="86.1"
height="12.6"
x="185"
y="97"
fill="var(--three-bars-bg)"
ry="2.3"
transform="skewY(15) scale(.9669 1)"
/>
<path
fill="var(--three-bars-bg)"
d="M181 169l99 26 2 3v8c0 1-1 2-2 1l-99-26-2-3v-7c0-2 1-2 2-2zm0-47l99 27 2 2v8l-2 2-99-27c-1 0-2-1-2-3v-7l2-2z"
style="fill: var(--background)"
d="M437.985 493.919c-17.255 50.411-51.835 93.065-97.588 120.372-45.752 27.307-99.71 37.496-152.269 28.753-52.56-8.743-100.313999-35.85-134.763-76.498C18.916 525.898.006 474.347 0 421.064v-110.25l79.121001 33.053v77.152c-.007 20.779 4.434 41.318 13.024 60.237C100.735 500.176 113.276 517.037 128.925 530.707c2.97 2.587 6.053 5.107 9.236 7.459 23.357 17.388 51.402 27.352 80.494 28.597.99 0 1.946.079 2.925.101.979.023 2.25 0 3.375 0h3.375c1.125 0 1.935 0 2.925-.101 29.081-1.248 57.116-11.203 80.471-28.575 3.173-2.351 6.255-4.86 9.237-7.447 20.556-17.98 35.656-41.366 43.582-67.5zM450 225v196.065c0 2.531 0 5.074-.158 7.605l-78.963-33.019V225c-.013-37.684-14.607-73.901-40.725-101.066-26.119-27.164003-61.735-43.168003-99.389-44.660003-37.655-1.492-74.426 11.644-102.611 36.657003-28.185999 25.013-45.599999 59.962-48.593999 97.526-.281 3.803-.439 7.662-.439 11.543v48.712L0 240.637V0h225c59.674 0 116.903 23.705 159.099 65.901C426.295 108.097 450 165.326 450 225Z"
/>
</g>
</symbol>
<symbol id="themed-logo" viewBox="0 0 450 646.09491">
<path
style="fill: var(--paragraph)"
d="M437.985 493.919c-17.255 50.411-51.835 93.065-97.588 120.372-45.752 27.307-99.71 37.496-152.269 28.753-52.56-8.743-100.313999-35.85-134.763-76.498C18.916 525.898.006 474.347 0 421.064v-110.25l79.121001 33.053v77.152c-.007 20.779 4.434 41.318 13.024 60.237C100.735 500.176 113.276 517.037 128.925 530.707c2.97 2.587 6.053 5.107 9.236 7.459 23.357 17.388 51.402 27.352 80.494 28.597.99 0 1.946.079 2.925.101.979.023 2.25 0 3.375 0h3.375c1.125 0 1.935 0 2.925-.101 29.081-1.248 57.116-11.203 80.471-28.575 3.173-2.351 6.255-4.86 9.237-7.447 20.556-17.98 35.656-41.366 43.582-67.5zM450 225v196.065c0 2.531 0 5.074-.158 7.605l-78.963-33.019V225c-.013-37.684-14.607-73.901-40.725-101.066-26.119-27.164003-61.735-43.168003-99.389-44.660003-37.655-1.492-74.426 11.644-102.611 36.657003-28.185999 25.013-45.599999 59.962-48.593999 97.526-.281 3.803-.439 7.662-.439 11.543v48.712L0 240.637V0h225c59.674 0 116.903 23.705 159.099 65.901C426.295 108.097 450 165.326 450 225Z"
/>
</symbol>
</svg>
<div id="splash" class="hidden">
<svg style="height: 120px">
<use href="#themed-logo" />
</svg>
</div>
<div id="dialogContainer"></div>

View File

@@ -18,31 +18,59 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { init } from "@notesnook/web/src/bootstrap";
import { render } from "react-dom";
import { createRoot } from "react-dom/client";
import {
ErrorBoundary,
ErrorComponent
} from "@notesnook/web/src/components/error-boundary";
import { BaseThemeProvider } from "@notesnook/web/src/components/theme-provider";
import { App } from "./app";
renderApp();
async function renderApp() {
const { component, props } = await init();
const rootElement = document.getElementById("root");
if (!rootElement) return;
const root = createRoot(rootElement);
try {
const { component, props } = await init();
const { default: Component } = await component();
render(
<BaseThemeProvider
sx={{
display: "flex",
"#app": { flex: 1, height: "unset" },
"& > :first-child:not(#menu-wrapper)": { flex: 1 },
height: "100%"
}}
>
<Component route={props?.route || "login:email"} />
<App />
</BaseThemeProvider>,
document.getElementById("root"),
() => {
document.getElementById("splash")?.remove();
}
);
const { useKeyStore } = await import(
"@notesnook/web/src/interfaces/key-store"
);
await useKeyStore.getState().init();
const { default: Component } = await component();
const { default: AppLock } = await import(
"@notesnook/web/src/views/app-lock"
);
root.render(
<ErrorBoundary>
<BaseThemeProvider
onRender={() => document.getElementById("splash")?.remove()}
sx={{
display: "flex",
"#app": { flex: 1, height: "unset" },
"& > :first-child:not(#menu-wrapper)": { flex: 1 },
height: "100%"
}}
>
<AppLock>
<Component route={props?.route || "login:email"} />
</AppLock>
<App />
</BaseThemeProvider>
</ErrorBoundary>
);
} catch (e) {
root.render(
<>
<ErrorComponent
error={e}
resetErrorBoundary={() => window.location.reload()}
/>
</>
);
}
}

3
apps/web/.gitignore vendored
View File

@@ -30,4 +30,5 @@ dist
public/workbox
scripts/secrets
test-results
.swc
.swc
public/models

View File

@@ -259,14 +259,15 @@ test("unlock a note for editing", async ({ page }) => {
await page.waitForTimeout(150);
await page.reload();
await page.waitForTimeout(500);
await notes.waitForList();
const newContent = `${NOTE.content}${content}`;
const editedNote = await notes.findNote({
title: NOTE.title,
content: newContent
});
await editedNote?.openLockedNote(PASSWORD);
if (!editedNote) throw new Error("Could not find note.");
await editedNote.openLockedNote(PASSWORD);
await notes.editor.waitForLoading();
expect(await notes.editor.getContent("text")).toContain(newContent);
});

View File

@@ -0,0 +1,128 @@
/*
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 { describe, bench } from "vitest";
import { pipeline } from "@xenova/transformers";
import path from "path";
const text = `I am generally not in favor of huge feature packed releases instead preferring small releases without one or two features at most. v3 is a special case in that the sheer amount of changes required to migrate from our old key value database to SQLite forced us to release all of these updates together.
Migrating to SQLite
Initially, as all plans go, our only goal was to migrate to SQLite as quickly and with as few changes as possible. Unfortunately, that plan died in its infancy. Our old legacy database was not designed to handle tens of thousands of notes or gigabytes of data — something we quickly realized after some users with over 100K notes approached us with significant performance issues. While it "worked", it was neither efficient nor particularly safe.
In August 2023, we began slowly migrating the Notesnook Core to SQLite. While migrating we quickly realized we'd need TypeScript if this was ever going to work because Kysely (the underlying query builder we used) heavily relied on types being there. So from the very first day we were doing 2 things: migrating the database to SQLite, and migrating the codebase to TypeScript.
I won't bore you with too many technical details but we had a few primary goals behind all this upheaval:
The clients should perform predictably regardless of how much data you have.
The clients should use as less RAM as possible to work in extremely constrained enviroments (e.g. iOS limits all Share Extensions to around 50 MB before crashing).
With v3 we have acheived both of these goals.
Fixing the Sync
Sync has notoriously been one of the least stable parts of Notesnook. From the beginning, we opted for a decentralized syncing system where each client could independently sync items without depending on a central server. This had a few drawbacks:
Since each client was independent, it was a nightmare to debug why something wasn't syncing.
In order to be efficient, we detected changed items based on their modified timestamp, but this forced us to juggle with the various inconsistencies of time on different devices.
Building everything on top of timestamps meant resumability during sync could never be stable.
In short, our old sync logic heavily depended on time to be the exact same (down to the last millisecond) for things to work. As you can imagine, this didn't work out that well.
While migrating everything to SQLite, we greatly simplified the sync logic opting for a centralized system this time. This allowed us to completely get rid of all the time juggling resolving all the edge cases in our previous logic.
As a result, in v3 you'll notice a bug free sync that "just works" regardless of where you are, how many devices you are on, the time difference between them etc.
Features, Features
In every new release, it is always a struggle to decide which features to add and which features to leave behind for later. v3 was no different because ultimately each feature you add pushes the deadline further and further away. Even then, v3 is one of our biggest releases ever.
Note linking
The crown jewel of v3! Yes, finally, you can link 2 notes together and even link directly to a specific block inside a note.
We have also added controls to quickly see which notes you have linked to, and which notes link to a particular note.
To keep things simple, a note link is no different than a normal hyperlink except that clicking on it will take you to the note instead of an external page. This also means that when you export your notes, your note links are automatically converted into hyperlinks that directly open the linked Markdown or HTML files of the notes. This is something no other app does instead keeping the links in their proprietary format making them essentially useless.
Tabs
The only reason we added tabs was to allow quick navigation to/from a note on clicking a note link. You can, of course, use tabs without ever linking a note.
Notesnook is one of the few note taking apps (besides Obsidian & OneNote) that has a full fledged tab experience. With v3, we are slowly upgrading Notesnook to become a power user's tool while keeping things just as simple as before.
Nested notebooks
At rest encryption
Thanks to SQLite, at rest encryption is now a thing on all platforms. The encryption key is randomly generated on database creation without requiring any user intervention. In other words, it "just works" keeping your notes secure even if your device is compromised. Which brings me to...
App lock
App lock is an enhancement on at rest encryption. When you enable app lock, it further encrypts your database encryption key with your app lock pin (or security key) so it's not just an "overlay". If you forget your app lock pin, the only way into the app is to reset & clear the database and start over.
At Notesnook, we take your security & privacy very seriously.
Export attachments with notes
Exports got a lot of love in v3 with things like automatic downloading & linking of attachments, organization of note files based on your notebook structure, and resolution of internal note links to actual HTML/MD files.
User profile
Custom colors for note organization
Fixed colors were nice but not nicer than custom colors.
Customizable side bar
You can now drag/drop and reorganize how things in your side bar looks including hiding things that you don't use. It can be as simple or as complex as you want.
Callouts
Beta release channels
Nested notebooks/topics
Note linking
Tabs
Table of Contents
Customizable/sortable sidebar
User profile
Custom colors
Yearly reminders
Callouts in editor
Migration to SQLite
New sync system
At rest encryption
app lock
export attachments with notes
.ccccccccccxccccccc`;
describe("benchmarking embeddings", async () => {
const models = [
["Xenova/jina-embeddings-v2-small-en", 87],
["Snowflake/snowflake-arctic-embed-s", 41],
["Snowflake/snowflake-arctic-embed-m", 20],
["Snowflake/snowflake-arctic-embed-xs", 62],
// "Alibaba-NLP/gte-base-en-v1.5",
// "andersonbcdefg/bge-small-4096"
["Xenova/GIST-small-Embedding-v0", 57],
["Xenova/GIST-all-MiniLM-L6-v2", 88],
// "Xenova/all-MiniLM-L12-v2" // 96
["Xenova/NoInstruct-small-Embedding-v0", 40],
["TaylorAI/bge-micro-v2", 100],
// "Snowflake/snowflake-arctic-embed-m-long" // 19
// "Xenova/e5-small-v2" // 67
// "nomic-ai/nomic-embed-text-v1.5",
// "Xenova/bert-base-uncased",
["TaylorAI/gte-tiny", 88],
["Xenova/gte-small", 63]
];
for (const provider of ["cpu"]) {
for (const [model, rank] of models) {
const extractor = await pipeline("feature-extraction", model, {
// progress_callback: console.log,
device: provider,
dtype: "q8",
cache_dir: path.join(__dirname, "..", "..", "public", "models")
});
console.log(model, extractor.tokenizer.model_max_length);
extractor.tokenizer.model_max_length = 512;
bench(`${rank}. ${model} (${provider})`, async () => {
const output = await extractor(text, {
pooling: extractor.tokenizer._tokenizer_config.cls_token
? "cls"
: "mean"
});
});
}
}
});
// console.time("extraction");
// const output = await extractor(text);
// console.timeEnd("extraction");
// console.log(output.data.length);

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": "3.0.4",
"version": "3.0.6",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -38,6 +38,7 @@
"@theme-ui/core": "^0.16.1",
"@trpc/client": "10.38.3",
"@trpc/react-query": "10.38.3",
"@xenova/transformers": "github:xenova/transformers.js#v3",
"@zip.js/zip.js": "^2.7.32",
"async-mutex": "^0.4.0",
"axios": "^1.3.4",
@@ -60,6 +61,7 @@
"libsodium-wrappers": "^0.7.13",
"mac-scrollbar": "^0.13.5",
"marked": "^4.1.0",
"onnxruntime-node": "^1.18.0",
"pdfjs-dist": "3.6.172",
"phone": "^3.1.14",
"platform": "^1.3.6",
@@ -130,6 +132,9 @@
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0"
},
"overrides": {
"onnxruntime-node": "^1.18.0"
},
"scripts": {
"start": "cross-env PLATFORM=web vite",
"start:desktop": "cross-env PLATFORM=desktop vite",

View File

@@ -26,6 +26,7 @@ import { createDialect } from "./sqlite";
import { isFeatureSupported } from "../utils/feature-check";
import { generatePassword } from "../utils/password-generator";
import { deriveKey } from "../interfaces/key-store";
import { logManager } from "@notesnook/core/dist/logger";
const db = database;
async function initializeDatabase(persistence: DatabasePersistence) {
@@ -58,7 +59,7 @@ async function initializeDatabase(persistence: DatabasePersistence) {
database.setup({
sqliteOptions: {
dialect: createDialect,
dialect: (name, init) => createDialect(name, true, init),
...(IS_DESKTOP_APP || isFeatureSupported("opfs")
? { journalMode: "WAL", lockingMode: "exclusive" }
: {
@@ -99,8 +100,11 @@ async function initializeDatabase(persistence: DatabasePersistence) {
await db.init();
window.addEventListener("beforeunload", () => {
if (IS_DESKTOP_APP) db.sql().destroy();
window.addEventListener("beforeunload", async () => {
if (IS_DESKTOP_APP) {
await db.sql().destroy();
await logManager?.close();
}
});
logger.measure("Database initialization");

View File

@@ -44,11 +44,272 @@ import { User } from "@notesnook/core";
import { LegacyBackupFile } from "@notesnook/core";
import { useEditorStore } from "../stores/editor-store";
import { formatDate } from "@notesnook/core/dist/utils/date";
import {
pipeline,
AutoModel,
env,
Tensor,
mean_pooling,
FeatureExtractionPipeline
} from "@xenova/transformers";
import { toChunks } from "@notesnook/core/dist/utils/array";
env.allowLocalModels = true;
console.log(env);
const text = `I am generally not in favor of huge feature packed releases instead preferring small releases without one or two features at most. v3 is a special case in that the sheer amount of changes required to migrate from our old key value database to SQLite forced us to release all of these updates together.
Migrating to SQLite
Initially, as all plans go, our only goal was to migrate to SQLite as quickly and with as few changes as possible. Unfortunately, that plan died in its infancy. Our old legacy database was not designed to handle tens of thousands of notes or gigabytes of data — something we quickly realized after some users with over 100K notes approached us with significant performance issues. While it "worked", it was neither efficient nor particularly safe.
In August 2023, we began slowly migrating the Notesnook Core to SQLite. While migrating we quickly realized we'd need TypeScript if this was ever going to work because Kysely (the underlying query builder we used) heavily relied on types being there. So from the very first day we were doing 2 things: migrating the database to SQLite, and migrating the codebase to TypeScript.
I won't bore you with too many technical details but we had a few primary goals behind all this upheaval:
The clients should perform predictably regardless of how much data you have.
The clients should use as less RAM as possible to work in extremely constrained enviroments (e.g. iOS limits all Share Extensions to around 50 MB before crashing).
With v3 we have acheived both of these goals.
Fixing the Sync
Sync has notoriously been one of the least stable parts of Notesnook. From the beginning, we opted for a decentralized syncing system where each client could independently sync items without depending on a central server. This had a few drawbacks:
Since each client was independent, it was a nightmare to debug why something wasn't syncing.
In order to be efficient, we detected changed items based on their modified timestamp, but this forced us to juggle with the various inconsistencies of time on different devices.
Building everything on top of timestamps meant resumability during sync could never be stable.
In short, our old sync logic heavily depended on time to be the exact same (down to the last millisecond) for things to work. As you can imagine, this didn't work out that well.
While migrating everything to SQLite, we greatly simplified the sync logic opting for a centralized system this time. This allowed us to completely get rid of all the time juggling resolving all the edge cases in our previous logic.
As a result, in v3 you'll notice a bug free sync that "just works" regardless of where you are, how many devices you are on, the time difference between them etc.
Features, Features
In every new release, it is always a struggle to decide which features to add and which features to leave behind for later. v3 was no different because ultimately each feature you add pushes the deadline further and further away. Even then, v3 is one of our biggest releases ever.
Note linking
The crown jewel of v3! Yes, finally, you can link 2 notes together and even link directly to a specific block inside a note.
We have also added controls to quickly see which notes you have linked to, and which notes link to a particular note.
To keep things simple, a note link is no different than a normal hyperlink except that clicking on it will take you to the note instead of an external page. This also means that when you export your notes, your note links are automatically converted into hyperlinks that directly open the linked Markdown or HTML files of the notes. This is something no other app does instead keeping the links in their proprietary format making them essentially useless.
Tabs
The only reason we added tabs was to allow quick navigation to/from a note on clicking a note link. You can, of course, use tabs without ever linking a note.
Notesnook is one of the few note taking apps (besides Obsidian & OneNote) that has a full fledged tab experience. With v3, we are slowly upgrading Notesnook to become a power user's tool while keeping things just as simple as before.
Nested notebooks
At rest encryption
Thanks to SQLite, at rest encryption is now a thing on all platforms. The encryption key is randomly generated on database creation without requiring any user intervention. In other words, it "just works" keeping your notes secure even if your device is compromised. Which brings me to...
App lock
App lock is an enhancement on at rest encryption. When you enable app lock, it further encrypts your database encryption key with your app lock pin (or security key) so it's not just an "overlay". If you forget your app lock pin, the only way into the app is to reset & clear the database and start over.
At Notesnook, we take your security & privacy very seriously.
Export attachments with notes
Exports got a lot of love in v3 with things like automatic downloading & linking of attachments, organization of note files based on your notebook structure, and resolution of internal note links to actual HTML/MD files.
User profile
Custom colors for note organization
Fixed colors were nice but not nicer than custom colors.
Customizable side bar
You can now drag/drop and reorganize how things in your side bar looks including hiding things that you don't use. It can be as simple or as complex as you want.
Callouts
Beta release channels
Nested notebooks/topics
Note linking
Tabs
Table of Contents
Customizable/sortable sidebar
User profile
Custom colors
Yearly reminders
Callouts in editor
Migration to SQLite
New sync system
At rest encryption
app lock
export attachments with notes.`;
const average = (array: number[]) =>
array.reduce((a, b) => a + b) / array.length;
function createTensorWithBatchSize(opt: [number, number]) {
const size = opt.reduce((n, i) => n * i, 1);
return new Tensor("int64", new BigInt64Array(size).fill(BigInt(1)), opt);
}
function createBatch(batchSize: number, sequenceLength: number) {
const tensor = createTensorWithBatchSize([batchSize, sequenceLength]);
return {
input_ids: tensor,
attention_mask: tensor
};
}
function chunkify(tokens: string[], chunkSize: number, overlap: number) {
const chunks = [];
const totalParts = Math.ceil(tokens.length / chunkSize);
for (let i = 0; i < totalParts; ++i) {
const start = i === 0 ? 0 : i * chunkSize - i * overlap;
const end = start + chunkSize;
chunks.push(tokens.slice(start, end));
}
return chunks;
}
/**
*
* Helper function for padding values of an object, which are each arrays.
* NOTE: No additional checks are made here for validity of arguments.
* @param {Record<string, any[]>} item The input object.
* @param {number} length The length to pad to.
* @param {(key: string) => any} value_fn Determine the value to fill the array, based on its key.
* @param {string} side Which side to pad the array.
* @private
*/
function padHelper(
item: Record<string, any[]>,
length: number,
value_fn: (key: string) => any,
side: string
) {
for (const key of Object.keys(item)) {
const diff = length - item[key].length;
const value = value_fn(key);
const padData = new Array(diff).fill(value);
item[key] =
side === "right"
? [...item[key], ...padData]
: [...padData, ...item[key]];
}
}
function tokenizeInput(
pipeline: FeatureExtractionPipeline,
options: { max_length: number }
) {
const { tokenizer } = pipeline;
const { max_length } = options;
const encoded = tokenizer._encode_text(text) ?? [];
const encodedTokens = chunkify(encoded, max_length - 12, 20).map((chunk) => {
const { tokens, token_type_ids } = tokenizer.post_processor._call(
chunk,
[],
{ add_special_tokens: true }
);
const input_ids = tokenizer.model.convert_tokens_to_ids(tokens);
return {
input_ids,
attention_mask: new Array(input_ids.length).fill(1),
token_type_ids: token_type_ids || []
};
});
for (let i = 0; i < encodedTokens.length; ++i) {
if (encodedTokens[i].input_ids.length === max_length) {
continue;
} else if (encodedTokens[i].input_ids.length > max_length) {
throw new Error("Truncation is not allowed.");
} else {
// t.length < max_length
// possibly pad
padHelper(
encodedTokens[i],
max_length,
(key) => (key === "input_ids" ? tokenizer.pad_token_id : 0),
tokenizer.padding_side
);
}
}
const dims = [encodedTokens.length, encodedTokens[0].input_ids.length];
const input: Record<string, Tensor> = {};
for (const key of Object.keys(encodedTokens[0])) {
input[key] = new Tensor(
"int64",
BigInt64Array.from(encodedTokens.flatMap((x) => x[key]).map(BigInt)),
dims
);
}
return input;
}
async function generateEmbeddings(
pipeline: FeatureExtractionPipeline,
input: Record<string, Tensor>
) {
const { tokenizer, model } = pipeline;
const outputs = await model._call(input);
let result =
outputs.last_hidden_state ?? outputs.logits ?? outputs.token_embeddings;
const pooling: "mean" | "cls" = tokenizer._tokenizer_config.cls_token
? "cls"
: "mean";
if (pooling === "mean") {
result = mean_pooling(result, input.attention_mask);
} else if (pooling === "cls") {
result = result.slice(null, 0);
}
return result;
}
export const CREATE_BUTTON_MAP = {
notes: {
title: "Add a note",
onClick: () => useEditorStore.getState().newSession()
onClick: async () => {
const models = [
// ["Xenova/jina-embeddings-v2-small-en", 87]
["Snowflake/snowflake-arctic-embed-s", 41],
// ["Snowflake/snowflake-arctic-embed-m", 20],
["Snowflake/snowflake-arctic-embed-xs", 62],
// ["Xenova/GIST-small-Embedding-v0", 57],
["Xenova/GIST-all-MiniLM-L6-v2", 88],
// ["Xenova/NoInstruct-small-Embedding-v0", 40],
["TaylorAI/bge-micro-v2", 100]
// ["TaylorAI/gte-tiny", 88],
// ["Xenova/gte-small", 63]
] as const;
// env.backends.onnx.wasm.proxy = false;
// env.backends.onnx.webgpu.profiling = {
// mode: "default"
// };
// env.webgpu.profiling = {
// mode: "default"
// };
console.log(env);
for (const provider of ["webgpu"] as const) {
if (provider === "webgpu") env.backends.onnx.wasm.proxy = false;
else env.backends.onnx.wasm.proxy = true;
for (const [modelId, rank] of models) {
console.log("loading model", modelId);
const extractor = await pipeline("feature-extraction", modelId, {
// progress_callback: console.log,
device: provider,
dtype: "fp32",
cache_dir: "/models",
local_files_only: true
// session_options: {
// // enableGraphCapture: true
// }
});
const input = tokenizeInput(extractor, { max_length: 512 });
// console.time(`[warming up] ${rank}. ${modelId} (${provider})`);
for (let i = 0; i < 3; ++i) {
console.time(`${rank}. ${modelId} (${provider})`);
await generateEmbeddings(extractor, input);
console.timeEnd(`${rank}. ${modelId} (${provider})`);
}
// console.timeEnd(`[warming up] ${rank}. ${modelId} (${provider})`);
// const timings: number[] = [];
// for (let i = 0; i < 10; ++i) {
// const now = performance.now();
// await generateEmbeddings(extractor, input);
// timings.push(performance.now() - now);
// }
// console.log(
// `[bench] ${rank}. ${modelId} (${provider})`,
// `avg: ${average(timings)}ms`,
// `min: ${Math.min(...timings)}ms`,
// `max: ${Math.max(...timings)}ms`
// );
}
}
} // useEditorStore.getState().newSession()
},
notebooks: {
title: "Create a notebook",
@@ -77,12 +338,19 @@ export async function introduceFeatures() {
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
export async function createBackup(rescueMode = false) {
export async function createBackup(
options: {
rescueMode?: boolean;
noVerify?: boolean;
} = {}
) {
const { rescueMode, noVerify } = options;
const { isLoggedIn } = useUserStore.getState();
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
const verified = rescueMode || encryptBackups || (await verifyAccount());
const verified =
rescueMode || encryptBackups || noVerify || (await verifyAccount());
if (!verified) {
showToast("error", "Could not create a backup: user verification failed.");
return false;

View File

@@ -18,9 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import hotkeys from "hotkeys-js";
import { navigate } from "../navigation";
// import { store as themestore } from "../stores/theme-store";
import { GlobalKeyboard } from "../utils/keyboard";
import { useEditorStore } from "../stores/editor-store";
import { useStore as useSearchStore } from "../stores/search-store";
import { useEditorManager } from "../components/editor/manager";
const KEYMAP = [
// {
@@ -57,11 +58,24 @@ const KEYMAP = [
keys: ["command+f", "ctrl+f"],
description: "Search all notes",
global: false,
action: (e) => {
if (e.target?.classList.contains("ProseMirror")) return;
action: (e: KeyboardEvent) => {
const isInEditor =
e.target instanceof HTMLElement &&
!!e.target?.closest(".editor-container");
if (isInEditor) {
const activeSession = useEditorStore.getState().getActiveSession();
if (activeSession?.type === "readonly") {
e.preventDefault();
const editor = useEditorManager
.getState()
.getEditor(activeSession.id);
editor?.editor?.startSearch();
}
return;
}
e.preventDefault();
navigate("/search/notes");
useSearchStore.setState({ isSearching: true, searchType: "notes" });
}
}
// {
@@ -137,7 +151,11 @@ export function registerKeyMap() {
KEYMAP.forEach((key) => {
hotkeys(
key.keys.join(","),
{ element: key.global ? GlobalKeyboard : window },
{
element: key.global
? (GlobalKeyboard as unknown as HTMLElement)
: document.body
},
key.action
);
});

View File

@@ -177,7 +177,7 @@ function isIgnored(key: keyof typeof NoticesData) {
let openedToast: { hide: () => void } | null = null;
async function saveBackup() {
if (IS_DESKTOP_APP) {
await createBackup();
await createBackup({ noVerify: true });
} else if (isUserPremium() && !IS_TESTING) {
if (openedToast !== null) return;
openedToast = showToast(

View File

@@ -18,39 +18,93 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
SqliteDriver as KSqliteDriver,
SqliteDialectConfig,
Dialect,
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
SqliteQueryCompiler,
DatabaseConnection,
QueryResult,
CompiledQuery,
Driver
} from "kysely";
import { desktop } from "../desktop-bridge";
import Worker from "./sqlite.worker.desktop.ts?worker";
import type { SQLiteWorker } from "./sqlite.worker.desktop";
import { wrap, Remote } from "comlink";
import { Mutex } from "async-mutex";
class SqliteDriver extends KSqliteDriver {
constructor(private readonly config: SqliteDialectConfig & { name: string }) {
super(config);
class SqliteDriver implements Driver {
connection?: DatabaseConnection;
private connectionMutex = new Mutex();
worker: Remote<SQLiteWorker> = wrap<SQLiteWorker>(new Worker());
constructor(private readonly config: { name: string }) {}
async init(): Promise<void> {
const path = await desktop!.integration.resolvePath.query({
filePath: `userData/${this.config.name}.sql`
});
await this.worker.open(path);
this.connection = new SqliteWorkerConnection(this.worker);
}
async acquireConnection(): Promise<DatabaseConnection> {
if (!this.connection) throw new Error("Driver not initialized.");
// SQLite only has one single connection. We use a mutex here to wait
// until the single connection has been released.
await this.connectionMutex.waitForUnlock();
await this.connectionMutex.acquire();
return this.connection;
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("begin"));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("commit"));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("rollback"));
}
async releaseConnection(): Promise<void> {
this.connectionMutex.release();
}
async destroy(): Promise<void> {
await this.worker.close();
}
async delete() {
const path = await desktop!.integration.resolvePath.query({
filePath: `userData/${this.config.name}.sql`
});
await desktop?.integration.deleteFile.query(path);
await this.destroy();
await this.worker.delete(path);
}
}
export const createDialect = (name: string): Dialect => {
class SqliteWorkerConnection implements DatabaseConnection {
constructor(private readonly worker: Remote<SQLiteWorker>) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(
compiledQuery: CompiledQuery<unknown>
): Promise<QueryResult<R>> {
const { parameters, sql } = compiledQuery;
return this.worker.run(sql, parameters as any) as unknown as QueryResult<R>;
}
}
export const createDialect = (name: string, _encrypted: boolean): Dialect => {
return {
createDriver: () =>
new SqliteDriver({
name,
database: async () => {
const path = await desktop!.integration.resolvePath.query({
filePath: `userData/${name}.sql`
});
return window.createSQLite3Database(path).unsafeMode(true);
}
name
}),
createAdapter: () => new SqliteAdapter(),
createIntrospector: (db) => new SqliteIntrospector(db),

View File

@@ -34,6 +34,7 @@ declare module "kysely" {
export const createDialect = (
name: string,
encrypted: boolean,
init?: () => Promise<void>
): Dialect => {
return {
@@ -41,6 +42,7 @@ export const createDialect = (
new WaSqliteWorkerDriver({
async: !isFeatureSupported("opfs"),
dbName: name,
encrypted,
init
}),
createAdapter: () => new SqliteAdapter(),

View File

@@ -83,7 +83,7 @@ export class SharedService<T extends object> extends EventTarget {
}
activate(
portProviderFunc: () => MessagePort | Promise<MessagePort>,
portProviderFunc: () => Promise<{ port: MessagePort; onclose: () => void }>,
onClientConnected: () => Promise<void>
) {
if (this.#onDeactivate) return;
@@ -97,7 +97,7 @@ export class SharedService<T extends object> extends EventTarget {
navigator.locks
.request(LOCK_NAME, { signal: this.#onDeactivate.signal }, async () => {
// Get the port to request client ports.
const port = await portProviderFunc();
const { port, onclose } = await portProviderFunc();
port.start();
// Listen for client requests. A separate BroadcastChannel
@@ -159,6 +159,7 @@ export class SharedService<T extends object> extends EventTarget {
// Release the lock only on user abort or context destruction.
return new Promise((_, reject) => {
this.#onDeactivate?.signal.addEventListener("abort", () => {
onclose();
broadcastChannel.close();
reject(this.#onDeactivate?.signal.reason);
});

View File

@@ -0,0 +1,25 @@
/*
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 { SQLite } from "@notesnook/desktop";
import { expose } from "comlink";
export type SQLiteWorker = typeof SQLite.prototype;
const db = new SQLite();
expose(db);

View File

@@ -32,219 +32,230 @@ type PreparedStatement = {
columns: string[];
};
let sqlite: SQLiteAPI;
let db: number | undefined = undefined;
let vfs: IDBBatchAtomicVFS | AccessHandlePoolVFS | null = null;
let initialized = false;
const preparedStatements: Map<string, PreparedStatement> = new Map();
const retryCounter: Record<string, number> = {};
console.log("new sqlite worker");
async function open(dbName: string, async: boolean, url?: string) {
if (db) {
console.error("Database is already initialized", db);
return;
}
const option = url ? { locateFile: () => url } : {};
const sqliteModule = async
? await import("./wa-sqlite-async").then(
({ default: SQLiteAsyncESMFactory }) => SQLiteAsyncESMFactory(option)
)
: await import("./wa-sqlite").then(({ default: SQLiteSyncESMFactory }) =>
SQLiteSyncESMFactory(option)
);
sqlite = Factory(sqliteModule);
vfs = await getVFS(dbName, async);
sqlite.vfs_register(vfs, false);
db = await sqlite.open_v2(dbName, undefined, `multipleciphers-${vfs.name}`);
}
/**
* Wrapper function for preparing SQL statements with caching
* to avoid unnecessary computations.
*/
async function prepare(sql: string) {
if (!db) throw new Error("Database is not initialized.");
try {
const cached = preparedStatements.get(sql);
if (cached !== undefined) return cached;
const str = sqlite.str_new(db, sql);
const prepared = await sqlite.prepare_v2(db, sqlite.str_value(str));
if (!prepared) return;
const statement: PreparedStatement = {
stmt: prepared.stmt,
columns: sqlite.column_names(prepared.stmt)
};
preparedStatements.set(sql, statement);
sqlite.str_finish(str);
// reset retry count on success
retryCounter[sql] = 0;
return statement;
} catch (ex) {
console.error(ex);
// statement prepare process can be flaky so retry at least 5 times
// before giving up.
if (retryCounter[sql] < 5) {
retryCounter[sql] = (retryCounter[sql] || 0) + 1;
console.warn("Failed to prepare statement. Retrying:", sql);
return prepare(sql);
} else retryCounter[sql] = 0;
if (ex instanceof Error || ex instanceof SQLiteError)
ex.message += ` (query: ${sql})`;
throw ex;
}
}
async function run(
sql: string,
mode: RunMode,
parameters?: SQLiteCompatibleType[]
) {
const prepared = await prepare(sql);
if (!prepared) return [];
try {
if (parameters) sqlite.bind_collection(prepared.stmt, parameters);
// fast path for exec statements
if (mode === "exec") {
while ((await sqlite.step(prepared.stmt)) === SQLITE_ROW);
return [];
}
const rows: Record<string, SQLiteCompatibleType>[] = [];
while ((await sqlite.step(prepared.stmt)) === SQLITE_ROW) {
const row = sqlite.row(prepared.stmt);
const acc: Record<string, SQLiteCompatibleType> = {};
row.forEach((v, i) => (acc[prepared.columns[i]] = v));
rows.push(acc);
}
return rows;
} catch (e) {
if (e instanceof Error || e instanceof SQLiteError)
e.message += ` (query: ${sql})`;
throw e;
} finally {
await sqlite
.reset(prepared.stmt)
// we must clear/destruct the prepared statement if it can't be reset
.catch(() =>
sqlite
.finalize(prepared.stmt)
// ignore error (we will just prepare a new statement)
.catch(console.error)
.finally(() => preparedStatements.delete(sql))
);
}
}
async function exec<R>(
mode: RunMode,
sql: string,
parameters?: SQLiteCompatibleType[]
): Promise<QueryResult<R>> {
if (!sql.startsWith("PRAGMA key")) {
await waitForDatabase();
}
if (!db) throw new Error("No database is not opened.");
const rows = (await run(sql, mode, parameters)) as R[];
if (mode === "query") return { rows };
// initialize the database after it has been successfully decrypted.
// all queries prior to that must wait otherwise we get the
// "file is not a database" error
if (sql.startsWith("PRAGMA key")) await initialize();
return {
insertId: BigInt(sqlite.last_insert_rowid(db)),
numAffectedRows: BigInt(sqlite.changes(db)),
rows: mode === "raw" ? rows : []
};
}
async function close() {
if (!db) return;
for (const [_, prepared] of preparedStatements) {
await sqlite.finalize(prepared.stmt);
}
preparedStatements.clear();
await sqlite.close(db);
await vfs?.close();
db = undefined;
class _SQLiteWorker {
sqlite!: SQLiteAPI;
db: number | undefined = undefined;
vfs: IDBBatchAtomicVFS | AccessHandlePoolVFS | null = null;
initialized = false;
}
preparedStatements: Map<string, PreparedStatement> = new Map();
retryCounter: Record<string, number> = {};
constructor(
private readonly dbName: string,
private readonly encrypted: boolean
) {
console.log("new sqlite worker", dbName, encrypted);
}
async function exportDatabase(dbName: string, async: boolean) {
const vfs = await getVFS(dbName, async);
const stream = new ReadableStream(new DatabaseSource(vfs, dbName));
return transfer(stream, [stream]);
}
async open(async: boolean, url?: string) {
if (this.db) {
console.error("Database is already initialized", this.db);
return;
}
async function deleteDatabase(dbName: string, async: boolean) {
await close();
if (vfs) await vfs.delete();
else await (await getVFS(dbName, async)).delete();
}
const option = url ? { locateFile: () => url } : {};
const sqliteModule = async
? await import("./wa-sqlite-async").then(
({ default: SQLiteAsyncESMFactory }) => SQLiteAsyncESMFactory(option)
)
: await import("./wa-sqlite").then(({ default: SQLiteSyncESMFactory }) =>
SQLiteSyncESMFactory(option)
);
this.sqlite = Factory(sqliteModule);
this.vfs = await this.getVFS(this.dbName, async);
async function getVFS(dbName: string, async: boolean) {
const vfs = async
? await import("./IDBBatchAtomicVFS").then(
({ IDBBatchAtomicVFS }) =>
new IDBBatchAtomicVFS(dbName, { durability: "strict" })
)
: await import("./AccessHandlePoolVFS").then(
({ AccessHandlePoolVFS }) => new AccessHandlePoolVFS(dbName)
this.sqlite.vfs_register(this.vfs, false);
this.db = await this.sqlite.open_v2(
this.dbName,
undefined,
`multipleciphers-${this.vfs.name}`
);
}
/**
* Wrapper function for preparing SQL statements with caching
* to avoid unnecessary computations.
*/
async prepare(sql: string): Promise<PreparedStatement | undefined> {
if (!this.db) throw new Error("Database is not initialized.");
try {
const cached = this.preparedStatements.get(sql);
if (cached !== undefined) return cached;
const str = this.sqlite.str_new(this.db, sql);
const prepared = await this.sqlite.prepare_v2(
this.db,
this.sqlite.str_value(str)
);
if ("isReady" in vfs) await vfs.isReady;
return vfs;
if (!prepared) return;
const statement: PreparedStatement = {
stmt: prepared.stmt,
columns: this.sqlite.column_names(prepared.stmt)
};
this.preparedStatements.set(sql, statement);
this.sqlite.str_finish(str);
// reset retry count on success
this.retryCounter[sql] = 0;
return statement;
} catch (ex) {
console.error(ex);
// statement prepare process can be flaky so retry at least 5 times
// before giving up.
if (this.retryCounter[sql] < 5) {
this.retryCounter[sql] = (this.retryCounter[sql] || 0) + 1;
console.warn("Failed to prepare statement. Retrying:", sql);
return this.prepare(sql);
} else this.retryCounter[sql] = 0;
if (ex instanceof Error || ex instanceof SQLiteError)
ex.message += ` (query: ${sql})`;
throw ex;
}
}
async exec(sql: string, mode: RunMode, parameters?: SQLiteCompatibleType[]) {
const prepared = await this.prepare(sql);
if (!prepared) return [];
try {
if (parameters) this.sqlite.bind_collection(prepared.stmt, parameters);
// fast path for exec statements
if (mode === "exec") {
while ((await this.sqlite.step(prepared.stmt)) === SQLITE_ROW);
return [];
}
const rows: Record<string, SQLiteCompatibleType>[] = [];
while ((await this.sqlite.step(prepared.stmt)) === SQLITE_ROW) {
const row = this.sqlite.row(prepared.stmt);
const acc: Record<string, SQLiteCompatibleType> = {};
row.forEach((v, i) => (acc[prepared.columns[i]] = v));
rows.push(acc);
}
return rows;
} catch (e) {
if (e instanceof Error || e instanceof SQLiteError)
e.message += ` (query: ${sql})`;
throw e;
} finally {
await this.sqlite
.reset(prepared.stmt)
// we must clear/destruct the prepared statement if it can't be reset
.catch(() =>
this.sqlite
.finalize(prepared.stmt)
// ignore error (we will just prepare a new statement)
.catch(console.error)
.finally(() => this.preparedStatements.delete(sql))
);
}
}
async run<R>(
mode: RunMode,
sql: string,
parameters?: SQLiteCompatibleType[]
): Promise<QueryResult<R>> {
if (this.encrypted && !sql.startsWith("PRAGMA key")) {
await this.waitForDatabase();
}
if (!this.db) throw new Error("No database is not opened.");
const rows = (await this.exec(sql, mode, parameters)) as R[];
if (mode === "query") return { rows };
// initialize the database after it has been successfully decrypted.
// all queries prior to that must wait otherwise we get the
// "file is not a database" error
if (this.encrypted && sql.startsWith("PRAGMA key")) await this.initialize();
return {
insertId: BigInt(this.sqlite.last_insert_rowid(this.db)),
numAffectedRows: BigInt(this.sqlite.changes(this.db)),
rows: mode === "raw" ? rows : []
};
}
async close() {
if (!this.db) return;
for (const [_, prepared] of this.preparedStatements) {
await this.sqlite.finalize(prepared.stmt);
}
this.preparedStatements.clear();
await this.sqlite.close(this.db);
await this.vfs?.close();
this.db = undefined;
this.initialized = false;
}
async export(dbName: string, async: boolean) {
const vfs = await this.getVFS(dbName, async);
const stream = new ReadableStream(new DatabaseSource(vfs, dbName));
return transfer(stream, [stream]);
}
async delete(dbName: string, async: boolean) {
await this.close();
if (this.vfs) await this.vfs.delete();
else await (await this.getVFS(dbName, async)).delete();
}
async getVFS(dbName: string, async: boolean) {
const vfs = async
? await import("./IDBBatchAtomicVFS").then(
({ IDBBatchAtomicVFS }) =>
new IDBBatchAtomicVFS(dbName, { durability: "strict" })
)
: await import("./AccessHandlePoolVFS").then(
({ AccessHandlePoolVFS }) => new AccessHandlePoolVFS(dbName)
);
if ("isReady" in vfs) await vfs.isReady;
return vfs;
}
async initialize() {
self.dispatchEvent(
new MessageEvent("message", {
data: { type: "databaseInitialized", dbName: this.dbName }
})
);
console.log("Database initialized", this.db);
this.initialized = true;
}
async waitForDatabase() {
// if the database hasn't yet been initialized.
if (!this.initialized) {
console.log("Waiting for database to be initialized...", this.db);
return await new Promise<boolean>((resolve) =>
self.addEventListener("message", (ev) => {
if (
ev.data.type === "databaseInitialized" &&
ev.data.dbName === this.dbName
)
resolve(true);
})
);
}
return true;
}
}
async function initialize() {
self.dispatchEvent(
new MessageEvent("message", { data: { type: "databaseInitialized" } })
);
console.log("Database initialized", db);
initialized = true;
}
const worker = {
close,
open,
run: exec,
export: exportDatabase,
delete: deleteDatabase
};
export type SQLiteWorker = typeof worker;
export type SQLiteWorker = typeof _SQLiteWorker.prototype;
addEventListener("message", async (event) => {
if (!event.data.type) {
await worker.open(event.data.dbName, event.data.async, event.data.uri);
const worker = new _SQLiteWorker(event.data.dbName, event.data.encrypted);
await worker.open(event.data.async, event.data.uri);
const providerPort = createSharedServicePort(worker);
postMessage(null, [providerPort]);
self.addEventListener("beforeunload", () => worker.close());
}
});
async function waitForDatabase() {
// if the database hasn't yet been initialized.
if (!initialized) {
console.log("Waiting for database to be initialized...", db);
return await new Promise<boolean>((resolve) =>
self.addEventListener("message", (ev) => {
if (ev.data.type === "databaseInitialized") resolve(true);
})
);
}
return true;
}

View File

@@ -26,7 +26,12 @@ import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url";
import { Mutex } from "async-mutex";
import { SharedService } from "./shared-service";
type Config = { dbName: string; async: boolean; init?: () => Promise<void> };
type Config = {
dbName: string;
async: boolean;
encrypted: boolean;
init?: () => Promise<void>;
};
const servicePool = new Map<
string,
@@ -55,7 +60,6 @@ export class WaSqliteWorkerDriver implements Driver {
if (closed) {
console.log("Already activated. Reinitializing...");
await service.proxy.open(
this.config.dbName,
this.config.async,
this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
);
@@ -72,19 +76,24 @@ export class WaSqliteWorkerDriver implements Driver {
service.activate(
() =>
new Promise<MessagePort>((resolve) => {
new Promise<{ port: MessagePort; onclose: () => void }>((resolve) => {
console.log("initializing worker");
this.needsInitialization = true;
const worker = new Worker();
worker.addEventListener(
"message",
(event) => resolve(event.ports[0]),
(event) =>
resolve({
port: event.ports[0],
onclose: () => worker.terminate()
}),
{ once: true }
);
worker.postMessage({
dbName: this.config.dbName,
async: this.config.async,
encrypted: this.config.encrypted,
uri: this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
});
}),

View File

@@ -307,7 +307,7 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
break;
}
case "backup": {
await createBackup(true);
await createBackup();
break;
}
default: {

View File

@@ -26,7 +26,7 @@ type ContentToggle = {
onToggle: () => void;
label: string;
dateEdited: number;
resolveConflict: (options: { saveCopy: boolean }) => void;
resolveConflict: (saveCopy: boolean) => void;
readonly: boolean;
sx: FlexProps["sx"];
};
@@ -50,7 +50,7 @@ function ContentToggle(props: ContentToggle) {
<Button
variant="accent"
mr={2}
onClick={() => resolveConflict({ saveCopy: true })}
onClick={() => resolveConflict(true)}
p={1}
px={2}
>
@@ -61,7 +61,7 @@ function ContentToggle(props: ContentToggle) {
variant={isOtherSelected ? "error" : "accent"}
onClick={() => {
if (isOtherSelected) {
resolveConflict({ saveCopy: false });
resolveConflict(false);
} else {
onToggle();
}

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 { useLayoutEffect, useRef, useState } from "react";
import { useCallback, useLayoutEffect, useRef, useState } from "react";
import { Flex, Text, Button } from "@theme-ui/components";
import { Copy, Restore } from "../icons";
import ContentToggle from "./content-toggle";
@@ -45,6 +45,24 @@ function DiffViewer(props: DiffViewerProps) {
);
const root = useRef<HTMLDivElement>(null);
const onResolveContent = useCallback(
(saveCopy: boolean) => {
const toKeep =
selectedContent === 1 ? session.content?.conflicted : session.content;
const toCopy =
selectedContent === 1 ? session.content : session.content?.conflicted;
if (!toKeep) return;
resolveConflict({
note: session.note,
toKeep,
toCopy: saveCopy ? toCopy : undefined,
toKeepDateEdited: toKeep.dateEdited,
dateResolved: conflictedContent!.dateModified
});
},
[conflictedContent, selectedContent, session.content, session.note]
);
useLayoutEffect(() => {
setConflictedContent((c) => {
if (c?.dateEdited === session.content?.conflicted?.dateEdited) return c;
@@ -185,15 +203,7 @@ function DiffViewer(props: DiffViewerProps) {
isSelected={selectedContent === 0}
isOtherSelected={selectedContent === 1}
onToggle={() => setSelectedContent((s) => (s === 0 ? -1 : 0))}
resolveConflict={({ saveCopy }) => {
resolveConflict({
note: session.note,
toKeep: content.data,
toCopy: saveCopy ? conflictedContent : undefined,
toKeepDateEdited: content.dateEdited,
dateResolved: conflictedContent.dateModified
});
}}
resolveConflict={onResolveContent}
sx={{
borderStyle: "solid",
borderWidth: 0,
@@ -263,15 +273,7 @@ function DiffViewer(props: DiffViewerProps) {
<>
<ContentToggle
readonly={session.type === "diff"}
resolveConflict={({ saveCopy }) => {
resolveConflict({
note: session.note,
toKeep: conflictedContent.data,
toCopy: saveCopy ? content : undefined,
toKeepDateEdited: conflictedContent.dateEdited,
dateResolved: conflictedContent.dateModified
});
}}
resolveConflict={onResolveContent}
label={
session.type === "diff"
? "Current version"
@@ -326,31 +328,31 @@ async function resolveConflict({
dateResolved
}: {
note: Note;
toKeep: string;
toKeep: ContentItem;
toCopy?: ContentItem;
toKeepDateEdited: number;
dateResolved?: number;
}) {
await db.content.add({
id: note.contentId,
dateResolved,
sessionId: `${Date.now()}`,
...(toKeep.locked
? { data: toKeep.data, locked: true }
: { data: toKeep.data, locked: false })
});
await db.notes.add({
id: note.id,
dateEdited: toKeepDateEdited,
conflicted: false
});
await db.content.add({
id: note.contentId,
data: toKeep,
type: "tiptap",
dateResolved,
sessionId: `${Date.now()}`
});
if (toCopy) {
await createCopy(note, toCopy);
}
await notesStore.refresh();
useEditorStore.getState().openSession(note.id, { force: true });
}
async function createCopy(note: Note, content: ContentItem) {

View File

@@ -152,7 +152,6 @@ export function EditorActionBar() {
activeSession &&
activeSession.type !== "new" &&
activeSession.type !== "locked" &&
activeSession.type !== "readonly" &&
activeSession.type !== "diff" &&
activeSession.type !== "conflicted",
onClick: editor?.startSearch

View File

@@ -271,7 +271,7 @@ function EditorView({
const isContent =
item.type === "tiptap" && item.noteId === session.note.id;
const isNote = item.type === "note" && item.id === session.note.id;
if (isContent && lastChangedTime.current < item.dateEdited) {
if (isContent && lastChangedTime.current < item.dateModified) {
if (!item.locked) return editor.updateContent(item.data);
const result = await db.vault

View File

@@ -344,7 +344,6 @@ function TipTap(props: TipTapProps) {
};
}, []);
if (readonly) return null;
return (
<>
<ScopedThemeProvider
@@ -418,7 +417,7 @@ function TiptapWrapper(
editorContainer={() => {
if (editorContainerRef.current) return editorContainerRef.current;
const editorContainer = document.createElement("div");
editorContainer.classList.add("selectable");
editorContainer.classList.add("selectable", "editor-container");
editorContainer.style.flex = "1";
editorContainer.style.cursor = "text";
editorContainer.style.color =
@@ -427,6 +426,7 @@ function TiptapWrapper(
editorContainer.style.fontSize = `${editorConfig.fontSize}px`;
editorContainer.style.fontFamily =
getFontById(editorConfig.fontFamily)?.font || "sans-serif";
editorContainer.tabIndex = -1;
editorContainerRef.current = editorContainer;
return editorContainer;
}}

View File

@@ -162,9 +162,10 @@ function getErrorHelp(props: FallbackProps) {
if (
errorText.includes("file is not a database") ||
errorText.includes("unsupported file format") ||
errorText.includes("database disk image is malformed") ||
errorText.includes("null function or function signature mismatch") ||
errorText.includes("malformed database schema")
errorText.includes("malformed database schema") ||
/table ".+?" already exists/.test(errorText) ||
errorText.includes("corrupted migrations:")
) {
return {
explanation: `This error usually means the database file is either corrupt or it could not be decrypted.`,
@@ -195,6 +196,17 @@ function getErrorHelp(props: FallbackProps) {
resetErrorBoundary();
}
};
} else if (errorText.includes("database disk image is malformed")) {
return {
explanation: `This error usually means the search index is corrupted.`,
action:
"This error can be fixed by rebuilding the search index. This action won't result in any kind of data loss.",
fix: async () => {
const { db } = await import("../../common/db");
await db.lookup.rebuild();
resetErrorBoundary();
}
};
}
}

View File

@@ -181,7 +181,11 @@ function NavigationItem(
) : !isTablet && count !== undefined ? (
<Text
variant="subBody"
sx={{ mr: 1, bg: "hover", px: "3px", borderRadius: "default" }}
sx={{
mr: 1,
px: "3px",
borderRadius: "default"
}}
>
{count > 100 ? "100+" : count}
</Text>

View File

@@ -524,8 +524,6 @@ const menuItems: (
type: "button",
key: "duplicate",
title: "Duplicate",
//!isSynced ||
isDisabled: context?.locked,
icon: Duplicate.path,
onClick: () => store.get().duplicate(...ids),
multiSelect: true

View File

@@ -89,32 +89,7 @@ const features: Record<FeatureKeys, Feature> = {
)
}
]
: [
{
title: "Bi-directional note linking",
subtitle: "You can now link any 2 notes for easier referencing."
},
{
title: "Tabs",
subtitle:
"Open multiple notes side by side for faster multi-tasking."
},
{
title: "Nested notebooks",
subtitle:
"Create subnotebooks inside notebooks upto unlimited depth."
},
{
title: "At rest encryption",
subtitle:
"All your data will now be stored encrypted on your device to keep you safe even if your device gets compromised."
},
{
title: "App lock",
subtitle:
"App lock is now here on desktop/web apps! Put a lock on the app, and keep it safe from intruders."
}
],
: [],
cta: {
title: "Got it",
icon: Checkmark,

View File

@@ -244,98 +244,102 @@ function SettingsSideBar(props: SettingsSideBarProps) {
}}
data-test-id="settings-navigation-menu"
>
<Flex
sx={{
flexDirection: "column",
display: "flex",
overflow: "hidden"
}}
>
<Input
id="search"
name="search"
placeholder="Search"
data-test-id="settings-search"
<ScopedThemeProvider scope="navigationMenu" injectCssVars={false}>
<Flex
sx={{
m: 2,
mb: 2,
width: "auto",
bg: "background",
py: "7px"
flexDirection: "column",
display: "flex",
overflow: "hidden"
}}
onChange={(e) => {
const query = e.target.value.toLowerCase().trim();
if (!query)
return onNavigate(
SettingsGroups.filter((g) => g.section === route)
);
>
<Input
id="search"
name="search"
placeholder="Search"
data-test-id="settings-search"
sx={{
m: 2,
mb: 2,
width: "auto",
bg: "background",
py: "7px"
}}
onChange={(e) => {
const query = e.target.value.toLowerCase().trim();
if (!query)
return onNavigate(
SettingsGroups.filter((g) => g.section === route)
);
const groups: SettingsGroup[] = [];
for (const group of SettingsGroups) {
const isTitleMatch =
typeof group.header === "string" &&
group.header.toLowerCase().includes(query);
const isSectionMatch = group.section.includes(query);
const groups: SettingsGroup[] = [];
for (const group of SettingsGroups) {
const isTitleMatch =
typeof group.header === "string" &&
group.header.toLowerCase().includes(query);
const isSectionMatch = group.section.includes(query);
if (isTitleMatch || isSectionMatch) {
groups.push(group);
continue;
if (isTitleMatch || isSectionMatch) {
groups.push(group);
continue;
}
const settings = group.settings.filter((setting) => {
const description =
typeof setting.description === "function"
? setting.description()
: setting.description;
return [
description || "",
setting.keywords?.join(" ") || "",
setting.title
]
.join(" ")
?.toLowerCase()
.includes(query);
});
if (!settings.length) continue;
groups.push({ ...group, settings });
}
const settings = group.settings.filter((setting) => {
const description =
typeof setting.description === "function"
? setting.description()
: setting.description;
return [
description || "",
setting.keywords?.join(" ") || "",
setting.title
]
.join(" ")
?.toLowerCase()
.includes(query);
});
if (!settings.length) continue;
groups.push({ ...group, settings });
}
onNavigate(groups);
}}
/>
{sectionGroups.map((group) => (
<Flex key={group.key} sx={{ flexDirection: "column", mb: 2 }}>
<Text
variant={"subBody"}
sx={{
fontWeight: "bold",
color: "paragraph",
mx: 3,
mb: 1
}}
>
{group.title}
</Text>
{group.sections.map(
(section) =>
(!section.isHidden || !section.isHidden()) && (
<NavigationItem
key={section.key}
icon={section.icon}
title={section.title}
selected={section.key === route}
onClick={() => {
onNavigate(
SettingsGroups.filter((g) => g.section === section.key)
);
setRoute(section.key);
}}
/>
)
)}
</Flex>
))}
</Flex>
onNavigate(groups);
}}
/>
{sectionGroups.map((group) => (
<Flex key={group.key} sx={{ flexDirection: "column", mb: 2 }}>
<Text
variant={"subBody"}
sx={{
fontWeight: "bold",
color: "paragraph",
mx: 3,
mb: 1
}}
>
{group.title}
</Text>
{group.sections.map(
(section) =>
(!section.isHidden || !section.isHidden()) && (
<NavigationItem
key={section.key}
icon={section.icon}
title={section.title}
selected={section.key === route}
onClick={() => {
onNavigate(
SettingsGroups.filter(
(g) => g.section === section.key
)
);
setRoute(section.key);
}}
/>
)
)}
</Flex>
))}
</Flex>
</ScopedThemeProvider>
</FlexScrollContainer>
);
}

View File

@@ -134,7 +134,7 @@ class AppStore extends BaseStore<AppStore> {
});
onPageVisibilityChanged(async (_, documentHidden) => {
if (!documentHidden) return;
if (documentHidden) return;
logger.info("Page visibility changed. Reconnecting SSE...");
await db.connectSSE({ force: false }).catch(logger.error);

View File

@@ -233,6 +233,15 @@ class EditorStore extends BaseStore<EditorStore> {
if (noteId && session.id !== noteId && session.note.id !== noteId)
continue;
if (isDeleted(item) || isTrashItem(item)) clearIds.push(session.id);
// if a note becomes conflicted, reopen the session
else if (
session.type !== "conflicted" &&
item.type === "tiptap" &&
item.conflicted
)
waitForSync().then(() =>
openSession(session.note.id, { force: true, silent: true })
);
// if a note is locked, reopen the session
else if (
session.type === "default" &&
@@ -257,9 +266,7 @@ class EditorStore extends BaseStore<EditorStore> {
}
// if a deleted note is restored, reopen the session
else if (session.type === "deleted" && item.type === "note") {
waitForSync().then(() =>
openSession(session.note.id, { force: true, silent: true })
);
openSession(session.note.id, { force: true, silent: true });
}
// if a readonly note is made editable, reopen the session
else if (
@@ -267,9 +274,7 @@ class EditorStore extends BaseStore<EditorStore> {
item.type === "note" &&
!item.readonly
)
waitForSync().then(() =>
openSession(session.note.id, { force: true, silent: true })
);
openSession(session.note.id, { force: true, silent: true });
// update the note in all sessions
else if (item.type === "note") {
updateSession(
@@ -344,6 +349,10 @@ class EditorStore extends BaseStore<EditorStore> {
session.type !== "new" && session.note.contentId;
if (!contentId || !event.ids.includes(contentId)) continue;
if (
// if note's conflict is resolved
(session.type === "conflicted" && !event.item.conflicted) ||
// if note becomes conflicted
(session.type !== "conflicted" && event.item.conflicted) ||
// if note is locked
(session.type === "default" &&
!session.locked &&
@@ -548,12 +557,14 @@ class EditorStore extends BaseStore<EditorStore> {
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
const session = getSession(noteId);
if (session && !options.force && !session.needsHydration) {
return this.activateSession(noteId, options.activeBlockId);
}
if (session && !options.force) {
if (!session.needsHydration) {
return this.activateSession(noteId, options.activeBlockId);
}
if (session && (session.type === "diff" || session.type === "conflicted")) {
return openDiffSession(session.note.id, session.id);
if (session.type === "diff" || session.type === "conflicted") {
return openDiffSession(session.note.id, session.id);
}
}
if (session && session.id) await db.fs().cancel(session.id);
@@ -566,19 +577,7 @@ class EditorStore extends BaseStore<EditorStore> {
const isPreview = session ? session.preview : !options?.newSession;
const isLocked = await db.vaults.itemExists(note);
if (isLocked && note.type !== "trash") {
this.addSession(
{
type: "locked",
id: note.id,
pinned: session?.pinned,
note,
preview: isPreview,
activeBlockId: options.activeBlockId
},
!options.silent
);
} else if (note.conflicted) {
if (note.conflicted) {
const content = note.contentId
? await db.content.get(note.contentId)
: undefined;
@@ -612,6 +611,18 @@ class EditorStore extends BaseStore<EditorStore> {
},
!options.silent
);
} else if (isLocked && note.type !== "trash") {
this.addSession(
{
type: "locked",
id: note.id,
pinned: session?.pinned,
note,
preview: isPreview,
activeBlockId: options.activeBlockId
},
!options.silent
);
} else {
const content = note.contentId
? await db.content.get(note.contentId)

View File

@@ -22,15 +22,32 @@ import {
logger as _logger,
logManager
} from "@notesnook/core/dist/logger";
import { LogMessage } from "@notesnook/logger";
import { DatabasePersistence, NNStorage } from "../interfaces/storage";
import { LogMessage, NoopLogger, format } from "@notesnook/logger";
import { ZipFile, createZipStream } from "./streams/zip-stream";
import { createWriteStream } from "./stream-saver";
import { sanitizeFilename } from "@notesnook/common";
import { createDialect } from "../common/sqlite";
import { isFeatureSupported } from "./feature-check";
let logger: typeof _logger;
async function initializeLogger(persistence: DatabasePersistence = "db") {
initialize(new NNStorage("Logs", () => null, persistence), false);
let logger: typeof _logger = new NoopLogger();
async function initializeLogger() {
await initialize(
{
dialect: (name, init) => createDialect(name, false, init),
...(IS_DESKTOP_APP || isFeatureSupported("opfs")
? { journalMode: "WAL", lockingMode: "exclusive" }
: {
journalMode: "MEMORY",
lockingMode: "exclusive"
}),
tempStore: "memory",
synchronous: "normal",
pageSize: 8192,
cacheSize: -32000,
skipInitialization: !IS_DESKTOP_APP
},
false
);
logger = _logger.scope("notesnook-web");
}
@@ -47,11 +64,9 @@ async function downloadLogs() {
return;
}
controller.enqueue({
path: sanitizeFilename(log.key, { replacement: "-" }),
path: sanitizeFilename(log.key, { replacement: "-" }) + ".log",
data: textEncoder.encode(
(log.logs as LogMessage[])
.map((line) => JSON.stringify(line))
.join("\n")
(log.logs as LogMessage[]).map((line) => format(line)).join("\n")
)
});
}

View File

@@ -66,24 +66,28 @@ export function onPageVisibilityChanged(
if (isEventIgnored()) return;
if (!window.document.hasFocus()) return;
handler("focus", isDocumentHidden());
handler("focus", false);
}, 1000);
const onBlur = debounce((_) => {
if (isEventIgnored()) return;
if (window.document.hasFocus()) return;
handler("blur", isDocumentHidden());
handler("blur", true);
}, 1000);
document.addEventListener(visibilityChange(), onVisibilityChanged);
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
window.addEventListener("pageshow", onFocus);
window.addEventListener("pagehide", onBlur);
return () => {
document.removeEventListener(visibilityChange(), onVisibilityChanged);
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
window.removeEventListener("pageshow", onFocus);
window.removeEventListener("pagehide", onBlur);
};
}

View File

@@ -458,7 +458,7 @@ function BackupData(props: BaseRecoveryComponentProps<"backup">) {
"Please wait while we create a backup file for you to download."
}}
onSubmit={async () => {
await createBackup(true);
await createBackup({ rescueMode: true });
navigate("new");
}}
>

View File

@@ -56,6 +56,7 @@ export default defineConfig({
minify: "esbuild",
cssMinify: true,
emptyOutDir: true,
sourcemap: !isDesktop,
rollupOptions: {
output: {
plugins: [emitEditorStyles()],

View File

@@ -0,0 +1,3 @@
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,3 @@
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,3 @@
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,3 @@
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,3 @@
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -63,6 +63,15 @@ exports[`get web pricing tier > get monthly web tier > monthly-web-pricing 1`] =
}
`;
exports[`get web pricing tier > get monthly web tier > yearly-ios-pricing 1`] = `
{
"country": Any<String>,
"countryCode": Any<String>,
"discount": Any<Number>,
"sku": Any<String>,
}
`;
exports[`get web pricing tier > get yearly web tier > yearly-web-pricing 1`] = `
{
"country": Any<String>,

View File

@@ -28,7 +28,7 @@ import { login } from "./utils";
import { SqliteDialect } from "kysely";
import BetterSQLite3 from "better-sqlite3-multiple-ciphers";
const TEST_TIMEOUT = 30 * 1000;
const TEST_TIMEOUT = 60 * 1000;
test(
"case 1: device A & B should only download the changes from device C (no uploading)",
@@ -156,6 +156,111 @@ test(
TEST_TIMEOUT * 10
);
test(
"issue: syncing should not affect the items' dateModified",
async (t) => {
const [deviceA] = await Promise.all([initializeDevice("deviceA")]);
t.onTestFinished(async (r) => {
console.log(`${t.task.name} log out`);
await cleanup(deviceA);
});
const noteId = await deviceA.notes.add({
title: "Test note from device A",
content: { data: "<p>Hello</p>", type: "tiptap" }
});
const noteDateBefore = (await deviceA.notes.note(noteId)).dateModified;
const contentDateBefore = (await deviceA.content.findByNoteId(noteId))
.dateModified;
await deviceA.sync({ type: "full" });
const noteDateAfter = (await deviceA.notes.note(noteId)).dateModified;
const contentDateAfter = (await deviceA.content.findByNoteId(noteId))
.dateModified;
expect(noteDateBefore).toBe(noteDateAfter);
expect(contentDateBefore).toBe(contentDateAfter);
},
TEST_TIMEOUT
);
test(
"case 4: local content changed after remote content should create a conflict",
async (t) => {
const [deviceA, deviceB] = await Promise.all([
initializeDevice("deviceA"),
initializeDevice("deviceB")
]);
t.onTestFinished(async (r) => {
console.log(`${t.task.name} log out`);
await cleanup(deviceA, deviceB);
});
const noteId = await deviceA.notes.add({
title: "Test note from device A",
content: { data: "<p>Hello</p>", type: "tiptap" }
});
await deviceA.sync({ type: "full" });
await deviceB.sync({ type: "full" });
await deviceB.notes.add({
id: noteId,
content: { data: "<p>Hello (I am from device B)</p>", type: "tiptap" }
});
await deviceB.sync({ type: "full" });
await new Promise((resolve) => setTimeout(resolve, 10000));
await deviceA.notes.add({
id: noteId,
content: { data: "<p>Hello (I am from device A)</p>", type: "tiptap" }
});
await deviceA.sync({ type: "full" });
expect(await deviceA.notes.conflicted.count()).toBeGreaterThan(0);
},
TEST_TIMEOUT * 10
);
test(
"case 5: remote content changed after local content should create a conflict",
async (t) => {
const [deviceA, deviceB] = await Promise.all([
initializeDevice("deviceA"),
initializeDevice("deviceB")
]);
t.onTestFinished(async (r) => {
console.log(`${t.task.name} log out`);
await cleanup(deviceA, deviceB);
});
const noteId = await deviceA.notes.add({
title: "Test note from device A",
content: { data: "<p>Hello</p>", type: "tiptap" }
});
await deviceA.sync({ type: "full" });
await deviceB.sync({ type: "full" });
await deviceA.notes.add({
id: noteId,
content: { data: "<p>Hello (I am from device B)</p>", type: "tiptap" }
});
await deviceA.sync({ type: "full" });
await new Promise((resolve) => setTimeout(resolve, 10000));
await deviceB.notes.add({
id: noteId,
content: { data: "<p>Hello (I am from device A)</p>", type: "tiptap" }
});
await deviceB.sync({ type: "full" });
expect(await deviceB.notes.conflicted.count()).toBeGreaterThan(0);
},
TEST_TIMEOUT * 10
);
// test(
// "case 4: Device A's sync is interrupted halfway and Device B makes some changes afterwards and syncs.",
// async () => {
@@ -378,6 +483,8 @@ test(
* @returns {Promise<Database>}
*/
async function initializeDevice(id, capabilities = []) {
// initialize(new NodeStorageInterface(), false);
console.time(`Init ${id}`);
EV.subscribe(EVENTS.userCheckStatus, async (type) => {
return {
@@ -429,7 +536,6 @@ async function cleanup(...devices) {
await device.syncer.stop();
await device.user.logout();
device.eventManager.unsubscribeAll();
await device.reset();
}
EV.unsubscribeAll();
}

View File

@@ -63,6 +63,7 @@ import { Settings } from "../collections/settings";
import {
DatabaseAccessor,
DatabaseSchema,
RawDatabaseSchema,
SQLiteOptions,
changeDatabasePassword,
createDatabase,
@@ -74,7 +75,8 @@ import { Vaults } from "../collections/vaults";
import { KVStorage } from "../database/kv";
import { QueueValue } from "../utils/queue-value";
import { Sanitizer } from "../database/sanitizer";
import { dropTriggers } from "../database/triggers";
import { createTriggers, dropTriggers } from "../database/triggers";
import { NNMigrationProvider } from "../database/migrations";
type EventSourceConstructor = new (
uri: string,
@@ -179,7 +181,7 @@ class Database {
subscriptions = new Subscriptions(this.tokenManager);
offers = new Offers();
debug = new Debug();
pricing = new Pricing();
pricing = Pricing;
user = new UserManager(this);
syncer = new Sync(this);
@@ -244,7 +246,11 @@ class Database {
await sql.raw(statement).execute(this.sql());
}
await initializeDatabase(this.sql().withTables());
await initializeDatabase(
this.sql().withTables(),
new NNMigrationProvider()
);
await this.onInit(this.sql() as Kysely<RawDatabaseSchema>);
await this.initCollections();
return true;
}
@@ -274,10 +280,11 @@ class Database {
this.disconnectSSE();
});
this._sql = (await createDatabase(
"notesnook",
this.options.sqliteOptions
)) as unknown as Kysely<DatabaseSchema>;
this._sql = (await createDatabase<RawDatabaseSchema>("notesnook", {
...this.options.sqliteOptions,
migrationProvider: new NNMigrationProvider(),
onInit: (db) => this.onInit(db)
})) as unknown as Kysely<DatabaseSchema>;
await this.sanitizer.init();
@@ -290,6 +297,10 @@ class Database {
}
}
private async onInit(db: Kysely<RawDatabaseSchema>) {
await createTriggers(db);
}
async initCollections() {
await this.legacySettings.init();
// collections

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import http from "../utils/http";
type Product = {
export type Product = {
country: string;
countryCode: string;
sku?: string;

View File

@@ -123,7 +123,7 @@ describe.concurrent("merge content", (test) => {
await loginFakeUser(db);
const merger = new Merger(db);
const merged = await merger.mergeContent(
const merged = merger.mergeContent(
{
type: "tiptap",
data: "Remote",
@@ -132,6 +132,7 @@ describe.concurrent("merge content", (test) => {
{
type: "tiptap",
data: "Local",
synced: true,
dateEdited: Date.now() - 1000
}
);
@@ -140,13 +141,13 @@ describe.concurrent("merge content", (test) => {
expect(merged.data).toBe("Remote");
}));
test("trigger conflict if local item dateEdited is newer", () =>
test("trigger conflict if local item is unsynced", () =>
databaseTest().then(async (db) => {
await loginFakeUser(db);
const merger = new Merger(db);
const noteId = await db.notes.add(TEST_NOTE);
const merged = await merger.mergeContent(
const merged = merger.mergeContent(
{
type: "tiptap",
data: "Remote",
@@ -157,7 +158,7 @@ describe.concurrent("merge content", (test) => {
type: "tiptap",
data: "Local",
noteId,
synced: false,
dateEdited: Date.now()
}
);
@@ -166,7 +167,6 @@ describe.concurrent("merge content", (test) => {
expect(merged.data).toBe("Local");
expect(merged.conflicted).toBeDefined();
expect(merged.conflicted.data).toBe("Remote");
expect(await db.notes.conflicted.has(noteId)).toBe(true);
}));
test("merge conflicts if local item is already conflicted", () =>

View File

@@ -57,37 +57,34 @@ class Collector {
const collection = this.db[collectionKey].collection;
let pushTimestamp = Date.now();
for await (const chunk of collection.unsynced(chunkSize, isForceSync)) {
const items = await this.prepareChunk(chunk, key);
const { ids, items: syncableItems } = filterSyncableItems(chunk);
if (!ids.length) continue;
const ciphers = await this.db
.storage()
.encryptMulti(key, syncableItems);
const items = toPushItem(ids, ciphers);
if (!items) continue;
yield { items, type: itemType };
await collection.update(
chunk.map((i) => i.id),
{ synced: true },
{
sendEvent: false,
// EDGE CASE:
// Sometimes an item can get updated while it's being pushed.
// The result is that its `synced` property becomes true even
// though it's modification wasn't yet synced.
// In order to prevent that, we only set the `synced` property
// to true for items that haven't been modified since we last ran
// the push. Everything else will be collected again in the next
// push.
condition: (eb) => eb("dateModified", "<=", pushTimestamp)
}
);
await this.db
.sql()
.updateTable(collection.type)
.where("id", "in", ids)
// EDGE CASE:
// Sometimes an item can get updated while it's being pushed.
// The result is that its `synced` property becomes true even
// though it's modification wasn't yet synced.
// In order to prevent that, we only set the `synced` property
// to true for items that haven't been modified since we last ran
// the push. Everything else will be collected again in the next
// push.
.where("dateModified", "<=", pushTimestamp)
.set({ synced: true })
.execute();
pushTimestamp = Date.now();
}
}
}
async prepareChunk(chunk: MaybeDeletedItem<Item>[], key: SerializedKey) {
const { ids, items } = filterSyncableItems(chunk);
if (!ids.length) return;
const ciphers = await this.db.storage().encryptMulti(key, items);
return toPushItem(ids, ciphers);
}
}
export default Collector;
@@ -113,9 +110,6 @@ function filterSyncableItems(items: MaybeDeletedItem<Item>[]): {
const ids = [];
const syncableItems = [];
for (const item of items) {
// do not sync conflicted note or content
if ("conflicted" in item && item.conflicted) continue;
delete item.synced;
ids.push(item.id);

View File

@@ -242,6 +242,20 @@ class Sync {
this.connection?.off("SendItems");
this.connection?.off("SendVaultKey");
await this.db
.sql()
.updateTable("notes")
.where("id", "in", (eb) =>
eb
.selectFrom("content")
.select("noteId as id")
.where("conflicted", "is not", null)
.where("conflicted", "!=", false)
.$castTo<string | null>()
)
.set({ conflicted: true })
.execute();
}
async send(deviceId: string, isForceSync?: boolean) {
@@ -334,10 +348,8 @@ class Sync {
const localItems = await collection.records(chunk.items.map((i) => i.id));
let items: (MaybeDeletedItem<Item> | undefined)[] = [];
if (itemType === "content") {
items = await Promise.all(
deserialized.map((item) =>
this.merger.mergeContent(item, localItems[item.id])
)
items = deserialized.map((item) =>
this.merger.mergeContent(item, localItems[item.id])
);
} else {
items =

View File

@@ -46,7 +46,7 @@ class Merger {
}
}
async mergeContent(
mergeContent(
remoteItem: MaybeDeletedItem<Item>,
localItem: MaybeDeletedItem<Item> | undefined
) {
@@ -75,10 +75,7 @@ class Merger {
else if (!conflicted) return;
// otherwise we trigger the conflicts
await this.db.notes.add({
id: localItem.noteId,
conflicted: true
});
this.logger.info("conflict marked", { id: localItem.noteId });
localItem.conflicted = remoteItem;
return localItem;
}
@@ -124,9 +121,8 @@ export function isContentConflicted(
remoteItem.dateModified &&
localItem.dateResolved === remoteItem.dateModified;
const isEdited =
// the local item is edited if it was changed/edited after the remote
// note and it also wasn't synced yet.
localItem.dateEdited > remoteItem.dateEdited && !localItem.synced;
// the local item is edited if it wasn't synced yet.
!localItem.synced;
if (isEdited && !isResolved) {
// If time difference between local item's edits & remote item's edits
// is less than threshold, we shouldn't trigger a merge conflict; instead

View File

@@ -94,7 +94,7 @@ class TokenManager {
) {
return await getSafeToken(async () => {
const token = await this.getToken(true, forceRenew);
if (!token) return;
if (!token || !token.scope) return;
if (!scopes.some((s) => token.scope.includes(s))) return;
return token.access_token;
}, "Error getting access token:");

View File

@@ -100,9 +100,6 @@ export class Attachments implements ICollection {
async init() {
await this.collection.init();
logger.debug("attachments initialized", {
total: await this.collection.count()
});
}
async add(

View File

@@ -175,10 +175,7 @@ export class Content implements ICollection {
const content = await this.collection.get(id);
if (!content || isDeleted(content)) return;
if (!content.locked && this.preProcess(content)) {
await this.db.content.add({
...content,
sessionId: `${Date.now()}`
});
await this.collection.update([content.id], content, { modify: false });
}
return content;
}
@@ -246,10 +243,7 @@ export class Content implements ICollection {
.executeTakeFirst()) as ContentItem;
if (!content || isDeleted(content)) return;
if (!content.locked && this.preProcess(content)) {
await this.db.content.add({
...content,
sessionId: `${Date.now()}`
});
await this.collection.update([content.id], content, { modify: false });
}
return content;
}

View File

@@ -318,17 +318,9 @@ export class Notes implements ICollection {
const content = note.contentId
? await this.db.content.get(note.contentId)
: undefined;
if (content && (isDeleted(content) || content.locked))
throw new Error("Cannot duplicate a locked or deleted note.");
const duplicateId = await this.db.notes.add({
...clone(note),
id: undefined,
content: content
? {
type: content.type,
data: content.data
}
: undefined,
readonly: false,
favorite: false,
pinned: false,
@@ -338,19 +330,38 @@ export class Notes implements ICollection {
dateCreated: undefined,
dateModified: undefined
});
if (!duplicateId) continue;
for (const relation of await this.db.relations
.to(note, "notebook")
.get()) {
const contentId = await this.db.content.add({
...clone(content),
id: undefined,
noteId: duplicateId,
dateResolved: undefined,
dateEdited: undefined,
dateCreated: undefined,
dateModified: undefined
});
await this.db.notes.add({ id: duplicateId, contentId });
for (const relation of await this.db.relations.to(note).get()) {
await this.db.relations.add(
{ type: "notebook", id: relation.fromId },
{ type: relation.fromType, id: relation.fromId },
{
id: duplicateId,
type: "note"
}
);
}
for (const relation of await this.db.relations.from(note).get()) {
await this.db.relations.add(
{
id: duplicateId,
type: "note"
},
{ type: relation.toType, id: relation.toId }
);
}
}
}

View File

@@ -63,6 +63,9 @@ export class Relations implements ICollection {
});
}
from(
reference: ItemReference | ItemReferences
): RelationsArray<keyof RelatableTable>;
from(
reference: ItemReference | ItemReferences,
types: (keyof RelatableTable)[]
@@ -73,16 +76,19 @@ export class Relations implements ICollection {
): RelationsArray<TType>;
from<TType extends keyof RelatableTable = keyof RelatableTable>(
reference: ItemReference | ItemReferences,
type: TType | keyof RelatableTable[]
type?: TType | keyof RelatableTable[]
) {
return new RelationsArray(
this.db,
reference,
Array.isArray(type) ? type : [type],
type ? (Array.isArray(type) ? type : ([type] as TType[])) : undefined,
"from"
);
}
to(
reference: ItemReference | ItemReferences
): RelationsArray<keyof RelatableTable>;
to(
reference: ItemReference | ItemReferences,
types: (keyof RelatableTable)[]
@@ -93,12 +99,12 @@ export class Relations implements ICollection {
): RelationsArray<TType>;
to<TType extends keyof RelatableTable = keyof RelatableTable>(
reference: ItemReference | ItemReferences,
type: TType | keyof RelatableTable[]
type?: TType | keyof RelatableTable[]
) {
return new RelationsArray(
this.db,
reference,
Array.isArray(type) ? type : [type],
type ? (Array.isArray(type) ? type : ([type] as TType[])) : undefined,
"to"
);
}
@@ -224,21 +230,26 @@ const TABLE_MAP = {
type RelatableTable = typeof TABLE_MAP;
class RelationsArray<TType extends keyof RelatableTable> {
private table: ValueOf<RelatableTable> = TABLE_MAP[this.types[0]];
constructor(
private readonly db: Database,
private readonly reference: ItemReference | ItemReferences,
private readonly types: TType[],
private readonly types: TType[] | undefined,
private readonly direction: "from" | "to"
) {}
get selector() {
if (!this.types)
throw new Error("Cannot use selector when no tables are specified.");
if (this.types.length > 1)
throw new Error(
"Cannot use selector when more than 1 tables are specified."
);
const table: ValueOf<RelatableTable> = TABLE_MAP[this.types[0]];
return new FilteredSelector<ItemMap[TType]>(
this.table,
table,
this.db
.sql()
.selectFrom<keyof DatabaseSchema>(this.table)
.selectFrom<keyof DatabaseSchema>(table)
.where("id", "in", (b) =>
b
.selectFrom("relations")
@@ -356,10 +367,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
) => {
if (this.direction === "to") {
return builder
.where(
"fromType",
this.types.length > 1 ? "in" : "==",
this.types.length > 1 ? this.types : this.types[0]
.$if(!!this.types, (eb) =>
eb.where(
"fromType",
this.types!.length > 1 ? "in" : "==",
this.types!.length > 1 ? this.types : this.types![0]
)
)
.where("toType", "==", this.reference.type)
.where(
@@ -370,12 +383,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
: this.reference.id
)
.$if(
this.types.includes("note" as TType) &&
!!this.types?.includes("note" as TType) &&
this.db.trash.cache.notes.length > 0,
(b) => b.where("fromId", "not in", this.db.trash.cache.notes)
)
.$if(
this.types.includes("notebook" as TType) &&
!!this.types?.includes("notebook" as TType) &&
this.db.trash.cache.notebooks.length > 0,
(b) => b.where("fromId", "not in", this.db.trash.cache.notebooks)
)
@@ -383,10 +396,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
.$narrowType<{ id: string }>();
} else {
return builder
.where(
"toType",
this.types.length > 1 ? "in" : "==",
this.types.length > 1 ? this.types : this.types[0]
.$if(!!this.types, (eb) =>
eb.where(
"toType",
this.types!.length > 1 ? "in" : "==",
this.types!.length > 1 ? this.types : this.types![0]
)
)
.where("fromType", "==", this.reference.type)
.where(
@@ -397,12 +412,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
: this.reference.id
)
.$if(
this.types.includes("note" as TType) &&
!!this.types?.includes("note" as TType) &&
this.db.trash.cache.notes.length > 0,
(b) => b.where("toId", "not in", this.db.trash.cache.notes)
)
.$if(
this.types.includes("notebook" as TType) &&
!!this.types?.includes("notebook" as TType) &&
this.db.trash.cache.notebooks.length > 0,
(b) => b.where("toId", "not in", this.db.trash.cache.notebooks)
)

View File

@@ -184,7 +184,7 @@ export class Tiptap {
this.data = new HTMLRewriter({
ontag: (name, attr) => {
const href = attr[ATTRIBUTES.href];
if (name === "a" && href.startsWith("nn://")) {
if (name === "a" && href && href.startsWith("nn://")) {
const link = resolve(href);
if (!link) return;
attr[ATTRIBUTES.href] = link;
@@ -244,7 +244,10 @@ export class Tiptap {
if (types.includes("internalLinks")) {
result.internalLinks.push(
...findAll(
(e) => e.tagName === "a" && e.attribs.href.startsWith("nn://"),
(e) =>
e.tagName === "a" &&
!!e.attribs.href &&
e.attribs.href.startsWith("nn://"),
document.childNodes
)
.map((e) => parseInternalLink(e.attribs.href))

View File

@@ -34,7 +34,8 @@ import {
ColumnType,
ExpressionBuilder,
ReferenceExpression,
Dialect
Dialect,
MigrationProvider
} from "kysely";
import {
Attachment,
@@ -58,8 +59,6 @@ import {
Vault,
isDeleted
} from "../types";
import { NNMigrationProvider } from "./migrations";
import { createTriggers } from "./triggers";
import { logger } from "../logger";
// type FilteredKeys<T, U> = {
@@ -160,6 +159,7 @@ type AsyncOrSyncResult<Async extends boolean, Response> = Async extends true
: Response;
export interface DatabaseCollection<T, IsAsync extends boolean> {
type: keyof DatabaseSchema;
clear(): Promise<void>;
init(): Promise<void>;
upsert(item: T): Promise<void>;
@@ -257,8 +257,8 @@ const DataMappers: Partial<Record<ItemType, (row: any) => void>> = {
}
};
async function setupDatabase(
db: Kysely<RawDatabaseSchema>,
async function setupDatabase<Schema>(
db: Kysely<Schema>,
options: SQLiteOptions
) {
if (options.password)
@@ -294,11 +294,14 @@ async function setupDatabase(
);
}
export async function initializeDatabase(db: Kysely<RawDatabaseSchema>) {
export async function initializeDatabase<Schema>(
db: Kysely<Schema>,
migrationProvider: MigrationProvider
) {
try {
const migrator = new Migrator({
db,
provider: new NNMigrationProvider()
provider: migrationProvider
});
const { error, results } = await migrator.migrateToLatest();
@@ -313,8 +316,6 @@ export async function initializeDatabase(db: Kysely<RawDatabaseSchema>) {
.join(", ")}`
);
await createTriggers(db);
return db;
} catch (e) {
logger.error(e, "Failed to initialized database.");
@@ -335,8 +336,14 @@ export type SQLiteOptions = {
skipInitialization?: boolean;
};
export async function createDatabase(name: string, options: SQLiteOptions) {
const db = new Kysely<RawDatabaseSchema>({
export async function createDatabase<Schema>(
name: string,
options: SQLiteOptions & {
migrationProvider: MigrationProvider;
onInit?: (db: Kysely<Schema>) => Promise<void>;
}
) {
const db = new Kysely<Schema>({
// log: (event) => {
// if (event.queryDurationMillis > 5)
// console.warn(event.query.sql, event.queryDurationMillis);
@@ -344,7 +351,8 @@ export async function createDatabase(name: string, options: SQLiteOptions) {
dialect: options.dialect(name, async () => {
await db.connection().execute(async (db) => {
await setupDatabase(db, options);
await initializeDatabase(db);
await initializeDatabase(db, options.migrationProvider);
if (options.onInit) await options.onInit(db);
});
}),
plugins: [new SqliteBooleanPlugin()]
@@ -352,7 +360,8 @@ export async function createDatabase(name: string, options: SQLiteOptions) {
if (!options.skipInitialization)
await db.connection().execute(async (db) => {
await setupDatabase(db, options);
await initializeDatabase(db);
await initializeDatabase(db, options.migrationProvider);
if (options.onInit) await options.onInit(db);
});
return db;

View File

@@ -289,6 +289,26 @@ export class NNMigrationProvider implements MigrationProvider {
async up(db) {
await rebuildSearchIndex(db);
}
},
"3": {
async up(db) {
await db
.updateTable("notes")
.where("id", "in", (eb) =>
eb
.selectFrom("content")
.select("noteId as id")
.where((eb) =>
eb.or([
eb("conflicted", "is", null),
eb("conflicted", "==", false)
])
)
.$castTo<string | null>()
)
.set({ conflicted: false })
.execute();
}
}
};
}

View File

@@ -38,7 +38,7 @@ export class SQLCachedCollection<
startTransaction: (
executor: (tr: Kysely<DatabaseSchema>) => Promise<void>
) => Promise<void>,
type: TCollectionType,
public type: TCollectionType,
eventManager: EventManager,
sanitizer: Sanitizer
) {

View File

@@ -75,7 +75,7 @@ export class SQLCollection<
_startTransaction: (
executor: (tr: Kysely<DatabaseSchema>) => Promise<void>
) => Promise<void>,
private readonly type: TCollectionType,
public readonly type: TCollectionType,
private readonly eventManager: EventManager,
private readonly sanitizer: Sanitizer
) {}
@@ -227,14 +227,16 @@ export class SQLCollection<
ids: string[],
partial: Partial<SQLiteItem<T>>,
options: {
sendEvent: boolean;
sendEvent?: boolean;
modify?: boolean;
condition?: ExpressionOrFactory<
DatabaseSchema,
keyof DatabaseSchema,
SqlBool
>;
} = { sendEvent: true }
} = {}
) {
const { sendEvent = true, modify = true, condition } = options;
if (!this.sanitizer.sanitize(this.type, partial)) return;
await this.db()
@@ -244,16 +246,16 @@ export class SQLCollection<
await tx
.updateTable<keyof DatabaseSchema>(this.type)
.where("id", "in", chunk)
.$if(!!options.condition, (eb) => eb.where(options.condition!))
.$if(!!condition, (eb) => eb.where(condition!))
.set({
...partial,
dateModified: Date.now(),
dateModified: modify ? Date.now() : undefined,
synced: partial.synced || false
})
.execute();
}
});
if (options.sendEvent) {
if (sendEvent) {
this.eventManager.publish(EVENTS.databaseUpdated, <UpdateEvent>{
type: "update",
collection: this.type,
@@ -284,6 +286,12 @@ export class SQLCollection<
.selectFrom<keyof DatabaseSchema>(this.type)
.select((a) => a.fn.count<number>("id").as("count"))
.where(isFalse("synced"))
.$if(this.type === "content", (eb) =>
eb.where("conflicted", "is", null)
)
.$if(this.type === "notes", (eb) =>
eb.where("conflicted", "is not", true)
)
.$if(this.type === "attachments", (eb) =>
eb.where((eb) =>
eb.or([eb("dateUploaded", ">", 0), eb("deleted", "==", true)])
@@ -304,6 +312,12 @@ export class SQLCollection<
.selectAll()
.$if(lastRowId != null, (qb) => qb.where("id", ">", lastRowId!))
.$if(!forceSync, (eb) => eb.where(isFalse("synced")))
.$if(this.type === "content", (eb) =>
eb.where("conflicted", "is", null)
)
.$if(this.type === "notes", (eb) =>
eb.where("conflicted", "is not", true)
)
.$if(this.type === "attachments", (eb) =>
eb.where((eb) =>
eb.or([eb("dateUploaded", ">", 0), eb("deleted", "==", true)])

View File

@@ -28,7 +28,9 @@ import {
format,
ILogger
} from "@notesnook/logger";
import { IStorage } from "./interfaces";
import { Kysely, Migration, MigrationProvider } from "kysely";
import { SQLiteOptions, createDatabase } from "./database";
import { toChunks } from "./utils/array";
const WEEK = 86400000 * 7;
@@ -38,10 +40,46 @@ const WEEK = 86400000 * 7;
// 3. Keep 7 days of logs
// 4. Implement functions for log retrieval & filtering
type SQLiteItem<T> = {
[P in keyof T]?: T[P] | null;
};
type LogMessageWithDate = LogMessage & { date: string };
export type LogDatabaseSchema = {
logs: SQLiteItem<LogMessageWithDate>;
};
class NNLogsMigrationProvider implements MigrationProvider {
async getMigrations(): Promise<Record<string, Migration>> {
return {
"1": {
async up(db) {
await db.schema
.createTable("logs")
.addColumn("timestamp", "integer", (c) => c.notNull())
.addColumn("message", "text", (c) => c.notNull())
.addColumn("level", "integer", (c) => c.notNull())
.addColumn("date", "text")
.addColumn("scope", "text")
.addColumn("extras", "text")
.addColumn("elapsed", "integer")
.execute();
await db.schema
.createIndex("log_timestamp_index")
.on("logs")
.column("timestamp")
.execute();
}
}
};
}
}
class DatabaseLogReporter {
writer: DatabaseLogWriter;
constructor(storage: IStorage) {
this.writer = new DatabaseLogWriter(storage);
constructor(db: Kysely<LogDatabaseSchema>) {
this.writer = new DatabaseLogWriter(db);
}
write(log: LogMessage) {
@@ -50,10 +88,10 @@ class DatabaseLogReporter {
}
class DatabaseLogWriter {
private queue: Map<string, LogMessage> = new Map();
private queue: LogMessageWithDate[] = [];
private hasCleared = false;
constructor(private readonly storage: IStorage) {
constructor(private readonly db: Kysely<LogDatabaseSchema>) {
setInterval(() => {
setTimeout(() => {
if (!this.hasCleared) {
@@ -66,86 +104,92 @@ class DatabaseLogWriter {
}
push(message: LogMessage) {
const key = new Date(message.timestamp).toLocaleDateString();
this.queue.set(`${key}:${message.timestamp}`, message);
const date = new Date(message.timestamp);
(message as LogMessageWithDate).date = `${date.getFullYear()}-${
date.getMonth() + 1
}-${date.getDate()}`;
this.queue.push(message as LogMessageWithDate);
}
async flush() {
if (this.queue.size === 0) return;
const queueCopy = Array.from(this.queue.entries());
this.queue = new Map();
await this.storage.writeMulti(queueCopy);
if (this.queue.length === 0) return;
const queueCopy = this.queue.slice();
this.queue = [];
for (const chunk of toChunks(queueCopy, 1000)) {
await this.db.insertInto("logs").values(chunk).execute();
}
}
async rotate() {
const logKeys = (await this.storage.getAllKeys()).sort();
const keysToRemove = [];
for (const key of logKeys) {
const keyParts = key.split(":");
if (keyParts.length === 1 || parseInt(keyParts[1]) < Date.now() - WEEK) {
keysToRemove.push(key);
}
}
if (keysToRemove.length) await this.storage.removeMulti(keysToRemove);
const range = Date.now() - WEEK;
await this.db.deleteFrom("logs").where("timestamp", "<", range).execute();
}
}
class DatabaseLogManager {
constructor(private readonly storage: IStorage) {}
constructor(private readonly db: Kysely<LogDatabaseSchema>) {}
async get() {
const logKeys = await this.storage.getAllKeys();
const logEntries = await this.storage.readMulti<LogMessage>(logKeys);
const logs: Record<string, LogMessage[]> = {};
const logs = await this.db
.selectFrom("logs")
.select([
"timestamp",
"message",
"level",
"scope",
"extras",
"elapsed",
"date"
])
.execute();
const groupedLogs: Record<string, LogMessage[]> = {};
for (const [logKey, log] of logEntries) {
const keyParts = logKey.split(":");
if (keyParts.length === 1) continue;
const key = keyParts[0];
if (!logs[key]) logs[key] = [];
logs[key].push(log);
for (const log of logs) {
const key = log.date!;
if (!groupedLogs[key]) groupedLogs[key] = [];
groupedLogs[key].push(log as LogMessage);
}
return Object.keys(logs)
return Object.keys(groupedLogs)
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
.map((key) => ({
key,
logs: logs[key]?.sort((a, b) => a.timestamp - b.timestamp)
logs: groupedLogs[key]?.sort((a, b) => a.timestamp - b.timestamp)
}));
}
async clear() {
const logKeys = await this.storage.getAllKeys();
await this.storage.removeMulti(logKeys);
await this.db.deleteFrom("logs").execute();
}
async delete(key: string) {
const logKeys = await this.storage.getAllKeys();
const keysToRemove = [];
for (const logKey of logKeys) {
const keyParts = logKey.split(":");
if (keyParts.length === 1) continue;
const currKey = keyParts[0];
if (currKey === key) keysToRemove.push(logKey);
}
if (keysToRemove.length) await this.storage.removeMulti(keysToRemove);
await this.db.deleteFrom("logs").where("date", "==", key).execute();
}
close() {
return this.db.destroy();
}
}
function initialize(storage: IStorage, disableConsoleLogs?: boolean) {
if (storage) {
const reporters: ILogReporter[] = [new DatabaseLogReporter(storage)];
if (process.env.NODE_ENV !== "production" && !disableConsoleLogs)
reporters.push(consoleReporter);
logger = new Logger({
reporter: combineReporters(reporters),
lastTime: Date.now()
});
logManager = new DatabaseLogManager(storage);
}
async function initialize(
options: SQLiteOptions,
disableConsoleLogs?: boolean
) {
const db = await createDatabase<LogDatabaseSchema>("notesnook-logs", {
...options,
migrationProvider: new NNLogsMigrationProvider()
});
const reporters: ILogReporter[] = [new DatabaseLogReporter(db)];
if (process.env.NODE_ENV !== "production" && !disableConsoleLogs)
reporters.push(consoleReporter);
const instance = new Logger({
reporter: combineReporters(reporters),
lastTime: Date.now()
});
if (logger instanceof NoopLogger) logger.replaceWith(instance);
logger = instance;
logManager = new DatabaseLogManager(db);
}
let logger: ILogger = new NoopLogger();

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