Compare commits

..

325 Commits

Author SHA1 Message Date
Ammar Ahmed
bb42a2e1a4 ci: skip testflight 2024-08-08 10:19:10 +05:00
Ammar Ahmed
12c5b6d227 mobile: release v3.0.12 2024-07-29 10:06:21 +05:00
Ammar Ahmed
364ae51e36 mobile: fix backup share button not working 2024-07-29 09:53:20 +05:00
Ammar Ahmed
18947e0c93 mobile: fix missing fonts in share sheet 2024-07-29 09:43:37 +05:00
François Martin
3880db4339 global: clarify explanations for force push and force pull (#6255)
Signed-off-by: François Martin <f.martin@fastmail.com>
2024-07-29 09:38:30 +05:00
Ammar Ahmed
ae5b0b6fcc mobile: fix share sheet not loading on ios 2024-07-29 09:25:46 +05:00
Abdullah Atta
2c729f90cc global: update lockfiles 2024-07-27 13:30:25 +05:00
Ammar Ahmed
800514d769 mobile: fix position of password input 2024-07-27 12:49:57 +05:00
Ammar Ahmed
b03b9482dc mobile: app stuck after unlock 2024-07-27 12:49:57 +05:00
Abdullah Atta
9019490dbd web: bump version to v3.0.13 2024-07-27 11:16:42 +05:00
Abdullah Atta
4a3e23c288 web: fix reminder can't be set in january 2024-07-27 11:16:09 +05:00
Ammar Ahmed
1800ebea2d mobile: fix keyboard blocks continue button on applock 2024-07-27 11:12:09 +05:00
Ammar Ahmed
a708c9e9eb mobile: fix home shortcut not working 2024-07-27 10:56:09 +05:00
Abdullah Atta
2aeb875fec web: fix crash on moving temporary tab 2024-07-27 10:08:38 +05:00
Abdullah Atta
a8164e9f6c web: enter in title should focus editor 2024-07-27 09:46:37 +05:00
Abdullah Atta
0ee8e738e5 web: fix attachments ui in properties 2024-07-27 09:46:23 +05:00
Abdullah Atta
8fdef2ac7d web: fix attachments flicker in editor note properties 2024-07-27 09:46:23 +05:00
Abdullah Atta
920b1d91f3 web: bump version to 3.0.12 2024-07-22 15:42:18 +05:00
Abdullah Atta
0ad382aced web: fix incorrect attachment upload progress 2024-07-22 15:30:38 +05:00
Abdullah Atta
c8aa8e37cf web: add support for clearing attachments cache 2024-07-22 15:30:21 +05:00
Abdullah Atta
9a37f7c506 web: fix image picker dialog shown for non-image attachments 2024-07-22 13:08:49 +05:00
Abdullah Atta
717750d5d4 web: subscribe to user related events before fetching user 2024-07-22 13:08:36 +05:00
Abdullah Atta
1274ecc3b5 core: refresh token on user subscription change 2024-07-22 13:08:12 +05:00
Abdullah Atta
4c2ad4567c setup: use listr2 2024-07-20 10:34:40 +05:00
Abdullah Atta
ebc01af34a global: publish packages 2024-07-20 10:34:28 +05:00
Abdullah Atta
e66d8eee13 ui: add support for ss 2024-07-20 01:16:46 +05:00
Abdullah Atta
55e6a4b65a editor: do not throw during ssr 2024-07-20 00:42:54 +05:00
Abdullah Atta
0aa1db584d editor: do not keep last line in view for readonly editor 2024-07-19 23:50:20 +05:00
Abdullah Atta
a0e6266d28 ui: fix Cannot access 'mouseMoveListenerAttached' before initialization 2024-07-19 23:26:35 +05:00
Abdullah Atta
bd0aaed863 ui: add support for ssr 2024-07-19 23:22:47 +05:00
Abdullah Atta
9410c42f29 web: fix search icon overlapping route header text 2024-07-19 12:42:18 +05:00
Abdullah Atta
80186c1812 web: fix type errors 2024-07-19 11:11:51 +05:00
Abdullah Atta
37dfda5c54 desktop: set updaterCacheDirName in app-update.yml 2024-07-19 10:52:59 +05:00
Abdullah Atta
ec83517dc5 desktop: fix update check gives no feedback in ui 2024-07-19 10:51:10 +05:00
Abdullah Atta
2eb4911592 web: bump version to 3.0.11 2024-07-18 17:41:26 +05:00
Abdullah Atta
cd0a6a8fcb editor: fix editing link adds a new link right beside it 2024-07-18 17:38:18 +05:00
Abdullah Atta
b15f192244 desktop: update esbuild 2024-07-18 16:47:08 +05:00
Abdullah Atta
406cb663c9 editor: pasting list item with single spaced paragraphs shouldn't break formatting 2024-07-18 12:02:39 +05:00
Abdullah Atta
301a30a50a editor: copying a link from a list item shouldn't add spaces at the end 2024-07-18 12:02:39 +05:00
Abdullah Atta
81e3432143 editor: fix copying multiple lists only copied the first one 2024-07-18 12:02:39 +05:00
Abdullah Atta
c43c2936ac web: use resizeobserver to wait for scroll container to resize 2024-07-18 11:56:48 +05:00
Abdullah Atta
fdea1807d2 web: fix editor scroll bar hiding sometimes on startup 2024-07-18 11:56:48 +05:00
Abdullah Atta
5439578fcf server(theme): add health endpoint 2024-07-17 15:25:52 +05:00
Abdullah Atta
e05e52f870 web: fix crash on sync 2024-07-16 17:18:25 +05:00
Abdullah Atta
1c6243d12e web: fix error on resetting database on crash 2024-07-16 17:18:25 +05:00
Abdullah Atta
a51dfb5b6e web: remove all traces of immer 2024-07-16 17:18:25 +05:00
Abdullah Atta
c98bddd35b web: use sqlite for storing editor session data 2024-07-16 17:18:25 +05:00
Abdullah Atta
1eb80ccc88 web: get rid of dialog controller
dialog controller was redundant and created unnecessary overhead.
It also coupled all the dialogs together so you couldn't import a dialog
independently. This new change isolates each dialog to its own module
while keeping the behaviour mostly the same.
2024-07-16 17:18:25 +05:00
Abdullah Atta
5a346aa7de web: fix tags & colors not updating/loading on opening a note 2024-07-16 17:18:25 +05:00
Abdullah Atta
910095a769 web: replace immer with mutative for perf reasons 2024-07-16 17:18:25 +05:00
Abdullah Atta
119059befa core: update tag on rename instead of upserting 2024-07-16 17:18:25 +05:00
Abdullah Atta
a8f5dea315 core: fix type definition 2024-07-16 17:18:25 +05:00
Abdullah Atta
ca0a4cb02c core: add localStorage like ConfigStorage based on SQLite 2024-07-16 17:18:25 +05:00
Abdullah Atta
a04cd6b8dc core: send event when directly unlinking a relation 2024-07-16 17:18:25 +05:00
Abdullah Atta
3e4ab0f6df setup: update typescript to 5.5 2024-07-16 17:18:25 +05:00
Abdullah Atta
a7fe1aca44 global: update lockfiles 2024-07-16 17:18:25 +05:00
Abdullah Atta
ff0188f602 web: fix app lock verification always succeeds 2024-07-12 14:30:37 +05:00
Ammar Ahmed
c846f8abc8 mobile: release v3.0.11 2024-07-11 12:08:15 +05:00
Abdullah Atta
8d8acfb99d web: fix crash on browsers that doesn't have asyncIterator 2024-07-11 11:55:17 +05:00
Abdullah Atta
3a5b5772ad web: bump version to 3.0.10 2024-07-10 10:44:44 +05:00
Abdullah Atta
8046932e4e web: fix tags & other items lists glitching 2024-07-10 10:43:06 +05:00
Abdullah Atta
c45aa84739 editor: fix pro popup shown on every paste 2024-07-10 09:04:40 +05:00
Abdullah Atta
d98be849e3 web: refresh editor on changing editor options 2024-07-10 09:04:40 +05:00
Abdullah Atta
727ac6d907 web: allow multi tab access when using idb vfs 2024-07-10 09:04:23 +05:00
Abdullah Atta
8302f8ba4c web: update wa-sqlite 2024-07-10 09:04:23 +05:00
Abdullah Atta
4db8b0db01 desktop: update electron-builder 2024-07-08 14:22:24 +05:00
Ammar Ahmed
3f1614ab9a mobile: fix stacktrace formatting 2024-07-08 13:58:04 +05:00
Ammar Ahmed
909d8c35f8 mobile: fix issue reporting 2024-07-08 13:52:57 +05:00
Ammar Ahmed
a73780f0b0 mobile: bump version to v3.0.10 2024-07-08 13:32:45 +05:00
Ammar Ahmed
286fdbeec0 mobile: show toast error if upload file url not resolved 2024-07-08 13:32:30 +05:00
Abdullah Atta
59099552f8 web: bump version to 3.0.9 2024-07-08 12:47:45 +05:00
Ammar Ahmed
0b6013be58 mobile: fix color popup on mobile 2024-07-08 12:07:39 +05:00
Abdullah Atta
2d93a17f34 web: fix all failing tests 2024-07-08 11:22:14 +05:00
Abdullah Atta
5ef7e39e17 web: optimize ui for mobile & tablet form factor 2024-07-08 10:24:50 +05:00
Abdullah Atta
f7de4c9084 editor: fix keyboard shortcut for internal link not working 2024-07-08 10:23:30 +05:00
Abdullah Atta
0672c864f7 web: fix drag/drop with multiple items 2024-07-08 10:23:02 +05:00
Abdullah Atta
cb3706c150 editor: fix editor options not applied 2024-07-08 10:22:25 +05:00
Abdullah Atta
08c6a99336 web: fix note toc not loading on app restart 2024-07-08 10:21:46 +05:00
Abdullah Atta
adf961fa08 editor: fix search options not respected 2024-07-08 10:21:10 +05:00
Abdullah Atta
0eb9ae47e9 web: restrict tab drag/drop to horizontal axis 2024-07-08 10:15:04 +05:00
Abdullah Atta
aa706300a4 web: fix tab not closing on single click 2024-07-04 11:14:20 +05:00
Ammar Ahmed
fd183a0e1e mobile: fix error in editor with incorrect tools data 2024-07-01 11:48:52 +05:00
Ammar Ahmed
b27cd4be0f mobile: set initial lastContentChangeTime to 0 2024-07-01 11:16:52 +05:00
Abdullah Atta
9575561832 web: fix real time editor sync not updating content 2024-07-01 11:16:52 +05:00
Ammar Ahmed
02f96f8d14 mobile: fix cannot read property jsx of undefined 2024-07-01 11:14:43 +05:00
Ammar Ahmed
3cf46e7ad1 mobile: bump deps 2024-07-01 10:12:08 +05:00
Abdullah Atta
93551aa0b7 web: improve focus item in list behavior 2024-07-01 09:51:58 +05:00
Abdullah Atta
d00c35bdaf web: fix tab focus requires 2 clicks 2024-07-01 09:51:44 +05:00
Abdullah Atta
6e6b793568 web: fix web app on mobile browsers 2024-07-01 09:51:29 +05:00
Ammar Ahmed
9e064d88c6 mobile: fix privacy screen 2024-07-01 09:48:14 +05:00
Abdullah Atta
1658ad8d18 core: fix SSE not connecting 2024-06-25 08:31:07 +05:00
Abdullah Atta
c94da3bdf0 core: use single listener for receiving pushed items 2024-06-25 08:30:41 +05:00
Abdullah Atta
1f0fc83a7c editor: remove images on paste if user is not pro 2024-06-07 10:31:02 +05:00
Abdullah Atta
a6af92c330 web: handle errors when resolving upload urls 2024-06-07 10:30:36 +05:00
Abdullah Atta
ef93ecc481 web: handle errors during bootstrapping 2024-06-07 10:30:16 +05:00
Abdullah Atta
039709822c desktop: add instructions for creating packages 2024-06-05 10:38:54 +05:00
Abdullah Atta
25c861ce77 global: publish packages to npm 2024-06-05 10:28:28 +05:00
Abdullah Atta
670f1570b5 setup: add script for publishing packages 2024-06-05 10:28:13 +05:00
Abdullah Atta
c93d82d2d6 web: bump version to 3.0.8 2024-06-03 17:34:53 +05:00
Abdullah Atta
bced20031b web: fix Please unlock the key store to get values 2024-06-03 17:31:44 +05:00
Abdullah Atta
f8bd089b51 web: bump version to 3.0.7 2024-06-03 13:32:01 +05:00
Abdullah Atta
2a4d04a26f mobile: use clearer wording on logout confirmation 2024-06-03 13:27:51 +05:00
Abdullah Atta
a0c32191f9 web: use clearer wording on logout confirmation 2024-06-03 13:27:51 +05:00
Abdullah Atta
662d59f60a web: use clearer wording on database corruption errors 2024-06-03 13:27:51 +05:00
Abdullah Atta
13befe1947 web: handle crashes during export 2024-06-03 13:27:21 +05:00
Abdullah Atta
21f8b6d557 web: allow resetting side bar order & hidden items 2024-06-03 13:08:20 +05:00
Abdullah Atta
aef53cace9 web: disable non pro items in context menu 2024-06-03 13:00:36 +05:00
Abdullah Atta
382b5b0240 web: improve web app loading performance by 10x (#5845)
* web: improve web app loading performance by 10x

* web: update test snapshots
2024-06-03 11:07:44 +05:00
Ammar Ahmed
a45b66d449 mobile: release v3.0.9 2024-06-01 14:14:01 +05:00
Ammar Ahmed
dd67b1a803 mobile: fix login 2fa sheet gets blocked by keyboard 2024-06-01 13:45:28 +05:00
Ammar Ahmed
00568ae839 mobile: add back undo/redo buttons 2024-06-01 13:21:27 +05:00
Abdullah Atta
2b2ea5717c web: bring back undo/redo buttons 2024-06-01 13:21:27 +05:00
Abdullah Atta
5672de8565 desktop: allow using native titlebar on desktop app (#5826) 2024-06-01 13:21:05 +05:00
Ammar Ahmed
1c70ad8cec web: fix tags & colors not included in imported notes (#5801)
* web: include tags and colors in imports

* web: refactors

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-06-01 11:40:16 +05:00
Abdullah Atta
0c344a2146 core: fix sqlite hangs on restoring notebook 2024-06-01 11:28:40 +05:00
Yoonjae Choi
38f69b514c web: fix readonly locked notes opening as editable (#5731)
Signed-off-by: Yoonjae Choi <dbswo9795@email.com>
2024-06-01 11:27:59 +05:00
Abdullah Atta
b502f6a08f web: fix CVE-2024-4367 2024-06-01 11:26:43 +05:00
Abdullah Atta
d68c3269d3 editor: fix extra padding around image 2024-06-01 11:26:28 +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
Abdullah Atta
68d57e2b27 web: bump version to 3.0.4 2024-05-11 10:48:48 +05:00
Abdullah Atta
400aa574cd core: fix 2fa codes not being sent 2024-05-11 10:29:02 +05:00
Abdullah Atta
5028bb1275 web: update snapshots 2024-05-10 15:30:35 +05:00
Abdullah Atta
27ae5bb5a2 desktop: use npx prebuildify instead of yarn 2024-05-10 15:24:39 +05:00
Abdullah Atta
83316fe299 web: update snapshots for linux 2024-05-10 15:20:57 +05:00
Ammar Ahmed
8869224de2 mobile: fix image preview not rendered on iOS 2024-05-10 15:04:28 +05:00
Ammar Ahmed
c4152f0fc0 mobile: set editor reset time to 10 minutes 2024-05-10 14:53:10 +05:00
Ammar Ahmed
d803005637 mobile: release v3.0.3 2024-05-10 14:47:27 +05:00
Ammar Ahmed
53d896be2b mobile: fix opening exported files after export 2024-05-10 14:44:12 +05:00
Abdullah Atta
8c37c6409a ci: use actions/upload-artifact@v4 2024-05-10 12:36:56 +05:00
Abdullah Atta
9195244890 web: fix tests 2024-05-10 12:31:55 +05:00
Ammar Ahmed
152794ccd0 mobile: fix image filename in picker 2024-05-10 12:24:40 +05:00
Ammar Ahmed
2e8b234b63 mobile: fix editor content update on realtime sync 2024-05-10 12:24:40 +05:00
Abdullah Atta
055c647698 web: add Audios tab in attachments dialog 2024-05-10 12:08:38 +05:00
Abdullah Atta
2b7bcfe7c6 web: bump version to 3.0.3 2024-05-10 12:00:06 +05:00
Abdullah Atta
3c8b8a7bc9 web: fix real time sync test 2024-05-10 11:58:56 +05:00
Abdullah Atta
fde45fb6de web: saving timeout notice shown every time 2024-05-10 11:58:44 +05:00
Abdullah Atta
4a616a3e67 web: fix tab not updating when note is locked/unlocked 2024-05-10 11:27:26 +05:00
Abdullah Atta
bd0a338dd6 core: fix dateEdited not updating on saving locked content 2024-05-10 10:46:11 +05:00
Abdullah Atta
5ab721e3e2 core: fix async iterator on sql collection only returning dateCreated & id 2024-05-10 10:22:03 +05:00
Ammar Ahmed
cb70bb9429 mobile: fix errors 2024-05-10 09:22:23 +05:00
Abdullah Atta
82cadd473c web: show error if saving takes more than 30 seconds 2024-05-10 09:22:23 +05:00
Ammar Ahmed
0e8ea3ddeb mobile: fix editor loading 2024-05-10 09:22:23 +05:00
Ammar Ahmed
511acc5fca mobile: handle editor not responding 2024-05-10 09:22:23 +05:00
Ammar Ahmed
001cf8ee92 mobile: add editor lock safety 2024-05-10 09:22:23 +05:00
Muhammad Ali
a965cd694b web: fix color not updating on note (#5573) 2024-05-10 09:16:59 +05:00
Ammar Ahmed
d3c1c1959b mobile: fix wording of push/pull sync 2024-05-09 17:39:35 +05:00
Abdullah Atta
9a866a552a web: replace force sync with force push/pull 2024-05-09 17:39:35 +05:00
Ammar Ahmed
837c296821 mobile: push/pull force sync 2024-05-09 17:39:35 +05:00
Abdullah Atta
9aed6faed8 common: fix locked notes not getting exported 2024-05-09 17:39:16 +05:00
Abdullah Atta
734f491e8d editor: fix crash due to undefined is not an object (evaluating 'a.nodeSize') (#5553) 2024-05-09 17:38:18 +05:00
Ammar Ahmed
0ea13024e9 mobile: fix editor crash due to position (#5519)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-09 15:55:33 +05:00
Abdullah Atta
cb27039245 web: close shared service on window close 2024-05-09 10:30:46 +05:00
Abdullah Atta
714ac2deac core: do not use auth access tokens as normal access tokens 2024-05-09 10:30:25 +05:00
Abdullah Atta
d605bc29bf desktop: fix crash on toggling custom dns (#5538) 2024-05-09 10:15:14 +05:00
Abdullah Atta
080101bc45 core: fix database disk image malformed error (#5525) 2024-05-09 10:08:44 +05:00
Abdullah Atta
4819e17da3 core: rerun sync if there are unsynced changes after sync complete 2024-05-08 16:48:59 +05:00
Ammar Ahmed
69a5d5a1b6 global: update .gitignore 2024-05-08 10:43:35 +05:00
Ammar Ahmed
68a6d6c313 mobile: fix crash when taking camera picture 2024-05-08 09:48:28 +05:00
Ammar Ahmed
c51ef4d5f6 mobile: fix camera permission 2024-05-08 09:48:28 +05:00
Ammar Ahmed
d9c5c1f893 mobile: remove margin in editor 2024-05-08 09:47:56 +05:00
Ammar Ahmed
5a75ca73a9 mobile: fix editor scrolling on update 2024-05-08 09:47:56 +05:00
Abdullah Atta
626d393376 desktop: use yarn instead of npx for build scripts 2024-05-08 09:14:55 +05:00
Abdullah Atta
68d06ff3f8 core: fix legacy backup restoration 2024-05-07 15:57:22 +05:00
Abdullah Atta
79f20d100c core: fix File already exists error on duplicate downloads 2024-05-07 12:57:51 +05:00
Ammar Ahmed
9d5bec3bfb ci: upload sourcemaps 2024-05-07 11:49:58 +05:00
Abdullah Atta
ac96354b4c core: handle case when updating item while a push is ongoing 2024-05-06 22:04:04 +05:00
Abdullah Atta
63f44d6fbc core: set synced to false on removing content by note id 2024-05-06 15:45:45 +05:00
Abdullah Atta
f9bfa88c83 core: set synced to false on unlinking relations 2024-05-06 15:45:45 +05:00
Ammar Ahmed
ef192f3d22 mobile: release v3.0.2 2024-05-05 01:14:20 +05:00
Abdullah Atta
c9a8134265 core: fix notes & notebooks sorting 2024-05-05 00:00:49 +05:00
Ammar Ahmed
cd7a5d2ec0 mobile: fix error not rendered correctly 2024-05-04 23:52:47 +05:00
Abdullah Atta
b92bc03fa9 web: handle errors when saving attachment 2024-05-04 23:51:42 +05:00
Abdullah Atta
5f5e472800 web: bump version to 3.0.2 2024-05-04 23:35:49 +05:00
Ammar Ahmed
0951906cac mobile: do not check for .nnbackup file in zip 2024-05-04 23:29:59 +05:00
Abdullah Atta
6f0fe03760 core: do not throw if user not found during legacy backup 2024-05-04 23:28:37 +05:00
Muhammad Ali
159ce97e42 core: fix crash due to unsanitized sort options (#5324)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-05-04 23:26:58 +05:00
Ammar Ahmed
8a9848634c mobile: fix note not opening from notifications 2024-05-04 23:26:07 +05:00
Ammar Ahmed
282705a683 mobile: add logging 2024-05-04 23:11:30 +05:00
Ammar Ahmed
c91b706ed0 mobile: correctly show mime type 2024-05-04 23:11:30 +05:00
Ammar Ahmed
16afc07b2f mobile: fix all attachments show in audios 2024-05-04 23:11:30 +05:00
Ammar Ahmed
04f8be5a62 core: add missing audios attachments filter 2024-05-04 23:11:30 +05:00
Ammar Ahmed
0e8a5ca1fa editor: fix n.startsWith is not a function 2024-05-04 23:03:54 +05:00
Ammar Ahmed
1491bf771e Fix image and file uploads on mobile (#5304)
* mobile: fix image file formats support

* mobile: fix image and file uploads

---------

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-04 23:02:07 +05:00
Abdullah Atta
0f5ce465c3 web: add logging related to attachments 2024-05-04 22:51:01 +05:00
Abdullah Atta
647ff7ae0e web: prevent overwriting uploaded attachments 2024-05-04 22:51:01 +05:00
Abdullah Atta
45a552adef web: handle errors during uploaded file size check 2024-05-04 22:51:01 +05:00
Abdullah Atta
8e207d787d web: delete file before reuploading 2024-05-04 22:51:01 +05:00
Abdullah Atta
00754593ac web: fix attachment not getting marked as failed on error 2024-05-04 22:51:01 +05:00
Abdullah Atta
b22770f1ae core: do not reset attachment dateUploaded when marking as failed 2024-05-04 22:51:01 +05:00
Abdullah Atta
523da4e91f core: only delete attachment file (not the metadata) on sync conflict 2024-05-04 22:51:01 +05:00
Abdullah Atta
de6c8154a3 web: fix password recovery not working 2024-05-04 22:50:44 +05:00
Abdullah Atta
9ef32e8081 web: do not hide attachment manager for logged out user 2024-05-01 20:14:20 +05:00
Abdullah Atta
5f53593a26 web: do not throw if backup doesn't contain .nnbackup file 2024-05-01 20:12:23 +05:00
Abdullah Atta
1f404944d9 web: re-enable service worker 2024-05-01 16:38:09 +05:00
Ammar Ahmed
23dca36475 mobile: release v3.0.1 2024-05-01 16:34:01 +05:00
Ammar Ahmed
3dd8e20e9a mobile: add logging when loading attachments 2024-05-01 16:29:37 +05:00
Abdullah Atta
8f412f9487 editor: fix padding of react node views on mobile 2024-05-01 16:29:12 +05:00
Abdullah Atta
bf790219db web: bump version to 3.0.1 2024-05-01 16:16:08 +05:00
Ammar Ahmed
c6af310b2d mobile: add notebook search 2024-05-01 16:10:26 +05:00
Abdullah Atta
8d89b5dca9 web: bring back notebook search in move note dialog 2024-05-01 16:10:26 +05:00
Abdullah Atta
fa35244607 editor: update test snapshosts 2024-05-01 16:09:06 +05:00
Abdullah Atta
d96815fa36 ci: remove beta publish workflows 2024-05-01 15:23:43 +05:00
Abdullah Atta
4a9f5b1d8b core: remove .only from trash tests 2024-05-01 15:19:57 +05:00
Ammar Ahmed
43fac79011 mobile: fix moving notebook on ios 2024-05-01 15:18:55 +05:00
Ammar Ahmed
f2c62c8d8b mobile: fix moving notebook on ios 2024-05-01 15:18:55 +05:00
Ammar Ahmed
70c9dd0083 mobile: reduce font size in notebook header 2024-05-01 15:15:37 +05:00
Ammar Ahmed
ae6d8c5f2b mobile: hide breadcrumbs in top level notebooks 2024-05-01 15:15:37 +05:00
Abdullah Atta
38e2250fd1 web: log attachment on failure 2024-05-01 14:55:49 +05:00
Ammar Ahmed
17e2c5a8a2 desktop: fix app icon size for macos 2024-05-01 14:53:32 +05:00
Ammar Ahmed
2e226f0f72 mobile: fix cannot read property 'sortDirection' of undefined 2024-05-01 13:34:48 +05:00
Ammar Ahmed
167c54578b mobile: fix scrolling broken in reminder -> repeat -> month 2024-05-01 13:34:37 +05:00
Abdullah Atta
fb656af5ca web: sort tags by title in editor header 2024-05-01 13:34:20 +05:00
Ammar Ahmed
ddfab272d2 common: sort tags by title in resolved items 2024-05-01 13:34:20 +05:00
Ammar Ahmed
28958311c1 mobile: fix share extention crash on sharing from some apps 2024-05-01 13:34:06 +05:00
Ammar Ahmed
f1b8275b4b mobile: add support for setting note title form quick note widget 2024-05-01 13:34:06 +05:00
Ammar Ahmed
c8c75d40dd mobile: fix theme search returns empty results 2024-05-01 13:33:50 +05:00
Ammar Ahmed
bace90808d mobile: fix footer size for notebook screen 2024-05-01 13:33:31 +05:00
Ammar Ahmed
fe7c7e8db4 mobile: fix notebook-sheet position 2024-05-01 13:32:02 +05:00
Ammar Ahmed
519a569366 mobile: fix notebook sheet flicker 2024-05-01 13:32:02 +05:00
Ammar Ahmed
89d602a40f mobile: do not lock app when requesting permission 2024-05-01 13:31:38 +05:00
Abdullah Atta
a8db65b5af editor: fix performance issues due to using portals for react node views 2024-05-01 13:29:55 +05:00
Ammar Ahmed
716239abfd mobile: fix selection header 2024-05-01 13:21:31 +05:00
Ammar Ahmed
62dd5a5416 mobile: fix vault unlock with biometrics 2024-05-01 13:19:00 +05:00
Ammar Ahmed
96558c5df5 mobile: fix search bar position on ios 2024-05-01 13:18:21 +05:00
Ammar Ahmed
45997dd2e4 editor: fix scroll into view should bring heading to top 2024-05-01 13:16:54 +05:00
Ammar Ahmed
bde190f01c mobile: render settings icon if no profile pic is set 2024-05-01 13:15:24 +05:00
Ammar Ahmed
042c97688f mobile: add quick theme switch button 2024-05-01 13:15:24 +05:00
Ammar Ahmed
95b5114070 mobile: fix confusing settings/sync button 2024-05-01 13:15:24 +05:00
Ammar Ahmed
20742e2a1c mobile: fix back navigation from editor (#4867)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-01 13:13:44 +05:00
Ammar Ahmed
1e3149284e mobile: report error when migration fails due to backup error 2024-05-01 13:01:41 +05:00
Ammar Ahmed
d77c09f1fd mobile: fix opening internal link in readonly mode 2024-05-01 13:00:58 +05:00
Ammar Ahmed
aa602ea8e9 mobile: fix position of context menu 2024-05-01 12:59:50 +05:00
Ammar Ahmed
8ab6b6dc31 mobile: skip checking encrypted backup if user is logged out 2024-05-01 12:57:42 +05:00
Ammar Ahmed
404d136a73 mobile: fix sidebar order not loading on app launch 2024-05-01 12:50:13 +05:00
Abdullah Atta
cf9aeecd7e editor: fix placeholder showing always in first line of editor 2024-05-01 12:37:48 +05:00
Abdullah Atta
1adda96ce2 editor: fix many papercut ux issues in search replace 2024-05-01 12:36:00 +05:00
Abdullah Atta
6dd6d818c9 editor: add support for multi line search/replace 2024-05-01 12:36:00 +05:00
Abdullah Atta
c7e83961d4 web: show all notes if query is empty in note linking dialog 2024-05-01 12:35:38 +05:00
Abdullah Atta
03428e5848 core: allow full FTS5 search syntax 2024-05-01 12:35:38 +05:00
Abdullah Atta
d0fceed56d web: improve app performance by reducing react rerenders 2024-05-01 12:34:43 +05:00
Abdullah Atta
b8c68b5163 web: fix middle pane size increasing on exiting focus mode 2024-04-30 21:52:25 +05:00
Abdullah Atta
2496004896 web: fix position of image preview toolbar 2024-04-30 21:51:57 +05:00
Abdullah Atta
16ec35de25 web: do not change properties top on web app 2024-04-30 21:51:57 +05:00
Abdullah Atta
d248b8e2b8 web: properties & toc side bars must be below the titlebar 2024-04-30 21:51:57 +05:00
Abdullah Atta
641e7484e3 desktop: add custom dns toggle in settings 2024-04-30 21:51:35 +05:00
Abdullah Atta
a46409ff44 core: add more fts trigger tests 2024-04-30 21:51:17 +05:00
Abdullah Atta
5dbabc2706 core: fix database disk image is malformed error on updating deleted content 2024-04-30 21:51:17 +05:00
Abdullah Atta
2d4d656dd9 web: canceling backup on password change should abort 2024-04-30 21:50:48 +05:00
Abdullah Atta
ab51b22e4a web: do not ask for password when downloading backup during recovery 2024-04-30 21:50:32 +05:00
Abdullah Atta
781d09daca web: fix navigation menu resize issues 2024-04-30 21:50:21 +05:00
Abdullah Atta
f21533e5e4 core: fix sql collection only getting first batch on iteration 2024-04-30 21:50:00 +05:00
Abdullah Atta
2ea9b04863 web: fix toggles in properties not working 2024-04-30 21:49:37 +05:00
Abdullah Atta
675d517cbc web: reduce zoom factor step to 0.1 2024-04-30 21:49:13 +05:00
Abdullah Atta
c336c57bda web: disable pwa 2024-04-30 21:48:28 +05:00
441 changed files with 14580 additions and 10620 deletions

View File

@@ -147,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github - name: Upload sourcemaps
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: Notesnook.aab name: sourcemaps
path: ${{steps.sign_app.outputs.signedReleaseFile}} path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -133,7 +133,6 @@ jobs:
status: completed status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/ whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github - name: Create release draft on Github
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
@@ -148,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github - name: Upload sourcemaps
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: Notesnook.aab name: sourcemaps
path: ${{steps.sign_app.outputs.signedReleaseFile}} path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -89,16 +89,19 @@ jobs:
- name: CCache Stats After Build - name: CCache Stats After Build
run: ccache -sv run: ccache -sv
- name: "Upload app to TestFlight" # - name: "Upload app to TestFlight"
uses: apple-actions/upload-testflight-build@v1 # uses: apple-actions/upload-testflight-build@v1
with: # with:
app-path: Notesnook.ipa # app-path: Notesnook.ipa
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }} # issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
api-key-id: ${{ secrets.API_KEY_ID }} # api-key-id: ${{ secrets.API_KEY_ID }}
api-private-key: ${{ secrets.API_KEY }} # api-private-key: ${{ secrets.API_KEY }}
- name: Upload Notesnook.ipa to Github - name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: Notesnook.ipa name: Notesnook.zip
path: Notesnook.ipa path: |
Notesnook.ipa
apps/mobile/native/ios/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -1,69 +0,0 @@
name: Publish @notesnook/web Beta
on:
workflow_dispatch:
push:
branches:
- master
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
- "apps/desktop/**"
- "apps/vericrypt/**"
- "servers/**"
- "packages/clipper/**"
- "extensions/web-clipper/**"
pull_request:
types:
- closed
- ready_for_review
- reopened
- opened
- synchronize
branches:
- master
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
- "apps/desktop/**"
- "apps/vericrypt/**"
- "servers/**"
- "packages/clipper/**"
- "extensions/web-clipper/**"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate beta build
run: npm run build:beta:web
- name: Publish to Cloudflare Pages
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
id: pages
with:
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-project-name: notesnook-beta-app
directory: ./apps/web/build
github-token: ${{ secrets.GITHUB_TOKEN }}
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}

View File

@@ -40,7 +40,7 @@ jobs:
run: npm run build:test:web run: npm run build:test:web
- name: Archive build artifact - name: Archive build artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: build name: build
path: apps/web/build/**/* path: apps/web/build/**/*
@@ -56,7 +56,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Download build - name: Download build
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
name: build name: build
path: ./apps/web/build path: ./apps/web/build
@@ -88,7 +88,7 @@ jobs:
working-directory: apps/web working-directory: apps/web
- name: Upload test results - name: Upload test results
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: test-results-${{ matrix.shard }} name: test-results-${{ matrix.shard }}

View File

@@ -1,32 +0,0 @@
name: Publish @notesnook/web v3 Beta
on:
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Build
run: npm run build:web
- name: Publish to Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=notesnook-v3-beta ./apps/web/build/

View File

@@ -40,12 +40,35 @@ Once you are inside the `./notesnook` directory, run the preparation step:
npm install npm install
``` ```
Now you can finally start the desktop app: Now you can finally start the desktop app for development:
```bash ```bash
npm run start:desktop npm run start:desktop
``` ```
To run the app in release mode:
```bash
npm run staging -- --rebuild
```
This will compile and run the app in production mode but it won't generate any packages. To create the final packages, you'll have to run the following commands:
```bash
npm run release -- --rebuild
# For macOS
npx electron-builder --mac dmg --arm64 --x64 -publish never
# For Linux (AppImage)
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
# For Windows
npx electron-builder --win --publish never
```
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
## Developer guide ## Developer guide
### The tech stack ### The tech stack

View File

@@ -1,3 +1,4 @@
owner: streetwriters owner: streetwriters
repo: notesnook repo: notesnook
provider: github provider: github
updaterCacheDirName: Notesnook

Binary file not shown.

View File

@@ -1,12 +1,12 @@
{ {
"name": "@notesnook/desktop", "name": "@notesnook/desktop",
"version": "3.0.0", "version": "3.0.13",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@notesnook/desktop", "name": "@notesnook/desktop",
"version": "3.0.0", "version": "3.0.13",
"hasInstallScript": true, "hasInstallScript": true,
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
@@ -15,7 +15,7 @@
"@trpc/server": "10.38.3", "@trpc/server": "10.38.3",
"better-sqlite3-multiple-ciphers": "^9.5.0", "better-sqlite3-multiple-ciphers": "^9.5.0",
"electron-trpc": "0.5.2", "electron-trpc": "0.5.2",
"electron-updater": "6.1.4", "electron-updater": "^6.2.1",
"icojs": "^0.17.1", "icojs": "^0.17.1",
"sodium-native": "^4.1.1", "sodium-native": "^4.1.1",
"typed-emitter": "^2.1.0", "typed-emitter": "^2.1.0",
@@ -28,7 +28,8 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"electron": "^29.3.1", "electron": "^29.3.1",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"esbuild": "^0.20.0", "esbuild": "^0.23.0",
"kysely": "^0.27.3",
"node-gyp-build": "^4.8.0", "node-gyp-build": "^4.8.0",
"prebuildify": "^6.0.1", "prebuildify": "^6.0.1",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
@@ -40,7 +41,7 @@
}, },
"../../packages/crypto": { "../../packages/crypto": {
"name": "@notesnook/crypto", "name": "@notesnook/crypto",
"version": "1.1.1", "version": "2.0.7",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"@notesnook/sodium": "file:../sodium" "@notesnook/sodium": "file:../sodium"
@@ -70,9 +71,9 @@
} }
}, },
"node_modules/@electron/asar": { "node_modules/@electron/asar": {
"version": "3.2.9", "version": "3.2.10",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.9.tgz", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.10.tgz",
"integrity": "sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA==", "integrity": "sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"commander": "^5.0.0", "commander": "^5.0.0",
@@ -288,9 +289,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz",
"integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==", "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -300,13 +301,13 @@
"aix" "aix"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz",
"integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==", "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -316,13 +317,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz",
"integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==", "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -332,13 +333,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz",
"integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==", "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -348,13 +349,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz",
"integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==", "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -364,13 +365,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz",
"integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==", "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -380,13 +381,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz",
"integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==", "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -396,13 +397,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz",
"integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==", "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -412,13 +413,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz",
"integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==", "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -428,13 +429,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz",
"integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==", "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -444,13 +445,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz",
"integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==", "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -460,13 +461,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz",
"integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==", "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -476,13 +477,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz",
"integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==", "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -492,13 +493,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz",
"integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==", "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -508,13 +509,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz",
"integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==", "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -524,13 +525,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz",
"integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==", "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -540,13 +541,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz",
"integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==", "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -556,13 +557,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz",
"integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==", "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -572,13 +573,29 @@
"netbsd" "netbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz",
"integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz",
"integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==", "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -588,13 +605,13 @@
"openbsd" "openbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz",
"integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==", "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -604,13 +621,13 @@
"sunos" "sunos"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz",
"integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==", "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -620,13 +637,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz",
"integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==", "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -636,13 +653,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz",
"integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==", "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -652,7 +669,7 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
@@ -981,6 +998,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
"devOptional": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/7zip-bin": { "node_modules/7zip-bin": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
@@ -1107,19 +1133,6 @@
"electron-builder-squirrel-windows": "24.13.3" "electron-builder-squirrel-windows": "24.13.3"
} }
}, },
"node_modules/app-builder-lib/node_modules/builder-util-runtime": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1358,22 +1371,9 @@
} }
}, },
"node_modules/builder-util-runtime": { "node_modules/builder-util-runtime": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz",
"integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==",
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/builder-util/node_modules/builder-util-runtime": {
"version": "9.2.4", "version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": { "dependencies": {
"debug": "^4.3.4", "debug": "^4.3.4",
"sax": "^1.2.4" "sax": "^1.2.4"
@@ -1586,31 +1586,32 @@
} }
}, },
"node_modules/config-file-ts/node_modules/glob": { "node_modules/config-file-ts/node_modules/glob": {
"version": "10.3.12", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz",
"integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.1.0",
"jackspeak": "^2.3.6", "jackspeak": "^3.1.2",
"minimatch": "^9.0.1", "minimatch": "^9.0.4",
"minipass": "^7.0.4", "minipass": "^7.1.2",
"path-scurry": "^1.10.2" "package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
}, },
"bin": { "bin": {
"glob": "dist/esm/bin.mjs" "glob": "dist/esm/bin.mjs"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/config-file-ts/node_modules/minimatch": { "node_modules/config-file-ts/node_modules/minimatch": {
"version": "9.0.4", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
@@ -1623,9 +1624,9 @@
} }
}, },
"node_modules/config-file-ts/node_modules/minipass": { "node_modules/config-file-ts/node_modules/minipass": {
"version": "7.0.4", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@@ -1833,19 +1834,6 @@
"dmg-license": "^1.0.11" "dmg-license": "^1.0.11"
} }
}, },
"node_modules/dmg-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",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/dmg-license": { "node_modules/dmg-license": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
@@ -1951,19 +1939,6 @@
"node": ">=14.0.0" "node": ">=14.0.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",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-publish": { "node_modules/electron-publish": {
"version": "24.13.1", "version": "24.13.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz",
@@ -1979,19 +1954,6 @@
"mime": "^2.5.2" "mime": "^2.5.2"
} }
}, },
"node_modules/electron-publish/node_modules/builder-util-runtime": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-trpc": { "node_modules/electron-trpc": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/electron-trpc/-/electron-trpc-0.5.2.tgz", "resolved": "https://registry.npmjs.org/electron-trpc/-/electron-trpc-0.5.2.tgz",
@@ -2006,11 +1968,11 @@
} }
}, },
"node_modules/electron-updater": { "node_modules/electron-updater": {
"version": "6.1.4", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.4.tgz", "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.2.1.tgz",
"integrity": "sha512-yYAJc6RQjjV4WtInZVn+ZcLyXRhbVXoomKEfUUwDqIk5s2wxzLhWaor7lrNgxODyODhipjg4SVPMhJHi5EnsCA==", "integrity": "sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==",
"dependencies": { "dependencies": {
"builder-util-runtime": "9.2.1", "builder-util-runtime": "9.2.4",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lazy-val": "^1.0.5", "lazy-val": "^1.0.5",
@@ -2065,41 +2027,42 @@
"optional": true "optional": true
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.20.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz",
"integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==", "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.0", "@esbuild/aix-ppc64": "0.23.0",
"@esbuild/android-arm": "0.20.0", "@esbuild/android-arm": "0.23.0",
"@esbuild/android-arm64": "0.20.0", "@esbuild/android-arm64": "0.23.0",
"@esbuild/android-x64": "0.20.0", "@esbuild/android-x64": "0.23.0",
"@esbuild/darwin-arm64": "0.20.0", "@esbuild/darwin-arm64": "0.23.0",
"@esbuild/darwin-x64": "0.20.0", "@esbuild/darwin-x64": "0.23.0",
"@esbuild/freebsd-arm64": "0.20.0", "@esbuild/freebsd-arm64": "0.23.0",
"@esbuild/freebsd-x64": "0.20.0", "@esbuild/freebsd-x64": "0.23.0",
"@esbuild/linux-arm": "0.20.0", "@esbuild/linux-arm": "0.23.0",
"@esbuild/linux-arm64": "0.20.0", "@esbuild/linux-arm64": "0.23.0",
"@esbuild/linux-ia32": "0.20.0", "@esbuild/linux-ia32": "0.23.0",
"@esbuild/linux-loong64": "0.20.0", "@esbuild/linux-loong64": "0.23.0",
"@esbuild/linux-mips64el": "0.20.0", "@esbuild/linux-mips64el": "0.23.0",
"@esbuild/linux-ppc64": "0.20.0", "@esbuild/linux-ppc64": "0.23.0",
"@esbuild/linux-riscv64": "0.20.0", "@esbuild/linux-riscv64": "0.23.0",
"@esbuild/linux-s390x": "0.20.0", "@esbuild/linux-s390x": "0.23.0",
"@esbuild/linux-x64": "0.20.0", "@esbuild/linux-x64": "0.23.0",
"@esbuild/netbsd-x64": "0.20.0", "@esbuild/netbsd-x64": "0.23.0",
"@esbuild/openbsd-x64": "0.20.0", "@esbuild/openbsd-arm64": "0.23.0",
"@esbuild/sunos-x64": "0.20.0", "@esbuild/openbsd-x64": "0.23.0",
"@esbuild/win32-arm64": "0.20.0", "@esbuild/sunos-x64": "0.23.0",
"@esbuild/win32-ia32": "0.20.0", "@esbuild/win32-arm64": "0.23.0",
"@esbuild/win32-x64": "0.20.0" "@esbuild/win32-ia32": "0.23.0",
"@esbuild/win32-x64": "0.23.0"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@@ -2224,9 +2187,9 @@
} }
}, },
"node_modules/foreground-child": { "node_modules/foreground-child": {
"version": "3.1.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"cross-spawn": "^7.0.0", "cross-spawn": "^7.0.0",
@@ -2239,6 +2202,18 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -2370,6 +2345,7 @@
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
@@ -2670,6 +2646,7 @@
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"once": "^1.3.0", "once": "^1.3.0",
@@ -2767,15 +2744,15 @@
"dev": true "dev": true
}, },
"node_modules/jackspeak": { "node_modules/jackspeak": {
"version": "2.3.6", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.1.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "integrity": "sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@isaacs/cliui": "^8.0.2" "@isaacs/cliui": "^8.0.2"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@@ -2785,9 +2762,9 @@
} }
}, },
"node_modules/jake": { "node_modules/jake": {
"version": "10.8.7", "version": "10.9.1",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz",
"integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"async": "^3.2.3", "async": "^3.2.3",
@@ -2891,6 +2868,15 @@
"json-buffer": "3.0.1" "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": { "node_modules/lazy-val": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
@@ -3155,6 +3141,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/package-json-from-dist": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
"dev": true
},
"node_modules/path-is-absolute": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -3174,28 +3166,37 @@
} }
}, },
"node_modules/path-scurry": { "node_modules/path-scurry": {
"version": "1.10.2", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^10.2.0", "lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/path-scurry/node_modules/lru-cache": { "node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.2.1", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.0.tgz",
"integrity": "sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA==", "integrity": "sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "14 || >=16.14" "node": ">=18"
}
},
"node_modules/path-scurry/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/peek-readable": { "node_modules/peek-readable": {
@@ -3229,16 +3230,17 @@
} }
}, },
"node_modules/plist": { "node_modules/plist": {
"version": "3.0.6", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
"integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@xmldom/xmldom": "^0.8.8",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"xmlbuilder": "^15.1.1" "xmlbuilder": "^15.1.1"
}, },
"engines": { "engines": {
"node": ">=6" "node": ">=10.4.0"
} }
}, },
"node_modules/pngjs": { "node_modules/pngjs": {
@@ -3586,18 +3588,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/simple-concat": { "node_modules/simple-concat": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
@@ -3987,9 +3977,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.4.5", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -4032,9 +4022,9 @@
} }
}, },
"node_modules/utf8-byte-length": { "node_modules/utf8-byte-length": {
"version": "1.0.4", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
"integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
"dev": true "dev": true
}, },
"node_modules/util-deprecate": { "node_modules/util-deprecate": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop", "name": "@notesnook/desktop",
"productName": "Notesnook", "productName": "Notesnook",
"description": "Your private note taking space", "description": "Your private note taking space",
"version": "3.0.0", "version": "3.0.13",
"appAppleId": "1544027013", "appAppleId": "1544027013",
"private": true, "private": true,
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -16,7 +16,7 @@
"@trpc/server": "10.38.3", "@trpc/server": "10.38.3",
"better-sqlite3-multiple-ciphers": "^9.5.0", "better-sqlite3-multiple-ciphers": "^9.5.0",
"electron-trpc": "0.5.2", "electron-trpc": "0.5.2",
"electron-updater": "6.1.4", "electron-updater": "^6.2.1",
"icojs": "^0.17.1", "icojs": "^0.17.1",
"sodium-native": "^4.1.1", "sodium-native": "^4.1.1",
"typed-emitter": "^2.1.0", "typed-emitter": "^2.1.0",
@@ -29,7 +29,8 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"electron": "^29.3.1", "electron": "^29.3.1",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"esbuild": "^0.20.0", "esbuild": "^0.23.0",
"kysely": "^0.27.3",
"node-gyp-build": "^4.8.0", "node-gyp-build": "^4.8.0",
"prebuildify": "^6.0.1", "prebuildify": "^6.0.1",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",

View File

@@ -38,7 +38,7 @@ await fs.rm("./build/", { force: true, recursive: true });
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) { if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec( await exec(
"npx nx build:desktop @notesnook/web", "yarn nx build:desktop @notesnook/web",
path.join(__dirname, "..", "..", "..") path.join(__dirname, "..", "..", "..")
); );
} }
@@ -46,11 +46,11 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
// temporary until there's support for prebuilt binaries for linux ARM // temporary until there's support for prebuilt binaries for linux ARM
if (os.platform() === "linux") await patchBetterSQLite3(); if (os.platform() === "linux") await patchBetterSQLite3();
if (os.platform() === "win32") // if (os.platform() === "win32")
await exec( // await exec(
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`, // `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
path.join(__dirname, "..", "node_modules", "sodium-native") // path.join(__dirname, "..", "node_modules", "sodium-native")
); // );
await fs.cp(path.join(webAppPath, "build"), "build", { await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true, recursive: true,
@@ -58,15 +58,15 @@ await fs.cp(path.join(webAppPath, "build"), "build", {
}); });
if (args.variant === "mas") { if (args.variant === "mas") {
await exec(`npm run bundle:mas`); await exec(`yarn run bundle:mas`);
} else { } else {
await exec(`npm run bundle`); await exec(`yarn run bundle`);
} }
await exec(`npx tsc`); await exec(`yarn tsc`);
if (args.run) { if (args.run) {
await exec(`npx electron-builder --dir --x64`); await exec(`yarn electron-builder --dir --x64`);
if (process.platform === "win32") { if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`); await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") { } else if (process.platform === "darwin") {

View File

@@ -53,11 +53,11 @@ async function onChange(first) {
if (first) { if (first) {
await fs.rm("./build/", { force: true, recursive: true }); await fs.rm("./build/", { force: true, recursive: true });
await exec("npx electron-builder install-app-deps"); await exec("yarn electron-builder install-app-deps");
} }
await exec(`npm run bundle`); await exec(`yarn run bundle`);
execAsync(`npx`, [`tsc`]); execAsync(`yarn`, [`tsc`]);
if (await isBundleSame()) { if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing."); console.log("Bundle is same. Doing nothing.");
@@ -66,7 +66,7 @@ async function onChange(first) {
if (first) { if (first) {
await spawnAndWaitUntil( await spawnAndWaitUntil(
["npx", "nx", "start:desktop", "@notesnook/web"], ["yarn", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."), path.join(__dirname, "..", "..", ".."),
(data) => data.includes("Network: use --host to expose") (data) => data.includes("Network: use --host to expose")
); );
@@ -78,7 +78,7 @@ async function onChange(first) {
} }
execAsync( execAsync(
"npx", "yarn",
["electron", path.join("build", "electron.js")], ["electron", path.join("build", "electron.js")],
true, true,
cleanup cleanup

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { initTRPC } from "@trpc/server"; import { initTRPC } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { dialog, nativeTheme, Notification, shell } from "electron"; import { app, dialog, nativeTheme, Notification, shell } from "electron";
import { AutoLaunch } from "../utils/autolaunch"; import { AutoLaunch } from "../utils/autolaunch";
import { config, DesktopIntegration } from "../utils/config"; import { config, DesktopIntegration } from "../utils/config";
import { bringToFront } from "../utils/bring-to-front"; import { bringToFront } from "../utils/bring-to-front";
@@ -32,6 +32,7 @@ import { AssetManager } from "../utils/asset-manager";
import { isFlatpak } from "../utils"; import { isFlatpak } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration"; import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises"; import { rm } from "fs/promises";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
const t = initTRPC.create(); const t = initTRPC.create();
@@ -55,6 +56,15 @@ export const osIntegrationRouter = t.router({
config.zoomFactor = factor; config.zoomFactor = factor;
}), }),
customDns: t.procedure.query(() => config.customDns),
setCustomDns: t.procedure
.input(z.boolean().optional())
.mutation(({ input: customDns }) => {
if (customDns) enableCustomDns();
else disableCustomDns();
config.customDns = !!customDns;
}),
proxyRules: t.procedure.query(() => config.proxyRules), proxyRules: t.procedure.query(() => config.proxyRules),
setProxyRules: t.procedure setProxyRules: t.procedure
.input(z.string().optional()) .input(z.string().optional())
@@ -142,6 +152,10 @@ export const osIntegrationRouter = t.router({
await rm(input); await rm(input);
}), }),
restart: t.procedure.query(() => {
app.relaunch();
app.exit();
}),
showNotification: t.procedure showNotification: t.procedure
.input(NotificationOptions) .input(NotificationOptions)
.query(({ input }) => { .query(({ input }) => {
@@ -181,7 +195,10 @@ export const osIntegrationRouter = t.router({
({ input: { theme, windowControlsIconColor, backgroundColor } }) => { ({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
if (windowControlsIconColor) { if (windowControlsIconColor) {
config.windowControlsIconColor = windowControlsIconColor; config.windowControlsIconColor = windowControlsIconColor;
if (process.platform === "win32") if (
process.platform === "win32" &&
!config.desktopSettings.nativeTitlebar
)
globalThis.window?.setTitleBarOverlay({ globalThis.window?.setTitleBarOverlay({
symbolColor: windowControlsIconColor symbolColor: windowControlsIconColor
}); });

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

@@ -61,7 +61,18 @@ export const updaterRouter = t.router({
"update-not-available" "update-not-available"
), ),
onAvailable: createSubscription<"update-available", UpdateInfo>( onAvailable: createSubscription<"update-available", UpdateInfo>(
"update-available" "update-available",
() => {
if (!config.automaticUpdates) return false;
autoUpdater.emit("download-progress", {
bytesPerSecond: 0,
delta: 0,
percent: 0,
total: 100,
transferred: 0
});
return true;
}
), ),
onError: createSubscription("error") onError: createSubscription("error")
}); });
@@ -69,10 +80,11 @@ export const updaterRouter = t.router({
function createSubscription< function createSubscription<
TName extends keyof AppUpdaterEvents, TName extends keyof AppUpdaterEvents,
TReturnType = Parameters<AppUpdaterEvents[TName]>[0] TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
>(eventName: TName) { >(eventName: TName, handler?: (args: TReturnType) => boolean) {
return t.procedure.subscription(() => { return t.procedure.subscription(() => {
return observable<TReturnType>((emit) => { return observable<TReturnType>((emit) => {
const listener: AppUpdaterEvents[TName] = (...args: any[]) => { const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
if (handler?.(args[0])) return;
emit.next(args[0]); emit.next(args[0]);
}; };
autoUpdater.removeAllListeners(eventName); autoUpdater.removeAllListeners(eventName);

View File

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

View File

@@ -35,6 +35,7 @@ import path from "path";
import { bringToFront } from "./utils/bring-to-front"; import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge"; import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration"; import { setupDesktopIntegration } from "./utils/desktop-integration";
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
// only run a single instance // only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) { if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -83,22 +84,27 @@ async function createWindow() {
format: process.platform === "win32" ? "ico" : "png" format: process.platform === "win32" ? "ico" : "png"
}), }),
titleBarStyle: "hidden", ...(config.desktopSettings.nativeTitlebar
frame: process.platform === "win32" || process.platform === "darwin", ? {}
titleBarOverlay: { : {
height: 37, titleBarStyle: "hidden",
color: "#00000000", frame: process.platform === "win32" || process.platform === "darwin",
symbolColor: config.windowControlsIconColor titleBarOverlay: {
}, height: 37,
trafficLightPosition: { color: "#00000000",
x: 16, symbolColor: config.windowControlsIconColor
y: 12 },
}, trafficLightPosition: {
x: 16,
y: 12
}
}),
webPreferences: { webPreferences: {
zoomFactor: config.zoomFactor, zoomFactor: config.zoomFactor,
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false, contextIsolation: false,
nodeIntegrationInWorker: true,
spellcheck: config.isSpellCheckerEnabled, spellcheck: config.isSpellCheckerEnabled,
preload: __dirname + "/preload.js" preload: __dirname + "/preload.js"
} }
@@ -151,14 +157,8 @@ async function createWindow() {
app.once("ready", async () => { app.once("ready", async () => {
console.info("App ready. Opening window."); console.info("App ready. Opening window.");
app.configureHostResolver({ if (config.customDns) enableCustomDns();
secureDnsServers: [ else disableCustomDns();
"https://mozilla.cloudflare-dns.com/dns-query",
"https://dns.quad9.net/dns-query"
],
enableBuiltInResolver: true,
secureDnsMode: "automatic"
});
if (!isDevelopment()) registerProtocol(); if (!isDevelopment()) registerProtocol();
await createWindow(); await createWindow();

View File

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

View File

@@ -35,6 +35,7 @@ async function configureAutoUpdater() {
autoUpdater.allowDowngrade = false; autoUpdater.allowDowngrade = false;
autoUpdater.allowPrerelease = false; autoUpdater.allowPrerelease = false;
autoUpdater.autoInstallOnAppQuit = true; autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.disableWebInstaller = true;
} }
export { configureAutoUpdater }; export { configureAutoUpdater };

View File

@@ -25,7 +25,8 @@ export const DesktopIntegration = z.object({
autoStart: z.boolean().optional(), autoStart: z.boolean().optional(),
startMinimized: z.boolean().optional(), startMinimized: z.boolean().optional(),
minimizeToSystemTray: z.boolean().optional(), minimizeToSystemTray: z.boolean().optional(),
closeToSystemTray: z.boolean().optional() closeToSystemTray: z.boolean().optional(),
nativeTitlebar: z.boolean().optional()
}); });
export type DesktopIntegration = z.infer<typeof DesktopIntegration>; export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
@@ -35,7 +36,8 @@ export const config = {
autoStart: false, autoStart: false,
startMinimized: false, startMinimized: false,
minimizeToSystemTray: false, minimizeToSystemTray: false,
closeToSystemTray: false closeToSystemTray: false,
nativeTitlebar: false
}, },
privacyMode: false, privacyMode: false,
isSpellCheckerEnabled: true, isSpellCheckerEnabled: true,
@@ -43,6 +45,7 @@ export const config = {
theme: nativeTheme.themeSource, theme: nativeTheme.themeSource,
automaticUpdates: true, automaticUpdates: true,
proxyRules: "", proxyRules: "",
customDns: true,
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff", backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
windowControlsIconColor: windowControlsIconColor:

View File

@@ -0,0 +1,37 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { app } from "electron";
export function enableCustomDns() {
app.configureHostResolver({
secureDnsServers: [
"https://mozilla.cloudflare-dns.com/dns-query",
"https://dns.quad9.net/dns-query"
],
enableBuiltInResolver: true
});
}
export function disableCustomDns() {
app.configureHostResolver({
secureDnsServers: [],
enableBuiltInResolver: true
});
}

View File

@@ -16,13 +16,13 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import "react-native-gesture-handler";
import { import {
THEME_COMPATIBILITY_VERSION, THEME_COMPATIBILITY_VERSION,
useThemeEngineStore useThemeEngineStore
} from "@notesnook/theme"; } from "@notesnook/theme";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { I18nManager, View } from "react-native"; import { I18nManager, View } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler"; import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context"; import { SafeAreaProvider } from "react-native-safe-area-context";
import AppLockedOverlay from "./components/app-lock-overlay"; import AppLockedOverlay from "./components/app-lock-overlay";
@@ -40,30 +40,25 @@ import { useUserStore } from "./stores/use-user-store";
I18nManager.allowRTL(false); I18nManager.allowRTL(false);
I18nManager.forceRTL(false); I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false); I18nManager.swapLeftAndRightInRTL(false);
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
const App = () => { const App = () => {
const init = useAppEvents(); useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
useEffect(() => { useEffect(() => {
const { appLockEnabled, appLockMode } = SettingsService.get(); SettingsService.onFirstLaunch();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => { setTimeout(async () => {
SettingsService.onFirstLaunch();
await Notifications.get(); await Notifications.get();
if (SettingsService.get().notifNotes) { if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(true); Notifications.pinQuickNote(true);
} }
TipManager.init(); TipManager.init();
}, 100); }, 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return ( return (
<View <View
style={{ style={{

View File

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

View File

@@ -85,4 +85,10 @@ export async function setupDatabase(password) {
} }
export const db = database; 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,30 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 { 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() let loggerLoaded = false;
.withInstanceID("notesnook_logs") const initializeLogger = async () => {
.initialize(); if (loggerLoaded) return;
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();
loggerLoaded = true;
};
initialize(new KV(LoggerStorage)); export { initializeLogger };
export { LoggerStorage };

View File

@@ -218,12 +218,7 @@ export default async function downloadAttachment(
} }
if (options.base64 || options.text) { if (options.base64 || options.text) {
console.log( DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
"starting decrypt base64 file...",
options.base64,
options.text,
attachment.hash
);
return await db.attachments.read( return await db.attachments.read(
attachment.hash, attachment.hash,
options.base64 ? "base64" : "text" options.base64 ? "base64" : "text"
@@ -292,7 +287,7 @@ export default async function downloadAttachment(
return fileUri; return fileUri;
} catch (e) { } catch (e) {
console.log("download attachment error: ", e); DatabaseLogger.error(e);
if (attachment.dateUploaded) { if (attachment.dateUploaded) {
RNFetchBlob.fs RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`) .unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)

View File

@@ -22,21 +22,24 @@ import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util"; import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager"; import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store"; import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database"; import { DatabaseLogger, db } from "../database";
import { cacheDir, fileCheck } from "./utils"; import { cacheDir, fileCheck } from "./utils";
import { createCacheDir, exists } from "./io"; import { createCacheDir, exists } from "./io";
export async function downloadFile(filename, data, cancelToken) { export async function downloadFile(filename, data, cancelToken) {
if (!data) return false; if (!data) {
DatabaseLogger.log(`Error downloading file: ${filename}, reason: No data`);
return false;
}
console.log("Downloading", filename); DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir(); await createCacheDir();
let { url, headers } = data; let { url, headers } = data;
let path = `${cacheDir}/${filename}`; let path = `${cacheDir}/${filename}`;
try { try {
if (await exists(filename)) { if (await exists(filename)) {
console.log("Exists already", filename); DatabaseLogger.log(`File Exists already: ${filename}`);
return true; return true;
} }
@@ -44,13 +47,24 @@ export async function downloadFile(filename, data, cancelToken) {
method: "GET", method: "GET",
headers headers
}); });
if (!res.ok)
if (!res.ok) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
);
throw new Error(`${res.status}: Unable to resolve download url`); throw new Error(`${res.status}: Unable to resolve download url`);
}
const downloadUrl = await res.text(); const downloadUrl = await res.text();
if (!downloadUrl) throw new Error("Unable to resolve download url"); if (!downloadUrl) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: Unable to resolve download url`
);
throw new Error("Unable to resolve download url");
}
let totalSize = 0; let totalSize = 0;
console.log("Download starting"); DatabaseLogger.log(`Download starting: ${filename}`);
let request = RNFetchBlob.config({ let request = RNFetchBlob.config({
path: path, path: path,
IOSBackgroundTask: true IOSBackgroundTask: true
@@ -61,13 +75,15 @@ export async function downloadFile(filename, data, cancelToken) {
.getState() .getState()
.setProgress(0, total, filename, recieved, "download"); .setProgress(0, total, filename, recieved, "download");
totalSize = total; totalSize = total;
console.log("downloading: ", recieved, total); DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
}); });
cancelToken.cancel = () => { cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename); useAttachmentStore.getState().remove(filename);
request.cancel(); request.cancel();
DatabaseLogger.log(`Download cancelled: ${filename}`);
}; };
let response = await request; let response = await request;
await fileCheck(response, totalSize); await fileCheck(response, totalSize);
let status = response.info().status; let status = response.info().status;
@@ -91,7 +107,10 @@ export async function downloadFile(filename, data, cancelToken) {
useAttachmentStore.getState().remove(filename); useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(path).catch(console.log); RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("Download file error:", e, url, headers); DatabaseLogger.error(e, {
url,
headers
});
return false; return false;
} }
} }
@@ -100,18 +119,17 @@ export async function getUploadedFileSize(hash) {
try { try {
const url = `${hosts.API_HOST}/s3?name=${hash}`; const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken(); const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, { const attachmentInfo = await fetch(url, {
method: "HEAD", method: "HEAD",
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
}); });
const contentLength = parseInt( const contentLength = parseInt(
attachmentInfo.headers?.get("content-length") attachmentInfo.headers?.get("content-length")
); );
return isNaN(contentLength) ? 0 : contentLength; return isNaN(contentLength) ? 0 : contentLength;
} catch (e) { } catch (e) {
return 0; DatabaseLogger.error(e);
return -1;
} }
} }
@@ -125,7 +143,9 @@ export async function checkAttachment(hash) {
try { try {
const size = await getUploadedFileSize(hash); const size = await getUploadedFileSize(hash);
if (size <= 0) return { failed: "File length is 0." }; if (size === -1) return { success: true };
if (size === 0) return { failed: "File length is 0." };
} catch (e) { } catch (e) {
return { failed: e?.message }; return { failed: e?.message };
} }

View File

@@ -21,12 +21,12 @@ import Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native"; import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util"; import RNFetchBlob from "react-native-blob-util";
import { IOS_APPGROUPID } from "../../utils/constants"; import { IOS_APPGROUPID } from "../../utils/constants";
import { db } from "../database"; import { DatabaseLogger, db } from "../database";
import { cacheDir, cacheDirOld, getRandomId } from "./utils"; import { cacheDir, cacheDirOld, getRandomId } from "./utils";
export async function readEncrypted(filename, key, cipherData) { export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache(); await migrateFilesFromCache();
console.log("Read encrypted file..."); DatabaseLogger.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`; let path = `${cacheDir}/${filename}`;
try { try {
@@ -34,8 +34,6 @@ export async function readEncrypted(filename, key, cipherData) {
return false; return false;
} }
const attachment = await db.attachments.attachment(filename);
console.log("decrypting....");
let output = await Sodium.decryptFile( let output = await Sodium.decryptFile(
key, key,
{ {
@@ -45,12 +43,13 @@ export async function readEncrypted(filename, key, cipherData) {
}, },
cipherData.outputType === "base64" ? "base64" : "text" cipherData.outputType === "base64" ? "base64" : "text"
); );
console.log("file decrypted...", attachment?.mimeType);
DatabaseLogger.log("File decrypted...");
return output; return output;
} catch (e) { } catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log); RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("readEncrypted", e); DatabaseLogger.error(e);
return false; return false;
} }
} }
@@ -128,7 +127,7 @@ export async function clearFileStorage() {
export async function createCacheDir() { export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) { if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir); await RNFetchBlob.fs.mkdir(cacheDir);
console.log("Cache directory created"); DatabaseLogger.log("Cache directory created");
} }
} }
@@ -188,6 +187,9 @@ export async function exists(filename) {
); );
if (stat.size !== expectedFileSize) { if (stat.size !== expectedFileSize) {
DatabaseLogger.log(
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
);
RNFetchBlob.fs RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path) .unlink(existsInAppGroup ? appGroupPath : path)
.catch(console.log); .catch(console.log);

View File

@@ -25,6 +25,8 @@ import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants"; import { IOS_APPGROUPID } from "../../utils/constants";
import { createCacheDir } from "./io"; import { createCacheDir } from "./io";
import { getUploadedFileSize } from "./download";
import { ToastManager } from "../../services/event-manager";
export async function uploadFile(filename, data, cancelToken) { export async function uploadFile(filename, data, cancelToken) {
if (!data) return false; if (!data) return false;
@@ -33,13 +35,31 @@ export async function uploadFile(filename, data, cancelToken) {
DatabaseLogger.info(`Preparing to upload file: ${filename}`); DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try { try {
const uploadedFileSize = await getUploadedFileSize(filename);
if (uploadedFileSize === -1) {
DatabaseLogger.log("Upload verification failed.");
return false;
}
const isUploaded = uploadedFileSize !== 0;
if (isUploaded) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let res = await fetch(url, { let res = await fetch(url, {
method: "PUT", method: "PUT",
headers headers
}); });
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
const uploadUrl = await res.text(); const uploadUrl = res.ok ? await res.text() : await res.json();
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
if (typeof uploadUrl !== "string") {
throw new Error(
uploadUrl.error || "Unable to resolve attachment upload url."
);
}
let uploadFilePath = `${cacheDir}/${filename}`; let uploadFilePath = `${cacheDir}/${filename}`;
const iosAppGroup = const iosAppGroup =
@@ -50,7 +70,15 @@ export async function uploadFile(filename, data, cancelToken) {
let exists = await RNFetchBlob.fs.exists(uploadFilePath); let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") { if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath; uploadFilePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(uploadFilePath);
} }
if (!exists) {
throw new Error(
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
);
}
DatabaseLogger.info(`Starting upload: ${filename}`); DatabaseLogger.info(`Starting upload: ${filename}`);
let request = RNFetchBlob.config({ let request = RNFetchBlob.config({
@@ -104,8 +132,10 @@ export async function uploadFile(filename, data, cancelToken) {
return result; return result;
} catch (e) { } catch (e) {
useAttachmentStore.getState().remove(filename); useAttachmentStore.getState().remove(filename);
DatabaseLogger.info(`File upload error: ${filename}, ${e}`); ToastManager.error(e, "File upload failed");
DatabaseLogger.error(e); DatabaseLogger.error(e, "File upload failed", {
filename
});
return false; return false;
} }
} }

View File

@@ -20,15 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react"; import React from "react";
import { Linking, View } from "react-native"; import { Linking, View } from "react-native";
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore'; //import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Sync from "../../services/sync";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events"; import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eCloseAnnouncementDialog } from "../../utils/events";
import { SIZE } from "../../utils/size"; import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time"; import { sleep } from "../../utils/time";
import { PricingPlans } from "../premium/pricing-plans"; import { PricingPlans } from "../premium/pricing-plans";
import SheetProvider from "../sheet-provider"; import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { allowedOnPlatform, getStyle } from "./functions"; import { allowedOnPlatform, getStyle } from "./functions";
@@ -56,13 +54,6 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
/> />
) )
}); });
} else if (item.type === "force-sync") {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, true, () => {
eSendEvent(eCloseSheet);
});
} }
}; };
return ( return (

View File

@@ -18,10 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import React, { useCallback, useEffect, useRef } from "react"; import React, { useCallback, useEffect, useRef } from "react";
import { AppStateStatus, Platform, TextInput, View } from "react-native"; import {
AppStateStatus,
Platform,
TextInput,
useWindowDimensions,
View
} from "react-native";
//@ts-ignore //@ts-ignore
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import { enabled } from "react-native-privacy-snapshot";
import { DatabaseLogger } from "../../common/database"; import { DatabaseLogger } from "../../common/database";
import { import {
decrypt, decrypt,
@@ -34,17 +39,20 @@ import {
import { MMKV } from "../../common/database/mmkv"; import { MMKV } from "../../common/database/mmkv";
import { useAppState } from "../../hooks/use-app-state"; import { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics"; import BiometricService from "../../services/biometrics";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings"; import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store"; import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store"; import { useUserStore } from "../../stores/use-user-store";
import { NotesnookModule } from "../../utils/notesnook-module"; import { NotesnookModule } from "../../utils/notesnook-module";
import { SIZE } from "../../utils/size"; import { SIZE } from "../../utils/size";
import { Toast } from "../toast";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button"; import { IconButton } from "../ui/icon-button";
import Input from "../ui/input"; import Input from "../ui/input";
import Seperator from "../ui/seperator"; import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading"; import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph"; import Paragraph from "../ui/typography/paragraph";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
const getUser = () => { const getUser = () => {
const user = MMKV.getString("user"); const user = MMKV.getString("user");
@@ -85,6 +93,7 @@ const AppLockedOverlay = () => {
const appState = useAppState(); const appState = useAppState();
const lastAppState = useRef<AppStateStatus>(appState); const lastAppState = useRef<AppStateStatus>(appState);
const biometricUnlockAwaitingUserInput = useRef(false); const biometricUnlockAwaitingUserInput = useRef(false);
const { height } = useWindowDimensions();
const keyboardType = useSettingStore( const keyboardType = useSettingStore(
(state) => state.settings.applockKeyboardType (state) => state.settings.applockKeyboardType
); );
@@ -119,7 +128,6 @@ const AppLockedOverlay = () => {
); );
if (unlocked) { if (unlocked) {
lockApp(false); lockApp(false);
enabled(false);
password.current = undefined; password.current = undefined;
} }
biometricUnlockAwaitingUserInput.current = false; biometricUnlockAwaitingUserInput.current = false;
@@ -146,8 +154,13 @@ const AppLockedOverlay = () => {
} }
lockApp(false); lockApp(false);
enabled(false);
password.current = undefined; password.current = undefined;
} else {
ToastManager.show({
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
type: "error",
context: "local"
});
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -155,10 +168,6 @@ const AppLockedOverlay = () => {
}; };
useEffect(() => { useEffect(() => {
if (appState === "active") {
enabled(false);
}
const prevState = lastAppState.current; const prevState = lastAppState.current;
lastAppState.current = appState; lastAppState.current = appState;
@@ -184,31 +193,28 @@ const AppLockedOverlay = () => {
DatabaseLogger.info("Biometric unlock request"); DatabaseLogger.info("Biometric unlock request");
onUnlockAppRequested(); onUnlockAppRequested();
} }
enabled(false);
} else { } else {
SettingsService.appEnteredBackground(); SettingsService.appEnteredBackground();
if (
SettingsService.get().privacyScreen ||
SettingsService.getProperty("appLockEnabled")
) {
enabled(true);
}
} }
}, [appState, onUnlockAppRequested, appLocked]); }, [appState, onUnlockAppRequested, appLocked]);
return appLocked ? ( return appLocked ? (
<View <KeyboardAwareScrollView
style={{ style={{
backgroundColor: colors.primary.background, backgroundColor: colors.primary.background,
width: "100%", width: "100%",
height: "100%", height: "100%",
position: "absolute", position: "absolute",
zIndex: 999, zIndex: 999
justifyContent: "center"
}} }}
contentContainerStyle={{
justifyContent: "center",
minHeight: height
}}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
> >
<Toast context="local" />
<View <View
style={{ style={{
flex: 1, flex: 1,
@@ -321,7 +327,7 @@ const AppLockedOverlay = () => {
</View> </View>
</View> </View>
</View> </View>
</View> </KeyboardAwareScrollView>
) : null; ) : null;
}; };

View File

@@ -56,6 +56,7 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph"; import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store"; import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils"; import { editorController } from "../../screens/editor/tiptap/utils";
import SheetProvider from "../sheet-provider";
const Actions = ({ const Actions = ({
attachment, attachment,
@@ -218,6 +219,7 @@ const Actions = ({
}} }}
> >
<Dialog context={contextId} /> <Dialog context={contextId} />
<SheetProvider context={contextId} />
<View <View
style={{ style={{
borderBottomWidth: 1, borderBottomWidth: 1,
@@ -238,36 +240,20 @@ const Actions = ({
style={{ style={{
flexDirection: "row", flexDirection: "row",
marginBottom: 10, marginBottom: 10,
paddingHorizontal: 12 paddingHorizontal: 12,
marginTop: 6,
gap: 10
}} }}
> >
<Paragraph <Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
size={SIZE.xs} {attachment.mimeType}
style={{
marginRight: 10
}}
color={colors.secondary.paragraph}
>
{attachment.type}
</Paragraph> </Paragraph>
<Paragraph <Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{formatBytes(attachment.size)} {formatBytes(attachment.size)}
</Paragraph> </Paragraph>
{notes.length ? ( {notes.length ? (
<Paragraph <Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{notes.length} note {notes.length} note
{notes.length > 1 ? "s" : ""} {notes.length > 1 ? "s" : ""}
</Paragraph> </Paragraph>
@@ -366,7 +352,7 @@ const Actions = ({
{failed ? ( {failed ? (
<Notice <Notice
type="alert" type="alert"
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`} text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
size="small" size="small"
/> />
) : null} ) : null}

View File

@@ -177,7 +177,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
case "audio": case "audio":
items = note items = note
? db.attachments.ofNote(note.id, "audio") ? db.attachments.ofNote(note.id, "audio")
: db.attachments.all; : db.attachments.audios;
break; break;
case "documents": case "documents":
items = note items = note
@@ -355,6 +355,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
AttachmentDialog.present = (note?: Note) => { AttachmentDialog.present = (note?: Note) => {
presentSheet({ presentSheet({
component: () => <AttachmentDialog note={note} /> component: () => <AttachmentDialog note={note} />,
keyboardHandlerDisabled: true
}); });
}; };

View File

@@ -17,12 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native"; import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet"; import { SheetManager } from "react-native-actions-sheet";
import { DDS } from "../../services/device-detection"; import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager"; import { eSendEvent } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme"; import Sync from "../../services/sync";
import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import { eUserLoggedIn } from "../../utils/events";
import { SIZE } from "../../utils/size"; import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time"; import { sleep } from "../../utils/time";
import SheetProvider from "../sheet-provider"; import SheetProvider from "../sheet-provider";
@@ -34,11 +38,6 @@ import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common"; import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password"; import { ForgotPassword } from "./forgot-password";
import { useLogin } from "./use-login"; import { useLogin } from "./use-login";
import { useSettingStore } from "../../stores/use-setting-store";
import { eUserLoggedIn } from "../../utils/events";
import { useUserStore } from "../../stores/use-user-store";
import Sync from "../../services/sync";
import { Notice } from "../ui/notice";
const LoginSteps = { const LoginSteps = {
emailAuth: 1, emailAuth: 1,
@@ -67,7 +66,7 @@ export const Login = ({ changeMode }) => {
Progress.present(); Progress.present();
setTimeout(() => { setTimeout(() => {
if (!useUserStore.getState().syncing) { if (!useUserStore.getState().syncing) {
Sync.run("global", false, true); Sync.run("global", false, "full");
} }
}, 5000); }, 5000);
}); });
@@ -184,7 +183,11 @@ export const Login = ({ changeMode }) => {
defaultValue={email.current} defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading} editable={step === LoginSteps.emailAuth && !loading}
onSubmit={() => { onSubmit={() => {
passwordInputRef.current?.focus(); if (step === LoginSteps.emailAuth) {
login();
} else {
passwordInputRef.current?.focus();
}
}} }}
/> />
@@ -243,6 +246,7 @@ export const Login = ({ changeMode }) => {
width: 250, width: 250,
borderRadius: 100 borderRadius: 100
}} }}
height={50}
fontSize={SIZE.md} fontSize={SIZE.md}
type="accent" type="accent"
title={!loading ? "Continue" : null} title={!loading ? "Continue" : null}

View File

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

View File

@@ -37,6 +37,7 @@ import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading"; import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph"; import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react"; import { useCallback } from "react";
import { ScrollView } from "react-native-actions-sheet";
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => { const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
@@ -143,7 +144,10 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]); }, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
return ( return (
<View> <ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
>
<View <View
style={{ style={{
alignItems: "center", alignItems: "center",
@@ -214,6 +218,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
code.current = value; code.current = value;
//onNext(); //onNext();
}} }}
onSubmitEditing={onNext}
caretHidden caretHidden
inputStyle={{ inputStyle={{
fontSize: SIZE.lg, fontSize: SIZE.lg,
@@ -225,6 +230,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
keyboardType={ keyboardType={
currentMethod.method === "recoveryCode" ? "default" : "numeric" currentMethod.method === "recoveryCode" ? "default" : "numeric"
} }
enablesReturnKeyAutomatically
containerStyle={{ containerStyle={{
height: 60, height: 60,
borderWidth: 0, borderWidth: 0,
@@ -297,7 +303,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
</> </>
)} )}
</View> </View>
</View> </ScrollView>
); );
}; };

View File

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

View File

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

View File

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

View File

@@ -17,8 +17,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import React, { useState } from "react"; import React, { useState } from "react";
import { Image, ScrollView, TouchableOpacity, View } from "react-native"; import {
import { ImagePickerResponse } from "react-native-image-picker"; Image,
Platform,
ScrollView,
TouchableOpacity,
View
} from "react-native";
import { Image as ImageType } from "react-native-image-crop-picker";
import { useThemeColors } from "../../../../../../packages/theme/dist"; import { useThemeColors } from "../../../../../../packages/theme/dist";
import { presentSheet } from "../../../services/event-manager"; import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size"; import { SIZE } from "../../../utils/size";
@@ -32,7 +38,7 @@ export default function AttachImage({
onAttach, onAttach,
close close
}: { }: {
response: ImagePickerResponse; response: ImageType[];
onAttach: ({ compress }: { compress: boolean }) => void; onAttach: ({ compress }: { compress: boolean }) => void;
close: ((ctx?: string | undefined) => void) | undefined; close: ((ctx?: string | undefined) => void) | undefined;
}) { }) {
@@ -58,14 +64,17 @@ export default function AttachImage({
}} }}
> >
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}> <Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
Attaching {response.assets?.length} image(s): Attaching {response?.length} image(s):
</Paragraph> </Paragraph>
<ScrollView horizontal> <ScrollView horizontal>
{response.assets?.map((item) => ( {response?.map?.((item) => (
<TouchableOpacity key={item.fileName} activeOpacity={0.9}> <TouchableOpacity key={item.filename} activeOpacity={0.9}>
<Image <Image
source={{ source={{
uri: item.uri uri:
Platform.OS === "ios"
? item.sourceURL || item.path
: item.path
}} }}
style={{ style={{
width: 100, width: 100,
@@ -142,7 +151,7 @@ export default function AttachImage({
<Button <Button
title={`${ title={`${
(response.assets?.length || 0) > 1 ? "Attach Images" : "Attach Image" (response?.length || 0) > 1 ? "Attach Images" : "Attach Image"
}`} }`}
type="accent" type="accent"
width="100%" width="100%"
@@ -156,10 +165,16 @@ export default function AttachImage({
); );
} }
AttachImage.present = (response: ImagePickerResponse) => { AttachImage.present = (response: ImageType[], context?: string) => {
return new Promise((resolve) => { return new Promise<
| {
compress: boolean;
}
| undefined
>((resolve) => {
let resolved = false; let resolved = false;
presentSheet({ presentSheet({
context: context,
component: (ref, close, update) => ( component: (ref, close, update) => (
<AttachImage <AttachImage
response={response} response={response}

View File

@@ -128,7 +128,7 @@ export class VaultDialog extends Component {
: this.state.copyNote : this.state.copyNote
? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault." ? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.goToEditor : this.state.goToEditor
? "Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault." ? "Unlock note to open it in editor."
: "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault." : "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault."
: "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note."); : "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note.");
} }
@@ -586,7 +586,7 @@ export class VaultDialog extends Component {
} }
async _copyNote(note) { async _copyNote(note) {
Clipboard.setString(await convertNoteToText(note)); Clipboard.setString((await convertNoteToText(note, true)) || "");
ToastManager.show({ ToastManager.show({
heading: "Note copied", heading: "Note copied",
type: "success", type: "success",
@@ -602,7 +602,7 @@ export class VaultDialog extends Component {
await Share.open({ await Share.open({
heading: "Share note", heading: "Share note",
failOnCancel: false, failOnCancel: false,
message: await convertNoteToText(note) message: (await convertNoteToText(note)) || ""
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@@ -23,10 +23,7 @@ import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { Dialog } from "../dialog"; import { Dialog } from "../dialog";
import { Issue } from "../sheets/github/issue"; import { Issue } from "../sheets/github/issue";
const error = ( const error = (stack: string, component: string) => `
stack: string,
component: string
) => `Please let us know what happened. What steps we can take to reproduce the issue here.
_______________________________ _______________________________
Stacktrace: In ${component}::${stack}`; Stacktrace: In ${component}::${stack}`;
@@ -39,9 +36,9 @@ class ExceptionHandler extends React.Component<{
error: Error | null; error: Error | null;
hasError: boolean; hasError: boolean;
} = { } = {
hasError: false, hasError: false,
error: null error: null
}; };
static getDerivedStateFromError(error: Error) { static getDerivedStateFromError(error: Error) {
return { hasError: true, error: error }; return { hasError: true, error: error };
} }

View File

@@ -33,6 +33,7 @@ import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus"; import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus"; import { RightMenus } from "./right-menus";
import { Title } from "./title"; import { Title } from "./title";
import { useNavigation } from "@react-navigation/native";
type HeaderRightButton = { type HeaderRightButton = {
title: string; title: string;
@@ -66,6 +67,8 @@ export const Header = ({
hasSearch?: boolean; hasSearch?: boolean;
onSearch?: () => void; onSearch?: () => void;
}) => { }) => {
const navigation = useNavigation();
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets(); const insets = useGlobalSafeAreaInsets();
const [borderHidden, setBorderHidden] = useState(true); const [borderHidden, setBorderHidden] = useState(true);

View File

@@ -19,10 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import Sodium from "@ammarahmed/react-native-sodium"; import Sodium from "@ammarahmed/react-native-sodium";
import dataurl from "@notesnook/core/dist/utils/dataurl"; import dataurl from "@notesnook/core/dist/utils/dataurl";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import ImageViewer from "react-native-image-zoom-viewer"; import ImageViewer from "react-native-image-zoom-viewer";
import { db } from "../../common/database";
import downloadAttachment from "../../common/filesystem/download-attachment"; import downloadAttachment from "../../common/filesystem/download-attachment";
import { cacheDir } from "../../common/filesystem/utils"; import { cacheDir } from "../../common/filesystem/utils";
import { import {
@@ -32,13 +34,13 @@ import {
import BaseDialog from "../dialog/base-dialog"; import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button"; import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy"; import { ProgressBarComponent } from "../ui/svg/lazy";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
const ImagePreview = () => { const ImagePreview = () => {
const { colors } = useThemeColors("dialog"); const { colors } = useThemeColors("dialog");
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [image, setImage] = useState<string>(); const [image, setImage] = useState<string>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const imageRef = useRef<ImageAttributes>();
useEffect(() => { useEffect(() => {
eSubscribeEvent("ImagePreview", open); eSubscribeEvent("ImagePreview", open);
@@ -48,6 +50,7 @@ const ImagePreview = () => {
}, []); }, []);
const open = async (image: ImageAttributes) => { const open = async (image: ImageAttributes) => {
imageRef.current = image;
setVisible(true); setVisible(true);
setLoading(true); setLoading(true);
setTimeout(async () => { setTimeout(async () => {
@@ -60,6 +63,9 @@ const ImagePreview = () => {
type: "base64", type: "base64",
uri: "" uri: ""
}); });
if (imageRef.current) {
imageRef.current.hash = hash;
}
} }
if (!hash) return; if (!hash) return;
//@ts-ignore // FIX ME //@ts-ignore // FIX ME
@@ -80,7 +86,13 @@ const ImagePreview = () => {
return ( return (
visible && ( visible && (
<BaseDialog animation="slide" visible={true} onRequestClose={close}> <BaseDialog
background="black"
animation="slide"
visible={true}
onRequestClose={close}
transparent
>
<View <View
style={{ style={{
width: "100%", width: "100%",
@@ -101,6 +113,21 @@ const ImagePreview = () => {
color={colors.primary.accent} color={colors.primary.accent}
borderColor="transparent" borderColor="transparent"
/> />
<IconButton
onPress={() => {
if (imageRef.current?.hash) {
db.fs().cancel(imageRef.current?.hash);
}
close();
}}
style={{
position: "absolute",
top: Platform.OS === "android" ? 35 : 0,
right: 12
}}
color={colors.static.white}
name="close"
/>
</View> </View>
) : ( ) : (
<ImageViewer <ImageViewer
@@ -117,13 +144,12 @@ const ImagePreview = () => {
width: "100%", width: "100%",
justifyContent: "flex-end", justifyContent: "flex-end",
alignItems: "center", alignItems: "center",
height: 80, height: 50,
marginTop: 0, paddingHorizontal: 24,
paddingHorizontal: 12,
position: "absolute", position: "absolute",
zIndex: 999, zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)", backgroundColor: "rgba(0,0,0,0.3)",
paddingTop: Platform.OS === "android" ? 30 : 0 marginTop: Platform.OS === "android" ? 30 : 0
}} }}
> >
<IconButton <IconButton
@@ -137,7 +163,7 @@ const ImagePreview = () => {
)} )}
imageUrls={[ imageUrls={[
{ {
url: image url: image as string
} }
]} ]}
/> />

View File

@@ -19,6 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { View } from "react-native"; import { View } from "react-native";
import React from "react"; import React from "react";
export const Footer = () => { export const Footer = ({ height = 150 }) => {
return <View style={{ height: 150 }} />; return <View style={{ height: height }} />;
}; };

View File

@@ -95,7 +95,7 @@ export const NotebookHeader = ({
style={{ style={{
flexShrink: 1 flexShrink: 1
}} }}
size={SIZE.xxl} size={SIZE.lg}
> >
{notebook.title} {notebook.title}
</Heading> </Heading>
@@ -114,7 +114,7 @@ export const NotebookHeader = ({
width: 40, width: 40,
height: 40 height: 40
}} }}
type={isPinnedToMenu ? "secondary" : "secondary"} type="transparent"
color={isPinnedToMenu ? colors.primary.accent : colors.primary.icon} color={isPinnedToMenu ? colors.primary.accent : colors.primary.icon}
size={SIZE.lg} size={SIZE.lg}
/> />
@@ -123,7 +123,7 @@ export const NotebookHeader = ({
onPress={onEditNotebook} onPress={onEditNotebook}
tooltipText="Edit this notebook" tooltipText="Edit this notebook"
name="pencil" name="pencil"
type="secondary" type="transparent"
color={colors.primary.icon} color={colors.primary.icon}
style={{ style={{
width: 40, width: 40,
@@ -134,7 +134,7 @@ export const NotebookHeader = ({
</View> </View>
{notebook.description ? ( {notebook.description ? (
<Paragraph size={SIZE.md} color={colors.primary.paragraph}> <Paragraph size={SIZE.sm} color={colors.primary.paragraph}>
{notebook.description} {notebook.description}
</Paragraph> </Paragraph>
) : null} ) : null}

View File

@@ -49,7 +49,8 @@ export const Card = ({ color }: { color?: string }) => {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
paddingHorizontal: 0 paddingHorizontal: 0,
width: "100%"
}} }}
> >
<View <View
@@ -91,7 +92,6 @@ export const Card = ({ color }: { color?: string }) => {
<View <View
style={{ style={{
marginLeft: 10, marginLeft: 10,
flexShrink: 1,
marginRight: 10 marginRight: 10
}} }}
> >

View File

@@ -80,7 +80,7 @@ export default function List(props: ListProps) {
const groupOptions = useGroupOptions(groupType); const groupOptions = useGroupOptions(groupType);
const _onRefresh = async () => { const _onRefresh = async () => {
Sync.run("global", false, true, () => { Sync.run("global", false, "full", () => {
props.onRefresh?.(); props.onRefresh?.();
}); });
}; };
@@ -192,7 +192,9 @@ export default function List(props: ListProps) {
/> />
) : null ) : null
} }
ListFooterComponent={<Footer />} ListFooterComponent={
<Footer height={props.renderedInRoute === "Notebook" ? 300 : 150} />
}
ListHeaderComponent={ ListHeaderComponent={
<> <>
{props.CustomLisHeader ? ( {props.CustomLisHeader ? (

View File

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

View File

@@ -19,14 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Item, ItemType, VirtualizedGrouping } from "@notesnook/core"; import { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react"; import React, { useEffect, useRef } from "react";
import { import {
BackHandler, BackHandler,
NativeEventSubscription, NativeEventSubscription,
Platform, Platform,
View View
} from "react-native"; } from "react-native";
import Animated, { FadeInUp } from "react-native-reanimated"; import Menu from "react-native-reanimated-material-menu/src/Menu";
import { db } from "../../common/database"; import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager"; import { ToastManager } from "../../services/event-manager";
@@ -42,9 +42,10 @@ import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to"; import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes"; import ExportNotesSheet from "../sheets/export-notes";
import ManageTagsSheet from "../sheets/manage-tags"; import ManageTagsSheet from "../sheets/manage-tags";
import { MoveNotebookSheet } from "../sheets/move-notebook";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button"; import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading"; import Heading from "../ui/typography/heading";
import { MoveNotebookSheet } from "../sheets/move-notebook";
export const SelectionHeader = React.memo( export const SelectionHeader = React.memo(
({ ({
@@ -58,6 +59,8 @@ export const SelectionHeader = React.memo(
type?: ItemType; type?: ItemType;
renderedInRoute?: string; renderedInRoute?: string;
}) => { }) => {
const menuRef = useRef<Menu>(null);
const { colors: contextMenuColors } = useThemeColors("contextMenu");
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode); const selectionMode = useSelectionStore((state) => state.selectionMode);
const selectedItemsList = useSelectionStore( const selectedItemsList = useSelectionStore(
@@ -132,8 +135,7 @@ export const SelectionHeader = React.memo(
}, [clearSelection, selectionMode]); }, [clearSelection, selectionMode]);
return selectionMode !== type || focusedRouteId !== id ? null : ( return selectionMode !== type || focusedRouteId !== id ? null : (
<Animated.View <View
entering={FadeInUp}
style={{ style={{
width: "100%", width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50, height: Platform.OS === "android" ? 50 + insets.top : 50,
@@ -143,8 +145,7 @@ export const SelectionHeader = React.memo(
alignItems: "center", alignItems: "center",
flexDirection: "row", flexDirection: "row",
zIndex: 999, zIndex: 999,
paddingHorizontal: 12, paddingHorizontal: 12
marginVertical: 10
}} }}
> >
<View <View
@@ -164,18 +165,15 @@ export const SelectionHeader = React.memo(
borderRadius: 100, borderRadius: 100,
marginRight: 10 marginRight: 10
}} }}
type="secondary"
onPress={() => { onPress={() => {
clearSelection(); clearSelection();
}} }}
size={SIZE.xl}
color={colors.primary.icon} color={colors.primary.icon}
name="close" name="close"
/> />
<View <View
style={{ style={{
backgroundColor: colors.secondary.background,
height: 40, height: 40,
borderRadius: 100, borderRadius: 100,
paddingHorizontal: 16, paddingHorizontal: 16,
@@ -184,11 +182,12 @@ export const SelectionHeader = React.memo(
alignItems: "center" alignItems: "center"
}} }}
> >
<Heading size={SIZE.md} color={colors.primary.accent}> <Heading size={SIZE.lg} color={colors.primary.paragraph}>
{selectedItemsList.length} {selectedItemsList.length}
</Heading> </Heading>
</View> </View>
</View> </View>
<View <View
style={{ style={{
flexDirection: "row", flexDirection: "row",
@@ -211,159 +210,142 @@ export const SelectionHeader = React.memo(
allSelected ? colors.primary.accent : colors.primary.paragraph allSelected ? colors.primary.accent : colors.primary.paragraph
} }
name="select-all" name="select-all"
size={SIZE.xl}
/> />
{renderedInRoute === "Notebooks" && selectedItemsList.length ? ( {selectedItemsList.length ? (
<IconButton <Menu
ref={menuRef}
animationDuration={200}
style={{ style={{
marginLeft: 10 borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: -20
}} }}
onPress={async () => { onRequestClose={() => {
const ids = selectedItemsList; menuRef.current?.hide();
const notebooks = await db.notebooks.all.items(ids);
MoveNotebookSheet.present(notebooks);
}} }}
tooltipText="Move notebooks" anchor={
tooltipPosition={1} <IconButton
name="arrow-right-bold-box-outline" onPress={() => {
size={SIZE.xl} menuRef.current?.show();
/> }}
) : null} name="dots-vertical"
color={colors.primary.paragraph}
{type !== "note" || !selectedItemsList.length ? null : ( />
<> }
<IconButton >
onPress={async () => { {[
await sleep(100); {
ManageTagsSheet.present(selectedItemsList); title:
}} selectedItemsList.length > 1
style={{ ? "Move notebooks"
marginLeft: 10 : "Move notebook",
}} onPress: async () => {
color={colors.primary.icon} const ids = selectedItemsList;
tooltipText="Manage tags" const notebooks = await db.notebooks.all.items(ids);
tooltipPosition={4} MoveNotebookSheet.present(notebooks);
name="pound" },
size={SIZE.xl} visible: renderedInRoute === "Notebooks",
/> icon: "arrow-right-bold-box-outline"
},
<IconButton {
onPress={async () => { title: "Manage tags",
ExportNotesSheet.present(selectedItemsList); onPress: async () => {
}} await sleep(100);
tooltipText="Export" ManageTagsSheet.present(selectedItemsList);
tooltipPosition={4} },
style={{ visible: type === "note",
marginLeft: 10 icon: "pound"
}} },
color={colors.primary.paragraph} {
name="export" title: "Export",
size={SIZE.xl} onPress: async () => {
/> await sleep(100);
ExportNotesSheet.present(selectedItemsList);
<IconButton },
onPress={async () => { visible: type === "note",
MoveNoteSheet.present(); icon: "export"
}} },
style={{ {
marginLeft: 10 title: "Link notebook",
}} onPress: async () => {
tooltipText="Add to notebooks" await sleep(100);
tooltipPosition={4} MoveNoteSheet.present();
color={colors.primary.paragraph} },
name="plus" visible: type === "note",
size={SIZE.xl} icon: "plus"
/> },
</> {
)} title: "Unlink notebook",
onPress: async () => {
{renderedInRoute === "Notebook" && selectedItemsList.length ? ( if (!id) return;
<IconButton await db.notes.removeFromNotebook(id, ...selectedItemsList);
onPress={async () => { updateNotebook(id);
if (!id) return; Navigation.queueRoutesForUpdate();
await db.notes.removeFromNotebook(id, ...selectedItemsList); clearSelection();
updateNotebook(id); },
Navigation.queueRoutesForUpdate(); visible: renderedInRoute === "Notebook",
clearSelection(); icon: "minus"
}} },
style={{ {
marginLeft: 10 title: "Unfavorite",
}} onPress: addToFavorite,
tooltipText={`Remove from Notebook`} visible: focusedRouteId === "Favorites",
tooltipPosition={4} icon: "star-off"
testID="select-minus" },
color={colors.primary.paragraph} {
name="minus" title: `Move to trash`,
size={SIZE.xl} onPress: async () => {
/> deleteItems(
) : null} undefined,
useSelectionStore.getState().selectionMode
{focusedRouteId === "Favorites" && selectedItemsList.length ? ( ).then(() => {
<IconButton useSelectionStore.getState().clearSelection();
onPress={addToFavorite} useSelectionStore.getState().setSelectionMode(undefined);
style={{ });
marginLeft: 10 },
}} visible: type !== "trash",
tooltipText="Remove from favorites" icon: "delete"
tooltipPosition={4} },
color={colors.primary.paragraph} {
name="star-off" title: `Restore`,
size={SIZE.xl} onPress: restoreItem,
/> visible: type === "trash",
) : null} icon: "delete-restore"
},
{type === "trash" || !selectedItemsList.length ? null : ( {
<IconButton title: `Delete`,
style={{ onPress: deleteItem,
marginLeft: 10 visible: type === "trash",
}} icon: "delete"
onPress={() => { }
deleteItems( ].map((item) =>
undefined, !item.visible ? null : (
useSelectionStore.getState().selectionMode <Button
).then(() => { style={{
useSelectionStore.getState().clearSelection(); width: 150,
useSelectionStore.getState().setSelectionMode(undefined); justifyContent: "flex-start",
}); borderRadius: 0
}} }}
tooltipText="Move to trash" type="plain"
tooltipPosition={1} buttonType={{
color={colors.primary.paragraph} text: contextMenuColors.primary.paragraph
name="delete" }}
size={SIZE.xl} icon={item.icon}
/> key={item.title}
)} title={item.title}
onPress={async () => {
{type === "trash" && selectedItemsList.length ? ( menuRef.current?.hide();
<> if (Platform.OS === "ios") await sleep(300);
<IconButton item.onPress();
style={{ }}
marginLeft: 10 />
}} )
color={colors.primary.paragraph} )}
onPress={restoreItem} </Menu>
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
<IconButton
style={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>
</>
) : null} ) : null}
</View> </View>
</Animated.View> </View>
); );
} }
); );

View File

@@ -45,14 +45,16 @@ export const AddNotebookSheet = ({
notebook, notebook,
parentNotebook, parentNotebook,
close, close,
showMoveNotesOnComplete showMoveNotesOnComplete,
defaultTitle
}: { }: {
notebook?: Notebook; notebook?: Notebook;
parentNotebook?: Notebook; parentNotebook?: Notebook;
close?: (didAddNotebook: boolean) => void; close?: (didAddNotebook: boolean) => void;
showMoveNotesOnComplete: boolean; showMoveNotesOnComplete: boolean;
defaultTitle?: string;
}) => { }) => {
const title = useRef(notebook?.title); const title = useRef(notebook?.title || defaultTitle);
const description = useRef(notebook?.description); const description = useRef(notebook?.description);
const titleInput = useRef<TextInput>(null); const titleInput = useRef<TextInput>(null);
const descriptionInput = useRef<TextInput>(null); const descriptionInput = useRef<TextInput>(null);
@@ -139,7 +141,7 @@ export const AddNotebookSheet = ({
}} }}
returnKeyLabel="Next" returnKeyLabel="Next"
returnKeyType="next" returnKeyType="next"
defaultValue={notebook ? notebook.title : ""} defaultValue={notebook ? notebook.title : title.current}
/> />
<Input <Input
@@ -174,7 +176,8 @@ AddNotebookSheet.present = (
parentNotebook?: Notebook, parentNotebook?: Notebook,
context?: string, context?: string,
onClose?: (didAddNotebook: boolean) => void, onClose?: (didAddNotebook: boolean) => void,
showMoveNotesOnComplete = true showMoveNotesOnComplete = true,
defaultTitle?: string
) => { ) => {
presentSheet({ presentSheet({
context: context, context: context,
@@ -187,6 +190,7 @@ AddNotebookSheet.present = (
onClose?.(didAddNotebook); onClose?.(didAddNotebook);
}} }}
showMoveNotesOnComplete={showMoveNotesOnComplete || false} showMoveNotesOnComplete={showMoveNotesOnComplete || false}
defaultTitle={defaultTitle}
/> />
) )
}); });

View File

@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Note } from "@notesnook/core"; import { Note } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback, useEffect } from "react"; import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
} from "react";
import { import {
ActivityIndicator, ActivityIndicator,
Keyboard, Keyboard,
@@ -47,6 +53,8 @@ import Paragraph from "../../ui/typography/paragraph";
import { NotebookItem } from "./notebook-item"; import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store"; import { useNotebookItemSelectionStore } from "./store";
import { AddNotebookSheet } from "../add-notebook"; import { AddNotebookSheet } from "../add-notebook";
import Input from "../../ui/input";
import { presentDialog } from "../../dialog/functions";
async function updateInitialSelectionState(items: string[]) { async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations const relations = await db.relations
@@ -93,7 +101,11 @@ const MoveNoteSheet = ({
actionSheetRef: RefObject<ActionSheetRef>; actionSheetRef: RefObject<ActionSheetRef>;
}) => { }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const [notebooks, loading] = useNotebooks(); const [rootNotebooks, loading] = useNotebooks();
const searchQuery = useRef("");
const searchTimer = useRef<NodeJS.Timeout>();
const [notebooks, setNotebooks] = useState(rootNotebooks);
const dimensions = useSettingStore((state) => state.dimensions); const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore( const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList (state) => state.selectedItemsList
@@ -103,6 +115,12 @@ const MoveNoteSheet = ({
(state) => state.multiSelect (state) => state.multiSelect
); );
useEffect(() => {
if (!loading) {
setNotebooks(rootNotebooks);
}
}, [loading, rootNotebooks]);
useEffect(() => { useEffect(() => {
const items = note ? [note.id] : selectedItemsList; const items = note ? [note.id] : selectedItemsList;
updateInitialSelectionState(items); updateInitialSelectionState(items);
@@ -199,7 +217,6 @@ const MoveNoteSheet = ({
{hasSelected() ? ( {hasSelected() ? (
<IconButton <IconButton
name="restore" name="restore"
type="secondaryAccented"
color={colors.primary.icon} color={colors.primary.icon}
onPress={() => { onPress={() => {
const items = note ? [note.id] : selectedItemsList; const items = note ? [note.id] : selectedItemsList;
@@ -239,33 +256,48 @@ const MoveNoteSheet = ({
style={{ style={{
width: "100%" width: "100%"
}} }}
keyboardShouldPersistTaps="handled"
ListHeaderComponent={ ListHeaderComponent={
<View <View
style={{ style={{
paddingHorizontal: 12, paddingHorizontal: 12,
width: "100%" width: "100%",
paddingTop: 12
}} }}
> >
<Button <Input
title="Add new notebook" placeholder="Search notebooks...."
style={{ button={{
alignSelf: "flex-start", icon: "plus",
paddingHorizontal: 12, onPress: () => {
justifyContent: "space-between" AddNotebookSheet.present(
undefined,
undefined,
"link-notebooks",
undefined,
false,
searchQuery.current
);
},
color: colors.primary.icon
}} }}
onPress={() => { onChangeText={(value) => {
AddNotebookSheet.present( searchQuery.current = value;
undefined, if (!searchQuery.current) {
undefined, setNotebooks(rootNotebooks);
"link-notebooks", return;
undefined, }
false searchTimer.current = setTimeout(() => {
); db.lookup
.notebooks(searchQuery.current)
.sorted()
.then((result) => {
if (searchQuery.current === value) {
setNotebooks(result);
}
});
}, 300);
}} }}
icon="plus"
iconPosition="right"
type="secondaryAccented"
width="100%"
/> />
</View> </View>
} }

View File

@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { Fragment, useState } from "react"; import React, { Fragment, useState } from "react";
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native"; import {
ActivityIndicator,
Linking,
Platform,
StyleSheet,
View
} from "react-native";
import FileViewer from "react-native-file-viewer"; import FileViewer from "react-native-file-viewer";
import Share from "react-native-share"; import Share from "react-native-share";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
@@ -29,14 +35,12 @@ import { requestInAppReview } from "../../../services/app-review";
import { import {
PresentSheetOptions, PresentSheetOptions,
ToastManager, ToastManager,
eSendEvent,
presentSheet presentSheet
} from "../../../services/event-manager"; } from "../../../services/event-manager";
import Exporter from "../../../services/exporter"; import Exporter from "../../../services/exporter";
import PremiumService from "../../../services/premium"; import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store"; import { useUserStore } from "../../../stores/use-user-store";
import { getElevationStyle } from "../../../utils/elevation"; import { getElevationStyle } from "../../../utils/elevation";
import { eCloseSheet } from "../../../utils/events";
import { SIZE, ph, pv } from "../../../utils/size"; import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time"; import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog"; import { Dialog } from "../../dialog";
@@ -65,6 +69,7 @@ const ExportNotesSheet = ({
filePath: string; filePath: string;
name: string; name: string;
type: string; type: string;
fileDir: string;
} }
| undefined | undefined
>(); >();
@@ -295,7 +300,9 @@ const ExportNotesSheet = ({
successfully as {result?.fileName} successfully as {result?.fileName}
</Paragraph> </Paragraph>
<Button <Button
title="Open" title={
Platform.OS === "android" ? "Open file location" : "Open"
}
type="accent" type="accent"
width={250} width={250}
fontSize={SIZE.md} fontSize={SIZE.md}
@@ -305,20 +312,23 @@ const ExportNotesSheet = ({
}} }}
onPress={async () => { onPress={async () => {
if (!result?.filePath) return; if (!result?.filePath) return;
eSendEvent(eCloseSheet); if (Platform.OS === "android") {
await sleep(500); Linking.openURL(result.fileDir).catch((e) => {
FileViewer.open(result?.filePath, { ToastManager.error(e as Error);
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
console.log(e);
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
type: "success",
context: "local"
}); });
}); } else {
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
type: "success",
context: "local"
});
});
}
}} }}
/> />
<Button <Button
@@ -331,10 +341,10 @@ const ExportNotesSheet = ({
borderRadius: 100 borderRadius: 100
}} }}
onPress={async () => { onPress={async () => {
if (!result?.filePath) return; if (!result) return;
if (Platform.OS === "ios") { if (Platform.OS === "ios") {
Share.open({ Share.open({
url: result.filePath url: result?.fileDir + result.fileName
}).catch(console.log); }).catch(console.log);
} else { } else {
FileViewer.open(result.filePath, { FileViewer.open(result.filePath, {
@@ -347,7 +357,7 @@ const ExportNotesSheet = ({
/> />
<Button <Button
title="Export in another format" title="Export in another format"
type="secondaryAccented" type="inverted"
width={250} width={250}
fontSize={SIZE.md} fontSize={SIZE.md}
style={{ style={{

View File

@@ -18,35 +18,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Debug } from "@notesnook/core/dist/api/debug"; import { Debug } from "@notesnook/core/dist/api/debug";
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { Linking, Platform, Text, TextInput, View } from "react-native"; import { Linking, Platform, Text, TextInput, View } from "react-native";
import { getVersion } from "react-native-device-info"; import { getVersion } from "react-native-device-info";
import { useStoredRef } from "../../../hooks/use-stored-ref"; import { useStoredRef } from "../../../hooks/use-stored-ref";
import { ToastManager, eSendEvent } from "../../../services/event-manager"; import { ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium"; import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store"; import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { openLinkInBrowser } from "../../../utils/functions"; import { openLinkInBrowser } from "../../../utils/functions";
import { SIZE } from "../../../utils/size"; import { SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import DialogHeader from "../../dialog/dialog-header"; import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button"; import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator"; import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => { export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const body = useStoredRef("issueBody");
const body = useStoredRef("issueBody", defaultBody);
const title = useStoredRef("issueTitle", defaultTitle); const title = useStoredRef("issueTitle", defaultTitle);
const [done, setDone] = useState(false);
const user = useUserStore((state) => state.user); const user = useUserStore((state) => state.user);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const bodyRef = useRef(); const bodyRef = useRef();
const initialLayout = useRef(false); const initialLayout = useRef(false);
const issueUrl = useRef();
const onPress = async () => { const onPress = async () => {
if (loading) return; if (loading) return;
@@ -56,57 +54,24 @@ export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
try { try {
setLoading(true); setLoading(true);
let issue_url = await Debug.report({ issueUrl.current = await Debug.report({
title: title.current, title: title.current,
body: body:
body.current + body.current +
`\n${defaultBody || ""}` +
`\n_______________ `\n_______________
**Device information:** **Device information:**
App version: ${getVersion()} App version: ${getVersion()}
Platform: ${Platform.OS} Platform: ${Platform.OS}
Model: ${Platform.constants.Brand || ""}-${Platform.constants.Model || ""}-${ Device: ${getBrand() || ""}-${getModel() || ""}-${getSystemVersion() || ""}
Platform.constants.Version || ""
}
Pro: ${PremiumService.get()} Pro: ${PremiumService.get()}
Logged in: ${user ? "yes" : "no"}`, Logged in: ${user ? "yes" : "no"}`,
userId: user?.id userId: user?.id
}); });
setLoading(false); setLoading(false);
eSendEvent(eCloseSheet);
body.reset(); body.reset();
title.reset(); title.reset();
await sleep(300); setDone(true);
presentDialog({
title: "Issue reported",
paragraph: (
<Text>
You can track your issue at{" "}
<Text
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
onPress={() => {
Linking.openURL(issue_url);
}}
>
{issue_url}.
</Text>{" "}
Please note that we will respond to your issue on the given link. We
recommend that you save it.
</Text>
),
positiveText: "Copy link",
positivePress: () => {
Clipboard.setString(issue_url);
ToastManager.show({
heading: "Issue url copied!",
type: "success",
context: "global"
});
},
negativeText: "Close"
});
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);
ToastManager.show({ ToastManager.show({
@@ -124,125 +89,177 @@ Logged in: ${user ? "yes" : "no"}`,
width: "100%" width: "100%"
}} }}
> >
<DialogHeader {done ? (
title={issueTitle || "Report issue"} <>
paragraph={ <View
issueTitle style={{
? "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap." height: 250,
: "Let us know if you have faced any issue/bug while using Notesnook." justifyContent: "center",
} alignItems: "center",
/> gap: 10
}}
>
<Heading>Issue submitted</Heading>
<Paragraph
style={{
textAlign: "center"
}}
selectable={true}
>
You can track your issue at{" "}
<Paragraph
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
onPress={() => {
Linking.openURL(issueUrl.current);
}}
>
{issueUrl.current}
</Paragraph>
. Please note that we will respond to your issue on the given
link. We recommend that you save it.
</Paragraph>
<Seperator half /> <Button
title="Open issue"
<TextInput onPress={() => {
placeholder="Title" Linking.openURL(issueUrl.current);
onChangeText={(v) => (title.current = v)} }}
defaultValue={title.current} type="accent"
style={{ width="100%"
borderWidth: 1, />
borderColor: colors.primary.border, </View>
borderRadius: 5, </>
padding: 12, ) : (
fontFamily: "OpenSans-Regular", <>
marginBottom: 10, <DialogHeader
fontSize: SIZE.md, title={issueTitle || "Report issue"}
color: colors.primary.heading paragraph={
}} issueTitle
placeholderTextColor={colors.primary.placeholder} ? "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
/> : "Let us know if you have faced any issue/bug while using Notesnook."
<TextInput
ref={bodyRef}
placeholder={`Tell us more about the issue you are facing.
For example:
- Steps to reproduce the issue
- Things you have tried etc.`}
multiline
numberOfLines={5}
textAlignVertical="top"
onChangeText={(v) => (body.current = v)}
onLayout={() => {
if (initialLayout.current) return;
initialLayout.current = true;
if (body.current) {
bodyRef.current?.setNativeProps({
text: body.current,
selection: {
start: 0,
end: 0
}
});
}
}}
style={{
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: 5,
padding: 12,
fontFamily: "OpenSans-Regular",
maxHeight: 200,
fontSize: SIZE.sm,
marginBottom: 2.5,
color: colors.primary.paragraph
}}
placeholderTextColor={colors.primary.placeholder}
/>
<Paragraph
size={SIZE.xs}
color={colors.secondary.paragraph}
>{`App version: ${getVersion()} Platform: ${Platform.OS} Model: ${
Platform.constants.Brand
}-${Platform.constants.Model}-${Platform.constants.Version}`}</Paragraph>
<Seperator />
<Button
onPress={onPress}
title={loading ? null : "Submit"}
loading={loading}
width="100%"
type="accent"
/>
<Paragraph
color={colors.secondary.paragraph}
size={SIZE.xs}
style={{
marginTop: 10,
textAlign: "center"
}}
>
The information above will be publically available at{" "}
<Text
onPress={() => {
Linking.openURL("https://github.com/streetwriters/notesnook");
}}
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
>
github.com/streetwriters/notesnook.
</Text>{" "}
If you want to ask something in general or need some assistance, we
would suggest that you{" "}
<Text
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
onPress={async () => {
try {
await openLinkInBrowser("https://discord.gg/zQBK97EE22", colors);
} catch (e) {
console.error(e);
} }
}} />
>
join our community on Discord. <Seperator half />
</Text>
</Paragraph> <TextInput
placeholder="Title"
onChangeText={(v) => (title.current = v)}
defaultValue={title.current}
style={{
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: 5,
padding: 12,
fontFamily: "OpenSans-Regular",
marginBottom: 10,
fontSize: SIZE.md,
color: colors.primary.heading
}}
placeholderTextColor={colors.primary.placeholder}
/>
<TextInput
ref={bodyRef}
multiline
placeholder={`Tell us more about the issue you are facing.
For example:
- What were you trying to do in the app?
- What did you expect to happen?
- Steps to reproduce the issue
- Things you have tried etc.`}
numberOfLines={5}
textAlignVertical="top"
onChangeText={(v) => (body.current = v)}
onLayout={() => {
if (initialLayout.current) return;
initialLayout.current = true;
if (body.current) {
bodyRef.current?.setNativeProps({
text: body.current,
selection: {
start: 0,
end: 0
}
});
}
}}
style={{
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: 5,
padding: 12,
fontFamily: "OpenSans-Regular",
maxHeight: 200,
fontSize: SIZE.sm,
marginBottom: 2.5,
color: colors.primary.paragraph
}}
placeholderTextColor={colors.primary.placeholder}
/>
<Paragraph
size={SIZE.xs}
color={colors.secondary.paragraph}
>{`App version: ${getVersion()} Platform: ${
Platform.OS
} Model: ${getBrand()}-${getModel()}-${getSystemVersion()}`}</Paragraph>
<Seperator />
<Button
onPress={onPress}
title={loading ? null : "Submit"}
loading={loading}
width="100%"
type="accent"
/>
<Paragraph
color={colors.secondary.paragraph}
size={SIZE.xs}
style={{
marginTop: 10,
textAlign: "center"
}}
>
The information above will be publically available at{" "}
<Text
onPress={() => {
Linking.openURL("https://github.com/streetwriters/notesnook");
}}
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
>
github.com/streetwriters/notesnook.
</Text>{" "}
If you want to ask something in general or need some assistance, we
would suggest that you{" "}
<Text
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
onPress={async () => {
try {
await openLinkInBrowser(
"https://discord.gg/zQBK97EE22",
colors
);
} catch (e) {
console.error(e);
}
}}
>
join our community on Discord.
</Text>
</Paragraph>
</>
)}
</View> </View>
); );
}; };

View File

@@ -35,7 +35,10 @@ import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable"; import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link"; import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
import { editorController } from "../../../screens/editor/tiptap/utils"; import {
EditorEvents,
editorController
} from "../../../screens/editor/tiptap/utils";
const ListNoteItem = ({ const ListNoteItem = ({
id, id,
@@ -190,13 +193,13 @@ export default function LinkNote(props: {
} }
: undefined : undefined
); );
editorController.current.commands.createInternalLink( editorController.current?.postMessage(EditorEvents.resolve, {
{ data: {
href: link, href: link,
title: selectedNote.title title: selectedNote.title
}, },
props.resolverId resolverId: props.resolverId
); });
}; };
const onSelectNote = async (note: Note) => { const onSelectNote = async (note: Note) => {
@@ -219,7 +222,7 @@ export default function LinkNote(props: {
<View <View
style={{ style={{
paddingHorizontal: 12, paddingHorizontal: 12,
minHeight: 400, minHeight: "100%",
maxHeight: "100%" maxHeight: "100%"
}} }}
> >
@@ -311,6 +314,7 @@ export default function LinkNote(props: {
style={{ style={{
marginTop: 10 marginTop: 10
}} }}
keyboardShouldPersistTaps="handled"
windowSize={3} windowSize={3}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
data={nodes} data={nodes}
@@ -324,6 +328,7 @@ export default function LinkNote(props: {
onSelectNote={onSelectNote} onSelectNote={onSelectNote}
/> />
)} )}
keyboardShouldPersistTaps="handled"
style={{ style={{
marginTop: 10 marginTop: 10
}} }}

View File

@@ -42,10 +42,7 @@ import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { Issue } from "../github/issue"; import { Issue } from "../github/issue";
export const makeError = ( export const makeError = (stack: string, component: string) => `
stack: string,
component: string
) => `Please let us know what happened. What steps we can take to reproduce the issue here.
_______________________________ _______________________________
Stacktrace: In ${component}::${stack}`; Stacktrace: In ${component}::${stack}`;
@@ -96,9 +93,12 @@ export default function Migrate() {
}); });
setLoading(true); setLoading(true);
await sleep(1); await sleep(1);
const { error } = await BackupService.run(false, "local"); const { error, report } = await BackupService.run(false, "local");
if (error) { if (error) {
ToastManager.error(error, "Backup failed"); ToastManager.error(error, "Backup failed");
if (report) {
reportError(error);
}
setLoading(false); setLoading(false);
return; return;
} }

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Notebook, VirtualizedGrouping } from "@notesnook/core"; import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { useCallback } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native"; import { Text, View } from "react-native";
import { FlatList } from "react-native-actions-sheet"; import { FlatList } from "react-native-actions-sheet";
import create from "zustand"; import create from "zustand";
@@ -72,6 +72,20 @@ export const MoveNotebookSheet = ({
}) => { }) => {
const [notebooks] = useNotebooks(); const [notebooks] = useNotebooks();
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const [moveToTop, setMoveToTop] = useState(false);
useEffect(() => {
(async () => {
for (const notebook of selectedNotebooks) {
const root = await findRootNotebookId(notebook.id);
if (root !== notebook.id) {
setMoveToTop(true);
return;
}
}
})();
}, [selectedNotebooks]);
const renderItem = useCallback( const renderItem = useCallback(
({ index }: { index: number }) => { ({ index }: { index: number }) => {
return ( return (
@@ -93,6 +107,7 @@ export const MoveNotebookSheet = ({
: selectedNotebooks[0].title : selectedNotebooks[0].title
} to ${selectedNotebook.title}?`, } to ${selectedNotebook.title}?`,
positiveText: "Move", positiveText: "Move",
context: "move-notebook",
positivePress: async () => { positivePress: async () => {
for (const notebook of selectedNotebooks) { for (const notebook of selectedNotebooks) {
const parent = await getParentNotebookId(notebook.id); const parent = await getParentNotebookId(notebook.id);
@@ -157,36 +172,38 @@ export const MoveNotebookSheet = ({
paddingHorizontal: 12 paddingHorizontal: 12
}} }}
> >
<Button {moveToTop ? (
title="Move to top" <Button
style={{ title="Move to top"
alignSelf: "flex-start", style={{
width: "100%", alignSelf: "flex-start",
justifyContent: "space-between" width: "100%",
}} justifyContent: "space-between"
icon="arrow-up-bold" }}
iconPosition="right" icon="arrow-up-bold"
type="secondaryAccented" iconPosition="right"
onPress={async () => { type="secondaryAccented"
for (const notebook of selectedNotebooks) { onPress={async () => {
const parent = await getParentNotebookId(notebook.id); for (const notebook of selectedNotebooks) {
const root = await findRootNotebookId(notebook.id); const parent = await getParentNotebookId(notebook.id);
if (root !== notebook.id) { const root = await findRootNotebookId(notebook.id);
await db.relations.unlink( if (root !== notebook.id) {
{ await db.relations.unlink(
type: "notebook", {
id: parent type: "notebook",
}, id: parent
notebook },
); notebook
eSendEvent(eOnNotebookUpdated, parent); );
eSendEvent(eOnNotebookUpdated, notebook.id); eSendEvent(eOnNotebookUpdated, parent);
eSendEvent(eOnNotebookUpdated, notebook.id);
}
} }
} useNotebookStore.getState().refresh();
useNotebookStore.getState().refresh(); close?.();
close?.(); }}
}} />
/> ) : null}
</View> </View>
} }
ListEmptyComponent={ ListEmptyComponent={

View File

@@ -19,13 +19,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Notebook, VirtualizedGrouping } from "@notesnook/core"; import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { RefreshControl, View, useWindowDimensions } from "react-native"; import {
Platform,
RefreshControl,
View,
useWindowDimensions
} from "react-native";
import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet"; import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList"; import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Config from "react-native-config"; import Config from "react-native-config";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create from "zustand"; import create from "zustand";
import { notesnook } from "../../../../e2e/test.ids"; import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { MMKV } from "../../../common/database/mmkv"; import { MMKV } from "../../../common/database/mmkv";
import { useNotebook } from "../../../hooks/use-notebook"; import { useNotebook } from "../../../hooks/use-notebook";
import NotebookScreen from "../../../screens/notebook"; import NotebookScreen from "../../../screens/notebook";
@@ -43,9 +49,8 @@ import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable"; import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook"; import { AddNotebookSheet } from "../add-notebook";
import Sort from "../sort";
import { MoveNotebookSheet } from "../move-notebook"; import { MoveNotebookSheet } from "../move-notebook";
import { db } from "../../../common/database"; import Sort from "../sort";
const useItemSelectionStore = createItemSelectionStore(true, false); const useItemSelectionStore = createItemSelectionStore(true, false);
@@ -63,7 +68,8 @@ class NotebookSheetConfig {
} }
static get(item: ConfigItem) { static get(item: ConfigItem) {
return MMKV.getInt(NotebookSheetConfig.makeId(item)) || 0; const value = MMKV.getInt(NotebookSheetConfig.makeId(item));
return typeof value === "number" ? value : 0;
} }
static set(item: ConfigItem, index = 0) { static set(item: ConfigItem, index = 0) {
@@ -126,11 +132,6 @@ export const NotebookSheet = () => {
if (!focusedRouteId) return; if (!focusedRouteId) return;
const nextRoot = await findRootNotebookId(focusedRouteId); const nextRoot = await findRootNotebookId(focusedRouteId);
if (nextRoot !== currentItem.current) { if (nextRoot !== currentItem.current) {
console.log(
"NotebookSheet.useEffect.canShow",
"Root changed to",
nextRoot
);
useItemSelectionStore.setState({ useItemSelectionStore.setState({
enabled: false, enabled: false,
selection: {} selection: {}
@@ -145,11 +146,8 @@ export const NotebookSheet = () => {
if (ref.current?.isOpen()) { if (ref.current?.isOpen()) {
ref.current?.snapToIndex(snapPoint); ref.current?.snapToIndex(snapPoint);
} else { } else {
setTimeout(() => { ref.current?.show(snapPoint);
ref.current?.show(snapPoint);
}, 150);
} }
console.log("NotebookSheet.useEffect.didShow", focusedRouteId);
setRoot(nextRoot); setRoot(nextRoot);
onRequestUpdate(); onRequestUpdate();
}); });
@@ -162,7 +160,7 @@ export const NotebookSheet = () => {
ref.current?.hide(); ref.current?.hide();
} }
} }
}, [canShow, onRequestUpdate, focusedRouteId]); }, [canShow, focusedRouteId]);
return ( return (
<ActionSheet <ActionSheet
@@ -198,7 +196,11 @@ export const NotebookSheet = () => {
backgroundColor: colors.secondary.background backgroundColor: colors.secondary.background
}} }}
keyboardHandlerEnabled={false} keyboardHandlerEnabled={false}
snapPoints={Config.isTesting === "true" ? [100] : [20, 100]} snapPoints={
Config.isTesting === "true"
? [100]
: [Platform.OS === "android" ? 15 : 10, 100]
}
initialSnapIndex={1} initialSnapIndex={1}
backgroundInteractionEnabled backgroundInteractionEnabled
gestureEnabled gestureEnabled
@@ -237,7 +239,7 @@ export const NotebookSheet = () => {
> >
<Icon <Icon
name="notebook-plus" name="notebook-plus"
color={colors.primary.accent} color={colors.primary.icon}
size={SIZE.xxl} size={SIZE.xxl}
/> />
</View> </View>

View File

@@ -18,7 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useRef, useState } from "react"; import React, { RefObject, useRef, useState } from "react";
import { Platform, TextInput, View } from "react-native"; import {
Platform,
TextInput,
View,
ScrollView as RNScrollView
} from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet"; import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import DateTimePickerModal from "react-native-modal-datetime-picker"; import DateTimePickerModal from "react-native-modal-datetime-picker";
import { import {
@@ -364,7 +369,7 @@ export default function ReminderSheet({
))} ))}
</View> </View>
<ScrollView showsHorizontalScrollIndicator={false} horizontal> <RNScrollView showsHorizontalScrollIndicator={false} horizontal>
{recurringMode === RecurringModes.Daily || {recurringMode === RecurringModes.Daily ||
recurringMode === RecurringModes.Year recurringMode === RecurringModes.Year
? null ? null
@@ -425,7 +430,7 @@ export default function ReminderSheet({
}} }}
/> />
))} ))}
</ScrollView> </RNScrollView>
</View> </View>
) : null} ) : null}

View File

@@ -285,9 +285,9 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder); const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) { // if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
throw new Error("Backup file is invalid"); // throw new Error("Backup file is invalid");
} // }
await db.transaction(async () => { await db.transaction(async () => {
let password; let password;

View File

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

View File

@@ -21,15 +21,11 @@ import { useThemeColors } from "@notesnook/theme";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { DraxProvider, DraxScrollView } from "react-native-drax"; import { DraxProvider, DraxScrollView } from "react-native-drax";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database"; import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager"; import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store"; import { useMenuStore } from "../../stores/use-menu-store";
import { useSettingStore } from "../../stores/use-setting-store"; import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { useUserStore } from "../../stores/use-user-store"; import { useUserStore } from "../../stores/use-user-store";
import { SUBSCRIPTION_STATUS } from "../../utils/constants"; import { SUBSCRIPTION_STATUS } from "../../utils/constants";
import { eOpenPremiumDialog } from "../../utils/events"; import { eOpenPremiumDialog } from "../../utils/events";
@@ -59,26 +55,6 @@ export const SideMenu = React.memo(
const introCompleted = useSettingStore( const introCompleted = useSettingStore(
(state) => state.settings.introCompleted (state) => state.settings.introCompleted
); );
const BottomItemsList = [
{
name: isDark ? "Day" : "Night",
icon: "theme-light-dark",
func: () => {
useThemeStore.getState().setColorScheme();
},
switch: true,
on: !!isDark,
close: false
},
{
name: "Settings",
icon: "cog-outline",
close: true,
func: () => {
Navigation.navigate("Settings");
}
}
];
const pro = { const pro = {
name: "Notesnook Pro", name: "Notesnook Pro",
@@ -121,7 +97,7 @@ export const SideMenu = React.memo(
<PinnedSection /> <PinnedSection />
</> </>
), ),
[] [order, hiddensItems]
); );
return !isAppLoading && introCompleted ? ( return !isAppLoading && introCompleted ? (

View File

@@ -20,29 +20,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import { useNetInfo } from "@react-native-community/netinfo"; import { useNetInfo } from "@react-native-community/netinfo";
import React from "react"; import React from "react";
import { import { ActivityIndicator, Image, Platform, View } from "react-native";
ActivityIndicator,
Image,
Platform,
TouchableOpacity,
View
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useSyncProgress from "../../hooks/use-sync-progress"; import useSyncProgress from "../../hooks/use-sync-progress";
import { eSendEvent } from "../../services/event-manager"; import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync"; import Sync from "../../services/sync";
import { useThemeStore } from "../../stores/use-theme-store";
import { SyncStatus, useUserStore } from "../../stores/use-user-store"; import { SyncStatus, useUserStore } from "../../stores/use-user-store";
import { eOpenLoginDialog } from "../../utils/events"; import { eOpenLoginDialog } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs"; import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size"; import { SIZE } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable"; import { Pressable } from "../ui/pressable";
import { TimeSince } from "../ui/time-since"; import { TimeSince } from "../ui/time-since";
import Paragraph from "../ui/typography/paragraph"; import Paragraph from "../ui/typography/paragraph";
import Navigation from "../../services/navigation";
export const UserStatus = () => { export const UserStatus = () => {
const { colors } = useThemeColors(); const { colors, isDark } = useThemeColors();
const user = useUserStore((state) => state.user); const user = useUserStore((state) => state.user);
const syncing = useUserStore((state) => state.syncing); const syncing = useUserStore((state) => state.syncing);
const lastSyncStatus = useUserStore((state) => state.lastSyncStatus); const lastSyncStatus = useUserStore((state) => state.lastSyncStatus);
@@ -97,7 +93,16 @@ export const UserStatus = () => {
borderRadius: 100 borderRadius: 100
}} }}
/> />
) : null} ) : (
<Icon
name="cog-outline"
size={SIZE.lg - 2}
color={colors.secondary.icon}
style={{
paddingLeft: 8
}}
/>
)}
<View <View
style={{ style={{
@@ -110,14 +115,8 @@ export const UserStatus = () => {
size={SIZE.sm} size={SIZE.sm}
color={colors.primary.heading} color={colors.primary.heading}
> >
{!user {!user || !userProfile?.fullName
? "Login to sync your notes." ? "Settings"
: lastSyncStatus === SyncStatus.Failed
? "Sync failed, tap to retry"
: syncing
? `Syncing ${progress ? `(${progress.current})` : ""}`
: !userProfile?.fullName
? "Tap to sync"
: userProfile.fullName} : userProfile.fullName}
</Paragraph> </Paragraph>
@@ -132,16 +131,20 @@ export const UserStatus = () => {
"Not logged in" "Not logged in"
) : lastSynced && lastSynced !== "Never" ? ( ) : lastSynced && lastSynced !== "Never" ? (
<> <>
{lastSyncStatus === SyncStatus.Failed {syncing
? `Syncing ${progress ? `(${progress.current})` : ""}`
: lastSyncStatus === SyncStatus.Failed
? "Sync failed" ? "Sync failed"
: "Synced"}{" "} : "Synced"}{" "}
<TimeSince {!syncing ? (
style={{ <TimeSince
fontSize: SIZE.xs, style={{
color: colors.secondary.paragraph fontSize: SIZE.xs,
}} color: colors.secondary.paragraph
time={lastSynced} }}
/> time={lastSynced}
/>
) : null}
{isOffline ? " (offline)" : ""} {isOffline ? " (offline)" : ""}
</> </>
) : ( ) : (
@@ -149,7 +152,7 @@ export const UserStatus = () => {
)}{" "} )}{" "}
<Icon <Icon
name="checkbox-blank-circle" name="checkbox-blank-circle"
size={11} size={9}
allowFontScaling allowFontScaling
color={ color={
!user || lastSyncStatus === SyncStatus.Failed !user || lastSyncStatus === SyncStatus.Failed
@@ -162,56 +165,80 @@ export const UserStatus = () => {
</Paragraph> </Paragraph>
</View> </View>
<Pressable <View
style={{ style={{
borderRadius: 100, flexDirection: "row",
width: 40, gap: 0
height: 40
}}
hitSlop={{
top: 10,
bottom: 10,
left: 20
}}
onPress={() => {
if (user) {
Sync.run();
} else {
tabBarRef.current?.closeDrawer();
eSendEvent(eOpenLoginDialog);
}
}} }}
> >
{user ? ( <IconButton
syncing ? ( hitSlop={{
<ActivityIndicator top: 10,
color={colors.primary.accent} bottom: 10
size={SIZE.xl} }}
/> onPress={() => {
) : lastSyncStatus === SyncStatus.Failed ? ( useThemeStore.getState().setColorScheme();
<Icon }}
color={colors.error.icon} name="theme-light-dark"
name="sync-alert" color={isDark ? colors.primary.accent : colors.primary.icon}
size={SIZE.lg} size={SIZE.lg}
allowFontScaling style={{
/> borderRadius: 100,
width: 40,
height: 40
}}
/>
<Pressable
style={{
borderRadius: 100,
width: 40,
height: 40
}}
hitSlop={{
top: 10,
bottom: 10
}}
onPress={() => {
if (user) {
Sync.run();
} else {
tabBarRef.current?.closeDrawer();
eSendEvent(eOpenLoginDialog);
}
}}
>
{user ? (
syncing ? (
<ActivityIndicator
color={colors.primary.accent}
size={SIZE.xl}
/>
) : lastSyncStatus === SyncStatus.Failed ? (
<Icon
color={colors.error.icon}
name="sync-alert"
size={SIZE.lg}
allowFontScaling
/>
) : (
<Icon
allowFontScaling
color={colors.primary.icon}
name="sync"
size={SIZE.lg}
/>
)
) : ( ) : (
<Icon <Icon
allowFontScaling allowFontScaling
color={colors.primary.accent} color={colors.primary.accent}
name="sync"
size={SIZE.lg} size={SIZE.lg}
name="login"
/> />
) )}
) : ( </Pressable>
<Icon </View>
allowFontScaling
color={colors.primary.accent}
size={SIZE.lg}
name="login"
/>
)}
</Pressable>
</Pressable> </Pressable>
</View> </View>
</View> </View>

View File

@@ -59,7 +59,7 @@ export interface TabsRef {
lock: () => boolean; lock: () => boolean;
openDrawer: (animated?: boolean) => void; openDrawer: (animated?: boolean) => void;
closeDrawer: (animated?: boolean) => void; closeDrawer: (animated?: boolean) => void;
page: number; page: () => number;
setScrollEnabled: () => true; setScrollEnabled: () => true;
isDrawerOpen: () => boolean; isDrawerOpen: () => boolean;
node: RefObject<Animated.View>; node: RefObject<Animated.View>;
@@ -239,7 +239,7 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
onDrawerStateChange(false); onDrawerStateChange(false);
isDrawerOpen.value = false; isDrawerOpen.value = false;
}, },
page: currentTab.value, page: () => currentTab.value,
setScrollEnabled: () => true, setScrollEnabled: () => true,
node: node node: node
}), }),

View File

@@ -25,7 +25,8 @@ import {
DimensionValue, DimensionValue,
TextStyle, TextStyle,
View, View,
ViewStyle ViewStyle,
useWindowDimensions
} from "react-native"; } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useUserStore } from "../../../stores/use-user-store"; import { useUserStore } from "../../../stores/use-user-store";
@@ -92,6 +93,9 @@ export const Button = ({
}); });
const textColor = buttonType?.text ? buttonType.text : text; const textColor = buttonType?.text ? buttonType.text : text;
const { fontScale } = useWindowDimensions();
const growFactor = 1 + (fontScale - 1) / 10;
const Component = bold ? Heading : Paragraph; const Component = bold ? Heading : Paragraph;
return ( return (
@@ -117,8 +121,11 @@ export const Button = ({
customOpacity={buttonType?.opacity} customOpacity={buttonType?.opacity}
customAlpha={buttonType?.alpha} customAlpha={buttonType?.alpha}
style={{ style={{
height: height, height: typeof height === "number" ? height * growFactor : height,
width: (width as DimensionValue) || undefined, width:
typeof width === "number"
? width * growFactor
: (width as DimensionValue) || undefined,
paddingHorizontal: 12, paddingHorizontal: 12,
borderRadius: 5, borderRadius: 5,
alignSelf: "center", alignSelf: "center",

View File

@@ -21,11 +21,12 @@ import { VariantsWithStaticColors, useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback } from "react"; import React, { RefObject, useCallback } from "react";
import { import {
ColorValue, ColorValue,
PressableStateCallbackType,
Pressable as RNPressable, Pressable as RNPressable,
PressableProps as RNPressableProps, PressableProps as RNPressableProps,
PressableStateCallbackType,
View, View,
ViewStyle ViewStyle,
useWindowDimensions
} from "react-native"; } from "react-native";
import { import {
RGB_Linear_Shade, RGB_Linear_Shade,
@@ -256,6 +257,8 @@ export const Pressable = ({
? 1 ? 1
: colorOpacity; : colorOpacity;
const alpha = customAlpha ? customAlpha : isDark ? 0.03 : -0.03; const alpha = customAlpha ? customAlpha : isDark ? 0.03 : -0.03;
const { fontScale } = useWindowDimensions();
const growFactor = 1 + (fontScale - 1) / 8;
const getStyle = useCallback( const getStyle = useCallback(
({ pressed }: PressableStateCallbackType): ViewStyle | ViewStyle[] => [ ({ pressed }: PressableStateCallbackType): ViewStyle | ViewStyle[] => [
@@ -276,7 +279,13 @@ export const Pressable = ({
: borderColor || "transparent", : borderColor || "transparent",
borderWidth: borderWidth borderWidth: borderWidth
}, },
style style,
{
height:
typeof style.height === "number"
? style.height * growFactor
: style.height
}
], ],
[ [
alpha, alpha,
@@ -288,7 +297,8 @@ export const Pressable = ({
borderSelectedColor, borderSelectedColor,
borderColor, borderColor,
borderWidth, borderWidth,
style style,
growFactor
] ]
); );

View File

@@ -19,35 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature"; import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [ export const features: FeatureType[] = [];
{
title: "Bi-directional note linking",
body: "Now you can link notes to each other in both directions."
},
{
title: "Tabs",
body: "Tabs allow you to have multiple notes open at the same time. You can switch between them easily."
},
{
title: "Nested notebooks",
body: "You can now create notebooks inside other notebooks."
},
{
title: "At-rest encryption",
body: "Your notes are now encrypted when stored on the device."
},
{
title: "Material You themed icon",
body: "The app now has a Material You themed icon that changes color based on the wallpaper.",
platform: "android"
},
{
title: "New note quick settings tile",
body: "You can now add a new note tile to quickly create notes from quick settings panel.",
platform: "android"
},
{
title: "And so much more...",
body: "V3 packs so much that it's hard to put everything here, check out our blog at Notesnook V3 is here, and it's packed with new features and improvements such as bi-directional note linking, better syncing, at-rest encryption, nested notebooks, editor tabs, better app lock and so much more. https://blog.notesnook.com/introducing-notesnook-v3."
}
];

View File

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

View File

@@ -85,8 +85,8 @@ import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors"; import { updateStatusBarColor } from "../utils/colors";
import { BETA } from "../utils/constants"; import { BETA } from "../utils/constants";
import { import {
eClearEditor,
eCloseSheet, eCloseSheet,
eEditorReset,
eLoginSessionExpired, eLoginSessionExpired,
eOnLoadNote, eOnLoadNote,
eOpenAnnouncementDialog, eOpenAnnouncementDialog,
@@ -97,6 +97,7 @@ import {
import { getGithubVersion } from "../utils/github-version"; import { getGithubVersion } from "../utils/github-version";
import { tabBarRef } from "../utils/global-refs"; import { tabBarRef } from "../utils/global-refs";
import { sleep } from "../utils/time"; import { sleep } from "../utils/time";
import { initializeLogger } from "../common/database/logger";
const onCheckSyncStatus = async (type: SyncStatusEvent) => { const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get(); const { disableSync, disableAutoSync } = SettingsService.get();
@@ -201,11 +202,13 @@ const onRequestPartialSync = async (
`onRequestPartialSync full:${full}, force:${force}, lastSyncTime:${lastSyncTime}` `onRequestPartialSync full:${full}, force:${force}, lastSyncTime:${lastSyncTime}`
); );
if (full || force) { await Sync.run(
await Sync.run("global", force, full, undefined, lastSyncTime); "global",
} else { force,
await Sync.run("global", false, false, undefined, lastSyncTime); full ? "full" : "send",
} undefined,
lastSyncTime
);
}; };
const onLogout = async (reason: string) => { const onLogout = async (reason: string) => {
@@ -234,7 +237,7 @@ async function checkForShareExtensionLaunchedInBackground() {
if (notesAddedFromIntent || shareExtensionOpened) { if (notesAddedFromIntent || shareExtensionOpened) {
const id = useTabStore.getState().getCurrentNoteId(); const id = useTabStore.getState().getCurrentNoteId();
const note = id && (await db.notes.note(id)); const note = id && (await db.notes.note(id));
eSendEvent("webview_reset"); eSendEvent(eEditorReset);
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1); if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened"); MMKV.removeItem("shareExtensionOpened");
} }
@@ -278,8 +281,108 @@ const onSubscriptionError = async (error: RNIap.PurchaseError) => {
const SodiumEventEmitter = new NativeEventEmitter(NativeModules.Sodium); const SodiumEventEmitter = new NativeEventEmitter(NativeModules.Sodium);
const doAppLoadActions = async () => {
if (SettingsService.get().sessionExpired) {
eSendEvent(eLoginSessionExpired);
return;
}
notifee.setBadgeCount(0);
if (!(await db.user.getUser())) {
setLoginMessage();
return;
}
await useMessageStore.getState().setAnnouncement();
if (NewFeature.present()) return;
if (await checkAppUpdateAvailable()) return;
if (await checkForRateAppRequest()) return;
if (await PremiumService.getRemainingTrialDaysStatus()) return;
if (SettingsService.get().introCompleted) {
useMessageStore.subscribe((state) => {
const dialogs = state.dialogs;
if (dialogs.length > 0) {
eSendEvent(eOpenAnnouncementDialog, dialogs[0]);
}
});
}
};
const checkAppUpdateAvailable = async () => {
if (__DEV__ || Config.isTesting === "true" || Config.FDROID_BUILD || BETA)
return;
try {
const version =
Config.GITHUB_RELEASE === "true"
? await getGithubVersion()
: await checkVersion();
if (!version || !version?.needsUpdate) return false;
setUpdateAvailableMessage(version);
return true;
} catch (e) {
return false;
}
};
const checkForRateAppRequest = async () => {
const rateApp = SettingsService.get().rateApp as number;
if (
rateApp &&
rateApp < Date.now() &&
!useMessageStore.getState().message?.visible
) {
setRateAppMessage();
return false;
}
return false;
};
const IsDatabaseMigrationRequired = () => {
if (!db.migrations.required() || useUserStore.getState().appLocked)
return false;
presentSheet({
component: <Migrate />,
onClose: () => {
if (!db.migrations.required()) {
initializeDatabase();
}
},
disableClosing: true
});
return true;
};
const initializeDatabase = async (password?: string) => {
if (useUserStore.getState().appLocked) return;
if (!db.isInitialized) {
RNBootSplash.hide({ fade: true });
DatabaseLogger.info("Initializing database");
try {
await setupDatabase(password);
await db.init();
} catch (e) {
DatabaseLogger.error(e as Error);
ToastManager.error(e as Error, "Error initializing database", "global");
}
}
if (IsDatabaseMigrationRequired()) return;
if (db.isInitialized) {
Notifications.setupReminders(true);
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(false);
}
useSettingStore.getState().setAppLoading(false);
DatabaseLogger.info("Database initialized");
}
Walkthrough.init();
};
export const useAppEvents = () => { export const useAppEvents = () => {
const loading = useSettingStore((state) => state.isAppLoading); const isAppLoading = useSettingStore((state) => state.isAppLoading);
const [setLastSynced, setUser, appLocked, syncing] = useUserStore((state) => [ const [setLastSynced, setUser, appLocked, syncing] = useUserStore((state) => [
state.setLastSynced, state.setLastSynced,
state.setUser, state.setUser,
@@ -310,7 +413,7 @@ export const useAppEvents = () => {
}, [setLastSynced]); }, [setLastSynced]);
useEffect(() => { useEffect(() => {
if (loading) return; if (isAppLoading) return;
let subscriptions: EventManagerSubscription[] = []; let subscriptions: EventManagerSubscription[] = [];
const eventManager = db.eventManager; const eventManager = db.eventManager;
@@ -325,7 +428,7 @@ export const useAppEvents = () => {
return () => { return () => {
subscriptions.forEach((sub) => sub?.unsubscribe?.()); subscriptions.forEach((sub) => sub?.unsubscribe?.());
}; };
}, [loading, onSyncComplete]); }, [isAppLoading, onSyncComplete]);
const subscribeToPurchaseListeners = useCallback(async () => { const subscribeToPurchaseListeners = useCallback(async () => {
if (Platform.OS === "android") { if (Platform.OS === "android") {
@@ -531,13 +634,15 @@ export const useAppEvents = () => {
} }
//@ts-ignore //@ts-ignore
globalThis["IS_SHARE_EXTENSION"] = false; globalThis["IS_SHARE_EXTENSION"] = false;
} else {
const id = useTabStore.getState().getCurrentNoteId(); if (
const note = id ? await db.notes.note(id) : undefined; SettingsService.getBackgroundEnterTime() + 60 * 1000 * 10 <
const locked = note && (await db.vaults.itemExists(note)); Date.now()
if (locked && SettingsService.canLockAppInBackground()) { ) {
eSendEvent(eClearEditor); // Reset the editor if the app has been in background for more than 10 minutes.
eSendEvent(eEditorReset);
} }
} else {
await saveEditorState(); await saveEditorState();
if ( if (
SettingsService.canLockAppInBackground() && SettingsService.canLockAppInBackground() &&
@@ -568,7 +673,7 @@ export const useAppEvents = () => {
}); });
} }
let sub: NativeEventSubscription; let sub: NativeEventSubscription;
if (!loading && !appLocked) { if (!isAppLoading && !appLocked) {
setTimeout(() => { setTimeout(() => {
sub = AppState.addEventListener("change", onAppStateChanged); sub = AppState.addEventListener("change", onAppStateChanged);
if ( if (
@@ -590,7 +695,7 @@ export const useAppEvents = () => {
sub?.remove(); sub?.remove();
unsubscribePurchaseListeners(); unsubscribePurchaseListeners();
}; };
}, [loading, appLocked, checkAutoBackup]); }, [isAppLoading, appLocked, checkAutoBackup]);
useEffect(() => { useEffect(() => {
if (!appLocked && !syncing && refValues.current.backupDidWait) { if (!appLocked && !syncing && refValues.current.backupDidWait) {
@@ -639,127 +744,22 @@ export const useAppEvents = () => {
} }
useEffect(() => { useEffect(() => {
if (!loading) { if (!isAppLoading) {
onUserUpdated(); onUserUpdated();
doAppLoadActions(); doAppLoadActions();
} }
}, [loading, onUserUpdated]); }, [isAppLoading, onUserUpdated]);
const initializeDatabase = useCallback(async (password?: string) => {
const IsDatabaseMigrationRequired = () => {
if (!db.migrations.required() || useUserStore.getState().appLocked)
return false;
presentSheet({
component: <Migrate />,
onClose: () => {
if (!db.migrations.required()) {
initializeDatabase();
}
},
disableClosing: true
});
return true;
};
if (useUserStore.getState().appLocked) return;
if (!db.isInitialized) {
RNBootSplash.hide({ fade: true });
DatabaseLogger.info("Initializing database");
try {
await setupDatabase(password);
await db.init();
} catch (e) {
DatabaseLogger.error(e as Error);
ToastManager.error(e as Error, "Error initializing database", "global");
}
}
if (IsDatabaseMigrationRequired()) return;
if (db.isInitialized) {
Notifications.setupReminders(true);
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(false);
}
useSettingStore.getState().setAppLoading(false);
DatabaseLogger.info("Database initialized");
}
Walkthrough.init();
}, []);
useEffect(() => { useEffect(() => {
let sub: () => void; if (!appLocked && isAppLoading) {
if (appLocked) { initializeLogger()
const sub = useUserStore.subscribe((state) => { .catch((e) => {
if (!state.appLocked && useSettingStore.getState().isAppLoading) { console.log(e);
console.log("DB initialized"); })
.finally(() => {
//@ts-ignore
initializeDatabase(); initializeDatabase();
sub(); });
}
});
} }
return () => { }, [appLocked, isAppLoading]);
sub?.();
};
}, [appLocked, initializeDatabase]);
return initializeDatabase;
};
const doAppLoadActions = async () => {
if (SettingsService.get().sessionExpired) {
eSendEvent(eLoginSessionExpired);
return;
}
notifee.setBadgeCount(0);
if (!(await db.user.getUser())) {
setLoginMessage();
return;
}
await useMessageStore.getState().setAnnouncement();
if (NewFeature.present()) return;
if (await checkAppUpdateAvailable()) return;
if (await checkForRateAppRequest()) return;
if (await PremiumService.getRemainingTrialDaysStatus()) return;
if (SettingsService.get().introCompleted) {
useMessageStore.subscribe((state) => {
const dialogs = state.dialogs;
if (dialogs.length > 0) {
eSendEvent(eOpenAnnouncementDialog, dialogs[0]);
}
});
}
};
const checkAppUpdateAvailable = async () => {
if (__DEV__ || Config.isTesting === "true" || Config.FDROID_BUILD || BETA)
return;
try {
const version =
Config.GITHUB_RELEASE === "true"
? await getGithubVersion()
: await checkVersion();
if (!version || !version?.needsUpdate) return false;
setUpdateAvailableMessage(version);
return true;
} catch (e) {
return false;
}
};
const checkForRateAppRequest = async () => {
const rateApp = SettingsService.get().rateApp as number;
if (
rateApp &&
rateApp < Date.now() &&
!useMessageStore.getState().message?.visible
) {
setRateAppMessage();
return false;
}
return false;
}; };

View File

@@ -71,12 +71,10 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
if (typeof itemId === "string" && itemId !== itemIdRef.current) return; if (typeof itemId === "string" && itemId !== itemIdRef.current) return;
if (!isValidIdOrIndex(idOrIndex)) return; if (!isValidIdOrIndex(idOrIndex)) return;
console.log("useDBItem.onUpdateItem", idOrIndex, type);
if (items && typeof idOrIndex === "number") { if (items && typeof idOrIndex === "number") {
items.item(idOrIndex).then((item) => { items.item(idOrIndex).then((item) => {
setItem(item.item); setItem(item.item);
itemIdRef.current = item.item.id; itemIdRef.current = item.item?.id;
}); });
} else { } else {
if (!(db as any)[type + "s"][type]) { if (!(db as any)[type + "s"][type]) {

View File

@@ -36,7 +36,6 @@ export const useNotebook = (
const onRequestUpdate = React.useCallback(() => { const onRequestUpdate = React.useCallback(() => {
if (!item?.id) return; if (!item?.id) return;
console.log("useNotebook.onRequestUpdate", item?.id, Date.now());
const selector = db.relations.from( const selector = db.relations.from(
{ {
@@ -59,7 +58,6 @@ export const useNotebook = (
useEffect(() => { useEffect(() => {
if (nestedNotebooks) { if (nestedNotebooks) {
console.log("useNotebook.useEffect.onRequestUpdate");
onRequestUpdate(); onRequestUpdate();
} }
}, [item?.id, onRequestUpdate, nestedNotebooks]); }, [item?.id, onRequestUpdate, nestedNotebooks]);

View File

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

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import BiometicService from "../services/biometrics"; import BiometricService from "../services/biometrics";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager"; import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { db } from "../common/database"; import { db } from "../common/database";
@@ -39,8 +39,8 @@ export const useVaultStatus = () => {
const checkVaultStatus = useCallback(() => { const checkVaultStatus = useCallback(() => {
db.vault?.exists().then(async (exists) => { db.vault?.exists().then(async (exists) => {
const available = await BiometicService.isBiometryAvailable(); const available = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials(); const fingerprint = await BiometricService.hasInternetCredentials();
if ( if (
VaultStatusCache.exists === exists && VaultStatusCache.exists === exists &&
VaultStatusCache.biometryEnrolled === fingerprint && VaultStatusCache.biometryEnrolled === fingerprint &&

View File

@@ -62,9 +62,9 @@ import {
} from "../services/event-manager"; } from "../services/event-manager";
import { useSettingStore } from "../stores/use-setting-store"; import { useSettingStore } from "../stores/use-setting-store";
import { import {
eClearEditor,
eCloseFullscreenEditor, eCloseFullscreenEditor,
eOnEnterEditor, eOnEnterEditor,
eOnExitEditor,
eOnLoadNote, eOnLoadNote,
eOpenFullscreenEditor, eOpenFullscreenEditor,
eUnlockNote eUnlockNote
@@ -285,9 +285,10 @@ const _TabsHolder = () => {
break; break;
case "mobile": case "mobile":
if ( if (
state && (state &&
!state?.movedAway && !state?.movedAway &&
useTabStore.getState().getCurrentNoteId() useTabStore.getState().getCurrentNoteId()) ||
editorState().movedAway === false
) { ) {
tabBarRef.current?.goToIndex(2, false); tabBarRef.current?.goToIndex(2, false);
} else { } else {
@@ -511,6 +512,7 @@ const onChangeTab = async (event) => {
editorState().movedAway = false; editorState().movedAway = false;
editorState().isFocused = true; editorState().isFocused = true;
activateKeepAwake(); activateKeepAwake();
eSendEvent(eOnEnterEditor);
if (!useTabStore.getState().getCurrentNoteId()) { if (!useTabStore.getState().getCurrentNoteId()) {
eSendEvent(eOnLoadNote, { eSendEvent(eOnLoadNote, {
@@ -522,14 +524,13 @@ const onChangeTab = async (event) => {
) { ) {
eSendEvent(eUnlockNote); eSendEvent(eUnlockNote);
} }
eSendEvent(eOnEnterEditor);
} }
} else { } else {
if (event.from === 2) { if (event.from === 2) {
deactivateKeepAwake(); deactivateKeepAwake();
editorState().movedAway = true; editorState().movedAway = true;
editorState().isFocused = false; editorState().isFocused = false;
eSendEvent(eClearEditor, "removeHandler"); eSendEvent(eOnExitEditor);
// Lock all tabs with locked notes... // Lock all tabs with locked notes...
for (const tab of useTabStore.getState().tabs) { for (const tab of useTabStore.getState().tabs) {

View File

@@ -16,7 +16,7 @@
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"phone": "^3.1.14", "phone": "^3.1.14",
"qclone": "^1.2.0", "qclone": "^1.2.0",
"react-native-actions-sheet": "0.9.3", "react-native-actions-sheet": "0.9.6",
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version", "react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
"react-native-drax": "^0.10.2", "react-native-drax": "^0.10.2",
"react-native-image-zoom-viewer": "^3.0.1", "react-native-image-zoom-viewer": "^3.0.1",

View File

@@ -32,13 +32,14 @@ import WebView from "react-native-webview";
import { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes"; import { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes";
import { notesnook } from "../../../e2e/test.ids"; import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database"; import { db } from "../../common/database";
import BiometicService from "../../services/biometrics"; import BiometricService from "../../services/biometrics";
import { import {
ToastManager, ToastManager,
eSendEvent, eSendEvent,
eSubscribeEvent eSubscribeEvent
} from "../../services/event-manager"; } from "../../services/event-manager";
import { import {
eEditorReset,
eOnLoadNote, eOnLoadNote,
eUnlockNote, eUnlockNote,
eUnlockWithBiometrics, eUnlockWithBiometrics,
@@ -51,7 +52,13 @@ import { EditorProps, useEditorType } from "./tiptap/types";
import { useEditor } from "./tiptap/use-editor"; import { useEditor } from "./tiptap/use-editor";
import { useEditorEvents } from "./tiptap/use-editor-events"; import { useEditorEvents } from "./tiptap/use-editor-events";
import { syncTabs, useTabStore } from "./tiptap/use-tab-store"; import { syncTabs, useTabStore } from "./tiptap/use-tab-store";
import { editorController, editorState } from "./tiptap/utils"; import {
editorController,
editorState,
openInternalLink,
randId
} from "./tiptap/utils";
import { tabBarRef } from "../../utils/global-refs";
const style: ViewStyle = { const style: ViewStyle = {
height: "100%", height: "100%",
@@ -61,7 +68,10 @@ const style: ViewStyle = {
backgroundColor: "transparent" backgroundColor: "transparent"
}; };
const onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => { const onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => {
if (request.url.includes("https")) { if (request.url.includes("nn://")) {
openInternalLink(request.url);
return false;
} else if (request.url.includes("https")) {
if (Platform.OS === "ios" && !request.isTopFrame) return true; if (Platform.OS === "ios" && !request.isTopFrame) return true;
openLinkInBrowser(request.url); openLinkInBrowser(request.url);
return false; return false;
@@ -95,24 +105,20 @@ const Editor = React.memo(
noToolbar, noToolbar,
noHeader noHeader
}); });
const renderKey = useRef(`editor-0` + editorId); const renderKey = useRef(randId("editor-id") + editorId);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
get: () => editor get: () => editor
})); }));
useLockedNoteHandler(); useLockedNoteHandler();
const onError = useCallback(() => { const onError = useCallback(() => {
renderKey.current = renderKey.current = randId("editor-id") + editorId;
renderKey.current === `editor-0`
? `editor-1` + editorId
: `editor-0` + editorId;
editor.state.current.ready = false; editor.state.current.ready = false;
editor.setLoading(true); editor.setLoading(true);
}, [editor, editorId]); }, [editor, editorId]);
useEffect(() => { useEffect(() => {
const sub = [eSubscribeEvent("webview_reset", onError)]; const sub = [eSubscribeEvent(eEditorReset, onError)];
return () => { return () => {
sub.forEach((s) => s?.unsubscribe()); sub.forEach((s) => s?.unsubscribe());
}; };
@@ -139,6 +145,7 @@ const Editor = React.memo(
nestedScrollEnabled nestedScrollEnabled
onError={onError} onError={onError}
injectedJavaScriptBeforeContentLoaded={` injectedJavaScriptBeforeContentLoaded={`
globalThis.__DEV__ = ${__DEV__}
globalThis.readonly=${readonly}; globalThis.readonly=${readonly};
globalThis.noToolbar=${noToolbar}; globalThis.noToolbar=${noToolbar};
globalThis.noHeader=${noHeader}; globalThis.noHeader=${noHeader};
@@ -201,8 +208,8 @@ const useLockedNoteHandler = () => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const biometry = await BiometicService.isBiometryAvailable(); const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials(); const fingerprint = await BiometricService.hasInternetCredentials();
useTabStore.setState({ useTabStore.setState({
biometryAvailable: !!biometry, biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint biometryEnrolled: !!fingerprint
@@ -216,9 +223,9 @@ const useLockedNoteHandler = () => {
try { try {
if (!tabRef.current?.noteLocked || !tabRef.current) return; if (!tabRef.current?.noteLocked || !tabRef.current) return;
console.log("Trying to unlock with biometrics..."); console.log("Trying to unlock with biometrics...");
const credentials = await BiometicService.getCredentials( const credentials = await BiometricService.getCredentials(
"Unlock note", "Unlock note",
"Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault." "Unlock note to open it in editor."
); );
if (credentials && credentials?.password && tabRef.current.noteId) { if (credentials && credentials?.password && tabRef.current.noteId) {
@@ -226,6 +233,7 @@ const useLockedNoteHandler = () => {
tabRef.current.noteId, tabRef.current.noteId,
credentials?.password credentials?.password
); );
eSendEvent(eOnLoadNote, { eSendEvent(eOnLoadNote, {
item: note item: note
}); });
@@ -262,7 +270,7 @@ const useLockedNoteHandler = () => {
try { try {
const unlocked = await db.vault.unlock(password); const unlocked = await db.vault.unlock(password);
if (!unlocked) throw new Error("Incorrect vault password"); if (!unlocked) throw new Error("Incorrect vault password");
await BiometicService.storeCredentials(password); await BiometricService.storeCredentials(password);
eSendEvent("vaultUpdated"); eSendEvent("vaultUpdated");
ToastManager.show({ ToastManager.show({
heading: "Biometric unlocking enabled!", heading: "Biometric unlocking enabled!",
@@ -271,8 +279,8 @@ const useLockedNoteHandler = () => {
context: "global" context: "global"
}); });
const biometry = await BiometicService.isBiometryAvailable(); const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials(); const fingerprint = await BiometricService.hasInternetCredentials();
useTabStore.setState({ useTabStore.setState({
biometryAvailable: !!biometry, biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint biometryEnrolled: !!fingerprint
@@ -331,7 +339,7 @@ const useLockedNoteHandler = () => {
}), }),
eSubscribeEvent(eUnlockWithPassword, onSubmit) eSubscribeEvent(eUnlockWithPassword, onSubmit)
]; ];
if (tabRef.current?.locked) { if (tabRef.current?.locked && tabBarRef.current?.page() === 2) {
unlock(); unlock();
} }
return () => { return () => {

View File

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

View File

@@ -48,5 +48,6 @@ export const EventTypes = {
unlockWithBiometrics: "editor-events:unlock-biometrics", unlockWithBiometrics: "editor-events:unlock-biometrics",
disableReadonlyMode: "editor-events:disable-readonly-mode", disableReadonlyMode: "editor-events:disable-readonly-mode",
readonlyEditorLoaded: "readonlyEditorLoaded", readonlyEditorLoaded: "readonlyEditorLoaded",
error: "editorError" error: "editorError",
dbLogger: "editor-events:dbLogger"
}; };

View File

@@ -21,12 +21,11 @@ import Sodium from "@ammarahmed/react-native-sodium";
import { isImage } from "@notesnook/core/dist/utils/filename"; import { isImage } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native"; import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util"; import RNFetchBlob from "react-native-blob-util";
import DocumentPicker from "react-native-document-picker"; import DocumentPicker, {
import { DocumentPickerOptions,
ImagePickerResponse, DocumentPickerResponse
launchCamera, } from "react-native-document-picker";
launchImageLibrary import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
} from "react-native-image-picker";
import { DatabaseLogger, db } from "../../../common/database"; import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem"; import filesystem from "../../../common/filesystem";
import { compressToFile } from "../../../common/filesystem/compress"; import { compressToFile } from "../../../common/filesystem/compress";
@@ -42,8 +41,9 @@ import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
import { eCloseSheet } from "../../../utils/events"; import { eCloseSheet } from "../../../utils/events";
import { useTabStore } from "./use-tab-store"; import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils"; import { editorController, editorState } from "./utils";
import { basename } from "pathe";
const showEncryptionSheet = (file) => { const showEncryptionSheet = (file: DocumentPickerResponse) => {
presentSheet({ presentSheet({
title: "Encrypting attachment", title: "Encrypting attachment",
paragraph: `Please wait while we encrypt ${file.name} file for upload`, paragraph: `Please wait while we encrypt ${file.name} file for upload`,
@@ -51,24 +51,25 @@ const showEncryptionSheet = (file) => {
}); });
}; };
const santizeUri = (uri) => { const santizeUri = (uri: string) => {
uri = decodeURI(uri); uri = decodeURI(uri);
uri = Platform.OS === "ios" ? uri.replace("file:///", "/") : uri; uri = Platform.OS === "ios" ? uri.replace("file:///", "/") : uri;
return uri; return uri;
}; };
/** type PickerOptions = {
* @param {{ noteId?: string;
* noteId: string, tabId?: number;
* tabId: string, type: "image" | "camera" | "file";
* type: "image" | "camera" | "file" reupload: boolean;
* reupload: boolean hash?: string;
* hash?: string context?: string;
* }} fileOptions outputType?: "base64" | "url" | "cache";
*/ };
const file = async (fileOptions) => {
const file = async (fileOptions: PickerOptions) => {
try { try {
const options = { const options: DocumentPickerOptions<"ios"> = {
mode: "import", mode: "import",
allowMultiSelection: false allowMultiSelection: false
}; };
@@ -87,11 +88,11 @@ const file = async (fileOptions) => {
file = file[0]; file = file[0];
let uri = Platform.OS === "ios" ? file.fileCopyUri : file.uri; let uri = Platform.OS === "ios" ? file.fileCopyUri || file.uri : file.uri;
if (file.size > FILE_SIZE_LIMIT) { if ((file.size || 0) > FILE_SIZE_LIMIT) {
ToastManager.show({ ToastManager.show({
title: "File too large", heading: "File too large",
message: "The maximum allowed size per file is 500 MB", message: "The maximum allowed size per file is 500 MB",
type: "error" type: "error"
}); });
@@ -108,29 +109,40 @@ const file = async (fileOptions) => {
return; return;
} }
console.log("file uri: ", uri);
uri = Platform.OS === "ios" ? santizeUri(uri) : uri; uri = Platform.OS === "ios" ? santizeUri(uri) : uri;
showEncryptionSheet(file); showEncryptionSheet(file);
const hash = await Sodium.hashFile({ const hash = await Sodium.hashFile({
uri: uri, uri: uri,
type: "url" type: "url"
}); });
if (!(await attachFile(uri, hash, file.type, file.name, fileOptions))) if (
!(await attachFile(
uri,
hash,
file.type || "application/octet-stream",
file.name,
fileOptions
))
)
return; return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri); if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
if (!fileOptions.tabId) return;
if ( if (
useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId fileOptions.tabId &&
fileOptions.noteId &&
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
fileOptions.noteId
) { ) {
if (isImage(file.type)) { if (isImage(file.type || "application/octet-stream")) {
editorController.current?.commands.insertImage( editorController.current?.commands.insertImage(
{ {
hash: hash, hash: hash,
filename: file.name, filename: file.name,
mime: file.type, mime: file.type || "application/octet-stream",
size: file.size, size: file.size || 0,
dataurl: await db.attachments.read(hash, "base64"), dataurl: (await db.attachments.read(hash, "base64")) as string,
title: file.name type: "image"
}, },
fileOptions.tabId fileOptions.tabId
); );
@@ -139,8 +151,9 @@ const file = async (fileOptions) => {
{ {
hash: hash, hash: hash,
filename: file.name, filename: file.name,
mime: file.type, mime: file.type || "application/octet-stream",
size: file.size size: file.size || 0,
type: "file"
}, },
fileOptions.tabId fileOptions.tabId
); );
@@ -151,39 +164,74 @@ const file = async (fileOptions) => {
eSendEvent(eCloseSheet); eSendEvent(eCloseSheet);
}, 1000); }, 1000);
} catch (e) { } catch (e) {
eSendEvent(eCloseSheet);
ToastManager.show({ ToastManager.show({
heading: e.message, heading: (e as Error).message,
message: "You need internet access to attach a file", message: "You need internet access to attach a file",
type: "error", type: "error",
context: "global" context: "global"
}); });
console.log("attachment error: ", e); DatabaseLogger.error(e);
} }
}; };
/** const camera = async (options: PickerOptions) => {
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} options
*/
const camera = async (options) => {
try { try {
await db.attachments.generateKey(); await db.attachments.generateKey();
useSettingStore.getState().setAppDidEnterBackgroundForAction(true); useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
launchCamera( openCamera({
{ mediaType: "photo",
includeBase64: true, includeBase64: true,
mediaType: "photo" cropping: false,
}, multiple: true,
(response) => handleImageResponse(response, options) maxFiles: 10,
); writeTempFile: true,
compressImageQuality: 1
})
.then((response) => {
handleImageResponse(
Array.isArray(response) ? response : [response],
options
);
})
.catch((e) => {
console.log("camera error: ", e);
});
} catch (e) { } catch (e) {
ToastManager.show({ ToastManager.show({
heading: e.message, heading: (e as Error).message,
type: "error",
context: "global"
});
console.log("attachment error:", e);
}
};
const gallery = async (options: PickerOptions) => {
try {
await db.attachments.generateKey();
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
openPicker({
includeBase64: true,
mediaType: "photo",
maxFiles: 10,
cropping: false,
multiple: true,
compressImageQuality: 1
})
.then((response) =>
handleImageResponse(
Array.isArray(response) ? response : [response],
options
)
)
.catch((e) => {
console.log("gallery error: ", e);
});
} catch (e) {
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
ToastManager.show({
heading: (e as Error).message,
message: "You need internet access to attach a file", message: "You need internet access to attach a file",
type: "error", type: "error",
context: "global" context: "global"
@@ -192,53 +240,9 @@ const camera = async (options) => {
} }
}; };
const gallery = async (options) => { const pick = async (options: PickerOptions) => {
try {
await db.attachments.generateKey();
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
launchImageLibrary(
{
includeBase64: true,
mediaType: "photo",
selectionLimit: 10
},
(response) => handleImageResponse(response, options)
);
} catch (e) {
ToastManager.show({
heading: e.message,
message: "You need internet access to attach a file",
type: "error",
context: "global"
});
console.log("attachment error:", e);
}
};
/**
*
* @typedef {{
* noteId?: string,
* tabId?: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* context?: string
* }} ImagePickerOptions
*
* @param {{
* noteId?: string,
* tabId?: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* context?: string
* }} options
* @returns
*/
const pick = async (options) => {
if (!PremiumService.get()) { if (!PremiumService.get()) {
let user = await db.user.getUser(); const user = await db.user.getUser();
if (editorState().isFocused) { if (editorState().isFocused) {
editorState().isFocused = true; editorState().isFocused = true;
} }
@@ -259,58 +263,59 @@ const pick = async (options) => {
file(options); file(options);
} }
}; };
/**
*
* @param {ImagePickerResponse} response
* @param {ImagePickerOptions} options
* @returns
*/
const handleImageResponse = async (response, options) => {
if (
response.didCancel ||
response.errorMessage ||
!response.assets ||
response.assets?.length === 0
) {
return;
}
const result = await AttachImage.present(response);
const handleImageResponse = async (
response: Image[],
options: PickerOptions
) => {
const result = await AttachImage.present(response, options.context);
if (!result) return; if (!result) return;
const compress = result.compress; const compress = result.compress;
for (let image of response.assets) { for (const image of response) {
const isPng = /(png)/g.test(image.type); const isPng = /(png)/g.test(image.mime);
const isJpeg = /(jpeg|jpg)/g.test(image.type); const isJpeg = /(jpeg|jpg)/g.test(image.mime);
if (compress && (isPng || isJpeg)) { if (compress && (isPng || isJpeg)) {
image.uri = await compressToFile( image.path = await compressToFile(
Platform.OS === "ios" ? "file://" + image.uri : image.uri, Platform.OS === "ios" ? "file://" + image.path : image.path,
isPng ? "PNG" : "JPEG" isPng ? "PNG" : "JPEG"
); );
const stat = await RNFetchBlob.fs.stat(image.uri.replace("file://", ""));
image.fileSize = stat.size; const stat = await RNFetchBlob.fs.stat(image.path.replace("file://", ""));
image.size = stat.size;
image.path =
Platform.OS === "ios" ? image.path.replace("file://", "") : image.path;
} }
if (image.fileSize > IMAGE_SIZE_LIMIT) { if (image.size > IMAGE_SIZE_LIMIT) {
ToastManager.show({ ToastManager.show({
title: "File too large", heading: "File too large",
message: "The maximum allowed size per image is 50 MB", message: "The maximum allowed size per image is 50 MB",
type: "error" type: "error"
}); });
return; return;
} }
let b64 = `data:${image.type};base64, ` + image.base64; const b64 = `data:${image.mime};base64, ` + image.data;
const uri = decodeURI(image.uri); const uri = decodeURI(image.path);
const hash = await Sodium.hashFile({ const hash = await Sodium.hashFile({
uri: uri, uri: uri,
type: "url" type: "url"
}); });
let fileName = image.originalFileName || image.fileName; 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..."); console.log("attaching file...");
if (!(await attachFile(uri, hash, image.type, fileName, options))) return; if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri); if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
console.log("attaching image to note..."); console.log("attaching image to note...");
@@ -322,11 +327,13 @@ const handleImageResponse = async (response, options) => {
editorController.current?.commands.insertImage( editorController.current?.commands.insertImage(
{ {
hash: hash, hash: hash,
mime: image.type, mime: image.mime,
title: fileName, type: "image",
dataurl: b64, dataurl: b64,
size: image.fileSize, size: image.size,
filename: fileName filename: fileName as string,
width: image.width,
height: image.height
}, },
options.tabId options.tabId
); );
@@ -343,10 +350,16 @@ const handleImageResponse = async (response, options) => {
* @param {ImagePickerOptions} options * @param {ImagePickerOptions} options
* @returns * @returns
*/ */
export async function attachFile(uri, hash, type, filename, options) { export async function attachFile(
uri: string,
hash: string,
type: string,
filename: string,
options: PickerOptions
) {
try { try {
let exists = await db.attachments.exists(hash); const exists = await db.attachments.exists(hash);
let encryptionInfo; let encryptionInfo: any;
if (options?.hash && options.hash !== hash) { if (options?.hash && options.hash !== hash) {
ToastManager.show({ ToastManager.show({
heading: "Please select the same file for reuploading", heading: "Please select the same file for reuploading",
@@ -358,26 +371,36 @@ export async function attachFile(uri, hash, type, filename, options) {
} }
if (!options.reupload && exists) { if (!options.reupload && exists) {
options.reupload = (await filesystem.getUploadedFileSize(hash)) <= 0; options.reupload = (await filesystem.getUploadedFileSize(hash)) === 0;
}
if (options.reupload) {
DatabaseLogger.log(`Deleting file before reupload. ${hash}`);
const deleted = await db.fs().deleteFile(hash, false);
if (!deleted)
throw new Error(`Failed to delete file before reupload. ${hash}`);
} }
if (!exists || options?.reupload) { if (!exists || options?.reupload) {
let key = await db.attachments.generateKey(); const key = await db.attachments.generateKey();
encryptionInfo = await Sodium.encryptFile(key, { encryptionInfo = await Sodium.encryptFile(key, {
uri: uri, uri: uri,
type: options.type || "url", type: options.outputType || "url",
hash: hash hash: hash
}); } as any);
encryptionInfo.mimeType = type; encryptionInfo.mimeType = type;
encryptionInfo.filename = filename; encryptionInfo.filename = filename;
encryptionInfo.alg = "xcha-stream"; encryptionInfo.alg = "xcha-stream";
encryptionInfo.size = encryptionInfo.length; encryptionInfo.size = encryptionInfo.length;
encryptionInfo.key = key; encryptionInfo.key = key;
if (options?.reupload && exists) await db.attachments.reset(hash); if (options?.reupload && exists) {
const attachment = await db.attachments.attachment(hash);
if (attachment) await db.attachments.reset(attachment?.id);
}
} else { } else {
encryptionInfo = { hash: hash }; encryptionInfo = { hash: hash };
} }
await db.attachments.add(encryptionInfo, options.noteId); await db.attachments.add(encryptionInfo);
return true; return true;
} catch (e) { } catch (e) {
DatabaseLogger.error(e); DatabaseLogger.error(e);

View File

@@ -37,6 +37,7 @@ export type EditorState = {
isAwaitingResult: boolean; isAwaitingResult: boolean;
scrollPosition: number; scrollPosition: number;
overlay?: boolean; overlay?: boolean;
initialLoadCalled?: boolean;
}; };
export type Settings = { export type Settings = {
@@ -74,6 +75,8 @@ export type EditorMessage<T> = {
type: string; type: string;
noteId: string; noteId: string;
tabId: number; tabId: number;
resolverId?: string;
hasTimeout?: boolean;
}; };
export type SavePayload = { export type SavePayload = {
@@ -84,6 +87,7 @@ export type SavePayload = {
sessionHistoryId?: number; sessionHistoryId?: number;
ignoreEdit: boolean; ignoreEdit: boolean;
tabId: number; tabId: number;
pendingChanges?: boolean;
}; };
export type AppState = { export type AppState = {

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import { parseInternalLink } from "@notesnook/core";
import { ItemReference } from "@notesnook/core/dist/types"; import { ItemReference } from "@notesnook/core/dist/types";
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index"; import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
import { getDefaultPresets } from "@notesnook/editor/dist/toolbar/tool-definitions"; import { getDefaultPresets } from "@notesnook/editor/dist/toolbar/tool-definitions";
@@ -27,7 +26,6 @@ import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef } from "react"; import React, { useCallback, useEffect, useRef } from "react";
import { import {
BackHandler, BackHandler,
InteractionManager,
Keyboard, Keyboard,
KeyboardEventListener, KeyboardEventListener,
NativeEventSubscription, NativeEventSubscription,
@@ -37,6 +35,7 @@ import { WebViewMessageEvent } from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database"; import { DatabaseLogger, db } from "../../../common/database";
import downloadAttachment from "../../../common/filesystem/download-attachment"; import downloadAttachment from "../../../common/filesystem/download-attachment";
import EditorTabs from "../../../components/sheets/editor-tabs"; import EditorTabs from "../../../components/sheets/editor-tabs";
import { Issue } from "../../../components/sheets/github/issue";
import LinkNote from "../../../components/sheets/link-note"; import LinkNote from "../../../components/sheets/link-note";
import ManageTagsSheet from "../../../components/sheets/manage-tags"; import ManageTagsSheet from "../../../components/sheets/manage-tags";
import { RelationsList } from "../../../components/sheets/relations-list"; import { RelationsList } from "../../../components/sheets/relations-list";
@@ -61,6 +60,7 @@ import {
eCloseFullscreenEditor, eCloseFullscreenEditor,
eEditorTabFocused, eEditorTabFocused,
eOnEnterEditor, eOnEnterEditor,
eOnExitEditor,
eOnLoadNote, eOnLoadNote,
eOpenFullscreenEditor, eOpenFullscreenEditor,
eOpenLoginDialog, eOpenLoginDialog,
@@ -75,8 +75,7 @@ import { useDragState } from "../../settings/editor/state";
import { EventTypes } from "./editor-events"; import { EventTypes } from "./editor-events";
import { EditorMessage, EditorProps, useEditorType } from "./types"; import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store"; import { useTabStore } from "./use-tab-store";
import { EditorEvents, editorState } from "./utils"; import { EditorEvents, editorState, openInternalLink } from "./utils";
import { Issue } from "../../../components/sheets/github/issue";
const publishNote = async () => { const publishNote = async () => {
const user = useUserStore.getState().user; const user = useUserStore.getState().user;
@@ -270,21 +269,20 @@ export const useEditorEvents = (
}, [editor, deviceMode, fullscreen]); }, [editor, deviceMode, fullscreen]);
const onHardwareBackPress = useCallback(() => { const onHardwareBackPress = useCallback(() => {
if (tabBarRef.current?.page === 2) { console.log(tabBarRef.current?.page());
if (tabBarRef.current?.page() === 2) {
onBackPress(); onBackPress();
return true; return true;
} }
}, [onBackPress]); }, [onBackPress]);
const onEnterEditor = useCallback(async () => { const onEnterEditor = useCallback(async () => {
InteractionManager.runAfterInteractions(() => { if (!DDS.isTab) {
if (!DDS.isTab) { handleBack.current = BackHandler.addEventListener(
handleBack.current = BackHandler.addEventListener( "hardwareBackPress",
"hardwareBackPress", onHardwareBackPress
onHardwareBackPress );
); }
}
});
}, [onHardwareBackPress]); }, [onHardwareBackPress]);
const onClearEditorSessionRequest = useCallback( const onClearEditorSessionRequest = useCallback(
@@ -331,7 +329,14 @@ export const useEditorEvents = (
}, [fullscreen, onHardwareBackPress]); }, [fullscreen, onHardwareBackPress]);
useEffect(() => { useEffect(() => {
const onExitEditor = () => {
if (handleBack.current) {
handleBack.current.remove();
}
};
eSubscribeEvent(eOnEnterEditor, onEnterEditor); eSubscribeEvent(eOnEnterEditor, onEnterEditor);
eSubscribeEvent(eOnExitEditor, onExitEditor);
eSubscribeEvent( eSubscribeEvent(
eClearEditor + editor.editorId, eClearEditor + editor.editorId,
onClearEditorSessionRequest onClearEditorSessionRequest
@@ -339,6 +344,7 @@ export const useEditorEvents = (
return () => { return () => {
eUnSubscribeEvent(eClearEditor, onClearEditorSessionRequest); eUnSubscribeEvent(eClearEditor, onClearEditorSessionRequest);
eUnSubscribeEvent(eOnEnterEditor, onEnterEditor); eUnSubscribeEvent(eOnEnterEditor, onEnterEditor);
eUnSubscribeEvent(eOnExitEditor, onExitEditor);
}; };
}, [editor.editorId, onClearEditorSessionRequest, onEnterEditor]); }, [editor.editorId, onClearEditorSessionRequest, onEnterEditor]);
@@ -347,8 +353,15 @@ export const useEditorEvents = (
const data = event.nativeEvent.data; const data = event.nativeEvent.data;
const editorMessage = JSON.parse(data) as EditorMessage<any>; const editorMessage = JSON.parse(data) as EditorMessage<any>;
if (editorMessage.hasTimeout && editorMessage.resolverId) {
editor.postMessage(EditorEvents.resolve, {
data: true,
resolverId: editorMessage.resolverId
});
}
if (editorMessage.type === EventTypes.load) { if (editorMessage.type === EventTypes.load) {
console.log("Editor loaded"); DatabaseLogger.log("Editor is ready");
editor.onLoad(); editor.onLoad();
return; return;
} }
@@ -376,21 +389,36 @@ export const useEditorEvents = (
content: editorMessage.value.html as string, content: editorMessage.value.html as string,
noteId: noteId, noteId: noteId,
tabId: editorMessage.tabId, tabId: editorMessage.tabId,
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
pendingChanges: editorMessage.value?.pendingChanges
}); });
break; break;
case EventTypes.title: case EventTypes.title:
DatabaseLogger.log("EventTypes.title"); DatabaseLogger.log("EventTypes.title");
editor.saveContent({ editor.saveContent({
type: editorMessage.type, type: editorMessage.type,
title: editorMessage.value as string, title: editorMessage.value?.title as string,
noteId: noteId, noteId: noteId,
tabId: editorMessage.tabId, tabId: editorMessage.tabId,
ignoreEdit: false ignoreEdit: false,
pendingChanges: editorMessage.value?.pendingChanges
}); });
break; break;
case EventTypes.logger: case EventTypes.logger:
logger.info("[WEBVIEW LOG]", editorMessage.value); logger.info("[EDITOR LOG]", editorMessage.value);
break;
case EventTypes.dbLogger:
if (editorMessage.value.error) {
DatabaseLogger.error(
editorMessage.value.error,
editorMessage.value.error,
{
message: "[EDITOR_ERROR]" + editorMessage.value.message
}
);
} else {
DatabaseLogger.info("[EDITOR_LOG]" + editorMessage.value.message);
}
break; break;
case EventTypes.contentchange: case EventTypes.contentchange:
editor.onContentChanged(editorMessage.noteId); editor.onContentChanged(editorMessage.noteId);
@@ -443,7 +471,7 @@ export const useEditorEvents = (
break; break;
case EventTypes.filepicker: case EventTypes.filepicker:
editorState().isAwaitingResult = true; editorState().isAwaitingResult = true;
const { pick } = require("./picker.js").default; const { pick } = require("./picker").default;
pick({ pick({
type: editorMessage.value, type: editorMessage.value,
noteId: noteId, noteId: noteId,
@@ -462,12 +490,10 @@ export const useEditorEvents = (
case EventTypes.getAttachmentData: { case EventTypes.getAttachmentData: {
const attachment = (editorMessage.value as any) const attachment = (editorMessage.value as any)
.attachment as Attachment; ?.attachment as Attachment;
console.log( DatabaseLogger.log(
"Getting attachment data:", `Getting attachment data: ${attachment?.hash} ${attachment?.type}`
attachment.hash,
attachment.type
); );
downloadAttachment(attachment.hash, true, { downloadAttachment(attachment.hash, true, {
base64: attachment.type === "image", base64: attachment.type === "image",
@@ -480,17 +506,17 @@ export const useEditorEvents = (
console.log( console.log(
"Got attachment data:", "Got attachment data:",
!!data, !!data,
(editorMessage.value as any).resolverId editorMessage.resolverId
); );
editor.postMessage(EditorEvents.attachmentData, { editor.postMessage(EditorEvents.resolve, {
resolverId: (editorMessage.value as any).resolverId, resolverId: editorMessage.resolverId,
data data
}); });
}) })
.catch(() => { .catch((e) => {
console.log("Error downloading attachment data"); DatabaseLogger.error(e);
editor.postMessage(EditorEvents.attachmentData, { editor.postMessage(EditorEvents.resolve, {
resolverId: (editorMessage.value as any).resolverId, resolverId: editorMessage.resolverId,
data: undefined data: undefined
}); });
}); });
@@ -519,27 +545,7 @@ export const useEditorEvents = (
break; break;
case EventTypes.link: case EventTypes.link:
if (editorMessage.value.startsWith("nn://")) { if (editorMessage.value.startsWith("nn://")) {
const data = parseInternalLink(editorMessage.value); openInternalLink(editorMessage.value);
if (!data?.id) break;
if (
data.id ===
useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab)
) {
if (data.params?.blockId) {
setTimeout(() => {
if (!data.params?.blockId) return;
editor.commands.scrollIntoViewById(data.params.blockId);
}, 150);
}
return;
}
eSendEvent(eOnLoadNote, {
item: await db.notes.note(data?.id),
blockId: data.params?.blockId
});
console.log( console.log(
"Opening note from internal link:", "Opening note from internal link:",
editorMessage.value editorMessage.value
@@ -637,7 +643,7 @@ export const useEditorEvents = (
case EventTypes.createInternalLink: { case EventTypes.createInternalLink: {
LinkNote.present( LinkNote.present(
editorMessage.value.attributes, editorMessage.value.attributes,
editorMessage.value.resolverId editorMessage.resolverId as string
); );
break; break;
} }
@@ -662,10 +668,8 @@ export const useEditorEvents = (
.updateTab(useTabStore.getState().currentTab, { .updateTab(useTabStore.getState().currentTab, {
readonly: false readonly: false
}); });
Navigation.queueRoutesForUpdate(); setTimeout(() => {
ToastManager.show({ Navigation.queueRoutesForUpdate();
heading: "Readonly mode disabled.",
type: "success"
}); });
} }
break; break;

View File

@@ -39,6 +39,7 @@ import { DatabaseLogger, db } from "../../../common/database";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets"; import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import { DDS } from "../../../services/device-detection"; import { DDS } from "../../../services/device-detection";
import { import {
ToastManager,
eSendEvent, eSendEvent,
eSubscribeEvent, eSubscribeEvent,
eUnSubscribeEvent eUnSubscribeEvent
@@ -46,16 +47,20 @@ import {
import Navigation from "../../../services/navigation"; import Navigation from "../../../services/navigation";
import Notifications from "../../../services/notifications"; import Notifications from "../../../services/notifications";
import SettingsService from "../../../services/settings"; import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useTagStore } from "../../../stores/use-tag-store"; import { useTagStore } from "../../../stores/use-tag-store";
import { import {
eEditorReset,
eEditorTabFocused, eEditorTabFocused,
eOnLoadNote, eOnLoadNote,
eShowMergeDialog, eShowMergeDialog,
eUpdateNoteInEditor eUpdateNoteInEditor
} from "../../../utils/events"; } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs"; import { tabBarRef } from "../../../utils/global-refs";
import { unlockVault } from "../../../utils/unlock-vault";
import { onNoteCreated } from "../../notes/common"; import { onNoteCreated } from "../../notes/common";
import Commands from "./commands"; import Commands from "./commands";
import { EventTypes } from "./editor-events";
import { SessionHistory } from "./session-history"; import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types"; import { EditorState, SavePayload } from "./types";
import { syncTabs, useTabStore } from "./use-tab-store"; import { syncTabs, useTabStore } from "./use-tab-store";
@@ -68,6 +73,7 @@ import {
isEditorLoaded, isEditorLoaded,
post post
} from "./utils"; } from "./utils";
import { sleep } from "../../../utils/time";
type NoteWithContent = Note & { type NoteWithContent = Note & {
content?: NoteContent<false>; content?: NoteContent<false>;
@@ -145,8 +151,9 @@ export const useEditor = (
useEffect(() => { useEffect(() => {
const event = eSubscribeEvent(eEditorTabFocused, (tabId) => { const event = eSubscribeEvent(eEditorTabFocused, (tabId) => {
console.log("Editot tab focus changed", lastTabFocused.current, tabId);
if (lastTabFocused.current !== tabId) lock.current = false;
lastTabFocused.current = tabId as number; lastTabFocused.current = tabId as number;
console.log(tabId);
}); });
return () => { return () => {
event?.unsubscribe(); event?.unsubscribe();
@@ -165,10 +172,11 @@ export const useEditor = (
useEffect(() => { useEffect(() => {
if (loading) { if (loading) {
overlay(true);
state.current.ready = false; state.current.ready = false;
setLoading(false); setLoading(false);
} }
}, [loading]); }, [loading, overlay]);
const withTimer = useCallback( const withTimer = useCallback(
(id: string, fn: () => void, duration: number) => { (id: string, fn: () => void, duration: number) => {
@@ -216,7 +224,8 @@ export const useEditor = (
type, type,
ignoreEdit, ignoreEdit,
sessionHistoryId: currentSessionHistoryId, sessionHistoryId: currentSessionHistoryId,
tabId tabId,
pendingChanges
}: SavePayload) => { }: SavePayload) => {
if (currentNotes.current[id as string]?.readonly || readonly) return; if (currentNotes.current[id as string]?.readonly || readonly) return;
try { try {
@@ -277,12 +286,26 @@ export const useEditor = (
); );
}, 50); }, 50);
const saveTimer = setTimeout(() => {
DatabaseLogger.log(`Note save timeout: ${id}...`);
ToastManager.error(
new Error(
"Copy your changes and restart the app to avoid data loss. If the issue persists, please report to us at support@streetwriters.co."
),
"Saving note is taking too long",
"global",
15000
);
}, 30 * 1000);
if (!locked) { if (!locked) {
DatabaseLogger.log(`Saving note: ${id}...`); DatabaseLogger.log(`Saving note: ${id}...`);
id = await db.notes?.add({ ...noteData }); id = await db.notes?.add({ ...noteData });
saved = true; saved = true;
DatabaseLogger.log(`Note saved: ${id}...`); DatabaseLogger.log(`Note saved: ${id}...`);
clearTimeout(saveTimer);
if (!note && id) { if (!note && id) {
editorSessionHistory.newSession(id); editorSessionHistory.newSession(id);
if (id) { if (id) {
@@ -316,9 +339,31 @@ export const useEditor = (
Notifications.pinNote(id as string); Notifications.pinNote(id as string);
} }
} else { } else {
noteData.contentId = note?.contentId; if (!db.vault.unlocked) {
if (pendingChanges) await sleep(3000);
const unlocked = await unlockVault({
title: "Unlock vault to save note",
paragraph: `This note is locked, unlock to save ${
pendingChanges ? "some pending" : ""
} changes`,
context: "global"
});
if (!unlocked)
throw new Error("Could not save note, vault is locked");
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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);
} }
if (id && useTabStore.getState().getTabForNote(id) === tabId) { if (id && useTabStore.getState().getTabForNote(id) === tabId) {
@@ -341,6 +386,16 @@ export const useEditor = (
} }
} }
if (
id &&
id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges
) {
postMessage(EditorEvents.title, title || note?.title, tabId);
postMessage(EditorEvents.html, data, tabId);
currentNotes.current[id] = note;
}
saveCount.current++; saveCount.current++;
return id; return id;
} catch (e) { } catch (e) {
@@ -415,7 +470,11 @@ export const useEditor = (
commands.focus(tabId); commands.focus(tabId);
}); });
} else { } else {
if (!event.item) return; if (!event.item) {
overlay(false);
return;
}
console.log("LOADING NOTE", event.item.id);
const item = event.item; const item = event.item;
const currentTab = useTabStore const currentTab = useTabStore
@@ -443,7 +502,6 @@ export const useEditor = (
useTabStore.getState().focusTab(tabId); useTabStore.getState().focusTab(tabId);
setTimeout(() => { setTimeout(() => {
if (blockIdRef.current) { if (blockIdRef.current) {
console.log("scrolling to block", blockIdRef.current);
commands.scrollIntoViewById(blockIdRef.current); commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined; blockIdRef.current = undefined;
} }
@@ -502,6 +560,7 @@ export const useEditor = (
loadingState.current === currentContents.current[item.id]?.data loadingState.current === currentContents.current[item.id]?.data
) { ) {
// If note is already loading, return. // If note is already loading, return.
console.log("Note is already loading...");
return; return;
} }
@@ -510,7 +569,7 @@ export const useEditor = (
return; return;
} }
lastContentChangeTime.current[item.id] = item.dateEdited; lastContentChangeTime.current[item.id] = 0;
currentLoadingNoteId.current = item.id; currentLoadingNoteId.current = item.id;
currentNotes.current[item.id] = item; currentNotes.current[item.id] = item;
@@ -535,11 +594,8 @@ export const useEditor = (
10000 10000
); );
console.log("blockId", blockIdRef.current);
setTimeout(() => { setTimeout(() => {
if (blockIdRef.current) { if (blockIdRef.current) {
console.log("scrolling to block", blockIdRef.current);
commands.scrollIntoViewById(blockIdRef.current); commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined; blockIdRef.current = undefined;
} }
@@ -573,124 +629,130 @@ export const useEditor = (
data: Note | ContentItem | TrashItem | DeletedItem, data: Note | ContentItem | TrashItem | DeletedItem,
isLocal?: boolean isLocal?: boolean
) => { ) => {
if (SettingsService.get().disableRealtimeSync && !isLocal) return; try {
if (!data) return; await (async () => {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
if (isDeleted(data) || isTrashItem(data)) { if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id); const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) { if (tabId !== undefined) {
console.log("Removing tab"); console.log("Removing tab");
await commands.clearContent(tabId); await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId); useTabStore.getState().removeTab(tabId);
}
return;
}
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
const tab = useTabStore.getState().getTab(tabId);
const note = data.type === "note" ? data : await db.notes?.note(noteId);
lock.current = true;
// Handle this case where note was locked on another device and synced.
const locked = await db.vaults.itemExists(
currentNotes.current[noteId] as ItemReference
);
if (note) {
if (!locked && tab?.noteLocked) {
// Note lock removed.
if (tab.locked) {
if (useTabStore.getState().currentTab === tabId) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
} else {
useTabStore.getState().updateTab(tabId, {
locked: false,
noteLocked: false
});
commands.setLoading(true, tabId);
} }
}
} else if (!tab?.noteLocked && locked) {
// Note lock added.
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
}
if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(EditorEvents.title, note.title, tabId);
}
commands.setTags(note);
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved",
tabId as number
);
}
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) {
lock.current = false;
return;
}
if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(data, noteId);
if (!decryptedContent) {
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
} else {
await postMessage(
EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
currentContents.current[note.id] = decryptedContent;
}
} else {
const _nextContent = data.data;
if (_nextContent === currentContents.current?.data) {
lock.current = false;
return; return;
} }
lastContentChangeTime.current[note.id] = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent, tabId); const noteId =
if (!isEncryptedContent(data)) { (data as ContentItem).type === "tiptap"
currentContents.current[note.id] = data as UnencryptedContentItem; ? (data as ContentItem).noteId
: data.id;
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
const tab = useTabStore.getState().getTab(tabId);
const note =
data.type === "note" ? data : await db.notes?.note(noteId);
lock.current = true;
// Handle this case where note was locked on another device and synced.
const locked = await db.vaults.itemExists(
currentNotes.current[noteId] as ItemReference
);
if (note) {
if (!locked && tab?.noteLocked) {
// Note lock removed.
if (tab.locked) {
if (useTabStore.getState().currentTab === tabId) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
} else {
useTabStore.getState().updateTab(tabId, {
locked: false,
noteLocked: false
});
commands.setLoading(true, tabId);
}
}
} else if (!tab?.noteLocked && locked) {
// Note lock added.
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
}
if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(EditorEvents.title, note.title, tabId);
}
commands.setTags(note);
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved",
tabId as number
);
}
useTabStore.getState().updateTab(tabId, {
readonly: note.readonly
});
} }
}
if (data.type === "tiptap" && note && !isLocal) {
if (lastContentChangeTime.current[noteId] >= data.dateModified) {
return;
}
if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(data);
if (!decryptedContent) {
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
} else {
await postMessage(
EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
currentContents.current[note.id] = decryptedContent;
}
} else {
const _nextContent = data.data;
if (_nextContent === currentContents.current[note.id]?.data) {
return;
}
lastContentChangeTime.current[note.id] = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) {
currentContents.current[note.id] =
data as UnencryptedContentItem;
}
}
}
})();
} catch (e) {
DatabaseLogger.error(e as Error, "Error when applying sync changes");
} finally {
lock.current = false;
} }
lock.current = false;
}, },
[postMessage, commands] [postMessage, commands]
); );
@@ -713,7 +775,8 @@ export const useEditor = (
type, type,
ignoreEdit, ignoreEdit,
noteId, noteId,
tabId tabId,
pendingChanges
}: { }: {
noteId?: string; noteId?: string;
title?: string; title?: string;
@@ -721,18 +784,29 @@ export const useEditor = (
type: string; type: string;
ignoreEdit: boolean; ignoreEdit: boolean;
tabId: number; tabId: number;
pendingChanges?: boolean;
}) => { }) => {
DatabaseLogger.log(`Saving content...`); DatabaseLogger.log(
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
);
if ( if (
lock.current || lock.current ||
(currentLoadingNoteId.current && (currentLoadingNoteId.current &&
currentLoadingNoteId.current === noteId) currentLoadingNoteId.current === noteId)
) { ) {
DatabaseLogger.log(`Skipped saving conent: DatabaseLogger.log(`Skipped saving content:
lock.current: ${lock.current} lock.current: ${lock.current}
currentLoadingNoteId.current: ${currentLoadingNoteId.current} currentLoadingNoteId.current: ${currentLoadingNoteId.current}
`); `);
if (lock.current) {
setTimeout(() => {
if (lock.current) {
DatabaseLogger.warn("Editor force removed lock after 5 seconds");
lock.current = false;
}
}, 5000);
}
return; return;
} }
@@ -740,7 +814,7 @@ export const useEditor = (
lastContentChangeTime.current[noteId] = Date.now(); lastContentChangeTime.current[noteId] = Date.now();
} }
if (type === EditorEvents.content && noteId) { if (type === EventTypes.content && noteId) {
currentContents.current[noteId as string] = { currentContents.current[noteId as string] = {
data: content, data: content,
type: "tiptap", type: "tiptap",
@@ -755,7 +829,8 @@ export const useEditor = (
id: noteId, id: noteId,
ignoreEdit, ignoreEdit,
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined, sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
tabId: tabId tabId: tabId,
pendingChanges
}; };
withTimer( withTimer(
noteId || "newnote", noteId || "newnote",
@@ -767,7 +842,16 @@ export const useEditor = (
onChange(params.data); onChange(params.data);
return; return;
} }
saveNote(params); if (useSettingStore.getState().isAppLoading) {
const sub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
saveNote(params);
sub();
}
});
} else {
saveNote(params);
}
}, },
ignoreEdit ? 0 : 150 ignoreEdit ? 0 : 150
); );
@@ -810,7 +894,7 @@ export const useEditor = (
useTabStore.getState().currentTab useTabStore.getState().currentTab
)) ))
) { ) {
eSendEvent("webview_reset", "onReady"); eSendEvent(eEditorReset, "onReady");
return false; return false;
} else { } else {
syncTabs(); syncTabs();
@@ -820,7 +904,6 @@ export const useEditor = (
}, [isDefaultEditor, restoreEditorState]); }, [isDefaultEditor, restoreEditorState]);
const onLoad = useCallback(async () => { const onLoad = useCallback(async () => {
if (currentNotes.current) overlay(true);
setTimeout(() => { setTimeout(() => {
postMessage(EditorEvents.theme, theme); postMessage(EditorEvents.theme, theme);
}); });
@@ -835,12 +918,22 @@ export const useEditor = (
const noteId = useTabStore.getState().getCurrentNoteId(); const noteId = useTabStore.getState().getCurrentNoteId();
if (!noteId) { if (!noteId) {
overlay(false);
loadNote({ newNote: true }); loadNote({ newNote: true });
if (tabBarRef.current?.page === 1) { if (tabBarRef.current?.page() === 1) {
state.current.currentlyEditing = false; state.current.currentlyEditing = false;
} }
} else if (state.current?.initialLoadCalled) {
const note = currentNotes.current[noteId];
if (note) {
loadNote({
item: note
});
}
} }
if (!state.current?.initialLoadCalled) {
state.current.initialLoadCalled = true;
}
overlay(false);
}, [ }, [
postMessage, postMessage,
theme, theme,

View File

@@ -22,10 +22,15 @@ import { TextInput } from "react-native";
import WebView from "react-native-webview"; import WebView from "react-native-webview";
import { MMKV } from "../../../common/database/mmkv"; import { MMKV } from "../../../common/database/mmkv";
import { import {
eSendEvent,
eSubscribeEvent, eSubscribeEvent,
eUnSubscribeEvent eUnSubscribeEvent
} from "../../../services/event-manager"; } from "../../../services/event-manager";
import { AppState, EditorState, useEditorType } from "./types"; import { AppState, EditorState, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { parseInternalLink } from "@notesnook/core";
import { eOnLoadNote } from "../../../utils/events";
import { db } from "../../../common/database";
export const textInput = createRef<TextInput>(); export const textInput = createRef<TextInput>();
export const editorController = export const editorController =
createRef<useEditorType>() as MutableRefObject<useEditorType>; createRef<useEditorType>() as MutableRefObject<useEditorType>;
@@ -41,7 +46,7 @@ export function editorState() {
return editorController.current?.state.current || defaultState; return editorController.current?.state.current || defaultState;
} }
export const EditorEvents: { [name: string]: string } = { export const EditorEvents = {
html: "native:html", html: "native:html",
updatehtml: "native:updatehtml", updatehtml: "native:updatehtml",
title: "native:title", title: "native:title",
@@ -50,7 +55,8 @@ export const EditorEvents: { [name: string]: string } = {
logger: "native:logger", logger: "native:logger",
status: "native:status", status: "native:status",
keyboardShown: "native:keyboardShown", keyboardShown: "native:keyboardShown",
attachmentData: "native:attachment-data" attachmentData: "native:attachment-data",
resolve: "native:resolve"
}; };
export function randId(prefix: string) { export function randId(prefix: string) {
@@ -176,3 +182,27 @@ export function clearAppState() {
appState = undefined; appState = undefined;
MMKV.removeItem("appState"); MMKV.removeItem("appState");
} }
export async function openInternalLink(url: string) {
const data = parseInternalLink(url);
if (!data?.id) return false;
if (
data.id ===
useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab)
) {
if (data.params?.blockId) {
setTimeout(() => {
if (!data.params?.blockId) return;
editorController.current.commands.scrollIntoViewById(
data.params.blockId
);
}, 150);
}
return;
}
eSendEvent(eOnLoadNote, {
item: await db.notes.note(data?.id),
blockId: data.params?.blockId
});
}

View File

@@ -32,6 +32,7 @@ import { openEditor } from "../notes/common";
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => { export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const [notes, loading] = useNotes(); const [notes, loading] = useNotes();
const isFocused = useNavigationFocus(navigation, { const isFocused = useNavigationFocus(navigation, {
onFocus: (prev) => { onFocus: (prev) => {
Navigation.routeNeedsUpdate( Navigation.routeNeedsUpdate(

View File

@@ -16,40 +16,37 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { resolveItems } from "@notesnook/common";
import { VirtualizedGrouping } from "@notesnook/core"; import { VirtualizedGrouping } from "@notesnook/core";
import { Note, Notebook } from "@notesnook/core/dist/types"; import { Note, Notebook } from "@notesnook/core/dist/types";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database"; import { db } from "../../common/database";
import DelayLayout from "../../components/delay-layout"; import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header"; import { Header } from "../../components/header";
import List from "../../components/list"; import List from "../../components/list";
import { NotebookHeader } from "../../components/list-items/headers/notebook-header"; import { NotebookHeader } from "../../components/list-items/headers/notebook-header";
import SelectionHeader from "../../components/selection-header";
import { AddNotebookSheet } from "../../components/sheets/add-notebook"; import { AddNotebookSheet } from "../../components/sheets/add-notebook";
import { IconButton } from "../../components/ui/icon-button";
import { Pressable } from "../../components/ui/pressable";
import Paragraph from "../../components/ui/typography/paragraph";
import { useNavigationFocus } from "../../hooks/use-navigation-focus"; import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { import { eSendEvent, eSubscribeEvent } from "../../services/event-manager";
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation"; import Navigation, { NavigationProps } from "../../services/navigation";
import useNavigationStore, { import useNavigationStore, {
NotebookScreenParams NotebookScreenParams
} from "../../stores/use-navigation-store"; } from "../../stores/use-navigation-store";
import { eUpdateNotebookRoute } from "../../utils/events"; import { eUpdateNotebookRoute } from "../../utils/events";
import { findRootNotebookId } from "../../utils/notebooks"; import { findRootNotebookId } from "../../utils/notebooks";
import { openEditor, setOnFirstSave } from "../notes/common";
import SelectionHeader from "../../components/selection-header";
import Paragraph from "../../components/ui/typography/paragraph";
import { View } from "react-native";
import { SIZE } from "../../utils/size"; import { SIZE } from "../../utils/size";
import { IconButton } from "../../components/ui/icon-button"; import { openEditor, setOnFirstSave } from "../notes/common";
import { Pressable } from "../../components/ui/pressable";
import { resolveItems } from "@notesnook/common";
const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => { const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>(); const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const params = useRef<NotebookScreenParams>(route?.params); const params = useRef<NotebookScreenParams>(route?.params);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const updateOnFocus = useRef(false);
const [breadcrumbs, setBreadcrumbs] = useState< const [breadcrumbs, setBreadcrumbs] = useState<
{ {
id: string; id: string;
@@ -59,11 +56,17 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
useNavigationFocus(navigation, { useNavigationFocus(navigation, {
onFocus: () => { onFocus: () => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate); if (updateOnFocus.current) {
onRequestUpdate();
updateOnFocus.current = false;
} else {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
}
syncWithNavigation(); syncWithNavigation();
return false; return false;
}, },
onBlur: () => { onBlur: () => {
updateOnFocus.current = false;
setOnFirstSave(null); setOnFirstSave(null);
return false; return false;
} }
@@ -79,6 +82,15 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const onRequestUpdate = React.useCallback( const onRequestUpdate = React.useCallback(
async (data?: NotebookScreenParams) => { async (data?: NotebookScreenParams) => {
if (
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
!data
) {
updateOnFocus.current = true;
return;
}
if (data?.item?.id && params.current.item?.id !== data?.item?.id) { if (data?.item?.id && params.current.item?.id !== data?.item?.id) {
const nextRootNotebookId = await findRootNotebookId(data?.item?.id); const nextRootNotebookId = await findRootNotebookId(data?.item?.id);
const currentNotebookRoot = await findRootNotebookId( const currentNotebookRoot = await findRootNotebookId(
@@ -104,7 +116,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
if (notebook) { if (notebook) {
const breadcrumbs = await db.notebooks.breadcrumbs(notebook.id); const breadcrumbs = await db.notebooks.breadcrumbs(notebook.id);
setBreadcrumbs(breadcrumbs); setBreadcrumbs(breadcrumbs.slice(0, breadcrumbs.length - 1));
params.current.item = notebook; params.current.item = notebook;
const notes = await db.relations const notes = await db.relations
.from(notebook, "note") .from(notebook, "note")
@@ -122,10 +134,10 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
); );
useEffect(() => { useEffect(() => {
onRequestUpdate(); onRequestUpdate(params.current);
eSubscribeEvent(eUpdateNotebookRoute, onRequestUpdate); const sub = eSubscribeEvent(eUpdateNotebookRoute, onRequestUpdate);
return () => { return () => {
eUnSubscribeEvent(eUpdateNotebookRoute, onRequestUpdate); sub?.unsubscribe();
}; };
}, [onRequestUpdate]); }, [onRequestUpdate]);
@@ -167,7 +179,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
onPressDefaultRightButton={openEditor} onPressDefaultRightButton={openEditor}
/> />
{breadcrumbs ? ( {breadcrumbs && breadcrumbs.length > 0 ? (
<View <View
style={{ style={{
width: "100%", width: "100%",

View File

@@ -81,16 +81,21 @@ const NotesPage = ({
route.name === "ColoredNotes" route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode ? (params.current?.item as Color)?.colorCode
: undefined; : undefined;
const updateOnFocus = useRef(false);
const isFocused = useNavigationFocus(navigation, { const isFocused = useNavigationFocus(navigation, {
onFocus: (prev) => { onFocus: (prev) => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate); if (updateOnFocus.current) {
onRequestUpdate();
updateOnFocus.current = false;
} else {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
}
syncWithNavigation(); syncWithNavigation();
if (focusControl) return !prev.current; if (focusControl) return !prev.current;
return false; return false;
}, },
onBlur: () => { onBlur: () => {
updateOnFocus.current = false;
setOnFirstSave(null); setOnFirstSave(null);
return false; return false;
}, },
@@ -99,7 +104,6 @@ const NotesPage = ({
const syncWithNavigation = React.useCallback(() => { const syncWithNavigation = React.useCallback(() => {
const { item } = params.current; const { item } = params.current;
useNavigationStore useNavigationStore
.getState() .getState()
.setFocusedRouteId(params?.current?.item?.id || route.name); .setFocusedRouteId(params?.current?.item?.id || route.name);
@@ -113,6 +117,14 @@ const NotesPage = ({
const onRequestUpdate = React.useCallback( const onRequestUpdate = React.useCallback(
async (data?: NotesScreenParams) => { async (data?: NotesScreenParams) => {
if (
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
!data
) {
updateOnFocus.current = false;
return;
}
const isNew = data && data?.item?.id !== params.current?.item?.id; const isNew = data && data?.item?.id !== params.current?.item?.id;
if (data) params.current = data; if (data) params.current = data;

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { useRef } from "react"; import React, { useRef } from "react";
import { View } from "react-native"; import { Platform, View } from "react-native";
import { TextInput } from "react-native-gesture-handler"; import { TextInput } from "react-native-gesture-handler";
import { IconButton } from "../../components/ui/icon-button"; import { IconButton } from "../../components/ui/icon-button";
import Navigation from "../../services/navigation"; import Navigation from "../../services/navigation";
@@ -49,8 +49,8 @@ export const SearchBar = ({
return selectionMode && isFocused ? null : ( return selectionMode && isFocused ? null : (
<View <View
style={{ style={{
height: 50 + insets.top, height: Platform.OS === "android" ? 50 + insets.top : 50,
paddingTop: insets.top, paddingTop: Platform.OS === "ios" ? 0 : insets.top,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
flexShrink: 1, flexShrink: 1,

View File

@@ -26,7 +26,7 @@ import { Pressable } from "../../components/ui/pressable";
import Seperator from "../../components/ui/seperator"; import Seperator from "../../components/ui/seperator";
import Heading from "../../components/ui/typography/heading"; import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph"; import Paragraph from "../../components/ui/typography/paragraph";
import BiometicService from "../../services/biometrics"; import BiometricService from "../../services/biometrics";
import { ToastManager, presentSheet } from "../../services/event-manager"; import { ToastManager, presentSheet } from "../../services/event-manager";
import SettingsService from "../../services/settings"; import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store"; import { useSettingStore } from "../../stores/use-setting-store";
@@ -119,7 +119,7 @@ const AppLock = () => {
type={appLockMode === item.value ? "secondary" : "transparent"} type={appLockMode === item.value ? "secondary" : "transparent"}
onPress={async () => { onPress={async () => {
if ( if (
!(await BiometicService.isBiometryAvailable()) && !(await BiometricService.isBiometryAvailable()) &&
!useUserStore.getState().user && !useUserStore.getState().user &&
item.value !== modes[0].value && item.value !== modes[0].value &&
!SettingsService.getProperty("appLockHasPasswordSecurity") !SettingsService.getProperty("appLockHasPasswordSecurity")
@@ -139,7 +139,7 @@ const AppLock = () => {
) && ) &&
item.value !== modes[0].value item.value !== modes[0].value
) { ) {
const verified = await BiometicService.validateUser( const verified = await BiometricService.validateUser(
"Verify it's you" "Verify it's you"
); );
if (verified) { if (verified) {

View File

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

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { db } from "../../common/database"; import { db } from "../../common/database";
import { validateAppLockPassword } from "../../common/database/encryption"; import { validateAppLockPassword } from "../../common/database/encryption";
import { presentDialog } from "../../components/dialog/functions"; import { presentDialog } from "../../components/dialog/functions";
import BiometicService from "../../services/biometrics"; import BiometricService from "../../services/biometrics";
import { ToastManager } from "../../services/event-manager"; import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings"; import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store"; import { useUserStore } from "../../stores/use-user-store";
@@ -90,19 +90,32 @@ export async function verifyUserWithApplock() {
positiveText: "Verify", positiveText: "Verify",
secureTextEntry: true, secureTextEntry: true,
negativeText: "Cancel", negativeText: "Cancel",
keyboardType: keyboardType,
positivePress: async (value) => { positivePress: async (value) => {
try { try {
const verified = await validateAppLockPassword(value); const verified = await validateAppLockPassword(value);
if (!verified) {
ToastManager.show({
heading: `Invalid ${
keyboardType === "numeric" ? "pin" : "password"
}`,
type: "error",
context: "local"
});
return false;
}
resolve(verified); resolve(verified);
} catch (e) { } catch (e) {
resolve(false); resolve(false);
return false;
} }
return true;
} }
}); });
} else { } else {
BiometicService.isBiometryAvailable().then((available) => { BiometricService.isBiometryAvailable().then((available) => {
if (available) { if (available) {
BiometicService.validateUser("Verify it's you").then((verified) => { BiometricService.validateUser("Verify it's you").then((verified) => {
resolve(verified); resolve(verified);
}); });
} else if (useUserStore.getState().user) { } else if (useUserStore.getState().user) {

View File

@@ -164,12 +164,6 @@ export const LICENSES = [
author: "dooboolab", author: "dooboolab",
link: "https://github.com/dooboolab/react-native-iap" link: "https://github.com/dooboolab/react-native-iap"
}, },
{
name: "react-native-image-picker",
licenseType: "MIT",
author: "Johan du Toit (Johan-dutoit)",
link: "https://github.com/react-native-image-picker/react-native-image-picker"
},
{ {
name: "react-native-keychain", name: "react-native-keychain",
licenseType: "MIT", licenseType: "MIT",

View File

@@ -38,7 +38,7 @@ import { Update } from "../../components/sheets/update";
import { VaultStatusType, useVaultStatus } from "../../hooks/use-vault-status"; import { VaultStatusType, useVaultStatus } from "../../hooks/use-vault-status";
import { BackgroundSync } from "../../services/background-sync"; import { BackgroundSync } from "../../services/background-sync";
import BackupService from "../../services/backup"; import BackupService from "../../services/backup";
import BiometicService from "../../services/biometrics"; import BiometricService from "../../services/biometrics";
import { import {
ToastManager, ToastManager,
eSendEvent, eSendEvent,
@@ -70,6 +70,7 @@ import { useDragState } from "./editor/state";
import { verifyUser, verifyUserWithApplock } from "./functions"; import { verifyUser, verifyUserWithApplock } from "./functions";
import { SettingSection } from "./types"; import { SettingSection } from "./types";
import { getTimeLeft } from "./user-section"; import { getTimeLeft } from "./user-section";
import ScreenGuardModule from "react-native-screenguard";
type User = any; type User = any;
@@ -333,12 +334,14 @@ export const settingsGroups: SettingSection[] = [
{ {
id: "logout", id: "logout",
name: "Log out", name: "Log out",
description: "Clear all your data and reset the app.", description:
"Logging out will clear all data stored on THIS DEVICE.",
icon: "logout", icon: "logout",
modifer: () => { modifer: () => {
presentDialog({ presentDialog({
title: "Logout", title: "Logout",
paragraph: "Clear all your data and reset the app.", paragraph:
"Logging out will clear all data stored on THIS DEVICE. Make sure you have synced all your changes before logging out.",
positiveText: "Logout", positiveText: "Logout",
positivePress: async () => { positivePress: async () => {
try { try {
@@ -349,7 +352,7 @@ export const settingsGroups: SettingSection[] = [
await db.user?.logout(); await db.user?.logout();
setLoginMessage(); setLoginMessage();
await PremiumService.setPremiumStatus(); await PremiumService.setPremiumStatus();
await BiometicService.resetCredentials(); await BiometricService.resetCredentials();
MMKV.clearStore(); MMKV.clearStore();
clearAllStores(); clearAllStores();
refreshAllStores(); refreshAllStores();
@@ -394,7 +397,7 @@ export const settingsGroups: SettingSection[] = [
if (verified) { if (verified) {
eSendEvent("settings-loading", true); eSendEvent("settings-loading", true);
await db.user?.deleteUser(value); await db.user?.deleteUser(value);
await BiometicService.resetCredentials(); await BiometricService.resetCredentials();
SettingsService.set({ SettingsService.set({
introCompleted: true introCompleted: true
}); });
@@ -458,7 +461,7 @@ export const settingsGroups: SettingSection[] = [
id: "background-sync", id: "background-sync",
name: "Background sync (experimental)", name: "Background sync (experimental)",
description: description:
"Periodically wake up the app in background to sync your notes from other devices.", "Periodically wake up the app in background to run sync.",
type: "switch", type: "switch",
property: "backgroundSync", property: "backgroundSync",
onChange: (value) => { onChange: (value) => {
@@ -470,22 +473,43 @@ export const settingsGroups: SettingSection[] = [
} }
}, },
{ {
id: "sync-issues-fix", id: "pull-sync",
name: "Having problems with sync", name: "Force pull changes",
description: "Try force sync to resolve issues with syncing", description: `Use this if changes from other devices are not appearing on this device. This will overwrite the data on this device with the latest data from the server.\n\nThis must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.`,
icon: "sync-alert", modifer: () => {
modifer: async () => {
presentDialog({ presentDialog({
title: "Force sync", title: "Force Pull changes",
paragraph: paragraph:
"If your data on two devices is out of sync even after trying to sync normally. You can run force sync to solve such problems. Usually you should never need to run this otherwise. Force sync means that all your data on this device is reuploaded to the server.", "This must only be used for troubleshooting. Using this regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
negativeText: "Cancel", negativeText: "Cancel",
positiveText: "Start", positiveText: "Start",
positivePress: async () => { positivePress: async () => {
eSendEvent(eCloseSheet); eSendEvent(eCloseSheet);
await sleep(300); await sleep(300);
Progress.present(); Progress.present();
Sync.run("global", true, true, () => { Sync.run("global", true, "fetch", () => {
eSendEvent(eCloseSheet);
});
}
});
}
},
{
id: "push-sync",
name: "Force push changes",
description: `Use this if changes made on this device are not appearing on other devices. This will overwrite the data on the server with the data from this device.\n\nThis must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.`,
modifer: () => {
presentDialog({
title: "Force Push changes",
paragraph:
"This must only be used for troubleshooting. Using this regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
negativeText: "Cancel",
positiveText: "Start",
positivePress: async () => {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, "send", () => {
eSendEvent(eCloseSheet); eSendEvent(eCloseSheet);
}); });
} }
@@ -789,7 +813,7 @@ export const settingsGroups: SettingSection[] = [
} }
}, },
{ {
id: "biometic-unlock", id: "biometric-unlock",
type: "switch", type: "switch",
name: "Biometric unlocking", name: "Biometric unlocking",
icon: "fingerprint", icon: "fingerprint",
@@ -827,9 +851,18 @@ export const settingsGroups: SettingSection[] = [
"Hide app contents when you switch to other apps. This will also disable screenshot taking in the app.", "Hide app contents when you switch to other apps. This will also disable screenshot taking in the app.",
modifer: () => { modifer: () => {
const settings = SettingsService.get(); const settings = SettingsService.get();
Platform.OS === "android" if (Platform.OS === "ios") {
? NotesnookModule.setSecureMode(!settings.privacyScreen) enabled(!settings.privacyScreen);
: enabled(true); if (settings.privacyScreen) {
ScreenGuardModule.unregister();
} else {
ScreenGuardModule.register({
backgroundColor: "#000000"
});
}
} else {
NotesnookModule.setSecureMode(!settings.privacyScreen);
}
SettingsService.set({ privacyScreen: !settings.privacyScreen }); SettingsService.set({ privacyScreen: !settings.privacyScreen });
}, },
@@ -850,6 +883,9 @@ export const settingsGroups: SettingSection[] = [
type: "switch", type: "switch",
property: "appLockEnabled", property: "appLockEnabled",
onChange: () => { onChange: () => {
SettingsService.set({
privacyScreen: true
});
SettingsService.setPrivacyScreen(SettingsService.get()); SettingsService.setPrivacyScreen(SettingsService.get());
}, },
onVerify: async () => { onVerify: async () => {
@@ -859,13 +895,13 @@ export const settingsGroups: SettingSection[] = [
if (!SettingsService.getProperty("appLockEnabled")) { if (!SettingsService.getProperty("appLockEnabled")) {
if ( if (
!SettingsService.getProperty("appLockHasPasswordSecurity") && !SettingsService.getProperty("appLockHasPasswordSecurity") &&
(await BiometicService.isBiometryAvailable()) (await BiometricService.isBiometryAvailable())
) { ) {
SettingsService.setProperty("biometricsAuthEnabled", true); SettingsService.setProperty("biometricsAuthEnabled", true);
} }
if ( if (
!(await BiometicService.isBiometryAvailable()) && !(await BiometricService.isBiometryAvailable()) &&
!SettingsService.getProperty("appLockHasPasswordSecurity") !SettingsService.getProperty("appLockHasPasswordSecurity")
) { ) {
ToastManager.show({ ToastManager.show({
@@ -952,7 +988,7 @@ export const settingsGroups: SettingSection[] = [
SettingsService.getProperty("applockKeyboardType") === "numeric" SettingsService.getProperty("applockKeyboardType") === "numeric"
? "pin" ? "pin"
: "password" : "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: () => { hidden: () => {
return !SettingsService.getProperty("appLockHasPasswordSecurity"); return !SettingsService.getProperty("appLockHasPasswordSecurity");
}, },

View File

@@ -141,7 +141,6 @@ const onBoot = async () => {
} }
await Notifications.setupReminders(); await Notifications.setupReminders();
SettingsService.init();
if (SettingsService.get().notifNotes) { if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(false); Notifications.pinQuickNote(false);
} }

View File

@@ -151,13 +151,14 @@ async function updateNextBackupTime() {
/** /**
* @param {boolean=} progress * @param {boolean=} progress
* @param {string=} context * @param {string=} context
* @returns {Promise<{path?: string, error?: Error}}> * @returns {Promise<{path?: string, error?: Error, report?: boolean}}>
*/ */
async function run(progress = false, context) { async function run(progress = false, context) {
let androidBackupDirectory = await checkBackupDirExists(false, context); let androidBackupDirectory = await checkBackupDirExists(false, context);
if (!androidBackupDirectory) if (!androidBackupDirectory)
return { return {
error: new Error("Backup directory not selected") error: new Error("Backup directory not selected"),
report: false
}; };
if (progress) { if (progress) {
@@ -170,7 +171,6 @@ async function run(progress = false, context) {
} }
let path; let path;
let backupFilePath;
let backupFileName = "notesnook_backup_" + Date.now(); let backupFileName = "notesnook_backup_" + Date.now();
if (Platform.OS === "ios") { if (Platform.OS === "ios") {
@@ -191,9 +191,10 @@ async function run(progress = false, context) {
await RNFetchBlob.fs.mkdir(zipSourceFolder); await RNFetchBlob.fs.mkdir(zipSourceFolder);
try { try {
const user = await db.user.getUser();
for await (const file of db.backup.export( for await (const file of db.backup.export(
"mobile", "mobile",
SettingsService.get().encryptedBackup SettingsService.get().encryptedBackup && user
)) { )) {
console.log("Writing backup chunk of size...", file?.data?.length); console.log("Writing backup chunk of size...", file?.data?.length);
await RNFetchBlob.fs.writeFile( await RNFetchBlob.fs.writeFile(
@@ -234,7 +235,7 @@ async function run(progress = false, context) {
await sleep(300); await sleep(300);
if (showBackupCompleteSheet) { if (showBackupCompleteSheet) {
presentBackupCompleteSheet(backupFilePath); presentBackupCompleteSheet(path);
} else { } else {
progress && eSendEvent(eCloseSheet); progress && eSendEvent(eCloseSheet);
} }
@@ -257,11 +258,12 @@ async function run(progress = false, context) {
return run(progress, context); return run(progress, context);
} }
DatabaseLogger.error(e, "Backup failed"); DatabaseLogger.error(e);
await sleep(300); await sleep(300);
progress && eSendEvent(eCloseSheet); progress && eSendEvent(eCloseSheet);
return { return {
error: e error: e,
report: true
}; };
} }
} }

View File

@@ -179,7 +179,7 @@ async function validateUser(title: string, description?: string) {
} }
} }
const BiometicService = { const BiometricService = {
isBiometryAvailable, isBiometryAvailable,
enableFingerprintAuth, enableFingerprintAuth,
isFingerprintAuthEnabled, isFingerprintAuthEnabled,
@@ -190,4 +190,4 @@ const BiometicService = {
validateUser validateUser
}; };
export default BiometicService; export default BiometricService;

View File

@@ -162,7 +162,7 @@ export const ToastManager = {
}); });
}, },
hide: () => eSendEvent(eHideToast), hide: () => eSendEvent(eHideToast),
error: (e: Error, title?: string, context?: any) => { error: (e: Error, title?: string, context?: any, duration = 5000) => {
ToastManager.show({ ToastManager.show({
heading: title, heading: title,
message: e?.message || "", message: e?.message || "",
@@ -176,7 +176,7 @@ export const ToastManager = {
heading: "Logs copied!", heading: "Logs copied!",
type: "success", type: "success",
context: "global", context: "global",
duration: 5000 duration: duration
}); });
} }
}); });

View File

@@ -17,40 +17,26 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { decode, EntityLevel } from "entities";
import { Platform } from "react-native"; import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util"; import RNFetchBlob from "react-native-blob-util";
import RNHTMLtoPDF from "react-native-html-to-pdf-lite"; import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
import * as ScopedStorage from "react-native-scoped-storage"; import * as ScopedStorage from "react-native-scoped-storage";
import { zip } from "react-native-zip-archive"; import { zip } from "react-native-zip-archive";
import { DatabaseLogger, db } from "../common/database/index"; import { DatabaseLogger } from "../common/database/index";
import Storage from "../common/database/storage"; import Storage from "../common/database/storage";
import { import {
exportNote as _exportNote, exportNote as _exportNote,
ExportableAttachment, ExportableAttachment,
ExportableNote, ExportableNote,
exportNotes, exportNotes
sanitizeFilename
} from "@notesnook/common"; } from "@notesnook/common";
import { Note } from "@notesnook/core"; import { Note } from "@notesnook/core";
import { NoteContent } from "@notesnook/core/dist/collections/session-content";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection"; import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
import { basename, dirname, join } from "pathe"; import { basename, dirname, join } from "pathe";
import downloadAttachment from "../common/filesystem/download-attachment"; import downloadAttachment from "../common/filesystem/download-attachment";
import { presentDialog } from "../components/dialog/functions";
import { useSettingStore } from "../stores/use-setting-store";
import BiometicService from "./biometrics";
import { ToastManager } from "./event-manager";
import { cacheDir } from "../common/filesystem/utils"; import { cacheDir } from "../common/filesystem/utils";
import { unlockVault } from "../utils/unlock-vault";
const MIMETypes = {
txt: "text/plain",
pdf: "application/pdf",
md: "text/markdown",
"md-frontmatter": "text/markdown",
html: "text/html"
};
const FolderNames: { [name: string]: string } = { const FolderNames: { [name: string]: string } = {
txt: "Text", txt: "Text",
@@ -59,16 +45,6 @@ const FolderNames: { [name: string]: string } = {
html: "Html" html: "Html"
}; };
async function releasePermissions(path: string) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (const uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}
async function getPath(type: string) { async function getPath(type: string) {
let path = let path =
Platform.OS === "ios" && Platform.OS === "ios" &&
@@ -82,153 +58,11 @@ async function getPath(type: string) {
return path; return path;
} }
async function save( async function unlockVaultForNoteExport() {
path: string, return await unlockVault({
data: string, title: "Unlock vault",
fileName: string, paragraph: "Some exported notes are locked, Unlock to export them",
extension: "txt" | "pdf" | "md" | "html" | "md-frontmatter" context: "export-notes"
) {
let uri;
if (Platform.OS === "android") {
uri = await ScopedStorage.writeFile(
path,
data,
`${fileName}.${extension}`,
MIMETypes[extension],
extension === "pdf" ? "base64" : "utf8",
false
);
await releasePermissions(path);
} else {
path = path + fileName + `.${extension}`;
await RNFetchBlob.fs.writeFile(path, data, "utf8");
}
return uri || path;
}
async function makeHtml(note: Note, content?: NoteContent<false>) {
let html = await db.notes.export(note.id, {
format: "html",
contentItem: content
});
if (!html) return "";
html = decode(html, {
level: EntityLevel.HTML
});
return html;
}
async function exportAs(
type: string,
note: Note,
bulk?: boolean,
content?: NoteContent<false>
) {
let data;
switch (type) {
case "html":
{
data = await makeHtml(note, content);
}
break;
case "md":
data = await db.notes.export(note.id, {
format: "md",
contentItem: content
});
break;
case "md-frontmatter":
data = await db.notes.export(note.id, {
format: "md-frontmatter",
contentItem: content
});
break;
case "pdf":
{
const html = await makeHtml(note, content);
const fileName = sanitizeFilename(note.title + Date.now(), {
replacement: "_"
});
const options = {
html: html,
fileName:
Platform.OS === "ios" ? "/exported/PDF/" + fileName : fileName,
width: 595,
height: 852,
bgColor: "#FFFFFF",
padding: 30,
base64: bulk || Platform.OS === "android"
} as { [name: string]: any };
if (Platform.OS === "ios") {
options.directory = "Documents";
}
const res = await RNHTMLtoPDF.convert(options);
data = !bulk && Platform.OS === "ios" ? res.filePath : res.base64;
if (bulk && res.filePath) {
RNFetchBlob.fs.unlink(res.filePath);
}
}
break;
case "txt":
{
data = await db.notes.export(note.id, {
format: "txt",
contentItem: content
});
}
break;
}
return data;
}
async function unlockVault() {
const biometry = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
if (biometry && fingerprint) {
const credentials = await BiometicService.getCredentials(
"Unlock vault",
"Unlock vault to export locked notes"
);
if (credentials) {
return db.vault.unlock(credentials.password);
}
}
useSettingStore.getState().setSheetKeyboardHandler(false);
return new Promise((resolve) => {
setImmediate(() => {
presentDialog({
context: "export-notes",
input: true,
secureTextEntry: true,
positiveText: "Unlock",
title: "Unlock vault",
paragraph: "Some exported notes are locked, Unlock to export them",
inputPlaceholder: "Enter password",
positivePress: async (value) => {
const unlocked = await db.vault.unlock(value);
if (!unlocked) {
ToastManager.show({
heading: "Invalid password",
message: "Please enter a valid password",
type: "error",
context: "local"
});
return false;
}
resolve(unlocked);
useSettingStore.getState().setSheetKeyboardHandler(true);
return true;
},
onClose: () => {
resolve(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}
});
});
}); });
} }
@@ -285,6 +119,7 @@ async function createZip(
callback: (progress?: string) => void callback: (progress?: string) => void
) { ) {
const fileName = `nn-export-${totalNotes}-${type}-${Date.now()}.zip`; const fileName = `nn-export-${totalNotes}-${type}-${Date.now()}.zip`;
const dir = path;
try { try {
callback("Creating zip"); callback("Creating zip");
const zipOutputPath = const zipOutputPath =
@@ -292,6 +127,7 @@ async function createZip(
? join(path, fileName) ? join(path, fileName)
: join(RNFetchBlob.fs.dirs.CacheDir, fileName); : join(RNFetchBlob.fs.dirs.CacheDir, fileName);
await zip(cacheFolder, zipOutputPath); await zip(cacheFolder, zipOutputPath);
callback("Saving zip file"); callback("Saving zip file");
if (Platform.OS === "android") { if (Platform.OS === "android") {
const file = await ScopedStorage.createFile( const file = await ScopedStorage.createFile(
@@ -303,6 +139,8 @@ async function createZip(
await copyFileAsync("file://" + zipOutputPath, path); await copyFileAsync("file://" + zipOutputPath, path);
await RNFetchBlob.fs.unlink(zipOutputPath); await RNFetchBlob.fs.unlink(zipOutputPath);
callback(); callback();
} else {
path = zipOutputPath;
} }
RNFetchBlob.fs.unlink(cacheFolder); RNFetchBlob.fs.unlink(cacheFolder);
} catch (e) { } catch (e) {
@@ -311,6 +149,7 @@ async function createZip(
return { return {
filePath: path, filePath: path,
fileDir: dir,
type: "application/zip", type: "application/zip",
name: "zip", name: "zip",
fileName: fileName, fileName: fileName,
@@ -388,7 +227,7 @@ async function bulkExport(
let currentAttachmentProgress = 0; let currentAttachmentProgress = 0;
for await (const item of exportNotes(notes, { for await (const item of exportNotes(notes, {
format: type, format: type,
unlockVault: unlockVault as () => Promise<boolean> unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
})) { })) {
if (item instanceof Error) { if (item instanceof Error) {
DatabaseLogger.error(item); DatabaseLogger.error(item);
@@ -432,7 +271,7 @@ async function exportNote(
let currentAttachmentProgress = 0; let currentAttachmentProgress = 0;
for await (const item of _exportNote(note, { for await (const item of _exportNote(note, {
format: type, format: type,
unlockVault: unlockVault as () => Promise<boolean> unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
})) { })) {
if (item instanceof Error) { if (item instanceof Error) {
DatabaseLogger.error(item); DatabaseLogger.error(item);

View File

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

View File

@@ -49,6 +49,8 @@ import { DDS } from "./device-detection";
import { eSendEvent } from "./event-manager"; import { eSendEvent } from "./event-manager";
import Navigation from "./navigation"; import Navigation from "./navigation";
import SettingsService from "./settings"; import SettingsService from "./settings";
import { useUserStore } from "../stores/use-user-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
let pinned: DisplayedNotification[] = []; let pinned: DisplayedNotification[] = [];
@@ -139,6 +141,7 @@ const onEvent = async ({ type, detail }: Event) => {
} }
editorState().movedAway = false; editorState().movedAway = false;
const noteId = notification?.id; const noteId = notification?.id;
console.log("NOTE ID", noteId);
loadNote(noteId as string, true); loadNote(noteId as string, true);
} }
@@ -407,18 +410,31 @@ async function loadNote(id: string, jump: boolean) {
if (!DDS.isTab && jump) { if (!DDS.isTab && jump) {
tabBarRef.current?.goToPage(1); tabBarRef.current?.goToPage(1);
} }
eSendEvent("loadingNote", note);
setTimeout( MMKV.setString(
() => { "appState",
eSendEvent(eOnLoadNote, { JSON.stringify({
item: note editing: true,
}); movedAway: false,
if (!jump && !DDS.isTab) { timestamp: Date.now()
tabBarRef.current?.goToPage(1); })
}
},
tabBarRef?.current ? 0 : 2000
); );
const isLocked = await db.vaults.itemExists({
type: "note",
id: id
});
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
useTabStore.getState().focusTab(tab);
} else {
useTabStore.getState().focusPreviewTab(id, {
noteId: id,
readonly: note.readonly,
noteLocked: isLocked
});
}
} }
async function getChannelId(id: "silent" | "vibrate" | "urgent" | "default") { async function getChannelId(id: "silent" | "vibrate" | "urgent" | "default") {
@@ -476,7 +492,15 @@ async function displayNotification({
reply_button_text?: string; reply_button_text?: string;
id?: string; id?: string;
}) { }) {
if (!(await checkAndRequestPermissions())) return; useUserStore.setState({
disableAppLockRequests: true
});
const permission = await checkAndRequestPermissions();
useUserStore.setState({
disableAppLockRequests: false
});
if (!permission) return;
try { try {
await notifee.displayNotification({ await notifee.displayNotification({
@@ -846,7 +870,16 @@ async function remove(id: string) {
} }
async function pinQuickNote(launch: boolean) { async function pinQuickNote(launch: boolean) {
if (!(await checkAndRequestPermissions())) return; useUserStore.setState({
disableAppLockRequests: true
});
const permission = await checkAndRequestPermissions();
useUserStore.setState({
disableAppLockRequests: false
});
if (!permission) {
return;
}
get().then((items) => { get().then((items) => {
const notification = items.filter((n) => n.id === "notesnook_note_input"); const notification = items.filter((n) => n.id === "notesnook_note_input");
if (notification && launch) { if (notification && launch) {
@@ -919,13 +952,13 @@ async function pinNote(id: string) {
const note = await db.notes.note(id as string); const note = await db.notes.note(id as string);
if (!note) return; if (!note) return;
let text = await convertNoteToText(note, false); let text = await convertNoteToText(note, true);
if (!text) text = ""; if (!text) text = "";
const html = text.replace(/\n/g, "<br />"); const html = text.replace(/\n/g, "<br />");
Notifications.displayNotification({ Notifications.displayNotification({
title: note.title, title: note.title,
message: note.headline || text, message: note.headline || text,
subtitle: note.headline || text, subtitle: "",
bigText: html, bigText: html,
ongoing: true, ongoing: true,
actions: ["UNPIN"], actions: ["UNPIN"],

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