Compare commits

..

277 Commits

Author SHA1 Message Date
Ammar Ahmed
8e34679fd5 mobile: beta release 2024-02-10 22:36:29 +05:00
Ammar Ahmed
c2200ee8e1 mobile: fix notebook move to trash 2024-02-10 22:34:13 +05:00
Ammar Ahmed
4c86bac8e0 mobile: cleanup 2024-02-10 22:34:13 +05:00
Abdullah Atta
b152fa7463 desktop: disable downgrades 2024-02-10 22:16:39 +05:00
Ammar Ahmed
949f6ace54 mobile: stop app updates check in beta 2024-02-10 21:23:02 +05:00
Ammar Ahmed
5c6fd953eb mobile: minor fixes 2024-02-10 21:16:26 +05:00
Abdullah Atta
0326c3fee9 web: improve database deletion 2024-02-10 19:52:54 +05:00
Abdullah Atta
d7967d322e web: reload page on reset from error boundary 2024-02-10 19:52:54 +05:00
Abdullah Atta
62d2a4471d web: locked status not updating on note 2024-02-10 19:52:54 +05:00
Abdullah Atta
42df6536c0 web: only save note's content through vault 2024-02-10 19:52:54 +05:00
Abdullah Atta
e9d9252182 core: fix adding note to vault should return its id 2024-02-10 19:52:53 +05:00
Abdullah Atta
a400350e48 core: fix database resetting 2024-02-10 19:52:53 +05:00
Abdullah Atta
1dcbf93b3a theme: fix error input variant 2024-02-10 19:52:53 +05:00
Abdullah Atta
620c0b5006 web: allow typing custom date when adding reminder 2024-02-10 19:52:53 +05:00
Abdullah Atta
d3468401f9 common: use upcoming reminder when resolving reminders on note 2024-02-10 19:52:53 +05:00
Ammar Ahmed
a6b83f503e mobile: fix reminders 2024-02-10 19:06:16 +05:00
Ammar Ahmed
e52ad6186f mobile: fix notebooks & colors 2024-02-10 18:56:45 +05:00
Abdullah Atta
59486a7894 core: set mode to "sync" for migrations during sync 2024-02-10 13:42:43 +05:00
Abdullah Atta
76fbfd3251 desktop: patch better-sqlite3-multiple-ciphers to get prebuilt binaries 2024-02-10 13:30:45 +05:00
Abdullah Atta
804dff4a53 web: bump to 3.0.1-beta 2024-02-10 11:11:45 +05:00
Abdullah Atta
a1df8fbf21 core: enable sync during tests 2024-02-10 11:08:48 +05:00
Abdullah Atta
24c69a8c73 core: improve backup importing 2024-02-10 11:00:51 +05:00
Abdullah Atta
16e483954e core: add support for nested transactions 2024-02-10 11:00:51 +05:00
Abdullah Atta
7315192e1a web: use single transaction for importing a backup 2024-02-10 11:00:51 +05:00
Abdullah Atta
f0d3cca188 web: make sqlite slightly faster 2024-02-10 11:00:51 +05:00
Ammar Ahmed
557baf43a3 mobile: fix release build 2024-02-09 12:24:46 +05:00
Abdullah Atta
b9c8ac091f core: fix type errors 2024-02-09 11:06:11 +05:00
Ammar Ahmed
5add1b1d48 mobile: minor fixes 2024-02-09 10:41:21 +05:00
Ammar Ahmed
2734e50a97 core: fix migration bugs 2024-02-09 10:41:00 +05:00
Ammar Ahmed
ab7814c0cb mobile: fix saving user key 2024-02-07 23:04:27 +05:00
Abdullah Atta
f07269f4ea web: fix monograph publishing popup 2024-02-07 19:52:15 +05:00
Abdullah Atta
131ed88906 web: ui fixes 2024-02-07 19:35:50 +05:00
Abdullah Atta
d03b46d2c1 web: fix app lock not getting disabled 2024-02-07 19:35:49 +05:00
Ammar Ahmed
848c25adeb mobile: minor fixes 2024-02-07 19:18:10 +05:00
Ammar Ahmed
61f5153c9f mobile: locked notes 2024-02-07 18:52:35 +05:00
Abdullah Atta
86b6c71501 web: fix indexedDB not getting detected 2024-02-07 15:59:48 +05:00
Abdullah Atta
fa11312c46 core: destroy kysely on error 2024-02-07 13:40:17 +05:00
Abdullah Atta
e032852632 crypto: move libsodium & sodium-native to peer deps 2024-02-07 13:40:17 +05:00
Abdullah Atta
9a4ebb5662 global: pin node version to 20 2024-02-07 13:40:17 +05:00
Abdullah Atta
7631612935 web: various fixes 2024-02-07 13:40:17 +05:00
Abdullah Atta
d5ce877d11 common: fix note resolution 2024-02-07 13:40:17 +05:00
Abdullah Atta
446005b8e2 core: fix database migration 2024-02-07 13:40:17 +05:00
Abdullah Atta
dd7765df97 core: disable sync push 2024-02-07 13:40:17 +05:00
Abdullah Atta
b32b3a5af3 web: trigger error boundary on unhandled errors 2024-02-07 13:40:16 +05:00
Abdullah Atta
e3cc211355 web: use safeStorage to encrypt secrets on desktop 2024-02-07 13:40:16 +05:00
Abdullah Atta
41fed6e347 desktop: use better-sqlite3 on desktop for SQLite 2024-02-07 13:40:16 +05:00
Abdullah Atta
8766966c27 desktop: fix autoupdater 2024-02-07 13:40:16 +05:00
Abdullah Atta
a2972ea9e6 desktop: add api to delete files 2024-02-07 13:40:16 +05:00
Abdullah Atta
99a3f9e779 desktop: expose electron's safeStorage api via trpc 2024-02-07 13:40:16 +05:00
Abdullah Atta
33aad3ec3f desktop: enable native modules rebuild during build 2024-02-07 13:40:16 +05:00
Ammar Ahmed
92c6e9c0bb core: return decrypted content on unlock note 2024-02-07 13:31:53 +05:00
Ammar Ahmed
7a0ec7ca11 mobile: applock with pin and password 2024-02-07 13:03:25 +05:00
Ammar Ahmed
fd1e39cee4 mobile: minor fixes 2024-02-07 11:00:24 +05:00
Ammar Ahmed
bc0f92ea5d mobile: sqlite-multiple-ciphers patch 2024-02-06 23:15:40 +05:00
Ammar Ahmed
2b2a9deb71 mobile: refactor 2024-02-06 12:08:41 +05:00
Ammar Ahmed
b2ac5965d4 mobile: fix reorderable side menu 2024-02-06 00:00:14 +05:00
Ammar Ahmed
7397d5bd01 mobile: yearly reminders 2024-02-05 23:00:46 +05:00
Abdullah Atta
7bca839f6d web: add support for yearly reminders 2024-02-05 22:58:50 +05:00
Abdullah Atta
017b466dc0 core: add support for yearly reminders 2024-02-05 22:57:43 +05:00
Ammar Ahmed
b4f7a7112b mobile: stop bg fetch events when toggled off 2024-02-05 22:55:50 +05:00
Ammar Ahmed
47c6b80594 mobile: enable experimental background sync 2024-02-05 22:55:19 +05:00
Abdullah Atta
4e28793c0e web: add support for hiding items from side bar 2024-02-05 22:50:50 +05:00
Abdullah Atta
24f1307955 web: add support for reordering navigation menu items 2024-02-05 22:50:45 +05:00
Ammar Ahmed
56a9b5a5d9 mobile: cleanup 2024-02-05 22:47:54 +05:00
Ammar Ahmed
884fbf65e9 core: add sidebar order settings 2024-02-05 22:47:54 +05:00
Ammar Ahmed
bc03ad8339 mobile: reorder sidebar items 2024-02-05 22:47:43 +05:00
Abdullah Atta
2fca6b2431 web: add support for custom colors 2024-02-05 22:39:23 +05:00
Ammar Ahmed
4067a7f7c7 mobile: allow custom colors 2024-02-05 22:38:55 +05:00
Ammar Ahmed
a9179734d0 mobile: migrate app lock 2024-02-05 22:34:46 +05:00
Abdullah Atta
0f2f2f085b web: remove cbor-x package 2024-02-05 22:31:37 +05:00
Abdullah Atta
b9e04b763f web: add support for at rest encryption and app lock 2024-02-05 22:30:46 +05:00
Ammar Ahmed
eb23224687 mobile: at rest encryption 2024-02-05 22:28:51 +05:00
Ammar Ahmed
70f39fdc2d mobile: simplify tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
3eea4f73e4 core: remove dummy test files 2024-02-05 22:21:14 +05:00
Abdullah Atta
226b9f5892 core: migrate key value storage to sqlite 2024-02-05 22:21:14 +05:00
Ammar Ahmed
47ef642648 mobile: update sheet component 2024-02-05 22:21:14 +05:00
Abdullah Atta
35b7ef9204 core: fix moving nested notebooks to trash 2024-02-05 22:21:14 +05:00
Abdullah Atta
cf024eefd8 global: move item resolution logic to @notesnook/common 2024-02-05 22:21:14 +05:00
Ammar Ahmed
6bba171df0 mobile: add theme switch animation 2024-02-05 22:21:14 +05:00
Abdullah Atta
e52492488f web: fix all type errors 2024-02-05 22:21:14 +05:00
Abdullah Atta
bdaa2a5977 web: fix duplicate imports 2024-02-05 22:21:14 +05:00
Abdullah Atta
cd85cc32e2 core: fix type error 2024-02-05 22:21:14 +05:00
Abdullah Atta
1d7d5fd565 core: add tests for sync merger 2024-02-05 22:21:14 +05:00
Abdullah Atta
4baeee9429 core: migrate vault to a database collection 2024-02-05 22:21:14 +05:00
Ammar Ahmed
667a760c89 mobile: fix exporter and attachment download 2024-02-05 22:21:14 +05:00
Ammar Ahmed
da3ce62f08 global: fix type errors during build 2024-02-05 22:21:14 +05:00
Abdullah Atta
b5adf70853 web: use simple transform stream for export progress 2024-02-05 22:21:14 +05:00
Abdullah Atta
973c6e0d6f web: fix exporting 2024-02-05 22:21:14 +05:00
Ammar Ahmed
ca15a016fd mobile: add patches 2024-02-05 22:21:14 +05:00
Abdullah Atta
5cce557a14 web: update export note snapshots for linux 2024-02-05 22:21:14 +05:00
Abdullah Atta
e81ee6e4ce web: fix reminder tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
339e3444e7 web: refresh notebooks after adding a new notebook 2024-02-05 22:21:14 +05:00
Abdullah Atta
a32df1a009 web: fix force redirect to home on navigating to a color 2024-02-05 22:21:14 +05:00
Abdullah Atta
cb7e438cd2 web: fix crash when sorting tags 2024-02-05 22:21:14 +05:00
Abdullah Atta
5cf5c1a7f7 web: fix crash when removing a notebook 2024-02-05 22:21:14 +05:00
Ammar Ahmed
35ae19acce mobile: sync with latest core changes 2024-02-05 22:21:14 +05:00
Abdullah Atta
e4130556e9 setup: optimize bootstrap script 2024-02-05 22:21:14 +05:00
Abdullah Atta
8dc25e9cac setup: only run npm rebuild if required 2024-02-05 22:21:14 +05:00
Abdullah Atta
90cc89cd6e core: remove unnecessary console.log 2024-02-05 22:21:14 +05:00
Abdullah Atta
4fc7c49ed5 setup: run npm rebuild after bootstrap 2024-02-05 22:21:14 +05:00
Abdullah Atta
387a05e24e setup: ignore scripts during bootstrap 2024-02-05 22:21:14 +05:00
Abdullah Atta
0129513ec6 setup: use 1 thread on CI 2024-02-05 22:21:14 +05:00
Ammar Ahmed
20e5f7babb mobile: fix search to use filtered selector for notes 2024-02-05 22:21:14 +05:00
Abdullah Atta
12dc4caff3 core: prevent recursive sync after pushing items 2024-02-05 22:21:14 +05:00
Abdullah Atta
58bab91bc8 web: simplify sync status 2024-02-05 22:21:14 +05:00
Abdullah Atta
78a25dccdd web: fix previewing trashed notes 2024-02-05 22:21:14 +05:00
Abdullah Atta
1ce15d8e50 core: fix trash 2024-02-05 22:21:14 +05:00
Abdullah Atta
4fb10e3507 web: improve sync test reliability 2024-02-05 22:21:14 +05:00
Abdullah Atta
f5e43f753f core: auto sync not triggered on note update 2024-02-05 22:21:14 +05:00
Abdullah Atta
f088ddf50b web: fix note history tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
60974182e5 web: fix selection logic in list 2024-02-05 22:21:14 +05:00
Abdullah Atta
d1c613c2ec web: set testId on auth input fields 2024-02-05 22:21:14 +05:00
Abdullah Atta
676ffd1116 core: fix note content is not saving 2024-02-05 22:21:14 +05:00
Abdullah Atta
c759350b4c core: fix data is not iterable error on backup restore 2024-02-05 22:21:14 +05:00
Abdullah Atta
930fadeae4 global: update lockfiles 2024-02-05 22:21:14 +05:00
Abdullah Atta
3728179a5b core: add retry if a test fails 2024-02-05 22:21:14 +05:00
Abdullah Atta
0fa9649997 core: fix invalid object-id generation for some timestamps 2024-02-05 22:21:14 +05:00
Abdullah Atta
c8b2b14412 core: fix sync & monographs tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
fe6007bc51 core: fix auto sync starting even when auto sync is disabled 2024-02-05 22:21:14 +05:00
Abdullah Atta
4df150d8a0 core: fix sync tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
d62ee05e4b core: remove unnecessary migration success log 2024-02-05 22:21:14 +05:00
Abdullah Atta
a88d9ee134 core: fix Crypto is not supported on this platform. error during tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
f77a95ca2b core: make all the core tests pass again 2024-02-05 22:21:14 +05:00
Abdullah Atta
5d2c6e0509 web: fix crash when note has no title 2024-02-05 22:21:14 +05:00
Abdullah Atta
8064cecc89 core: set default title if note has no title 2024-02-05 22:21:14 +05:00
Abdullah Atta
bf238ea560 web: update kysely patch 2024-02-05 22:21:14 +05:00
Abdullah Atta
b0c69b5d5a core: run patch-package after npm i 2024-02-05 22:21:14 +05:00
Abdullah Atta
82a0273cac web: run patch-package after npm i 2024-02-05 22:21:14 +05:00
Abdullah Atta
28edcadc22 web: more fixes 2024-02-05 22:21:14 +05:00
Abdullah Atta
6ab5264587 web: fix many type errors 2024-02-05 22:21:14 +05:00
Abdullah Atta
b732337ca2 global: update lockfiles 2024-02-05 22:21:14 +05:00
Abdullah Atta
c164ec4de7 theme: fix tinycolor2 not found error 2024-02-05 22:21:14 +05:00
Abdullah Atta
30f4fb3d9a web: fix notes search in tags, favorites etc 2024-02-05 22:21:14 +05:00
Abdullah Atta
aa12f5975f web: update wa-sqlite 2024-02-05 22:21:14 +05:00
Abdullah Atta
f3c2757b2a web: use memory tempStore for sqlite 2024-02-05 22:21:14 +05:00
Abdullah Atta
f1f67f871c web: add support for exporting sql database 2024-02-05 22:21:14 +05:00
Abdullah Atta
f352b8edec core: fix FTS5 SQLite search 2024-02-05 22:21:14 +05:00
Ammar Ahmed
a37dababe3 mobile: cleanup 2024-02-05 22:21:14 +05:00
Ammar Ahmed
fee13715f4 mobile: fix types 2024-02-05 22:21:14 +05:00
Ammar Ahmed
b046f17644 mobile: refactor zustand stores 2024-02-05 22:21:14 +05:00
Ammar Ahmed
890bb6e057 mobile: bug fixes 2024-02-05 22:21:14 +05:00
Ammar Ahmed
96ad097434 core: add fn to get cache item 2024-02-05 22:21:14 +05:00
Abdullah Atta
74e921930b web: only auto align title & headline 2024-02-05 22:21:14 +05:00
Abdullah Atta
e8eb0ee598 web: getKey -> key 2024-02-05 22:21:14 +05:00
Abdullah Atta
9fc7d644fc web: fix resolved item data misaligned with items 2024-02-05 22:21:14 +05:00
Abdullah Atta
2c6c7f7774 core: fix many issues with virtualized grouping 2024-02-05 22:21:14 +05:00
Ammar Ahmed
e9f5f22601 core: early return when items are empty 2024-02-05 22:21:14 +05:00
Abdullah Atta
2db4bd99b6 web: fix random crashes when scrolling in list 2024-02-05 22:21:14 +05:00
Abdullah Atta
65edcf8e6c web: replace react-virtual with react-virtuouso 2024-02-05 22:21:14 +05:00
Ammar Ahmed
8b927ca94f mobile: fix build ang bugs 2024-02-05 22:21:14 +05:00
Abdullah Atta
81fe67253a web: hide attachments access if user is logged out 2024-02-05 22:21:14 +05:00
Abdullah Atta
4cb418ff75 web: allow core to specify custom sql db name 2024-02-05 22:21:14 +05:00
Abdullah Atta
4cf281da08 web: close idb connection on sqlite connection close 2024-02-05 22:21:14 +05:00
Abdullah Atta
0682345db8 core: clear database on logout 2024-02-05 22:21:14 +05:00
Ammar Ahmed
f5613bd7ae core: add tempStore option to DB 2024-02-05 22:21:14 +05:00
Abdullah Atta
b121f75548 core: migrate items sequentially during sync 2024-02-05 22:21:14 +05:00
Abdullah Atta
1c837f5fc2 core: fix build error 2024-02-05 22:21:14 +05:00
Abdullah Atta
255bda8e61 web: execute only 1 query at a time 2024-02-05 22:21:14 +05:00
Abdullah Atta
c8c1871906 web: fix diff viewer crashing the app 2024-02-05 22:21:14 +05:00
Abdullah Atta
d56916fc33 core: do not stop sync if a conflict occurs 2024-02-05 22:21:14 +05:00
Abdullah Atta
a3fab3b5ca core: allow only 1 transaction at a time 2024-02-05 22:21:14 +05:00
Abdullah Atta
321bd2aa93 core: simplify item key derivation in virtualized grouping 2024-02-05 22:21:14 +05:00
Abdullah Atta
05ccfcd9fb web: improve list item rendering performance 2024-02-05 22:21:14 +05:00
Abdullah Atta
159c4454a3 core: always send realtime sync event 2024-02-05 22:21:14 +05:00
Abdullah Atta
371d7e527f core: fix content date edited not updating 2024-02-05 22:21:14 +05:00
Abdullah Atta
5de4e16b12 core: put items in chunks of 200 2024-02-05 22:21:14 +05:00
Abdullah Atta
bba809cef9 web: set name prop on fields 2024-02-05 22:21:14 +05:00
Abdullah Atta
384db9d27e core: implement sync v2 2024-02-05 22:21:14 +05:00
Ammar Ahmed
b236e7c9b8 mobile: use index based grouping 2024-02-05 22:21:14 +05:00
Abdullah Atta
fceb8a29c7 web: changes to use new grouping api 2024-02-05 22:21:14 +05:00
Abdullah Atta
c2791f2bbf core: implement new high perf indexed based grouping 2024-02-05 22:21:14 +05:00
Ammar Ahmed
4a7d3f5caf mobile: fix share extension in release 2024-02-05 22:21:14 +05:00
Abdullah Atta
e11fea58f6 web: fix router types 2024-02-05 22:21:14 +05:00
Abdullah Atta
ff33ced879 web: enable vite hot reloading 2024-02-05 22:21:14 +05:00
Abdullah Atta
5f7068c376 web: fix minor build issues 2024-02-05 22:21:14 +05:00
Abdullah Atta
c988c510ba web: migrate everything to react@18 2024-02-05 22:21:14 +05:00
Abdullah Atta
4917a888f1 web: fix notebook tests 2024-02-05 22:21:14 +05:00
Ammar Ahmed
3ba5250d59 mobile: share extension cleanup 2024-02-05 22:21:14 +05:00
Ammar Ahmed
d595913722 mobile: cleanup 2024-02-05 22:21:14 +05:00
Ammar Ahmed
1f94c47880 mobile: migrate share extension 2024-02-05 22:21:14 +05:00
Ammar Ahmed
c1b7c66e81 mobile: fix navigation 2024-02-05 22:21:14 +05:00
Abdullah Atta
8712dd286f web: send noteId when decrypting real-time locked content 2024-02-05 22:21:14 +05:00
Abdullah Atta
d836170b6f web: fix tag tests 2024-02-05 22:21:14 +05:00
Ammar Ahmed
0f2122bc41 mobile: pass tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
77a5f83a84 web: fix html export snapshot 2024-02-05 22:21:14 +05:00
Abdullah Atta
f618f97845 core: fix md with frontmatter export 2024-02-05 22:21:14 +05:00
Ammar Ahmed
26bdab05dd core: fix ts error 2024-02-05 22:21:14 +05:00
Ammar Ahmed
ccd078f660 mobile: fix webpack build 2024-02-05 22:21:14 +05:00
Ammar Ahmed
74c4fdac21 mobile: push changes 2024-02-05 22:21:14 +05:00
Abdullah Atta
fc270459b0 web: fix notes tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
a51f84c1f4 core: prevent duplicate colors based on color code 2024-02-05 22:21:14 +05:00
Abdullah Atta
c452e1524d core: fix error on locking notes 2024-02-05 22:21:14 +05:00
Ammar Ahmed
bc428dd11f mobile: push changes 2024-02-05 22:21:14 +05:00
Abdullah Atta
8311db0572 web: reset search results on navigation 2024-02-05 22:21:14 +05:00
Abdullah Atta
26d08615a7 web: reset prepared sql stmt on error 2024-02-05 22:21:14 +05:00
Abdullah Atta
130f39ef8a web: reuse search query 2024-02-05 22:21:14 +05:00
Abdullah Atta
4b83e7319b core: use porter trigram tokenizer for fts 2024-02-05 22:21:14 +05:00
Abdullah Atta
96f45bf61e core: properly escape fts query 2024-02-05 22:21:14 +05:00
Abdullah Atta
ebf20c31c8 web: add support for assigning tags to notes 2024-02-05 22:21:14 +05:00
Abdullah Atta
9c858c0037 core: fix "type" does not exist 2024-02-05 22:21:14 +05:00
Abdullah Atta
16b68aae4f core: fix build error 2024-02-05 22:21:14 +05:00
Ammar Ahmed
3179c0ca2a mobile: migrate to sqlite 2024-02-05 22:21:14 +05:00
Abdullah Atta
905c7cdf31 web: fix note editor properties 2024-02-05 22:21:14 +05:00
Abdullah Atta
dbeb6f7835 common: type is now optional in getFormattedDate 2024-02-05 22:21:14 +05:00
Abdullah Atta
72015fcbeb core: return FilteredSelector from attachments.ofNote 2024-02-05 22:21:14 +05:00
Abdullah Atta
d32f3994e8 web: show notebook & tag shortcuts when assigning notes 2024-02-05 22:21:14 +05:00
Abdullah Atta
eb9f99dc4b web: implement virtualized search for all items 2024-02-05 22:21:14 +05:00
Abdullah Atta
803ef1ca36 web: use lazy loading to load attachments 2024-02-05 22:21:14 +05:00
Abdullah Atta
9b420c200a web: load tag & color title lazily 2024-02-05 22:21:14 +05:00
Abdullah Atta
204d8f37fd web: show all nested notebooks as tree when moving notes 2024-02-05 22:21:14 +05:00
Abdullah Atta
3d202c834a web: add support for nested notebooks 2024-02-05 22:21:14 +05:00
Abdullah Atta
d5adb0c195 web: use new virtualized grouping for reminders 2024-02-05 22:21:14 +05:00
Abdullah Atta
fd4174d8d5 web: use new virtualized grouping for trash 2024-02-05 22:21:14 +05:00
Abdullah Atta
cb376a6144 web: use new virtualized grouping for monographs 2024-02-05 22:21:14 +05:00
Abdullah Atta
254db0fb49 core: add support for fetching trash as grouped 2024-02-05 22:21:14 +05:00
Abdullah Atta
34ec5c0eaa web: install missing @hazae41/foras 2024-02-05 22:21:14 +05:00
Abdullah Atta
9b31bb8598 core: fix attachment extraction for locked notes 2024-02-05 22:21:14 +05:00
Abdullah Atta
9f33de94cb global: update lockfiles 2024-02-05 22:21:14 +05:00
Abdullah Atta
f3cbcf47a5 web: start initial migration to sqlite 2024-02-05 22:21:14 +05:00
Abdullah Atta
ec7e81d5e8 ui: add support for lazily loading menu items 2024-02-05 22:21:14 +05:00
Abdullah Atta
200547415c common: remove total-notes utility 2024-02-05 22:21:14 +05:00
Abdullah Atta
4322559680 core: changes according to client needs 2024-02-05 22:21:14 +05:00
Abdullah Atta
de187ec433 core: fix database migration 2024-02-05 22:21:14 +05:00
Abdullah Atta
9e9a25861d core: add support for incremental grouping 2024-02-05 22:21:14 +05:00
Abdullah Atta
3ecf9eab3b core: fix size of nanoid in benchmarks 2024-02-05 22:21:14 +05:00
Abdullah Atta
a0f8666b7a core: optimize bson objectid generation & add benchmarks 2024-02-05 22:21:14 +05:00
Abdullah Atta
7cf8895b4d core: fix monographs 2024-02-05 22:21:14 +05:00
Abdullah Atta
84b786b5a3 core: fix sync tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
b28ab28f57 core: remove temporary sqlite db 2024-02-05 22:21:14 +05:00
Abdullah Atta
70ef954de5 core: migrate everything to sqlite 2024-02-05 22:21:14 +05:00
Abdullah Atta
608761ee0d core: migrate all database writes to sqlite 2024-02-05 22:21:14 +05:00
Abdullah Atta
a70b309df8 global: update lockfiles 2024-02-05 22:21:14 +05:00
Abdullah Atta
58ef5050c4 core: remove outbox 2024-02-05 22:21:14 +05:00
Abdullah Atta
c1594c1687 core: convert everything else to typescript 2024-02-05 22:21:14 +05:00
Abdullah Atta
bcb8c14abf core: remove unnecessary sync tests 2024-02-05 22:21:14 +05:00
Abdullah Atta
adb325af4d core: migrate monographs to typescript 2024-02-05 22:21:14 +05:00
Abdullah Atta
315961eaae core: make migration tests less flaky 2024-02-05 22:21:14 +05:00
Abdullah Atta
65d3ef87d2 core: add tests for migrations 2024-02-05 22:21:14 +05:00
Abdullah Atta
d4d9ebf378 core: migrate settings to its own collection 2024-02-05 22:21:14 +05:00
Abdullah Atta
d90fa07ed8 core: get rid of topics; move to sub notebooks 2024-02-05 22:21:14 +05:00
Abdullah Atta
adae1b6187 core: migrate attachments to use relations for linking to notes 2024-02-05 22:21:14 +05:00
Abdullah Atta
b40f52b12b core: minor updates and fixes 2024-02-05 22:21:14 +05:00
Abdullah Atta
0b8e0f9848 core: fix types & syncing issues 2024-02-05 22:21:14 +05:00
ammarahm-ed
8cbb7f88d6 mobile: pass all tests 2024-02-05 22:21:14 +05:00
ammarahm-ed
17201979ba mobile: fix filesystem not loaded 2024-02-05 22:21:14 +05:00
ammarahm-ed
0bcd162559 mobile: fix navigating to tags 2024-02-05 22:21:14 +05:00
ammarahm-ed
fe2fc26bc0 mobile: migrate to typescript 2024-02-05 22:21:14 +05:00
ammarahm-ed
7b13d064e7 common: fix ts errors 2024-02-05 22:21:14 +05:00
Abdullah Atta
cad18cb9e8 mobile: use new types from core 2024-02-05 22:21:14 +05:00
Abdullah Atta
fa45ead4cb web: use new types from core 2024-02-05 22:21:14 +05:00
Abdullah Atta
e781273345 core: fix more typings 2024-02-05 22:21:14 +05:00
Abdullah Atta
d28179e0cd core: fix typing errors 2024-02-05 22:21:14 +05:00
Abdullah Atta
ee6a515683 core: convert almost everything to typescript 2024-02-05 22:21:14 +05:00
Abdullah Atta
37263304fa core: fix attachment mime type on read 2024-02-05 22:21:14 +05:00
Ammar Ahmed
e96ba80583 mobile: fix lint errors 2024-02-02 18:46:30 +05:00
Abdullah Atta
cfd80d2008 ci: update node version to 20 2024-02-02 18:35:42 +05:00
Abdullah Atta
288226a3af config: update nx 2024-02-02 18:32:53 +05:00
Abdullah Atta
4d2c9cfd4e desktop: set release channel to beta 2024-02-02 18:03:38 +05:00
Ammar Ahmed
fe52ce38e1 mobile: update ios version number 2024-02-02 15:51:05 +05:00
Ammar Ahmed
46acdf4279 ci: update workflow name
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-02-02 15:39:46 +05:00
Ammar Ahmed
1f698280df mobile: v3 beta release 2024-02-02 15:32:11 +05:00
Ammar Ahmed
94babf2a97 ci: update iOS workflow
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-02-02 15:12:47 +05:00
Ammar Ahmed
b19be80738 Update android.publish.internal.yml
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-02-02 15:11:54 +05:00
Ammar Ahmed
f77746cf0e ci: android.publish.yml
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-02-02 15:10:38 +05:00
Abdullah Atta
d3c033faf2 desktop: do not set teamId for notarization 2024-01-31 16:34:08 +05:00
Abdullah Atta
b9f22e1dd0 ci: APPLE_API_KEY_ISSUER > APPLE_API_ISSUER 2024-01-31 16:26:10 +05:00
Abdullah Atta
b0dd1c2095 desktop: fix unknown property 'notarize' error 2024-01-31 16:14:43 +05:00
Abdullah Atta
0beffd5165 desktop: use official electron builder notarization 2024-01-31 15:45:37 +05:00
Abdullah Atta
a1c0a834ee ci: update {upload,download}-artifact to v4 2024-01-31 15:34:10 +05:00
Abdullah Atta
27dc19cb58 ci: get rid of you may only define one of branches and branches-ignore for a single event 2024-01-31 15:20:21 +05:00
Abdullah Atta
896884ff59 ci: use m1 for building on mac 2024-01-31 15:19:10 +05:00
Abdullah Atta
97cb724bc1 ci: use wrangler directly to publish to cf 2024-01-31 14:52:18 +05:00
Abdullah Atta
f08e372bcf desktop: use app version from autoUpdater 2024-01-31 14:46:42 +05:00
Abdullah Atta
0aecb2d27a ci: update cloudflare action 2024-01-31 14:34:24 +05:00
Abdullah Atta
3735d9ee18 desktop: ready beta for deployment 2024-01-31 14:31:42 +05:00
Abdullah Atta
3d4264257e ci: deploy to correct cf pages project
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-31 12:53:37 +05:00
Abdullah Atta
96b97c4bec core: turn off sync push by default 2024-01-31 12:47:53 +05:00
737 changed files with 297028 additions and 91607 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

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: macos-13
runs-on: macos-14
timeout-minutes: 60
steps:
@@ -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/cache

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.1-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
});

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();
@@ -54,14 +55,6 @@ export const osIntegrationRouter = t.router({
config.zoomFactor = factor;
}),
proxyRules: t.procedure.query(() => config.proxyRules),
setProxyRules: t.procedure
.input(z.string().optional())
.mutation(({ input: proxyRules }) => {
globalThis.window?.webContents.session.setProxy({ proxyRules });
config.proxyRules = proxyRules || "";
}),
privacyMode: t.procedure.query(() => config.privacyMode),
setPrivacyMode: t.procedure
.input(z.object({ enabled: z.boolean() }))
@@ -134,10 +127,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

