Compare commits

..

661 Commits

Author SHA1 Message Date
Abdullah Atta
59945411e8 editor: improve task lists 2024-03-20 10:59:43 +05:00
Abdullah Atta
0f2554b9b3 web: fix assigning a color removes that color from all other notes 2024-03-20 07:24:29 +05:00
Abdullah Atta
181739dcd2 web: fix changes in callout heading not causing toc update 2024-03-20 07:16:34 +05:00
Abdullah Atta
ab8d283059 web: fix toc items not indented 2024-03-20 07:16:07 +05:00
Abdullah Atta
468f7c6a56 editor: add block-id on nodes inside callout 2024-03-20 07:15:48 +05:00
Abdullah Atta
c36a76cfdc editor: disable conflicting simple checklist input rule 2024-03-20 06:55:15 +05:00
Ammar Ahmed
57834bc9f1 mobile: fix tab title 2024-03-20 06:42:28 +05:00
Abdullah Atta
cad28f3436 web: fix image picker shown when there are 0 images 2024-03-19 22:44:21 +05:00
Abdullah Atta
cfb8b1ef0b editor: add support for copy/pasting code blocks with highlighting 2024-03-19 22:40:44 +05:00
Abdullah Atta
b626577730 web: fix crash on copying from codeblock 2024-03-19 22:32:43 +05:00
Abdullah Atta
e8ed3e2d8b web: only show content blocks with direct links in referenced in tab 2024-03-19 22:15:40 +05:00
Abdullah Atta
0ac3e4b67f core: fix extraction of blocks with links 2024-03-19 22:15:20 +05:00
Abdullah Atta
6de0112600 editor: fix hover popup appearing in wrong position 2024-03-19 22:14:42 +05:00
Abdullah Atta
4b8c35b6af editor: fix outline list has no data-block-id attribute 2024-03-19 21:56:38 +05:00
Abdullah Atta
c72ee27aa4 editor: fix DOMException when removing react node views 2024-03-19 16:29:53 +05:00
Abdullah Atta
875436a1ad editor: minor refactors 2024-03-19 12:30:05 +05:00
Abdullah Atta
1ede80c33c editor: fix crash on select all + backspace with lists 2024-03-19 12:29:56 +05:00
Abdullah Atta
e6de183f77 editor: fix crash on removing task list 2024-03-19 12:29:37 +05:00
Ammar Ahmed
514302d20d editor: enable block id on checklist and callout 2024-03-19 12:03:07 +05:00
Abdullah Atta
14946e092d editor: fix react node views for react 18 2024-03-19 12:02:25 +05:00
Ammar Ahmed
d9fafc7703 editor: always show note internal link button in toolbar 2024-03-19 11:29:48 +05:00
Abdullah Atta
5e8ab57271 editor: handle link open on click 2024-03-18 12:09:03 +05:00
Abdullah Atta
00c648341b ui: fix weird zIndex animation on popups 2024-03-18 12:05:18 +05:00
Abdullah Atta
4063c573ec editor: update tiptap & other packages 2024-03-18 12:00:23 +05:00
Abdullah Atta
72f908bc90 editor: fix newline added in callout on typing anything 2024-03-18 11:20:51 +05:00
Abdullah Atta
4eebe10f2e editor: fix => turning into a callout 2024-03-18 11:18:41 +05:00
Abdullah Atta
6c0d6127ba editor: fix image size resetting on resize 2024-03-18 11:13:05 +05:00
Abdullah Atta
4c9381599f web: fix images not getting attached 2024-03-18 11:12:36 +05:00
Abdullah Atta
8650ed2ba5 web: fix attachment delete dialog not rendering any buttons 2024-03-18 10:27:31 +05:00
Abdullah Atta
600953baa7 core: fix attachment deletion 2024-03-18 10:27:31 +05:00
Ammar Ahmed
8967280e54 mobile: use contentBlocksWithLinks 2024-03-18 10:22:59 +05:00
Ammar Ahmed
57a2181e62 core: fix duplicate backlinks shown on note 2024-03-18 10:22:59 +05:00
Ammar Ahmed
911cacd9e9 mobile: cleanup 2024-03-18 10:22:59 +05:00
Ammar Ahmed
361be50e3f mobile: show error if attachment download fails 2024-03-18 10:22:59 +05:00
Ammar Ahmed
6a306ddda2 core: support video,audio,document filter in attachments 2024-03-18 10:22:59 +05:00
Ammar Ahmed
1d6045f5ac mobile: fix all attachments showin in note attachments 2024-03-18 10:22:59 +05:00
Abdullah Atta
3d286f6899 editor: #dialogContainer > .dialogContainer 2024-03-18 10:20:56 +05:00
Abdullah Atta
859c35e387 web: fix editor dom structure 2024-03-18 10:20:31 +05:00
Abdullah Atta
8d16611124 web: fix editor toolbar is not sticky 2024-03-18 08:31:14 +05:00
Ammar Ahmed
36afdf182c editor: fix collapsing a callout collapses all parent callouts 2024-03-16 13:10:16 +05:00
Ammar Ahmed
ad448b567b mobile: fix webpack config 2024-03-16 10:35:21 +05:00
Ammar Ahmed
e80f329f33 mobile: release v3.0.10 2024-03-16 09:54:23 +05:00
Ammar Ahmed
0c6ece1e5b mobile: open note in new tab 2024-03-16 09:43:17 +05:00
Ammar Ahmed
5b2a72c8d6 mobile: fix incorrect tab content by default 2024-03-16 08:03:36 +05:00
Ammar Ahmed
fc3a788575 mobile: fix incorrect button type in dialog 2024-03-16 07:23:10 +05:00
Ammar Ahmed
fd61dbfe32 mobile: fix publish button hidden on tablet 2024-03-15 11:04:04 +05:00
Ammar Ahmed
1a11bea05e mobile: cleanup 2024-03-15 10:54:27 +05:00
Ammar Ahmed
43e7c8c7f9 mobile: update deps 2024-03-15 10:51:36 +05:00
Ammar Ahmed
db90d4c443 mobile: fix app lock shows on session expired sheet 2024-03-15 10:50:53 +05:00
Ammar Ahmed
f375c7d3e4 mobile: support showing diff of conflicted note 2024-03-15 10:50:26 +05:00
Ammar Ahmed
fc5aeeb221 editor: add highlight diff extension 2024-03-15 10:49:37 +05:00
Ammar Ahmed
31685d12bd mobile: remove redundant code 2024-03-15 09:02:15 +05:00
Ammar Ahmed
5ffa11a7f6 mobile: render title after loaded 2024-03-15 08:51:24 +05:00
Ammar Ahmed
08e8035fcb mobile: bump android version 2024-03-15 08:08:16 +05:00
Ammar Ahmed
76b3c5ed7a mobile: fix button action 2024-03-15 08:07:11 +05:00
Ammar Ahmed
29468916e3 mobile: check if editorController.current is undefined 2024-03-15 08:05:22 +05:00
Ammar Ahmed
dde5dcd8fb mobile: cleanup 2024-03-15 07:44:33 +05:00
Ammar Ahmed
381184f8c4 mobile: add readonly editor 2024-03-15 07:43:25 +05:00
Abdullah Atta
5a78d9e517 web: make opfs safer by allowing only 1 operation at a time 2024-03-15 07:14:08 +05:00
Abdullah Atta
9edf5402c9 web: fix more tests 2024-03-14 13:07:01 +05:00
Abdullah Atta
74b49f24a8 editor: fix some images not loading 2024-03-14 13:07:01 +05:00
Abdullah Atta
dfcbf3045a web: fix lock/unlock from properties not working 2024-03-14 13:07:01 +05:00
Abdullah Atta
345922e74a web: fix title of locked note not saving 2024-03-14 13:07:01 +05:00
Abdullah Atta
21475c0514 web: update url on switching between sessions 2024-03-14 13:07:01 +05:00
Abdullah Atta
d498a7a2b9 web: hide tags input in new session 2024-03-14 13:07:01 +05:00
Abdullah Atta
e627c3326c web: bring back edited date in footer 2024-03-14 13:07:01 +05:00
Abdullah Atta
ad35e83269 web: fix typing fast in title input caused some characters to miss 2024-03-14 13:07:01 +05:00
Ammar Ahmed
38f04ca70a mobile: fix webpack config 2024-03-14 11:57:59 +05:00
Abdullah Atta
eeeda75408 global: update lockfiles 2024-03-14 11:11:52 +05:00
Abdullah Atta
5aec94f8a9 editor: fix type errors 2024-03-14 11:11:39 +05:00
Ammar Ahmed
ea274c5adf mobile: release v3.0.9 2024-03-14 10:56:12 +05:00
Abdullah Atta
81408f7763 web: show error if backup import fails 2024-03-14 10:53:23 +05:00
Ammar Ahmed
15dc333969 mobile: readd editor overlay 2024-03-14 10:44:55 +05:00
Ammar Ahmed
44a07c7cf6 mobile: fix tab bugs 2024-03-14 10:43:49 +05:00
Abdullah Atta
114338ab80 web: bump version to 3.0.7 2024-03-14 10:42:22 +05:00
Abdullah Atta
64e0129107 editor: do not add data-block-id attribute if no blockid 2024-03-14 10:37:28 +05:00
Abdullah Atta
749eea4979 editor: highlight active block for 5 seconds 2024-03-14 10:37:06 +05:00
Abdullah Atta
7ba785aa32 core: fix imports 2024-03-14 10:27:58 +05:00
Abdullah Atta
b4ec3fbbf1 web: improve internal links ui 2024-03-14 10:21:36 +05:00
Abdullah Atta
c0ff54afbb core: do not wrap text in content blocks 2024-03-14 10:13:19 +05:00
Abdullah Atta
6807871bb0 web: ignore realtime sync if it doesn't update content or note title 2024-03-14 10:04:00 +05:00
Abdullah Atta
6f07a3c03c web: fix behavior of editor action bar 2024-03-14 10:03:28 +05:00
Abdullah Atta
539c270c18 web: always show note statistics of active editor 2024-03-14 10:03:05 +05:00
Abdullah Atta
a7b190116a web: fix diff viewer not showing images 2024-03-14 10:02:42 +05:00
Abdullah Atta
ecba9f87be web: fix concurrent opfs reads throwing errors 2024-03-14 10:02:21 +05:00
Abdullah Atta
b11539d20f web: await promises 2024-03-14 10:01:47 +05:00
Abdullah Atta
66469b1fd5 web: use database updated event to add editor reactivity 2024-03-14 10:01:34 +05:00
Abdullah Atta
e098d65008 core: fix attachment not unlinked from notes on removal 2024-03-14 09:59:22 +05:00
Abdullah Atta
b837b9728e core: publish database updated events on content remove/update 2024-03-14 09:59:02 +05:00
Abdullah Atta
959f34c05c core: refactor database update event logic 2024-03-14 09:58:36 +05:00
Abdullah Atta
7465d4d149 core: add method to rebuild trash cache 2024-03-14 09:57:47 +05:00
Abdullah Atta
670e2bbd6d core: simplify remove from all notebooks logic 2024-03-14 09:57:02 +05:00
Abdullah Atta
7e4120e242 core: publish database updated event on relations unlink 2024-03-14 09:55:01 +05:00
Abdullah Atta
a76b26e64c core: fix note not unlinked from vault on unlock 2024-03-14 09:54:21 +05:00
Abdullah Atta
84d91d70d1 core: rebuild trash cache on sync complete 2024-03-14 09:54:00 +05:00
Abdullah Atta
cc2ba899e8 web: reopen notes after permanently deleting an attachment 2024-03-14 06:42:09 +05:00
Abdullah Atta
047efd5079 web: fix content is not updated for new notes in realtime sync 2024-03-14 06:41:39 +05:00
Abdullah Atta
3ae297ca04 web: update editor sessions in realtime 2024-03-14 06:41:17 +05:00
Abdullah Atta
82c7bcd0db web: do not show internal links, notebooks etc in properties for trash preview 2024-03-14 06:40:19 +05:00
Abdullah Atta
00e087023a web: fix editor title not changing on realtime sync 2024-03-14 06:39:39 +05:00
Abdullah Atta
c254f453ce web: special icons for readonly and trash preview tabs 2024-03-14 06:39:12 +05:00
Abdullah Atta
fb903f48e7 web: fix trash note preview is not readonly 2024-03-14 06:38:30 +05:00
Abdullah Atta
248d2a87b3 core: standarize database updated event 2024-03-13 11:01:50 +05:00
Abdullah Atta
268f5f012b global: upgrade typescript to 5.4.2 2024-03-13 09:01:12 +05:00
Abdullah Atta
ba0e8d5d3f web: fix type errors 2024-03-13 09:01:12 +05:00
Abdullah Atta
925cc7a3bc core: sort & group reminders in sqlite 2024-03-13 09:01:12 +05:00
Ammar Ahmed
e39e5a7bf0 mobile: minor cleanup 2024-03-13 08:59:57 +05:00
Ammar Ahmed
57de6fd014 mobile: fix search in share extension 2024-03-12 11:55:57 +05:00
Ammar Ahmed
679f6644f7 editor: do not focus editor on end search 2024-03-12 11:55:57 +05:00
Ammar Ahmed
b5e18fe350 mobile: fix editor tab bugs 2024-03-12 11:55:57 +05:00
Ammar Ahmed
9156c57b39 mobile: fix use-db-item hook not working 2024-03-12 11:55:57 +05:00
Ammar Ahmed
02bf88252c mobile: fix attachment exists check when uploading file 2024-03-12 11:55:57 +05:00
Ammar Ahmed
fe54930332 mobile: fix attachment progress not working 2024-03-12 11:55:57 +05:00
Ammar Ahmed
b7d72437de mobile: fix note with attachments not saved from share extension 2024-03-12 11:55:57 +05:00
Ammar Ahmed
2250299608 mobile: fix share extension append to exisitng note not working 2024-03-12 11:55:57 +05:00
Ammar Ahmed
1807109805 common: up version when migrating toolbar 2024-03-12 11:55:57 +05:00
Abdullah Atta
8b47626173 core: fix table x has no column y errors 2024-03-12 08:24:53 +05:00
Ammar Ahmed
ee9474afb7 mobile: blur editor on back 2024-03-12 08:24:53 +05:00
Abdullah Atta
a9b4c5e256 web: use dnd-kit for smoother drag/drop in tabs 2024-03-12 08:24:53 +05:00
Abdullah Atta
d41f049cee web: always sort pinned tabs first 2024-03-12 08:24:53 +05:00
Abdullah Atta
0e43408bff web: fix pinned tabs getting unpinned after app refresh 2024-03-12 08:24:53 +05:00
Ammar Ahmed
1c0a8e00a6 mobile: add useNoteLocked hook 2024-03-12 08:24:53 +05:00
Ammar Ahmed
c106db2422 mobile: fix ui of linked blocks 2024-03-12 08:24:53 +05:00
Ammar Ahmed
23734558dd mobile: fix jump to block 2024-03-12 08:24:53 +05:00
Ammar Ahmed
8ca8f54c33 mobile: fix content block ui 2024-03-12 08:24:53 +05:00
Ammar Ahmed
cad349ca12 mobile: fix loading placeholder position 2024-03-12 08:24:53 +05:00
Ammar Ahmed
ac7fb315d6 mobile: fix colors and size of icons in editor header 2024-03-12 08:24:53 +05:00
Ammar Ahmed
d7302a2cf0 mobile: use flatlist for rendering tabs 2024-03-12 08:24:53 +05:00
Ammar Ahmed
3254a1ff30 mobile: show keyboard when opening new tab or empty tab 2024-03-12 08:24:52 +05:00
Ammar Ahmed
2cab3dd486 mobile: make tab active on focus 2024-03-12 08:24:52 +05:00
Ammar Ahmed
e2767f2f77 mobile: fix tabs 2024-03-12 08:24:52 +05:00
Ammar Ahmed
4382576bf9 mobile: fix minor bugs in tabs 2024-03-12 08:24:52 +05:00
Ammar Ahmed
e89f7e3819 mobile: fix duplicate imports 2024-03-12 08:24:31 +05:00
Abdullah Atta
bced2b7872 web: bring back editor margins toggle in action bar 2024-03-12 08:24:31 +05:00
Abdullah Atta
7c6596f826 web: minor ui fixes 2024-03-12 08:24:31 +05:00
Abdullah Atta
1afad07b2d web: fix section subtitle horizontal margin 2024-03-12 08:24:31 +05:00
Abdullah Atta
43ccb398c0 web: add placeholder in table of contents 2024-03-12 08:24:30 +05:00
Abdullah Atta
5fbeb152e5 web: update diff content on tab switch 2024-03-12 08:24:30 +05:00
Abdullah Atta
d5e7fbabaa web: minor refactor 2024-03-12 08:24:30 +05:00
Abdullah Atta
33ff702295 web: fix properties section horizontal margin 2024-03-12 08:24:30 +05:00
Abdullah Atta
f3322cc8f1 core: remove note from vault if its content is not locked 2024-03-12 08:24:30 +05:00
Abdullah Atta
4356d10028 web: fix locked notes getting overwritten with unencrypted content 2024-03-12 08:24:30 +05:00
Abdullah Atta
371984a366 web: fix diff session not opening after app refresh 2024-03-12 08:24:30 +05:00
Abdullah Atta
e25016354e editor: fix search decorations not cleared on search end 2024-03-12 08:24:30 +05:00
Abdullah Atta
8fe4bddb85 web: add all missing functionality in note linking dialog 2024-03-12 08:24:30 +05:00
Abdullah Atta
fc121ef396 web: remove custom search state logic & use editor commands 2024-03-12 08:24:30 +05:00
Abdullah Atta
73a1295e29 editor: fix text on selected search result not visible 2024-03-12 08:24:30 +05:00
Abdullah Atta
8abf57ae24 editor: use global search store for managing search state 2024-03-12 08:24:30 +05:00
Abdullah Atta
a0f14dc3ef editor: minor refactor 2024-03-12 08:24:30 +05:00
Abdullah Atta
ce8cba4f13 web: remove locked session type guard (switch to simple boolean)
the type guard was incorrect
2024-03-12 08:24:30 +05:00
Abdullah Atta
e726bd073b web: add content to new session type 2024-03-12 08:24:30 +05:00
Abdullah Atta
3ee5ea097b web: fix editor title not updating on creating a note 2024-03-12 08:24:30 +05:00
Abdullah Atta
6272e8b805 web: remove preview session type 2024-03-12 08:24:30 +05:00
Abdullah Atta
ae5c323d3b web: ${id}_editorScroll > editorScroll_${id} 2024-03-12 08:24:29 +05:00
Abdullah Atta
afc3ede9b0 web: set session content before saving 2024-03-12 08:24:11 +05:00
Abdullah Atta
7671e0e297 web: add dialog container inside tab 2024-03-12 08:24:11 +05:00
Abdullah Atta
005f2d8ef1 web: handle locked notes properly in real time editor sync 2024-03-12 08:24:11 +05:00
Abdullah Atta
7b20acaa5a web: freeze unhydrated sessions 2024-03-12 08:24:11 +05:00
Abdullah Atta
da938ec348 web: remove unused & commented out code 2024-03-12 08:24:11 +05:00
Abdullah Atta
d32d438eeb web: focus editor on switching tab and toggle active class 2024-03-12 08:23:47 +05:00
Abdullah Atta
e289caf561 web: define editor & session once and pass it everywhere 2024-03-12 08:23:47 +05:00
Abdullah Atta
e8cd0c0a52 web: remove unlocked session type 2024-03-12 08:23:47 +05:00
Abdullah Atta
62bdee7789 web: set session id on saving note 2024-03-12 08:23:47 +05:00
Abdullah Atta
ccb9c492a8 core: add block ids before returning content 2024-03-12 08:23:46 +05:00
Abdullah Atta
4506e5e90a editor: fix range error crash 2024-03-12 08:23:46 +05:00
Abdullah Atta
2cc772106b editor: do not add block id on list items
(because they already contain paragraphs and we don't want duplicates)
2024-03-12 08:23:46 +05:00
Abdullah Atta
a82caf5da2 editor: do not touch editor when its view is destroyed 2024-03-12 08:23:46 +05:00
Abdullah Atta
1cebcb7876 editor: fix selection/focus is lost when editor state is updated 2024-03-12 08:23:46 +05:00
Abdullah Atta
0247903772 editor: fix list type thumbnails in toolbar 2024-03-12 08:23:46 +05:00
Abdullah Atta
1f5164660d editor: hide hover popup on editor destroy 2024-03-12 08:23:46 +05:00
Abdullah Atta
3a835ad90d editor: render interactive popups in popup instead of menu 2024-03-12 08:23:46 +05:00
Abdullah Atta
18c13ed8fe editor: scroll blocks into view only for active editor 2024-03-12 08:23:46 +05:00
Abdullah Atta
35345b9214 editor: query editor dom based on active editor class 2024-03-12 08:23:46 +05:00
Abdullah Atta
2c98b98765 editor: render all popups in popup container 2024-03-12 08:23:46 +05:00
Abdullah Atta
ace224b03c editor: close all popups and dropdowns on switching tabs 2024-03-12 08:23:46 +05:00
Abdullah Atta
23e9b07310 editor: fix placeholder is not visible 2024-03-12 08:23:46 +05:00
Abdullah Atta
fdc5642b3c ui: allow setting popup container 2024-03-12 08:23:46 +05:00
Abdullah Atta
9c81432dfd web: disable properties button in action bar when diff is open 2024-03-12 08:23:46 +05:00
Abdullah Atta
1825796649 web: allow session item size to grow 2024-03-12 08:23:46 +05:00
Abdullah Atta
9c5940e872 web: add support for /notes/:noteId/edit hash route 2024-03-12 08:23:46 +05:00
Abdullah Atta
ad77334e00 core: do not change date modified on items when bulk adding 2024-03-12 08:23:46 +05:00
Abdullah Atta
761b5b7793 editor: make image size clamping responsive 2024-03-12 08:23:45 +05:00
Abdullah Atta
eadf5260c4 web: fix race condition where editor context would be destroyed and updated at the same time 2024-03-12 08:23:45 +05:00
Abdullah Atta
bf62cd93b2 web: create error from ErrorEvent if none found 2024-03-12 08:23:45 +05:00
Abdullah Atta
92c9f31e01 editor: fix crash on loading web clip 2024-03-12 08:23:45 +05:00
Abdullah Atta
3404999bcb web: reenable tags refreshing in editor 2024-03-12 08:23:45 +05:00
Abdullah Atta
e9816ef3a2 editor: fix position of loading placeholder for floating image 2024-03-12 08:23:45 +05:00
Abdullah Atta
f10b7c9b66 editor: fix react node views not rendering on editor load 2024-03-12 08:23:45 +05:00
Abdullah Atta
36cf1fb950 web: fix crash on refreshing editor store 2024-03-12 08:23:45 +05:00
Ammar Ahmed
c1f4a4e3ab mobile: fix randId function duplicate 2024-03-12 08:23:45 +05:00
Abdullah Atta
50a22ddf85 editor: fix crash when resizing window due to a memory leak 2024-03-12 08:23:45 +05:00
Abdullah Atta
d3d74ff7df web: fix temporary tabs cannot be made permanent 2024-03-12 08:23:45 +05:00
Abdullah Atta
ed91e1e22f web: fix jumping tabs when pinning/unpinning tabs 2024-03-12 08:23:45 +05:00
Abdullah Atta
a765d22f4b web: fix crashes when moving tabs 2024-03-12 08:23:45 +05:00
Abdullah Atta
aca0e40fd9 web: fix new sessions can't be opened after app refresh 2024-03-12 08:23:45 +05:00
Abdullah Atta
183613296b web: fix type errors 2024-03-12 08:23:45 +05:00
Ammar Ahmed
3d75791006 editor: remove editor.current usage 2024-03-12 08:23:45 +05:00
Ammar Ahmed
516c214174 mobile: fix editor loading multiple times 2024-03-12 08:23:44 +05:00
Abdullah Atta
541868e681 web: use createInternalLink everywhere 2024-03-12 08:23:44 +05:00
Ammar Ahmed
c18513a04d mobile: finalize note linking 2024-03-12 08:23:44 +05:00
Ammar Ahmed
37220256fa core: parse internal link from href 2024-03-12 08:23:44 +05:00
Ammar Ahmed
e28a4b4e23 core: fix extracting internal links 2024-03-12 08:23:44 +05:00
Abdullah Atta
04d66576cb web: show references & linked notes in properties 2024-03-12 08:23:44 +05:00
Abdullah Atta
63ae8a4a86 web: reset active block after scroll 2024-03-12 08:23:44 +05:00
Abdullah Atta
6e0404fe8b core: add internal links extraction 2024-03-12 08:23:44 +05:00
Abdullah Atta
db79ad3f47 web: add full support for adding internal links 2024-03-12 08:23:44 +05:00
Abdullah Atta
46ac983770 editor: add support for adding/removing internal links 2024-03-12 08:23:44 +05:00
Abdullah Atta
03cc358e61 core: add support for processing internal links in note content 2024-03-12 08:23:43 +05:00
Ammar Ahmed
e457215248 mobile: note linking 2024-03-12 08:23:43 +05:00
Ammar Ahmed
bf393b8015 mobile: update deps 2024-03-12 08:21:28 +05:00
Abdullah Atta
b8eee77646 web: fix type errors and broken imports 2024-03-12 08:21:28 +05:00
Abdullah Atta
4ef4f8798c editor: reset editor on options update 2024-03-12 08:21:28 +05:00
Abdullah Atta
99886fdc96 web: add support for table of contents 2024-03-12 08:21:27 +05:00
Abdullah Atta
80ef5c890a web: add support for tabs 2024-03-12 08:21:03 +05:00
Abdullah Atta
c925ddd462 common: add utility for formatting history session time 2024-03-12 08:19:11 +05:00
Abdullah Atta
73d380d069 mobile: simplify unencrypted content check 2024-03-12 08:19:11 +05:00
Abdullah Atta
a67515118c core: simplify encryption content checks 2024-03-12 08:19:11 +05:00
Abdullah Atta
ad46f82337 core: restoring a history session should create a new version 2024-03-12 08:19:11 +05:00
Abdullah Atta
055a991fcb editor: create editor only once 2024-03-12 08:19:11 +05:00
Ammar Ahmed
d23c8203a3 mobile: implement toc 2024-03-12 08:19:11 +05:00
Ammar Ahmed
3b0ec278d4 mobile: support vault notes in tabs 2024-03-12 08:19:11 +05:00
Ammar Ahmed
55b4c77ad1 mobile: fix tab bugs 2024-03-12 08:19:11 +05:00
Ammar Ahmed
aa74541d6e mobile: add tab history 2024-03-12 08:19:11 +05:00
Ammar Ahmed
5c5e6c7438 mobile: some more fixes 2024-03-12 08:19:11 +05:00
Ammar Ahmed
a1235b48ef mobile: fix numerous issues with tabs 2024-03-12 08:19:10 +05:00
Ammar Ahmed
e015b180a8 mobile: push changes 2024-03-12 08:19:10 +05:00
Ammar Ahmed
e3c37c470d mobile: add editor tabs 2024-03-12 08:19:10 +05:00
Abdullah Atta
855203bd0e core: remove unnecessary notes collection init before migration 2024-03-12 08:19:10 +05:00
Abdullah Atta
6e4700121a web: bump version to 3.0.6 2024-03-12 08:19:10 +05:00
Abdullah Atta
a67b04db7b setup: only disable esbuild rebuild on mobile 2024-03-12 08:19:10 +05:00
Abdullah Atta
0f84d02abc setup: ignore esbuild rebuild script 2024-03-12 08:19:10 +05:00
Abdullah Atta
85c2be9500 mobile: update package lockfile 2024-03-12 08:19:10 +05:00
Abdullah Atta
a5a05f501a editor: fix task items not showing correctly 2024-03-12 08:19:10 +05:00
Abdullah Atta
9bb946d27e ui: fix menu is not scrollable on small screens 2024-03-12 08:19:10 +05:00
Abdullah Atta
cdd089fcf3 web: fix loading skeleton for compact items in list 2024-03-12 08:19:10 +05:00
Ammar Ahmed
49d4d2b6f9 mobile: bump version code 2024-03-12 08:19:09 +05:00
Ammar Ahmed
8d1408997f mobile: release v3.0.8 2024-03-12 08:19:09 +05:00
Ammar Ahmed
d18a8742f1 mobile: improve notebook sheet wording 2024-03-12 08:19:09 +05:00
Ammar Ahmed
d9d229b020 mobile: change sheet button positions 2024-03-12 08:19:09 +05:00
Ammar Ahmed
045527d502 mobile: cleanup 2024-03-12 08:19:09 +05:00
Ammar Ahmed
3c29303ac8 mobile: reminder notification shows even after removing reminders 2024-03-12 08:19:09 +05:00
Ammar Ahmed
98c6ee7904 mobile: simplify reminder sheet ui 2024-03-12 08:19:09 +05:00
Ammar Ahmed
a1dc396564 mobile: remove usage of customStyle prop 2024-03-12 08:19:09 +05:00
Abdullah Atta
4767597fc1 core: fix settings are not migrated 2024-03-12 08:19:09 +05:00
Ammar Ahmed
1e05512c98 mobile: update deps 2024-03-12 08:19:09 +05:00
Ammar Ahmed
68908e0e87 mobile: fix tags not showing properly 2024-03-12 08:19:09 +05:00
Ammar Ahmed
1808b84b0b mobile: cleanup 2024-03-12 08:19:09 +05:00
Ammar Ahmed
1a5595e3db mobile: update Pods 2024-03-12 08:19:08 +05:00
Ammar Ahmed
a96250b937 mobile: release v3.0.7 beta 2024-03-12 08:19:08 +05:00
Ammar Ahmed
19f4fbfcf7 mobile: fix sorting attachments 2024-03-12 08:19:08 +05:00
Abdullah Atta
35c8848b89 web: fix height of attachments table 2024-03-12 08:19:08 +05:00
Abdullah Atta
970f51595b core: do not include deleted attachments in filters 2024-03-12 08:19:08 +05:00
Abdullah Atta
b31d279e15 web: bump version to 3.0.5 2024-03-12 08:19:08 +05:00
Ammar Ahmed
f708eacdc3 mobile: fix mobile sheet ui 2024-03-12 08:19:08 +05:00
Abdullah Atta
7827a35de9 web: show error if there's no internet on updating profile 2024-03-12 08:19:08 +05:00
Abdullah Atta
5e0217bcb1 web: allow clearing database on database disk image is malformed 2024-03-12 08:19:08 +05:00
Abdullah Atta
54debb035a web: fix error due to changed file picker return 2024-03-12 08:19:08 +05:00
Abdullah Atta
3e42d3020d web: fix attachFile- when saving web clip 2024-03-12 08:19:08 +05:00
Ammar Ahmed
c7ab4c9ccf mobile: enable auto sync 2024-03-12 08:19:08 +05:00
Ammar Ahmed
d684c3a8b2 mobile: finalize image upload 2024-03-12 08:19:08 +05:00
Abdullah Atta
3feee27ad6 web: add image compression 2024-03-12 08:19:08 +05:00
Abdullah Atta
4add55adf2 web: add support for multiselecting attachments 2024-03-12 08:19:08 +05:00
ammarahm-ed
4ba90510ea mobile: handle uploading multiple files 2024-03-12 08:19:07 +05:00
ammarahm-ed
4b693b301f mobile: add optional image compression on upload 2024-03-12 08:19:07 +05:00
Abdullah Atta
a9fcd11e11 web: fix sync icon spin direction 2024-03-12 08:19:07 +05:00
Abdullah Atta
01ee348870 core: fix migration for db version > 6 2024-03-12 08:19:07 +05:00
Abdullah Atta
c0cec22e10 core: only get attachment id when processing content 2024-03-12 08:19:07 +05:00
Abdullah Atta
12b87cceeb web: enable auto sync 2024-03-12 08:19:07 +05:00
Abdullah Atta
b9cd40937d web: edit profile directly 2024-03-12 08:19:07 +05:00
Ammar Ahmed
1efae55a85 mobile: add user profile 2024-03-12 08:19:07 +05:00
Ammar Ahmed
932cf0c877 core: fetch user profile before updating profile 2024-03-12 08:19:07 +05:00
Abdullah Atta
0ba6be4fe6 web: add support for setting user profile 2024-03-12 08:19:06 +05:00
Abdullah Atta
cdaa3184a9 core: add support for setting user profile 2024-03-12 08:18:50 +05:00
Ammar Ahmed
03445bb45a mobile: cleanup 2024-03-12 08:18:50 +05:00
Ammar Ahmed
9d649c9bce mobile: fix note assigned to multiple colors 2024-03-12 08:18:50 +05:00
Ammar Ahmed
9a34e4884a mobile: small ui fixes 2024-03-12 08:18:50 +05:00
Ammar Ahmed
df0b51f5ec mobile: ui bug fixes 2024-03-12 08:18:50 +05:00
Ammar Ahmed
2554e5d63a mobile: user profile 2024-03-12 08:18:50 +05:00
Abdullah Atta
5479d800cf core: fix migration errors & compact deleted items 2024-03-12 08:17:21 +05:00
Ammar Ahmed
f729b8f908 mobile: release v3.0.6 2024-03-12 08:17:21 +05:00
Ammar Ahmed
af21da8f2e mobile: fix toolbar migration 2024-03-12 08:17:21 +05:00
Ammar Ahmed
9d13e9f700 core: check for deleted attachments in pending attachments 2024-03-12 08:17:21 +05:00
Ammar Ahmed
d9932a68d9 mobile: fix backup sheet never closes on manual backups 2024-03-12 08:17:21 +05:00
Ammar Ahmed
17a9d1851f mobile: allow sort by title in all groupings 2024-03-12 08:17:20 +05:00
Abdullah Atta
17f68b5315 desktop: disable os encryption of keystore in portable app 2024-03-12 08:16:11 +05:00
Abdullah Atta
b1ce214571 web: migrate toolbar on first use 2024-03-12 08:16:11 +05:00
Abdullah Atta
cfeba093d9 common: add toolbar migration 2024-03-12 08:16:11 +05:00
Abdullah Atta
69038d8edd global: update lockfiles 2024-03-12 08:16:10 +05:00
Abdullah Atta
934faf3b87 web: bump version to 3.0.4-beta 2024-03-12 08:16:01 +05:00
Abdullah Atta
71272d51c9 common: get note's locked status from its content 2024-03-12 08:16:01 +05:00
Abdullah Atta
b295e79677 core: always delete vault references on vault deletion 2024-03-12 08:16:01 +05:00
Abdullah Atta
74789675eb web: fix note not selected after switching from locked note 2024-03-12 08:16:01 +05:00
Abdullah Atta
1234d289eb core: fix sync tests 2024-03-12 08:16:01 +05:00
Abdullah Atta
723c29bad6 core: fix database image is malformed error when deleting vault 2024-03-12 08:16:01 +05:00
Abdullah Atta
343f1f320c core: fix database image is malformed error on unlocking a note 2024-03-12 08:16:01 +05:00
Abdullah Atta
50e70b90db web: fix sync not running after login 2024-03-12 08:16:01 +05:00
Abdullah Atta
a2fd5b266c web: refresh nav items on deleting a tag 2024-03-12 08:16:01 +05:00
Abdullah Atta
ebf32f0603 core: fix database image is malformed error 2024-03-12 08:16:01 +05:00
Abdullah Atta
4dae3a424c core: speed up backup restore 2024-03-12 08:16:01 +05:00
Ammar Ahmed
086aa53212 mobile: fix color of + button on hidden items
in side menu
2024-03-12 08:16:01 +05:00
Abdullah Atta
6d4b53b489 core: fix deleted items getting imported during backup restore 2024-03-12 08:16:00 +05:00
Abdullah Atta
c09064d2d6 core: fix duplicate colors created during backup restore 2024-03-12 08:16:00 +05:00
Ammar Ahmed
024be4ce89 mobile: fix readonly mode 2024-03-12 08:16:00 +05:00
Ammar Ahmed
513d6b3d72 editor: fix keyboard opens on drag in editor 2024-03-12 08:16:00 +05:00
Ammar Ahmed
513b4016b9 mobile: clear selection after deleting items 2024-03-12 08:16:00 +05:00
Ammar Ahmed
8de57a1b5d mobile: fix reminder ui in selection mode 2024-03-12 08:16:00 +05:00
Ammar Ahmed
34aba64bde mobile: remove pass min limit on vault 2024-03-12 08:16:00 +05:00
Abdullah Atta
eedfe38b26 web: clear editor if opened note is unlocked 2024-03-12 08:16:00 +05:00
Abdullah Atta
36406875fc core: remove note's vault relation regardless of vault 2024-03-12 08:15:43 +05:00
Abdullah Atta
854c2323e6 core: don't unlink vault notes on vault remove 2024-03-12 08:15:43 +05:00
Abdullah Atta
1c3694df9b web: fix vault settings not updating on deleting vault 2024-03-12 08:15:43 +05:00
Abdullah Atta
305b62b1a2 core: fix sqlite crash on calling delete with empty array 2024-03-12 08:15:43 +05:00
Abdullah Atta
5550a9f592 web: fix unnecessary rerendering of notebook header 2024-03-12 08:15:43 +05:00
Abdullah Atta
a97d917c7a web: disable auto sync 2024-03-12 08:15:42 +05:00
Abdullah Atta
a55e188ba8 core: enable sync push 2024-03-12 08:15:42 +05:00
Abdullah Atta
f0a70ac4da web: add drag/drop tests 2024-03-12 08:15:42 +05:00
Abdullah Atta
4d675a87a6 web: fix cannot navigate into nested notebook by hovering on it 2024-03-12 08:15:42 +05:00
Abdullah Atta
38c0a5a034 editor: fix react error 409 2024-03-12 08:15:42 +05:00
Abdullah Atta
607e901673 web: expand parent when a child notebook is added 2024-03-12 08:15:42 +05:00
Muhammad Ali
35b44c69b9 web: add support for item drag/droping (#3230)
* web: basic drag drop function for single note

* web: added check for noteId

* web: add support for item drag drop

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-12 08:15:42 +05:00
Ammar Ahmed
d920c73acb mobile: disable auto sync 2024-03-12 08:15:42 +05:00
Ammar Ahmed
34f909e37f editor: fix callout margins 2024-03-12 08:15:42 +05:00
Ammar Ahmed
a33970250e mobile: fix keyboard jump on tap in dropdown 2024-03-12 08:15:42 +05:00
Abdullah Atta
297ca467d4 editor: fix rtl for outline list 2024-03-12 08:15:42 +05:00
Abdullah Atta
eeeb0908dd editor: add callout component 2024-03-12 08:15:42 +05:00
Ammar Ahmed
359a5ead1b mobile: release v3.0.5 beta 2024-03-12 08:15:42 +05:00
Ammar Ahmed
92fe455fc3 mobile: update toolbar 2024-03-12 08:15:41 +05:00
Abdullah Atta
31b2fe7e85 editor: add support for simple checklists 2024-03-12 08:15:41 +05:00
Ammar Ahmed
4b9f284ed9 editor: implement simple checklists 2024-03-12 08:15:25 +05:00
Abdullah Atta
85c812f271 web: show error if saving a note fails 2024-03-12 08:15:25 +05:00
Abdullah Atta
4aa4167351 core: fix database image is malformed error on updating locked note 2024-03-12 08:15:25 +05:00
Abdullah Atta
a8dfd91d12 web: bump version to 3.0.3-beta 2024-03-12 08:15:25 +05:00
Abdullah Atta
d23321aa68 desktop: include preload script when comparing bundles 2024-03-12 08:15:25 +05:00
Abdullah Atta
6c4ac1f2e7 core: add more lookup tests 2024-03-12 08:15:25 +05:00
Abdullah Atta
35e8b4bad6 core: allow testing with persisted database 2024-03-12 08:15:25 +05:00
Abdullah Atta
3285664e9f core: return items in same order when using .records 2024-03-12 08:15:24 +05:00
Abdullah Atta
7839bc2b6d core: improve notes lookup performance 2024-03-12 08:15:24 +05:00
Abdullah Atta
65f725dd00 core: refactor test setup & use better-sqlite-multiple-cipher 2024-03-12 08:15:24 +05:00
Abdullah Atta
b1f8b3d8f8 web: destroy sql database on app close 2024-03-12 08:15:24 +05:00
Abdullah Atta
83b6cbd98e core: fix database image is malformed error 2024-03-12 08:15:24 +05:00
Abdullah Atta
89737d9f90 core: fix note headling getting removed 2024-03-12 08:15:24 +05:00
Abdullah Atta
3b0e873c31 web: fix notebook indentation 2024-03-12 08:15:24 +05:00
Abdullah Atta
4bcd82fd7a core: fix sorting of newly created notes 2024-03-12 08:15:24 +05:00
Abdullah Atta
f5de8caa0d web: allow sorting by title everywhere 2024-03-12 08:15:24 +05:00
Abdullah Atta
13b821e061 core: add support for sorting by title & date together 2024-03-12 08:15:24 +05:00
Abdullah Atta
479f60518b web: add child notebook under opened notebook 2024-03-12 08:15:24 +05:00
Abdullah Atta
a71ac363bb web: rerender subnotebooks on root notebook change 2024-03-12 08:15:24 +05:00
Abdullah Atta
3b49a06d29 web: hide sort button in notebook view 2024-03-12 08:15:24 +05:00
Abdullah Atta
e443f3a8a3 web: await notes context refreshing 2024-03-12 08:15:24 +05:00
Abdullah Atta
6bb67318d8 web: clear keystore when clearing database 2024-03-12 08:15:24 +05:00
Abdullah Atta
0e79a5bfff web: allow clearing keystore 2024-03-12 08:15:24 +05:00
Abdullah Atta
20e7ff88b0 web: fix empty root notebook indentation 2024-03-12 08:15:24 +05:00
Abdullah Atta
e36f57850f web: fix monograph publish status not updating 2024-03-12 08:15:24 +05:00
Abdullah Atta
f76446440b web: fix nested notebook navigation from shortcuts 2024-03-12 08:15:23 +05:00
Abdullah Atta
873c3824e5 web: fix color cannot be renamed 2024-03-12 08:15:23 +05:00
Abdullah Atta
0db0b56be6 web: allow non-pro users to customize sidebar 2024-03-12 08:15:23 +05:00
Abdullah Atta
d3a7ec6b8c web: reimplement app lock & web key store 2024-03-12 08:15:23 +05:00
Ammar Ahmed
9d58dd9bab mobile: fix app lock on ios 2024-03-12 08:15:23 +05:00
Ammar Ahmed
409f5f1706 mobile: v3.0.4 2024-03-12 08:15:23 +05:00
Ammar Ahmed
1cd0c7b80a editor: fix color picker missing prop 2024-03-12 08:15:23 +05:00
Ammar Ahmed
bd6a2b2c48 mobile: improve verification flow before changing app lock 2024-03-12 08:15:23 +05:00
Ammar Ahmed
065a7fc683 mobile: fix reordering in side menu 2024-03-12 08:15:23 +05:00
Ammar Ahmed
adc55f719d editor: fix keyboard focus issues on mobile 2024-03-12 08:15:23 +05:00
Ammar Ahmed
4a266487b5 mobile: fix bugs 2024-03-12 08:15:23 +05:00
Ammar Ahmed
d7d5623cd6 mobile: fix sidemenu shortcuts 2024-03-12 08:15:22 +05:00
Abdullah Atta
5d21d3b621 web: remove security key config on app lock disable 2024-03-12 08:15:22 +05:00
Abdullah Atta
e622a379fd core: fix object id date parsing 2024-03-12 08:15:22 +05:00
Abdullah Atta
34acf8488e web: fix cr.isIndexedDBSupported is not a function 2024-03-12 08:15:22 +05:00
Ammar Ahmed
6d45a324db mobile: v3.0.3 beta 2024-03-12 08:15:22 +05:00
Ammar Ahmed
c8ccd31fe0 mobile: fix backup failed on iOS 2024-03-12 08:15:22 +05:00
Ammar Ahmed
6a2734acff mobile: bump version 2024-03-12 08:15:22 +05:00
Ammar Ahmed
e48aecc198 mobile: fix issue reporting 2024-03-12 08:15:22 +05:00
Ammar Ahmed
129dec46b3 mobile: fix rendering slow in list 2024-03-12 08:15:22 +05:00
Abdullah Atta
cbb360b714 ui: fix right clicking when menu is open doesn't close it 2024-03-12 08:15:22 +05:00
Abdullah Atta
eb9b67c0ca ui: fix hover highlighting 2 items in menu 2024-03-12 08:15:22 +05:00
Abdullah Atta
c71c3d834c web: fix color renaming and add color remove support 2024-03-12 08:15:22 +05:00
Abdullah Atta
3ccfad63bf web: add readonly indicator on notes 2024-03-12 08:15:22 +05:00
Abdullah Atta
806ce833cf web: fix compact view not affecting notes 2024-03-12 08:15:22 +05:00
Abdullah Atta
854a4d4f52 editor: fix Position -1 is outside of fragment error 2024-03-12 08:15:22 +05:00
Ammar Ahmed
9d16428781 mobile: fix migration bugs 2024-03-12 08:15:21 +05:00
Abdullah Atta
d3d4ab2f25 core: minor refactoring 2024-03-12 08:11:12 +05:00
Abdullah Atta
4575b0b516 core: migrate deleted items as well 2024-03-12 08:11:12 +05:00
Abdullah Atta
62226a186a web: refresh app on sync completed 2024-03-12 08:11:12 +05:00
Abdullah Atta
28e21acfc3 core: fix continuous invalid_grant error if login fails 2024-03-12 08:11:11 +05:00
Abdullah Atta
6d24a2e429 web: bump to v3.0.2-beta 2024-03-12 08:11:00 +05:00
Abdullah Atta
5a95ca5be8 web: hide window/tab title on app lock 2024-03-12 08:11:00 +05:00
Abdullah Atta
1ad1b91cc6 core: add support for creating backups of legacy collections 2024-03-12 08:11:00 +05:00
Abdullah Atta
a77a4f552d web: fix P.item is undefined error 2024-03-12 08:11:00 +05:00
Abdullah Atta
dade84ff7f core: remove deleteReason property 2024-03-12 08:11:00 +05:00
Ammar Ahmed
da5a7c7137 Update ios.publish.yml
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-03-12 08:11:00 +05:00
Ammar Ahmed
e21d182cec mobile: cleanup gradle.properties 2024-03-12 08:11:00 +05:00
Ammar Ahmed
189f46cb3d mobile: fix share extension 2024-03-12 08:11:00 +05:00
Ammar Ahmed
ee98d87b11 mobile: beta release 2024-03-12 08:11:00 +05:00
Ammar Ahmed
d3a1aa0b0d mobile: fix notebook move to trash 2024-03-12 08:10:59 +05:00
Ammar Ahmed
b0e304bcea mobile: cleanup 2024-03-12 08:10:59 +05:00
Abdullah Atta
e84afd37a8 desktop: disable downgrades 2024-03-12 08:10:59 +05:00
Ammar Ahmed
64851302f4 mobile: stop app updates check in beta 2024-03-12 08:10:59 +05:00
Ammar Ahmed
dddbc671fb mobile: minor fixes 2024-03-12 08:10:59 +05:00
Abdullah Atta
5ea0254849 web: improve database deletion 2024-03-12 08:10:59 +05:00
Abdullah Atta
7ec9380fc9 web: reload page on reset from error boundary 2024-03-12 08:10:59 +05:00
Abdullah Atta
c9fb213fbb web: locked status not updating on note 2024-03-12 08:10:59 +05:00
Abdullah Atta
9a48f50a79 web: only save note's content through vault 2024-03-12 08:10:59 +05:00
Abdullah Atta
395dd8fb9e core: fix adding note to vault should return its id 2024-03-12 08:10:59 +05:00
Abdullah Atta
fcb88a9764 core: fix database resetting 2024-03-12 08:10:59 +05:00
Abdullah Atta
7d70cd2d3d theme: fix error input variant 2024-03-12 08:10:59 +05:00
Abdullah Atta
e5fbc61c26 web: allow typing custom date when adding reminder 2024-03-12 08:10:59 +05:00
Abdullah Atta
daf201cd6b common: use upcoming reminder when resolving reminders on note 2024-03-12 08:10:59 +05:00
Ammar Ahmed
6210dbc52a mobile: fix reminders 2024-03-12 08:10:59 +05:00
Ammar Ahmed
80f88f85ba mobile: fix notebooks & colors 2024-03-12 08:10:58 +05:00
Abdullah Atta
027e343841 core: set mode to "sync" for migrations during sync 2024-03-12 08:10:58 +05:00
Abdullah Atta
fc2f4ee57d desktop: patch better-sqlite3-multiple-ciphers to get prebuilt binaries 2024-03-12 08:10:58 +05:00
Abdullah Atta
0bd477ce9e web: bump to 3.0.1-beta 2024-03-12 08:10:58 +05:00
Abdullah Atta
145e778397 core: enable sync during tests 2024-03-12 08:10:58 +05:00
Abdullah Atta
0287257905 core: improve backup importing 2024-03-12 08:10:58 +05:00
Abdullah Atta
4b916dff47 core: add support for nested transactions 2024-03-12 08:10:58 +05:00
Abdullah Atta
7d18656033 web: use single transaction for importing a backup 2024-03-12 08:10:58 +05:00
Abdullah Atta
a935b677df web: make sqlite slightly faster 2024-03-12 08:09:52 +05:00
Ammar Ahmed
40ca5a3b4c mobile: fix release build 2024-03-12 08:09:52 +05:00
Abdullah Atta
4f9cd8dbb7 core: fix type errors 2024-03-12 08:09:51 +05:00
Ammar Ahmed
0d08318185 mobile: minor fixes 2024-03-12 08:09:51 +05:00
Ammar Ahmed
961dd2ef30 core: fix migration bugs 2024-03-12 08:09:21 +05:00
Ammar Ahmed
f7a5dbc71c mobile: fix saving user key 2024-03-12 08:09:21 +05:00
Abdullah Atta
303ac1db56 web: fix monograph publishing popup 2024-03-12 08:09:21 +05:00
Abdullah Atta
a421df8f1b web: ui fixes 2024-03-12 08:09:21 +05:00
Abdullah Atta
a8a9f56e02 web: fix app lock not getting disabled 2024-03-12 08:09:21 +05:00
Ammar Ahmed
236d17620d mobile: minor fixes 2024-03-12 08:09:21 +05:00
Ammar Ahmed
946b2a8555 mobile: locked notes 2024-03-12 08:09:21 +05:00
Abdullah Atta
6c868fb589 web: fix indexedDB not getting detected 2024-03-12 08:09:21 +05:00
Abdullah Atta
8bdd4b2638 core: destroy kysely on error 2024-03-12 08:09:21 +05:00
Abdullah Atta
d4deb90616 crypto: move libsodium & sodium-native to peer deps 2024-03-12 08:09:20 +05:00
Abdullah Atta
15744feb41 global: pin node version to 20 2024-03-12 08:09:20 +05:00
Abdullah Atta
e75005df89 web: various fixes 2024-03-12 08:09:20 +05:00
Abdullah Atta
b1fac8f9ce common: fix note resolution 2024-03-12 08:09:20 +05:00
Abdullah Atta
4ef77c5924 core: fix database migration 2024-03-12 08:09:20 +05:00
Abdullah Atta
9f1db293a0 core: disable sync push 2024-03-12 08:09:20 +05:00
Abdullah Atta
03ac543ae0 web: trigger error boundary on unhandled errors 2024-03-12 08:09:20 +05:00
Abdullah Atta
cf929ba1ce web: use safeStorage to encrypt secrets on desktop 2024-03-12 08:09:20 +05:00
Abdullah Atta
f3928db1ab desktop: use better-sqlite3 on desktop for SQLite 2024-03-12 08:09:20 +05:00
Abdullah Atta
e92dbadd8e desktop: fix autoupdater 2024-03-12 08:09:20 +05:00
Abdullah Atta
8eca2cb2b8 desktop: add api to delete files 2024-03-12 08:09:20 +05:00
Abdullah Atta
f0c5951eb0 desktop: expose electron's safeStorage api via trpc 2024-03-12 08:09:20 +05:00
Abdullah Atta
c3d18e4177 desktop: enable native modules rebuild during build 2024-03-12 08:09:20 +05:00
Ammar Ahmed
847037b2f7 core: return decrypted content on unlock note 2024-03-12 08:09:20 +05:00
Ammar Ahmed
6227e50dec mobile: applock with pin and password 2024-03-12 08:09:20 +05:00
Ammar Ahmed
89b6030bc3 mobile: minor fixes 2024-03-12 08:09:19 +05:00
Ammar Ahmed
248f692c2b mobile: sqlite-multiple-ciphers patch 2024-03-12 08:09:19 +05:00
Ammar Ahmed
6bac78117f mobile: refactor 2024-03-12 08:09:19 +05:00
Ammar Ahmed
e3ce44878e mobile: fix reorderable side menu 2024-03-12 08:09:19 +05:00
Ammar Ahmed
5f99783683 mobile: yearly reminders 2024-03-12 08:09:19 +05:00
Abdullah Atta
3d4cc1c4d7 web: add support for yearly reminders 2024-03-12 08:09:19 +05:00
Abdullah Atta
a2a05c5d8c core: add support for yearly reminders 2024-03-12 08:09:19 +05:00
Ammar Ahmed
c8eea0a0e4 mobile: stop bg fetch events when toggled off 2024-03-12 08:09:18 +05:00
Ammar Ahmed
8736a58a02 mobile: enable experimental background sync 2024-03-12 08:09:18 +05:00
Abdullah Atta
53c75ca6ef web: add support for hiding items from side bar 2024-03-12 08:09:18 +05:00
Abdullah Atta
506fc227c3 web: add support for reordering navigation menu items 2024-03-12 08:09:18 +05:00
Ammar Ahmed
4f40bdd262 mobile: cleanup 2024-03-12 08:09:18 +05:00
Ammar Ahmed
7bcd1df35a core: add sidebar order settings 2024-03-12 08:09:18 +05:00
Ammar Ahmed
1e4d63f84f mobile: reorder sidebar items 2024-03-12 08:09:18 +05:00
Abdullah Atta
7e7bb1b1b3 web: add support for custom colors 2024-03-12 08:09:18 +05:00
Ammar Ahmed
8e612db63f mobile: allow custom colors 2024-03-12 08:09:04 +05:00
Ammar Ahmed
140468cf3d mobile: migrate app lock 2024-03-12 08:09:04 +05:00
Abdullah Atta
114466e3a6 web: remove cbor-x package 2024-03-12 08:09:04 +05:00
Abdullah Atta
6c90653937 web: add support for at rest encryption and app lock 2024-03-12 08:09:03 +05:00
Ammar Ahmed
aec4c510f1 mobile: at rest encryption 2024-03-12 08:04:34 +05:00
Ammar Ahmed
8722157f33 mobile: simplify tests 2024-03-12 08:04:10 +05:00
Abdullah Atta
15b2aa6445 core: remove dummy test files 2024-03-12 08:04:10 +05:00
Abdullah Atta
7bddfa096f core: migrate key value storage to sqlite 2024-03-12 08:04:09 +05:00
Ammar Ahmed
7048c7e8db mobile: update sheet component 2024-03-12 08:03:42 +05:00
Abdullah Atta
508a259884 core: fix moving nested notebooks to trash 2024-03-12 08:03:42 +05:00
Abdullah Atta
8c5c9d447f global: move item resolution logic to @notesnook/common 2024-03-12 08:03:42 +05:00
Ammar Ahmed
fd951c3aaa mobile: add theme switch animation 2024-03-12 08:03:42 +05:00
Abdullah Atta
a026eb4a04 web: fix all type errors 2024-03-12 08:03:42 +05:00
Abdullah Atta
b02c43c99d web: fix duplicate imports 2024-03-12 08:03:12 +05:00
Abdullah Atta
c2be3a4f4d core: fix type error 2024-03-12 08:03:12 +05:00
Abdullah Atta
2b4155ad1d core: add tests for sync merger 2024-03-12 08:03:11 +05:00
Abdullah Atta
829a8a7ef8 core: migrate vault to a database collection 2024-03-12 08:03:11 +05:00
Ammar Ahmed
77c3e41564 mobile: fix exporter and attachment download 2024-03-12 08:03:11 +05:00
Ammar Ahmed
8e932b7ca0 global: fix type errors during build 2024-03-12 08:03:11 +05:00
Abdullah Atta
6e5c3e83d8 web: use simple transform stream for export progress 2024-03-12 08:03:11 +05:00
Abdullah Atta
a3db77285e web: fix exporting 2024-03-12 08:03:11 +05:00
Ammar Ahmed
0d864dfa10 mobile: add patches 2024-03-12 08:03:11 +05:00
Abdullah Atta
ae3d900af6 web: update export note snapshots for linux 2024-03-12 08:03:11 +05:00
Abdullah Atta
992a1d5231 web: fix reminder tests 2024-03-12 08:03:11 +05:00
Abdullah Atta
0a01ef6357 web: refresh notebooks after adding a new notebook 2024-03-12 08:03:11 +05:00
Abdullah Atta
7c6e79d78d web: fix force redirect to home on navigating to a color 2024-03-12 08:03:11 +05:00
Abdullah Atta
6084bb78a1 web: fix crash when sorting tags 2024-03-12 08:03:11 +05:00
Abdullah Atta
df987f6e94 web: fix crash when removing a notebook 2024-03-12 08:03:10 +05:00
Ammar Ahmed
a1e237a82a mobile: sync with latest core changes 2024-03-12 08:03:10 +05:00
Abdullah Atta
1e97e81fe7 core: remove unnecessary console.log 2024-03-12 08:03:10 +05:00
Ammar Ahmed
3ecda367f8 mobile: fix search to use filtered selector for notes 2024-03-12 08:03:10 +05:00
Abdullah Atta
afa4432521 core: prevent recursive sync after pushing items 2024-03-12 08:03:10 +05:00
Abdullah Atta
b24bc4fda0 web: simplify sync status 2024-03-12 08:03:10 +05:00
Abdullah Atta
0cca379a97 web: fix previewing trashed notes 2024-03-12 08:03:10 +05:00
Abdullah Atta
ca9e701386 core: fix trash 2024-03-12 08:03:10 +05:00
Abdullah Atta
48d79acbd3 web: improve sync test reliability 2024-03-12 08:03:10 +05:00
Abdullah Atta
2dabc62df3 core: auto sync not triggered on note update 2024-03-12 08:03:10 +05:00
Abdullah Atta
2ab7564622 web: fix note history tests 2024-03-12 08:01:37 +05:00
Abdullah Atta
0c0f19b23f web: fix selection logic in list 2024-03-12 08:01:37 +05:00
Abdullah Atta
1e5847e09e web: set testId on auth input fields 2024-03-12 08:01:37 +05:00
Abdullah Atta
e6d0061ca0 core: fix note content is not saving 2024-03-12 08:01:37 +05:00
Abdullah Atta
35b56fe8d2 core: fix data is not iterable error on backup restore 2024-03-12 08:01:37 +05:00
Abdullah Atta
4cd7f604df global: update lockfiles 2024-03-12 08:01:37 +05:00
Abdullah Atta
e65c36eff7 core: add retry if a test fails 2024-03-12 08:01:37 +05:00
Abdullah Atta
2e39e96846 core: fix invalid object-id generation for some timestamps 2024-03-12 08:01:37 +05:00
Abdullah Atta
357019a807 core: fix sync & monographs tests 2024-03-12 08:01:36 +05:00
Abdullah Atta
bb014cbb36 core: fix auto sync starting even when auto sync is disabled 2024-03-12 08:01:20 +05:00
Abdullah Atta
5d0e7ea28b core: fix sync tests 2024-03-12 08:00:59 +05:00
Abdullah Atta
96903c512b core: remove unnecessary migration success log 2024-03-12 08:00:59 +05:00
Abdullah Atta
b9acb8a213 core: fix Crypto is not supported on this platform. error during tests 2024-03-12 08:00:59 +05:00
Abdullah Atta
48e625480c core: make all the core tests pass again 2024-03-12 08:00:59 +05:00
Abdullah Atta
bc3dce6ecf web: fix crash when note has no title 2024-03-12 08:00:59 +05:00
Abdullah Atta
0da883b40a core: set default title if note has no title 2024-03-12 08:00:59 +05:00
Abdullah Atta
56d230191f web: update kysely patch 2024-03-12 08:00:59 +05:00
Abdullah Atta
abdb9626d0 web: run patch-package after npm i 2024-03-12 08:00:59 +05:00
Abdullah Atta
567eb789fc web: more fixes 2024-03-12 08:00:58 +05:00
Abdullah Atta
aa85b57e75 web: fix many type errors 2024-03-12 08:00:58 +05:00
Abdullah Atta
b5d9a83e1b global: update lockfiles 2024-03-12 07:58:39 +05:00
Abdullah Atta
ee2d0912fd theme: fix tinycolor2 not found error 2024-03-12 07:58:39 +05:00
Abdullah Atta
22cb2f1829 web: fix notes search in tags, favorites etc 2024-03-12 07:58:39 +05:00
Abdullah Atta
c3f76fa7c9 web: update wa-sqlite 2024-03-12 07:58:39 +05:00
Abdullah Atta
6923801426 web: use memory tempStore for sqlite 2024-03-12 07:58:39 +05:00
Abdullah Atta
210b6ba10c web: add support for exporting sql database 2024-03-12 07:58:39 +05:00
Abdullah Atta
b3bf5ff7a9 core: fix FTS5 SQLite search 2024-03-12 07:58:39 +05:00
Ammar Ahmed
ab1ff2f0bb mobile: cleanup 2024-03-12 07:58:38 +05:00
Ammar Ahmed
a813a5dd92 mobile: fix types 2024-03-12 07:58:38 +05:00
Ammar Ahmed
b85fc2d473 mobile: refactor zustand stores 2024-03-12 07:58:38 +05:00
Ammar Ahmed
112b44802f mobile: bug fixes 2024-03-12 07:58:38 +05:00
Ammar Ahmed
c121d4a010 core: add fn to get cache item 2024-03-12 07:58:07 +05:00
Abdullah Atta
26c7f04317 web: only auto align title & headline 2024-03-12 07:58:07 +05:00
Abdullah Atta
1fe3e5102b web: getKey -> key 2024-03-12 07:58:07 +05:00
Abdullah Atta
e0d44e58e6 web: fix resolved item data misaligned with items 2024-03-12 07:58:07 +05:00
Abdullah Atta
3103a7b97c core: fix many issues with virtualized grouping 2024-03-12 07:58:07 +05:00
Ammar Ahmed
ef802bfa5c core: early return when items are empty 2024-03-12 07:58:07 +05:00
Abdullah Atta
a1c2f3f679 web: fix random crashes when scrolling in list 2024-03-12 07:58:07 +05:00
Abdullah Atta
c7843effc6 web: replace react-virtual with react-virtuouso 2024-03-12 07:58:07 +05:00
Ammar Ahmed
1bd9ecca37 mobile: fix build ang bugs 2024-03-12 07:58:07 +05:00
Abdullah Atta
2f55f36802 web: hide attachments access if user is logged out 2024-03-12 07:58:07 +05:00
Abdullah Atta
af5b05df70 web: allow core to specify custom sql db name 2024-03-12 07:58:07 +05:00
Abdullah Atta
1b2479cad2 web: close idb connection on sqlite connection close 2024-03-12 07:58:07 +05:00
Abdullah Atta
b92b4bff4c core: clear database on logout 2024-03-12 07:58:06 +05:00
Ammar Ahmed
e8301907cd core: add tempStore option to DB 2024-03-12 07:58:06 +05:00
Abdullah Atta
2c3b400b5d core: migrate items sequentially during sync 2024-03-12 07:58:06 +05:00
Abdullah Atta
8350d24822 core: fix build error 2024-03-12 07:58:06 +05:00
Abdullah Atta
934f77d369 web: execute only 1 query at a time 2024-03-12 07:58:06 +05:00
Abdullah Atta
e8a9301434 web: fix diff viewer crashing the app 2024-03-12 07:58:06 +05:00
Abdullah Atta
58b51201d9 core: do not stop sync if a conflict occurs 2024-03-12 07:58:06 +05:00
Abdullah Atta
02bc3a9db4 core: allow only 1 transaction at a time 2024-03-12 07:58:06 +05:00
Abdullah Atta
b777546bc9 core: simplify item key derivation in virtualized grouping 2024-03-12 07:58:06 +05:00
Abdullah Atta
5b96b956e8 web: improve list item rendering performance 2024-03-12 07:58:06 +05:00
Abdullah Atta
e90f2d7087 core: always send realtime sync event 2024-03-12 07:58:06 +05:00
Abdullah Atta
8c1e0d2414 core: fix content date edited not updating 2024-03-12 07:58:06 +05:00
Abdullah Atta
007c9859dc core: put items in chunks of 200 2024-03-12 07:58:06 +05:00
Abdullah Atta
c72cac1213 web: set name prop on fields 2024-03-12 07:58:06 +05:00
Abdullah Atta
f8b6dd6d1a core: implement sync v2 2024-03-12 07:58:05 +05:00
Ammar Ahmed
5e71df03af mobile: use index based grouping 2024-03-12 07:55:47 +05:00
Abdullah Atta
e3f74f7f6f web: changes to use new grouping api 2024-03-12 07:55:47 +05:00
Abdullah Atta
5a759268b8 core: implement new high perf indexed based grouping 2024-03-12 07:55:47 +05:00
Ammar Ahmed
a9d98f73e4 mobile: fix share extension in release 2024-03-12 07:55:21 +05:00
Abdullah Atta
65dcd6e035 web: fix router types 2024-03-12 07:55:21 +05:00
Abdullah Atta
921b802aa0 web: enable vite hot reloading 2024-03-12 07:55:21 +05:00
Abdullah Atta
0b767de51b web: fix minor build issues 2024-03-12 07:55:21 +05:00
Abdullah Atta
e9bd553116 web: migrate everything to react@18 2024-03-12 07:55:21 +05:00
Abdullah Atta
d4fcf1687a web: fix notebook tests 2024-03-12 07:55:21 +05:00
Ammar Ahmed
996fb6d60f mobile: share extension cleanup 2024-03-12 07:55:21 +05:00
Ammar Ahmed
fd6d259d6c mobile: cleanup 2024-03-12 07:55:21 +05:00
Ammar Ahmed
4f16d172f8 mobile: migrate share extension 2024-03-12 07:55:21 +05:00
Ammar Ahmed
b94c3ac182 mobile: fix navigation 2024-03-12 07:55:21 +05:00
Abdullah Atta
ea383c80f2 web: send noteId when decrypting real-time locked content 2024-03-12 07:55:21 +05:00
Abdullah Atta
1b769f23cb web: fix tag tests 2024-03-12 07:55:20 +05:00
Ammar Ahmed
d5feab78c8 mobile: pass tests 2024-03-12 07:55:20 +05:00
Abdullah Atta
043099d217 web: fix html export snapshot 2024-03-12 07:55:20 +05:00
Abdullah Atta
670a8a05d1 core: fix md with frontmatter export 2024-03-12 07:55:20 +05:00
Ammar Ahmed
01ea478e46 core: fix ts error 2024-03-12 07:55:20 +05:00
Ammar Ahmed
ebd3ed06ab mobile: fix webpack build 2024-03-12 07:55:20 +05:00
Ammar Ahmed
f34591228e mobile: push changes 2024-03-12 07:55:20 +05:00
Abdullah Atta
7247ad4a05 web: fix notes tests 2024-03-12 07:55:20 +05:00
Abdullah Atta
e4fd25cb76 core: prevent duplicate colors based on color code 2024-03-12 07:55:00 +05:00
Abdullah Atta
3c67928583 core: fix error on locking notes 2024-03-12 07:55:00 +05:00
Ammar Ahmed
9aaa915caa mobile: push changes 2024-03-12 07:55:00 +05:00
Abdullah Atta
43318b185b web: reset search results on navigation 2024-03-12 07:55:00 +05:00
Abdullah Atta
015666388f web: reset prepared sql stmt on error 2024-03-12 07:55:00 +05:00
Abdullah Atta
542f7687f5 web: reuse search query 2024-03-12 07:55:00 +05:00
Abdullah Atta
5b744eae3c core: use porter trigram tokenizer for fts 2024-03-12 07:54:59 +05:00
Abdullah Atta
28cd11fdc7 core: properly escape fts query 2024-03-12 07:54:59 +05:00
Abdullah Atta
29ccbd0942 web: add support for assigning tags to notes 2024-03-12 07:54:59 +05:00
Abdullah Atta
30a127d2da core: fix "type" does not exist 2024-03-12 07:54:59 +05:00
Abdullah Atta
0d7973a34c core: fix build error 2024-03-12 07:54:59 +05:00
Ammar Ahmed
487edd7165 mobile: migrate to sqlite 2024-03-12 07:54:59 +05:00
Abdullah Atta
b3fe79b0a7 web: fix note editor properties 2024-03-12 07:53:23 +05:00
Abdullah Atta
966047585c common: type is now optional in getFormattedDate 2024-03-12 07:53:05 +05:00
Abdullah Atta
d57ca9b07c core: return FilteredSelector from attachments.ofNote 2024-03-12 07:53:05 +05:00
Abdullah Atta
e76b7ee0a5 web: show notebook & tag shortcuts when assigning notes 2024-03-12 07:53:05 +05:00
Abdullah Atta
2b42315c53 web: implement virtualized search for all items 2024-03-12 07:53:05 +05:00
Abdullah Atta
947443b4c7 web: use lazy loading to load attachments 2024-03-12 07:53:04 +05:00
Abdullah Atta
2547ab42e9 web: load tag & color title lazily 2024-03-12 07:52:42 +05:00
Abdullah Atta
306aca0645 web: show all nested notebooks as tree when moving notes 2024-03-12 07:52:42 +05:00
Abdullah Atta
f2b2dc33e6 web: add support for nested notebooks 2024-03-12 07:52:42 +05:00
Abdullah Atta
7a8f8dc565 web: use new virtualized grouping for reminders 2024-03-12 07:52:42 +05:00
Abdullah Atta
0528115ad3 web: use new virtualized grouping for trash 2024-03-12 07:52:42 +05:00
Abdullah Atta
4a5ee49db6 web: use new virtualized grouping for monographs 2024-03-12 07:52:42 +05:00
Abdullah Atta
3ca761013d core: add support for fetching trash as grouped 2024-03-12 07:52:42 +05:00
Abdullah Atta
bdeb123f18 web: install missing @hazae41/foras 2024-03-12 07:52:42 +05:00
Abdullah Atta
3f5f84aac0 core: fix attachment extraction for locked notes 2024-03-12 07:52:41 +05:00
Abdullah Atta
01d3bd7810 global: update lockfiles 2024-03-12 07:52:41 +05:00
Abdullah Atta
8e6d6e20d7 web: start initial migration to sqlite 2024-03-12 07:52:41 +05:00
Abdullah Atta
375185a9ea ui: add support for lazily loading menu items 2024-03-12 07:51:38 +05:00
Abdullah Atta
c2e9773486 common: remove total-notes utility 2024-03-12 07:51:38 +05:00
Abdullah Atta
5e0e7b17e2 core: changes according to client needs 2024-03-12 07:51:38 +05:00
Abdullah Atta
aa90684616 core: fix database migration 2024-03-12 07:51:37 +05:00
Abdullah Atta
fa47b4151f core: add support for incremental grouping 2024-03-12 07:51:24 +05:00
Abdullah Atta
eca5db19a2 core: fix size of nanoid in benchmarks 2024-03-12 07:51:24 +05:00
Abdullah Atta
b053b0eab4 core: optimize bson objectid generation & add benchmarks 2024-03-12 07:51:23 +05:00
Abdullah Atta
70da1045d7 core: fix monographs 2024-03-12 07:51:10 +05:00
Abdullah Atta
b5fd2b0751 core: fix sync tests 2024-03-12 07:51:10 +05:00
Abdullah Atta
97f6f09882 core: remove temporary sqlite db 2024-03-12 07:51:10 +05:00
Abdullah Atta
0c773727ee core: migrate everything to sqlite 2024-03-12 07:51:10 +05:00
Abdullah Atta
952ce39f1c core: migrate all database writes to sqlite 2024-03-12 07:49:55 +05:00
Abdullah Atta
057c7f3f5d global: update lockfiles 2024-03-12 07:49:14 +05:00
Abdullah Atta
9439c52801 core: convert everything else to typescript 2024-03-12 07:48:50 +05:00
Abdullah Atta
b1cebbe8f2 core: remove unnecessary sync tests 2024-03-12 07:48:50 +05:00
Abdullah Atta
836a47fd96 core: migrate monographs to typescript 2024-03-12 07:48:50 +05:00
Abdullah Atta
136b2ed457 core: make migration tests less flaky 2024-03-12 07:48:50 +05:00
Abdullah Atta
7f4006de95 core: add tests for migrations 2024-03-12 07:48:49 +05:00
Abdullah Atta
dc96209ed9 core: migrate settings to its own collection 2024-03-12 07:48:09 +05:00
Abdullah Atta
9b0332d5e0 core: get rid of topics; move to sub notebooks 2024-03-12 07:47:20 +05:00
Abdullah Atta
a1300fefd6 core: migrate attachments to use relations for linking to notes 2024-03-12 07:47:20 +05:00
Abdullah Atta
27032e853b core: minor updates and fixes 2024-03-12 07:47:20 +05:00
Abdullah Atta
22646e1398 core: fix types & syncing issues 2024-03-12 07:47:20 +05:00
ammarahm-ed
6e5e7c92c9 mobile: pass all tests 2024-03-12 07:41:34 +05:00
ammarahm-ed
3254109ea5 mobile: fix filesystem not loaded 2024-03-12 07:41:34 +05:00
ammarahm-ed
98ab2bfd8f mobile: fix navigating to tags 2024-03-12 07:41:34 +05:00
ammarahm-ed
6cbf22c163 mobile: migrate to typescript 2024-03-12 07:41:33 +05:00
ammarahm-ed
1714c1abca common: fix ts errors 2024-03-12 07:41:33 +05:00
Abdullah Atta
1bf65c7a53 mobile: use new types from core 2024-03-12 07:41:33 +05:00
Abdullah Atta
0075fde915 web: use new types from core 2024-03-12 07:41:33 +05:00
Abdullah Atta
318c61f531 core: fix more typings 2024-03-12 07:39:14 +05:00
Abdullah Atta
6dc502fd92 core: fix typing errors 2024-03-12 07:38:43 +05:00
Abdullah Atta
db7d7edcb5 core: convert almost everything to typescript 2024-03-12 07:38:43 +05:00
Ammar Ahmed
278840bcfd mobile: fix lint errors 2024-03-12 07:28:59 +05:00
Abdullah Atta
f149ad7c71 ci: update node version to 20 2024-03-12 07:28:59 +05:00
Abdullah Atta
ad29de8a04 config: update nx 2024-03-12 07:28:58 +05:00
Abdullah Atta
0759098414 desktop: set release channel to beta 2024-03-12 07:28:45 +05:00
Ammar Ahmed
528751a3ce mobile: update ios version number 2024-03-12 07:28:45 +05:00
Ammar Ahmed
3dd5bd8c4a ci: update workflow name
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-03-12 07:28:23 +05:00
Ammar Ahmed
1ed84fe1b4 mobile: v3 beta release 2024-03-12 07:28:23 +05:00
Ammar Ahmed
1dcf8daecb ci: update iOS workflow
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-03-12 07:25:38 +05:00
Ammar Ahmed
fc0ac1e02a Update android.publish.internal.yml
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-03-12 07:25:38 +05:00
Ammar Ahmed
a1efab6673 ci: android.publish.yml
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-03-12 07:25:38 +05:00
Abdullah Atta
4a6445e241 desktop: do not set teamId for notarization 2024-03-12 07:25:38 +05:00
Abdullah Atta
3fadb6016c ci: APPLE_API_KEY_ISSUER > APPLE_API_ISSUER 2024-03-12 07:25:38 +05:00
Abdullah Atta
99f469ba38 desktop: fix unknown property 'notarize' error 2024-03-12 07:25:38 +05:00
Abdullah Atta
d1753f449a desktop: use official electron builder notarization 2024-03-12 07:25:38 +05:00
Abdullah Atta
fe19a6b0cc ci: update {upload,download}-artifact to v4 2024-03-12 07:25:38 +05:00
Abdullah Atta
bb67472dee ci: get rid of you may only define one of branches and branches-ignore for a single event 2024-03-12 07:25:38 +05:00
Abdullah Atta
13213cf4cd ci: use m1 for building on mac 2024-03-12 07:25:23 +05:00
Abdullah Atta
b49a890849 ci: use wrangler directly to publish to cf 2024-03-12 07:25:23 +05:00
Abdullah Atta
3eb21afd42 desktop: use app version from autoUpdater 2024-03-12 07:25:23 +05:00
Abdullah Atta
b98021929f ci: update cloudflare action 2024-03-12 07:25:23 +05:00
Abdullah Atta
40ccad1181 desktop: ready beta for deployment 2024-03-12 07:25:23 +05:00
Abdullah Atta
e2db62d551 ci: deploy to correct cf pages project
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-12 07:24:45 +05:00
Abdullah Atta
0229641917 core: turn off sync push by default 2024-03-12 07:24:45 +05:00
1707 changed files with 179541 additions and 231263 deletions

View File

@@ -10,12 +10,11 @@ const authors = readFileSync("AUTHORS", "utf-8");
const isAuthor = authors.includes(`<${authorEmail}>`);
const SCOPES = [
// for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
// for full list of scopes + details see: https://github.com/streetwriters/notesnook-private/blob/master/CONTRIBUTING.md#commit-guidelines
"mobile",
"web",
"vericrypt",
"monograph",
"desktop",
"crypto",
"editor",
@@ -35,9 +34,7 @@ const SCOPES = [
"common",
"global",
"docs",
"themebuilder",
"intl",
"webclipper"
"themebuilder"
];
module.exports = {

View File

@@ -1,6 +1,8 @@
name: Bug Report
description: Are you facing a bug or a crash in Notesnook?
type: Bug
labels: ["Type: Bug", "Status: Pending"]
assignees:
- thecodrr
body:
- type: markdown
attributes:

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Notesnook Discord Community
url: https://go.notesnook.com/discord
url: https://discord.gg/6mHHyncHJE
about: Reach out to us directly & discuss your issues, suggestions & other feedback!

View File

@@ -1,6 +1,6 @@
name: Feature request
description: Suggest a feature you are missing in Notesnook
type: Feature
labels: ["Type: Feature Request", "Status: Pending"]
body:
- type: markdown
attributes:

View File

@@ -14,8 +14,6 @@ runs:
apps/mobile/package-lock.json
apps/desktop/package-lock.json
apps/web/package-lock.json
apps/monograph/package-lock.json
apps/theme-builder/package-lock.json
extensions/web-clipper/package-lock.json
packages/core/package-lock.json
packages/crypto/package-lock.json
@@ -26,4 +24,3 @@ runs:
packages/logger/package-lock.json
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
packages/intl/package-lock.json

View File

@@ -1,100 +0,0 @@
name: e2e-android
on: workflow_dispatch
jobs:
e2e-android:
runs-on: ubuntu-latest
env:
API_LEVEL: 34
ARCH: x86_64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-version: wrapper
cache-read-only: false
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
architecture: "x64"
- name: Install Detox CLI
run: npm install detox-cli --global
- name: Detox build
run: |
yarn build:android
ls apps/mobile/native/android/app/build/outputs/apk
ls apps/mobile/native/android/app/build/outputs/apk/release
- name: Get device name
id: device
run: |
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: ${{ steps.device.outputs.AVD_NAME }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Detox test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
disable-animations: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
force-avd-creation: false
script: yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: detox-artifacts
path: artifacts
retention-days: 14

View File

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
@@ -22,7 +22,7 @@ jobs:
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
java-version: "11"
architecture: "x64"
- name: Install node modules
@@ -58,7 +58,7 @@ jobs:
ccache -p
- name: Cache gradle
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
@@ -69,6 +69,8 @@ jobs:
run: |
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
- name: CCache Stats Before Build
run: ccache -sv
@@ -127,7 +129,7 @@ jobs:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: beta
track: alpha
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
@@ -136,7 +138,7 @@ jobs:
with:
draft: true
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
name: Notesnook Android v${{ steps.package-version.outputs.current-version}}
name: Notesnook Android v${{ steps.package-version.outputs.current-version}} Beta
repository: streetwriters/notesnook
token: ${{ secrets.GITHUB_TOKEN }}
files: |
@@ -144,12 +146,9 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.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
${{steps.sign_app.outputs.signedReleaseFile}}
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
with:
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}

View File

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
@@ -22,7 +22,7 @@ jobs:
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
java-version: "11"
architecture: "x64"
- name: Install node modules
@@ -58,7 +58,7 @@ jobs:
ccache -p
- name: Cache gradle
uses: actions/cache@v3
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
@@ -69,6 +69,8 @@ jobs:
run: |
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
- name: CCache Stats Before Build
run: ccache -sv
@@ -131,6 +133,7 @@ jobs:
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
@@ -145,10 +148,8 @@ jobs:
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
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
with:
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}

View File

@@ -18,10 +18,10 @@ on:
jobs:
test:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -33,22 +33,14 @@ on:
required: true
default: true
description: "Build for macOS?"
release-track:
type: choice
required: true
default: stable
description: "Select the release track"
options:
- stable
- beta
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -57,30 +49,14 @@ jobs:
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Generate desktop build (stable)
if: ${{ inputs.release-track == 'stable' }}
- name: Generate desktop build
run: npx nx build:desktop @notesnook/web
- name: Generate desktop build (beta)
if: ${{ inputs.release-track == 'beta' }}
run: BETA=true npx nx build:desktop @notesnook/web
- name: Build desktop bundle
working-directory: ./apps/desktop
run: npm run bundle
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
@@ -95,8 +71,7 @@ jobs:
- name: Package desktop build
run: |
cp -r ./apps/web/build ./apps/desktop/
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/desktop/build/
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/web/build/
echo "Build folder archived to ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip"
- name: Upload desktop build
@@ -107,28 +82,6 @@ jobs:
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
prerelease: ${{ inputs.release-track == 'beta' }}
target_commitish: ${{ github.ref }}
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
run: |
sudo apt-get update
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
cd /tmp/flatpak-builder-tools/node
pipx install .
cd -
node scripts/generate-sources.mjs
- name: Upload flatpak sources
uses: softprops/action-gh-release@v1
if: inputs.publish-github && inputs.build-linux
with:
draft: true
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./generated-sources.json
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
build-macos:
@@ -139,14 +92,11 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install setuptools
run: brew install python-setuptools
- name: Setup notarization
run: |
mkdir -p ~/private_keys/
@@ -183,12 +133,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
@@ -200,7 +144,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
run: |
npx nx run release --project @notesnook/desktop -- --variant=mas
yarn electron-builder --config=electron-builder.config.js --mac mas --universal -p never
npx electron-builder --mac mas --universal -p never
working-directory: ./apps/desktop
- name: Build zip and dmg
@@ -214,9 +158,9 @@ jobs:
run: |
npx nx run release --project @notesnook/desktop
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p always
npx electron-builder --mac zip dmg --arm64 --x64 -p always
else
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p never
npx electron-builder --mac zip dmg --arm64 --x64 -p never
fi
working-directory: ./apps/desktop
@@ -233,15 +177,15 @@ jobs:
xcrun altool --upload-package $package -t osx --apiKey $API_KEY_ID --apiIssuer $API_KEY_ISSUER_ID --apple-id ${{ steps.app_metadata.outputs.apple_app_id }} --bundle-id ${{ steps.app_metadata.outputs.app_bundle_id }} --bundle-short-version-string ${{ steps.app_metadata.outputs.app_version }} --bundle-version ${{ steps.app_metadata.outputs.bundle_version }}
working-directory: ./apps/desktop
build-linux-x64:
name: Build for Linux x64
build-linux:
name: Build for Linux
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -251,7 +195,7 @@ jobs:
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v3
uses: samuelmeuli/action-snapcraft@v1
if: inputs.publish-snap
- name: Download build
@@ -265,11 +209,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
@@ -277,7 +216,7 @@ jobs:
- name: Build snap
if: inputs.publish-snap
run: |
yarn electron-builder --config=electron-builder.config.js --linux snap:x64 -p never
npx electron-builder --linux snap:x64 -p never
working-directory: ./apps/desktop
- name: Build AppImage
@@ -285,86 +224,18 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p always
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
else
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
fi
working-directory: ./apps/desktop
- name: Publish on Snapcraft
if: inputs.publish-snap
run: |
if [ ${{ inputs.release-track }} == 'beta' ]; then
snapcraft upload --release=beta ./output/notesnook_linux_amd64.snap
else
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
fi
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
working-directory: ./apps/desktop
build-linux-arm64:
name: Build for Linux arm64
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04-arm
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
# - name: Setup Snapcraft Auth
# if: inputs.publish-snap
# run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
# - name: Install Snapcraft
# uses: samuelmeuli/action-snapcraft@v1
# if: inputs.publish-snap
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
# - name: Build snap
# if: inputs.publish-snap
# run: |
# yarn electron-builder --config=electron-builder.config.js --linux snap:arm64 -p never
# working-directory: ./apps/desktop
- name: Build AppImage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p always
else
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
fi
working-directory: ./apps/desktop
# - name: Publish on Snapcraft
# if: inputs.publish-snap
# run: |
# snapcraft upload --release=stable ./output/notesnook_linux_arm64.snap
# working-directory: ./apps/desktop
build-windows:
name: Build for Windows
needs: build
@@ -373,15 +244,15 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
- name: Install TrustedSigning
run: Install-Module -Name TrustedSigning -RequiredVersion 0.3.15 -Force -Repository PSGallery
- name: Install AzureCodeSigning
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -Force -Repository PSGallery
shell: pwsh
- name: Setup Node
@@ -398,12 +269,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build
run: npx nx run release --project @notesnook/desktop
@@ -415,8 +280,8 @@ jobs:
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: |
if ($${{ inputs.publish-github }} -eq $true) {
yarn electron-builder --config=electron-builder.config.js --win --publish always
yarn electron-builder --win --publish always
} else {
yarn electron-builder --config=electron-builder.config.js --win --publish never
yarn electron-builder --win --publish never
}
working-directory: ./apps/desktop

View File

@@ -1,229 +0,0 @@
name: Test @notesnook/desktop
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "app/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
pull_request:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- 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
- name: Generate desktop build
run: npx nx build:desktop @notesnook/web
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
test-macos-x64:
name: Test macOS x64
needs: build
runs-on: macos-13
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
working-directory: ./apps/desktop
- name: Run tests x64
run: npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos-x64
path: apps/desktop/test-results
retention-days: 5
test-macos:
name: Test macOS
needs: build
runs-on: macos-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
working-directory: ./apps/desktop
- name: Run tests arm64
run: npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos
path: apps/desktop/test-results
retention-days: 5
test-linux:
name: Test for Linux
needs: build
runs-on: ubuntu-22.04
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-linux
path: apps/desktop/test-results
retention-days: 5
test-windows:
name: Test for Windows
needs: build
runs-on: windows-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-win
path: apps/desktop/test-results
retention-days: 5

View File

@@ -19,10 +19,10 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -1,6 +1,6 @@
name: Publish Notesnook Help
on:
on:
workflow_dispatch:
push:
branches:
@@ -11,10 +11,10 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable

View File

@@ -4,21 +4,16 @@ on: workflow_dispatch
jobs:
build:
runs-on: macos-14
runs-on: macos-13
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "16.1"
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
@@ -52,17 +47,14 @@ jobs:
ccache -p
- name: Cache Pods
uses: actions/cache@v3
uses: actions/cache@v2
id: pods-cache
with:
path: apps/mobile/native/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/native/ios/Podfile.lock') }}
- name: Install Pods
run: |
cd apps/mobile/native/ios
bundle install
RCT_NEW_ARCH_ENABLED=0 bundle exec pod install
run: npm run prepare:ios
- name: CCache Stats Before Build
run: ccache -sv
@@ -106,10 +98,7 @@ jobs:
api-private-key: ${{ secrets.API_KEY }}
- name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: Notesnook.zip
path: |
Notesnook.ipa
apps/mobile/native/ios/**/*.map
packages/editor-mobile/sourcemaps/*.map
name: Notesnook.ipa
path: Notesnook.ipa

View File

@@ -1,89 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: Publish @notesnook/monograph
on: workflow_dispatch
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-22.04
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
- 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=monograph
- name: Collect package metadata
id: package_metadata
working-directory: ./apps/monograph
run: |
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
- name: Generate build
run: npx nx build @notesnook/monograph
# Setup Buildx
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
ecr: auto
logout: true
# Pull previous image from docker hub to use it as cache to improve the image build time.
- name: docker pull cache image
continue-on-error: true
run: docker pull streetwriters/monograph:latest
# Setup QEMU
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: streetwriters/monograph
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: apps/monograph
file: apps/monograph/Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
cache-from: streetwriters/monograph:latest
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: index.docker.io/streetwriters/monograph
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@@ -5,10 +5,10 @@ on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -5,10 +5,10 @@ on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -1,44 +0,0 @@
name: Benchmark @notesnook/web
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.benchmarks.yml"
pull_request:
jobs:
bench:
name: Bench
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- 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
- name: Build
run: npm run build:web
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: apps/web
- name: Run benchmarks
id: benchmark
run: node __bench__/app.bench.mjs
working-directory: apps/web

69
.github/workflows/web.beta.publish.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
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

@@ -1,24 +1,14 @@
name: Publish @notesnook/web
on:
workflow_dispatch:
inputs:
release-track:
type: choice
required: true
default: stable
description: "Select the release track"
options:
- stable
- beta
on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -34,20 +24,8 @@ jobs:
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate build (stable)
if: ${{ inputs.release-track == 'stable' }}
- name: Generate build
run: npm run build:web
- name: Publish to Cloudflare Pages (stable)
if: ${{ inputs.release-track == 'stable' }}
working-directory: ./apps/web
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./build/
- name: Generate build (beta)
if: ${{ inputs.release-track == 'beta' }}
run: npm run build:beta:web
- name: Publish to Cloudflare Pages (beta)
if: ${{ inputs.release-track == 'beta' }}
working-directory: ./apps/web
run: npx --yes wrangler pages deploy --branch=beta --project-name=notesnook-app-beta ./build/
- name: Publish to Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./apps/web/build/

View File

@@ -19,10 +19,10 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -40,7 +40,7 @@ jobs:
run: npm run build:test:web
- name: Archive build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: build
path: apps/web/build/**/*
@@ -49,14 +49,14 @@ jobs:
name: 🧪 Test (${{ matrix.shard }}/${{ strategy.job-total }})
strategy:
matrix:
shard: [1, 2, 3, 4, 5]
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/web/build
@@ -67,7 +67,7 @@ jobs:
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm install -D @playwright/test otplib dayjs @zip.js/zip.js
npm install -D @playwright/test otplib
- name: Validate build extraction
run: ls -ld ./apps/web/build/index.html
@@ -88,7 +88,7 @@ jobs:
working-directory: apps/web
- name: Upload test results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
if: failure()
with:
name: test-results-${{ matrix.shard }}

View File

@@ -0,0 +1,32 @@
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/

1
.gitignore vendored
View File

@@ -11,4 +11,3 @@ nx-cloud.env
site
node_modules.backup
.nx
/*.patch

View File

@@ -66,7 +66,7 @@ We take all queries, issues and bug reports that you might have. Feel free to as
## Additional Resources
- [Migrating & Importing your data from other apps — Importer](https://help.notesnook.com/importing-notes)
- [Migrating & Importing your data from other apps — Importer](https://importer.notesnook.com/)
- [Privacy policy](https://notesnook.com/privacy) & [Terms of service](https://notesnook.com/terms)
- [Verify Notesnook encryption claims yourself — Vericrypt](https://vericrypt.notesnook.com/)
- [Why Notesnook requires an email address?](https://blog.notesnook.com/why-notesnook-requires-an-email-address/)

View File

@@ -2,6 +2,4 @@ node_modules
build
output
dist
_catalog
test-results
test-artifacts
_catalog

View File

@@ -40,35 +40,12 @@ Once you are inside the `./notesnook` directory, run the preparation step:
npm install
```
Now you can finally start the desktop app for development:
Now you can finally start the desktop app:
```bash
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 --config=electron-builder.config.js --mac dmg --arm64 --x64 --publish never
# For Linux (AppImage)
npx electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 --publish never
# For Windows
npx electron-builder --config=electron-builder.config.js --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
### The tech stack

View File

@@ -1,246 +0,0 @@
/*
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 { test } from "vitest";
import { harness } from "./utils.js";
import { writeFile } from "fs/promises";
import { Page } from "playwright";
import { gt, lt } from "semver";
test("update starts downloading if version is outdated", async (t) => {
await harness(
t,
async ({ page }) => {
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /updating/i })
.waitFor({ state: "attached" });
},
{ version: "3.0.0" }
);
});
test("update is only shown if version is outdated and auto updates are disabled", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.waitFor({ state: "attached" });
},
{ version: "3.0.0" }
);
});
test("update to stable if it is newer", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
t.expect(gt(version, "3.0.0-beta.0")).toBe(true);
},
{ version: "3.0.0-beta.0" }
);
});
test("update is not available if it latest stable version is older", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
t.expect(
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.isHidden()
).toBe(true);
},
{ version: "99.0.0-beta.0" }
);
});
test("downgrade to stable on switching to stable release track", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "stable"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
t.expect(lt(version, "99.0.0-beta.0")).toBe(true);
},
{ version: "99.0.0-beta.0" }
);
});
async function skipDialog(page: Page) {
await page
.waitForSelector(".ReactModal__Content", {
timeout: 1000
})
.catch(() => {})
.then(async () => {
const positiveButton = page.locator(
"button[data-role='positive-button']"
);
const negativeButton = page.locator(
"button[data-role='negative-button']"
);
if (await positiveButton.isVisible()) await positiveButton.click();
else if (await negativeButton.isVisible()) await negativeButton.click();
});
}

View File

@@ -1,38 +0,0 @@
/*
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 { test } from "vitest";
import { harness } from "./utils.js";
import assert from "assert";
test("make sure app loads", async (t) => {
await harness(t, async ({ page }) => {
await page.waitForSelector("#authForm");
assert.ok(
await page.getByRole("button", { name: "Create account" }).isVisible()
);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
});
});

View File

@@ -1,172 +0,0 @@
/*
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 { execSync } from "child_process";
import { mkdir } from "fs/promises";
import { fileURLToPath } from "node:url";
import path from "path";
import { _electron as electron } from "playwright";
import slugify from "slugify";
import { TaskContext } from "vitest";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
interface AppContext {
app: import("playwright").ElectronApplication;
page: import("playwright").Page;
configPath: string;
relaunch: () => Promise<void>;
}
interface TestOptions {
version: string;
}
export async function harness(
t: TaskContext,
cb: (ctx: AppContext) => Promise<void>,
options?: TestOptions
) {
const ctx = await buildAndLaunchApp(options);
t.onTestFinished(async (result) => {
if (result.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(t.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
});
await cb(ctx);
}
async function buildAndLaunchApp(options?: TestOptions): Promise<AppContext> {
const productName = makeid(10);
const executablePath = await buildApp({ ...options, productName });
const { app, page, configPath } = await launchApp(executablePath);
const ctx: AppContext = {
app,
page,
configPath,
relaunch: async () => {
const { app, page, configPath } = await launchApp(executablePath);
ctx.app = app;
ctx.page = page;
ctx.configPath = configPath;
}
};
return ctx;
}
async function launchApp(executablePath: string) {
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [] : ["--hidden"],
env:
process.platform === "linux"
? {
...(process.env as Record<string, string>),
APPIMAGE: "true"
}
: (process.env as Record<string, string>)
});
const page = await app.firstWindow();
const userDataDirectory = await app.evaluate((a) => {
return a.app.getPath("userData");
});
const configPath = path.join(userDataDirectory, "config.json");
return {
app,
page,
configPath
};
}
async function buildApp({
version,
productName
}: {
version?: string;
productName: string;
}) {
const buildRoot = path.join("test-artifacts", `${productName}-build`);
const output = path.join("test-artifacts", `${productName}-output`);
execSync(`npm run release -- --root ${buildRoot} --skip-tsc-build`, {
stdio: IS_DEBUG ? "inherit" : "ignore"
});
const args = [
`--config electron-builder.config.js`,
`--c.extraMetadata.productName=${productName}`,
"--publish=never"
];
if (version) args.push(`--c.extraMetadata.version=${version}`);
execSync(`npx electron-builder --dir --${process.arch} ${args.join(" ")}`, {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
NOTESNOOK_STAGING: "true",
NN_BUILD_ROOT: buildRoot,
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,
NN_OUTPUT_DIR: output
}
});
return path.join(
__dirname,
"..",
output,
process.platform === "linux"
? process.arch === "arm64"
? "linux-arm64-unpacked"
: "linux-unpacked"
: process.platform === "darwin"
? process.arch === "arm64"
? `mac-arm64/${productName}.app/Contents/MacOS/`
: `mac/${productName}.app/Contents/MacOS/`
: "win-unpacked",
process.platform === "win32" ? `${productName}.exe` : productName
);
}
function makeid(length: number) {
let result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result.toLowerCase();
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -1,219 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const path = require("path");
const pkg = require("./package.json");
const buildRoot = process.env.NN_BUILD_ROOT || ".";
const buildFiles = [
`${buildRoot}/build/`,
`!${buildRoot}/build/screenshots\${/*}`,
`!${buildRoot}/build/banner.jpg`,
`!${buildRoot}/build/*.ico`,
`!${buildRoot}/build/*.png`
];
const productName = process.env.NN_PRODUCT_NAME || "Notesnook";
const appId = process.env.NN_APP_ID || "org.streetwriters.notesnook";
const outputDir = process.env.NN_OUTPUT_DIR || "output";
const linuxExecutableName = process.env.NN_PRODUCT_NAME
? process.env.NN_PRODUCT_NAME.toLowerCase().replace(/\s+/g, "-")
: "notesnook";
const year = new Date().getFullYear();
const isBeta = pkg.version.includes("-beta");
/**
* @type {import("app-builder-lib").Configuration}
*/
module.exports = {
appId: appId,
productName: productName,
copyright: `Copyright © ${year} Streetwriters (Private) Limited`,
artifactName: "notesnook_${os}_${arch}.${ext}",
generateUpdatesFilesForAllChannels: true,
asar: false,
files: [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
...buildFiles,
"!node_modules${/*}",
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
"node_modules/better-sqlite3-multiple-ciphers/lib",
"node_modules/better-sqlite3-multiple-ciphers/package.json",
"node_modules/file-uri-to-path",
"node_modules/bindings",
"node_modules/node-gyp-build",
"node_modules/sqlite-better-trigram",
"node_modules/sqlite3-fts5-html",
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
{
from: "node_modules/sqlite-better-trigram-linux-${arch}",
to: "node_modules/sqlite-better-trigram-linux-${arch}"
},
{
from: "node_modules/sqlite-better-trigram-darwin-${arch}",
to: "node_modules/sqlite-better-trigram-darwin-${arch}"
},
{
from: "node_modules/sqlite-better-trigram-windows-${arch}",
to: "node_modules/sqlite-better-trigram-windows-${arch}"
},
{
from: "node_modules/sqlite3-fts5-html-linux-${arch}",
to: "node_modules/sqlite3-fts5-html-linux-${arch}"
},
{
from: "node_modules/sqlite3-fts5-html-darwin-${arch}",
to: "node_modules/sqlite3-fts5-html-darwin-${arch}"
},
{
from: "node_modules/sqlite3-fts5-html-windows-${arch}",
to: "node_modules/sqlite3-fts5-html-windows-${arch}"
},
"node_modules/sodium-native/index.js",
"node_modules/sodium-native/package.json"
],
afterPack: "./scripts/removeLocales.js",
mac: {
bundleVersion: "240",
minimumSystemVersion: "10.12.0",
target: [
{
target: "dmg",
arch: ["arm64", "x64"]
},
{
target: "zip",
arch: ["arm64", "x64"]
}
],
category: "public.app-category.productivity",
darkModeSupport: true,
type: "distribution",
hardenedRuntime: true,
entitlements: "assets/entitlements.mac.plist",
entitlementsInherit: "assets/entitlements.mac.plist",
gatekeeperAssess: false,
icon: "assets/icons/app.icns",
notarize: true
},
dmg: {
contents: [
{
x: 130,
y: 220
},
{
x: 410,
y: 220,
type: "link",
path: "/Applications"
}
],
icon: "assets/icons/app.icns",
title: "Install Notesnook"
},
mas: {
entitlements: "assets/entitlements.mas.plist",
entitlementsInherit: "assets/entitlements.mas.inherit.plist",
entitlementsLoginHelper: "assets/entitlements.mas.loginhelper.plist",
hardenedRuntime: true
},
win: {
target: [
{
target: "nsis",
arch: ["x64", "arm64"]
},
{
target: "portable",
arch: ["x64", "arm64"]
}
],
signtoolOptions: {
signingHashAlgorithms: ["sha256"],
sign: "./scripts/sign.js"
},
icon: "assets/icons/app.ico"
},
portable: {
artifactName: "notesnook_${os}_${arch}_portable.${ext}"
},
nsis: {
oneClick: true,
createDesktopShortcut: "always",
deleteAppDataOnUninstall: true
},
linux: {
target: [
{
target: "AppImage",
arch: ["x64", "arm64"]
},
{
target: "snap",
arch: ["x64", "arm64"]
}
],
category: "Office",
icon: "assets/icons/app.icns",
description: "Your private note taking space",
executableName: linuxExecutableName,
desktop: {
desktopActions: {
"new-note": {
Name: "New note",
Exec: `${linuxExecutableName} new note`
},
"new-notebook": {
Name: "New notebook",
Exec: `${linuxExecutableName} new notebook`
},
"new-reminder": {
Name: "New reminder",
Exec: `${linuxExecutableName} new reminder`
}
}
}
},
snap: {
autoStart: false,
confinement: "strict",
allowNativeWayland: true,
base: "core22"
},
extraResources: ["app-update.yml", "./assets/**"],
extraMetadata: {
main: path.join(buildRoot, "build", "electron.js")
},
directories: {
buildResources: "assets",
output: outputDir
},
publish: [
{
provider: "github",
repo: "notesnook",
owner: "streetwriters",
channel: isBeta ? "beta" : "latest"
}
]
};

File diff suppressed because it is too large Load Diff

View File

@@ -2,80 +2,226 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.2.2",
"version": "3.0.7-beta",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"require": {
"types": "./dist/types/index.d.ts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/esm/index.js"
}
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"homepage": "https://notesnook.com/",
"repository": "https://github.com/streetwriters/notesnook",
"license": "GPL-3.0-or-later",
"dependencies": {
"@lingui/core": "5.1.2",
"@notesnook/intl": "file:../../packages/intl",
"@notesnook/ui": "file:../../packages/ui",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"better-sqlite3-multiple-ciphers": "^11.10.0",
"electron-trpc": "0.7.1",
"electron-updater": "^6.6.2",
"icojs": "^0.19.5",
"sqlite-better-trigram": "0.0.3",
"sqlite3-fts5-html": "^0.0.3",
"@notesnook/crypto": "file:../../packages/crypto",
"@trpc/client": "10.38.3",
"@trpc/server": "10.38.3",
"better-sqlite3-multiple-ciphers": "^9.4.0",
"electron-trpc": "0.5.2",
"electron-updater": "6.1.4",
"icojs": "^0.17.1",
"sodium-native": "^4.0.6",
"typed-emitter": "^2.1.0",
"yargs": "^17.7.2",
"zod": "3.24.3"
"yargs": "^17.6.2",
"zod": "^3.21.4"
},
"devDependencies": {
"@streetwriters/kysely": "^0.27.4",
"@types/node": "22.15.3",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^34.0.0",
"electron-builder": "^26.0.12",
"esbuild": "0.21.5",
"node-abi": "^4.5.0",
"node-gyp-build": "^4.8.4",
"playwright": "1.48.2",
"prebuildify": "^6.0.1",
"slugify": "1.6.6",
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "^28.2.1",
"electron-builder": "^24.9.1",
"esbuild": "^0.20.0",
"tree-kill": "^1.2.2",
"undici": "^7.8.0",
"vitest": "2.1.8"
"undici": "^6.6.1"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
},
"overrides": {
"@electron/node-gyp": "^10.2.0-electron.2"
},
"scripts": {
"start": "node scripts/dev.mjs",
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "node ../../scripts/build.mjs",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"build": "tsc",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
"postinstall": "patch-package",
"test": "vitest run"
"postinstall": "patch-package"
},
"author": {
"name": "Streetwriters (Private) Limited",
"email": "support@streetwriters.co",
"url": "https://streetwriters.co"
},
"build": {
"appId": "org.streetwriters.notesnook",
"productName": "Notesnook",
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
"artifactName": "notesnook_${os}_${arch}.${ext}",
"asar": false,
"generateUpdatesFilesForAllChannels": true,
"files": [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
"build/",
"!build/screenshots${/*}",
"!build/banner.jpg",
"!build/*.ico",
"!build/*.png",
"!node_modules${/*}",
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
"node_modules/better-sqlite3-multiple-ciphers/lib",
"node_modules/better-sqlite3-multiple-ciphers/package.json",
"node_modules/file-uri-to-path",
"node_modules/bindings",
"node_modules/node-gyp-build",
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
"node_modules/sodium-native/index.js",
"node_modules/sodium-native/package.json"
],
"afterPack": "./scripts/removeLocales.js",
"mac": {
"bundleVersion": "240",
"minimumSystemVersion": "10.12.0",
"target": [
{
"target": "dmg",
"arch": [
"arm64",
"x64"
]
},
{
"target": "zip",
"arch": [
"arm64",
"x64"
]
}
],
"category": "public.app-category.productivity",
"darkModeSupport": true,
"type": "distribution",
"hardenedRuntime": true,
"entitlements": "assets/entitlements.mac.plist",
"entitlementsInherit": "assets/entitlements.mac.plist",
"gatekeeperAssess": false,
"icon": "assets/icons/app.icns",
"notarize": true
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
],
"icon": "assets/icons/app.icns",
"title": "Install Notesnook"
},
"mas": {
"entitlements": "assets/entitlements.mas.plist",
"entitlementsInherit": "assets/entitlements.mas.inherit.plist",
"entitlementsLoginHelper": "assets/entitlements.mas.loginhelper.plist",
"hardenedRuntime": true
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"arm64"
]
},
{
"target": "portable",
"arch": [
"x64",
"arm64"
]
}
],
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js",
"icon": "assets/icons/app.ico"
},
"portable": {
"artifactName": "notesnook_${os}_${arch}_portable.${ext}"
},
"nsis": {
"oneClick": true,
"createDesktopShortcut": "always"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64",
"arm64"
]
},
{
"target": "snap",
"arch": [
"x64",
"arm64"
]
}
],
"category": "Office",
"icon": "assets/icons/app.icns",
"description": "Your private note taking space",
"executableName": "notesnook",
"desktop": {
"actions": [
{
"id": "new-note",
"name": "New note",
"args": "new note"
},
{
"id": "new-notebook",
"name": "New notebook",
"args": "new notebook"
},
{
"id": "new-reminder",
"name": "New reminder",
"args": "new reminder"
}
]
}
},
"snap": {
"autoStart": false,
"confinement": "strict",
"allowNativeWayland": true
},
"extraResources": [
"app-update.yml",
"./assets/**"
],
"extraMetadata": {
"main": "./build/electron.js"
},
"directories": {
"buildResources": "assets",
"output": "./output/"
},
"publish": [
{
"provider": "github",
"repo": "notesnook",
"owner": "streetwriters",
"channel": "beta"
}
]
}
}

