Compare commits

...

602 Commits

Author SHA1 Message Date
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
884 changed files with 283441 additions and 108305 deletions

View File

@@ -15,6 +15,8 @@ apps/mobile/e2e/
# web
apps/web/public/an.js
apps/web/src/common/sqlite/wa-sqlite-async.js
apps/web/src/common/sqlite/wa-sqlite.js
# editor
packages/editor/styles/

View File

@@ -78,7 +78,13 @@ module.exports = {
semi: ["error", "always"],
"@typescript-eslint/no-empty-function": "off",
"react/prop-types": "off",
"header/header": ["error", "block", LICENSE, 1]
"header/header": ["error", "block", LICENSE, 1],
"@typescript-eslint/no-empty-interface": [
"error",
{
allowSingleExtends: true
}
]
},
settings: {
react: {

View File

@@ -5,9 +5,9 @@ runs:
using: "composite"
steps:
- name: Setup Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 16.x
node-version: 20.x
cache: "npm"
cache-dependency-path: |
package-lock.json

View File

@@ -6,6 +6,11 @@ jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
CMAKE_CXX_COMPILER_LAUNCHER: ccache
NDK_CCACHE: ccache
USE_CCACHE: 1
steps:
- name: Checkout
@@ -24,30 +29,126 @@ jobs:
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Make Gradlew Executable
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Install CCache
uses: hendrikmuhs/ccache-action@v1.2.11
with:
key: ${{ runner.os }}-ccache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
max-size: 1500M
restore-keys: |
${{ runner.os }}-ccache-
- name: Setup CCache
run: |
ln -s $(which ccache) /usr/local/bin/gcc
ln -s $(which ccache) /usr/local/bin/g++
ln -s $(which ccache) /usr/local/bin/cc
ln -s $(which ccache) /usr/local/bin/c++
ln -s $(which ccache) /usr/local/bin/clang
ln -s $(which ccache) /usr/local/bin/clang++
ccache --set-config=base_dir=$PWD
ccache --set-config=inode_cache=true
ccache --set-config=compiler_check=content
ccache --set-config=depend_mode=true
ccache --set-config=file_clone=true
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
ccache -p
- name: Cache gradle
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Patch CPP Projects to use CCache
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
- name: Build unsigned app bundle
run: yarn release:android:bundle
- name: CCache Stats after build
run: ccache -sv
- name: Sign app bundle for Playstore release
id: sign_app
uses: r0adkll/sign-android-release@v1
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
- name: Build apks for Github release
run: yarn release:android
- name: Sign apk files
id: sign_apk
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
alias: ${{ secrets.PUBLIC_ALIAS }}
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
- name: Rename apk files
run: |
cd apps/mobile/native/android/app/build/outputs/apk/release/
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
mv app-x86_64-release-unsigned-signed.apk notesnook-x86_64.apk
- name: Get app version
id: package-version
uses: martinbeentjes/npm-get-version-action@master
with:
path: apps/mobile
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
track: internal
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: alpha
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
name: Notesnook Android v${{ steps.package-version.outputs.current-version}} Beta
repository: streetwriters/notesnook
token: ${{ secrets.GITHUB_TOKEN }}
files: |
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
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
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}

View File

@@ -6,6 +6,11 @@ jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
CMAKE_CXX_COMPILER_LAUNCHER: ccache
NDK_CCACHE: ccache
USE_CCACHE: 1
steps:
- name: Checkout
@@ -28,9 +33,54 @@ jobs:
- name: Make Gradlew Executable
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Install CCache
uses: hendrikmuhs/ccache-action@v1.2.11
with:
key: ${{ runner.os }}-ccache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
max-size: 1500M
restore-keys: |
${{ runner.os }}-ccache-
- name: Setup CCache
run: |
ln -s $(which ccache) /usr/local/bin/gcc
ln -s $(which ccache) /usr/local/bin/g++
ln -s $(which ccache) /usr/local/bin/cc
ln -s $(which ccache) /usr/local/bin/c++
ln -s $(which ccache) /usr/local/bin/clang
ln -s $(which ccache) /usr/local/bin/clang++
ccache --set-config=base_dir=$PWD
ccache --set-config=inode_cache=true
ccache --set-config=compiler_check=content
ccache --set-config=depend_mode=true
ccache --set-config=file_clone=true
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
ccache -p
- name: Cache gradle
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Patch CPP Projects to use CCache
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
- name: Build unsigned app bundle
run: yarn release:android:bundle
- name: CCache Stats after build
run: ccache -sv
- name: Sign app bundle for Playstore release
id: sign_app
uses: r0adkll/sign-android-release@master
@@ -43,17 +93,6 @@ jobs:
env:
BUILD_TOOLS_VERSION: "33.0.0"
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Build apks for Github release
run: yarn release:android
@@ -83,6 +122,18 @@ jobs:
with:
path: apps/mobile
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
@@ -96,3 +147,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
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}

View File

@@ -58,7 +58,7 @@ jobs:
run: npx nx build:desktop @notesnook/web
- name: Archive build artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
@@ -82,12 +82,13 @@ 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: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
build-macos:
name: Build for macOS
needs: build
if: inputs.build-mac
runs-on: macos-12
runs-on: macos-14
steps:
- name: Check out Git repository
@@ -122,7 +123,7 @@ jobs:
platform: "MAC_OS"
- name: Download build
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
@@ -153,7 +154,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
run: |
npx nx run release --project @notesnook/desktop
if [ ${{ inputs.publish-github }} == true ]; then
@@ -198,7 +199,7 @@ jobs:
if: inputs.publish-snap
- name: Download build
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
@@ -258,7 +259,7 @@ jobs:
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build

View File

@@ -22,11 +22,45 @@ jobs:
- name: Build packages
run: npx nx run @notesnook/mobile:build
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.11
with:
key: ${{ runner.os }}-ccache-${{ hashFiles(format('apps/mobile/native/ios/Podfile.lock')) }}
max-size: 1500M
restore-keys: |
${{ runner.os }}-ccache-
- name: Setup ccache
run: |
ccache --set-config=base_dir=$PWD
ccache --set-config=inode_cache=true
ccache --set-config=compiler_check=content
ccache --set-config=depend_mode=true
ccache --set-config=file_clone=true
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
ln -s $(which ccache) /usr/local/bin/gcc
ln -s $(which ccache) /usr/local/bin/g++
ln -s $(which ccache) /usr/local/bin/cc
ln -s $(which ccache) /usr/local/bin/c++
ln -s $(which ccache) /usr/local/bin/clang
ln -s $(which ccache) /usr/local/bin/clang++
ccache -p
- name: Cache Pods
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: npm run prepare:ios
- name: CCache Stats Before Build
run: ccache -sv
- name: Build iOS App
uses: yukiarrr/ios-build-action@v1.11.2
uses: ammarahm-ed/ios-build-action@master
with:
bundle-identifier: org.streetwriters.notesnook
scheme: Notesnook
@@ -52,15 +86,18 @@ jobs:
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
- name: 'Upload app to TestFlight'
- name: CCache Stats After Build
run: ccache -sv
- name: "Upload app to TestFlight"
uses: apple-actions/upload-testflight-build@v1
with:
with:
app-path: Notesnook.ipa
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
api-key-id: ${{ secrets.API_KEY_ID }}
api-private-key: ${{ secrets.API_KEY }}
- name: Upload to Github
- name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v2
with:
name: Notesnook.ipa

View File

@@ -5,7 +5,7 @@ on:
push:
branches:
- master
- '!v3-beta'
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
@@ -23,7 +23,7 @@ on:
- synchronize
branches:
- master
- '!v3-beta'
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"

View File

@@ -25,16 +25,8 @@ jobs:
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: Build
run: npm run build: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-v3-app
directory: ./apps/web/build
github-token: ${{ secrets.GITHUB_TOKEN }}
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}
run: npx --yes wrangler pages deploy --project-name=notesnook-v3-beta ./apps/web/build/

2
.gitignore vendored
View File

@@ -10,4 +10,4 @@ nx-cloud.env
.nyc_output
site
node_modules.backup
.nx
.nx

View File

@@ -11,4 +11,5 @@ apps/mobile/native/
apps/web/public/an.js
# editor
packages/editor/styles/
packages/editor/styles/
/.nx/cache

View File

@@ -1,4 +1,5 @@
node_modules
build
output
dist
dist
_catalog

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.6.16",
"version": "3.0.7-beta",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
@@ -14,9 +14,11 @@
"@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.6.2",
"zod": "^3.21.4"
@@ -25,12 +27,11 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "25.9.8",
"electron": "^28.2.1",
"electron-builder": "^24.9.1",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
"esbuild": "^0.20.0",
"tree-kill": "^1.2.2",
"undici": "^5.23.0"
"undici": "^6.6.1"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
@@ -40,7 +41,7 @@
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "tsc",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"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"
},
@@ -54,8 +55,8 @@
"productName": "Notesnook",
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
"artifactName": "notesnook_${os}_${arch}.${ext}",
"npmRebuild": false,
"asar": false,
"generateUpdatesFilesForAllChannels": true,
"files": [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
@@ -64,9 +65,17 @@
"!build/banner.jpg",
"!build/*.ico",
"!build/*.png",
"!node_modules${/*}"
"!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"
],
"afterSign": "electron-builder-notarize",
"afterPack": "./scripts/removeLocales.js",
"mac": {
"bundleVersion": "240",
@@ -94,7 +103,8 @@
"entitlements": "assets/entitlements.mac.plist",
"entitlementsInherit": "assets/entitlements.mac.plist",
"gatekeeperAssess": false,
"icon": "assets/icons/app.icns"
"icon": "assets/icons/app.icns",
"notarize": true
},
"dmg": {
"contents": [
@@ -138,7 +148,7 @@
"signingHashAlgorithms": [
"sha256"
],
"sign": "./sign.js",
"sign": "./scripts/sign.js",
"icon": "assets/icons/app.ico"
},
"portable": {
@@ -209,7 +219,8 @@
{
"provider": "github",
"repo": "notesnook",
"owner": "streetwriters"
"owner": "streetwriters",
"channel": "beta"
}
]
}

View File