@@ -16,27 +16,23 @@ 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";
class Session {
const t = initTRPC.create();
export const safeStorageRouter = t.router({
/**
*
* @param {import("../database/storage").default} context
* Takes a string and returns an encrypted base64 string
*/
constructor(context) {
this._storage = context;
}
get() {
return this._storage.read("t");
}
set() {
return this._storage.write("t", Date.now());
}
async valid() {
const t = await this.get();
return !t || t < Date.now();
}
}
export default Session;
encryptString: t.procedure.input(z.string()).query(async ({ input }) => {
return safeStorage.encryptString(input).toString("base64");
}),
/**
* Takes an encrypted base64 string and returns a string
*/
decryptString: t.procedure.input(z.string()).query(async ({ input }) => {
return safeStorage.decryptString(Buffer.from(input, "base64"));
})
});

View File

@@ -118,13 +118,5 @@ export const spellCheckerRouter = t.router({
.mutation(({ input: { enabled } }) => {
globalThis.window?.webContents.session.setSpellCheckerEnabled(enabled);
config.isSpellCheckerEnabled = enabled;
}),
words: t.procedure.query(() =>
globalThis.window?.webContents.session.listWordsInSpellCheckerDictionary()
),
deleteWord: t.procedure.input(z.string()).mutation(({ input: word }) => {
globalThis.window?.webContents.session.removeWordFromSpellCheckerDictionary(
word
);
})
})
});

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