View File

@@ -0,0 +1,36 @@
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
index fcb7f54..cb1c7f7 100644
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
StartupWMClass: appInfo.productName,
...extra,
...targetSpecificOptions.desktop,
+ actions: undefined
};
const description = this.getDescription(targetSpecificOptions);
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
data += `\n${name}=${desktopMeta[name]}`;
}
data += "\n";
+
+ if (targetSpecificOptions.desktop.actions) {
+ let actionsData = "";
+ const validActions = [];
+ for (const action of targetSpecificOptions.desktop.actions) {
+ if (!action.id || !action.name || !action.args) continue;
+ actionsData += "\n";
+ actionsData += `[Desktop Action ${action.id}]
+Name=${action.name}
+Exec=${desktopMeta.Exec} ${action.args}`;
+ actionsData += "\n";
+
+ validActions.push(action.id);
+ }
+
+ data += `Actions=${validActions.join(";")};\n${actionsData}`
+ }
return Promise.resolve(data);
}
}

View File

@@ -1,13 +0,0 @@
diff --git a/node_modules/app-builder-lib/out/targets/snap.js b/node_modules/app-builder-lib/out/targets/snap.js
index 0b5405e..753f6c6 100644
--- a/node_modules/app-builder-lib/out/targets/snap.js
+++ b/node_modules/app-builder-lib/out/targets/snap.js
@@ -115,7 +115,7 @@ class SnapTarget extends core_1.Target {
else {
const archTriplet = archNameToTriplet(arch);
appDescriptor.environment = {
- DISABLE_WAYLAND: options.allowNativeWayland ? "" : "1",
+ DISABLE_WAYLAND: options.allowNativeWayland ? "0" : "1",
PATH: "$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH",
SNAP_DESKTOP_RUNTIME: "$SNAP/gnome-platform",
LD_LIBRARY_PATH: [

View File

@@ -1,11 +0,0 @@
diff --git a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
index 1a14ecd..ff938f1 100644
--- a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
+++ b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
@@ -38,5 +38,6 @@
'SQLITE_TRACE_SIZE_LIMIT=32',
'SQLITE_USER_AUTHENTICATION=0',
'SQLITE_USE_URI=0',
+ 'SQLITE_ENABLE_REGEXP',
],
}

View File

@@ -0,0 +1,23 @@
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
index 379cf3b..2644e5d 100644
--- a/node_modules/electron-trpc/dist/main.mjs
+++ b/node_modules/electron-trpc/dist/main.mjs
@@ -221,9 +221,16 @@ class G {
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
}
detachWindow(r) {
+
y(this, c, i(this, c).filter((n) => n !== r));
- for (const [n, t] of i(this, u).entries())
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
+ for (const [n, t] of i(this, u).entries()) {
+ try {
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
+ } catch(e) {
+ console.error(e);
+ // ignore
+ }
+ }
}
}
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {

View File

@@ -18,58 +18,53 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "path";
import fs from "fs/promises";
import fs, { readFile, writeFile } from "fs/promises";
import { existsSync } from "fs";
import yargs from "yargs-parser";
import os from "os";
import * as childProcess from "child_process";
import { fileURLToPath } from "url";
import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = args.root || path.join(__dirname, "..");
const skipTscBuild = args.skipTscBuild || false;
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
await fs.rm(path.join(root, "build"), { force: true, recursive: true });
await fs.rm("./build/", { force: true, recursive: true });
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec(
"npx nx build:desktop @notesnook/web",
path.join(__dirname, "..", "..", "..")
);
await exec(`cd ${webAppPath} && npm run build:desktop`);
}
// temporary until there's support for prebuilt binaries for windows ARM & linux ARM
await patchBetterSQLite3();
await fs.cp(path.join(webAppPath, "build"), path.join(root, "build"), {
if (os.platform() === "win32")
await exec(
"npx prebuildify --arch=arm64 --strip",
path.join(__dirname, "..", "node_modules", "sodium-native")
);
await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true,
force: true
});
if (args.variant === "mas") {
await exec(`yarn run bundle:mas --outdir=${path.join(root, "build")}`);
await exec(`npm run bundle:mas`);
} else {
await exec(`yarn run bundle --outdir=${path.join(root, "build")}`);
await exec(`npm run bundle`);
}
if (!skipTscBuild) {
await exec(`yarn run build`);
}
await exec(`npx tsc`);
if (args.run) {
await exec(
`yarn electron-builder --dir --${process.arch} --config=electron-builder.config.js`
);
await exec(`npx electron-builder --dir --x64`);
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {
if (process.arch === "arm64")
await exec(`./output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook`);
else await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
await exec(`./output/darwin-unpacked/Notesnook`);
} else {
await exec(`./output/linux-unpacked/Notesnook`);
}
@@ -82,3 +77,21 @@ async function exec(cmd, cwd) {
cwd: cwd || process.cwd()
});
}
async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "9.4.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}