@@ -18,7 +18,7 @@ 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";
@@ -28,19 +28,7 @@ import { fileURLToPath } from "url";
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const sodiumNativePrebuildPath = (arch) =>
path.join(
`node_modules`,
`@notesnook`,
`crypto`,
`node_modules`,
`@notesnook`,
`sodium`,
`node_modules`,
`sodium-native`,
`prebuilds`,
`${os.platform()}-${arch}`
);
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
await fs.rm("./build/", { force: true, recursive: true });
@@ -49,6 +37,15 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec(`cd ${webAppPath} && npm run build:desktop`);
}
// temporary until there's support for prebuilt binaries for windows ARM & linux ARM
await patchBetterSQLite3();
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
@@ -62,42 +59,6 @@ if (args.variant === "mas") {
await exec(`npx tsc`);
if (existsSync(sodiumNativePrebuildPath("x64"))) {
console.log("copying sodium-native-x64");
await fs.cp(
sodiumNativePrebuildPath("x64"),
path.join("build", "prebuilds", `${process.platform}-x64`),
{
recursive: true,
force: true
}
);
}
if (existsSync(sodiumNativePrebuildPath("ia32"))) {
console.log("copying sodium-native-ia32");
await fs.cp(
sodiumNativePrebuildPath("ia32"),
path.join("build", "prebuilds", `${process.platform}-ia32`),
{
recursive: true,
force: true
}
);
}
if (existsSync(sodiumNativePrebuildPath("arm64"))) {
console.log("copying sodium-native-arm64");
await fs.cp(
sodiumNativePrebuildPath("arm64"),
path.join("build", "prebuilds", `${process.platform}-arm64`),
{
recursive: true,
force: true
}
);
}
if (args.run) {
await exec(`npx electron-builder --dir --x64`);
if (process.platform === "win32") {
@@ -109,9 +70,28 @@ if (args.run) {
}
}
async function exec(cmd) {
async function exec(cmd, cwd) {
return childProcess.execSync(cmd, {
env: process.env,
stdio: "inherit"
env: { ...process.env, NOTESNOOK_STAGING: true },
stdio: "inherit",
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

@@ -27,21 +27,15 @@ import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const sodiumNativePrebuildPath = path.join(
`node_modules`,
`@notesnook`,
`crypto`,
`node_modules`,
`@notesnook`,
`sodium`,
`node_modules`,
`sodium-native`,
`prebuilds`
);
const RUNNING_PROCESSES = [];
const RESTARTABLE_PROCESSES = [];
let lastBundleHash = null;
const ENV = {
...process.env,
NO_COLOR: "true",
FORCE_COLOR: "false",
COLOR: "0"
};
await onChange(true);
console.log("Watching...");
@@ -58,6 +52,8 @@ process.on("SIGINT", async (s) => {
async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npx electron-rebuild");
}
await exec(`npm run bundle`);
@@ -68,13 +64,6 @@ async function onChange(first) {
return;
}
if (first) {
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
recursive: true,
force: true
});
}
if (first) {
await spawnAndWaitUntil(
["npx", "nx", "start:desktop", "@notesnook/web"],
@@ -102,7 +91,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
const s = spawn(cmd[0], cmd.slice(1), {
cwd,
env: { ...process.env, NO_COLOR: "true" },
env: ENV,
shell: false
});
@@ -116,14 +105,15 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
});
}
async function exec(cmd) {
async function exec(cmd, cwd) {
try {
console.log(">", cmd);
return execSync(cmd, {
env: process.env,
env: ENV,
stdio: "inherit",
shell: false
shell: false,
cwd: cwd || process.cwd()
});
} catch {
//ignore
@@ -136,7 +126,7 @@ function execAsync(cmd, args, restartable, onExit) {
const proc = spawn(cmd, args, {
stdio: "inherit",
env: process.env,
env: ENV,
shell: false
});
@@ -179,9 +169,10 @@ async function cleanup() {
}
async function isBundleSame() {
const bundle = await fs.readFile(
path.join(__dirname, "..", "build", "electron.js")
);
const bundle = Buffer.concat([
await fs.readFile(path.join(__dirname, "..", "build", "electron.js")),
await fs.readFile(path.join(__dirname, "..", "build", "preload.js"))
]);
const hashSum = crypto.createHash("sha256");
hashSum.update(bundle);

View File

@@ -16,11 +16,13 @@ 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 { writeFileSync, rmSync, readFileSync } = require("fs");
const { writeFileSync, rmSync } = require("fs");
const { execSync } = require("child_process");
const { relative, join } = require("path");
module.exports = async function (configuration) {
if (process.env.NOTESNOOK_STAGING) return;
const Endpoint = "https://weu.codesigning.azure.net";
const CodeSigningAccountName = "Notesnook";
const CertificateProfileName = "Notesnook";
@@ -39,16 +41,13 @@ module.exports = async function (configuration) {
console.debug("Signed", configuration.path);
deleteCatalog();
rmSync(FilesCatalog);
};
function createCatalog(path) {
writeFileSync("_catalog", relative(__dirname, path));
return join(__dirname, "_catalog");
}
function deleteCatalog() {
rmSync("_catalog");
const catalogPath = join(__dirname, "_catalog");
writeFileSync(catalogPath, relative(__dirname, path));
return catalogPath;
}
function psexec(cmd) {

View File

@@ -23,6 +23,7 @@ import { osIntegrationRouter } from "./os-integration";
import { spellCheckerRouter } from "./spell-checker";
import { updaterRouter } from "./updater";
import { bridgeRouter } from "./bridge";
import { safeStorageRouter } from "./safe-storage";
const t = initTRPC.create();
@@ -31,7 +32,8 @@ export const router = t.router({
integration: osIntegrationRouter,
spellChecker: spellCheckerRouter,
updater: updaterRouter,
bridge: bridgeRouter
bridge: bridgeRouter,
safeStorage: safeStorageRouter
});
export const api = router.createCaller({});

View File

@@ -31,6 +31,7 @@ import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises";
const t = initTRPC.create();
@@ -134,10 +135,13 @@ export const osIntegrationRouter = t.router({
.input(z.object({ filePath: z.string() }))
.query(({ input }) => {
const { filePath } = input;
if (!filePath) return;
return resolvePath(filePath);
}),
deleteFile: t.procedure.input(z.string()).query(async ({ input }) => {
await rm(input);
}),
showNotification: t.procedure
.input(NotificationOptions)
.query(({ input }) => {

View File

@@ -0,0 +1,44 @@
/*
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 { safeStorage } from "electron";
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const safeStorageRouter = t.router({
isEncryptionAvailable: t.procedure.query(() => {
return (
!process.env.PORTABLE_EXECUTABLE_DIR &&
safeStorage.isEncryptionAvailable()
);
}),
/**
* Takes a string and returns an encrypted base64 string
*/
encryptString: t.procedure.input(z.string()).query(({ input }) => {
return safeStorage.encryptString(input).toString("base64");
}),
/**
* Takes an encrypted base64 string and returns a string
*/
decryptString: t.procedure.input(z.string()).query(({ input }) => {
return safeStorage.decryptString(Buffer.from(input, "base64"));
})
});

View File

@@ -20,3 +20,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
export * from "./constants";
export type { AppRouter } from "./api";
export { type UpdateInfo } from "builder-util-runtime";
export { type DesktopIntegration } from "./utils/config";

View File

@@ -23,11 +23,13 @@ import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
import type { NNCrypto } from "@notesnook/crypto";
import { ipcRenderer } from "electron";
import { platform } from "os";
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
declare global {
var os: () => "mas" | ReturnType<typeof platform>;
var electronTRPC: RendererGlobalElectronTRPC;
var NativeNNCrypto: (new () => NNCrypto) | undefined;
var createSQLite3Database: (filename: string) => Database;
}
process.once("loaded", async () => {
@@ -40,11 +42,6 @@ process.once("loaded", async () => {
globalThis.electronTRPC = electronTRPC;
});
globalThis.NativeNNCrypto =
process.platform === "win32" &&
process.arch !== "x64" &&
process.arch !== "ia32"
? undefined
: 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,11 +20,15 @@ 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() {
autoUpdater.setFeedURL({
provider: "generic",
url: `https://notesnook.com/releases/${process.platform}/`,
useMultipleRangeRequest: false
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
useMultipleRangeRequest: false,
channel: CHANNEL
});
autoUpdater.autoDownload = config.automaticUpdates;

View File

@@ -16,14 +16,13 @@ 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 "@azure/core-asynciterator-polyfill";
import "react-native-gesture-handler";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { I18nManager, View } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import AppLockedOverlay from "./components/app-lock-overlay";
@@ -42,14 +41,15 @@ I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
SettingsService.checkOrientation();
const App = () => {
const init = useAppEvents();
useEffect(() => {
let { appLockMode } = SettingsService.get();
if (appLockMode && appLockMode !== "none") {
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
@@ -101,10 +101,10 @@ const App = () => {
let currTheme =
useThemeStore.getState().colorScheme === "dark"
? SettingsService.getProperty("darkTheme")
: SettingsService.getProperty("lightTheme");
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element) => {
export const withTheme = (Element: () => JSX.Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,

View File

@@ -17,16 +17,39 @@ 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 Sodium from "@ammarahmed/react-native-sodium";
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 IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
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: {
@@ -37,61 +60,232 @@ const KEYSTORE_CONFIG = Platform.select({
android: {}
});
export async function deriveCryptoKey(name, data) {
try {
let credentials = await Sodium.deriveKey(data.password, data.salt);
await Keychain.setInternetCredentials(
"notesnook",
name,
credentials.key,
KEYSTORE_CONFIG
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"
);
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
return credentials.key;
}
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) {
console.error(e);
DatabaseLogger.error(e);
console.log(e);
}
}
async function upgradeIOSKeychain(username, password) {
if (Platform.OS !== "ios") return;
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
await Keychain.setInternetCredentials(
"notesnook",
username,
password,
KEYSTORE_CONFIG
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
);
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
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 {
if (await Keychain.hasInternetCredentials("notesnook")) {
let credentials = await Keychain.getInternetCredentials(
"notesnook",
KEYSTORE_CONFIG
);
// upgrades ios keychain to use accessGroups
// so we have access to keychain in share extension.
await upgradeIOSKeychain(credentials.username, credentials.password);
return credentials.password;
} else {
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.error(e);
console.log("getCryptoKey", e);
DatabaseLogger.error(e);
}
}
export async function removeCryptoKey(_name) {
try {
let result = await Keychain.resetInternetCredentials("notesnook");
return result;
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
return true;
} catch (e) {
console.error(e);
DatabaseLogger.error(e);
}
}
@@ -109,7 +303,7 @@ export async function generateCryptoKey(password, salt) {
let credentials = await Sodium.deriveKey(password, salt || null);
return credentials;
} catch (e) {
console.log("generateCryptoKey: ", e);
DatabaseLogger.error(e);
}
}
@@ -123,6 +317,9 @@ export async function decrypt(password, data) {
return undefined;
let _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
return await Sodium.decrypt(password, _data);
}
@@ -135,6 +332,11 @@ export async function decryptMulti(password, data) {
d.output = "plain";
return d;
});
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
return await Sodium.decryptMulti(password, data);
}

View File

@@ -16,15 +16,18 @@ 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 } from "@notesnook/core/dist/logger";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import "./logger";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { getDatabaseKey } from "./encryption";
database.host(
__DEV__
@@ -34,11 +37,11 @@ database.host(
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
// API_HOST: "http://192.168.43.108:5264",
// AUTH_HOST: "http://192.168.43.108:8264",
// SSE_HOST: "http://192.168.43.108:7264",
// SUBSCRIPTIONS_HOST: "http://192.168.43.108:9264",
// ISSUES_HOST: "http://192.168.43.108:2624"
// 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",
@@ -49,15 +52,37 @@ database.host(
}
);
database.setup(
Storage,
Platform.OS === "ios" ? EventSource : AndroidEventSource,
filesystem,
{
compress: Gzip.deflate,
decompress: Gzip.inflate
}
);
export async function setupDatabase(password) {
const key = await getDatabaseKey(password);
if (!key)
throw new Error("Database setup failed, could not get database key");
console.log("Opening database with key:", !!key);
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 100,
sqliteOptions: {
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",
password: key
}
});
}
export const db = database;
export const DatabaseLogger = dbLogger;

View File

@@ -16,14 +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 { initalize } from "@notesnook/core/dist/logger";
import { MMKVLoader } from "react-native-mmkv-storage";
import { initialize } from "@notesnook/core/dist/logger";
import { KV } from "./storage";
const LoggerStorage = new MMKVLoader()
.withInstanceID("notesnook_logs")
.initialize();
initalize(new KV(LoggerStorage));
initialize(new KV(LoggerStorage));
export { LoggerStorage };

View File

@@ -0,0 +1,123 @@
/*
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 { DatabaseConnection, Driver, QueryResult } from "kysely";
import { CompiledQuery } from "kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
type Config = { dbName: string; async: boolean; location: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
private connectionMutex = new ConnectionMutex();
private db: QuickSQLiteConnection;
constructor(private readonly config: Config) {
this.db = open({
name: config.dbName
});
}
async init(): Promise<void> {
this.connection = new RNSqliteConnection(this.db);
}
async acquireConnection(): Promise<DatabaseConnection> {
// SQLite only has one single connection. We use a mutex here to wait
// until the single connection has been released.
await this.connectionMutex.lock();
return this.connection!;
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("BEGIN TRANSACTION"));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("COMMIT"));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw("ROLLBACK"));
}
async releaseConnection(): Promise<void> {
this.connectionMutex.unlock();
}
async destroy(): Promise<void> {
this.db.close();
}
}
class ConnectionMutex {
private promise?: Promise<void>;
private resolve?: () => void;
async lock(): Promise<void> {
while (this.promise) {
await this.promise;
}
this.promise = new Promise((resolve) => {
this.resolve = resolve;
});
}
unlock(): void {
const resolve = this.resolve;
this.promise = undefined;
this.resolve = undefined;
resolve?.();
}
}
class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(
compiledQuery: CompiledQuery<unknown>
): Promise<QueryResult<R>> {
const { parameters, sql, query } = compiledQuery;
const mode =
query.kind === "SelectQueryNode"
? "query"
: query.kind === "RawNode"
? "raw"
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);
if (mode === "query" || !result.insertId)
return {
rows: result.rows?._array || []
};
return {
insertId: BigInt(result.insertId),
numAffectedRows: BigInt(result.rowsAffected),
rows: mode === "raw" ? result.rows?._array || [] : []
};
}
}

View File

@@ -91,7 +91,8 @@ export class KV {
}
async removeMulti(keys) {
return this.storage.removeItems(...keys);
if (!keys) return true;
return this.storage.removeItems(keys);
}
async clear() {

View File

@@ -16,20 +16,61 @@ 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 { Dimensions, Platform } from "react-native";
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.
* @returns
*/
export async function compressToBase64(path, type) {
const { width, scale } = Dimensions.get("window");
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,
99999,
type,
width <= 1920 ? 100 : 90,
0,
undefined,
true,
{
mode: "contain",
onlyScaleDown: true
}
);
const base64 = await RNFetchBlob.fs.readFile(
Platform.OS === "ios"
? response.uri?.replace("file://", "")
: response.uri,
"base64"
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
RNFetchBlob.fs
.unlink(response.uri.replace("file://", ""))
.catch(console.log);
resolve(base64);
});
});
}
export async function compressToFile(path, type) {
const response = await ImageResizer.createResizedImage(
path,
width * scale,
9999,
1920,
99999,
type,
80,
0,
@@ -40,11 +81,6 @@ export async function compressToBase64(path, type) {
onlyScaleDown: true
}
);
const base64 = await RNFetchBlob.fs.readFile(
Platform.OS === "ios" ? response.uri?.replace("file://", "") : response.uri,
"base64"
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(console.log);
return base64;
return response.uri;
}

View File

@@ -29,7 +29,7 @@ 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 { ToastEvent, presentSheet } from "../../services/event-manager";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import Storage from "../database/storage";
@@ -46,15 +46,15 @@ export const FileDownloadStatus = {
* Download all user's attachments
* @returns
*/
export function downloadAllAttachments() {
const attachments = db.attachments.all;
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 attachments
* @param {string[]} attachments
* @param onProgress
* @returns
*/
@@ -89,8 +89,8 @@ export async function downloadAttachments(
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = attachments[i];
const hash = attachment.metadata.hash;
let attachment = await db.attachments.attachment(attachments[i]);
const hash = attachment.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -114,15 +114,16 @@ export async function downloadAttachments(
}
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.metadata.filename}`;
const filePath = `${zipSourceFolder}/${attachment.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.metadata.filename,
status: FileDownloadStatus.Success
filename: attachment.filename,
status: FileDownloadStatus.Success,
attachment: attachment
});
} catch (e) {
result.set(hash, {
filename: attachment.metadata.filename,
filename: attachment.filename,
status: FileDownloadStatus.Fail,
reason: e
});
@@ -189,7 +190,7 @@ export default async function downloadAttachment(
) {
await createCacheDir();
let attachment = db.attachments.attachment(hash);
let attachment = await db.attachments.attachment(hash);
if (!attachment) {
console.log("attachment not found");
return;
@@ -208,39 +209,37 @@ export default async function downloadAttachment(
try {
console.log(
"starting download attachment",
attachment.metadata.hash,
attachment.hash,
options.groupId
);
await db.fs.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
if (!(await exists(attachment.metadata.hash))) {
await db
.fs()
.downloadFile(options.groupId || attachment.hash, attachment.hash);
if (!(await exists(attachment.hash))) {
return;
}
if (options.base64 || options.text) {
return await db.attachments.read(
attachment.metadata.hash,
attachment.hash,
options.base64 ? "base64" : "text"
);
}
let filename = getFileNameWithExtension(
attachment.metadata.filename,
attachment.metadata.type
attachment.filename,
attachment.mimeType
);
let key = await db.attachments.decryptKey(attachment.key);
let info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.length,
length: attachment.size,
alg: attachment.alg,
hash: attachment.metadata.hash,
hashType: attachment.metadata.hashType,
mime: attachment.metadata.type,
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize,
@@ -254,7 +253,7 @@ export default async function downloadAttachment(
);
if (!options.silent) {
ToastEvent.show({
ToastManager.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"
@@ -263,11 +262,11 @@ export default async function downloadAttachment(
if (
attachment.dateUploaded &&
!isImage(attachment.metadata?.type) &&
!isDocument(attachment.metadata?.type)
!isImage(attachment.mimeType) &&
!isDocument(attachment.mimeType)
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios" && !options.cache) {
@@ -282,7 +281,7 @@ export default async function downloadAttachment(
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.metadata.hash,
context: global ? null : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
@@ -292,10 +291,13 @@ export default async function downloadAttachment(
console.log("download attachment error: ", e);
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.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.metadata.hash);
useAttachmentStore.getState().remove(attachment.hash);
if (options.throwError) {
throw e;
}

View File

@@ -20,7 +20,7 @@ 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 { ToastEvent } from "../../services/event-manager";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { cacheDir, fileCheck } from "./utils";
@@ -74,13 +74,13 @@ export async function downloadFile(filename, data, cancelToken) {
return status >= 200 && status < 300;
} catch (e) {
if (e.message !== "canceled") {
ToastEvent.show({
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
});
ToastEvent.show({
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
@@ -98,7 +98,7 @@ export async function downloadFile(filename, data, cancelToken) {
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.user.tokenManager.getAccessToken();
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
@@ -119,7 +119,7 @@ export async function checkAttachment(hash) {
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
if (!isInternetReachable) return { success: true };
const attachment = db.attachments.attachment(hash);
const attachment = await db.attachments.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {

View File

@@ -17,13 +17,12 @@ 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 Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
import { db } from "../database";
import { compressToBase64 } from "./compress";
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();
@@ -35,14 +34,8 @@ export async function readEncrypted(filename, key, cipherData) {
return false;
}
const attachment = db.attachments.attachment(filename);
const isPng = !attachment.metadata.type
? false
: /(png)/g.test(attachment?.metadata.type);
const isJpeg = !attachment.metadata.type
? false
: /(jpeg|jpg)/g.test(attachment?.metadata.type);
const attachment = await db.attachments.attachment(filename);
console.log("decrypting....");
let output = await Sodium.decryptFile(
key,
{
@@ -50,20 +43,9 @@ export async function readEncrypted(filename, key, cipherData) {
hash: filename,
appGroupId: IOS_APPGROUPID
},
cipherData.outputType === "base64"
? isPng || isJpeg
? "cache"
: "base64"
: "text"
cipherData.outputType === "base64" ? "base64" : "text"
);
console.log("file decrypted...");
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
const dCachePath = `${cacheDir}/${output}`;
output = await compressToBase64(
`file://${dCachePath}`,
isPng ? "PNG" : "JPEG"
);
}
console.log("file decrypted...", attachment?.mimeType);
return output;
} catch (e) {
@@ -194,10 +176,10 @@ export async function exists(filename) {
}
if (exists || existsInAppGroup) {
const attachment = db.attachments.attachment(filename);
const totalChunks = Math.ceil(attachment.length / attachment.chunkSize);
const attachment = await db.attachments.attachment(filename);
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.length + totalAbytes;
const expectedFileSize = attachment.size + totalAbytes;
const stat = await RNFetchBlob.fs.stat(
existsInAppGroup ? appGroupPath : path

View File

@@ -87,12 +87,9 @@ export async function uploadFile(filename, data, cancelToken) {
DatabaseLogger.info(
`File upload status: ${filename}, ${status}, ${text}`
);
let attachment = db.attachments.attachment(filename);
let attachment = await db.attachments.attachment(filename);
if (!attachment) return result;
if (
!isImage(attachment.metadata.type) &&
!isDocument(attachment.metadata?.type)
) {
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
} else {

View File

@@ -17,18 +17,20 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { FlatList, View } from "react-native";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useMessageStore } from "../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { useMessageStore } from "../../stores/use-message-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { allowedOnPlatform, renderItem } from "./functions";
import { getContainerBorder } from "../../utils/colors";
export const Announcement = ({ color }) => {
const { colors } = useThemeColors();
const announcements = useMessageStore((state) => state.announcements);
let announcement = announcements.length > 0 ? announcements[0] : null;
const selectionMode = useSelectionStore((state) => state.selectionMode);
return !announcement || selectionMode ? null : (
<View
style={{
@@ -45,7 +47,8 @@ export const Announcement = ({ color }) => {
borderRadius: 10,
overflow: "hidden",
backgroundColor: colors.secondary.background,
paddingBottom: 12
paddingBottom: 12,
...getContainerBorder(colors.secondary.background)
}}
>
<View

View File

@@ -103,7 +103,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
key={item.title}
title={item.title}
fontSize={SIZE.sm}
type="gray"
type="plain"
onPress={() => onPress(item)}
width={null}
height={30}
@@ -149,7 +149,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
key={item.title}
title={item.title}
fontSize={SIZE.xs}
type="gray"
type="plain"
onPress={() => onPress(item)}
width={null}
height={30}

View File

@@ -54,7 +54,7 @@ export const Title = ({ text, style = {}, inline }) => {
</Heading>
<Button
type="gray"
type="plain"
icon="close"
height={null}
onPress={() => {

View File

@@ -1,215 +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 React, { useCallback, useEffect, useRef } from "react";
import { Platform, View } from "react-native";
import { enabled } from "react-native-privacy-snapshot";
import { db } from "../../common/database";
import { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../stores/use-user-store";
import { NotesnookModule } from "../../utils/notesnook-module";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
const AppLockedOverlay = () => {
const { colors } = useThemeColors();
const user = useUserStore((state) => state.user);
const appLocked = useUserStore((state) => state.appLocked);
const lockApp = useUserStore((state) => state.lockApp);
const deviceMode = useSettingStore((state) => state.deviceMode);
const passwordInputRef = useRef();
const password = useRef();
const appState = useAppState();
const biometricUnlockAwaitingUserInput = useRef(false);
const onUnlockAppRequested = useCallback(async () => {
if (!(await BiometricService.isBiometryAvailable())) return;
if (Platform.OS === "android") {
const activityName = await NotesnookModule.getActivityName();
if (activityName !== "MainActivity") return;
}
useSettingStore.getState().setRequestBiometrics(true);
let unlocked = await BiometricService.validateUser(
"Unlock to access your notes",
""
);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = null;
}
setTimeout(() => {
biometricUnlockAwaitingUserInput.current = false;
useSettingStore.getState().setRequestBiometrics(false);
}, 1);
}, [lockApp]);
const onSubmit = async () => {
if (!password.current) return;
try {
let unlocked = await db.user.verifyPassword(password.current);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = null;
}
} catch (e) {
console.error(e);
}
};
useEffect(() => {
if (
biometricUnlockAwaitingUserInput.current ||
useUserStore.getState().disableAppLockRequests
)
return;
if (appLocked && appState === "active") {
biometricUnlockAwaitingUserInput.current = true;
onUnlockAppRequested();
}
}, [appState, onUnlockAppRequested, appLocked]);
return (
appLocked && (
<View
style={{
backgroundColor: colors.primary.background,
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999,
justifyContent: "center"
}}
>
<View
style={{
flex: 1,
justifyContent: "center",
width:
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,
alignSelf: "center"
}}
>
<IconButton
name="fingerprint"
size={100}
customStyle={{
width: 100,
height: 100,
marginBottom: 20,
marginTop: user ? 0 : 50
}}
onPress={onUnlockAppRequested}
color={colors.primary.border}
/>
<Heading
color={colors.primary.heading}
style={{
alignSelf: "center",
textAlign: "center"
}}
>
Unlock your notes
</Heading>
<Paragraph
style={{
alignSelf: "center",
textAlign: "center",
maxWidth: "90%"
}}
>
{"Please verify it's you"}
</Paragraph>
<Seperator />
<View
style={{
width: "100%",
padding: 12,
backgroundColor: colors.primary.background
}}
>
{user ? (
<>
<Input
fwdRef={passwordInputRef}
secureTextEntry
placeholder="Enter account password"
onChangeText={(v) => (password.current = v)}
onSubmit={onSubmit}
/>
</>
) : null}
<View
style={{
marginTop: user ? 25 : 25
}}
>
{user ? (
<>
<Button
title="Continue"
type="accent"
onPress={onSubmit}
width={250}
height={45}
style={{
borderRadius: 150,
marginBottom: 10
}}
fontSize={SIZE.md}
/>
</>
) : null}
<Button
title="Unlock with Biometrics"
width={250}
onPress={onUnlockAppRequested}
icon={"fingerprint"}
type="transparent"
/>
</View>
</View>
</View>
</View>
)
);
};
export default AppLockedOverlay;

View File

@@ -0,0 +1,299 @@
/*
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 React, { useCallback, useEffect, useRef } from "react";
import { Platform, TextInput, View } from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import { enabled } from "react-native-privacy-snapshot";
import { DatabaseLogger } from "../../common/database";
import {
decrypt,
encrypt,
getCryptoKey,
getDatabaseKey,
setAppLockVerificationCipher,
validateAppLockPassword
} from "../../common/database/encryption";
import { MMKV } from "../../common/database/mmkv";
import { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import { NotesnookModule } from "../../utils/notesnook-module";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
const getUser = () => {
const user = MMKV.getString("user");
if (user) {
return JSON.parse(user);
}
};
const verifyUserPassword = async (password: string) => {
try {
await getDatabaseKey();
const key = await getCryptoKey();
const user = getUser();
const cipher = await encrypt(
{
key: key,
salt: user.salt
},
"notesnook"
);
const plainText = await decrypt({ password }, cipher);
return plainText === "notesnook";
} catch (e) {
DatabaseLogger.error(e as Error);
return false;
}
};
const AppLockedOverlay = () => {
const { colors } = useThemeColors();
const user = getUser();
const appLocked = useUserStore((state) => state.appLocked);
const lockApp = useUserStore((state) => state.lockApp);
const deviceMode = useSettingStore((state) => state.deviceMode);
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>();
const appState = useAppState();
const biometricUnlockAwaitingUserInput = useRef(false);
const keyboardType = useSettingStore(
(state) => state.settings.applockKeyboardType
);
const appLockHasPasswordSecurity = useSettingStore(
(state) => state.settings.appLockHasPasswordSecurity
);
const biometricsAuthEnabled = useSettingStore(
(state) =>
state.settings.biometricsAuthEnabled === true ||
(state.settings.biometricsAuthEnabled === undefined &&
!state.settings.appLockHasPasswordSecurity)
);
const onUnlockAppRequested = useCallback(async () => {
if (
!biometricsAuthEnabled ||
!(await BiometricService.isBiometryAvailable())
)
return;
biometricUnlockAwaitingUserInput.current = true;
if (Platform.OS === "android") {
const activityName = await NotesnookModule.getActivityName();
if (activityName !== "MainActivity") return;
}
useSettingStore.getState().setRequestBiometrics(true);
const unlocked = await BiometricService.validateUser(
"Unlock to access your notes",
""
);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = undefined;
}
biometricUnlockAwaitingUserInput.current = false;
setTimeout(() => {
if (biometricUnlockAwaitingUserInput.current) return;
useSettingStore.getState().setRequestBiometrics(false);
}, 500);
}, [biometricsAuthEnabled, lockApp]);
const onSubmit = async () => {
if (!password.current) return;
try {
const unlocked = appLockHasPasswordSecurity
? validateAppLockPassword(password.current)
: await verifyUserPassword(password.current);
if (unlocked) {
if (!appLockHasPasswordSecurity) {
await setAppLockVerificationCipher(password.current);
SettingsService.set({
appLockHasPasswordSecurity: true,
applockKeyboardType: "default"
});
DatabaseLogger.info("App lock migrated to password security");
}
lockApp(false);
enabled(false);
password.current = undefined;
}
} catch (e) {
console.error(e);
}
};
useEffect(() => {
if (
biometricUnlockAwaitingUserInput.current ||
useUserStore.getState().disableAppLockRequests
)
return;
if (
appLocked &&
appState === "active" &&
!useSettingStore.getState().requestBiometrics
) {
biometricUnlockAwaitingUserInput.current = true;
onUnlockAppRequested();
}
}, [appState, onUnlockAppRequested, appLocked]);
return appLocked ? (
<View
style={{
backgroundColor: colors.primary.background,
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999,
justifyContent: "center"
}}
>
<View
style={{
flex: 1,
justifyContent: "center",
width:
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,
alignSelf: "center"
}}
>
<IconButton
name="fingerprint"
size={100}
style={{
width: 100,
height: 100,
marginBottom: 20,
marginTop: user ? 0 : 50
}}
onPress={onUnlockAppRequested}
color={colors.primary.border}
/>
<Heading
color={colors.primary.heading}
style={{
alignSelf: "center",
textAlign: "center"
}}
>
Unlock your notes
</Heading>
<Paragraph
style={{
alignSelf: "center",
textAlign: "center",
maxWidth: "90%"
}}
>
{"Please verify it's you"}
</Paragraph>
<Seperator />
<View
style={{
width: "100%",
padding: 12,
backgroundColor: colors.primary.background
}}
>
{user || appLockHasPasswordSecurity ? (
<>
<Input
fwdRef={passwordInputRef}
secureTextEntry
keyboardType={
appLockHasPasswordSecurity ? keyboardType : "default"
}
placeholder={`Enter ${
appLockHasPasswordSecurity
? `app lock ${
keyboardType === "numeric" ? "pin" : "password"
}`
: "account password"
}`}
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();
}}
/>
</>
) : null}
<View
style={{
marginTop: user ? 25 : 25
}}
>
{user || appLockHasPasswordSecurity ? (
<>
<Button
title="Continue"
type="accent"
onPress={onSubmit}
width={250}
height={45}
style={{
borderRadius: 150,
marginBottom: 10
}}
fontSize={SIZE.md}
/>
</>
) : null}
{biometricsAuthEnabled ? (
<Button
title="Unlock with Biometrics"
width={250}
onPress={onUnlockAppRequested}
icon={"fingerprint"}
type="transparent"
/>
) : null}
</View>
</View>
</View>
</View>
) : null;
};
export default AppLockedOverlay;

View File

@@ -17,9 +17,13 @@ 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 { formatBytes } from "@notesnook/common";
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useState } from "react";
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { ScrollView } from "react-native-gesture-handler";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
@@ -27,14 +31,18 @@ import downloadAttachment from "../../common/filesystem/download-attachment";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import picker from "../../screens/editor/tiptap/picker";
import {
ToastManager,
eSendEvent,
presentSheet,
ToastEvent
presentSheet
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
import {
eCloseAttachmentDialog,
eCloseSheet,
eDBItemUpdate,
eOnLoadNote
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
@@ -43,31 +51,42 @@ import { openNote } from "../list-items/note/wrapper";
import { DateMeta } from "../properties/date-meta";
import { Button } from "../ui/button";
import { Notice } from "../ui/notice";
import { PressableButton } from "../ui/pressable";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { formatBytes } from "@notesnook/common";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
const Actions = ({
attachment,
close,
setAttachments,
fwdRef
}: {
attachment: Attachment;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close?: () => void;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const contextId = attachment.metadata.hash;
const [filename, setFilename] = useState(attachment.metadata.filename);
const contextId = attachment.hash;
const [filename, setFilename] = useState(attachment.filename);
const [currentProgress] = useAttachmentProgress(attachment);
const [failed, setFailed] = useState(attachment.failed);
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState({
name: null
});
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState<{
name?: string;
}>({});
const actions = [
{
name: "Download",
onPress: async () => {
if (currentProgress) {
await db.fs.cancel(attachment.metadata.hash);
useAttachmentStore.getState().remove(attachment.metadata.hash);
await db.fs().cancel(attachment.hash);
useAttachmentStore.getState().remove(attachment.hash);
}
downloadAttachment(attachment.metadata.hash, false);
downloadAttachment(attachment.hash, false);
eSendEvent(eCloseSheet, contextId);
},
icon: "download"
@@ -76,7 +95,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
name: "Reupload",
onPress: async () => {
if (!PremiumService.get()) {
ToastEvent.show({
ToastManager.show({
heading: "Upgrade to pro",
type: "error",
context: "local"
@@ -85,9 +104,9 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
}
await picker.pick({
reupload: true,
hash: attachment.metadata.hash,
hash: attachment.hash,
context: contextId,
type: attachment.metadata.type
type: attachment.mimeType.startsWith("image") ? "image" : "file"
});
},
icon: "upload"
@@ -98,19 +117,20 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
setLoading({
name: "Run file check"
});
let res = await filesystem.checkAttachment(attachment.metadata.hash);
const res = await filesystem.checkAttachment(attachment.hash);
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
ToastEvent.show({
ToastManager.show({
heading: "File check failed with error: " + res.failed,
type: "error",
context: "local"
});
} else {
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
ToastEvent.show({
setFailed(undefined);
db.attachments.markAsFailed(attachment.id);
eSendEvent(eDBItemUpdate, attachment.id);
ToastManager.show({
heading: "File check passed",
type: "success",
context: "local"
@@ -119,7 +139,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
setAttachments();
setLoading({
name: null
name: undefined
});
},
icon: "file-check"
@@ -128,19 +148,20 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
name: "Rename",
onPress: () => {
presentDialog({
context: contextId,
context: contextId as any,
input: true,
title: "Rename file",
paragraph: "Enter a new name for the file",
defaultValue: attachment.metadata.filename,
defaultValue: attachment.filename,
positivePress: async (value) => {
if (value && value.length > 0) {
await db.attachments.add({
hash: attachment.metadata.hash,
hash: attachment.hash,
filename: value
});
setFilename(value);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
}
},
positiveText: "Rename"
@@ -151,34 +172,40 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
{
name: "Delete",
onPress: async () => {
await db.attachments.remove(attachment.metadata.hash, false);
const relations = await db.relations.to(attachment, "note").get();
await db.attachments.remove(attachment.hash, false);
setAttachments();
close();
eSendEvent(eDBItemUpdate, attachment.id);
relations
.map((relation) => relation.fromId)
.forEach(async (id) => {
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
const isFocused = useTabStore.getState().currentTab === tab;
if (isFocused) {
eSendEvent(eOnLoadNote, {
item: await db.notes.note(id),
forced: true
});
} else {
editorController.current.commands.setLoading(true, tab);
}
}
});
close?.();
},
icon: "delete-outline"
}
];
const getNotes = useCallback(() => {
let allNotes = db.notes.all;
let attachmentNotes = attachment.noteIds?.map((id) => {
let index = allNotes?.findIndex((note) => id === note.id);
if (index !== -1) {
return allNotes[index];
} else {
return {
type: "notfound",
title: `Note with id ${id} does not exist.`,
id: id
};
}
});
return attachmentNotes;
}, [attachment.noteIds]);
useEffect(() => {
setNotes(getNotes());
}, [attachment, getNotes]);
db.relations
.to(attachment, "note")
.selector.items()
.then((items) => {
setNotes(items);
});
}, [attachment]);
return (
<ScrollView
@@ -221,7 +248,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
}}
color={colors.secondary.paragraph}
>
{attachment.metadata.type}
{attachment.type}
</Paragraph>
<Paragraph
style={{
@@ -230,10 +257,10 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{formatBytes(attachment.length)}
{formatBytes(attachment.size)}
</Paragraph>
{attachment.noteIds ? (
{notes.length ? (
<Paragraph
style={{
marginRight: 10
@@ -241,14 +268,14 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{attachment.noteIds.length} note
{attachment.noteIds.length > 1 ? "s" : ""}
{notes.length} note
{notes.length > 1 ? "s" : ""}
</Paragraph>
) : null}
<Paragraph
onPress={() => {
Clipboard.setString(attachment.metadata.hash);
ToastEvent.show({
Clipboard.setString(attachment.hash);
ToastManager.show({
type: "success",
heading: "Attachment hash copied",
context: "local"
@@ -257,7 +284,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{attachment.metadata.hash}
{attachment.hash}
</Paragraph>
</View>
@@ -284,25 +311,15 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
</Heading>
{notes.map((item) => (
<PressableButton
<Pressable
onPress={async () => {
if (item.type === "notfound") {
ToastEvent.show({
heading: "Note not found",
message:
"A note with the given id was not found. Maybe you have deleted the note or moved it to trash already.",
type: "error",
context: "local"
});
return;
}
eSendEvent(eCloseSheet, contextId);
await sleep(150);
eSendEvent(eCloseAttachmentDialog);
await sleep(300);
openNote(item, item.type === "trash");
openNote(item, (item as any).type === "trash");
}}
customStyle={{
style={{
paddingVertical: 12,
alignItems: "flex-start",
@@ -311,7 +328,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
key={item.id}
>
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
</PressableButton>
</Pressable>
))}
</>
</View>
@@ -321,17 +338,16 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
<Button
key={item.name}
buttonType={{
text: item.on
? colors.primary.accent
: item.name === "Delete" || item.name === "PermDelete"
? colors.error.paragraph
: colors.primary.paragraph
text:
item.name === "Delete"
? colors.error.paragraph
: colors.primary.paragraph
}}
onPress={item.onPress}
title={item.name}
icon={item.icon}
loading={loading?.name === item.name}
type={item.on ? "shade" : "gray"}
type="plain"
fontSize={SIZE.sm}
style={{
borderRadius: 0,
@@ -359,7 +375,11 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
);
};
Actions.present = (attachment, set, context) => {
Actions.present = (
attachment: Attachment,
set: (attachments?: VirtualizedGrouping<Attachment>) => void,
context?: string
) => {
presentSheet({
context: context,
component: (ref, close) => (

View File

@@ -1,171 +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 { formatBytes } from "@notesnook/common";
import React from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
function getFileExtension(filename) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
attachment,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading,
context
}) => {
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const onPress = () => {
if (!pressable) return;
Actions.present(attachment, setAttachments, context);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
style={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.secondary.background
}}
type="grayBg"
>
<View
style={{
flexShrink: 1,
flexDirection: "row",
alignItems: "center"
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginLeft: -5
}}
>
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
<Paragraph
adjustsFontSizeToFit
size={6}
color={colors.static.white}
style={{
position: "absolute"
}}
>
{getFileExtension(attachment.metadata.filename).toUpperCase()}
</Paragraph>
</View>
<View
style={{
flexShrink: 1,
marginLeft: 10
}}
>
<Paragraph
size={SIZE.sm - 1}
style={{
flexWrap: "wrap",
marginBottom: 2.5
}}
numberOfLines={1}
lineBreakMode="middle"
color={colors.primary.paragraph}
>
{attachment.metadata.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
</View>
</View>
{currentProgress ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
style={{
justifyContent: "center",
marginLeft: 5,
marginTop: 5,
marginRight: -5
}}
>
<ProgressCircleComponent
size={SIZE.xxl}
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
showsText
textStyle={{
fontSize: 10
}}
color={colors.primary.accent}
formatText={(progress) => (progress * 100).toFixed(0)}
borderWidth={0}
thickness={2}
/>
</TouchableOpacity>
) : (
<>
{attachment.failed ? (
<IconButton
onPress={onPress}
name="alert-circle-outline"
color={colors.error.paragraph}
/>
) : null}
</>
)}
</TouchableOpacity>
);
};

View File

@@ -0,0 +1,191 @@
/*
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 { formatBytes } from "@notesnook/common";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useDBItem } from "../../hooks/use-db-item";
import { SIZE } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
id,
attachments,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading,
context
}: {
id: string | number;
attachments?: VirtualizedGrouping<Attachment>;
encryption?: boolean;
setAttachments: (attachments: any) => void;
pressable?: boolean;
hideWhenNotDownloading?: boolean;
context?: string;
}) => {
const [attachment] = useDBItem(id, "attachment", attachments);
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const onPress = () => {
if (!pressable || !attachment) return;
Actions.present(attachment, setAttachments, context);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
style={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.secondary.background,
minHeight: 45
}}
>
{!attachment ? null : (
<>
<View
style={{
flexShrink: 1,
flexDirection: "row",
alignItems: "center"
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginLeft: -5
}}
>
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
<Paragraph
adjustsFontSizeToFit
size={6}
color={colors.static.white}
style={{
position: "absolute"
}}
>
{getFileExtension(attachment.filename).toUpperCase()}
</Paragraph>
</View>
<View
style={{
flexShrink: 1,
marginLeft: 10
}}
>
<Paragraph
size={SIZE.sm - 1}
style={{
flexWrap: "wrap",
marginBottom: 2.5
}}
numberOfLines={1}
lineBreakMode="middle"
color={colors.primary.paragraph}
>
{attachment.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.size)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
</View>
</View>
{currentProgress ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs().cancel(attachment.hash);
setCurrentProgress(undefined);
}}
style={{
justifyContent: "center",
marginLeft: 5,
marginTop: 5,
marginRight: -5
}}
>
<ProgressCircleComponent
size={SIZE.xxl}
progress={
currentProgress?.value ? currentProgress?.value / 100 : 0
}
showsText
textStyle={{
fontSize: 10
}}
color={colors.primary.accent}
formatText={(progress) => (progress * 100).toFixed(0)}
borderWidth={0}
thickness={2}
/>
</TouchableOpacity>
) : (
<>
{attachment.failed ? (
<IconButton
onPress={onPress}
name="alert-circle-outline"
color={colors.error.paragraph}
/>
) : null}
</>
)}
</>
)}
</TouchableOpacity>
);
};

View File

@@ -21,7 +21,10 @@ import React, { useRef, useState } from "react";
import { Platform, View } from "react-native";
import { db } from "../../common/database";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import { presentSheet } from "../../services/event-manager";
import {
PresentSheetOptions,
presentSheet
} from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -29,8 +32,19 @@ import { ProgressBarComponent } from "../ui/svg/lazy";
import { useThemeColors } from "@notesnook/theme";
import { FlatList } from "react-native-actions-sheet";
import { AttachmentItem } from "./attachment-item";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
const DownloadAttachments = ({
close,
attachments,
isNote,
update
}: {
attachments: VirtualizedGrouping<Attachment>;
close?: ((ctx?: string | undefined) => void) | undefined;
isNote?: boolean;
update?: (props: PresentSheetOptions) => void;
}) => {
const { colors } = useThemeColors();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
@@ -39,63 +53,57 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef();
const groupId = useRef<string>();
const onDownload = async () => {
update({
update?.({
disableClosing: true
});
} as PresentSheetOptions);
setDownloading(true);
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
attachments,
(progress, statusText) => setProgress({ value: progress, statusText }),
await attachments.ids(),
(progress: number, statusText: string) =>
setProgress({ value: progress, statusText }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update({
update?.({
disableClosing: false
});
} as PresentSheetOptions);
};
const cancel = async () => {
update({
update?.({
disableClosing: false
});
} as PresentSheetOptions);
canceled.current = true;
if (!groupId.current) return;
console.log(groupId.current, "canceling groupId downloads");
await db.fs.cancel(groupId.current);
await db.fs().cancel(groupId.current, "download");
setDownloading(false);
groupId.current = null;
};
const successResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
if (value.status === 1) results.push(db.attachments.attachment(key));
}
return results;
groupId.current = undefined;
};
const failedResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
if (value.status === 0) results.push(db.attachments.attachment(key));
for (const value of result.values()) {
if (value.status === 0) results.push(value.attachment);
}
return results;
};
function getResultText() {
const downloadedAttachmentsCount =
attachments.length - failedResults().length;
attachments?.placeholders?.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments.length
attachments?.placeholders.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
@@ -157,7 +165,11 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
width={null}
animated={true}
useNativeDriver
progress={progress.value ? progress.value / attachments.length : 0}
progress={
progress.value
? progress.value / attachments.placeholders?.length
: 0
}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
@@ -174,7 +186,7 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments : []}
data={downloading ? attachments.placeholders : undefined}
ListEmptyComponent={
<View
style={{
@@ -189,14 +201,15 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
</Paragraph>
</View>
}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
keyExtractor={(index) => "attachment" + index}
renderItem={({ index }) => {
return (
<AttachmentItem
attachment={item}
id={index}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
attachments={attachments}
/>
);
}}
@@ -209,7 +222,9 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
borderRadius: 100,
marginTop: 20
}}
onPress={close}
onPress={() => {
close?.();
}}
type="accent"
title="Done"
/>
@@ -227,8 +242,10 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
borderRadius: 100,
marginRight: 5
}}
onPress={close}
type="grayBg"
onPress={() => {
close?.();
}}
type="secondary"
title="No"
/>
<Button
@@ -258,7 +275,11 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
);
};
DownloadAttachments.present = (context, attachments, isNote) => {
DownloadAttachments.present = (
context: string,
attachments: VirtualizedGrouping<Attachment>,
isNote?: boolean
) => {
presentSheet({
context: context,
component: (ref, close, update) => (

View File

@@ -17,16 +17,25 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import {
Attachment,
Note,
SortOptions,
VirtualizedGrouping
} from "@notesnook/core";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { presentSheet } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { useSettingStore } from "../../stores/use-setting-store";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
@@ -34,82 +43,87 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
import { Button } from "../ui/button";
import {
isAudio,
isDocument,
isImage,
isVideo
} from "@notesnook/core/dist/utils/filename";
import { useSettingStore } from "../../stores/use-setting-store";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
export const AttachmentDialog = ({ note }) => {
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
sortDirection: "desc"
};
export const AttachmentDialog = ({ note }: { note?: Note }) => {
const { colors } = useThemeColors();
const { height } = useSettingStore((state) => state.dimensions);
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])]
);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>();
const searchTimer = useRef<NodeJS.Timeout>();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
const onChangeText = (text) => {
const attachments = note?.id
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])];
const refresh = React.useCallback(() => {
if (note) {
db.attachments.ofNote(note.id, "all").sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
});
} else {
db.attachments.all
.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
})
.then((attachments) => setAttachments(attachments));
}
}, [note]);
useEffect(() => {
refresh();
}, [note, refresh]);
const onChangeText = async (text: string) => {
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments(filterAttachments(currentFilter));
setAttachments(await filterAttachments(currentFilter));
refresh();
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
attachments,
attachmentSearchValue.current
);
searchTimer.current = setTimeout(async () => {
const results = await db.lookup
.attachments(attachmentSearchValue.current as string)
.sorted();
if (results.length === 0) return;
setAttachments(filterAttachments(currentFilter, results));
setAttachments(results);
}, 300);
};
const renderItem = ({ item }) => (
const renderItem = ({ index }: { item: boolean; index: number }) => (
<AttachmentItem
setAttachments={() => {
setAttachments(filterAttachments(currentFilter));
setAttachments={async () => {
setAttachments(await filterAttachments(currentFilter));
}}
attachment={item}
attachments={attachments}
id={index}
context="attachments-list"
/>
);
const onCheck = async () => {
if (!attachments) return;
setLoading(true);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
for (let i = 0; i < attachments.placeholders.length; i++) {
const attachment = (await attachments.item(i))?.item;
if (!attachment) continue;
const result = await filesystem.checkAttachment(attachment.hash);
if (result.failed) {
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
await db.attachments.markAsFailed(attachment.hash, result.failed);
} else {
await db.attachments.markAsFailed(attachment.id, null);
await db.attachments.markAsFailed(attachment.id, undefined);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
refresh();
setLoading(false);
};
@@ -136,33 +150,36 @@ export const AttachmentDialog = ({ note }) => {
}
];
const filterAttachments = (type, _attachments) => {
const attachments = _attachments
? _attachments
: note
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])];
const filterAttachments = async (type: string) => {
let items: FilteredSelector<Attachment> = db.attachments.all;
switch (type) {
case "all":
return attachments;
items = db.attachments.all;
break;
case "images":
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
items = note
? db.attachments.ofNote(note.id, "images")
: db.attachments.images;
break;
case "video":
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
items = items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.videos;
break;
case "audio":
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
items = db.attachments.all;
break;
case "documents":
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.documents;
}
return await items.sorted({
...DEFAULT_SORTING,
sortBy: "dateModified"
});
};
return (
@@ -201,7 +218,7 @@ export const AttachmentDialog = ({ note }) => {
) : (
<IconButton
name="check-all"
customStyle={{
style={{
height: 40,
width: 40,
marginRight: 10
@@ -214,12 +231,13 @@ export const AttachmentDialog = ({ note }) => {
<IconButton
name="download"
customStyle={{
style={{
height: 40,
width: 40
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
@@ -236,7 +254,7 @@ export const AttachmentDialog = ({ note }) => {
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
onChangeText(attachmentSearchValue.current as string);
}}
/>
@@ -256,12 +274,11 @@ export const AttachmentDialog = ({ note }) => {
>
{attachmentTypes.map((item) => (
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
type={
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
}
key={item.title}
title={item.title}
style={{
borderRadius: 0,
borderBottomWidth: 1,
@@ -271,9 +288,9 @@ export const AttachmentDialog = ({ note }) => {
? "transparent"
: colors.primary.accent
}}
onPress={() => {
onPress={async () => {
setCurrentFilter(item.filterBy);
setAttachments(filterAttachments(item.filterBy));
setAttachments(await filterAttachments(item.filterBy));
}}
/>
))}
@@ -304,7 +321,7 @@ export const AttachmentDialog = ({ note }) => {
/>
}
estimatedItemSize={50}
data={attachments}
data={attachments?.placeholders}
renderItem={renderItem}
/>
@@ -327,7 +344,7 @@ export const AttachmentDialog = ({ note }) => {
);
};
AttachmentDialog.present = (note) => {
AttachmentDialog.present = (note?: Note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
});

View File

@@ -158,7 +158,7 @@ const AuthModal = () => {
hideAuth();
}}
iconSize={16}
type="gray"
type="plain"
iconPosition="right"
icon="chevron-right"
height={25}

View File

@@ -23,7 +23,7 @@ import { db } from "../../common/database";
import {
eSendEvent,
presentSheet,
ToastEvent
ToastManager
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
@@ -48,7 +48,7 @@ export const ChangePassword = () => {
const changePassword = async () => {
if (!user?.isEmailConfirmed) {
ToastEvent.show({
ToastManager.show({
heading: "Email not confirmed",
message: "Please confirm your email to change account password",
type: "error",
@@ -57,7 +57,7 @@ export const ChangePassword = () => {
return;
}
if (error || !oldPassword.current || !password.current) {
ToastEvent.show({
ToastManager.show({
heading: "All fields required",
message: "Fill all the fields and try again.",
type: "error",
@@ -72,7 +72,7 @@ export const ChangePassword = () => {
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastEvent.show({
ToastManager.show({
heading: "Account password updated",
type: "success",
context: "global"
@@ -83,7 +83,7 @@ export const ChangePassword = () => {
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
setLoading(false);
ToastEvent.show({
ToastManager.show({
heading: "Failed to change password",
message: e.message,
type: "error",

View File

@@ -22,7 +22,7 @@ import { View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastEvent } from "../../services/event-manager";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useThemeColors } from "@notesnook/theme";
import DialogHeader from "../dialog/dialog-header";
@@ -43,7 +43,7 @@ export const ForgotPassword = () => {
const sendRecoveryEmail = async () => {
if (!email.current || error) {
ToastEvent.show({
ToastManager.show({
heading: "Account email is required.",
type: "error",
context: "local"
@@ -63,7 +63,7 @@ export const ForgotPassword = () => {
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
ToastEvent.show({
ToastManager.show({
heading: "Check your email to reset password",
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
type: "success",
@@ -74,7 +74,7 @@ export const ForgotPassword = () => {
setSent(true);
} catch (e) {
setLoading(false);
ToastEvent.show({
ToastManager.show({
heading: "Recovery email not sent",
message: e.message,
type: "error",
@@ -113,7 +113,7 @@ export const ForgotPassword = () => {
}}
>
<IconButton
customStyle={{
style={{
width: null,
height: null
}}

View File

@@ -59,22 +59,6 @@ const Auth = ({ navigation, route }) => {
/>
)}
{/* {initialAuthMode.current === AuthMode.welcomeSignup ? null : (
<IconButton
name="arrow-left"
onPress={() => {
hideAuth();
}}
color={colors.primary.paragraph}
customStyle={{
position: 'absolute',
zIndex: 999,
left: 12,
top: Platform.OS === 'ios' ? 12 + insets.top : insets.top
}}
/>
)} */}
<Toast context="local" />
</View>
);

View File

@@ -103,7 +103,7 @@ export const Login = ({ changeMode }) => {
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.secondary.background,
borderBottomWidth: 1,
borderBottomWidth: 0.8,
marginBottom: 12,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
@@ -222,7 +222,7 @@ export const Login = ({ changeMode }) => {
textDecorationLine: "underline"
}}
fontSize={SIZE.xs}
type="gray"
type="plain"
/>
</>
)}

View File

@@ -19,12 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { Modal, View } from "react-native";
import { View } from "react-native";
import { db } from "../../common/database";
import { MMKV } from "../../common/database/mmkv";
import BiometricService from "../../services/biometrics";
import {
ToastEvent,
ToastManager,
eSendEvent,
eSubscribeEvent
} from "../../services/event-manager";
@@ -37,6 +37,7 @@ import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import BaseDialog from "../dialog/base-dialog";
import { presentDialog } from "../dialog/functions";
import SheetProvider from "../sheet-provider";
import { Toast } from "../toast";
@@ -46,7 +47,6 @@ import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
import BaseDialog from "../dialog/base-dialog";
function getObfuscatedEmail(email) {
if (!email) return "";
@@ -85,7 +85,7 @@ export const SessionExpired = () => {
clearAllStores();
setVisible(false);
} catch (e) {
ToastEvent.show({
ToastManager.show({
heading: e.message,
type: "error",
context: "local"
@@ -102,9 +102,9 @@ export const SessionExpired = () => {
const open = React.useCallback(async () => {
try {
let res = await db.user.tokenManager.getToken();
let res = await db.tokenManager.getToken();
if (!res) throw new Error("no token found");
if (db.user.tokenManager._isTokenExpired(res))
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
Sync.run("global", false, true, async (complete) => {
if (!complete) {
@@ -166,7 +166,7 @@ export const SessionExpired = () => {
}}
>
<IconButton
customStyle={{
style={{
width: 60,
height: 60
}}

View File

@@ -21,7 +21,7 @@ import React, { useRef, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastEvent } from "../../services/event-manager";
import { ToastManager } from "../../services/event-manager";
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
@@ -52,7 +52,7 @@ export const Signup = ({ changeMode, trial }) => {
const { width, height } = useWindowDimensions();
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastEvent.show({
ToastManager.show({
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
@@ -85,7 +85,7 @@ export const Signup = ({ changeMode, trial }) => {
}
} catch (e) {
setLoading(false);
ToastEvent.show({
ToastManager.show({
heading: "Signup failed",
message: e.message,
type: "error",
@@ -111,7 +111,7 @@ export const Signup = ({ changeMode, trial }) => {
paddingHorizontal: 20,
backgroundColor: colors.secondary.background,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomWidth: 0.8,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,

View File

@@ -24,7 +24,7 @@ import useTimer from "../../hooks/use-timer";
import {
eSendEvent,
presentSheet,
ToastEvent
ToastManager
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { eCloseSheet } from "../../utils/events";
@@ -32,7 +32,7 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import { PressableButton } from "../ui/pressable";
import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -138,7 +138,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
setSending(false);
} catch (e) {
setSending(false);
ToastEvent.error(e, "Error sending 2FA Code", "local");
ToastManager.error(e, "Error sending 2FA Code", "local");
}
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
@@ -151,7 +151,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
}}
>
<IconButton
customStyle={{
style={{
width: 70,
height: 70
}}
@@ -183,7 +183,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
<Button
onPress={onSendCode}
type={seconds ? "gray" : "transparent"}
type={seconds ? "plain" : "transparent"}
title={
sending
? ""
@@ -247,7 +247,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
<Button
title={secondaryMethodsText[currentMethod.method]}
type="gray"
type="plain"
onPress={onRequestSecondaryMethod}
height={30}
/>
@@ -255,7 +255,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
) : (
<>
{getMethods().map((item) => (
<PressableButton
<Pressable
key={item.title}
onPress={() => {
setCurrentMethod({
@@ -263,7 +263,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
isPrimary: false
});
}}
customStyle={{
style={{
paddingHorizontal: 12,
paddingVertical: 12,
marginTop: 0,
@@ -275,8 +275,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
}}
>
<IconButton
type="grayAccent"
customStyle={{
type="secondaryAccented"
style={{
width: 40,
height: 40,
marginRight: 10
@@ -292,7 +292,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
>
<Paragraph size={SIZE.md}>{item.title}</Paragraph>
</View>
</PressableButton>
</Pressable>
))}
</>
)}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useRef, useState } from "react";
import { db } from "../../common/database";
import { eSendEvent, ToastEvent } from "../../services/event-manager";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { clearMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -48,7 +48,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
(!password.current && step === LoginSteps.passwordAuth) ||
(!email.current && step === LoginSteps.emailAuth)
) {
ToastEvent.show({
ToastManager.show({
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
@@ -121,7 +121,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
const finishWithError = async (e) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
ToastEvent.show({
ToastManager.show({
heading: "Login failed",
message: e.message,
type: "error",
@@ -135,7 +135,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastEvent.show({
ToastManager.show({
heading: "Login successful",
message: `Logged in as ${user.email}`,
type: "success",

View File

@@ -34,12 +34,13 @@ import { useSettingStore } from "../../stores/use-setting-store";
import { getElevationStyle } from "../../utils/elevation";
import { SIZE, normalize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import { PressableButton } from "../ui/pressable";
import { Pressable } from "../ui/pressable";
interface FloatingButton {
title?: string;
interface FloatingButtonProps {
title: string;
onPress: () => void;
color?: string;
shouldShow?: boolean;
alwaysVisible?: boolean;
}
@@ -48,7 +49,7 @@ const FloatingButton = ({
onPress,
color,
alwaysVisible = false
}: FloatingButton) => {
}: FloatingButtonProps) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
@@ -115,11 +116,11 @@ const FloatingButton = ({
animatedStyle
]}
>
<PressableButton
<Pressable
testID={notesnook.buttons.add}
type="accent"
accentColor={colors.static[color as keyof typeof colors.static]}
customStyle={{
accentColor={color}
style={{
...getElevationStyle(5),
borderRadius: 100
}}
@@ -144,7 +145,7 @@ const FloatingButton = ({
size={SIZE.xxl}
/>
</View>
</PressableButton>
</Pressable>
</Animated.View>
);
};

View File

@@ -17,41 +17,42 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { PropsWithChildren } from "react";
import { KeyboardAvoidingView, Platform } from "react-native";
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import { useSettingStore } from "../../stores/use-setting-store";
import { Header } from "../header";
import SelectionHeader from "../selection-header";
import useKeyboard from "../../hooks/use-keyboard";
export const Container = ({ children }: PropsWithChildren) => {
const floating = useIsFloatingKeyboard();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const insets = useGlobalSafeAreaInsets();
const keyboard = useKeyboard();
const [height, setHeight] = useState(0);
const windowHeightRef = useRef(Dimensions.get("window").height);
const { height: windowHeight } = useWindowDimensions();
useEffect(() => {
if (windowHeight !== windowHeightRef.current) {
setHeight(0);
windowHeightRef.current = windowHeight;
}
}, [windowHeight]);
return (
<KeyboardAvoidingView
behavior="padding"
enabled={Platform.OS === "ios" && !floating}
<View
style={{
flex: 1,
overflow: "hidden",
paddingTop: Platform.OS === "android" ? 0 : insets.top,
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom,
height: height || "100%",
width: "100%"
}}
onLayout={(event) => {
if (!keyboard.keyboardShown) {
setHeight(event.nativeEvent.layout.height);
}
}}
>
{!introCompleted ? null : (
<>
<SelectionHeader />
<Header />
</>
)}
{children}
</KeyboardAvoidingView>
</View>
);
};

View File

@@ -44,7 +44,7 @@ export default function DelayLayout({
}: IDelayLayoutProps) {
const { colors } = useThemeColors();
const loading = useDelayLayout(
!props.delay || props.delay < 300 ? 300 : props.delay
!props.delay || props.delay < 300 ? 0 : props.delay
);
const Placeholder = placeholder[props.type || "default"];

View File

@@ -17,15 +17,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
import { AddTopicDialog } from "../dialogs/add-topic";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { LoadingDialog } from "../dialogs/loading";
import PDFPreview from "../dialogs/pdf-preview";
import ResultDialog from "../dialogs/result";
import { VaultDialog } from "../dialogs/vault";
import ImagePreview from "../image-preview";
@@ -36,17 +36,16 @@ import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data";
import PDFPreview from "../dialogs/pdf-preview";
import { AppLockPassword } from "../dialogs/applock-password";
const DialogProvider = () => {
const { colors } = useThemeColors();
const loading = useNoteStore((state) => state.loading);
return (
<>
<AppLockPassword />
<LoadingDialog />
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
@@ -58,10 +57,11 @@ const DialogProvider = () => {
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
{loading ? null : <Expiring />}
<Expiring />
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
</>
);
};

View File

@@ -21,11 +21,9 @@ import { eSendEvent } from "../../services/event-manager";
import {
eCloseActionSheet,
eCloseAddNotebookDialog,
eCloseAddTopicDialog,
eCloseMoveNoteDialog,
eOpenActionSheet,
eOpenAddNotebookDialog,
eOpenAddTopicDialog,
eOpenMoveNoteDialog
} from "../../utils/events";
@@ -52,9 +50,3 @@ export const AddNotebookEvent = (notebook) => {
export const HideAddNotebookEvent = (notebook) => {
eSendEvent(eCloseAddNotebookDialog, notebook);
};
export const AddTopicEvent = (topic) => {
eSendEvent(eOpenAddTopicDialog, topic);
};
export const HideAddTopicEvent = (notebook) => {
eSendEvent(eCloseAddTopicDialog, notebook);
};

View File

@@ -17,8 +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/>.
*/
import React, { useEffect, useRef, useState } from "react";
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
import {
ColorValue,
KeyboardAvoidingView,
Modal,
Platform,
@@ -35,6 +36,25 @@ import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { useAppState } from "../../hooks/use-app-state";
export interface BaseDialogProps extends PropsWithChildren {
animation?: "fade" | "none" | "slide" | undefined;
visible: boolean;
onRequestClose?: () => void;
onShow?: () => void;
premium?: boolean;
statusBarTranslucent?: boolean;
transparent?: boolean;
centered?: boolean;
bottom?: boolean;
background?: string | ColorValue;
animated?: boolean;
bounce?: boolean;
closeOnTouch?: boolean;
useSafeArea?: boolean;
avoidKeyboardResize?: boolean;
enableSheetKeyboardHandler?: boolean;
}
const BaseDialog = ({
visible,
onRequestClose,
@@ -46,14 +66,14 @@ const BaseDialog = ({
transparent,
centered = true,
bottom = false,
background = null,
animated = true,
bounce = true,
closeOnTouch = true,
useSafeArea = true,
avoidKeyboardResize = false,
enableSheetKeyboardHandler = false
}) => {
enableSheetKeyboardHandler,
background
}: BaseDialogProps) => {
const floating = useIsFloatingKeyboard();
const appState = useAppState();
const lockEvents = useRef(false);
@@ -67,7 +87,8 @@ const BaseDialog = ({
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.get().appLockMode === "background") {
if (SettingsService.canLockAppInBackground()) {
if (appState === "background") {
setIntervalVisible(false);
if (useUserStore.getState().appLocked) {
@@ -148,7 +169,7 @@ const BaseDialog = ({
]}
>
<TouchableOpacity
onPress={closeOnTouch ? onRequestClose : null}
onPress={closeOnTouch ? onRequestClose : undefined}
style={styles.overlayButton}
/>
{premium}

View File

@@ -25,6 +25,7 @@ import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { getColorLinearShade } from "../../utils/colors";
const DialogButtons = ({
onPressPositive,
@@ -35,7 +36,7 @@ const DialogButtons = ({
doneText,
positiveType
}) => {
const { colors } = useThemeColors();
const { colors, isDark } = useThemeColors();
return (
<View
@@ -46,7 +47,13 @@ const DialogButtons = ({
height: 60,
borderBottomRightRadius: 10,
borderBottomLeftRadius: 10,
paddingHorizontal: 12
paddingHorizontal: 12,
borderTopWidth: 0.7,
borderTopColor: getColorLinearShade(
colors.secondary.background,
0.05,
isDark
)
}
]}
>
@@ -80,7 +87,7 @@ const DialogButtons = ({
onPress={onPressNegative}
fontSize={SIZE.md}
testID={notesnook.ids.default.dialog.no}
type="gray"
type="plain"
bold
title={negativeTitle}
/>

View File

@@ -18,12 +18,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { View } from "react-native";
import { View, ViewProps } from "react-native";
import { DDS } from "../../services/device-detection";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../utils/elevation";
import { getContainerBorder } from "../../utils/colors";
const DialogContainer = ({ width, height, ...restProps }) => {
const DialogContainer = ({
width,
height,
style,
...restProps
}: ViewProps & {
width?: any;
height?: any;
}) => {
const { colors } = useThemeColors();
return (
@@ -35,7 +44,8 @@ const DialogContainer = ({ width, height, ...restProps }) => {
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
paddingTop: 12,
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
}}
/>
);

View File

@@ -22,7 +22,7 @@ import { Text, View, ViewStyle } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { PressableButtonProps } from "../ui/pressable";
import { PressableProps } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -34,7 +34,7 @@ type DialogHeaderProps = {
onPress?: () => void;
loading?: boolean;
title?: string;
type?: PressableButtonProps["type"];
type?: PressableProps["type"];
icon?: string;
};
paragraphColor?: string;
@@ -86,7 +86,9 @@ const DialogHeader = ({
>
{title}{" "}
{titlePart ? (
<Text style={{ color: colors.primary.accent }}>{titlePart}</Text>
<Text style={{ color: colors.primary.accent }}>
{titlePart}
</Text>
) : null}
</Heading>
@@ -101,8 +103,8 @@ const DialogHeader = ({
fontSize={13}
title={button.title}
icon={button.icon}
type={button.type || "grayBg"}
height={25}
type={button.type || "secondary"}
height={30}
/>
) : null}
</View>

View File

@@ -25,12 +25,12 @@ type DialogInfo = {
paragraph?: string;
positiveText?: string;
negativeText?: string;
positivePress?: (value: unknown) => void;
positivePress?: (value: any) => void;
onClose?: () => void;
positiveType?:
| "transparent"
| "gray"
| "grayBg"
| "plain"
| "secondary"
| "accent"
| "inverted"
| "shade"
@@ -41,7 +41,9 @@ type DialogInfo = {
input: boolean;
inputPlaceholder: string;
defaultValue: string;
context: "global" | "local";
// eslint-disable-next-line @typescript-eslint/ban-types
context: "global" | "local" | (string & {});
secureTextEntry?: boolean;
};
export function presentDialog(data: Partial<DialogInfo>): void {

View File

@@ -65,16 +65,6 @@ export const Dialog = ({ context = "global" }) => {
}
});
useEffect(() => {
eSubscribeEvent(eOpenSimpleDialog, show);
eSubscribeEvent(eCloseSimpleDialog, hide);
return () => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, [show]);
const onPressPositive = async () => {
if (dialogInfo.positivePress) {
inputRef.current?.blur();
@@ -102,6 +92,16 @@ export const Dialog = ({ context = "global" }) => {
[context]
);
useEffect(() => {
eSubscribeEvent(eOpenSimpleDialog, show);
eSubscribeEvent(eCloseSimpleDialog, hide);
return () => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, [show]);
const hide = () => {
setChecked(false);
values.current.inputValue = undefined;

View File

@@ -1,190 +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 React, { createRef } from "react";
import { View } from "react-native";
import { useMenuStore } from "../../../stores/use-menu-store";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { db } from "../../../common/database";
import {
eCloseAddTopicDialog,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogContainer from "../../dialog/dialog-container";
import DialogHeader from "../../dialog/dialog-header";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { Toast } from "../../toast";
import { useRelationStore } from "../../../stores/use-relation-store";
export class AddTopicDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
titleFocused: false,
loading: false
};
this.ref = createRef();
this.title;
this.titleRef = createRef();
this.notebook = null;
this.toEdit = null;
}
addNewTopic = async () => {
try {
if (!this.title || this.title?.trim() === "") {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return;
}
if (!this.toEdit) {
await db.notebooks.notebook(this.notebook.id).topics.add(this.title);
} else {
let topic = this.toEdit;
topic.title = this.title;
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
}
this.close();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
});
eSendEvent(eOnTopicSheetUpdate);
useRelationStore.getState().update();
} catch (e) {
console.error(e);
}
};
componentDidMount() {
eSubscribeEvent(eOpenAddTopicDialog, this.open);
eSubscribeEvent(eCloseAddTopicDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenAddTopicDialog, this.open);
eUnSubscribeEvent(eCloseAddTopicDialog, this.close);
}
open = async ({ notebookId, toEdit }) => {
let id = notebookId;
if (id) {
this.notebook = await db.notebooks.notebook(id).data;
}
this.toEdit = toEdit;
if (this.toEdit) {
this.title = this.toEdit.title;
}
this.setState({
visible: true
});
};
close = () => {
this.title = null;
this.notebook = null;
this.toEdit = null;
this.setState({
visible: false
});
};
render() {
const { visible } = this.state;
if (!visible) return null;
return (
<BaseDialog
onShow={async () => {
if (this.toEdit) {
this.titleRef.current?.setNativeProps({
text: this.toEdit.title
});
}
this.ref.current?.animateNextTransition();
await sleep(300);
this.titleRef.current?.focus();
}}
bounce={false}
statusBarTranslucent={false}
visible={true}
onRequestClose={this.close}
>
<DialogContainer>
<DialogHeader
icon="book-outline"
title={this.toEdit ? "Edit topic" : "New topic"}
paragraph={
this.toEdit
? "Edit title of the topic"
: "Add a new topic in " + this.notebook.title
}
padding={12}
/>
<Seperator half />
<View
style={{
paddingHorizontal: 12,
zIndex: 10
}}
>
<Input
fwdRef={this.titleRef}
testID="input-title"
onChangeText={(value) => {
this.title = value;
}}
blurOnSubmit={false}
placeholder="Enter title"
onSubmit={() => this.addNewTopic()}
returnKeyLabel="Done"
returnKeyType="done"
/>
</View>
<DialogButtons
positiveTitle={this.toEdit ? "Save" : "Add"}
onPressNegative={() => this.close()}
onPressPositive={() => this.addNewTopic()}
/>
</DialogContainer>
<Toast context="local" />
</BaseDialog>
);
}
}

View File

@@ -0,0 +1,358 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import {
clearAppLockVerificationCipher,
setAppLockVerificationCipher,
validateAppLockPassword
} from "../../../common/database/encryption";
import BiometicService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseAppLocKPasswordDailog,
eOpenAppLockPasswordDialog
} from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
export const AppLockPassword = () => {
const { colors } = useThemeColors();
const [mode, setMode] = useState<"create" | "change" | "remove">("create");
const [keyboardType, setKeyboardType] = useState<"pin" | "password">(
useSettingStore.getState().settings.applockKeyboardType === "default"
? "password"
: "pin"
);
const [visible, setVisible] = useState(false);
const currentPasswordInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
const values = useRef<{
currentPassword?: string;
password?: string;
confirmPassword?: string;
}>({});
const [secureTextEntry, setSecureTextEntry] = useState(true);
useEffect(() => {
const subs = [
eSubscribeEvent(
eOpenAppLockPasswordDialog,
(mode: "create" | "change" | "remove") => {
setMode(mode);
setVisible(true);
}
),
eSubscribeEvent(eCloseAppLocKPasswordDailog, () => {
values.current = {};
setVisible(false);
})
];
return () => {
subs.forEach((sub) => sub?.unsubscribe());
};
}, []);
const close = () => {
values.current = {};
setVisible(false);
};
return !visible ? null : (
<BaseDialog
onShow={async () => {
await sleep(100);
if (mode !== "change") {
passwordInputRef.current?.focus();
} else {
currentPasswordInputRef.current?.focus();
}
}}
statusBarTranslucent={false}
onRequestClose={close}
visible={visible}
>
<View
style={{
...getElevationStyle(10),
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
}}
>
<DialogHeader
title={
mode === "change"
? `Change app lock ${keyboardType}`
: mode === "remove"
? `Remove app lock ${keyboardType}`
: `Set up app lock ${keyboardType}`
}
paragraph={
mode === "change"
? `Change app lock ${keyboardType}`
: mode === "remove"
? `Remove app lock ${keyboardType}`
: `Set up a custom app lock ${keyboardType} to unlock the app`
}
icon="shield"
padding={12}
/>
<Seperator half />
<View
style={{
paddingHorizontal: 12
}}
>
{mode === "change" ? (
<Input
fwdRef={currentPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.currentPassword = value;
}}
onSubmit={() => {
passwordInputRef.current?.focus();
}}
defaultValue={values.current.currentPassword}
autoComplete="password"
returnKeyLabel="Next"
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
returnKeyType="next"
secureTextEntry={secureTextEntry}
placeholder={`Current ${keyboardType}`}
/>
) : null}
<Input
fwdRef={passwordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.password = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.password}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
autoComplete="password"
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
returnKeyType={mode !== "remove" ? "next" : "done"}
secureTextEntry={secureTextEntry}
buttonLeft={
<IconButton
name={keyboardType === "password" ? "numeric" : "keyboard"}
onPress={() => {
setKeyboardType(
keyboardType === "password" ? "pin" : "password"
);
setSecureTextEntry(false);
setImmediate(() => {
setSecureTextEntry(true);
});
}}
style={{
width: 25,
height: 25,
marginRight: 5
}}
size={SIZE.lg}
/>
}
placeholder={
mode === "change"
? `New ${keyboardType}`
: `${keyboardType === "pin" ? "Pin" : "Password"}`
}
/>
{mode !== "remove" ? (
<Input
fwdRef={confirmPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.confirmPassword = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.confirmPassword}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
customValidator={() => values.current.password || ""}
validationType="confirmPassword"
autoComplete="password"
returnKeyLabel="Done"
returnKeyType="done"
secureTextEntry={secureTextEntry}
placeholder={`Confirm ${keyboardType}`}
/>
) : null}
</View>
<DialogButtons
onPressNegative={close}
onPressPositive={async () => {
if (mode === "create") {
if (!values.current.password || !values.current.confirmPassword) {
ToastManager.error(
new Error("All inputs are required"),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(
`${
keyboardType === "pin" ? "Pin" : "Password"
} does not match`
),
undefined,
"local"
);
return;
}
await setAppLockVerificationCipher(values.current.password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
} else if (mode === "change") {
if (
!values.current.currentPassword ||
!values.current.password ||
!values.current.confirmPassword
) {
ToastManager.error(
new Error("All inputs are required"),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(
`${
keyboardType === "pin" ? "Pin" : "Password"
} does not match`
),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.currentPassword
);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
),
undefined,
"local"
);
return;
}
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(values.current.password);
} else if (mode === "remove") {
if (!values.current.password) {
ToastManager.error(
new Error("All inputs are required"),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.password
);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
),
"local"
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometicService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
ToastManager.show({
message: "App lock disabled",
type: "success"
});
}
}
SettingsService.setProperty(
"applockKeyboardType",
keyboardType === "password" ? "default" : "numeric"
);
close();
}}
positiveTitle="Save"
negativeTitle="Cancel"
positiveType="transparent"
loading={false}
doneText=""
/>
</View>
<Toast context="local" />
</BaseDialog>
);
};
AppLockPassword.present = (mode: "create" | "change" | "remove") => {
eSendEvent(eOpenAppLockPasswordDialog, mode);
};

View File

@@ -0,0 +1,181 @@
/*
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 React, { useState } from "react";
import { Image, ScrollView, TouchableOpacity, View } from "react-native";
import { ImagePickerResponse } from "react-native-image-picker";
import { useThemeColors } from "../../../../../../packages/theme/dist";
import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Notice } from "../../ui/notice";
import Paragraph from "../../ui/typography/paragraph";
export default function AttachImage({
response,
onAttach,
close
}: {
response: ImagePickerResponse;
onAttach: ({ compress }: { compress: boolean }) => void;
close: ((ctx?: string | undefined) => void) | undefined;
}) {
const { colors } = useThemeColors();
const [compress, setCompress] = useState(true);
return (
<View
style={{
alignItems: "center",
paddingHorizontal: 12
}}
>
<View
style={{
backgroundColor: colors.secondary.background,
marginBottom: 12,
height: 140,
width: "100%",
borderRadius: 10,
padding: 5,
paddingHorizontal: 12
}}
>
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
Attaching {response.assets?.length} image(s):
</Paragraph>
<ScrollView horizontal>
{response.assets?.map((item) => (
<TouchableOpacity key={item.fileName} activeOpacity={0.9}>
<Image
source={{
uri: item.uri
}}
style={{
width: 100,
height: 100,
borderRadius: 5,
backgroundColor: "black",
marginRight: 6
}}
resizeMode="cover"
/>
</TouchableOpacity>
))}
</ScrollView>
</View>
<TouchableOpacity
activeOpacity={1}
style={{
flexDirection: "row",
alignSelf: "center",
marginBottom: 12,
alignItems: "center",
width: "100%"
}}
onPress={() => {
setCompress(!compress);
}}
>
<IconButton
size={SIZE.lg}
name={compress ? "checkbox-marked" : "checkbox-blank-outline"}
color={compress ? colors.primary.accent : colors.primary.icon}
style={{
width: 25,
height: 25
}}
onPress={() => {
setCompress(!compress);
}}
/>
<Paragraph
style={{
flexShrink: 1,
marginLeft: 3
}}
size={SIZE.sm}
>
Compress (recommended)
</Paragraph>
</TouchableOpacity>
{!compress ? (
<Notice
type="alert"
text="Images uploaded without compression are slow to load and take more bandwidth. We recommend compressing images unless you need image in original quality."
size="small"
style={{
width: "100%",
marginBottom: 12
}}
/>
) : (
<Notice
type="information"
text="Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases."
size="small"
style={{
width: "100%",
marginBottom: 12
}}
/>
)}
<Button
title={`${
(response.assets?.length || 0) > 1 ? "Attach Images" : "Attach Image"
}`}
type="accent"
width="100%"
onPress={() => {
onAttach({
compress
});
}}
/>
</View>
);
}
AttachImage.present = (response: ImagePickerResponse) => {
return new Promise((resolve) => {
let resolved = false;
presentSheet({
component: (ref, close, update) => (
<AttachImage
response={response}
close={close}
onAttach={(result) => {
resolved = true;
console.log("closing");
resolve(result);
close?.();
}}
/>
),
onClose: () => {
if (resolved) return;
resolve(undefined);
}
});
});
};

View File

@@ -0,0 +1,196 @@
/*
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 { Color } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import ColorPickerWheel from "react-native-wheel-color-picker";
import { db } from "../../../common/database";
import { useStoredRef } from "../../../hooks/use-stored-ref";
import { ToastManager } from "../../../services/event-manager";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
const HEX_COLOR_REGEX_ALPHA =
/^#(?:(?:[\da-fA-F]{3}){1,2}|(?:[\da-fA-F]{4}){1,2})$/;
const ColorPicker = ({
visible,
setVisible,
onColorAdded
}: {
visible: boolean;
setVisible: (value: boolean) => void;
onColorAdded: (color: Color) => void;
}) => {
const [selectedColor, setSelectedColor] = useState<string>();
const { colors } = useThemeColors();
const inputRef = useRef<TextInput>(null);
const title = useRef<string>();
const colorRef = useStoredRef("color-ref", "#f0f0f0");
return (
<BaseDialog
visible={visible}
onRequestClose={() => {
title.current = undefined;
setVisible(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}}
statusBarTranslucent={false}
centered
>
<Toast context="color-picker" />
<DialogContainer
style={{
paddingTop: 0,
maxHeight: 600
}}
>
<View>
<View
style={{
width: "100%",
height: 250,
backgroundColor: selectedColor,
borderRadius: 10
}}
>
<ColorPickerWheel
onColorChangeComplete={(color) => {
if (color === selectedColor) return;
colorRef.current = color;
setSelectedColor(color);
inputRef.current?.setNativeProps({
placeholder: color,
text: color
});
}}
useNativeLayout={true}
autoResetSlider={true}
color={colorRef.current}
sliderSize={30}
thumbSize={25}
shadeWheelThumb
shadeSliderThumb={true}
swatchesOnly={false}
swatches={false}
useNativeDriver={true}
/>
</View>
<View
style={{
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingTop: 10,
columnGap: 10,
marginBottom: 10
}}
>
<Input
fwdRef={inputRef}
placeholder="#f0f0f0"
marginBottom={0}
onChangeText={(value) => {
if (HEX_COLOR_REGEX_ALPHA.test(value)) {
colorRef.current = value;
setSelectedColor(value);
}
}}
defaultValue={colorRef.current}
/>
<Pressable
type="accent"
accentColor={selectedColor || colors.secondary.background}
style={{
width: 45,
height: 45,
borderRadius: 100,
justifyContent: "center",
alignItems: "center"
}}
/>
</View>
<Input
marginBottom={10}
onChangeText={(value) => {
title.current = value;
}}
defaultValue={title.current}
placeholder={title.current || "Color title"}
/>
<Button
title="Add color"
onPress={async () => {
if (!selectedColor)
return ToastManager.error(
new Error("Select a color"),
undefined,
"color-picker"
);
if (!title.current)
return ToastManager.error(
new Error("Enter a title for the color")
);
const exists = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", selectedColor)])
);
if (exists)
return ToastManager.error(
new Error(`Color #${selectedColor} already exists`)
);
const id = await db.colors.add({
title: title.current,
colorCode: selectedColor
});
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
setVisible(false);
const color = await db.colors.color(id);
if (color) {
onColorAdded?.(color);
}
}}
type={selectedColor ? "secondaryAccented" : "secondary"}
width="100%"
/>
</View>
</View>
</DialogContainer>
</BaseDialog>
);
};
export default ColorPicker;

View File

@@ -17,15 +17,22 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import { GroupHeader, Item, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
} from "react";
import { ActivityIndicator, FlatList, ScrollView, View } from "react-native";
import { DDS } from "../../../services/device-detection";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useMessageStore } from "../../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseJumpToDialog,
@@ -34,29 +41,51 @@ import {
} from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import BaseDialog from "../../dialog/base-dialog";
import { PressableButton } from "../../ui/pressable";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { useCallback } from "react";
const offsets = [];
let timeout = null;
const JumpToSectionDialog = ({ scrollRef, data, type }) => {
const JumpToSectionDialog = () => {
const scrollRef = useRef<RefObject<FlatList>>();
const [data, setData] = useState<VirtualizedGrouping<Item>>();
const { colors } = useThemeColors();
const notes = data;
const [visible, setVisible] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const currentScrollPosition = useRef(0);
const [loading, setLoading] = useState(false);
const onPress = (item) => {
let ind = notes.findIndex(
(i) => i.title === item.title && i.type === "header"
);
scrollRef.current?.scrollToIndex({
index: ind,
const [groups, setGroups] = useState<
{
index: number;
group: GroupHeader;
}[]
>();
const offsets = useRef<number[]>([]);
const onPress = (item: { index: number; group: GroupHeader }) => {
scrollRef.current?.current?.scrollToIndex({
index: item.index,
animated: true
});
close();
};
const open = useCallback(
({
data,
ref
}: {
data: VirtualizedGrouping<Item>;
ref: RefObject<FlatList>;
}) => {
setLoading(true);
setData(data);
scrollRef.current = ref;
setVisible(true);
},
[]
);
useEffect(() => {
eSubscribeEvent(eOpenJumpToDialog, open);
eSubscribeEvent(eCloseJumpToDialog, close);
@@ -69,54 +98,46 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
};
}, [open]);
const onScroll = (data) => {
let y = data.y;
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
timeout = setTimeout(() => {
let index = offsets.findIndex((o, i) => o <= y && offsets[i + 1] > y);
setCurrentIndex(index || 0);
}, 200);
const onScroll = (data: { x: number; y: number }) => {
currentScrollPosition.current = data.y;
};
const open = useCallback(
(_type) => {
if (_type !== type) return;
setVisible(true);
},
[type]
);
const close = () => {
setVisible(false);
};
useEffect(() => {
loadOffsets();
}, [loadOffsets, notes]);
const loadOffsets = useCallback(() => {
notes
.filter((i) => i.type === "header")
.map((item, index) => {
const loadGroupsAndOffsets = useCallback(() => {
notes?.groups?.().then((groups) => {
setGroups(groups);
offsets.current = [];
groups.map((item, index) => {
let offset = 35 * index;
let ind = notes.findIndex(
(i) => i.title === item.title && i.type === "header"
);
let messageState = useMessageStore.getState().message;
let msgOffset = messageState?.visible ? 60 : 10;
ind = ind + 1;
ind = ind - (index + 1);
offset = offset + ind * 100 + msgOffset;
offsets.push(offset);
let groupIndex = item.index;
const messageState = useMessageStore.getState().message;
const msgOffset = messageState?.visible ? 60 : 10;
groupIndex = groupIndex + 1;
groupIndex = groupIndex - (index + 1);
offset = offset + groupIndex * 100 + msgOffset;
offsets.current.push(offset);
});
const index = offsets.current?.findIndex((o, i) => {
return (
o <= currentScrollPosition.current + 100 &&
offsets.current[i + 1] - 100 > currentScrollPosition.current
);
});
setCurrentIndex(index < 0 ? 0 : index);
setLoading(false);
});
}, [notes]);
return !visible ? null : (
<BaseDialog
onShow={() => {
loadOffsets();
loadGroupsAndOffsets();
}}
onRequestClose={close}
visible={true}
@@ -135,35 +156,42 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
paddingTop: 30
}}
>
<ScrollView
style={{
maxHeight: "100%"
}}
>
<View
{loading ? (
<ActivityIndicator
size={SIZE.lg}
color={colors.primary.accent}
style={{
flexDirection: "row",
flexWrap: "wrap",
alignSelf: "center",
justifyContent: "center",
paddingBottom: 20
marginBottom: 20
}}
/>
) : (
<ScrollView
style={{
maxHeight: "100%"
}}
>
{notes
.filter((i) => i.type === "header")
.map((item, index) => {
return item.title ? (
<PressableButton
key={item.title}
onPress={() => onPress(item, index)}
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
alignSelf: "center",
justifyContent: "center",
paddingBottom: 20
}}
>
{groups?.map((item, index) => {
return (
<Pressable
key={item.group.id}
onPress={() => onPress(item)}
type={currentIndex === index ? "accent" : "transparent"}
style={{
minWidth: "20%",
width: null,
paddingHorizontal: 12,
margin: 5,
borderRadius: 100,
height: 25,
height: 30,
marginVertical: 10
}}
>
@@ -171,20 +199,21 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
size={SIZE.sm}
color={
currentIndex === index
? colors.selected.accent
: colors.primary.accent
? colors.static.white
: colors.primary.paragraph
}
style={{
textAlign: "center"
}}
>
{item.title}
{item.group.title}
</Paragraph>
</PressableButton>
) : null;
</Pressable>
);
})}
</View>
</ScrollView>
</View>
</ScrollView>
)}
</View>
</BaseDialog>
);

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 React, { useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import {
addOrientationListener,
@@ -85,7 +85,7 @@ const PDFPreview = () => {
return () => {
eUnSubscribeEvent("PDFPreview", open);
};
}, []);
}, [open]);
const onOrientationChange = (o) => {
if (o.includes("LANDSCAPE")) {
@@ -102,23 +102,26 @@ const PDFPreview = () => {
};
}, []);
const open = async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.metadata.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
};
const open = useCallback(
async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
},
[snapshot]
);
const close = () => {
setPDFSource(null);
@@ -130,7 +133,7 @@ const PDFPreview = () => {
if (error?.message === "Password required or incorrect password.") {
await sleep(300);
presentDialog({
context: attachment?.metadata?.hash,
context: attachment?.hash,
input: true,
inputPlaceholder: "Enter password",
positiveText: "Unlock",
@@ -151,8 +154,8 @@ const PDFPreview = () => {
return (
visible && (
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
<SheetProvider context={attachment?.metadata?.hash} />
<Dialog context={attachment?.metadata?.hash} />
<SheetProvider context={attachment?.hash} />
<Dialog context={attachment?.hash} />
<View
style={{
@@ -208,7 +211,7 @@ const PDFPreview = () => {
color={colors.static.white}
name="arrow-left"
onPress={close}
customStyle={{
style={{
marginRight: 12
}}
size={SIZE.xxl}
@@ -261,7 +264,7 @@ const PDFPreview = () => {
color={colors.static.white}
name="download"
onPress={() => {
downloadAttachment(attachment.metadata.hash, false);
downloadAttachment(attachment.hash, false);
}}
/>
</View>

View File

@@ -23,28 +23,26 @@ import { InteractionManager, View } from "react-native";
import Share from "react-native-share";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { editorController } from "../../../screens/editor/tiptap/utils";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
eUnSubscribeEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search";
import { convertNoteToText } from "../../../utils/note-to-text";
import { getElevationStyle } from "../../../utils/elevation";
import {
eClearEditor,
eCloseActionSheet,
eCloseVaultDialog,
eOnLoadNote,
eOpenVaultDialog
eOpenVaultDialog,
eUpdateNoteInEditor
} from "../../../utils/events";
import { deleteItems } from "../../../utils/functions";
import { tabBarRef } from "../../../utils/global-refs";
import { convertNoteToText } from "../../../utils/note-to-text";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
@@ -55,10 +53,6 @@ import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
let Keychain;
const passInputRef = createRef();
const confirmPassRef = createRef();
const changePassInputRef = createRef();
export class VaultDialog extends Component {
constructor(props) {
super(props);
@@ -86,8 +80,14 @@ export class VaultDialog extends Component {
description: null,
clearVault: false,
deleteVault: false,
deleteAll: false
deleteAll: false,
noteLocked: false
};
this.passInputRef = createRef();
this.confirmPassRef = createRef();
this.changePassInputRef = createRef();
this.password = null;
this.confirmPassword = null;
this.newPassword = null;
@@ -99,7 +99,7 @@ export class VaultDialog extends Component {
? "Revoke Vault Fingerprint Unlock"
: this.state.changePassword
? "Change Vault Password"
: this.state.note.locked
: this.state.noteLocked
? this.state.deleteNote
? "Delete note"
: this.state.share
@@ -120,7 +120,7 @@ export class VaultDialog extends Component {
? "Setup a new password for the vault."
: this.state.permanant
? "Enter password to remove note from vault."
: this.state.note.locked
: this.state.noteLocked
? this.state.deleteNote
? "Unlock note to delete it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.share
@@ -148,9 +148,6 @@ export class VaultDialog extends Component {
* @param {import('../../../services/event-manager').vaultType} data
*/
open = async (data) => {
if (!Keychain) {
Keychain = require("react-native-keychain");
}
let biometry = await BiometricService.isBiometryAvailable();
let available = false;
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
@@ -177,7 +174,8 @@ export class VaultDialog extends Component {
title: data.title,
description: data.description,
clearVault: data.clearVault,
deleteVault: data.deleteVault
deleteVault: data.deleteVault,
noteLocked: data.item && (await db.vaults.itemExists(data.item))
});
if (
@@ -199,7 +197,7 @@ export class VaultDialog extends Component {
close = () => {
if (this.state.loading) {
ToastEvent.show({
ToastManager.show({
heading: this.state.title,
message: "Please wait and do not close the app.",
type: "success",
@@ -212,7 +210,6 @@ export class VaultDialog extends Component {
this.password = null;
this.confirmPassword = null;
SearchService.updateAndSearch();
this.setState({
visible: false,
note: {},
@@ -236,7 +233,7 @@ export class VaultDialog extends Component {
if (this.state.loading) return;
if (!this.password) {
ToastEvent.show({
ToastManager.show({
heading: "Password not entered",
message: "Enter a password for the vault and try again.",
type: "error",
@@ -244,20 +241,10 @@ export class VaultDialog extends Component {
});
return;
}
if (this.password && this.password.length < 3) {
ToastEvent.show({
heading: "Password too short",
message: "Password must be longer than 3 characters.",
type: "error",
context: "local"
});
return;
}
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
ToastEvent.show({
ToastManager.show({
heading: "Passwords do not match",
type: "error",
context: "local"
@@ -283,7 +270,7 @@ export class VaultDialog extends Component {
if (this.state.biometricUnlock) {
this._enrollFingerprint(this.newPassword);
}
ToastEvent.show({
ToastManager.show({
heading: "Vault password updated successfully",
type: "success",
context: "global"
@@ -295,17 +282,19 @@ export class VaultDialog extends Component {
loading: false
});
if (e.message === db.vault.ERRORS.wrongPassword) {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
context: "local"
});
} else {
ToastManager.error(e);
}
});
} else if (this.state.locked) {
if (!this.password || this.password.trim() === 0) {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
@@ -316,7 +305,7 @@ export class VaultDialog extends Component {
});
return;
}
if (this.state.note.locked) {
if (this.state.noteLocked) {
await this._unlockNote();
} else {
db.vault
@@ -348,14 +337,33 @@ export class VaultDialog extends Component {
let verified = await db.user.verifyPassword(this.password);
if (!(await db.user.getUser())) verified = true;
if (verified) {
let noteIds = [];
if (this.state.deleteAll) {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault, "note").get();
noteIds = relations.map((item) => item.toId);
}
await db.vault.delete(this.state.deleteAll);
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
eSendEvent("vaultUpdated");
this.setState({
loading: false
});
this.close();
} else {
ToastEvent.show({
ToastManager.show({
heading: "Account password incorrect",
message: "Please enter correct password for your account.",
type: "error",
@@ -375,14 +383,29 @@ export class VaultDialog extends Component {
loading: true
});
try {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault, "note").get();
const noteIds = relations.map((item) => item.toId);
await db.vault.clear(this.password);
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
this.setState({
loading: false
});
this.close();
eSendEvent("vaultUpdated");
} catch (e) {
ToastEvent.show({
ToastManager.show({
heading: "Vault password incorrect",
message: "Please enter correct password to clear vault.",
type: "error",
@@ -396,7 +419,7 @@ export class VaultDialog extends Component {
async _lockNote() {
if (!this.password || this.password.trim() === 0) {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
type: "error",
context: "local"
@@ -404,11 +427,12 @@ export class VaultDialog extends Component {
return;
} else {
await db.vault.add(this.state.note.id);
if (this.state.note.id === editorController.current?.note?.id) {
eSendEvent(eClearEditor);
}
console.log("update note event...");
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.close();
ToastEvent.show({
ToastManager.show({
message: "Note locked successfully",
type: "error",
context: "local"
@@ -421,7 +445,7 @@ export class VaultDialog extends Component {
async _unlockNote() {
if (!this.password || this.password.trim() === 0) {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
@@ -459,7 +483,7 @@ export class VaultDialog extends Component {
async _deleteNote() {
try {
await db.vault.remove(this.state.note.id, this.password);
await deleteItems(this.state.note);
await deleteItems([this.state.note.id], "note");
this.close();
} catch (e) {
this._takeErrorAction(e);
@@ -479,7 +503,7 @@ export class VaultDialog extends Component {
loading: false
});
eSendEvent("vaultUpdated");
ToastEvent.show({
ToastManager.show({
heading: "Biometric unlocking enabled!",
message: "Now you can unlock notes in vault with biometrics.",
type: "success",
@@ -488,7 +512,7 @@ export class VaultDialog extends Component {
this.close();
} catch (e) {
this.close();
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
message:
"Please enter the correct vault password to enable biometrics.",
@@ -511,13 +535,11 @@ export class VaultDialog extends Component {
}
if (this.state.note?.id) {
await db.vault.add(this.state.note.id);
if (this.state.note.id === editorController.current?.note?.id) {
eSendEvent(eClearEditor);
}
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.setState({
loading: false
});
ToastEvent.show({
ToastManager.show({
heading: "Note added to vault",
type: "success",
context: "global"
@@ -525,7 +547,7 @@ export class VaultDialog extends Component {
this.close();
} else {
eSendEvent("vaultUpdated");
ToastEvent.show({
ToastManager.show({
heading: "Vault created successfully",
type: "success",
context: "global"
@@ -538,11 +560,12 @@ export class VaultDialog extends Component {
db.vault
.remove(this.state.note.id, this.password)
.then(() => {
ToastEvent.show({
ToastManager.show({
heading: "Note permanently unlocked.",
type: "success",
context: "global"
});
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.close();
})
.catch((e) => {
@@ -553,7 +576,9 @@ export class VaultDialog extends Component {
_openInEditor(note) {
this.close();
InteractionManager.runAfterInteractions(async () => {
eSendEvent(eOnLoadNote, note);
eSendEvent(eOnLoadNote, {
item: note
});
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
@@ -562,7 +587,7 @@ export class VaultDialog extends Component {
async _copyNote(note) {
Clipboard.setString(await convertNoteToText(note));
ToastEvent.show({
ToastManager.show({
heading: "Note copied",
type: "success",
message: "Note has been copied to clipboard!",
@@ -594,7 +619,7 @@ export class VaultDialog extends Component {
visible: true
});
setTimeout(() => {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
type: "error",
context: "local"
@@ -609,13 +634,13 @@ export class VaultDialog extends Component {
try {
await BiometricService.resetCredentials();
eSendEvent("vaultUpdated");
ToastEvent.show({
ToastManager.show({
heading: "Biometric unlocking disabled!",
type: "success",
context: "global"
});
} catch (e) {
ToastEvent.show({
ToastManager.show({
heading: "Failed to disable Biometric unlocking.",
message: e.message,
type: "success",
@@ -667,7 +692,8 @@ export class VaultDialog extends Component {
<BaseDialog
onShow={async () => {
await sleep(100);
passInputRef.current?.focus();
this.passInputRef.current?.focus();
}}
statusBarTranslucent={false}
onRequestClose={this.close}
@@ -702,7 +728,7 @@ export class VaultDialog extends Component {
!this.state.revokeFingerprintAccess ? (
<>
<Input
fwdRef={passInputRef}
fwdRef={this.passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
@@ -719,7 +745,7 @@ export class VaultDialog extends Component {
}
onSubmit={() => {
changePassword
? changePassInputRef.current?.focus()
? this.confirmPassRef.current?.focus()
: this.onPress;
}}
autoComplete="password"
@@ -771,7 +797,7 @@ export class VaultDialog extends Component {
<>
<Seperator half />
<Input
ref={changePassInputRef}
ref={this.confirmPassRef}
editable={!loading}
testID={notesnook.ids.dialogs.vault.changePwd}
autoCapitalize="none"
@@ -791,7 +817,7 @@ export class VaultDialog extends Component {
{!novault ? (
<View>
<Input
fwdRef={passInputRef}
fwdRef={this.passInputRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
@@ -802,13 +828,13 @@ export class VaultDialog extends Component {
returnKeyType="next"
secureTextEntry
onSubmit={() => {
confirmPassRef.current?.focus();
this.confirmPassRef.current?.focus();
}}
placeholder="Password"
/>
<Input
fwdRef={confirmPassRef}
fwdRef={this.confirmPassRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwdAlt}
secureTextEntry
@@ -865,10 +891,10 @@ export class VaultDialog extends Component {
title="Biometric unlocking"
iconColor={
this.state.biometricUnlock
? colors.selected.icon
? colors.selected.accent
: colors.primary.icon
}
type={this.state.biometricUnlock ? "transparent" : "selected"}
type={this.state.biometricUnlock ? "transparent" : "plain"}
/>
) : null}
</View>
@@ -891,7 +917,7 @@ export class VaultDialog extends Component {
? "Revoke"
: changePassword
? "Change"
: note.locked
: this.state.noteLocked
? deleteNote
? "Delete"
: share

View File

@@ -17,42 +17,73 @@ 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 { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { Platform, StyleSheet, View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { SearchBar } from "../../screens/search/search-bar";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import useNavigationStore from "../../stores/use-navigation-store";
import useNavigationStore, {
RouteName
} from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
const _Header = () => {
type HeaderRightButton = {
title: string;
onPress: () => void;
};
export const Header = ({
renderedInRoute,
onLeftMenuButtonPress,
title,
titleHiddenOnRender,
headerRightButtons,
id,
accentColor,
isBeta,
canGoBack,
onPressDefaultRightButton,
hasSearch,
onSearch
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute: RouteName;
id?: string;
title: string;
headerRightButtons?: HeaderRightButton[];
titleHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
canGoBack?: boolean;
onPressDefaultRightButton?: () => void;
hasSearch?: boolean;
onSearch?: () => void;
}) => {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [hide, setHide] = useState(true);
const [borderHidden, setBorderHidden] = useState(true);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const currentScreen = useNavigationStore(
(state) => state.currentScreen?.name
);
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
const onScroll = useCallback(
(data: { x: number; y: number }) => {
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
if (data.y > 150) {
if (!hide) return;
setHide(false);
if (!borderHidden) return;
setBorderHidden(false);
} else {
if (hide) return;
setHide(true);
if (borderHidden) return;
setBorderHidden(true);
}
},
[hide]
[borderHidden, id, renderedInRoute]
);
useEffect(() => {
@@ -60,9 +91,9 @@ const _Header = () => {
return () => {
eUnSubscribeEvent(eScrollEvent, onScroll);
};
}, [hide, onScroll]);
}, [borderHidden, onScroll]);
return selectionMode ? null : (
return selectionMode && isFocused ? null : (
<>
<View
style={[
@@ -72,29 +103,42 @@ const _Header = () => {
backgroundColor: colors.primary.background,
overflow: "hidden",
borderBottomWidth: 1,
borderBottomColor: hide
borderBottomColor: borderHidden
? "transparent"
: colors.secondary.background,
justifyContent: "space-between"
}
]}
>
{currentScreen === "Search" ? (
<SearchBar />
) : (
<>
<View style={styles.leftBtnContainer}>
<LeftMenus />
<Title />
</View>
<RightMenus />
</>
)}
<>
<View style={styles.leftBtnContainer}>
<LeftMenus
canGoBack={canGoBack}
onLeftButtonPress={onLeftMenuButtonPress}
/>
<Title
isHiddenOnRender={titleHiddenOnRender}
renderedInRoute={renderedInRoute}
id={id}
accentColor={accentColor}
title={title}
isBeta={isBeta}
/>
</View>
<RightMenus
renderedInRoute={renderedInRoute}
id={id}
headerRightButtons={headerRightButtons}
onPressDefaultRightButton={onPressDefaultRightButton}
search={hasSearch}
onSearch={onSearch}
/>
</>
</View>
</>
);
};
export const Header = React.memo(_Header, () => true);
const styles = StyleSheet.create({
container: {

View File

@@ -17,23 +17,29 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { notesnook } from "../../../e2e/test.ids";
import { DDS } from "../../services/device-detection";
import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { tabBarRef } from "../../utils/global-refs";
import { IconButton } from "../ui/icon-button";
export const LeftMenus = () => {
export const LeftMenus = ({
canGoBack,
onLeftButtonPress
}: {
canGoBack?: boolean;
onLeftButtonPress?: () => void;
}) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const canGoBack = useNavigationStore((state) => state.canGoBack);
const isTablet = deviceMode === "tablet";
const onLeftButtonPress = () => {
const _onLeftButtonPress = () => {
if (onLeftButtonPress) return onLeftButtonPress();
if (!canGoBack) {
if (tabBarRef.current?.isDrawerOpen()) {
Navigation.closeDrawer();
@@ -48,27 +54,23 @@ export const LeftMenus = () => {
return isTablet && !canGoBack ? null : (
<IconButton
testID={notesnook.ids.default.header.buttons.left}
customStyle={{
style={{
justifyContent: "center",
alignItems: "center",
height: 40,
width: 40,
borderRadius: 100,
marginLeft: -5,
marginRight: DDS.isLargeTablet() ? 10 : 7
}}
left={40}
top={40}
right={DDS.isLargeTablet() ? 10 : 10}
onPress={onLeftButtonPress}
onPress={_onLeftButtonPress}
onLongPress={() => {
Navigation.popToTop();
}}
name={canGoBack ? "arrow-left" : "menu"}
color={colors.primary.paragraph}
iconStyle={{
marginLeft: canGoBack ? -5 : 0
}}
/>
);
};

View File

@@ -20,55 +20,56 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useRef } from "react";
import { Platform, StyleSheet, View } from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import Menu from "react-native-reanimated-material-menu";
import { notesnook } from "../../../e2e/test.ids";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import {
HeaderRightButton,
RouteName
} from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
export const RightMenus = () => {
export const RightMenus = ({
headerRightButtons,
renderedInRoute,
id,
onPressDefaultRightButton,
search,
onSearch
}: {
headerRightButtons?: HeaderRightButton[];
renderedInRoute: RouteName;
id?: string;
onPressDefaultRightButton?: () => void;
search?: boolean;
onSearch?: () => void;
}) => {
const { colors } = useThemeColors();
const { colors: contextMenuColors } = useThemeColors("contextMenu");
const deviceMode = useSettingStore((state) => state.deviceMode);
const buttons = useNavigationStore((state) => state.headerRightButtons);
const currentScreen = useNavigationStore((state) => state.currentScreen.name);
const buttonAction = useNavigationStore((state) => state.buttonAction);
const menuRef = useRef<{
show: () => void;
hide: () => void;
}>();
const menuRef = useRef<Menu>(null);
return (
<View style={styles.rightBtnContainer}>
{!currentScreen.startsWith("Settings") ? (
{search ? (
<IconButton
onPress={async () => {
SearchService.prepareSearch();
Navigation.navigate(
{
name: "Search"
},
{}
);
}}
onPress={onSearch}
testID="icon-search"
name="magnify"
color={colors.primary.paragraph}
customStyle={styles.rightBtn}
style={styles.rightBtn}
/>
) : null}
{deviceMode !== "mobile" ? (
<Button
onPress={buttonAction}
onPress={onPressDefaultRightButton}
testID={notesnook.ids.default.addBtn}
icon={currentScreen === "Trash" ? "delete" : "plus"}
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
iconSize={SIZE.xl}
type="shade"
hitSlop={{
@@ -89,7 +90,7 @@ export const RightMenus = () => {
/>
) : null}
{buttons && buttons.length > 0 ? (
{headerRightButtons && headerRightButtons.length > 0 ? (
<Menu
ref={menuRef}
animationDuration={200}
@@ -107,18 +108,18 @@ export const RightMenus = () => {
}}
name="dots-vertical"
color={colors.primary.paragraph}
customStyle={styles.rightBtn}
style={styles.rightBtn}
/>
}
>
{buttons.map((item) => (
{headerRightButtons.map((item) => (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0
}}
type="gray"
type="plain"
buttonType={{
text: contextMenuColors.primary.paragraph
}}

View File

@@ -20,103 +20,76 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native";
import { db } from "../../common/database";
import Notebook from "../../screens/notebook";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import useNavigationStore from "../../stores/use-navigation-store";
import { eScrollEvent } from "../../utils/events";
import { SIZE } from "../../utils/size";
import Tag from "../ui/tag";
import Heading from "../ui/typography/heading";
const titleState: { [name: string]: boolean } = {};
export const Title = () => {
export const Title = ({
title,
isHiddenOnRender,
accentColor,
isBeta,
renderedInRoute,
id
}: {
title: string;
isHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
renderedInRoute: string;
id?: string;
}) => {
const { colors } = useThemeColors();
const currentScreen = useNavigationStore((state) => state.currentScreen);
const isNotebook = currentScreen.name === "Notebook";
const isTopic = currentScreen?.name === "TopicNotes";
const [hide, setHide] = useState(
isNotebook
? typeof titleState[currentScreen.id as string] === "boolean"
? titleState[currentScreen.id as string]
: true
: false
);
const isHidden = titleState[currentScreen.id as string];
const notebook =
isTopic && currentScreen.notebookId
? db.notebooks?.notebook(currentScreen.notebookId)?.data
: null;
const title = currentScreen.title;
const isTag = currentScreen?.name === "TaggedNotes";
const [visible, setVisible] = useState(isHiddenOnRender);
const isTag = title.startsWith("#");
const onScroll = useCallback(
(data: { x: number; y: number }) => {
if (currentScreen.name !== "Notebook") {
setHide(false);
return;
}
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== "Notebook") return;
if (data.route !== renderedInRoute || data.id !== id) return;
if (data.y > 150) {
if (!hide) return;
titleState[currentScreen.id as string] = false;
setHide(false);
if (!visible) return;
setVisible(false);
} else {
if (hide) return;
titleState[currentScreen.id as string] = true;
setHide(true);
if (visible) return;
setVisible(true);
}
},
[currentScreen.id, currentScreen.name, hide]
[id, renderedInRoute, visible]
);
useEffect(() => {
if (currentScreen.name === "Notebook") {
const value =
typeof titleState[currentScreen.id as string] === "boolean"
? titleState[currentScreen.id as string]
: true;
setHide(value);
} else {
setHide(titleState[currentScreen.id as string]);
}
}, [currentScreen.id, currentScreen.name]);
useEffect(() => {
eSubscribeEvent(eScrollEvent, onScroll);
return () => {
eUnSubscribeEvent(eScrollEvent, onScroll);
};
}, [hide, onScroll]);
}, [visible, onScroll]);
function navigateToNotebook() {
if (!isTopic) return;
Notebook.navigate(notebook, true);
}
return (
<>
{!hide && !isHidden ? (
{!visible ? (
<Heading
onPress={navigateToNotebook}
numberOfLines={1}
size={SIZE.lg}
style={{
flexWrap: "wrap",
marginTop: Platform.OS === "ios" ? -1 : 0
}}
color={currentScreen.color || colors.primary.heading}
color={accentColor || colors.primary.heading}
>
{isTag ? (
<Heading size={SIZE.xl} color={colors.primary.accent}>
#
</Heading>
) : null}
{title}{" "}
{isTag ? title.slice(1) : title}{" "}
<Tag
visible={currentScreen.beta}
visible={isBeta}
text="BETA"
style={{
backgroundColor: "transparent"

View File

@@ -32,28 +32,27 @@ import {
import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
const ImagePreview = () => {
const { colors } = useThemeColors("dialog");
const [visible, setVisible] = useState(false);
const [image, setImage] = useState("");
const [image, setImage] = useState<string>();
const [loading, setLoading] = useState(false);
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
return () => {
eUnSubscribeEvent("ImagePreview", open);
};
}, []);
const open = async (image) => {
const open = async (image: ImageAttributes) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
let hash = image.hash;
if (!hash && dataurl.toObject(image.src)) {
if (!hash && image.src && dataurl.toObject(image.src)) {
const data = dataurl.toObject(image.src);
if (!data) return;
hash = await Sodium.hashFile({
@@ -63,6 +62,7 @@ const ImagePreview = () => {
});
}
if (!hash) return;
//@ts-ignore // FIX ME
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
@@ -74,7 +74,7 @@ const ImagePreview = () => {
};
const close = () => {
setImage(null);
setImage(undefined);
setVisible(false);
};
@@ -88,7 +88,7 @@ const ImagePreview = () => {
backgroundColor: "black"
}}
>
{loading ? (
{loading || !image ? (
<View
style={{
flex: 1,

View File

@@ -36,6 +36,10 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import Navigation from "../../services/navigation";
import { eOpenLoginDialog } from "../../utils/events";
import { AuthMode } from "../auth";
import { eSendEvent } from "../../services/event-manager";
const Intro = ({ navigation }) => {
const { colors } = useThemeColors();
@@ -193,9 +197,15 @@ const Intro = ({ navigation }) => {
<Button
width={250}
onPress={async () => {
navigation.navigate("AppLock", {
welcome: true
});
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
setTimeout(() => {
SettingsService.set({
introCompleted: true
});
Navigation.replace("Notes", {
canGoBack: false
});
}, 1000);
}}
style={{
paddingHorizontal: 24,

View File

@@ -77,14 +77,9 @@ export const WelcomeNotice = () => {
SettingsService.set({
introCompleted: true
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
}
);
Navigation.replace("Notes", {
canGoBack: false
});
}, 1000);
}}
style={{
@@ -106,19 +101,14 @@ export const WelcomeNotice = () => {
SettingsService.set({
introCompleted: true
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
}
);
Navigation.replace("Notes", {
canGoBack: false
});
await sleep(1000);
eSendEvent(eCloseLoading);
}}
iconSize={20}
type="gray"
type="plain"
iconPosition="right"
icon="chevron-right"
height={25}

View File

@@ -17,57 +17,39 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { useMessageStore } from "../../../stores/use-message-store";
import { ColorValues } from "../../../utils/colors";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { Announcement } from "../../announcements/announcement";
import { Card } from "../../list/card";
import Paragraph from "../../ui/typography/paragraph";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { SIZE } from "../../../utils/size";
import { useSelectionStore } from "../../../stores/use-selection-store";
export type ListHeaderProps = {
noAnnouncement?: boolean;
color?: string;
messageCard?: boolean;
screen?: string;
shouldShow?: boolean;
};
export const Header = React.memo(
({
type,
messageCard = true,
color,
shouldShow = false,
noAnnouncement,
warning
}) => {
screen
}: ListHeaderProps) => {
const { colors } = useThemeColors();
const announcements = useMessageStore((state) => state.announcements);
const selectionMode = useSelectionStore((state) => state.selectionMode);
return selectionMode ? null : (
<>
{warning ? (
<View
style={{
padding: 12,
backgroundColor: colors.error.background,
width: "95%",
alignSelf: "center",
borderRadius: 5,
flexDirection: "row",
alignItems: "center"
}}
>
<Icon
name="sync-alert"
size={SIZE.md}
color={colors.error.icon}
f
/>
<Paragraph style={{ marginLeft: 5 }} color={colors.error.icon}>
{warning.title}
</Paragraph>
</View>
) : announcements.length !== 0 && !noAnnouncement ? (
{announcements.length !== 0 && !noAnnouncement ? (
<Announcement color={color || colors.primary.accent} />
) : type === "search" ? null : !shouldShow ? (
) : (screen as any) === "Search" ? null : !shouldShow ? (
<View
style={{
marginBottom: 5,
@@ -78,11 +60,7 @@ export const Header = React.memo(
}}
>
{messageCard ? (
<Card
color={
ColorValues[color?.toLowerCase()] || colors.primary.accent
}
/>
<Card color={color || colors.primary.accent} />
) : null}
</View>
) : null}

View File

@@ -17,27 +17,33 @@ 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 { useRef, useState } from "react";
import React from "react";
import { View } from "react-native";
import { Notebook } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { useMenuStore } from "../../../stores/use-menu-store";
import { ToastEvent } from "../../../services/event-manager";
import { getTotalNotes } from "@notesnook/common";
import React, { useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { ToastManager } from "../../../services/event-manager";
import { useMenuStore } from "../../../stores/use-menu-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
export const NotebookHeader = ({ notebook, onEditNotebook }) => {
export const NotebookHeader = ({
notebook,
onEditNotebook,
totalNotes = 0
}: {
notebook: Notebook;
onEditNotebook: () => void;
totalNotes: number;
}) => {
const { colors } = useThemeColors();
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
db.shortcuts.exists(notebook.id)
);
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const totalNotes = getTotalNotes(notebook);
const shortcutRef = useRef();
const onPinNotebook = async () => {
try {
@@ -50,7 +56,7 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
type: "notebook"
}
});
ToastEvent.show({
ToastManager.show({
heading: "Shortcut created",
type: "success"
});
@@ -76,7 +82,7 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
}}
>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{new Date(notebook.dateEdited).toLocaleString()}
{getFormattedDate(notebook.dateModified, "date-time")}
</Paragraph>
<View
style={{
@@ -103,13 +109,12 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
name={isPinnedToMenu ? "link-variant-off" : "link-variant"}
onPress={onPinNotebook}
tooltipText={"Create shortcut in side menu"}
fwdRef={shortcutRef}
customStyle={{
style={{
marginRight: 15,
width: 40,
height: 40
}}
type={isPinnedToMenu ? "grayBg" : "grayBg"}
type={isPinnedToMenu ? "secondary" : "secondary"}
color={isPinnedToMenu ? colors.primary.accent : colors.primary.icon}
size={SIZE.lg}
/>
@@ -118,9 +123,9 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
onPress={onEditNotebook}
tooltipText="Edit this notebook"
name="pencil"
type="grayBg"
type="secondary"
color={colors.primary.icon}
customStyle={{
style={{
width: 40,
height: 40
}}
@@ -138,15 +143,15 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
style={{
marginTop: 10,
fontStyle: "italic",
fontFamily: null
fontFamily: undefined
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{notebook.topics.length === 1
{/* {notebook.topics.length === 1
? "1 topic"
: `${notebook.topics.length} topics`}
,{" "}
,{" "} */}
{notebook && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1

View File

@@ -17,39 +17,52 @@ 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 { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef } from "react";
import React from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { ColorValues } from "../../../utils/colors";
import { RouteName } from "../../../stores/use-navigation-store";
import { ColorValues, getContainerBorder } from "../../../utils/colors";
import { GROUP } from "../../../utils/constants";
import { eOpenJumpToDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import Sort from "../../sheets/sort";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
export const SectionHeader = React.memo(
function SectionHeader({ item, index, type, color, screen, groupOptions }) {
type SectionHeaderProps = {
item: GroupHeader;
index: number;
dataType: ItemType;
color?: string;
screen?: RouteName;
groupOptions: GroupOptions;
onOpenJumpToDialog: () => void;
};
export const SectionHeader = React.memo<
React.FunctionComponent<SectionHeaderProps>
>(
function SectionHeader({
item,
index,
dataType,
color,
screen,
groupOptions,
onOpenJumpToDialog
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
let groupBy = Object.keys(GROUP).find(
(key) => GROUP[key] === groupOptions.groupBy
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
);
const jumpToRef = useRef();
const sortRef = useRef();
const compactModeRef = useRef();
const notebooksListMode = useSettingStore(
(state) => state.settings.notebooksListMode
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
);
const notesListMode = useSettingStore(
(state) => state.settings.notesListMode
);
const listMode = type === "notebooks" ? notebooksListMode : notesListMode;
groupBy = !groupBy
? "Default"
@@ -67,14 +80,14 @@ export const SectionHeader = React.memo(
backgroundColor: colors.secondary.background,
alignSelf: "center",
borderRadius: 5,
marginVertical: 5
marginVertical: 5,
...getContainerBorder(colors.secondary.background, 0.8)
}}
>
<TouchableOpacity
onPress={() => {
eSendEvent(eOpenJumpToDialog, type);
onOpenJumpToDialog();
}}
ref={jumpToRef}
activeOpacity={0.9}
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
style={{
@@ -83,7 +96,10 @@ export const SectionHeader = React.memo(
}}
>
<Heading
color={ColorValues[color?.toLowerCase()] || colors.primary.accent}
color={
ColorValues[color?.toLowerCase() as keyof typeof ColorValues] ||
colors.primary.accent
}
size={SIZE.sm}
style={{
minWidth: 60,
@@ -105,12 +121,12 @@ export const SectionHeader = React.memo(
<>
<Button
onPress={() => {
console.log("Opening Sort sheet", screen, dataType);
presentSheet({
component: <Sort screen={screen} type={type} />
component: <Sort screen={screen} type={dataType} />
});
}}
tooltipText="Change sorting of items in list"
fwdRef={sortRef}
title={groupBy}
icon={
groupOptions.sortDirection === "asc"
@@ -123,38 +139,42 @@ export const SectionHeader = React.memo(
paddingHorizontal: 0,
backgroundColor: "transparent",
marginRight:
type === "notes" || type === "home" || type === "notebooks"
dataType === "note" ||
screen === "Notes" ||
dataType === "notebook"
? 10
: 0
}}
type="gray"
type="plain"
iconPosition="right"
/>
<IconButton
customStyle={{
style={{
width: 25,
height: 25
}}
hidden={
type !== "notes" && type !== "notebooks" && type !== "home"
dataType !== "note" &&
dataType !== "notebook" &&
screen !== "Notes"
}
testID="icon-compact-mode"
tooltipText={
listMode == "compact"
isCompactModeEnabled
? "Switch to normal mode"
: "Switch to compact mode"
}
fwdRef={compactModeRef}
color={colors.secondary.icon}
name={listMode == "compact" ? "view-list" : "view-list-outline"}
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
onPress={() => {
let settings = {};
settings[
type !== "notebooks" ? "notesListMode" : "notebooksListMode"
] = listMode === "normal" ? "compact" : "normal";
SettingsService.set(settings);
SettingsService.set({
[dataType !== "notebook"
? "notesListMode"
: "notebooksListMode"]: !isCompactModeEnabled
? "compact"
: "normal"
});
}}
size={SIZE.lg - 2}
/>
@@ -166,6 +186,10 @@ export const SectionHeader = React.memo(
},
(prev, next) => {
if (prev.item.title !== next.item.title) return false;
if (prev.groupOptions?.groupBy !== next.groupOptions.groupBy) return false;
if (prev.groupOptions?.sortDirection !== next.groupOptions.sortDirection)
return false;
if (prev.groupOptions?.sortBy !== next.groupOptions.sortBy) return false;
return true;
}

View File

@@ -17,21 +17,28 @@ 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 { getUpcomingReminder } from "@notesnook/core/dist/collections/reminders";
import { decode, EntityLevel } from "entities";
import {
BaseTrashItem,
Color,
Note,
Reminder,
TrashItem
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { EntityLevel, decode } from "entities";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import Notebook from "../../../screens/notebook";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import NotebookScreen from "../../../screens/notebook";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../../utils/colors";
import { SIZE } from "../../../utils/size";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
@@ -39,79 +46,42 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useEditorStore } from "../../../stores/use-editor-store";
function navigateToTag(item) {
const tag = db.tags.tag(item.id);
if (!tag) return;
TaggedNotes.navigate(tag, true);
}
const showActionSheet = (item) => {
Properties.present(item);
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
index: number;
tags?: TagsWithDateEdited;
notebooks?: NotebooksWithDateEdited;
color?: Color;
reminder?: Reminder;
attachmentsCount: number;
date: number;
isTrash?: boolean;
noOpen?: boolean;
locked?: boolean;
};
function getNotebook(item) {
const isTrash = item.type === "trash";
const currentId = useNavigationStore.getState().currentScreen.id;
if (isTrash) return [];
const items = [];
const notebooks = db.relations.to(item, "notebook") || [];
for (let notebook of notebooks) {
if (items.length > 1) break;
if (notebook.id === currentId) continue;
items.push(notebook);
}
if (item.notebooks) {
for (let nb of item.notebooks) {
if (items.length > 1) break;
const notebook = db.notebooks?.notebook(nb.id)?.data;
if (!notebook) continue;
for (let topicId of nb.topics) {
if (items.length > 1) break;
if (topicId === currentId) continue;
const topic = notebook.topics.find((t) => t.id === topicId);
if (!topic) continue;
items.push(topic);
}
}
}
return items;
}
function getTags(item) {
const noteTags = item.tags?.slice(0, 3) || [];
const tags = [];
for (const tagName of noteTags) {
const tag = db.tags.tag(tagName);
if (!tag) continue;
tags.push(tag);
}
return tags;
}
const NoteItem = ({
item,
isTrash,
dateBy = "dateCreated",
date,
color,
notebooks,
reminder,
tags,
attachmentsCount,
locked,
noOpen = false
}) => {
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
}: NoteItemProps) => {
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item);
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const _update = useRelationStore((state) => state.updater);
// eslint-disable-next-line react-hooks/exhaustive-deps
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
const reminders = db.relations.from(item, "reminder");
const reminder = getUpcomingReminder(reminders);
const noteColor = ColorValues[item.color?.toLowerCase()];
const tags = getTags(item);
const primaryColors = isEditingNote ? colors.selected : colors.primary;
return (
@@ -133,44 +103,43 @@ const NoteItem = ({
flexWrap: "wrap"
}}
>
{notebooks?.map((item) => (
<Button
title={
item.title.length > 25
? item.title.slice(0, 25) + "..."
: item.title
}
tooltipText={item.title}
key={item.id}
height={25}
icon={item.type === "topic" ? "bookmark" : "book-outline"}
type="grayBg"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
textStyle={{
marginRight: 0
}}
style={{
borderRadius: 5,
marginRight: 5,
borderWidth: 0.5,
borderColor: primaryColors.border,
paddingHorizontal: 6,
marginBottom: 5
}}
onPress={() => {
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
Notebook.navigate(item);
{notebooks?.items
?.filter(
(item) =>
item.id !== useNavigationStore.getState().focusedRouteId
)
.map((item) => (
<Button
title={
item.title.length > 25
? item.title.slice(0, 25) + "..."
: item.title
}
}}
/>
))}
tooltipText={item.title}
key={item.id}
height={25}
icon="book-outline"
type="secondary"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
textStyle={{
marginRight: 0
}}
style={{
borderRadius: 5,
marginRight: 5,
paddingHorizontal: 6,
marginBottom: 5
}}
onPress={() => {
NotebookScreen.navigate(item, true);
}}
/>
))}
<ReminderTime
reminder={reminder}
color={noteColor}
color={color?.colorCode}
onPress={() => {
Properties.present(reminder);
}}
@@ -184,26 +153,22 @@ const NoteItem = ({
{compactMode ? (
<Paragraph
numberOfLines={1}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
style={{
flexWrap: "wrap"
}}
color={color?.colorCode || primaryColors.heading}
size={SIZE.sm}
style={{
paddingRight: 10
}}
>
{item.title}
</Paragraph>
) : (
<Heading
numberOfLines={1}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
style={{
flexWrap: "wrap"
}}
color={color?.colorCode || primaryColors.heading}
size={SIZE.md}
style={{
paddingRight: 10
}}
>
{item.title}
</Heading>
@@ -252,13 +217,11 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
{attachmentCount > 0 ? (
{attachmentsCount > 0 ? (
<View
style={{
flexDirection: "row",
@@ -275,7 +238,7 @@ const NoteItem = ({
color={colors.secondary.paragraph}
size={SIZE.xs}
>
{attachmentCount}
{attachmentsCount}
</Paragraph>
</View>
) : null}
@@ -288,14 +251,11 @@ const NoteItem = ({
style={{
marginRight: 6
}}
color={
ColorValues[item.color?.toLowerCase()] ||
primaryColors.accent
}
color={color?.colorCode || primaryColors.accent}
/>
) : null}
{item.locked ? (
{locked ? (
<Icon
name="lock"
testID="note-locked-icon"
@@ -320,13 +280,13 @@ const NoteItem = ({
) : null}
{!isTrash && !compactMode && tags
? tags.map((item) =>
? tags.items?.map((item) =>
item.id ? (
<Button
title={"#" + item.alias}
title={"#" + item.title}
key={item.id}
height={23}
type="gray"
type="plain"
textStyle={{
textDecorationLine: "underline"
}}
@@ -337,9 +297,9 @@ const NoteItem = ({
paddingHorizontal: 6,
marginRight: 4,
zIndex: 10,
maxWidth: tags.length > 1 ? 130 : null
maxWidth: tags.items?.length > 1 ? 130 : null
}}
onPress={() => navigateToTag(item)}
onPress={() => TaggedNotes.navigate(item, true)}
/>
) : null
)
@@ -367,7 +327,8 @@ const NoteItem = ({
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
</Paragraph>
</>
)}
@@ -393,7 +354,7 @@ const NoteItem = ({
/>
) : null}
{item.locked ? (
{locked ? (
<Icon
name="lock"
testID="note-locked-icon"
@@ -423,8 +384,8 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
</>
) : null}
@@ -434,8 +395,8 @@ const NoteItem = ({
color={primaryColors.paragraph}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
customStyle={{
onPress={() => !noOpen && Properties.present(item)}
style={{
justifyContent: "center",
height: 35,
width: 35,

View File

@@ -1,135 +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 React from "react";
import NoteItem from ".";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import {
eSendEvent,
hideSheet,
openVault,
presentSheet
} from "../../../services/event-manager";
import { useEditorStore } from "../../../stores/use-editor-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import { presentDialog } from "../../dialog/functions";
import NotePreview from "../../note-history/preview";
import SelectionWrapper from "../selection-wrapper";
const present = () =>
presentDialog({
title: "Note not synced",
negativeText: "Ok",
paragraph: "Please sync again to open this note for editing"
});
export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
let _note = item;
if (isSheet) hideSheet();
if (!isTrash) {
_note = db.notes.note(item.id).data;
if (!db.notes.note(item.id)?.synced()) {
present();
return;
}
} else {
if (!db.trash.synced(item.id)) {
present();
return;
}
}
const { selectedItemsList, selectionMode, clearSelection } =
useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode) {
setSelectedItem && setSelectedItem(_note);
return;
} else {
clearSelection();
}
if (_note.conflicted) {
eSendEvent(eShowMergeDialog, _note);
return;
}
if (_note.locked) {
openVault({
item: _note,
novault: true,
locked: true,
goToEditor: true,
title: "Open note",
description: "Unlock note to open it in editor."
});
return;
}
if (isTrash) {
const content = await db.content.get(item.contentId);
presentSheet({
component: (
<NotePreview note={item} content={{ type: "tiptap", data: content }} />
)
});
} else {
useEditorStore.getState().setReadonly(_note?.readonly);
eSendEvent(eOnLoadNote, _note);
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
}
};
export const NoteWrapper = React.memo(
function NoteWrapper({ item, index, dateBy, isSheet }) {
const isTrash = item.type === "trash";
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
return (
<SelectionWrapper
index={index}
height={100}
testID={notesnook.ids.note.get(index)}
onPress={() => openNote(item, isTrash, setSelectedItem, isSheet)}
isSheet={isSheet}
item={item}
>
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
</SelectionWrapper>
);
},
(prev, next) => {
if (prev.dateBy !== next.dateBy) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (prev.item !== next.item) {
return false;
}
return true;
}
);

View File

@@ -0,0 +1,154 @@
/*
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 { BaseTrashItem, Color, Note, Reminder } from "@notesnook/core";
import React from "react";
import NoteItem from ".";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import {
eSendEvent,
hideSheet,
openVault,
presentSheet
} from "../../../services/event-manager";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import NotePreview from "../../note-history/preview";
import SelectionWrapper from "../selection-wrapper";
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
export const openNote = async (
item: Note,
isTrash?: boolean,
isSheet?: boolean
) => {
let note: Note = item;
if (isSheet) hideSheet();
if (!isTrash) {
note = (await db.notes.note(item.id)) as Note;
}
if (useSelectionStore.getState().selectionMode === item.type) {
const {
selectedItemsList,
selectionMode,
clearSelection,
setSelectedItem
} = useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(note.id);
} else {
clearSelection();
}
return;
}
if (note.conflicted) {
eSendEvent(eShowMergeDialog, note);
return;
}
if (isTrash) {
if (!note.contentId) return;
const content = await db.content.get(note.contentId as string);
presentSheet({
component: (
<NotePreview note={item} content={{ type: "tiptap", data: content }} />
)
});
} else {
eSendEvent(eOnLoadNote, {
item: note
});
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
}
};
type NoteWrapperProps = {
item: Note | BaseTrashItem<Note>;
index: number;
tags?: TagsWithDateEdited;
notebooks?: NotebooksWithDateEdited;
color?: Color;
reminder?: Reminder;
attachmentsCount: number;
date: number;
isRenderedInActionSheet: boolean;
locked?: boolean;
};
export const NoteWrapper = React.memo<
React.FunctionComponent<NoteWrapperProps>
>(
function NoteWrapper({
item,
index,
isRenderedInActionSheet,
...restProps
}: NoteWrapperProps) {
const isTrash = item.type === "trash";
return (
<SelectionWrapper
testID={notesnook.ids.note.get(index)}
onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)}
isSheet={isRenderedInActionSheet}
item={item}
color={restProps.color?.colorCode}
>
<NoteItem {...restProps} item={item} index={index} isTrash={isTrash} />
</SelectionWrapper>
);
},
(prev, next) => {
if (prev.date !== next.date) {
return false;
}
if (
prev.tags?.dateEdited !== next.tags?.dateEdited ||
prev.tags?.items?.length !== next.tags?.items?.length
)
return false;
if (
prev.notebooks?.dateEdited !== next.notebooks?.dateEdited ||
prev.notebooks?.items?.length !== next.notebooks?.items?.length
)
return false;
if (prev.color !== next.color) return false;
if (prev.reminder?.id !== next.reminder?.id) return false;
if (prev.attachmentsCount !== next.attachmentsCount) return false;
if (prev.item?.dateModified !== next.item?.dateModified) {
return false;
}
if (prev.item?.id !== next.item?.id) return false;
return true;
}
);

View File

@@ -17,41 +17,38 @@ 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 { getFormattedDate } from "@notesnook/common";
import { BaseTrashItem, Notebook, TrashItem } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
const showActionSheet = (item) => {
Properties.present(item);
};
const navigateToTopic = (topic) => {
if (useSelectionStore.getState().selectedItemsList.length > 0) return;
TopicNotes.navigate(topic, true);
type NotebookItemProps = {
item: Notebook | BaseTrashItem<Notebook>;
totalNotes: number;
date: number;
index: number;
isTrash: boolean;
};
export const NotebookItem = ({
item,
isTopic = false,
isTrash,
dateBy,
date,
totalNotes
}) => {
}: NotebookItemProps) => {
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item);
const topics = item.topics?.slice(0, 3) || [];
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
return (
<>
@@ -83,7 +80,7 @@ export const NotebookItem = ({
</Heading>
)}
{isTopic || !item.description || compactMode ? null : (
{!item.description || compactMode ? null : (
<Paragraph
size={SIZE.sm}
numberOfLines={2}
@@ -95,42 +92,6 @@ export const NotebookItem = ({
</Paragraph>
)}
{isTopic || compactMode ? null : (
<View
style={{
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap"
}}
>
{topics.map((topic) => (
<Button
title={topic.title}
key={topic.id}
height={null}
textStyle={{
marginRight: 0
}}
type="grayBg"
fontSize={SIZE.xs}
icon="bookmark-outline"
iconSize={SIZE.sm}
style={{
borderRadius: 5,
maxWidth: 120,
borderWidth: 0.5,
paddingVertical: 2.5,
borderColor: colors.primary.border,
paddingHorizontal: 6,
marginVertical: 5,
marginRight: 5
}}
onPress={() => navigateToTopic(topic)}
/>
))}
</View>
)}
{!compactMode ? (
<View
style={{
@@ -152,7 +113,9 @@ export const NotebookItem = ({
}}
>
{"Deleted on " +
new Date(item.dateDeleted).toISOString().slice(0, 10)}
new Date((item as TrashItem).dateDeleted)
.toISOString()
.slice(0, 10)}
</Paragraph>
<Paragraph
color={colors.primary.accent}
@@ -162,7 +125,8 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
</Paragraph>
</>
) : (
@@ -173,7 +137,7 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{getFormattedDate(item[dateBy], "date")}
{getFormattedDate(date, "date")}
</Paragraph>
)}
<Paragraph
@@ -233,8 +197,8 @@ export const NotebookItem = ({
name="dots-horizontal"
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => showActionSheet(item)}
customStyle={{
onPress={() => Properties.present(item)}
style={{
justifyContent: "center",
height: 35,
width: 35,

View File

@@ -17,41 +17,24 @@ 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 { BaseTrashItem, Notebook } from "@notesnook/core";
import React from "react";
import { NotebookItem } from ".";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { ToastEvent } from "../../../services/event-manager";
import { db } from "../../../common/database";
import NotebookScreen from "../../../screens/notebook";
import { ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useTrashStore } from "../../../stores/use-trash-store";
import { db } from "../../../common/database";
import { presentDialog } from "../../dialog/functions";
import SelectionWrapper from "../selection-wrapper";
const navigateToNotebook = (item, canGoBack) => {
if (!item) return;
Navigation.navigate(
{
title: item.title,
name: "Notebook",
id: item.id,
type: "notebook"
},
{
title: item.title,
item: item,
canGoBack
}
);
};
export const openNotebookTopic = (item) => {
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
const isTrash = item.type === "trash";
const { selectedItemsList, setSelectedItem, selectionMode, clearSelection } =
useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode) {
setSelectedItem(item);
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(item.id);
return;
} else {
clearSelection();
@@ -59,24 +42,24 @@ export const openNotebookTopic = (item) => {
if (isTrash) {
presentDialog({
title: `Restore ${item.itemType}`,
paragraph: `Restore or delete ${item.itemType} forever`,
title: `Restore notebook`,
paragraph: `Restore or delete notebook forever`,
positiveText: "Restore",
negativeText: "Delete",
positivePress: async () => {
await db.trash.restore(item.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: "Restore successful",
type: "success"
});
},
onClose: async () => {
await db.trash.delete(item.id);
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: "Permanently deleted items",
type: "success",
context: "local"
@@ -85,29 +68,30 @@ export const openNotebookTopic = (item) => {
});
return;
}
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
navigateToNotebook(item, true);
}
NotebookScreen.navigate(item, true);
};
type NotebookWrapperProps = {
item: Notebook | BaseTrashItem<Notebook>;
totalNotes: number;
date: number;
index: number;
};
export const NotebookWrapper = React.memo(
function NotebookWrapper({ item, index, dateBy, totalNotes }) {
function NotebookWrapper({
item,
index,
date,
totalNotes
}: NotebookWrapperProps) {
const isTrash = item.type === "trash";
return (
<SelectionWrapper
pinned={item.pinned}
index={index}
onPress={() => openNotebookTopic(item)}
height={item.type === "topic" ? 80 : 110}
item={item}
>
<SelectionWrapper onPress={() => openNotebook(item)} item={item}>
<NotebookItem
isTopic={item.type === "topic"}
item={item}
dateBy={dateBy}
date={date}
index={index}
isTrash={isTrash}
totalNotes={totalNotes}
@@ -117,17 +101,9 @@ export const NotebookWrapper = React.memo(
},
(prev, next) => {
if (prev.totalNotes !== next.totalNotes) return false;
if (prev.item.title !== next.item.title) return false;
if (prev.dateBy !== next.dateBy) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
return false;
}
if (prev.date !== next.date) return false;
if (prev.item?.dateModified !== next.item?.dateModified) return false;
if (prev.item?.id !== next.item?.id) return false;
return true;
}

View File

@@ -16,20 +16,21 @@ 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 { Reminder } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import type { Reminder } from "../../../services/notifications";
import { useThemeColors } from "@notesnook/theme";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import ReminderSheet from "../../sheets/reminder";
import { IconButton } from "../../ui/icon-button";
import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper from "../selection-wrapper";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ReminderSheet from "../../sheets/reminder";
import { ReminderTime } from "../../ui/reminder-time";
const ReminderItem = React.memo(
({
@@ -43,21 +44,28 @@ const ReminderItem = React.memo(
}) => {
const { colors } = useThemeColors();
const openReminder = () => {
const {
selectedItemsList,
setSelectedItem,
selectionMode,
clearSelection
} = useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(item.id);
return;
} else {
clearSelection();
}
ReminderSheet.present(item, undefined, isSheet);
};
return (
<SelectionWrapper
//@ts-ignore
index={index}
height={100}
onPress={openReminder}
item={item}
isSheet={isSheet}
>
<SelectionWrapper onPress={openReminder} item={item} isSheet={isSheet}>
<View
style={{
flexShrink: 1,
opacity: item.disabled ? 0.5 : 1
opacity: item.disabled ? 0.5 : 1,
maxWidth: "80%",
flexGrow: 1
}}
>
<Heading
@@ -131,7 +139,11 @@ const ReminderItem = React.memo(
height: 30
}}
>
<Icon name="reload" size={SIZE.md} color={colors.primary.accent} />
<Icon
name="reload"
size={SIZE.md}
color={colors.primary.accent}
/>
<Paragraph
size={SIZE.xs}
color={colors.secondary.paragraph}
@@ -164,7 +176,7 @@ const ReminderItem = React.memo(
name="dots-horizontal"
size={SIZE.xl}
onPress={() => Properties.present(item, [], isSheet)}
customStyle={{
style={{
justifyContent: "center",
height: 35,
width: 35,

View File

@@ -17,16 +17,18 @@ 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 { Item } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import useIsSelected from "../../../hooks/use-selected";
import { useEditorStore } from "../../../stores/use-editor-store";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
export const Filler = ({ item }) => {
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
const { colors } = useThemeColors();
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
);
const [selected] = useIsSelected(item);
@@ -40,8 +42,8 @@ export const Filler = ({ item }) => {
backgroundColor: colors.selected.background,
borderLeftWidth: 5,
borderLeftColor: isEditingNote
? item.color
? colors.static[item.color]
? color
? color
: colors.selected.accent
: "transparent"
}}

View File

@@ -18,41 +18,49 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useRef } from "react";
import React, { PropsWithChildren, useRef } from "react";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { PressableButton } from "../../ui/pressable";
import { Pressable } from "../../ui/pressable";
import { Filler } from "./back-fill";
import { SelectionIcon } from "./selection";
import { Item, TrashItem } from "@notesnook/core";
type SelectionWrapperProps = PropsWithChildren<{
item: Item;
onPress: () => void;
testID?: string;
isSheet?: boolean;
color?: string;
}>;
const SelectionWrapper = ({
children,
item,
background,
onPress,
testID,
isSheet
}) => {
isSheet,
children,
color
}: SelectionWrapperProps) => {
const itemId = useRef(item.id);
const { colors, isDark } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item);
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
if (item.id !== itemId.current) {
itemId.current = item.id;
}
const onLongPress = () => {
if (!useSelectionStore.getState().selectionMode) {
useSelectionStore.setState({
selectedItemsList: []
});
useSelectionStore.getState().setSelectionMode(true);
if (useSelectionStore.getState().selectionMode !== item.type) {
useSelectionStore.getState().setSelectionMode(item.type);
}
useSelectionStore.getState().setSelectedItem(item);
useSelectionStore.getState().setSelectedItem(item.id);
};
return (
<PressableButton
<Pressable
customColor={isSheet ? colors.primary.hover : "transparent"}
testID={testID}
onLongPress={onLongPress}
@@ -60,7 +68,7 @@ const SelectionWrapper = ({
customSelectedColor={colors.primary.hover}
customAlpha={!isDark ? -0.02 : 0.02}
customOpacity={1}
customStyle={{
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
@@ -72,16 +80,10 @@ const SelectionWrapper = ({
marginBottom: isSheet ? 12 : undefined
}}
>
{item.type === "note" ? (
<Filler background={background} item={item} />
) : null}
<SelectionIcon
compactMode={compactMode}
item={item}
onLongPress={onLongPress}
/>
{item.type === "note" ? <Filler item={item} color={color} /> : null}
<SelectionIcon item={item} />
{children}
</PressableButton>
</Pressable>
);
};

View File

@@ -25,13 +25,16 @@ import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enab
import useIsSelected from "../../../hooks/use-selected";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { Item, TrashItem } from "@notesnook/core";
export const SelectionIcon = ({ item }) => {
export const SelectionIcon = ({ item }: { item: Item }) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const [selected] = useIsSelected(item);
const compactMode = useIsCompactModeEnabled(item);
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
return selectionMode ? (
<View

View File

@@ -17,44 +17,58 @@ 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 { Tag } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { useThemeColors } from "@notesnook/theme";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper from "../selection-wrapper";
const TagItem = React.memo(
({ item, index }) => {
const { colors, isDark } = useThemeColors();
({
item,
index,
totalNotes
}: {
item: Tag;
index: number;
totalNotes: number;
}) => {
const { colors } = useThemeColors();
const onPress = () => {
const {
selectedItemsList,
setSelectedItem,
selectionMode,
clearSelection
} = useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(item.id);
return;
} else {
clearSelection();
}
TaggedNotes.navigate(item, true);
};
return (
<PressableButton
<SelectionWrapper
onPress={onPress}
selectedColor={colors.secondary.background}
item={item}
testID={notesnook.ids.tag.get(index)}
alpha={!isDark ? -0.02 : 0.02}
opacity={1}
customStyle={{
paddingHorizontal: 12,
flexDirection: "row",
paddingVertical: 12,
alignItems: "center",
width: "100%",
justifyContent: "space-between"
}}
>
<View
style={{
maxWidth: "92%"
flexGrow: 1,
flexShrink: 1
}}
>
<Heading size={SIZE.md}>
@@ -66,7 +80,7 @@ const TagItem = React.memo(
>
#
</Heading>
{item.alias}
{item.title}
</Heading>
<Paragraph
color={colors.secondary.paragraph}
@@ -75,10 +89,10 @@ const TagItem = React.memo(
marginTop: 5
}}
>
{item && item.noteIds.length && item.noteIds.length > 1
? item.noteIds.length + " notes"
: item.noteIds.length === 1
? item.noteIds.length + " note"
{totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: null}
</Paragraph>
</View>
@@ -91,7 +105,7 @@ const TagItem = React.memo(
Properties.present(item);
}}
testID={notesnook.ids.tag.menu}
customStyle={{
style={{
justifyContent: "center",
height: 35,
width: 35,
@@ -99,14 +113,11 @@ const TagItem = React.memo(
alignItems: "center"
}}
/>
</PressableButton>
</SelectionWrapper>
);
},
(prev, next) => {
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
if (prev.item?.dateModified !== next.item?.dateModified) {
return false;
}

View File

@@ -22,28 +22,29 @@ import { Dimensions, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useMessageStore } from "../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import { hexToRGBA } from "../../utils/colors";
import { getContainerBorder, hexToRGBA } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import { PressableButton } from "../ui/pressable";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Card = ({ color, warning }) => {
export const Card = ({ color }: { color?: string }) => {
const { colors } = useThemeColors();
color = color ? color : colors.primary.accent;
const messageBoardState = useMessageStore((state) => state.message);
const announcement = useMessageStore((state) => state.announcement);
const announcements = useMessageStore((state) => state.announcements);
const fontScale = Dimensions.get("window").fontScale;
return !messageBoardState.visible || announcement || warning ? null : (
return !messageBoardState.visible ||
(announcements && announcements.length) ? null : (
<View
style={{
width: "95%"
}}
>
<PressableButton
<Pressable
onPress={messageBoardState.onPress}
type="gray"
customStyle={{
type="plain"
style={{
paddingVertical: 12,
flexDirection: "row",
alignItems: "center",
@@ -63,12 +64,18 @@ export const Card = ({ color, warning }) => {
width: 40 * fontScale,
backgroundColor:
messageBoardState.type === "error"
? hexToRGBA(colors.static.red, 0.15)
? hexToRGBA(colors.error.accent, 0.15)
: hexToRGBA(color, 0.15),
height: 40 * fontScale,
borderRadius: 100,
alignItems: "center",
justifyContent: "center"
justifyContent: "center",
...getContainerBorder(
messageBoardState.type === "error"
? colors.error.accent
: color || colors.primary.accent,
0.4
)
}}
>
<Icon
@@ -121,7 +128,7 @@ export const Card = ({ color, warning }) => {
/>
</View>
)}
</PressableButton>
</Pressable>
</View>
);
};

View File

@@ -1,167 +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 React from "react";
import { ActivityIndicator, useWindowDimensions, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useTip } from "../../services/tip-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import { Tip } from "../tip";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export const Empty = React.memo(
function Empty({
loading = true,
placeholderData,
headerProps,
type,
screen
}) {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const { height } = useWindowDimensions();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholderData.type || type,
screen === "Notes" ? "notes" : null
);
return (
<View
style={[
{
height: height - (140 + insets.top),
width: "80%",
justifyContent: "center",
alignSelf: "center"
}
]}
>
{!loading ? (
<>
<Tip
color={
ColorValues[headerProps.color?.toLowerCase()]
? headerProps.color
: "accent"
}
tip={tip || { text: placeholderData.paragraph }}
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
}}
/>
{placeholderData.button && (
<Button
testID={notesnook.buttons.add}
type="grayAccent"
title={placeholderData.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholderData.action}
buttonType={{
text:
colors.static[headerProps.color] || colors.primary.accent
}}
style={{
alignSelf: "flex-start",
borderRadius: 5,
height: 40
}}
/>
)}
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "flex-start",
width: "100%"
}}
>
<Heading>{placeholderData.heading}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholderData.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={
ColorValues[headerProps.color?.toLowerCase()] ||
colors.primary.accent
}
/>
</View>
</>
)}
</View>
);
},
(prev, next) => {
if (prev.loading === next.loading) return true;
return false;
}
);
/**
* Make a tips manager.
* The tip manager stores many tips. Each tip has following values
* 1. Text
* 2. contexts: An array of context strings. // Places where the tip can be shown
* 3. Button if any.
* 4. Image/Gif asset.
*
* Tip manager adds the following methods -> get(context). Returns a random tip for the following context.
*
* Tips can be shown in a sheet or in a list. For sheets, GeneralSheet can be used to
* render tips.
*
* Where can the tips be shown and how?
* 1. When transitioning, show tips in a sheet. Make sure its useful
* 2. Replace placeholders with tips.
* 3. Show tips in editor placeholder.
* 4. Show tips between list items?
*
* Tooltips.
* Small tooltips can be shown in initial render first time.
* Especially for items that are not shown on blank page. Should be
* in places where it makes sense and does not interrupt the user.
*
* Can also be shown when first time entering a screen that
* has something that the user might not know of. Like sorting and side menu.
*
* Todo:
* 1. Make a tip manager.
* 2. Make a list of tips.
* 3. Add images for those tips.
* 4. Show tips
*/

View File

@@ -0,0 +1,140 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ActivityIndicator, useWindowDimensions, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { TTip, useTip } from "../../services/tip-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { SIZE } from "../../utils/size";
import { Tip } from "../tip";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export type PlaceholderData = {
title: string;
paragraph: string;
button?: string;
action?: () => void;
loading?: string;
type?: string;
};
type EmptyListProps = {
loading?: boolean;
placeholder?: PlaceholderData;
title?: string;
color?: string;
dataType: string;
screen?: string;
};
export const Empty = React.memo(function Empty({
loading = true,
placeholder,
title,
color,
dataType,
screen
}: EmptyListProps) {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const { height } = useWindowDimensions();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholder?.type || ((dataType + "s") as any),
screen === "Notes" ? "notes" : "list"
);
return (
<View
style={[
{
height: height - (140 + insets.top),
width: "80%",
justifyContent: "center",
alignSelf: "center"
}
]}
>
{!loading ? (
<>
<Tip
color={color}
tip={
screen !== "Search"
? tip || ({ text: placeholder?.paragraph } as TTip)
: ({ text: placeholder?.paragraph } as TTip)
}
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
}}
/>
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="accent"
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholder?.action}
buttonType={{
text: "white"
}}
style={{
alignSelf: "flex-start",
borderRadius: 5,
height: 40
}}
/>
)}
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "flex-start",
width: "100%"
}}
>
<Heading>{placeholder?.title}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholder?.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={color || colors.primary.accent}
/>
</View>
</>
)}
</View>
);
});

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 React, { useEffect, useRef } from "react";
import { RefreshControl, View } from "react-native";
import { FlashList } from "@shopify/flash-list";
import Animated, { FadeInDown } from "react-native-reanimated";
import { notesnook } from "../../../e2e/test.ids";
import { eSendEvent } from "../../services/event-manager";
import Sync from "../../services/sync";
import { useThemeColors } from "@notesnook/theme";
import { db } from "../../common/database";
import { eScrollEvent } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { Footer } from "../list-items/footer";
import { Header } from "../list-items/headers/header";
import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import TagItem from "../list-items/tag";
import { Empty } from "./empty";
import { getTotalNotes } from "@notesnook/common";
import { useSettingStore } from "../../stores/use-setting-store";
import ReminderItem from "../list-items/reminder";
import { useGroupOptions } from "../../hooks/use-group-options";
const renderItems = {
note: NoteWrapper,
notebook: NotebookWrapper,
topic: NotebookWrapper,
tag: TagItem,
section: SectionHeader,
header: SectionHeader,
reminder: ReminderItem
};
const RenderItem = ({ item, index, type, ...restArgs }) => {
if (!item) return <View />;
const Item = renderItems[item.itemType || item.type] || View;
const totalNotes = getTotalNotes(item);
return (
<Item
item={item}
index={index}
type={type}
totalNotes={totalNotes}
{...restArgs}
/>
);
};
/**
*
* @param {any} param0
* @returns
*/
const List = ({
listData,
type,
refreshCallback,
placeholderData,
loading,
headerProps = {
heading: "Home",
color: null
},
screen,
ListHeader,
warning,
isSheet = false,
onMomentumScrollEnd,
handlers,
ScrollComponent
}) => {
const { colors } = useThemeColors();
const scrollRef = useRef();
const [notesListMode, notebooksListMode] = useSettingStore((state) => [
state.settings.notesListMode,
state.settings.notebooksListMode
]);
const isCompactModeEnabled =
(type === "notes" && notesListMode === "compact") ||
type === "notebooks" ||
notebooksListMode === "compact";
const groupType =
screen === "Home" ? "home" : screen === "Favorites" ? "favorites" : type;
const groupOptions = useGroupOptions(groupType);
const dateBy =
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
const renderItem = React.useCallback(
({ item, index }) => (
<RenderItem
item={item}
index={index}
color={headerProps?.color}
title={headerProps?.heading}
dateBy={dateBy}
type={groupType}
screen={screen}
isSheet={isSheet}
groupOptions={groupOptions}
/>
),
[
headerProps?.color,
headerProps?.heading,
screen,
isSheet,
dateBy,
groupType,
groupOptions
]
);
const _onRefresh = async () => {
Sync.run("global", false, true, () => {
if (refreshCallback) {
refreshCallback();
}
});
};
const _onScroll = React.useCallback(
(event) => {
if (!event) return;
let y = event.nativeEvent.contentOffset.y;
eSendEvent(eScrollEvent, {
y,
screen
});
},
[screen]
);
useEffect(() => {
eSendEvent(eScrollEvent, {
y: 0,
screen
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let styles = {
width: "100%",
minHeight: 1,
minWidth: 1
};
const ListView = ScrollComponent ? ScrollComponent : FlashList;
return (
<>
<Animated.View
style={{
flex: 1
}}
entering={type === "search" ? undefined : FadeInDown}
>
<ListView
{...handlers}
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
data={listData}
renderItem={renderItem}
onScroll={_onScroll}
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
tabBarRef.current?.unlock();
onMomentumScrollEnd?.();
}}
getItemType={(item) => item.itemType || item.type}
estimatedItemSize={isCompactModeEnabled ? 60 : 100}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
refreshControl={
<RefreshControl
tintColor={colors.primary.accent}
colors={[colors.primary.accent]}
progressBackgroundColor={colors.secondary.background}
onRefresh={_onRefresh}
refreshing={false}
/>
}
ListEmptyComponent={
placeholderData ? (
<Empty
loading={loading}
placeholderData={placeholderData}
headerProps={headerProps}
type={type}
screen={screen}
/>
) : null
}
ListFooterComponent={<Footer />}
ListHeaderComponent={
<>
{ListHeader ? (
ListHeader
) : !headerProps ? null : (
<Header
title={headerProps.heading}
color={headerProps.color}
type={type}
screen={screen}
warning={warning}
/>
)}
</>
}
/>
</Animated.View>
{listData ? (
<JumpToSectionDialog
screen={screen}
data={listData}
type={screen === "Home" ? "home" : type}
scrollRef={scrollRef}
/>
) : null}
</>
);
};
export default List;

View File

@@ -0,0 +1,212 @@
/*
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 { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { FlashList } from "@shopify/flash-list";
import React, { useEffect, useRef } from "react";
import {
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl
} from "react-native";
import Animated, { FadeInDown } from "react-native-reanimated";
import { notesnook } from "../../../e2e/test.ids";
import { useGroupOptions } from "../../hooks/use-group-options";
import { eSendEvent } from "../../services/event-manager";
import Sync from "../../services/sync";
import { RouteName } from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { eScrollEvent } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs";
import { Footer } from "../list-items/footer";
import { Header } from "../list-items/headers/header";
import { Empty, PlaceholderData } from "./empty";
import { ListItemWrapper } from "./list-item.wrapper";
type ListProps = {
data: VirtualizedGrouping<Item> | undefined;
dataType: Item["type"];
onRefresh?: () => void;
loading?: boolean;
headerTitle?: string;
customAccentColor?: string;
renderedInRoute?: RouteName;
CustomLisHeader?: React.JSX.Element;
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
id?: string;
};
export default function List(props: ListProps) {
const { colors } = useThemeColors();
const scrollRef = useRef();
const [notesListMode, notebooksListMode] = useSettingStore((state) => [
state.settings.notesListMode,
state.settings.notebooksListMode
]);
const isCompactModeEnabled =
(props.dataType === "note" && notesListMode === "compact") ||
props.dataType === "notebook" ||
notebooksListMode === "compact";
const groupType =
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash"
? "trash"
: `${props.dataType}s`;
const groupOptions = useGroupOptions(groupType);
const _onRefresh = async () => {
Sync.run("global", false, true, () => {
props.onRefresh?.();
});
};
const renderItem = React.useCallback(
({ index }: { index: number }) => {
return (
<ListItemWrapper
index={index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupOptions={groupOptions}
group={groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
scrollRef={scrollRef}
/>
);
},
[
groupOptions,
groupType,
props.customAccentColor,
props.data,
props.dataType,
props.isRenderedInActionSheet,
props.renderedInRoute
]
);
const onListScroll = React.useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!event) return;
eSendEvent(eScrollEvent, {
y: event.nativeEvent.contentOffset.y,
route: props.renderedInRoute,
id: props.id || props.renderedInRoute
});
},
[props.renderedInRoute, props.id]
);
useEffect(() => {
eSendEvent(eScrollEvent, {
y: 0,
route: props.renderedInRoute,
id: props.id || props.renderedInRoute
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const styles = {
width: "100%",
minHeight: 1,
minWidth: 1
};
const ListView = props.CustomListComponent
? props.CustomListComponent
: FlashList;
return (
<>
<Animated.View
style={{
flex: 1
}}
entering={props.renderedInRoute === "Search" ? undefined : FadeInDown}
>
<ListView
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
data={props.data?.placeholders || []}
renderItem={renderItem}
onScroll={onListScroll}
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
tabBarRef.current?.unlock();
}}
getItemType={(item: number, index: number) => {
return props.data?.type(index);
}}
estimatedItemSize={isCompactModeEnabled ? 60 : 120}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
refreshControl={
props.isRenderedInActionSheet ? null : (
<RefreshControl
tintColor={colors.primary.accent}
colors={[colors.primary.accent]}
progressBackgroundColor={colors.secondary.background}
onRefresh={_onRefresh}
refreshing={false}
/>
)
}
ListEmptyComponent={
props.placeholder ? (
<Empty
loading={props.loading}
title={props.headerTitle}
dataType={props.dataType}
color={props.customAccentColor}
placeholder={props.placeholder}
screen={props.renderedInRoute}
/>
) : null
}
ListFooterComponent={<Footer />}
ListHeaderComponent={
<>
{props.CustomLisHeader ? (
props.CustomLisHeader
) : !props.headerTitle ? null : (
<Header
color={props.customAccentColor}
screen={props.renderedInRoute}
/>
)}
</>
}
/>
</Animated.View>
</>
);
}

View File

@@ -0,0 +1,292 @@
/*
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 {
NotebooksWithDateEdited,
TagsWithDateEdited,
isNoteResolvedData,
resolveItems
} from "@notesnook/common";
import {
Color,
GroupHeader,
GroupOptions,
GroupingKey,
Item,
ItemType,
Note,
Notebook,
Reminder,
Tag,
TrashItem,
VirtualizedGrouping
} from "@notesnook/core";
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { eSendEvent } from "../../services/event-manager";
import { RouteName } from "../../stores/use-navigation-store";
import { eOpenJumpToDialog } from "../../utils/events";
import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
isSheet: boolean;
index: number;
renderedInRoute?: RouteName;
customAccentColor?: string;
dataType: string;
scrollRef: any;
groupOptions: GroupOptions;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
const { items, group, isSheet, index, groupOptions } = props;
const [item, setItem] = useState<Item>();
const tags = useRef<TagsWithDateEdited>();
const notebooks = useRef<NotebooksWithDateEdited>();
const reminder = useRef<Reminder>();
const color = useRef<Color>();
const totalNotes = useRef<number>(0);
const attachmentsCount = useRef(0);
const [groupHeader, setGroupHeader] = useState<GroupHeader>();
const previousIndex = useRef<number>();
const refreshTimeout = useRef<NodeJS.Timeout>();
const currentItemId = useRef<string>();
const locked = useRef(false);
const refreshItem = useCallback((resolvedItem: any) => {
if (!resolvedItem || !resolvedItem.data) {
tags.current = undefined;
notebooks.current = undefined;
reminder.current = undefined;
color.current = undefined;
attachmentsCount.current = 0;
totalNotes.current = 0;
locked.current = false;
}
if (resolvedItem && resolvedItem.item) {
const data = resolvedItem.data;
if (resolvedItem.item.type === "note" && isNoteResolvedData(data)) {
tags.current = data.tags;
notebooks.current = data.notebooks;
reminder.current = data.reminder;
color.current = data.color;
attachmentsCount.current = data.attachments?.total || 0;
locked.current = data.locked || false;
} else if (
resolvedItem.item.type === "note" &&
!isNoteResolvedData(data)
) {
tags.current = undefined;
notebooks.current = undefined;
reminder.current = undefined;
color.current = undefined;
attachmentsCount.current = 0;
totalNotes.current = 0;
locked.current = false;
} else if (
resolvedItem.item.type === "notebook" &&
typeof data === "number"
) {
totalNotes.current = data;
} else if (resolvedItem.item.type === "tag" && typeof data === "number") {
totalNotes.current = data;
}
currentItemId.current = resolvedItem.item.id;
setItem(resolvedItem.item);
setGroupHeader(resolvedItem.group);
}
}, []);
if (previousIndex.current !== index) {
previousIndex.current = index;
const resolvedItem = items?.cacheItem(index);
refreshItem(resolvedItem);
}
useEffect(() => {
(async function () {
try {
clearTimeout(refreshTimeout.current);
const idx = index;
refreshTimeout.current = setTimeout(async () => {
if (idx !== previousIndex.current) return;
const resolvedItem = await items?.item(idx, resolveItems);
if (idx !== previousIndex.current) return;
refreshItem(resolvedItem);
}, 100);
} catch (e) {
console.log("Error", e);
}
})();
}, [index, items, refreshItem]);
if (!item)
return (
<View
style={{
height: 120,
width: "100%"
}}
/>
);
const type = ((item as TrashItem).itemType || item.type) as ItemType;
switch (type) {
case "note": {
return (
<>
{groupHeader && previousIndex.current === index && !isSheet ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<NoteWrapper
item={item as Note}
tags={tags.current}
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
attachmentsCount={attachmentsCount.current}
date={getDate(item, group)}
isRenderedInActionSheet={isSheet}
index={index}
locked={locked.current}
/>
</>
);
}
case "notebook":
return (
<>
{groupHeader && previousIndex.current === index && !isSheet ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<NotebookWrapper
item={item as Notebook}
totalNotes={totalNotes.current}
date={getDate(item, group)}
index={index}
/>
</>
);
case "reminder":
return (
<>
{groupHeader && previousIndex.current === index && !isSheet ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<ReminderItem
item={item as Reminder}
index={index}
isSheet={isSheet}
/>
</>
);
case "tag":
return (
<>
{groupHeader && previousIndex.current === index && !isSheet ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<TagItem
item={item as Tag}
index={index}
totalNotes={totalNotes.current}
/>
</>
);
default:
return null;
}
}
function getDate(item: Item, groupType?: GroupingKey): number {
return (
getSortValue(
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},
item
) || 0
);
}

View File

@@ -0,0 +1,195 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { FlatList, StyleSheet, View } from "react-native";
import {
DraxList,
DraxListProps,
DraxListRenderItemContent,
DraxProvider
} from "react-native-drax";
import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size";
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
import { IconButton } from "../ui/icon-button";
interface ReorderableListProps<T extends { id: string }>
extends Omit<DraxListProps<T>, "renderItem" | "data" | "renderItemContent"> {
onListOrderChanged: (data: string[]) => void;
renderDraggableItem: DraxListRenderItemContent<T>;
data: T[];
itemOrder: string[];
hiddenItems: string[];
onHiddenItemsChanged?: (data: string[]) => void;
canHideItems?: boolean;
disableDefaultDrag?: boolean;
}
function ReorderableList<T extends { id: string }>({
renderDraggableItem,
data,
onListOrderChanged,
hiddenItems = [],
itemOrder = [],
onHiddenItemsChanged,
canHideItems = true,
disableDefaultDrag,
...restProps
}: ReorderableListProps<T>) {
const { colors } = useThemeColors();
const [itemOrderState, setItemsOrder] = useState(itemOrder);
const [hiddenItemsState, setHiddenItems] = useState(hiddenItems);
const dragging = useSideBarDraggingStore((state) => state.dragging);
const listRef = useRef<FlatList | null>(null);
if (dragging) {
tabBarRef.current?.lock();
} else {
tabBarRef.current?.unlock();
}
useEffect(() => {
setItemsOrder(itemOrder);
setHiddenItems(hiddenItems);
}, [itemOrder, hiddenItems]);
const renderItemContent: DraxListRenderItemContent<T> = React.useCallback(
(info, props) => {
const isHidden = hiddenItemsState.indexOf(info?.item?.id) > -1;
return isHidden && !dragging ? null : (
<View
style={{
flexDirection: "row",
paddingHorizontal: 12
}}
>
<View
style={{
flexGrow: 1,
opacity: isHidden ? 0.4 : 1
}}
>
{renderDraggableItem(info, props)}
</View>
{dragging && canHideItems ? (
<IconButton
name={!isHidden ? "minus" : "plus"}
color={colors.primary.icon}
size={SIZE.lg}
top={0}
bottom={0}
onPress={() => {
const _hiddenItems = hiddenItemsState.slice();
const index = _hiddenItems.indexOf(info.item.id);
if (index === -1) {
_hiddenItems.push(info.item?.id);
} else {
_hiddenItems.splice(index, 1);
}
onHiddenItemsChanged?.(_hiddenItems);
setHiddenItems(_hiddenItems);
}}
/>
) : null}
</View>
);
},
[
colors.primary.icon,
dragging,
hiddenItemsState,
onHiddenItemsChanged,
renderDraggableItem,
canHideItems
]
);
function getOrderedItems() {
const items: T[] = [];
itemOrderState.forEach((id) => {
const item = data.find((i) => i.id === id);
if (!item) return;
items.push(item);
});
items.push(...data.filter((i) => !itemOrderState.includes(i.id)));
return items;
}
return (
<DraxProvider>
<View style={styles.container}>
<DraxList
{...restProps}
ref={listRef}
data={getOrderedItems()}
renderItemContent={renderItemContent}
itemStyles={{
hoverDragReleasedStyle: {
display: "none"
},
hoverDraggingWithoutReceiverStyle: {
opacity: 0.5
},
dragReleasedStyle: {
opacity: 1
},
hoverDraggingStyle: {
backgroundColor: colors.secondary.background
}
}}
longPressDelay={500}
onItemDragStart={() =>
useSideBarDraggingStore.setState({
dragging: true
})
}
itemsDraggable={disableDefaultDrag ? dragging : true}
lockItemDragsToMainAxis
onItemReorder={({ fromIndex, fromItem, toIndex, toItem }) => {
const newOrder = getOrderedItems().map((item) => item.id);
const element = newOrder.splice(fromIndex, 1)[0];
if (toIndex === 0) {
newOrder.unshift(element);
} else {
newOrder.splice(toIndex, 0, element);
}
setItemsOrder(newOrder);
onListOrderChanged?.(newOrder);
}}
onItemDragEnd={(e) => {
console.log(e.receiver?.receiveOffset);
}}
keyExtractor={(item) => (item as any).id}
/>
</View>
</DraxProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1
}
});
export default ReorderableList;

View File

@@ -17,9 +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 { getFormattedDate } from "@notesnook/common";
import { useThemeColors } from "@notesnook/theme";
import KeepAwake from "@sayem314/react-native-keep-awake";
import React, { useEffect, useRef, useState } from "react";
import { Modal, SafeAreaView, Text, View } from "react-native";
import { SafeAreaView, Text, View } from "react-native";
import Animated from "react-native-reanimated";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
@@ -33,7 +35,7 @@ import {
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync";
import { useThemeColors } from "@notesnook/theme";
import { useSettingStore } from "../../stores/use-setting-store";
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
@@ -45,8 +47,7 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import { useSettingStore } from "../../stores/use-setting-store";
import { getFormattedDate } from "@notesnook/common";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
const MergeConflicts = () => {
const { colors } = useThemeColors();
@@ -62,7 +63,7 @@ const MergeConflicts = () => {
const applyChanges = async () => {
let _content = keep;
let note = db.notes.note(_content.noteId).data;
let note = await db.notes.note(_content.noteId);
await db.notes.add({
id: note.id,
conflicted: false,
@@ -88,10 +89,11 @@ const MergeConflicts = () => {
});
}
Navigation.queueRoutesForUpdate();
if (editorController.current?.note?.id === note.id) {
if (useTabStore.getState().getCurrentNoteId() === note.id) {
// reload the note in editor
eSendEvent(eOnLoadNote, {
...editorController.current?.note,
item: editorController.current.note.current[note.id],
forced: true
});
}
@@ -100,7 +102,7 @@ const MergeConflicts = () => {
};
const show = async (item) => {
let noteContent = await db.content.raw(item.contentId);
let noteContent = await db.content.get(item.contentId);
content.current = { ...noteContent };
if (__DEV__) {
if (!noteContent.conflicted) {
@@ -190,7 +192,7 @@ const MergeConflicts = () => {
setDialogVisible(true);
}}
title="Save a copy"
type="grayBg"
type="secondary"
height={30}
style={{
borderRadius: 100,
@@ -320,14 +322,16 @@ const MergeConflicts = () => {
readonly
editorId=":conflictPrimary"
onLoad={async () => {
const note = db.notes.note(content.current?.noteId)?.data;
const note = await db.notes.note(content.current?.noteId);
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictPrimary", {
...note,
content: {
...content.current,
isPreview: true
item: {
...note,
content: {
...content.current,
isPreview: true
}
}
});
}}
@@ -355,12 +359,14 @@ const MergeConflicts = () => {
readonly
editorId=":conflictSecondary"
onLoad={async () => {
const note = db.notes.note(content.current?.noteId)?.data;
const note = await db.notes.note(content.current?.noteId);
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictSecondary", {
...note,
content: { ...content.current.conflicted, isPreview: true }
item: {
...note,
content: { ...content.current.conflicted, isPreview: true }
}
});
}}
/>

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