@@ -112,7 +112,6 @@ async function createWindow() {
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
);
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
mainWindow.once("closed", () => {
globalThis.window = null;

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

@@ -25,11 +25,7 @@ Type=Application
Version=${app.getVersion()}
Name=${app.getName()}
Comment=${app.getName()} startup script
Exec=${
process.env.APPIMAGE
? `${process.env.APPIMAGE}${hidden ? " --hidden" : ""}`
: `${process.execPath}${hidden ? " --hidden" : ""}`
}
Exec=${process.execPath}${hidden ? " --hidden" : ""}
StartupNotify=false
Terminal=false`;

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

@@ -41,8 +41,7 @@ export const config = {
isSpellCheckerEnabled: true,
zoomFactor: 1,
theme: nativeTheme.themeSource,
automaticUpdates: true,
proxyRules: ""
automaticUpdates: true
};
type ConfigKey = keyof typeof config;

View File

@@ -131,17 +131,6 @@ function setupMenu() {
);
}
if (params.mediaType === "image")
menu.append(
new MenuItem({
id: "copy-image",
label: "Copy Image",
click() {
globalThis.window?.webContents.copyImageAt(params.x, params.y);
}
})
);
if (params.isEditable)
menu.append(
new MenuItem({

View File

@@ -17,6 +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 "@azure/core-asynciterator-polyfill";
import "react-native-gesture-handler";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
@@ -42,14 +43,27 @@ I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
SettingsService.checkOrientation();
// How app lock works
// 1. User goes to settings and setup app lock with a Pin/Password.
// 2. The Pin/Password is used to encrypt a random value or user's encryption key.
// 3. The encrypted value is stored in MMKV
// 4. When the app launches, the same value is decrypted with user provided key, if it works, we launch the app otherwise it remains locked.
// 5. If Biometrics are enabled, the app lock pin/password is stored in keychain. the value can be accessed if fingerprint auth works ONLY.
// 6. User can manually enter the pin if biometrics fails.
// 7. There is no way to enter the app if user forgets the PIN. The only way is to reset app data and start fresh again.
// How to handle app lock for existing users...
// 1.
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 +115,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,36 @@ 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 = "notesnook_applock_key_salt";
const NOTESNOOK_DB_KEY_SALT = "notesnook_database_key";
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
const APPLOCK_CIPHER = "applockCipher";
const KEYSTORE_CONFIG = Platform.select({
ios: {
@@ -37,61 +57,219 @@ 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,
export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword) {
try {
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const decrypted = await decrypt(
{
password: appLockPassword
},
appLockCipher
);
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
);
return typeof decrypted === "string";
} catch (e) {
DatabaseLogger.error(e);
return false;
}
}
let DB_KEY;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword) {
if (DB_KEY) return DB_KEY;
try {
if (appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
return databaseKey;
}
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;
return credentials.password;
}
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
derivedDatabaseKey.key,
KEYSTORE_CONFIG
);
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook",
KEYSTORE_CONFIG
);
if (userKeyCredentials) {
const userKeyCipher = await encrypt(
{
key: derivedDatabaseKey.key
},
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");
}
DB_KEY = derivedDatabaseKey.key;
return derivedDatabaseKey.key;
} catch (e) {
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()
},
credentials.key
);
DatabaseLogger.info("User key stored: ", !!userKeyCipher, credentials);
// 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);
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 +287,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);
}
}

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,36 @@ 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",
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[]);
// console.log("SQLITE result:", result?.rows?._array);
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

@@ -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

@@ -35,14 +35,10 @@ 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);
const isPng = /(png)/g.test(attachment?.mimeType || "");
const isJpeg = /(jpeg|jpg)/g.test(attachment?.mimeType || "");
console.log("decrypting....");
let output = await Sodium.decryptFile(
key,
{
@@ -194,10 +190,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,11 +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 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";
export const Announcement = ({ color }) => {

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,247 @@
/*
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 { 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";
import { validateAppLockPassword } from "../../common/database/encryption";
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<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;
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;
}
setTimeout(() => {
biometricUnlockAwaitingUserInput.current = false;
useSettingStore.getState().setRequestBiometrics(false);
}, 1);
}, [biometricsAuthEnabled, lockApp]);
const onSubmit = async () => {
if (!password.current) return;
try {
const unlocked = appLockHasPasswordSecurity
? validateAppLockPassword(password.current)
: await db.user.verifyPassword(password.current);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = undefined;
}
} 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 || 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,17 @@ 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
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
@@ -46,28 +53,37 @@ import { Notice } from "../ui/notice";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { formatBytes } from "@notesnook/common";
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 +92,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 +101,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.type
});
},
icon: "upload"
@@ -98,19 +114,20 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
setLoading({
name: "Run file check"
});
let res = await filesystem.checkAttachment(attachment.metadata.hash);
let 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 +136,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
setAttachments();
setLoading({
name: null
name: undefined
});
},
icon: "file-check"
@@ -128,19 +145,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 +169,23 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
{
name: "Delete",
onPress: async () => {
await db.attachments.remove(attachment.metadata.hash, false);
await db.attachments.remove(attachment.hash, false);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
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 +228,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
}}
color={colors.secondary.paragraph}
>
{attachment.metadata.type}
{attachment.type}
</Paragraph>
<Paragraph
style={{
@@ -230,10 +237,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 +248,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 +264,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{attachment.metadata.hash}
{attachment.hash}
</Paragraph>
</View>
@@ -286,21 +293,11 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
{notes.map((item) => (
<PressableButton
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={{
paddingVertical: 12,
@@ -321,17 +318,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" || item.name === "PermDelete"
? 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="gray"
fontSize={SIZE.sm}
style={{
borderRadius: 0,
@@ -359,7 +355,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

@@ -18,43 +18,58 @@ 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 { useThemeColors } from "@notesnook/theme";
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) {
var ext = /^.+\.([^.]+)$/.exec(filename);
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
attachment,
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) return;
if (!pressable || !attachment) return;
Actions.present(attachment, setAttachments, context);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
return (hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value)) ||
!attachment ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
@@ -67,7 +82,6 @@ export const AttachmentItem = ({
borderRadius: 5,
backgroundColor: colors.secondary.background
}}
type="grayBg"
>
<View
style={{
@@ -93,7 +107,7 @@ export const AttachmentItem = ({
position: "absolute"
}}
>
{getFileExtension(attachment.metadata.filename).toUpperCase()}
{getFileExtension(attachment.filename).toUpperCase()}
</Paragraph>
</View>
@@ -113,12 +127,12 @@ export const AttachmentItem = ({
lineBreakMode="middle"
color={colors.primary.paragraph}
>
{attachment.metadata.filename}
{attachment.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{formatBytes(attachment.size)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
@@ -132,8 +146,8 @@ export const AttachmentItem = ({
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
db.fs().cancel(attachment.metadata.hash);
setCurrentProgress(undefined);
}}
style={{
justifyContent: "center",

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,7 +242,9 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
borderRadius: 100,
marginRight: 5
}}
onPress={close}
onPress={() => {
close?.();
}}
type="grayBg"
title="No"
/>
@@ -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,81 @@ 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);
} else {
db.attachments.all
.sorted(DEFAULT_SORTING)
.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 +144,33 @@ 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);
};
return (
@@ -220,6 +228,7 @@ export const AttachmentDialog = ({ note }) => {
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
@@ -236,7 +245,7 @@ export const AttachmentDialog = ({ note }) => {
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
onChangeText(attachmentSearchValue.current as string);
}}
/>
@@ -258,10 +267,7 @@ export const AttachmentDialog = ({ note }) => {
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
title={item.title}
style={{
borderRadius: 0,
borderBottomWidth: 1,
@@ -271,9 +277,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 +310,7 @@ export const AttachmentDialog = ({ note }) => {
/>
}
estimatedItemSize={50}
data={attachments}
data={attachments?.placeholders}
renderItem={renderItem}
/>
@@ -327,7 +333,7 @@ export const AttachmentDialog = ({ note }) => {
);
};
AttachmentDialog.present = (note) => {
AttachmentDialog.present = (note?: Note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
});

View File

@@ -23,18 +23,15 @@ 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";
import { eCloseSheet } from "../../utils/events";
import DialogHeader from "../dialog/dialog-header";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { Notice } from "../ui/notice";
import Seperator from "../ui/seperator";
import { Dialog } from "../dialog";
import BackupService from "../../services/backup";
import { sleep } from "../../utils/time";
export const ChangePassword = () => {
const passwordInputRef = useRef();
@@ -48,7 +45,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 +54,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",
@@ -67,23 +64,18 @@ export const ChangePassword = () => {
}
setLoading(true);
try {
const result = await BackupService.run(false, "change-password-dialog");
if (!result) throw new Error("Failed to create backup");
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastEvent.show({
ToastManager.show({
heading: "Account password updated",
type: "success",
context: "global"
});
setLoading(false);
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
setLoading(false);
ToastEvent.show({
ToastManager.show({
heading: "Failed to change password",
message: e.message,
type: "error",
@@ -100,7 +92,6 @@ export const ChangePassword = () => {
padding: 12
}}
>
<Dialog context="change-password-dialog" />
<DialogHeader
title="Change password"
paragraph="Enter your old and new passwords"
@@ -138,14 +129,7 @@ export const ChangePassword = () => {
/>
<Notice
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
type="alert"
/>
<View style={{ height: 10 }} />
<Notice
text={`Once your password is changed, please make sure to save the new account recovery key.`}
text="Changing password is a non-undoable process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection."
type="alert"
/>

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",

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) {

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",

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";
@@ -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]);

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

@@ -36,10 +36,11 @@ import { SIZE, normalize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import { PressableButton } 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);
@@ -118,7 +119,7 @@ const FloatingButton = ({
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={colors.static[color as keyof typeof colors.static]}
accentColor={color}
customStyle={{
...getElevationStyle(5),
borderRadius: 100

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,15 @@ 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 (
<>
<LoadingDialog />
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
@@ -58,10 +56,12 @@ const DialogProvider = () => {
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
{loading ? null : <Expiring />}
<Expiring />
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
<AppLockPassword />
</>
);
};

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

@@ -18,25 +18,36 @@ 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";
const DialogContainer = ({ width, height, ...restProps }) => {
const DialogContainer = ({
width,
height,
style,
...restProps
}: ViewProps & {
width?: any;
height?: any;
}) => {
const { colors } = useThemeColors();
return (
<View
{...restProps}
style={{
...getElevationStyle(5),
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
}}
style={[
{
...getElevationStyle(5),
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
},
style
]}
/>
);
};

View File

@@ -25,7 +25,7 @@ type DialogInfo = {
paragraph?: string;
positiveText?: string;
negativeText?: string;
positivePress?: (value: unknown) => void;
positivePress?: (value: any) => void;
onClose?: () => void;
positiveType?:
| "transparent"
@@ -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

@@ -40,10 +40,7 @@ import { Button } from "../ui/button";
export const Dialog = ({ context = "global" }) => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [checked, setChecked] = useState(false);
const values = useRef({
inputValue: undefined
});
const [inputValue, setInputValue] = useState(null);
const inputRef = useRef();
const [dialogInfo, setDialogInfo] = useState({
title: "",
@@ -65,22 +62,11 @@ 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();
let result = await dialogInfo.positivePress(
values.current.inputValue || dialogInfo.defaultValue,
checked
inputValue || dialogInfo.defaultValue
);
if (result === false) {
return;
@@ -95,16 +81,24 @@ export const Dialog = ({ context = "global" }) => {
if (!data.context) data.context = "global";
if (data.context !== context) return;
setDialogInfo(data);
setChecked(false);
values.current.inputValue = data.defaultValue;
setVisible(true);
setInputValue(data.defaultValue);
},
[context]
);
useEffect(() => {
eSubscribeEvent(eOpenSimpleDialog, show);
eSubscribeEvent(eCloseSimpleDialog, hide);
return () => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, [show]);
const hide = () => {
setChecked(false);
values.current.inputValue = undefined;
setInputValue(null);
setVisible(false);
};
@@ -112,6 +106,7 @@ export const Dialog = ({ context = "global" }) => {
if (dialogInfo.onClose) {
await dialogInfo.onClose();
}
hide();
};
@@ -164,7 +159,7 @@ export const Dialog = ({ context = "global" }) => {
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.inputValue = value;
setInputValue(value);
}}
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
@@ -181,10 +176,10 @@ export const Dialog = ({ context = "global" }) => {
<>
<Button
onPress={() => {
setChecked(!checked);
setInputValue(!inputValue);
}}
icon={
checked
inputValue
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
@@ -192,10 +187,9 @@ export const Dialog = ({ context = "global" }) => {
justifyContent: "flex-start"
}}
height={35}
iconSize={20}
width="100%"
title={dialogInfo.check.info}
type={checked ? dialogInfo.check.type : "gray"}
type={inputValue ? dialogInfo.check.type : "gray"}
/>
</>
) : null}

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,357 @@
/*
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 { 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";
import { useUserStore } from "../../../stores/use-user-store";
import BiometicService from "../../../services/biometrics";
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 (
<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);
});
}}
customStyle={{
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 (!useUserStore.getState().user) {
if (
!(await BiometicService.isBiometryAvailable()) ||
!SettingsService.getProperty("biometricsAuthEnabled")
) {
SettingsService.setProperty("appLockEnabled", false);
}
}
}
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,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 { 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 { PressableButton } 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={() => {
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}
/>
<PressableButton
type="accent"
accentColor={selectedColor || colors.secondary.background}
customStyle={{
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 ? "grayAccent" : "grayBg"}
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,
@@ -36,27 +43,49 @@ import { SIZE } from "../../../utils/size";
import BaseDialog from "../../dialog/base-dialog";
import { PressableButton } 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 ? (
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
alignSelf: "center",
justifyContent: "center",
paddingBottom: 20
}}
>
{groups?.map((item, index) => {
return (
<PressableButton
key={item.title}
onPress={() => onPress(item, index)}
type={currentIndex === index ? "selected" : "transparent"}
key={item.group.id}
onPress={() => onPress(item)}
type={currentIndex === index ? "accent" : "transparent"}
customStyle={{
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;
);
})}
</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={{
@@ -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

@@ -27,14 +27,12 @@ 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,
@@ -45,6 +43,7 @@ import {
} 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 +54,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 +81,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 +100,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 +121,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 +149,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 +175,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 +198,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 +211,6 @@ export class VaultDialog extends Component {
this.password = null;
this.confirmPassword = null;
SearchService.updateAndSearch();
this.setState({
visible: false,
note: {},
@@ -236,7 +234,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",
@@ -245,7 +243,7 @@ export class VaultDialog extends Component {
return;
}
if (this.password && this.password.length < 3) {
ToastEvent.show({
ToastManager.show({
heading: "Password too short",
message: "Password must be longer than 3 characters.",
type: "error",
@@ -257,7 +255,7 @@ export class VaultDialog extends Component {
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
ToastEvent.show({
ToastManager.show({
heading: "Passwords do not match",
type: "error",
context: "local"
@@ -283,7 +281,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 +293,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 +316,7 @@ export class VaultDialog extends Component {
});
return;
}
if (this.state.note.locked) {
if (this.state.noteLocked) {
await this._unlockNote();
} else {
db.vault
@@ -355,7 +355,7 @@ export class VaultDialog extends Component {
});
this.close();
} else {
ToastEvent.show({
ToastManager.show({
heading: "Account password incorrect",
message: "Please enter correct password for your account.",
type: "error",
@@ -382,7 +382,7 @@ export class VaultDialog extends Component {
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 +396,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"
@@ -408,7 +408,7 @@ export class VaultDialog extends Component {
eSendEvent(eClearEditor);
}
this.close();
ToastEvent.show({
ToastManager.show({
message: "Note locked successfully",
type: "error",
context: "local"
@@ -421,7 +421,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 +459,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 +479,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 +488,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.",
@@ -517,7 +517,7 @@ export class VaultDialog extends Component {
this.setState({
loading: false
});
ToastEvent.show({
ToastManager.show({
heading: "Note added to vault",
type: "success",
context: "global"
@@ -525,7 +525,7 @@ export class VaultDialog extends Component {
this.close();
} else {
eSendEvent("vaultUpdated");
ToastEvent.show({
ToastManager.show({
heading: "Vault created successfully",
type: "success",
context: "global"
@@ -538,7 +538,7 @@ 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"
@@ -553,7 +553,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 +564,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 +596,7 @@ export class VaultDialog extends Component {
visible: true
});
setTimeout(() => {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
type: "error",
context: "local"
@@ -609,13 +611,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 +669,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 +705,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 +722,7 @@ export class VaultDialog extends Component {
}
onSubmit={() => {
changePassword
? changePassInputRef.current?.focus()
? this.confirmPassRef.current?.focus()
: this.onPress;
}}
autoComplete="password"
@@ -771,7 +774,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 +794,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 +805,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
@@ -891,7 +894,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();
@@ -54,21 +60,17 @@ export const LeftMenus = () => {
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,43 +20,46 @@ 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}
@@ -66,9 +69,9 @@ export const RightMenus = () => {
{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 +92,7 @@ export const RightMenus = () => {
/>
) : null}
{buttons && buttons.length > 0 ? (
{headerRightButtons && headerRightButtons.length > 0 ? (
<Menu
ref={menuRef}
animationDuration={200}
@@ -111,7 +114,7 @@ export const RightMenus = () => {
/>
}
>
{buttons.map((item) => (
{headerRightButtons.map((item) => (
<Button
style={{
width: 150,

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,23 +32,22 @@ 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 () => {
@@ -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,14 +101,9 @@ export const WelcomeNotice = () => {
SettingsService.set({
introCompleted: true
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
}
);
Navigation.replace("Notes", {
canGoBack: false
});
await sleep(1000);
eSendEvent(eCloseLoading);
}}

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,7 +109,6 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
name={isPinnedToMenu ? "link-variant-off" : "link-variant"}
onPress={onPinNotebook}
tooltipText={"Create shortcut in side menu"}
fwdRef={shortcutRef}
customStyle={{
marginRight: 15,
width: 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 { RouteName } from "../../../stores/use-navigation-store";
import { ColorValues } 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"
@@ -72,9 +85,8 @@ export const SectionHeader = React.memo(
>
<TouchableOpacity
onPress={() => {
eSendEvent(eOpenJumpToDialog, type);
onOpenJumpToDialog();
}}
ref={jumpToRef}
activeOpacity={0.9}
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
style={{
@@ -83,7 +95,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,
@@ -101,16 +116,17 @@ export const SectionHeader = React.memo(
alignItems: "center"
}}
>
{index === 0 ? (
{index === 0 && (
<>
<Button
onPress={() => {
console.log("Opening Sort sheet", screen, dataType);
presentSheet({
component: <Sort screen={screen} type={type} />
component: <Sort screen={screen} type={dataType} />
});
}}
hidden={screen === "Reminders"}
tooltipText="Change sorting of items in list"
fwdRef={sortRef}
title={groupBy}
icon={
groupOptions.sortDirection === "asc"
@@ -123,7 +139,9 @@ export const SectionHeader = React.memo(
paddingHorizontal: 0,
backgroundColor: "transparent",
marginRight:
type === "notes" || type === "home" || type === "notebooks"
dataType === "note" ||
screen === "Notes" ||
dataType === "notebook"
? 10
: 0
}}
@@ -137,35 +155,41 @@ export const SectionHeader = React.memo(
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}
/>
</>
) : null}
)}
</View>
</View>
);
},
(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,27 @@ 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 { useEditorStore } from "../../../stores/use-editor-store";
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 { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
@@ -39,79 +45,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";
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
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
}) => {
}: NoteItemProps) => {
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === 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 +102,45 @@ 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={item.type === "notebook" ? "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={() => {
NotebookScreen.navigate(item, true);
}}
/>
))}
<ReminderTime
reminder={reminder}
color={noteColor}
color={color?.colorCode}
onPress={() => {
Properties.present(reminder);
}}
@@ -184,26 +154,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 +218,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 +239,7 @@ const NoteItem = ({
color={colors.secondary.paragraph}
size={SIZE.xs}
>
{attachmentCount}
{attachmentsCount}
</Paragraph>
</View>
) : null}
@@ -288,14 +252,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,10 +281,10 @@ 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"
@@ -337,9 +298,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 +328,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 +355,7 @@ const NoteItem = ({
/>
) : null}
{item.locked ? (
{locked ? (
<Icon
name="lock"
testID="note-locked-icon"
@@ -423,8 +385,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,7 +396,7 @@ const NoteItem = ({
color={primaryColors.paragraph}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
onPress={() => !noOpen && Properties.present(item)}
customStyle={{
justifyContent: "center",
height: 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,169 @@
/*
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 { 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 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 (await db.vaults.itemExists(note)) {
openVault({
item: note,
novault: true,
locked: true,
goToEditor: true,
title: "Open note",
description: "Unlock note to open it in editor."
});
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 {
if (note?.readonly) {
useEditorStore.getState().setReadonly(note?.readonly);
}
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,7 +197,7 @@ export const NotebookItem = ({
name="dots-horizontal"
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => showActionSheet(item)}
onPress={() => Properties.present(item)}
customStyle={{
justifyContent: "center",
height: 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,17 +44,23 @@ 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,
@@ -131,7 +138,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}

View File

@@ -22,8 +22,9 @@ import React from "react";
import { View } from "react-native";
import useIsSelected from "../../../hooks/use-selected";
import { useEditorStore } from "../../../stores/use-editor-store";
import { Item } from "@notesnook/core";
export const Filler = ({ item }) => {
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
const { colors } = useThemeColors();
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id

View File

@@ -18,37 +18,45 @@ 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 { 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 (
@@ -72,14 +80,8 @@ 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>
);

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>
@@ -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

@@ -27,14 +27,15 @@ import { SIZE } from "../../utils/size";
import { PressableButton } 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%"

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="grayAccent"
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholder?.action}
buttonType={{
text: color || colors.primary.accent
}}
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,189 @@
/*
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",
opacity: isHidden ? 0.4 : 1,
paddingHorizontal: 12
}}
>
<View
style={{
flexGrow: 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[] = [];
data.forEach((item) => {
const index = itemOrderState.indexOf(item?.id);
if (index === -1) {
items.push(item);
} else {
items.splice(index, 0, item);
}
});
return items;
}
return (
<DraxProvider>
<View style={styles.container}>
<DraxList
{...restProps}
ref={listRef}
data={getOrderedItems()}
renderItemContent={renderItemContent}
itemStyles={{
hoverDragReleasedStyle: {
display: "none"
},
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);
}}
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,6 @@ 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";
const MergeConflicts = () => {
const { colors } = useThemeColors();
@@ -62,7 +62,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,
@@ -91,7 +91,7 @@ const MergeConflicts = () => {
if (editorController.current?.note?.id === note.id) {
// reload the note in editor
eSendEvent(eOnLoadNote, {
...editorController.current?.note,
item: editorController.current?.note,
forced: true
});
}
@@ -100,7 +100,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) {
@@ -320,14 +320,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 +357,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 }
}
});
}}
/>

View File

@@ -17,36 +17,40 @@ 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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import Editor from "../../screens/editor";
import EditorOverlay from "../../screens/editor/loading";
import { editorController } from "../../screens/editor/tiptap/utils";
import { eSendEvent, ToastEvent } from "../../services/event-manager";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useEditorStore } from "../../stores/use-editor-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { useTrashStore } from "../../stores/use-trash-store";
import { eCloseSheet, eOnLoadNote } from "../../utils/events";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import DialogHeader from "../dialog/dialog-header";
import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
/**
*
* @param {any} param0
* @returns
*/
export default function NotePreview({ session, content, note }) {
const { colors } = useThemeColors();
const editorId = ":noteHistory";
const [locked, setLocked] = useState(false);
async function restore() {
if (note && note.type === "trash") {
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
ToastManager.show({
heading: "Restore successful",
type: "success"
});
@@ -57,7 +61,7 @@ export default function NotePreview({ session, content, note }) {
if (useEditorStore.getState()?.currentEditingNote === session?.noteId) {
if (editorController.current?.note) {
eSendEvent(eOnLoadNote, {
...editorController.current?.note,
item: editorController.current?.note,
forced: true
});
}
@@ -66,12 +70,16 @@ export default function NotePreview({ session, content, note }) {
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
ToastEvent.show({
ToastManager.show({
heading: "Note restored successfully",
type: "success"
});
}
useEffect(() => {
db.vaults.itemExists(note).then((locked) => setLocked(locked));
}, [note]);
const deleteNote = async () => {
presentDialog({
title: `Delete note permanently`,
@@ -81,9 +89,9 @@ export default function NotePreview({ session, content, note }) {
context: "local",
positivePress: async () => {
await db.trash.delete(note.id);
useTrashStore.getState().setTrash();
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
ToastManager.show({
heading: "Permanently deleted items",
type: "success",
context: "local"
@@ -97,13 +105,13 @@ export default function NotePreview({ session, content, note }) {
return (
<View
style={{
height: note?.locked || session?.locked ? null : 600,
height: locked || session?.locked ? null : 600,
width: "100%"
}}
>
<Dialog context="local" />
<DialogHeader padding={12} title={note?.title || session?.session} />
{!session?.locked && !note?.locked ? (
{!session?.locked && !locked ? (
<View
style={{
flex: 1
@@ -117,10 +125,12 @@ export default function NotePreview({ session, content, note }) {
onLoad={async () => {
const _note = note || db.notes.note(session?.noteId)?.data;
eSendEvent(eOnLoadNote + editorId, {
..._note,
content: {
...content,
isPreview: true
item: {
..._note,
content: {
...content,
isPreview: true
}
}
});
}}

View File

@@ -25,7 +25,7 @@ import { usePricing } from "../../hooks/use-pricing";
import {
eSendEvent,
presentSheet,
ToastEvent
ToastManager
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
@@ -517,7 +517,7 @@ export const PricingPlans = ({
try {
if (!(await getPromo(value as string)))
throw new Error("Error applying promo code");
ToastEvent.show({
ToastManager.show({
heading: "Discount applied!",
type: "success",
context: "local"
@@ -525,7 +525,7 @@ export const PricingPlans = ({
setBuying(false);
} catch (e) {
setBuying(false);
ToastEvent.show({
ToastManager.show({
heading: "Promo code invalid or expired",
message: (e as Error).message,
type: "error",

View File

@@ -23,6 +23,11 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useThemeColors } from "@notesnook/theme";
import Paragraph from "../ui/typography/paragraph";
/**
*
* @param {any} param0
* @returns
*/
export const ProTag = ({ width, size, background }) => {
const { colors } = useThemeColors();

View File

@@ -1,98 +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, { useState } 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 { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { ColorValues } from "../../utils/colors";
import { refreshNotesPage } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { PressableButton } from "../ui/pressable";
import { useThemeColors } from "@notesnook/theme";
export const ColorTags = ({ item }) => {
const { colors } = useThemeColors();
const [note, setNote] = useState(item);
const setColorNotes = useMenuStore((state) => state.setColorNotes);
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const changeColor = async (color) => {
if (note.color === color.name) {
await db.notes.note(note.id).uncolor();
} else {
await db.notes.note(note.id).color(color.name);
}
let _note = db.notes.note(note.id).data;
setNote({ ..._note });
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
const _renderColor = (c) => {
const color = {
name: c,
value: ColorValues[c?.toLowerCase()]
};
return (
<PressableButton
type="accent"
accentColor={colors.static[color.name?.toLowerCase()]}
accentText={colors.static.white}
testID={notesnook.ids.dialogs.actionsheet.color(c)}
key={color.value}
onPress={() => changeColor(color)}
customStyle={{
width: 30,
height: 30,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: isTablet ? 10 : undefined
}}
>
{note.color?.toLowerCase() === color.name ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
);
};
return (
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
flexGrow: isTablet ? undefined : 1,
paddingHorizontal: 12,
paddingRight: 0,
alignItems: "center",
justifyContent: isTablet ? "center" : "space-between"
}}
>
{Object.keys(ColorValues).map(_renderColor)}
</View>
);
};

View File

@@ -0,0 +1,186 @@
/*
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, Note } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { View } from "react-native";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { refreshNotesPage } from "../../utils/events";
import { SIZE } from "../../utils/size";
import ColorPicker from "../dialogs/color-picker";
import { Button } from "../ui/button";
import { PressableButton } from "../ui/pressable";
import NativeTooltip from "../../utils/tooltip";
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
const { colors } = useThemeColors();
const [isLinked, setIsLinked] = useState<boolean>();
const setColorNotes = useMenuStore((state) => state.setColorNotes);
useEffect(() => {
const checkIsLinked = async (color: Color) => {
const hasRelation = await db.relations.from(color, "note").has(note.id);
return hasRelation;
};
checkIsLinked(item).then((info) => setIsLinked(info));
}, [item, note.id]);
const toggleColor = async () => {
await db.relations.unlinkOfType("color", [item.id]);
if (!isLinked) {
await db.relations.add(item, note);
}
useRelationStore.getState().update();
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
return (
<PressableButton
type="accent"
accentColor={item.colorCode}
accentText={colors.static.white}
testID={notesnook.ids.dialogs.actionsheet.color(item.colorCode)}
key={item.id}
onPress={toggleColor}
onLongPress={(event) => {
NativeTooltip.show(event, item.title, NativeTooltip.POSITIONS.TOP);
}}
customStyle={{
width: 35,
height: 35,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: 5
}}
>
{isLinked ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
);
};
export const ColorTags = ({ item }: { item: Note }) => {
const { colors } = useThemeColors();
const colorNotes = useMenuStore((state) => state.colorNotes);
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const updater = useRelationStore((state) => state.updater);
const [visible, setVisible] = useState(false);
const note = item;
const renderItem = useCallback(
({ item }: { item: Color }) => (
<ColorItem note={note} key={item.id} item={item} />
),
[note]
);
return (
<>
<ColorPicker
visible={visible}
setVisible={setVisible}
onColorAdded={async (color) => {
await db.relations.add(color, note);
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
}}
/>
<View
style={{
flexGrow: isTablet ? undefined : 1,
flexDirection: "row",
marginLeft: 5
}}
>
<FlashList
data={colorNotes}
estimatedItemSize={30}
horizontal
extraData={updater}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
ListFooterComponent={
!colorNotes || !colorNotes.length ? (
<Button
onPress={async () => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
buttonType={{
text: colors.primary.accent
}}
title="Add color"
type="grayBg"
icon="plus"
iconPosition="right"
height={30}
fontSize={SIZE.xs}
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
) : (
<PressableButton
customStyle={{
width: 35,
height: 35,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: 5
}}
type="grayBg"
onPress={() => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
>
<Icon
testID="icon-plus"
name="plus"
color={colors.primary.icon}
size={SIZE.lg}
/>
</PressableButton>
)
}
/>
</View>
</>
);
};

View File

@@ -53,23 +53,24 @@ export const DateMeta = ({ item }) => {
return keys.filter((key) => key.startsWith("date") && key !== "date");
}
const renderItem = (key) => (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 3
}}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getFormattedDate(item[key], "date-time")}
</Paragraph>
</View>
);
const renderItem = (key) =>
!item[key] ? null : (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 3
}}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getFormattedDate(item[key], "date-time")}
</Paragraph>
</View>
);
return (
<View
@@ -77,7 +78,7 @@ export const DateMeta = ({ item }) => {
paddingVertical: 5,
marginTop: 5,
borderTopWidth: 1,
borderTopColor: colors.secondary.background,
borderTopColor: colors.primary.border,
paddingHorizontal: 12
}}
>

View File

@@ -16,17 +16,17 @@ 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, { useRef } from "react";
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { presentSheet } from "../../services/event-manager";
import SearchService from "../../services/search";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { PressableButton } from "../ui/pressable";
import { ReminderTime } from "../ui/reminder-time";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -34,7 +34,8 @@ import { DateMeta } from "./date-meta";
import { Items } from "./items";
import Notebooks from "./notebooks";
import { Synced } from "./synced";
import { Tags, TagStrip } from "./tags";
import { TagStrip, Tags } from "./tags";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
return (
@@ -52,7 +53,6 @@ const Line = ({ top = 6, bottom = 6 }) => {
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
const { colors } = useThemeColors();
const alias = item.alias || item.title;
const isColor = !!ColorValues[item.title];
if (!item || !item.id) {
return (
@@ -81,17 +81,42 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
paddingHorizontal: 12,
marginTop: 5,
zIndex: 10,
marginBottom: 6
marginBottom: 5
}}
>
<Heading size={SIZE.lg}>
{item.type === "tag" && !isColor ? (
<Heading size={SIZE.xl} color={colors.primary.accent}>
#
</Heading>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{item.type === "color" ? (
<PressableButton
type="accent"
accentColor={item.colorCode}
accentText={colors.static.white}
customStyle={{
width: 30,
height: 30,
borderRadius: 100,
marginRight: 10
}}
/>
) : null}
{alias}
</Heading>
<Heading size={SIZE.lg}>
{item.type === "tag" && !isColor ? (
<Heading size={SIZE.xl} color={colors.primary.accent}>
#
</Heading>
) : null}
{item.title}
</Heading>
</View>
{item.type === "notebook" && item.description ? (
<Paragraph>{item.description}</Paragraph>
) : null}
{item.type === "note" ? (
<TagStrip close={close} item={item} />
@@ -115,24 +140,28 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
<DateMeta item={item} />
<Line bottom={0} />
{item.type === "note" ? <Tags close={close} item={item} /> : null}
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
{item.type === "note" ? (
<>
<Tags close={close} item={item} />
<Line bottom={0} />
</>
) : null}
{item.type === "note" ? (
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
) : null}
<Items
item={item}
buttons={buttons}
close={() => {
close();
setTimeout(() => {
SearchService.updateAndSearch();
}, 1000);
}}
/>
@@ -152,7 +181,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
);
};
Properties.present = (item, buttons = [], isSheet) => {
Properties.present = async (item, buttons = [], isSheet) => {
if (!item) return;
let type = item?.type;
let props = [];
@@ -164,7 +193,7 @@ Properties.present = (item, buttons = [], isSheet) => {
break;
case "note":
android = Platform.OS === "android" ? ["pin-to-notifications"] : [];
props[0] = db.notes.note(item.id).data;
props[0] = await db.notes.note(item.id);
props.push([
"notebooks",
"add-reminder",
@@ -189,33 +218,27 @@ Properties.present = (item, buttons = [], isSheet) => {
]);
break;
case "notebook":
props[0] = db.notebooks.notebook(item.id).data;
props[0] = await db.notebooks.notebook(item.id);
props.push([
"edit-notebook",
"pin",
"add-shortcut",
"trash",
"default-notebook"
]);
break;
case "topic":
props[0] = db.notebooks
.notebook(item.notebookId)
.topics.topic(item.id)._topic;
props.push([
"move-notes",
"edit-topic",
"add-shortcut",
"trash",
"default-topic"
"default-notebook",
"add-notebook",
"move-notes"
]);
break;
case "tag":
props[0] = db.tags.tag(item.id);
props[0] = await db.tags.tag(item.id);
props.push(["add-shortcut", "trash", "rename-tag"]);
break;
case "color":
props[0] = await db.colors.color(item.id);
props.push(["trash", "rename-color", "reorder"]);
break;
case "reminder": {
props[0] = db.reminders.reminder(item.id);
props[0] = await db.reminders.reminder(item.id);
props.push(["edit-reminder", "trash", "disable-reminder"]);
break;
}

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