View File

@@ -36,8 +36,6 @@ const ENV = {
FORCE_COLOR: "false",
COLOR: "0"
};
process.chdir(path.join(__dirname, ".."));
await onChange(true);
console.log("Watching...");
@@ -55,13 +53,11 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npm rebuild electron --verbose --foreground-scripts");
await exec("yarn electron-builder install-app-deps");
await exec("npx electron-rebuild");
}
await exec(`yarn run bundle`);
await exec(`yarn run build`);
await exec(`npm run bundle`);
execAsync(`npx`, [`tsc`]);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
@@ -70,8 +66,8 @@ async function onChange(first) {
if (first) {
await spawnAndWaitUntil(
["npm", "run", "start:desktop"],
path.join(__dirname, "..", "..", "web"),
["npx", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."),
(data) => data.includes("Network: use --host to expose")
);
}
@@ -82,7 +78,7 @@ async function onChange(first) {
}
execAsync(
"yarn",
"npx",
["electron", path.join("build", "electron.js")],
true,
cleanup

View File

@@ -1,58 +0,0 @@
/*
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 { readFile, rm, writeFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function patchBetterSQLite3() {
console.log("Patching better-sqlite3");
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
delete json.homepage;
delete json.repository;
await writeFile(jsonPath, JSON.stringify(json));
await rm(
path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"build"
),
{ force: true, recursive: true }
);
}
if (process.argv[1] === __filename) {
patchBetterSQLite3();
}

View File

@@ -33,7 +33,7 @@ module.exports = async function (configuration) {
const DescriptionUrl = "https://notesnook.com/";
const FilesCatalog = createCatalog(configuration.path);
const command = `Invoke-TrustedSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
const command = `Invoke-AzureCodeSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
console.debug("Signing", configuration.path, "using command", command);

View File

@@ -24,7 +24,6 @@ import { spellCheckerRouter } from "./spell-checker";
import { updaterRouter } from "./updater";
import { bridgeRouter } from "./bridge";
import { safeStorageRouter } from "./safe-storage";
import { windowRouter } from "./window";
const t = initTRPC.create();
@@ -34,8 +33,7 @@ export const router = t.router({
spellChecker: spellCheckerRouter,
updater: updaterRouter,
bridge: bridgeRouter,
safeStorage: safeStorageRouter,
window: windowRouter
safeStorage: safeStorageRouter
});
export const api = router.createCaller({});

View File

@@ -19,16 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { initTRPC } from "@trpc/server";
import { z } from "zod";
import {
app,
dialog,
Menu,
MenuItem,
nativeImage,
nativeTheme,
Notification,
shell
} from "electron";
import { dialog, nativeTheme, Notification, shell } from "electron";
import { AutoLaunch } from "../utils/autolaunch";
import { config, DesktopIntegration } from "../utils/config";
import { bringToFront } from "../utils/bring-to-front";
@@ -38,11 +29,9 @@ import { dirname } from "path";
import { resolvePath } from "../utils/resolve-path";
import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak, isSnap } from "../utils";
import { isFlatpak } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
import type { MenuItem as NNMenuItem } from "@notesnook/ui";
const t = initTRPC.create();
@@ -59,7 +48,6 @@ const NotificationOptions = z.object({
export const osIntegrationRouter = t.router({
isFlatpak: t.procedure.query(() => isFlatpak()),
isSnap: t.procedure.query(() => isSnap()),
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
@@ -67,15 +55,6 @@ export const osIntegrationRouter = t.router({
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),
setProxyRules: t.procedure
.input(z.string().optional())
@@ -163,10 +142,6 @@ export const osIntegrationRouter = t.router({
await rm(input);
}),
restart: t.procedure.query(() => {
app.relaunch();
app.exit();
}),
showNotification: t.procedure
.input(NotificationOptions)
.query(({ input }) => {
@@ -195,103 +170,16 @@ export const osIntegrationRouter = t.router({
}),
bringToFront: t.procedure.query(() => bringToFront()),
changeTheme: t.procedure
.input(
z.object({
theme: Theme,
windowControlsIconColor: z.string().optional(),
backgroundColor: z.string().optional()
})
)
.mutation(
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
if (windowControlsIconColor) {
config.windowControlsIconColor = windowControlsIconColor;
if (
process.platform === "win32" &&
!config.desktopSettings.nativeTitlebar
)
globalThis.window?.setTitleBarOverlay({
symbolColor: windowControlsIconColor
});
}
if (backgroundColor) {
config.backgroundColor = backgroundColor;
}
setTheme(theme);
}
),
.input(Theme)
.mutation(({ input }) => setTheme(input)),
onThemeChanged: t.procedure.subscription(() =>
observable<"dark" | "light">((emit) => {
const updated = () => {
nativeTheme.on("updated", () => {
if (getTheme() === "system") {
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
}
};
nativeTheme.on("updated", updated);
return () => {
nativeTheme.off("updated", updated);
};
})
),
showMenu: t.procedure
.input(
z.object({
menuItems: z.array(z.any())
})
)
.subscription(({ input: { menuItems } }) =>
observable<string[]>((emit) => {
const items = menuItems as NNMenuItem[];
const menu = new Menu();
for (const item of items) {
const menuItem = toMenuItem(item, (id) => emit.next(id));
if (menuItem) menu.append(menuItem);
}
if (menu.items.length > 0) menu.popup();
return () => {
menu.removeAllListeners();
menu.closePopup();
};
})
)
});
function toMenuItem(
item: NNMenuItem,
onClick: (id: string[]) => void,
parentKey?: string
): MenuItem | undefined {
switch (item.type) {
case "lazy-loader":
return undefined;
case "separator":
return new MenuItem({ type: "separator" });
case "button": {
const submenu = item.menu ? new Menu() : undefined;
if (submenu && item.menu) {
for (const subitem of item.menu.items) {
const subMenuItem = toMenuItem(subitem, onClick, item.key);
if (subMenuItem) submenu.append(subMenuItem);
}
}
return new MenuItem({
label: item.title,
enabled: !item.isDisabled,
visible: !item.isHidden,
toolTip: item.tooltip,
sublabel: item.tooltip,
checked: item.isChecked,
type: submenu ? "submenu" : item.isChecked ? "checkbox" : "normal",
id: item.key,
submenu,
click: () => onClick(parentKey ? [parentKey, item.key] : [item.key]),
accelerator: item.modifier?.replace("Mod", "CommandOrControl")
});
}
}
}
})
)
});

View File

@@ -1,177 +0,0 @@
/*
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 "@streetwriters/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> = {};
extensionsLoaded = false;
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;
} finally {
// Since SQLite 3.48.0 (SQLite3MC v2.0.2) it's not possible to load fts5
// extensions before database has been decrypting. This is because
// executing a `SELECT` now accesses the underlying databases resulting in
// an error. Since FTS5 extensions depend on `SELECT fts5` to load the
// fts5 API, we must wait decrypt the database before we can load
// the extensions.
if (!this.extensionsLoaded && (await this.isDatabaseReady())) {
const betterTrigram = require("sqlite-better-trigram");
const fts5Html = require("sqlite3-fts5-html");
betterTrigram.load(this.sqlite);
fts5Html.load(this.sqlite);
this.extensionsLoaded = true;
}
}
}
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
});
}
/**
* This just executes `SELECT 1` on the database to make sure its ready.
* On an encrypted database, this will fail until `PRAGMA key` has been
* called.
*/
private async isDatabaseReady() {
// return this.exec(`SELECT 1;`)
// .then(() => true)
// .catch(() => false);
if (!this.sqlite) return false;
try {
this.sqlite.prepare(`SELECT 1;`).run();
return true;
} catch {
return false;
}
}
}

View File

@@ -23,45 +23,21 @@ import { CancellationToken, autoUpdater } from "electron-updater";
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
import { z } from "zod";
import { config } from "../utils/config";
import { app } from "electron";
type UpdateInfo = { version: string };
type Progress = { percent: number };
const t = initTRPC.create();
let cancellationToken: CancellationToken | undefined = undefined;
let downloadTimeout: NodeJS.Timeout | undefined = undefined;
export const updaterRouter = t.router({
autoUpdates: t.procedure.query(() => config.automaticUpdates),
releaseTrack: t.procedure.query(() => config.releaseTrack),
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
download: t.procedure.query(async () => {
if (cancellationToken) return;
clearTimeout(downloadTimeout);
await new Promise<string[]>((resolve, reject) => {
downloadTimeout = setTimeout(async () => {
cancellationToken = new CancellationToken();
autoUpdater.isUpdaterActive();
await autoUpdater
.downloadUpdate(cancellationToken)
.then(resolve)
.catch(reject)
.finally(() => (cancellationToken = undefined));
}, 1000);
});
const cancellationToken = new CancellationToken();
await autoUpdater.downloadUpdate(cancellationToken);
}),
check: t.procedure.query(async () => {
if (cancellationToken) return;
clearTimeout(downloadTimeout);
await new Promise<void>((resolve) => {
downloadTimeout = setTimeout(async () => {
await autoUpdater
.checkForUpdates()
.catch(console.error)
.finally(resolve);
}, 1000);
});
await autoUpdater.checkForUpdates();
}),
toggleAutoUpdates: t.procedure
@@ -69,13 +45,7 @@ export const updaterRouter = t.router({
.mutation(({ input: { enabled } }) => {
config.automaticUpdates = enabled;
}),
changeReleaseTrack: t.procedure
.input(z.object({ track: z.string() }))
.mutation(({ input: { track } }) => {
config.releaseTrack = track;
app.relaunch();
app.exit();
}),
onChecking: createSubscription("checking-for-update"),
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
"update-downloaded"
@@ -87,18 +57,7 @@ export const updaterRouter = t.router({
"update-not-available"
),
onAvailable: createSubscription<"update-available", UpdateInfo>(
"update-available",
() => {
if (!config.automaticUpdates) return false;
autoUpdater.emit("download-progress", {
bytesPerSecond: 0,
delta: 0,
percent: 0,
total: 100,
transferred: 0
});
return true;
}
"update-available"
),
onError: createSubscription("error")
});
@@ -106,14 +65,12 @@ export const updaterRouter = t.router({
function createSubscription<
TName extends keyof AppUpdaterEvents,
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
>(eventName: TName, handler?: (args: TReturnType) => boolean) {
>(eventName: TName) {
return t.procedure.subscription(() => {
return observable<TReturnType>((emit) => {
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
if (handler?.(args[0])) return;
emit.next(args[0]);
};
autoUpdater.removeAllListeners(eventName);
autoUpdater.addListener(eventName, listener);
return () => {
autoUpdater.removeListener(eventName, listener);

View File

@@ -1,83 +0,0 @@
/*
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 { initTRPC } from "@trpc/server";
import { observable } from "@trpc/server/observable";
const t = initTRPC.create();
export const windowRouter = t.router({
maximize: t.procedure.mutation(() => {
globalThis.window?.maximize();
}),
restore: t.procedure.mutation(() => {
globalThis.window?.restore();
}),
minimze: t.procedure.mutation(() => {
globalThis.window?.minimize();
}),
maximized: t.procedure.query(() => globalThis.window?.isMaximized()),
fullscreen: t.procedure.query(() => globalThis.window?.isFullScreen()),
onWindowStateChanged: t.procedure.subscription(() => {
return observable<{ maximized: boolean; fullscreen: boolean }>((emit) => {
function listener() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: !!globalThis.window?.isFullScreen()
});
}
function enterFullscreen() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: true
});
}
function leaveFullscreen() {
emit.next({
maximized: !!globalThis.window?.isMaximized(),
fullscreen: false
});
}
globalThis.window?.addListener("maximize", listener);
globalThis.window?.addListener("minimize", listener);
globalThis.window?.addListener("unmaximize", listener);
globalThis.window?.addListener("restore", listener);
globalThis.window?.addListener("enter-full-screen", enterFullscreen);
globalThis.window?.addListener("leave-full-screen", leaveFullscreen);
globalThis.window?.addListener("leave-html-full-screen", leaveFullscreen);
globalThis.window?.addListener("enter-html-full-screen", enterFullscreen);
return () => {
globalThis.window?.removeListener("maximize", listener);
globalThis.window?.removeListener("minimize", listener);
globalThis.window?.removeListener("unmaximize", listener);
globalThis.window?.removeListener("restore", listener);
globalThis.window?.removeListener("enter-full-screen", enterFullscreen);
globalThis.window?.removeListener("leave-full-screen", leaveFullscreen);
globalThis.window?.removeListener(
"leave-html-full-screen",
leaveFullscreen
);
globalThis.window?.removeListener(
"enter-html-full-screen",
enterFullscreen
);
};
});
})
});

View File

@@ -17,8 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export { PATHS } from "./constants";
export * from "./constants";
export type { AppRouter } from "./api";
export { type UpdateInfo } from "builder-util-runtime";
export { type DesktopIntegration } from "./utils/config";
export { SQLite } from "./api/sqlite-kysely.js";

View File

@@ -35,21 +35,6 @@ import path from "path";
import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
import { Messages, setI18nGlobal } from "@notesnook/intl";
import { i18n } from "@lingui/core";
const locale =
process.env.NODE_ENV === "development"
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
: import("@notesnook/intl/locales/$en.json");
locale.then(({ default: locale }) => {
i18n.load({
en: locale.messages as unknown as Messages
});
i18n.activate("en");
});
setI18nGlobal(i18n);
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -85,8 +70,6 @@ async function createWindow() {
const mainWindowState = new WindowState({});
const mainWindow = new BrowserWindow({
show: !cliOptions.hidden,
paintWhenInitiallyHidden: cliOptions.hidden,
skipTaskbar: cliOptions.hidden,
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
@@ -99,31 +82,10 @@ async function createWindow() {
size: 512,
format: process.platform === "win32" ? "ico" : "png"
}),
...(config.desktopSettings.nativeTitlebar
? {}
: {
titleBarStyle:
process.platform === "win32" || process.platform === "darwin"
? "hidden"
: "default",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
trafficLightPosition: {
x: 16,
y: 12
}
}),
webPreferences: {
zoomFactor: config.zoomFactor,
nodeIntegration: true,
contextIsolation: false,
nodeIntegrationInWorker: true,
spellcheck: config.isSpellCheckerEnabled,
preload: __dirname + "/preload.js"
}
@@ -147,11 +109,6 @@ async function createWindow() {
await AssetManager.loadIcons();
setupDesktopIntegration(config.desktopSettings);
mainWindow.webContents.session.setPermissionRequestHandler(
(webContents, permission, callback) => {
callback(permission === "geolocation" ? false : true);
}
);
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
);
@@ -165,7 +122,7 @@ async function createWindow() {
setupJumplist();
if (isDevelopment())
mainWindow.webContents.openDevTools({ mode: "bottom", activate: true });
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
@@ -181,9 +138,6 @@ async function createWindow() {
app.once("ready", async () => {
console.info("App ready. Opening window.");
if (config.customDns) enableCustomDns();
else disableCustomDns();
if (!isDevelopment()) registerProtocol();
await createWindow();
configureAutoUpdater();

View File

@@ -19,25 +19,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable no-var */
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
// import type { NNCrypto } from "@notesnook/crypto";
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
import type { NNCrypto } from "@notesnook/crypto";
import { ipcRenderer } from "electron";
import { platform } from "os";
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
declare global {
var os: () => "mas" | ReturnType<typeof platform>;
var electronTRPC: any;
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
var electronTRPC: RendererGlobalElectronTRPC;
var NativeNNCrypto: (new () => NNCrypto) | undefined;
var createSQLite3Database: (filename: string) => Database;
}
process.once("loaded", async () => {
const electronTRPC = {
sendMessage: (operation: any) =>
const electronTRPC: RendererGlobalElectronTRPC = {
sendMessage: (operation) =>
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
onMessage: (callback: any) =>
onMessage: (callback) =>
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
};
globalThis.electronTRPC = electronTRPC;
});
// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { NativeImage, nativeImage } from "electron";
import path from "path";
import { isDevelopment } from "./index";
import { ParsedImage, parseICO } from "icojs";
import { parse, ParsedImage } from "icojs";
import { getSystemTheme } from "./theme";
import { readFile } from "fs/promises";
@@ -71,7 +71,7 @@ export class AssetManager {
`${icon}${prefix}.ico`
);
const icoBuffer = await readFile(icoPath);
const images = await parseICO(icoBuffer, "image/png");
const images = await parse(icoBuffer, "image/png");
ALL_ICONS.push({ id: icon, images, prefix });
}
}

View File

@@ -20,25 +20,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { autoUpdater } from "electron-updater";
import { config } from "./config";
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
? "beta"
: "latest";
async function configureAutoUpdater() {
const releaseTrack =
config.releaseTrack === "stable" ? "latest" : config.releaseTrack;
autoUpdater.setFeedURL({
provider: "generic",
url: `https://notesnook.com/api/v1/releases/${process.platform}/${releaseTrack}`,
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
useMultipleRangeRequest: false,
channel: releaseTrack
channel: CHANNEL
});
autoUpdater.autoDownload = config.automaticUpdates;
autoUpdater.allowDowngrade =
// only allow downgrade if the current version is a prerelease
// and the user has changed the release track to stable
config.releaseTrack === "stable" &&
autoUpdater.currentVersion.prerelease.length > 0;
autoUpdater.allowDowngrade = false;
autoUpdater.allowPrerelease = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.disableWebInstaller = true;
}
export { configureAutoUpdater };

View File

@@ -20,14 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { nativeTheme } from "electron";
import { JSONStorage } from "./json-storage";
import { z } from "zod";
import { autoUpdater } from "electron-updater";
export const DesktopIntegration = z.object({
autoStart: z.boolean().optional(),
startMinimized: z.boolean().optional(),
minimizeToSystemTray: z.boolean().optional(),
closeToSystemTray: z.boolean().optional(),
nativeTitlebar: z.boolean().optional()
closeToSystemTray: z.boolean().optional()
});
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
@@ -37,23 +35,14 @@ export const config = {
autoStart: false,
startMinimized: false,
minimizeToSystemTray: false,
closeToSystemTray: false,
nativeTitlebar: false
closeToSystemTray: false
},
privacyMode: false,
isSpellCheckerEnabled: true,
zoomFactor: 1,
theme: nativeTheme.themeSource,
automaticUpdates: true,
proxyRules: "",
customDns: true,
releaseTrack: autoUpdater.currentVersion.raw.includes("-beta")
? "beta"
: "stable",
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
windowControlsIconColor:
nativeTheme.themeSource === "dark" ? "#ffffff" : "#000000"
proxyRules: ""
};
type ConfigKey = keyof typeof config;

View File

@@ -29,7 +29,3 @@ export function isDevelopment() {
export function isFlatpak() {
return existsSync("/.flatpak-info");
}
export function isSnap() {
return process.env.SNAP !== undefined;
}

View File

@@ -17,7 +17,6 @@ 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 { strings } from "@notesnook/intl";
import { Menu, MenuItem, clipboard, shell } from "electron";
function setupMenu() {
@@ -41,7 +40,7 @@ function setupMenu() {
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: strings.addToDictionary(),
label: "Add to dictionary",
click: () =>
globalThis.window?.webContents.session.addWordToSpellCheckerDictionary(
params.misspelledWord
@@ -60,7 +59,7 @@ function setupMenu() {
if (params.linkURL.length) {
menu.append(
new MenuItem({
label: strings.openInBrowser(),
label: "Open in browser",
click: () => shell.openExternal(params.linkURL)
})
);
@@ -69,7 +68,7 @@ function setupMenu() {
if (params.isEditable) {
menu.append(
new MenuItem({
label: strings.undo(),
label: "Undo",
role: "undo",
enabled: params.isEditable,
accelerator: "CommandOrControl+Z"
@@ -78,7 +77,7 @@ function setupMenu() {
menu.append(
new MenuItem({
label: strings.redo(),
label: "Redo",
role: "redo",
enabled: params.isEditable,
accelerator: "CommandOrControl+Y"
@@ -95,7 +94,7 @@ function setupMenu() {
if (params.isEditable)
menu.append(
new MenuItem({
label: strings.cut(),
label: "Cut",
role: "cut",
enabled: params.selectionText.length > 0,
accelerator: "CommandOrControl+X"
@@ -105,7 +104,7 @@ function setupMenu() {
if (params.linkURL?.length) {
menu.append(
new MenuItem({
label: strings.copyLink(),
label: "Copy link",
click() {
clipboard.writeText(params.linkURL);
}
@@ -114,7 +113,7 @@ function setupMenu() {
menu.append(
new MenuItem({
label: strings.copyLinkText(),
label: "Copy link text",
click() {
clipboard.writeText(params.linkText);
}
@@ -125,7 +124,7 @@ function setupMenu() {
if (params.selectionText.length) {
menu.append(
new MenuItem({
label: strings.copy(),
label: "Copy",
role: "copy",
accelerator: "CommandOrControl+C"
})
@@ -136,51 +135,23 @@ function setupMenu() {
menu.append(
new MenuItem({
id: "copy-image",
label: strings.copyImage(),
label: "Copy Image",
click() {
globalThis.window?.webContents.copyImageAt(params.x, params.y);
}
})
);
if (params.isEditable) {
if (params.isEditable)
menu.append(
new MenuItem({
label: strings.paste(),
role: "paste",
label: "Paste",
role: "pasteAndMatchStyle",
enabled: clipboard.readText("clipboard").length > 0,
accelerator: "CommandOrControl+V"
})
);
menu.append(
new MenuItem({
label:
process.platform === "darwin"
? strings.pasteAndMatchStyle()
: strings.pasteWithoutFormatting(),
role: "pasteAndMatchStyle",
enabled: clipboard.readText("clipboard").length > 0,
accelerator:
process.platform === "darwin"
? "Option+Shift+Command+V"
: "Shift+CommandOrControl+V"
})
);
menu.append(
new MenuItem({
type: "separator"
})
);
menu.append(
new MenuItem({
label: strings.spellCheck(),
role: "toggleSpellChecker"
})
);
}
if (menu.items.length > 0) menu.popup();
});
}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { net, protocol } from "electron";
import { protocol } from "electron";
import { isDevelopment } from "./index";
import { createReadStream } from "fs";
import { extname, normalize } from "path";
@@ -62,7 +62,26 @@ function registerProtocol() {
headers: { "Content-Type": extensionToMimeType[fileExtension] }
});
} else {
return net.fetch(request, { bypassCustomProtocolHandlers: true });
if (request.headers.has("X-Content-Length")) {
request.headers.set(
"Content-Length",
request.headers.get("X-Content-Length") || "0"
);
request.headers.delete("X-Content-Length");
}
const headers = Object.fromEntries(request.headers.entries());
return await fetch(request.url, {
signal: request.signal,
mode: request.mode,
headers,
method: request.method,
body: request.body,
credentials: request.credentials,
referrer: (request as any).referrer,
duplex: "half",
redirect: "manual"
});
}
});
console.info(`${SCHEME} protocol inteception "successful"`);

View File

@@ -41,10 +41,7 @@ function setTheme(theme: Theme) {
}
function getBackgroundColor() {
return (
config.backgroundColor ||
(nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff")
);
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
}
function getSystemTheme() {
@@ -57,8 +54,7 @@ function getSystemTheme() {
nativeTheme.themeSource = oldThemeSource;
setTimeout(
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
1000
);
return currentTheme;
@@ -73,8 +69,7 @@ function changeTheme(theme: Theme) {
nativeTheme.themeSource = theme;
setTimeout(
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
1000
);
}

View File

@@ -2,8 +2,7 @@
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist/",
"lib": ["ESNext"],
"moduleResolution": "Bundler"
"lib": ["ESNext"]
},
"include": ["src", "global.d.ts"]
}

View File

@@ -1,40 +0,0 @@
/*
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 { defineConfig } from "vitest/config";
export default defineConfig({
test: {
testTimeout: process.env.CI ? 120 * 1000 : 120 * 1000,
sequence: {
concurrent: true,
shuffle: true
},
dir: "./__tests__/",
exclude: [
"**/node_modules/**",
"**/dist/**",
"**/cypress/**",
"**/.{idea,git,cache,output,temp}/**",
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*",
"**/test-results/**",
"**/test-artifacts/**"
]
}
});

View File

@@ -38,8 +38,7 @@ DerivedData
*.ipa
*.xcuserstate
*.hprof
**/.xcode.env.local
native/cache
ios/.xcode.env.local
# Android/IntelliJ
#
rn-build-deps/
@@ -69,7 +68,6 @@ buck-out/
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
native/vendor
# Bundle artifact
*.jsbundle

View File

@@ -16,81 +16,85 @@ 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 { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import "react-native-gesture-handler";
import {
ScopedThemeProvider,
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { Appearance, I18nManager, StatusBar } from "react-native";
import "react-native-gesture-handler";
import { I18nManager, View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import DialogProvider from "./components/dialog-provider";
import AppLockedOverlay from "./components/app-lock-overlay";
import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import { Toast } from "./components/toast";
import { useAppEvents } from "./hooks/use-app-events";
import { NotePreviewConfigure } from "./screens/note-preview-configure";
import { RootNavigation } from "./navigation/navigation-stack";
import { ApplicationHolder } from "./navigation";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { changeSystemBarColors, useThemeStore } from "./stores/use-theme-store";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import RNBootSplash from "react-native-bootsplash";
import AppLocked from "./components/app-lock";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
RNBootSplash.hide();
const App = (props: { configureMode: "note-preview" }) => {
useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
const App = () => {
const init = useAppEvents();
useEffect(() => {
changeSystemBarColors();
SettingsService.onFirstLaunch();
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
SettingsService.onFirstLaunch();
await Notifications.get();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(true);
}
TipManager.init();
}, 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<SafeAreaProvider>
<StatusBar translucent={true} backgroundColor="transparent" />
<View
style={{
height: "100%",
width: "100%"
}}
>
<View
style={{
position: "absolute",
width: "100%",
height: "100%",
zIndex: -1
}}
pointerEvents="none"
>
<SafeAreaProvider>
<GlobalSafeAreaProvider />
</SafeAreaProvider>
</View>
<GestureHandlerRootView
style={{
height: "100%",
width: "100%"
}}
>
<GlobalSafeAreaProvider />
{props.configureMode === "note-preview" ? (
<NotePreviewConfigure />
) : (
<RootNavigation />
)}
<ScopedThemeProvider value="dialog">
<Toast />
</ScopedThemeProvider>
<DialogProvider />
<AppLocked />
<ApplicationHolder />
</GestureHandlerRootView>
</SafeAreaProvider>
<AppLockedOverlay />
</View>
);
};
@@ -100,8 +104,8 @@ let currTheme =
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: (props: any) => JSX.Element) => {
return function AppWithThemeProvider(props: any) {
export const withTheme = (Element: () => JSX.Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,
state.darkTheme,
@@ -120,42 +124,26 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
})
.then((theme) => {
if (theme) {
console.log(theme.version, "theme updated");
theme.colorScheme === "dark"
? useThemeStore.getState().setDarkTheme(theme)
: useThemeStore.getState().setLightTheme(theme);
}
})
.catch(() => {
/* empty */
});
.catch(console.log);
}, 1000);
const listener = Appearance.addChangeListener(({ colorScheme }) => {
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
useThemeStore.getState().setColorScheme(colorScheme);
}
});
return () => {
listener.remove();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
useThemeEngineStore
.getState()
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
currTheme = nextTheme;
}
}, [colorScheme, darkTheme, lightTheme]);
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
useThemeEngineStore
.getState()
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
currTheme = nextTheme;
}
return (
<I18nProvider i18n={i18n}>
<Element {...props} />
</I18nProvider>
);
return <Element />;
};
};

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,156 @@
/*
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 { Platform } from "react-native";
import { MMKV } from "../database/mmkv";
import { useSettingStore } from "../../stores/use-setting-store";
const WEBSITE_ID = "3c6890ce-8410-49d5-8831-15fb2eb28a21";
const baseUrl = "https://analytics.streetwriters.co/api/collect";
const UA =
Platform.OS === "ios"
? "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
: `
Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36`;
/**
*
* @param {Routes} route
* @param {Routes[]} conditions
* @returns
*/
async function canUpdateAnalytics(route, conditions = []) {
if (!useSettingStore?.getState()?.settings?.telemetry) return false;
let eventsList = MMKV.getString("notesnookUserEvents");
if (eventsList) {
eventsList = JSON.parse(eventsList);
}
if (eventsList && eventsList[route]) {
console.log("analytics: event already sent", route);
return false;
}
if (route !== "/welcome") {
for (let cond of conditions) {
if (!eventsList || !eventsList[cond]) {
console.log("analytics: conditions not met for event", route, cond);
return false;
}
}
}
console.log("analytics: will send event", route);
return true;
}
async function saveAnalytics(route, value = true) {
let eventsList = MMKV.getString("notesnookUserEvents");
if (eventsList) {
eventsList = JSON.parse(eventsList);
} else {
eventsList = {};
}
eventsList[route] = value;
MMKV.setString("notesnookUserEvents", JSON.stringify(eventsList));
}
/**
*@typedef {"/welcome" | "/home" | "/signup" | "/first-note" | "/account-created" | "/pro-sheet" | "/pro-plans" | "/iap-native" | "/pro-screen" | "/editor" | "/editor-toolbar" | "/properties" | "/sidemenu"} Routes
* @param {Routes} prevRoute
* @param {Routes} route
* @param {Routes[]} conditions
* @param {boolean} once
* @returns
*/
async function pageView(
route,
prevRoute = "",
conditions = ["/welcome"],
once = true
) {
if (__DEV__) return;
if (!(await canUpdateAnalytics(route, conditions)) && once) return;
let body = {
payload: {
website: WEBSITE_ID,
url: `notesnook-${Platform.OS}${prevRoute}${route}`,
referrer: `https://notesnook.com/notesnook-${Platform.OS}${prevRoute}`,
hostname: `notesnook-${Platform.OS}`,
language: "en-US",
screen: "1920x1080"
},
type: "pageview"
};
try {
let response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA
},
body: JSON.stringify(body)
});
console.log("analytics: event sent", route);
await saveAnalytics(route);
return await response.text();
} catch (e) {
console.log(e);
}
}
async function sendEvent(type, value, once = true) {
if (__DEV__) return;
if (!(await canUpdateAnalytics(type)) && once) return;
let body = {
payload: {
website: WEBSITE_ID,
url: "/",
event_type: type,
event_value: value,
hostname: "notesnook-android-app",
language: "en-US",
screen: "1920x1080"
},
type: "event"
};
try {
let response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA
},
body: JSON.stringify(body)
});
console.log(response.status);
return await response.text();
} catch (e) {
console.log(e);
}
}
export default {
sendEvent,
pageView,
saveAnalytics
};

View File

@@ -0,0 +1,391 @@
/*
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 Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { MMKV } from "./mmkv";
// 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
// app lock with password enabled.
export const CipherStorage = new MMKVLoader()
.withInstanceID("cipher_storage")
.setProcessingMode(
Platform.OS === "ios"
? ProcessingModes.MULTI_PROCESS
: ProcessingModes.SINGLE_PROCESS
)
.disableIndexing()
.initialize();
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
const APPLOCK_CIPHER = "applockCipher";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
},
android: {}
});
function generatePassword() {
const length = 80;
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
"Crypto API is not supported. Please upgrade your web browser"
);
}
const charset =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
const indexes = crypto.getRandomValues(new Uint32Array(length));
let secret = "";
for (const index of indexes) {
secret += charset[index % charset.length];
}
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
const key = getDatabaseKey();
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = await encrypt(appLockCredentials, key);
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
databaseKey,
KEYSTORE_CONFIG
);
MMKV.removeItem(DB_KEY_CIPHER);
return true;
}
export async function setAppLockVerificationCipher(appLockPassword) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = await encrypt(appLockCredentials, generatePassword());
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
DatabaseLogger.error(e);
console.log(e);
}
}
export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword) {
try {
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const decrypted = await decrypt(
{
password: appLockPassword
},
appLockCipher
);
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
);
return typeof decrypted === "string";
} catch (e) {
DatabaseLogger.error(e);
return false;
}
}
let DB_KEY;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword) {
if (DB_KEY) return DB_KEY;
try {
if (appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
let credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
KEYSTORE_CONFIG
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = credentials.password;
}
}
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook",
KEYSTORE_CONFIG
);
if (userKeyCredentials) {
const userKeyCipher = await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_USER_KEY_SALT
},
userKeyCredentials.password
);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
return DB_KEY;
} catch (e) {
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKey(data) {
try {
let credentials = await Sodium.deriveKey(data.password, data.salt);
const userKeyCipher = await encrypt(
{
key: await getDatabaseKey(),
salt: NOTESNOOK_USER_KEY_SALT
},
credentials.key
);
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey(_name) {
try {
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return null;
}
const key = await decrypt(
{
key: await getDatabaseKey()
},
keyCipher
);
DatabaseLogger.info("User key decrypted: ", !!key);
return key;
} catch (e) {
console.log("getCryptoKey", e);
DatabaseLogger.error(e);
}
}
export async function removeCryptoKey(_name) {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
return true;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getRandomBytes(length) {
return await generateSecureRandom(length);
}
export async function hash(password, email) {
let result = await Sodium.hashPassword(password, email);
return result;
}
export async function generateCryptoKey(password, salt) {
try {
let credentials = await Sodium.deriveKey(password, salt || null);
return credentials;
} catch (e) {
DatabaseLogger.error(e);
}
}
export function getAlgorithm(base64Variant) {
return `xcha-argon2i13-${base64Variant}`;
}
export async function decrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
return await Sodium.decrypt(password, _data);
}
export async function decryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
data = data.map((d) => {
d.output = "plain";
return d;
});
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
return await Sodium.decryptMulti(password, data);
}
export function parseAlgorithm(alg) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
return {
encryptionAlgorithm: enc,
kdfAlgorithm: kdf,
compressionAlgorithm: compressionAlg,
isCompress: compressed === "1",
base64_variant: base64variant
};
}
export async function encrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let message = {
type: "plain",
data: data
};
let result = await Sodium.encrypt(password, message);
return {
...result,
alg: getAlgorithm(7)
};
}
export async function encryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let results = await Sodium.encryptMulti(
password,
data.map((item) => ({
type: "plain",
data: item
}))
);
return !results
? []
: results.map((result) => ({
...result,
alg: getAlgorithm(7)
}));
}

View File

@@ -1,473 +0,0 @@
/*
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 Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
import { SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
// 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
// app lock with password enabled.
export const CipherStorage = new MMKVLoader()
.withInstanceID("cipher_storage")
.setProcessingMode(
Platform.OS === "ios"
? ProcessingModes.MULTI_PROCESS
: ProcessingModes.SINGLE_PROCESS
)
.disableIndexing()
.initialize();
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
const APPLOCK_CIPHER = "applockCipher";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
},
android: {}
});
function generatePassword() {
const length = 80;
//@ts-ignore
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
"Crypto API is not supported. Please upgrade your web browser"
);
}
const charset =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
const indexes = crypto.getRandomValues(new Uint32Array(length));
let secret = "";
for (const index of indexes) {
secret += charset[index % charset.length];
}
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
const key = (await getDatabaseKey()) as string;
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = (await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
)) as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
databaseKey,
KEYSTORE_CONFIG
);
MMKV.removeItem(DB_KEY_CIPHER);
return true;
}
export async function setAppLockVerificationCipher(appLockPassword: string) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = (await encrypt(
appLockCredentials,
generatePassword()
)) as Cipher;
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword: string) {
try {
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
);
return typeof decrypted === "string";
} catch (e) {
DatabaseLogger.error(e);
return false;
}
}
let DB_KEY: string | undefined;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword?: string) {
if (DB_KEY) return DB_KEY;
if (appLockPassword) {
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
}
}
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
if (!DB_KEY) {
throw new Error(
`Failed to get database key, ${await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
)}`
);
}
return DB_KEY;
}
export async function deriveCryptoKeyFallback(data: SerializedKey) {
if (Platform.OS !== "ios") return;
try {
if (!data.password || !data.salt)
throw new Error(
"Invalid password and salt provided to deriveCryptoKeyFallback"
);
const credentials = await Sodium.deriveKeyFallback?.(
data.password,
data.salt
);
if (!credentials) return;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key fallback stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function deriveCryptoKey(data: SerializedKey) {
try {
if (!data.password || !data.salt)
throw new Error("Invalid password and salt provided to deriveCryptoKey");
const credentials = (await Sodium.deriveKey(
data.password,
data.salt
)) as Password;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey() {
try {
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return undefined;
}
const key = await decrypt(
{
key: (await getDatabaseKey()) as string,
salt: keyCipher.salt
},
keyCipher
);
return key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function removeCryptoKey() {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
return true;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getRandomBytes(length: number) {
return await generateSecureRandom(length);
}
export async function hash(
password: string,
email: string,
options?: { usesFallback?: boolean }
) {
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
if (options?.usesFallback && Platform.OS !== "ios") {
return "";
}
return (
options?.usesFallback
? await Sodium.hashPasswordFallback?.(password, email)
: await Sodium.hashPassword(password, email)
) as string;
}
export async function generateCryptoKey(password: string, salt?: string) {
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
}
export async function generateCryptoKeyFallback(
password: string,
salt?: string
): Promise<SerializedKey> {
return Sodium.deriveKeyFallback?.(
password,
salt as string
) as Promise<SerializedKey>;
}
export function getAlgorithm(base64Variant: number) {
return `xcha-argon2i13-${base64Variant}`;
}
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
const _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decrypt(key, _data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decrypt(fallbackKey, _data);
} else {
throw e;
}
}
}
return await Sodium.decrypt(password, _data);
}
export async function decryptMulti(
password: Password,
data: Cipher<"base64">[]
) {
data = data.map((d) => {
d.output = "plain";
return d;
});
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decryptMulti(key, data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt as string
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decryptMulti(fallbackKey, data);
} else {
throw e;
}
}
}
return await Sodium.decryptMulti(password, data);
}
export function parseAlgorithm(alg: string) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
return {
encryptionAlgorithm: enc,
kdfAlgorithm: kdf,
compressionAlgorithm: compressionAlg,
isCompress: compressed === "1",
base64_variant: base64variant
};
}
export async function encrypt(password: SerializedKey, plainText: string) {
const result = await Sodium.encrypt<"base64">(password, {
type: "plain",
data: plainText
});
return {
...result,
alg: getAlgorithm(7)
};
}
export async function encryptMulti(
password: SerializedKey,
plainText: string[]
) {
const results = await Sodium.encryptMulti<"base64">(
password,
plainText.map((item) => ({
type: "plain",
data: item
}))
);
return !results
? []
: results.map((result) => ({
...result,
alg: getAlgorithm(7)
}));
}

View File

@@ -16,50 +16,57 @@ 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 "./logger";
import { database } from "@notesnook/common";
import { logger as dbLogger, ICompressor } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import { logger as dbLogger } from "@notesnook/core/dist/logger";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import SettingsService from "../../services/settings";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { FileStorage } from "../filesystem";
import { getDatabaseKey } from "./encryption";
import "./logger";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { Storage } from "./storage";
import { getDatabaseKey } from "./encryption";
export async function setupDatabase(password?: string) {
database.host(
__DEV__
? {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
// API_HOST: "http://192.168.43.5:5264",
// AUTH_HOST: "http://192.168.43.5:8264",
// SSE_HOST: "http://192.168.43.5:7264",
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
// ISSUES_HOST: "http://192.168.43.5:2624"
}
: {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
}
);
export async function setupDatabase(password) {
const key = await getDatabaseKey(password);
if (!key) throw new Error(strings.databaseSetupFailed());
if (!key)
throw new Error("Database setup failed, could not get database key");
database.host({
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
MONOGRAPH_HOST: "https://monogr.ph",
...(SettingsService.getProperty("serverUrls") || {})
});
console.log("Opening database with key:", !!key);
database.setup({
storage: Storage,
eventsource: (Platform.OS === "ios"
? EventSource
: AndroidEventSource) as any,
fs: FileStorage,
compressor: async () =>
({
compress: Gzip.deflate,
decompress: Gzip.inflate
} as ICompressor),
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({
@@ -78,10 +85,4 @@ export async function setupDatabase(password?: string) {
}
export const db = database;
let DatabaseLogger = dbLogger.scope(Platform.OS);
const setLogger = () => {
DatabaseLogger = dbLogger.scope(Platform.OS);
};
export { DatabaseLogger, setLogger };
export const DatabaseLogger = dbLogger;

View File

@@ -16,11 +16,14 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { MMKVLoader } from "react-native-mmkv-storage";
import { initialize } from "@notesnook/core/dist/logger";
import { KV } from "./storage";
import { LoaderFunctionArgs } from "@remix-run/node";
const LoggerStorage = new MMKVLoader()
.withInstanceID("notesnook_logs")
.initialize();
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
url.pathname = url.pathname.replace(/.png/, ".jpg");
return Response.redirect(url, 308);
}
initialize(new KV(LoggerStorage));
export { LoggerStorage };

View File

@@ -1,49 +0,0 @@
/*
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 { initialize } from "@notesnook/core";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import { Platform } from "react-native";
import { setLogger } from ".";
import { RNSqliteDriver } from "./sqlite.kysely";
let loggerLoaded = false;
const initializeLogger = async () => {
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;
};
export { initializeLogger };

View File

@@ -17,16 +17,11 @@ 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 {
DatabaseConnection,
Driver,
QueryResult
} from "@streetwriters/kysely";
import { CompiledQuery } from "@streetwriters/kysely";
import type { DatabaseConnection, Driver, QueryResult } from "kysely";
import { CompiledQuery } from "kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
import { strings } from "@notesnook/intl";
type Config = { dbName: string; async: boolean; location?: string };
type Config = { dbName: string; async: boolean; location: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
@@ -98,7 +93,7 @@ class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error(strings.streamingNotSupported());
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(

View File

@@ -16,52 +16,58 @@ 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 { IStorage } from "@notesnook/core";
import { MMKVInstance } from "react-native-mmkv-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import {
decrypt,
decryptMulti,
deriveCryptoKey,
deriveCryptoKeyFallback,
encrypt,
encryptMulti,
generateCryptoKey,
getCryptoKey,
getRandomBytes,
hash,
generateCryptoKeyFallback
removeCryptoKey,
decryptMulti,
encryptMulti
} from "./encryption";
import { MMKV } from "./mmkv";
export class KV {
storage: MMKVInstance;
constructor(storage: MMKVInstance) {
/**
* @type {typeof MMKV}
*/
storage = null;
constructor(storage) {
this.storage = storage;
}
async read<T>(key: string, isArray?: boolean) {
if (!key) return undefined;
const data = this.storage.getString(key);
if (!data) return undefined;
async read(key) {
if (!key) return null;
let data = this.storage.getString(key);
if (!data) return null;
try {
return JSON.parse(data) as T;
let parse = JSON.parse(data);
return parse;
} catch (e) {
return data as T;
return data;
}
}
async write<T>(key: string, data: T) {
async write(key, data) {
this.storage.setString(
key,
typeof data === "string" ? data : JSON.stringify(data)
);
return true;
}
async readMulti<T>(keys: string[]) {
async readMulti(keys) {
if (keys.length <= 0) {
return [];
} else {
try {
const data = await this.storage.getMultipleItemsAsync<any>(
let data = await this.storage.getMultipleItemsAsync(
keys.slice(),
"string"
);
@@ -73,24 +79,24 @@ export class KV {
obj = value;
}
return [key, obj];
}) as [string, T][];
});
} catch (e) {
return [];
console.log(e);
}
}
}
async remove(key: string) {
this.storage.removeItem(key);
async remove(key) {
return this.storage.removeItem(key);
}
async removeMulti(keys: string[]) {
if (!keys) return;
this.storage.removeItems(keys);
async removeMulti(keys) {
if (!keys) return true;
return this.storage.removeItems(keys);
}
async clear() {
this.storage.clearStore();
return this.storage.clearStore();
}
async getAllKeys() {
@@ -107,46 +113,54 @@ export class KV {
return keys;
}
async writeMulti(items: [string, any][]) {
await this.storage.setMultipleItemsAsync(items, "object");
async writeMulti(items) {
return this.storage.setMultipleItemsAsync(items, "object");
}
}
const DefaultStorage = new KV(MMKV);
export const Storage: IStorage = {
write<T>(key: string, data: T): Promise<void> {
return DefaultStorage.write(key, data);
},
writeMulti<T>(entries: [string, T][]): Promise<void> {
return DefaultStorage.writeMulti(entries);
},
readMulti<T>(keys: string[]): Promise<[string, T][]> {
return DefaultStorage.readMulti(keys);
},
read<T>(key: string, isArray?: boolean): Promise<T | undefined> {
return DefaultStorage.read(key, isArray);
},
remove(key: string): Promise<void> {
return DefaultStorage.remove(key);
},
removeMulti(keys: string[]): Promise<void> {
return DefaultStorage.removeMulti(keys);
},
clear(): Promise<void> {
return DefaultStorage.clear();
},
getAllKeys(): Promise<string[]> {
return DefaultStorage.getAllKeys();
},
hash,
getCryptoKey,
async function requestPermission() {
if (Platform.OS === "ios") return true;
return true;
}
async function checkAndCreateDir(path) {
let dir =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.DocumentDir + path
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
try {
let exists = await RNFetchBlob.fs.exists(dir);
let isDir = await RNFetchBlob.fs.isDir(dir);
if (!exists || !isDir) {
await RNFetchBlob.fs.mkdir(dir);
}
} catch (e) {
await RNFetchBlob.fs.mkdir(dir);
}
return dir;
}
export default {
read: (key) => DefaultStorage.read(key),
write: (key, value) => DefaultStorage.write(key, value),
readMulti: (keys) => DefaultStorage.readMulti(keys),
remove: (key) => DefaultStorage.remove(key),
clear: () => DefaultStorage.clear(),
getAllKeys: () => DefaultStorage.getAllKeys(),
writeMulti: (items) => DefaultStorage.writeMulti(items),
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
encrypt,
encryptMulti,
decrypt,
decryptMulti,
getRandomBytes,
checkAndCreateDir,
requestPermission,
deriveCryptoKey,
getCryptoKey,
removeCryptoKey,
hash,
generateCryptoKey,
generateCryptoKeyFallback,
deriveCryptoKeyFallback
encryptMulti
};

View File

@@ -20,6 +20,7 @@ import { Dimensions, Image, Platform } from "react-native";
import ImageResizer from "@bam.tech/react-native-image-resizer";
import RNFetchBlob from "react-native-blob-util";
console.log("hello world");
/**
* Scale down & compress images to screen width
* for loading in editor.
@@ -27,9 +28,13 @@ import RNFetchBlob from "react-native-blob-util";
*/
export async function compressToBase64(path, type) {
const { width: screenWidth, scale } = Dimensions.get("window");
console.log("COMPRESSING TO BASE64...");
return new Promise((resolve) => {
console.log(path, "image path...");
Image.getSize(path, async (width) => {
console.log("image width", width);
const response = await ImageResizer.createResizedImage(
path,
screenWidth * scale,
@@ -51,12 +56,10 @@ export async function compressToBase64(path, type) {
: response.uri,
"base64"
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
RNFetchBlob.fs
.unlink(response.uri.replace("file://", ""))
.catch(console.log);
resolve(base64);
});
@@ -78,8 +81,6 @@ export async function compressToFile(path, type) {
onlyScaleDown: true
}
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
return response.uri;
}

View File

@@ -0,0 +1,309 @@
/*
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 Sodium from "@ammarahmed/react-native-sodium";
import {
getFileNameWithExtension,
isImage,
isDocument
} from "@notesnook/core/dist/utils/filename";
import React from "react";
import { Platform } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import Storage from "../database/storage";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
import { createCacheDir, exists } from "./io";
import { IOS_APPGROUPID } from "../../utils/constants";
export const FileDownloadStatus = {
Success: 1,
Fail: 0
};
/**
* Download all user's attachments
* @returns
*/
export async function downloadAllAttachments() {
const attachments = await db.attachments.all.ids();
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachments
* @param onProgress
* @returns
*/
export async function downloadAttachments(
attachments,
onProgress,
canceled,
groupId
) {
await createCacheDir();
if (!attachments || !attachments.length) return;
const result = new Map();
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
let file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await Storage.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
const zipOutputFile =
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
await RNFetchBlob.fs.unlink(zipSourceFolder);
}
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = await db.attachments.attachment(attachments[i]);
const hash = attachment.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
onProgress?.(
i + 1 / attachments.length,
`Downloading attachments (${i + 1}/${
attachments.length
})... Please wait`
);
// Download to cache
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Success,
attachment: attachment
});
} catch (e) {
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Fail,
reason: e
});
ToastManager.error(e, "Error downloading attachment");
}
}
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (result?.size) {
let sub;
try {
onProgress?.(0, `Zipping... Please wait`);
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
onProgress(
progress,
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
);
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
onProgress(1, `Saving zip file... Please wait`);
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
onProgress?.(1, `Done`);
releasePermissions(outputFolder);
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
}
}
return result;
}
export default async function downloadAttachment(
hash,
global = true,
options = {
silent: false,
cache: false,
throwError: false,
groupId: undefined,
base64: false,
text: false
}
) {
await createCacheDir();
let attachment = await db.attachments.attachment(hash);
if (!attachment) {
DatabaseLogger.log("Attachment not found");
return;
}
let folder = {};
if (!options.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder.uri = await Storage.checkAndCreateDir("/downloads/");
}
}
try {
await db
.fs()
.downloadFile(options.groupId || attachment.hash, attachment.hash);
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
return;
}
if (options.base64 || options.text) {
console.log(
"starting decrypt base64 file...",
options.base64,
options.text,
attachment.hash
);
return await db.attachments.read(
attachment.hash,
options.base64 ? "base64" : "text"
);
}
let filename = getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
let key = await db.attachments.decryptKey(attachment.key);
let info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
alg: attachment.alg,
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
};
let fileUri = await Sodium.decryptFile(
key,
info,
options.cache ? "cache" : "file"
);
if (!options.silent) {
ToastManager.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"
});
}
if (
attachment.dateUploaded &&
!isImage(attachment.mimeType) &&
!isDocument(attachment.mimeType)
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios" && !options.cache) {
fileUri = folder.uri + `/${filename}`;
}
if (!options.silent) {
presentSheet({
title: "File downloaded",
paragraph: `${filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
return fileUri;
} catch (e) {
console.log("download attachment error: ", e);
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(console.log);
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
.catch(console.log);
}
useAttachmentStore.getState().remove(attachment.hash);
if (options.throwError) {
throw e;
}
}
}

View File

@@ -1,353 +0,0 @@
/*
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 Sodium from "@ammarahmed/react-native-sodium";
import { getFileNameWithExtension } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import React from "react";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import filesystem from ".";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { createCacheDir, exists } from "./io";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
export const FileDownloadStatus = {
Success: 1,
Fail: 0
};
/**
* Download all user's attachments
* @returns
*/
export async function downloadAllAttachments() {
const attachments = await db.attachments.all.ids();
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachmentIds
* @param onProgress
* @returns
*/
export async function downloadAttachments(attachmentIds: string[]) {
await createCacheDir();
if (!attachmentIds || !attachmentIds.length) return;
const groupId = `download-all-${Date.now()}`;
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
const file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
const zipOutputFile =
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
await RNFetchBlob.fs.unlink(zipSourceFolder);
}
await RNFetchBlob.fs.mkdir(zipSourceFolder);
const isCancelled = () => {
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
useAttachmentStore.getState().setDownloading({
groupId,
current: 0,
total: 0,
success: false,
message: strings.network.dowloadCancelled(),
canceled: true
});
return true;
}
};
for (let i = 0; i < attachmentIds.length; i++) {
if (isCancelled()) return;
const attachment = await db.attachments.attachment(attachmentIds[i]);
if (!attachment) continue;
const hash = attachment.hash;
try {
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: i + 1,
total: attachmentIds.length,
filename: attachment.hash
});
// Download to cache
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (isCancelled()) return;
if (!uri) throw new Error(strings.failedToDownloadFile());
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
} catch (e) {
DatabaseLogger.error(e);
}
}
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: 0,
total: 0,
success: true
});
if (isCancelled()) return;
let sub;
try {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 1,
message: strings.savingZipFile(),
groupId
});
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
useAttachmentStore.getState().setDownloading({
groupId,
current: progress,
total: 1,
message: strings.savingZipFile((progress * 100).toFixed(1))
});
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
useAttachmentStore.getState().setDownloading({
current: 0,
total: 0,
message: undefined,
success: true,
groupId
});
releasePermissions(outputFolder);
} catch (e) {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 0,
message: undefined,
success: false,
groupId
});
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e as Error, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
/* empty */
});
}
}
export default async function downloadAttachment(
hashOrId: string,
global = true,
options?: {
silent?: boolean;
cache?: boolean;
throwError?: boolean;
groupId?: string;
base64?: boolean;
text?: boolean;
}
) {
await createCacheDir();
const attachment = await db.attachments.attachment(hashOrId);
if (!attachment) {
DatabaseLogger.log("Attachment not found", {
hash: hashOrId
});
return;
}
let folder: {
uri: string;
} | null = null;
if (!options?.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder = {
uri: await filesystem.checkAndCreateDir("/downloads/")
};
}
}
try {
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 0,
total: 1,
filename: attachment.filename
});
await db
.fs()
.downloadFile(
options?.groupId || attachment.hash,
attachment.hash,
attachment.chunkSize
);
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 1,
total: 1,
filename: attachment.filename,
success: true
});
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
return;
}
if (options?.base64 || options?.text) {
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
options.base64 ? "base64" : "text"
);
}
const filename = await getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
const key = await db.attachments.decryptKey(attachment.key);
if (!key) return;
const info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
alg: attachment.alg,
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
fileName: options?.cache ? undefined : filename,
uri: options?.cache ? undefined : folder?.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
};
let fileUri = await Sodium.decryptFile(
key,
info,
options?.cache ? "cache" : "file"
);
if (!options?.silent) {
ToastManager.show({
heading: strings.network.downloadSuccess(),
message: strings.network.fileDownloaded(filename),
type: "success"
});
}
if (Platform.OS === "ios" && !options?.cache) {
fileUri = folder?.uri + `/${filename}`;
}
if (!options?.silent) {
presentSheet({
title: strings.network.fileDownloaded(),
paragraph: strings.fileSaved(filename, Platform.OS),
icon: "download",
context: global ? "global" : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
return fileUri;
} catch (e) {
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(() => {
/* empty */
});
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
.catch(() => {
/* empty */
});
}
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 0,
total: 0,
filename: attachment.filename,
success: false
});
DatabaseLogger.error(e);
useAttachmentStore.getState().remove(attachment.hash);
if (options?.throwError) {
throw e;
}
}
}

View File

@@ -0,0 +1,133 @@
/*
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 hosts from "@notesnook/core/dist/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { cacheDir, fileCheck } from "./utils";
import { createCacheDir, exists } from "./io";
export async function downloadFile(filename, data, cancelToken) {
if (!data) return false;
console.log("Downloading", filename);
await createCacheDir();
let { url, headers } = data;
let path = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
console.log("Exists already", filename);
return true;
}
let res = await fetch(url, {
method: "GET",
headers
});
if (!res.ok)
throw new Error(`${res.status}: Unable to resolve download url`);
const downloadUrl = await res.text();
if (!downloadUrl) throw new Error("Unable to resolve download url");
let totalSize = 0;
console.log("Download starting");
let request = RNFetchBlob.config({
path: path,
IOSBackgroundTask: true
})
.fetch("GET", downloadUrl, null)
.progress((recieved, total) => {
useAttachmentStore
.getState()
.setProgress(0, total, filename, recieved, "download");
totalSize = total;
console.log("downloading: ", recieved, total);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
};
let response = await request;
await fileCheck(response, totalSize);
let status = response.info().status;
useAttachmentStore.getState().remove(filename);
return status >= 200 && status < 300;
} catch (e) {
if (e.message !== "canceled") {
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
});
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "local"
});
}
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("Download file error:", e, url, headers);
return false;
}
}
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
return 0;
}
}
export async function checkAttachment(hash) {
const internetState = await NetInfo.fetch();
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
if (!isInternetReachable) return { success: true };
const attachment = await db.attachments.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {
const size = await getUploadedFileSize(hash);
if (size <= 0) return { failed: "File length is 0." };
} catch (e) {
return { failed: e?.message };
}
return { success: true };
}

View File

@@ -1,212 +0,0 @@
/*
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 { RequestOptions } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { createCacheDir, exists } from "./io";
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
export async function downloadFile(
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) {
if (!requestOptions) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: No requestOptions`
);
return false;
}
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
const { url, headers, chunkSize } = requestOptions;
const tempFilePath = `${cacheDir}/${filename}_temp`;
const originalFilePath = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
DatabaseLogger.log(`File Exists already: ${filename}`);
return true;
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
const size = await getUploadedFileSize(filename);
if (size === -1) {
const error = `${strings.fileVerificationFailed()} (File hash: ${filename})`;
throw new Error(error);
}
if (size === 0) {
const error = `${strings.fileLengthError()} (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
if (attachment && attachment.size !== decryptedLength) {
const error = `${strings.fileLengthMismatch(
attachment.size,
decryptedLength
)} (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
const resolveUrlResponse = await fetch(url, {
method: "GET",
headers
});
if (!resolveUrlResponse.ok) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
);
throw new Error(
`${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
);
}
const downloadUrl = await resolveUrlResponse.text();
if (!downloadUrl) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: Unable to resolve download url`
);
throw new Error(strings.failedToResolvedDownloadUrl());
}
DatabaseLogger.log(`Download starting: ${filename}`);
const request = RNFetchBlob.config({
path: tempFilePath,
IOSBackgroundTask: true,
overwrite: true
})
.fetch("GET", downloadUrl)
.progress(async (recieved, total) => {
useAttachmentStore
.getState()
.setProgress(0, total, filename, recieved, "download");
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});
cancelToken.cancel = async (reason) => {
useAttachmentStore.getState().remove(filename);
request.cancel();
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
/* empty */
});
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
};
const response = await request;
const contentType =
response.info().headers?.["content-type"] ||
response.info().headers?.["Content-Type"];
if (contentType === "application/xml") {
const error = parseS3Error(await response.text());
throw new Error(`[${error.Code}] ${error.Message}`);
}
const status = response.info().status;
useAttachmentStore.getState().remove(filename);
if (await exists(originalFilePath)) {
await RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
/* empty */
});
}
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath).catch(() => {
/* empty */
});
if (!(await exists(filename))) {
throw new Error("File size mismatch");
}
return status >= 200 && status < 300;
} catch (e) {
if (
(e as Error).message !== "canceled" &&
!(e as Error).message.includes("NoSuchKey")
) {
const toast = {
heading: strings.downloadError((e as Error).message),
message: (e as Error).message,
type: "error" as const,
context: "global"
};
ToastManager.show(toast);
toast.context = "local";
ToastManager.show(toast);
}
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
/* empty */
});
DatabaseLogger.error(e, "Download failed: ", {
url
});
return false;
}
}
export async function checkAttachment(hash: string) {
const internetState = await NetInfo.fetch();
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
if (!isInternetReachable) return;
const attachment = await db.attachments.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {
const size = await getUploadedFileSize(hash);
if (size === -1) return { success: true };
if (size === 0)
return {
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
};
} catch (e) {
return { failed: (e as Error)?.message };
}
return { success: true };
}

View File

@@ -17,41 +17,19 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { IFileStorage } from "@notesnook/core";
import { checkAttachment, downloadFile } from "./download";
import { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
import {
bulkExists,
clearCache,
clearFileStorage,
deleteCacheFileByName,
deleteCacheFileByPath,
deleteFile,
exists,
getCacheSize,
hashBase64,
readEncrypted,
writeEncryptedBase64
writeEncryptedBase64,
hashBase64
} from "./io";
import { uploadFile } from "./upload";
import {
cancelable,
checkAndCreateDir,
getUploadedFileSize,
requestPermission
} from "./utils";
import { cancelable } from "./utils";
export default {
checkAttachment,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
getCacheSize,
requestPermission,
checkAndCreateDir,
getUploadedFileSize
};
export const FileStorage: IFileStorage = {
readEncrypted,
writeEncryptedBase64,
hashBase64,
@@ -61,5 +39,5 @@ export const FileStorage: IFileStorage = {
exists,
clearFileStorage,
getUploadedFileSize,
bulkExists
checkAttachment
};

View File

@@ -0,0 +1,198 @@
/*
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 Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { IOS_APPGROUPID } from "../../utils/constants";
import { db } from "../database";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
console.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
return false;
}
const attachment = await db.attachments.attachment(filename);
console.log("decrypting....");
let output = await Sodium.decryptFile(
key,
{
...cipherData,
hash: filename,
appGroupId: IOS_APPGROUPID
},
cipherData.outputType === "base64" ? "base64" : "text"
);
console.log("file decrypted...", attachment?.mimeType);
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("readEncrypted", e);
return false;
}
}
export async function hashBase64(data) {
const hash = await Sodium.hashFile({
type: "base64",
data,
uri: ""
});
return {
hash: hash,
type: "xxh64"
};
}
export async function writeEncryptedBase64({ data, key }) {
await createCacheDir();
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
let output = await Sodium.encryptFile(key, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
return {
...output,
alg: "xcha-stream"
};
}
export async function deleteFile(filename, data) {
await createCacheDir();
let delFilePath = cacheDir + `/${filename}`;
if (!data) {
if (!filename) return;
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
return true;
}
let { url, headers } = data;
try {
let response = await RNFetchBlob.fetch("DELETE", url, headers);
let status = response.info().status;
let ok = status >= 200 && status < 300;
if (ok) {
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
}
return ok;
} catch (e) {
console.log("delete file: ", e, url, headers);
return false;
}
}
export async function clearFileStorage() {
try {
let files = await RNFetchBlob.fs.ls(cacheDir);
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
}
for (let file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
}
} catch (e) {
console.log("clearFileStorage", e);
}
}
export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
console.log("Cache directory created");
}
}
export async function migrateFilesFromCache() {
try {
await createCacheDir();
const migratedFilesPath = cacheDir + "/.migrated_1";
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
if (migrated) {
console.log("Files migrated already");
return;
}
let files = await RNFetchBlob.fs.ls(cacheDir);
console.log("Files to migrate:", files.join(","));
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
.catch(console.log);
console.log("Moved", file);
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
console.log("migrateFilesFromCache", e);
}
}
const ABYTES = 17;
export async function exists(filename) {
let path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
// Check if file is present in app group path.
let existsInAppGroup = false;
if (!exists && Platform.OS === "ios") {
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
}
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
const stat = await RNFetchBlob.fs.stat(
existsInAppGroup ? appGroupPath : path
);
if (stat.size !== expectedFileSize) {
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(console.log);
return false;
}
exists = true;
}
return exists;
}

View File

@@ -1,331 +0,0 @@
/*
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 Sodium from "@ammarahmed/react-native-sodium";
import {
FileEncryptionMetadataWithHash,
FileEncryptionMetadataWithOutputType,
Output,
RequestOptions
} from "@notesnook/core";
import { DataFormat, SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { eSendEvent } from "../../services/event-manager";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
export async function readEncrypted<TOutputFormat extends DataFormat>(
filename: string,
key: SerializedKey,
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
) {
await migrateFilesFromCache();
DatabaseLogger.log("Read encrypted file...");
const path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
return;
}
const output = await Sodium.decryptFile(
key,
{
...cipherData,
hash: filename,
appGroupId: IOS_APPGROUPID
},
cipherData.outputType === "base64" ? "base64" : "text"
);
DatabaseLogger.log("File decrypted...");
return output as Output<TOutputFormat>;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(() => {
/* empty */
});
DatabaseLogger.error(e);
}
}
export async function hashBase64(data: string) {
const hash = await Sodium.hashFile({
type: "base64",
data,
uri: ""
});
return {
hash: hash,
type: "xxh64"
};
}
export async function writeEncryptedBase64(
data: string,
encryptionKey: SerializedKey,
mimeType: string
): Promise<FileEncryptionMetadataWithHash> {
await createCacheDir();
const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
const output = await Sodium.encryptFile(encryptionKey, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
RNFetchBlob.fs.unlink(filepath).catch(() => {
/* empty */
});
return {
...output,
alg: "xcha-stream"
};
}
export async function deleteFile(
filename: string,
requestOptions?: RequestOptions
): Promise<boolean> {
await createCacheDir();
const localFilePath = cacheDir + `/${filename}`;
if (!requestOptions) {
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
/* empty */
});
return true;
}
const { url, headers } = requestOptions;
try {
const response = await RNFetchBlob.fetch("DELETE", url, headers);
const status = response.info().status;
const ok = status >= 200 && status < 300;
if (ok) {
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
/* empty */
});
}
return ok;
} catch (e) {
DatabaseLogger.error(e, "Delete file", {
url: url
});
return false;
}
}
export async function clearFileStorage() {
try {
const files = await RNFetchBlob.fs.ls(cacheDir);
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (const file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(() => {
/* empty */
});
}
for (const file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(() => {
/* empty */
});
}
} catch (e) {
DatabaseLogger.error(e, "clearFileStorage");
}
}
export async function createCacheDir() {
try {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
}
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function migrateFilesFromCache() {
try {
await createCacheDir();
const migratedFilesPath = cacheDir + "/.migrated_1";
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
if (migrated) {
return;
}
const files = await RNFetchBlob.fs.ls(cacheDir);
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (const file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
.catch(() => {
/* empty */
});
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
DatabaseLogger.error(e, "migrateFilesFromCache");
}
}
export async function clearCache() {
await RNFetchBlob.fs.unlink(cacheDir).catch(() => {
/* empty */
});
await createCacheDir();
eSendEvent("cache-cleared");
}
export async function deleteCacheFileByPath(path: string) {
await RNFetchBlob.fs.unlink(path).catch(() => {
/* empty */
});
}
export async function deleteCacheFileByName(name: string) {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${name}`;
await RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
/* empty */
});
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(() => {
/* empty */
});
}
export async function deleteDCacheFiles() {
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (file.includes("_dcache") || file.startsWith("NN_")) {
await RNFetchBlob.fs.unlink(file).catch(() => {
/* empty */
});
}
}
}
export async function getCachePathForFile(filename: string) {
const path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
const exists = await RNFetchBlob.fs.exists(path);
// Check if file is present in app group path.
let existsInAppGroup = false;
if (!exists && Platform.OS === "ios") {
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
}
return existsInAppGroup ? appGroupPath : path;
}
export async function exists(filename: string) {
const path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
// Check if file is present in app group path.
let existsInAppGroup = false;
if (!exists && Platform.OS === "ios") {
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
}
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
const stat = await RNFetchBlob.fs.stat(
existsInAppGroup ? appGroupPath : path
);
if (stat.size !== expectedFileSize) {
DatabaseLogger.log(
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
);
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(() => {
/* empty */
});
return false;
}
exists = true;
}
return exists;
}
export async function bulkExists(files: string[]) {
try {
await createCacheDir();
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
if (Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
missingFiles = missingFiles.filter(
(file) => !appGroupFiles.includes(file)
);
}
return missingFiles;
} catch (e) {
DatabaseLogger.error(e);
return [];
}
}
export async function getCacheSize() {
await createCacheDir();
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
let total = 0;
stat.forEach((file) => {
total += parseInt(file.size as unknown as string);
});
return total;
}

View File

@@ -0,0 +1,111 @@
/*
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 RNFetchBlob from "react-native-blob-util";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { cacheDir } from "./utils";
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
import { createCacheDir } from "./io";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
let { url, headers } = data;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
let res = await fetch(url, {
method: "PUT",
headers
});
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
const uploadUrl = await res.text();
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
let uploadFilePath = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
}
DatabaseLogger.info(`Starting upload: ${filename}`);
let request = RNFetchBlob.config({
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
"PUT",
uploadUrl,
{
"content-type": ""
},
RNFetchBlob.wrap(uploadFilePath)
)
.uploadProgress((sent, total) => {
useAttachmentStore
.getState()
.setProgress(sent, total, filename, 0, "upload");
DatabaseLogger.info(
`File upload progress: ${filename}, ${sent}/${total}`
);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
};
let response = await request;
let status = response.info().status;
let text = await response.text();
let result = status >= 200 && status < 300 && text.length === 0;
useAttachmentStore.getState().remove(filename);
if (result) {
DatabaseLogger.info(
`File upload status: ${filename}, ${status}, ${text}`
);
let attachment = await db.attachments.attachment(filename);
if (!attachment) return result;
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
} else {
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
throw new Error(
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(response.info())}`
);
}
return result;
} catch (e) {
useAttachmentStore.getState().remove(filename);
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
DatabaseLogger.error(e);
return false;
}
}

View File

@@ -1,147 +0,0 @@
/*
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 { RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { createCacheDir } from "./io";
import {
cacheDir,
checkUpload,
FileSizeResult,
getUploadedFileSize
} from "./utils";
export async function uploadFile(
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) {
if (!requestOptions) return false;
const { url, headers } = requestOptions;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
let filePath = `${cacheDir}/${filename}`;
let exists = await RNFetchBlob.fs.exists(filePath);
// Check for file in appGroupPath if it doesn't exist in cacheDir
if (!exists && Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
filePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(filePath);
}
if (!exists) {
throw new Error(
`Trying to upload file at path ${filePath} that doest not exist.`
);
}
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
const remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === FileSizeResult.Error) return false;
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
const uploadUrlResponse = await fetch(url, {
method: "PUT",
headers
});
const uploadUrl = uploadUrlResponse.ok
? await uploadUrlResponse.text()
: await uploadUrlResponse.json();
if (typeof uploadUrl !== "string") {
throw new Error(
uploadUrl.error || "Unable to resolve attachment upload url."
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
const uploadRequest = RNFetchBlob.config({
//@ts-ignore
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
"PUT",
uploadUrl,
{
"content-type": ""
},
RNFetchBlob.wrap(filePath)
)
.uploadProgress((sent, total) => {
useAttachmentStore
.getState()
.setProgress(sent, total, filename, 0, "upload");
DatabaseLogger.info(
`File upload progress: ${filename}, ${sent}/${total}`
);
});
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
uploadRequest.cancel();
};
const uploadResponse = await uploadRequest;
const status = uploadResponse.info().status;
const uploaded = status >= 200 && status < 300;
useAttachmentStore.getState().remove(filename);
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(uploadResponse.info())}`
);
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
return uploaded;
} catch (e) {
useAttachmentStore.getState().remove(filename);
ToastManager.error(e as Error, "File upload failed");
DatabaseLogger.error(e, "File upload failed", {
filename
});
return false;
}
}

View File

@@ -0,0 +1,93 @@
/*
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 * as ScopedStorage from "react-native-scoped-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
export const cacheDir =
Platform.OS == "ios"
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
export function getRandomId(prefix) {
return Math.random()
.toString(36)
.replace("0.", prefix || "");
}
export function extractValueFromXmlTag(code, xml) {
if (!xml.includes(code)) return `Unknown ${code}`;
return xml.slice(
xml.indexOf(`<${code}>`) + code.length + 2,
xml.indexOf(`</${code}>`)
);
}
export async function fileCheck(response, totalSize) {
if (totalSize < 1000) {
let text = await response.text();
if (text.startsWith("<?xml")) {
let errorJson = {
Code: extractValueFromXmlTag("Code", text),
Message: extractValueFromXmlTag("Message", text)
};
throw new Error(`${errorJson.Code}: ${errorJson.Message}`);
}
}
}
export function cancelable(operation) {
const cancelToken = {
cancel: () => {}
};
return (filename, { url, headers }) => {
return {
execute: () => operation(filename, { url, headers }, cancelToken),
cancel: async () => {
await cancelToken.cancel();
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
};
};
}
export function copyFileAsync(source, dest) {
return new Promise((resolve, reject) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
if (e) {
reject(e);
return;
}
resolve();
});
});
}
export async function releasePermissions(path) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (let uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}

View File

@@ -1,191 +0,0 @@
/*
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 { hosts, RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { DatabaseLogger, db } from "../database";
export const ABYTES = 17;
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
export const cacheDir =
Platform.OS == "ios"
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
export function getRandomId(prefix: string) {
return Math.random()
.toString(36)
.replace("0.", prefix || "");
}
export function parseS3Error(data?: string) {
const xml = typeof data === "string" ? data : null;
const error = {
Code: "UNKNOWN",
Message: xml
};
try {
if (!xml) return error;
const doc = new DOMParser().parseFromString(xml, "text/xml");
const ErrorElement = doc.getElementsByTagName("Error")[0];
if (!ErrorElement) return error;
for (const child of ErrorElement.children) {
if (
child.textContent &&
(child.tagName === "Code" || child.tagName === "Message")
)
error[child.tagName] = child.textContent;
}
return error;
} catch (e) {
return error;
}
}
export function cancelable(
operation: (
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) => Promise<boolean>
) {
const cancelToken = {
cancel: async (reason?: string) => {}
};
return (filename: string, requestOptions: RequestOptions) => {
return {
execute: () => operation(filename, requestOptions, cancelToken),
cancel: async () => {
await cancelToken.cancel();
}
};
};
}
export function copyFileAsync(source: string, dest: string) {
return new Promise((resolve, reject) => {
//@ts-ignore
ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
if (e) {
reject(e);
return;
}
resolve(true);
});
});
}
export 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);
}
}
}
export const FileSizeResult = {
Empty: 0,
Error: -1
};
export async function getUploadedFileSize(hash: string, retry = 0) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
if (
!attachmentInfo.ok ||
attachmentInfo.headers?.get("content-length") === null
) {
if (retry < 3) {
DatabaseLogger.log(`Retrying file size check: ${hash}, ${retry}`);
return getUploadedFileSize(hash, retry + 1);
}
throw new Error(
`File size check failed: ${hash}, ${
attachmentInfo.status
}, ${attachmentInfo.headers?.get("content-length")}`
);
}
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length") as string
);
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
} catch (e) {
DatabaseLogger.error(e);
return FileSizeResult.Error;
}
}
export async function checkUpload(
filename: string,
chunkSize: number,
expectedSize: number
) {
const size = await getUploadedFileSize(filename);
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
const error =
size === 0
? `File size is 0.`
: size === -1
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
if (error) throw new Error(error);
}
export async function requestPermission() {
if (Platform.OS === "ios") return true;
return true;
}
export async function checkAndCreateDir(path: string) {
const dir =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.DocumentDir + path
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
try {
const exists = await RNFetchBlob.fs.exists(dir);
const isDir = await RNFetchBlob.fs.isDir(dir);
if (!exists || !isDir) {
await RNFetchBlob.fs.mkdir(dir);
}
} catch (e) {
await RNFetchBlob.fs.mkdir(dir);
}
return dir;
}

View File

@@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function info(context: string, ...logs: unknown[]) {}
function info(context: string, ...logs: unknown[]) {
console.log(`${new Date().toLocaleDateString()}::info::${context}:`, ...logs);
}
function error(context: string, ...logs: unknown[]) {
console.log(

View File

@@ -24,7 +24,6 @@ import { useMessageStore } from "../../stores/use-message-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { allowedOnPlatform, renderItem } from "./functions";
import { getContainerBorder } from "../../utils/colors";
import { DefaultAppStyles } from "../../utils/styles";
export const Announcement = ({ color }) => {
const { colors } = useThemeColors();
@@ -37,8 +36,9 @@ export const Announcement = ({ color }) => {
style={{
backgroundColor: colors.primary.background,
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
paddingHorizontal: 12,
paddingTop: 12,
paddingBottom: 12
}}
>
<View
@@ -54,7 +54,7 @@ export const Announcement = ({ color }) => {
<View
style={{
width: "100%",
marginTop: DefaultAppStyles.GAP_VERTICAL
marginTop: 12
}}
>
{announcement?.body

View File

@@ -20,13 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import Paragraph from "../ui/typography/paragraph";
import { getStyle } from "./functions";
import { DefaultAppStyles } from "../../utils/styles";
export const Body = ({ text, style = {} }) => {
return (
<Paragraph
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingHorizontal: 12,
...getStyle(style)
}}
>

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