Compare commits

..

179 Commits

Author SHA1 Message Date
Licaon_Kter
c54759e766 Remove gradle property unused in Java 17
Signed-off-by: Licaon_Kter <licaon-kter@users.noreply.github.com>
2024-03-22 23:17:45 +05:00
Ammar Ahmed
f15446b1d6 mobile: bump version code 2024-03-21 11:45:03 +05:00
Ammar Ahmed
aa2a8ab6f4 mobile: ignore beta updates on gh release 2024-03-21 10:17:34 +05:00
Ammar Ahmed
8e59ff1318 mobile: release v2.6.18 2024-03-21 10:11:37 +05:00
Abdullah Atta
ad99c87f72 web: bump version to 2.6.17 2024-03-21 10:07:11 +05:00
Abdullah Atta
ba776b5854 global: update lockfiles 2024-03-21 10:07:02 +05:00
Abdullah Atta
e526145e48 web: fix cannot read property integration of undefined 2024-03-21 10:02:08 +05:00
Abdullah Atta
45c5e65bf6 core: add more sanity checks 2024-03-21 09:42:38 +05:00
Abdullah Atta
87bec4e2ab core: add sanity checks to handle cannot read properties of undefined (reading 'hash') error 2024-03-21 09:37:18 +05:00
Abdullah Atta
433e67a6dc web: fix pull failed error on downloading attachments 2024-03-21 09:12:55 +05:00
Ammar Ahmed
6acba51b88 mobile: fix app lock shows on session expired dialog 2024-03-21 07:46:49 +05:00
Abdullah Atta
cf1322952d web: handle errors during attachment downloading 2024-03-15 09:25:10 +05:00
Abdullah Atta
b5ac9dd430 web: make opfs safer by allowing only 1 operation at a time 2024-03-15 09:01:35 +05:00
Abdullah Atta
a3b590bcb7 core: add more login tests 2024-03-12 07:19:59 +05:00
Ammar Ahmed
d4f7e3ef07 mobile: add changelog 2024-03-06 15:31:33 +05:00
Abdullah Atta
16119f6958 setup: optimize bootstrap script 2024-03-06 13:40:42 +05:00
Abdullah Atta
e39c2583a5 setup: only run npm rebuild if required 2024-03-06 13:40:41 +05:00
Abdullah Atta
eecff009dd setup: run npm rebuild after bootstrap 2024-03-06 13:40:41 +05:00
Abdullah Atta
d9ed61a7da setup: ignore scripts during bootstrap 2024-03-06 13:40:41 +05:00
Abdullah Atta
58db265702 setup: use 1 thread on CI 2024-03-06 13:40:41 +05:00
Abdullah Atta
bd39f0cbf8 core: fix e.metadata.hash is not defined 2024-03-06 13:29:59 +05:00
Abdullah Atta
255015108a core: update lockfile 2024-03-06 13:01:00 +05:00
Abdullah Atta
b80f366f8d core: run patch-package after install 2024-03-06 13:00:34 +05:00
Ammar Ahmed
c845c7fce6 mobile: release v2.6.17 2024-03-06 12:59:23 +05:00
Ammar Ahmed
6e7b7ce0e1 mobile: ask user to save recovery key after changing password 2024-03-06 12:33:38 +05:00
Abdullah Atta
9b728902d4 web: bump version to 2.6.16 2024-03-06 12:03:06 +05:00
Abdullah Atta
9a1f54c41a desktop: set proxy on startup 2024-03-06 11:43:50 +05:00
Abdullah Atta
b3251eb016 core: fix login gets stuck on error 2024-03-06 11:28:45 +05:00
Abdullah Atta
f4064b7939 core: patch signalr to exclude electron renderer process as node 2024-03-06 11:28:27 +05:00
Abdullah Atta
f014f08ff4 web: fix export to html test snapshot 2024-03-05 15:41:53 +05:00
Abdullah Atta
dee1cf4b5a web: fix locked note status not updating in list 2024-03-05 15:38:01 +05:00
Abdullah Atta
f35a3d8fa3 web: clear editor on unlocking the note 2024-03-05 15:37:45 +05:00
Abdullah Atta
ca9a8d14fa core: fix sync tests 2024-03-05 15:03:18 +05:00
Abdullah Atta
35c35fa9d7 core: update test snapshots 2024-03-05 14:51:41 +05:00
Abdullah Atta
09fff95d58 editor: fix marks.filter is not a function error on pressing Enter 2024-03-05 14:27:29 +05:00
Abdullah Atta
287307a1ba core: move to json protocol for signalr 2024-03-05 14:23:44 +05:00
Abdullah Atta
d628e62302 web: fix app hangs when downloading logs 2024-03-05 14:18:54 +05:00
Ammar Ahmed
a070bc46ad mobile: check backup was successful 2024-03-05 14:16:35 +05:00
Ammar Ahmed
9027c44511 mobile: request backup before changing password 2024-03-05 14:16:35 +05:00
Abdullah Atta
722aa7b9c6 mobile: update database sync api to use new sync options 2024-03-05 14:16:35 +05:00
Abdullah Atta
a4e777b24b web: update database sync api to use new sync options 2024-03-05 14:16:35 +05:00
Abdullah Atta
af7120e953 web: create backup before changing password 2024-03-05 14:16:35 +05:00
Abdullah Atta
d5adc3bbac web: refactor backup verification logic 2024-03-05 14:16:35 +05:00
Abdullah Atta
aa3d778ad1 core: reduce unnecessary syncing during password change 2024-03-05 14:16:35 +05:00
Abdullah Atta
3465a816c6 web: rename to markdown shortcuts 2024-03-05 13:42:39 +05:00
Ammar Ahmed
3d1726c5aa mobile: toggle markdown shortcuts 2024-03-05 13:42:39 +05:00
Muhammad Ali
488de2d583 web: add setting for toggling markdown in editor 2024-03-05 13:42:39 +05:00
Abdullah Atta
c415b114a6 web: template not disabling when copying as markdown 2024-03-05 13:32:54 +05:00
Abdullah Atta
da36784e9c core: fix content inside span not converting to markdown 2024-03-05 13:32:54 +05:00
Muhammad Ali
78fafdc272 editor: convert google docs checklists on paste 2024-03-05 13:32:54 +05:00
Muhammad Ali
7d57055f73 editor: fix new lines from google docs not being preserved on paste (#3988)
* editor: handle <br> directly under root element

* editor: fix pasting from google docs

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-05 12:14:19 +05:00
Ammar Ahmed
b84cc747cb mobile: restore backup with encryption key 2024-03-04 15:56:12 +05:00
Abdullah Atta
9a4f3ccbad web: allow using encryption key to restore backup 2024-03-04 15:56:12 +05:00
Abdullah Atta
68de3cc928 core: allow using encryption key to decrypt backup 2024-03-04 15:56:12 +05:00
Ammar Ahmed
6e135cdd36 mobile: sort reminders 2024-03-04 15:27:42 +05:00
Abdullah Atta
696116ac85 web: add support for sorting reminders 2024-03-04 15:27:42 +05:00
Abdullah Atta
08a13ff35a core: add support for group options for reminders 2024-03-04 15:27:42 +05:00
Abdullah Atta
af1cea81dc theme: use radio focus color from theme 2024-03-04 15:27:42 +05:00
Muhammad Ali
2c22a7ca90 core: add support for sorting reminders by due date 2024-03-04 15:27:42 +05:00
Muhammad Ali
6fd6bd0375 desktop: add proxy settings (#4196) 2024-03-04 15:16:59 +05:00
Muhammad Ali
1a96dc965a desktop: add support for viewing and removing custom dictionary words (#4362)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-04 13:47:24 +05:00
Muhammad Ali
9550350f25 desktop: add menu item to copy images in editor (#4373) 2024-03-04 13:27:46 +05:00
Muhammad Ali
0ed605a5b9 editor: fix column toolbar position when table is scrolled horizontally (#4366) 2024-03-04 13:25:46 +05:00
Abdullah Atta
18910d931c config: add .nx to .gitignore 2024-03-04 13:24:25 +05:00
Muhammad Ali
87648b7d8b editor: fix katex math not being rendered correctly (#4353)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-04 13:23:27 +05:00
Muhammad Ali
7e7a1aeecb editor: add support for toggling highlight/background color (#4330) 2024-03-04 13:22:25 +05:00
Muhammad Ali
cb353358b7 web: fix editor text not selectable from margins (#4292)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-04 13:20:40 +05:00
Muhammad Ali
ba1619711b desktop: fix app autostart in appimage (#4223) 2024-03-04 12:40:51 +05:00
Ammar Ahmed
b3cc9475ad mobile: fix attachment search 2024-03-01 08:22:14 +05:00
Abdullah Atta
ee5c4077fe docs: improve skiff pages import guide 2024-02-13 01:07:14 +05:00
Abdullah Atta
47bbfc618e docs: add guide for migrating from skiff pages 2024-02-12 10:49:40 +05:00
Abdullah Atta
b377329a8b core: fix attachment mime type on read 2024-02-02 16:02:20 +05:00
Abdullah Atta
ae8bcf3865 ci: fix you may only define one of branches and branches-ignore for a single event
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-31 12:46:32 +05:00
Abdullah Atta
2f67611829 ci: add workflows for v3 beta publishing 2024-01-31 12:41:08 +05:00
Ammar Ahmed
058c612f4f ci: update default build tools version android
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-01-30 11:41:20 +05:00
Ammar Ahmed
be8df90ccb iOS Publish Workflow (#4194)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-01-30 11:32:48 +05:00
Abdullah Atta
aa730b5b93 web: fix title only txt exports 2024-01-30 00:29:46 +05:00
Abdullah Atta
34bbcf6387 web: add feature highlights for v2.6.15 2024-01-29 23:33:15 +05:00
Abdullah Atta
33c3c78e2f desktop: get rid of configuration.win.signingHashAlgorithms should be an array 2024-01-29 23:27:45 +05:00
Ammar Ahmed
bf144dcda4 mobile: add ExportOptions.plist 2024-01-29 23:23:48 +05:00
Ammar Ahmed
284ebdb86f ci: @notesnook/ios
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-01-29 23:23:01 +05:00
Abdullah Atta
9dac4cf7f4 desktop: set signing has to sha256 2024-01-29 23:22:29 +05:00
Abdullah Atta
fb973a5514 ci: run build job no matter what 2024-01-29 23:22:29 +05:00
Abdullah Atta
f9226510f9 desktop: use full path for FilesCatalog 2024-01-29 23:22:29 +05:00
Abdullah Atta
5c0a964699 ci: add inputs to disable build for specific platforms 2024-01-29 23:22:29 +05:00
Abdullah Atta
41ffaab3dc ci: install azurecodesigning module 2024-01-29 23:22:29 +05:00
Abdullah Atta
764c721f1d desktop: add windows code signing 2024-01-29 23:22:29 +05:00
Ammar Ahmed
3d267f91ef mobile: bump version code 2024-01-29 19:55:41 +05:00
Ammar Ahmed
f10f28a7f8 mobile: release v2.6.15 2024-01-29 19:32:14 +05:00
Ammar Ahmed
8d04f89a18 mobile: remove progress bar 2024-01-29 19:06:01 +05:00
Ammar Ahmed
9d1374dcd4 editor: fix progress not showing in image on mobile 2024-01-29 19:05:49 +05:00
Abdullah Atta
094a65e514 editor: hide progress on image if 0 2024-01-29 18:26:44 +05:00
Ammar Ahmed
372da979e1 mobile: support ignoring edits 2024-01-29 18:26:44 +05:00
Abdullah Atta
d3371326f2 editor: add progress indicator to image 2024-01-29 18:26:44 +05:00
Abdullah Atta
57452bd0e6 core: allow setting custom date edited 2024-01-29 18:26:44 +05:00
Abdullah Atta
993b8460bf web: do not update dateEdited if ignoring edits 2024-01-29 18:26:44 +05:00
Abdullah Atta
081201aa42 editor: add support for ignoring edits 2024-01-29 18:26:44 +05:00
Abdullah Atta
270e6778d1 editor: optimize image loading when fast scrolling 2024-01-29 16:23:59 +05:00
Abdullah Atta
2074320583 editor: revoke blob urls on image unmount 2024-01-29 16:09:05 +05:00
Abdullah Atta
cd90030616 web: fix note title in toolbar not changing 2024-01-29 15:45:13 +05:00
Abdullah Atta
807bd683b7 web: do not show success prompt on unlock cancel 2024-01-29 15:36:01 +05:00
Abdullah Atta
35929026a4 web: monograph.notesnook.com > monogr.ph 2024-01-29 15:22:22 +05:00
Ammar Ahmed
41eb94ee47 mobile: fix exporting locked notes 2024-01-29 11:02:32 +05:00
Ammar Ahmed
ba51e110ad mobile: fix opening webclips 2024-01-29 11:02:32 +05:00
Ammar Ahmed
ecad06d249 mobile: disable rtl mode 2024-01-29 11:02:32 +05:00
Abdullah Atta
19a151f637 editor: fix image query matching all images in editor 2024-01-28 17:17:45 +05:00
Ammar Ahmed
7c4a6f1e17 global: update deps 2024-01-27 23:06:24 +05:00
Ammar Ahmed
bc88e953d1 mobile: make sure tools render in toolbar 2024-01-27 23:05:30 +05:00
Ammar Ahmed
7d89ec676a mobile: fix attachments not loading when added from share ext 2024-01-27 22:50:41 +05:00
Ammar Ahmed
c8f8bcca93 mobile: organize bulk exports 2024-01-27 21:23:29 +05:00
Abdullah Atta
9fa904e849 web: fix exporting of single note 2024-01-27 21:23:29 +05:00
Abdullah Atta
05cc947cb4 web: fix pdf exports 2024-01-27 21:23:29 +05:00
alihamuh
a20a683b17 web: code refactoring 2024-01-27 21:23:29 +05:00
alihamuh
5fc43c931e web: added directory paths for exports 2024-01-27 21:23:29 +05:00
Abdullah Atta
0879c3b4d6 core: fix formatting of single spaced paragarphs in markdown exports (#4186)
* core: update showdown library to support single spaced paragraphs

* core: add tests

* core: update showdown library
2024-01-27 18:59:35 +05:00
Abdullah Atta
2eded71de3 core: fix restored attachments not syncing 2024-01-27 16:06:04 +05:00
Abdullah Atta
9e30ee50b8 core: add logs 2024-01-27 16:06:04 +05:00
Abdullah Atta
08621a5873 core: try to preserve mime type on backup restore 2024-01-27 16:06:04 +05:00
Abdullah Atta
17f5bd28ba web: do not overwrite filename & mimetype on attachment reupload 2024-01-27 16:06:04 +05:00
Ammar Ahmed
4558c0b2eb mobile: fix downloading attachments from editor 2024-01-27 16:06:04 +05:00
Abdullah Atta
a442677e91 editor: fix loading of images that have wrong mime type 2024-01-27 16:06:04 +05:00
Abdullah Atta
95666bee36 editor: remove attachment progress once it reaches 100 2024-01-27 16:06:04 +05:00
Abdullah Atta
c4232ac95a web: do not send hash when updating attachment 2024-01-27 16:06:04 +05:00
Abdullah Atta
0283037289 mobile: changes related to download cancellation 2024-01-27 16:06:04 +05:00
Abdullah Atta
b6fd0df715 web: get rid of all attachment loading logic 2024-01-27 16:06:04 +05:00
Abdullah Atta
f53fb58024 editor: eagerly load image/webclip instead of waiting on client 2024-01-27 16:06:04 +05:00
Abdullah Atta
21d452308d core: simplify download cancellation 2024-01-27 16:06:04 +05:00
Ammar Ahmed
823608cca1 mobile: daily trash cleanup 2024-01-27 16:05:28 +05:00
alihamuh
f3a0b998e4 web: added daily trash interval option 2024-01-27 16:05:28 +05:00
theProton
a47a38b16b web: fix incorrect title on scrolling note (#4137)
Signed-off-by: Parthiv-M <parthivmenon.dev@gmail.com>
2024-01-27 11:44:58 +05:00
Ammar Ahmed
a184c53dd5 mobile: fix loading empty conent in editor on tablets 2024-01-26 23:02:06 +05:00
Abdullah Atta
a3464255ec core: always use attachment mime type from backup if possible 2024-01-26 19:21:55 +05:00
Abdullah Atta
088c6670a9 core: fix crash if note title only contains whitespace 2024-01-26 19:12:41 +05:00
Abdullah Atta
27840b9bd6 editor: fix link pasting (#4178) 2024-01-26 18:57:59 +05:00
Muhammad Ali
ae819504f0 editor: fix input rules not working at the end of note (#4130)
* web: added condition in getDeletedNodes

* editor: some corrections

* editor: merge if conditions

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-26 16:43:19 +05:00
Muhammad Ali
af792e394c web: add support for moving locked notes to trash (#4134)
* web: added support for single/multi trash notes

* web: show error when trying to preview a locked note in trash

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-26 16:37:54 +05:00
Muhammad Ali
76a4d5c2a5 editor: fix subtasks getting unchecked on drag/drop reorder (#4135)
* editor: add a condition to check nested item state

* editor: force coerce value to boolean

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-26 16:31:23 +05:00
Muhammad Ali
4e23acd608 web: fix realtime editor sync cannot be disabled (#4140) 2024-01-26 16:22:56 +05:00
Ammar Ahmed
3c65dcdd8b ci: bump android build tools version 2024-01-15 12:31:37 +05:00
Ammar Ahmed
efea6e3972 theme: add missing alpha colors 2024-01-15 12:31:11 +05:00
Ammar Ahmed
185cca7880 mobile: release v2.6.15 2024-01-12 20:10:58 +05:00
Abdullah Atta
fbded93405 global: update lockfiles 2024-01-05 22:10:05 +05:00
Abdullah Atta
674881b873 web: add zip64 support for backups, export & imports (#4079)
* web: use @zip.js/zip.js for reading zip files

* web: always use zip streams for writing zips

* core: fix types

* theme: update lockfile
2024-01-05 22:05:04 +05:00
Abdullah Atta
248fb6dd2d web: bump version to 2.6.14 2024-01-05 21:55:18 +05:00
Abdullah Atta
3211e7a149 global: update lockfiles 2024-01-05 21:54:06 +05:00
Abdullah Atta
08496c699e editor: fix font size changes back after exiting list (fixes #3602) 2024-01-05 21:48:00 +05:00
Abdullah Atta
060fa25814 core: support underline when exporting as md (fix #3990) 2024-01-05 20:53:14 +05:00
Abdullah Atta
a6b1effc77 core: remove relations auto cleanup 2024-01-05 16:24:02 +05:00
Abdullah Atta
5cf9c4808e web: show resubscribe button if subscription is canceled 2024-01-05 15:55:36 +05:00
Muhammad Ali
d5a44067fd web: fix editor header title input not becoming readonly (#4064) 2024-01-05 11:08:16 +05:00
Muhammad Ali
0de463526f editor: add support for +/- font-size using shortcut keys (#4029) 2024-01-05 10:40:29 +05:00
Muhammad Ali
5fd79875ef editor: fix math/formula block getting cut off on mobile (#4023) 2024-01-05 10:39:20 +05:00
Muhammad Ali
bdb19ad325 web: add license to settings (#4020) 2024-01-05 10:38:13 +05:00
Ammar Ahmed
7a12efc823 mobile: release v2.6.14 2023-12-22 11:58:34 +05:00
Abdullah Atta
2f3bddf860 web: bump version to 2.6.13 2023-12-22 11:51:11 +05:00
Abdullah Atta
6127eb97af editor: do not mark task list as completed if there are no items 2023-12-22 11:49:10 +05:00
Abdullah Atta
86386e14f3 web: update test snapshots 2023-12-22 11:41:42 +05:00
Abdullah Atta
9cc17c29f6 web: use Cmd-a on macOS to select all items in list 2023-12-22 11:38:40 +05:00
Abdullah Atta
a31e863c7b global: update lockfiles 2023-12-22 11:16:26 +05:00
Muhammad Ali
15a32ac83a editor: fix pasting a word inside a list adds newlines around it (#3843)
* editor: added openstart, openEnd from old slice

* editor: removed extra import

* editor: update test snapshots

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-22 11:11:03 +05:00
Ammar Ahmed
fa5b84ec0d mobile: minor fixes 2023-12-22 11:10:19 +05:00
Muhammad Ali
7905584eac editor: disable invalid tools when inside a code block (#3997)
* editor: removed fontSize property from code-text style

* editor: TO BE CONTINUED

* editor: some comments for testing

* editor: comment removed

* editor: diabled font size and family in code-block

* editor: disable invalid tools when inside code block

* editor: remove unused console.log

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-22 10:26:25 +05:00
Abdullah Atta
afc9b96a11 web: make importer error resistant 2023-12-21 09:08:04 +05:00
Abdullah Atta
4e6466fc18 editor: handle md links with parentheses (#3996) 2023-12-19 14:54:22 +05:00
Abdullah Atta
233baf78ce web: handle file picker dialog cancelation 2023-12-19 14:39:45 +05:00
Abdullah Atta
42d0bee37f desktop: bump electron to 25.9.8 2023-12-19 13:07:34 +05:00
Abdullah Atta
ccadab6a10 web: add md + frontmatter to export all options 2023-12-19 12:44:23 +05:00
Muhammad Ali
bfe5a5cb96 editor: correctly style other types of lists in outline list (#3889) 2023-12-19 12:15:36 +05:00
Muhammad Ali
88fc24c4cf editor: fix code blocks pasting in a single line without formatting (#3910)
* editor: added code support for w3school

* editor: fixed newline issue in per tag code
2023-12-19 12:13:59 +05:00
Abdullah Atta
8be4fea7a3 ui: use symbols for shortcut keys on macOS 2023-12-19 11:12:20 +05:00
Muhammad Ali
a3d39d8b6d editor: add Ctrl/Cmd+\ keyboard shortcut for "Clear all formatting" (#3989)
* editor: added short-cut key in keyap

* editor: use Mod key instead of Ctrl

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-19 11:11:35 +05:00
Muhammad Ali
ff36f0259a editor: add support for converting md link syntax to link on paste (#3919) 2023-12-18 16:23:56 +05:00
Muhammad Ali
d13654088a editor: add support for checking/unchecking all items in a task list (#3975) 2023-12-18 16:21:44 +05:00
Muhammad Ali
8a6b97c757 editor: add special readonly mode for toolbar (#3913)
* editor: added a toolbar display on mobile in readonly mode

* editor: hide editable tools from readonly toolbar

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-18 16:04:02 +05:00
Muhammad Ali
d71e0c109b web: going back in search after editing a notebook opens notebook edit dialog again (#3979)
* web: changed afterAction()

* web: fix back navigation for dialogs that replace

* web: remove unused import

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-18 15:58:20 +05:00
Ammar Ahmed
7e345303fe mobile: fix app lock hides login page on enter background 2023-12-18 10:49:57 +05:00
Abdullah Atta
d881bdb77c web: fix attachment dialog not opening on error 2023-12-11 15:09:38 +05:00
Ammar Ahmed
4961102d8f mobile: fix subscriptions not verified on ios 2023-12-11 10:54:45 +05:00
Abdullah Atta
4f2ab4f1da docs: fix incorrect noun (fixes #3934)
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-07 09:56:55 +05:00
709 changed files with 84921 additions and 107458 deletions

View File

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

View File

@@ -1,4 +1,4 @@
name: Publish @notesnook/mobile
name: Publish @notesnook/android
on: workflow_dispatch
@@ -33,13 +33,15 @@ jobs:
- 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: Publish to Playstore
id: deploy
@@ -64,6 +66,8 @@ jobs:
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: |

View File

@@ -18,6 +18,21 @@ on:
required: true
default: true
description: "Publish on GitHub releases?"
build-windows:
type: boolean
required: true
default: true
description: "Build for Windows?"
build-linux:
type: boolean
required: true
default: true
description: "Build for Linux?"
build-mac:
type: boolean
required: true
default: true
description: "Build for macOS?"
jobs:
build:
@@ -71,6 +86,7 @@ jobs:
build-macos:
name: Build for macOS
needs: build
if: inputs.build-mac
runs-on: macos-12
steps:
@@ -163,6 +179,7 @@ jobs:
build-linux:
name: Build for Linux
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04
steps:
@@ -221,12 +238,22 @@ jobs:
build-windows:
name: Build for Windows
needs: build
if: inputs.build-windows
runs-on: windows-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
- name: Install AzureCodeSigning
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -Force -Repository PSGallery
shell: pwsh
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -247,6 +274,9 @@ jobs:
- name: Publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: |
if ($${{ inputs.publish-github }} -eq $true) {
yarn electron-builder --win --publish always

67
.github/workflows/ios.publish.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: Publish @notesnook/ios
on: workflow_dispatch
jobs:
build:
runs-on: macos-13
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Build packages
run: npx nx run @notesnook/mobile:build
- name: Install Pods
run: npm run prepare:ios
- name: Build iOS App
uses: yukiarrr/ios-build-action@v1.11.2
with:
bundle-identifier: org.streetwriters.notesnook
scheme: Notesnook
configuration: "Release"
export-options: apps/mobile/native/ios/ExportOptions.plist
project-path: apps/mobile/native/ios/Notesnook.xcodeproj
workspace-path: apps/mobile/native/ios/Notesnook.xcworkspace
update-targets: |
Notesnook
Make Note
NotesWidgetExtension
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
code-signing-identity: Apple Distribution
team-id: ${{ secrets.APPLE_TEAM_ID }}
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
output-path: Notesnook.ipa
mobileprovision-base64: |
${{ secrets.APPLE_MOBILE_PROVISION_APP }}
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
- name: 'Upload app to TestFlight'
uses: apple-actions/upload-testflight-build@v1
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
uses: actions/upload-artifact@v2
with:
name: Notesnook.ipa
path: Notesnook.ipa

View File

@@ -1,10 +1,11 @@
name: Publish @notesnook/web Beta
on:
on:
workflow_dispatch:
push:
branches:
- master
- '!v3-beta'
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
@@ -22,6 +23,7 @@ on:
- synchronize
branches:
- master
- '!v3-beta'
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"

View File

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

3
.gitignore vendored
View File

@@ -9,4 +9,5 @@ nx-cloud.env
.env.local
.nyc_output
site
node_modules.backup
node_modules.backup
.nx

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "2.6.12",
"version": "2.6.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "2.6.12",
"version": "2.6.17",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -24,8 +24,8 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "25.9.3",
"electron-builder": "^24.6.4",
"electron": "25.9.8",
"electron-builder": "^24.9.1",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
"tree-kill": "^1.2.2",
@@ -173,12 +173,11 @@
}
},
"node_modules/@electron/asar": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.4.tgz",
"integrity": "sha512-lykfY3TJRRWFeTxccEKdf1I6BLl2Plw81H0bbp4Fc5iEc67foDCa5pjJQULVgo0wF+Dli75f3xVcdb/67FFZ/g==",
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz",
"integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==",
"dev": true,
"dependencies": {
"chromium-pickle-js": "^0.2.0",
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
@@ -869,9 +868,9 @@
}
},
"node_modules/@types/debug": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"dependencies": {
"@types/ms": "*"
@@ -902,9 +901,9 @@
}
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"dev": true
},
"node_modules/@types/node": {
@@ -970,9 +969,9 @@
}
},
"node_modules/7zip-bin": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz",
"integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
"dev": true
},
"node_modules/agent-base": {
@@ -1054,9 +1053,9 @@
"dev": true
},
"node_modules/app-builder-lib": {
"version": "24.6.4",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.6.4.tgz",
"integrity": "sha512-m9931WXb83teb32N0rKg+ulbn6+Hl8NV5SUpVDOVz9MWOXfhV6AQtTdftf51zJJvCQnQugGtSqoLvgw6mdF/Rg==",
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.1.tgz",
"integrity": "sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g==",
"dev": true,
"dependencies": {
"@develar/schema-utils": "~2.6.5",
@@ -1065,15 +1064,15 @@
"@electron/universal": "1.4.1",
"@malept/flatpak-bundler": "^0.4.0",
"@types/fs-extra": "9.0.13",
"7zip-bin": "~5.1.1",
"7zip-bin": "~5.2.0",
"async-exit-hook": "^2.0.1",
"bluebird-lst": "^1.0.9",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chromium-pickle-js": "^0.2.0",
"debug": "^4.3.4",
"ejs": "^3.1.8",
"electron-publish": "24.5.0",
"electron-publish": "24.8.1",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"hosted-git-info": "^4.1.0",
@@ -1092,6 +1091,19 @@
"node": ">=14.0.0"
}
},
"node_modules/app-builder-lib/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1116,9 +1128,9 @@
}
},
"node_modules/async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
"dev": true
},
"node_modules/async-exit-hook": {
@@ -1280,16 +1292,16 @@
"dev": true
},
"node_modules/builder-util": {
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.5.0.tgz",
"integrity": "sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ==",
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz",
"integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==",
"dev": true,
"dependencies": {
"@types/debug": "^4.1.6",
"7zip-bin": "~5.1.1",
"7zip-bin": "~5.2.0",
"app-builder-bin": "4.0.0",
"bluebird-lst": "^1.0.9",
"builder-util-runtime": "9.2.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
@@ -1315,6 +1327,19 @@
"node": ">=12.0.0"
}
},
"node_modules/builder-util/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -1413,9 +1438,9 @@
"dev": true
},
"node_modules/ci-info": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
@@ -1702,14 +1727,14 @@
}
},
"node_modules/dmg-builder": {
"version": "24.6.4",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.4.tgz",
"integrity": "sha512-BNcHRc9CWEuI9qt0E655bUBU/j/3wUCYBVKGu1kVpbN5lcUdEJJJeiO0NHK3dgKmra6LUUZlo+mWqc+OCbi0zw==",
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.1.tgz",
"integrity": "sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ==",
"dev": true,
"dependencies": {
"app-builder-lib": "24.6.4",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"app-builder-lib": "24.9.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"fs-extra": "^10.1.0",
"iconv-lite": "^0.6.2",
"js-yaml": "^4.1.0"
@@ -1718,6 +1743,19 @@
"dmg-license": "^1.0.11"
}
},
"node_modules/dmg-builder/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/dmg-license": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
@@ -1774,9 +1812,9 @@
}
},
"node_modules/electron": {
"version": "25.9.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.3.tgz",
"integrity": "sha512-dacaHg/PuwVcFRgPDCM5j7UDzqGJWOsbBRdS5wPKLNS/ejPeccIjuNUT1cqcrpvCJKAFW8swHWg9kdizNSEDHQ==",
"version": "25.9.8",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.8.tgz",
"integrity": "sha512-PGgp6PH46QVENHuAHc2NT1Su8Q1qov7qIl2jI5tsDpTibwV2zD8539AeWBQySeBU4dhbj9onIl7+1bXQ0wefBg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -1792,16 +1830,16 @@
}
},
"node_modules/electron-builder": {
"version": "24.6.4",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.4.tgz",
"integrity": "sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA==",
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.1.tgz",
"integrity": "sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg==",
"dev": true,
"dependencies": {
"app-builder-lib": "24.6.4",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"app-builder-lib": "24.9.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"dmg-builder": "24.6.4",
"dmg-builder": "24.9.1",
"fs-extra": "^10.1.0",
"is-ci": "^3.0.0",
"lazy-val": "^1.0.5",
@@ -1863,6 +1901,19 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/electron-builder/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-notarize": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.2.tgz",
@@ -1893,20 +1944,33 @@
}
},
"node_modules/electron-publish": {
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz",
"integrity": "sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==",
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz",
"integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==",
"dev": true,
"dependencies": {
"@types/fs-extra": "^9.0.11",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"fs-extra": "^10.1.0",
"lazy-val": "^1.0.5",
"mime": "^2.5.2"
}
},
"node_modules/electron-publish/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-trpc": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/electron-trpc/-/electron-trpc-0.5.2.tgz",

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.6.12",
"version": "2.6.17",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
@@ -25,8 +25,8 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "25.9.3",
"electron-builder": "^24.6.4",
"electron": "25.9.8",
"electron-builder": "^24.9.1",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
"tree-kill": "^1.2.2",
@@ -135,6 +135,10 @@
]
}
],
"signingHashAlgorithms": [
"sha256"
],
"sign": "./sign.js",
"icon": "assets/icons/app.ico"
},
"portable": {

60
apps/desktop/sign.js Normal file
View File

@@ -0,0 +1,60 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const { writeFileSync, rmSync, readFileSync } = require("fs");
const { execSync } = require("child_process");
const { relative, join } = require("path");
module.exports = async function (configuration) {
const Endpoint = "https://weu.codesigning.azure.net";
const CodeSigningAccountName = "Notesnook";
const CertificateProfileName = "Notesnook";
const FileDigest = configuration.hash.toUpperCase();
const TimestampRfc3161 = "http://timestamp.acs.microsoft.com";
const TimestampDigest = configuration.hash.toUpperCase();
const Description = "The Notesnook app";
const DescriptionUrl = "https://notesnook.com/";
const FilesCatalog = createCatalog(configuration.path);
const command = `Invoke-AzureCodeSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
console.debug("Signing", configuration.path, "using command", command);
psexec(command);
console.debug("Signed", configuration.path);
deleteCatalog();
};
function createCatalog(path) {
writeFileSync("_catalog", relative(__dirname, path));
return join(__dirname, "_catalog");
}
function deleteCatalog() {
rmSync("_catalog");
}
function psexec(cmd) {
return execSync(cmd, {
env: process.env,
stdio: "inherit",
shell: "pwsh"
});
}

View File

@@ -54,6 +54,14 @@ 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() }))

View File

@@ -118,5 +118,13 @@ 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

@@ -112,6 +112,7 @@ 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

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

View File

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

View File

@@ -131,6 +131,17 @@ 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,14 +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 "@azure/core-asynciterator-polyfill";
import "react-native-gesture-handler";
import SettingsService from "./services/settings";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import { I18nManager, View } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import AppLockedOverlay from "./components/app-lock-overlay";
@@ -32,20 +31,25 @@ import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import { useAppEvents } from "./hooks/use-app-events";
import { ApplicationHolder } from "./navigation";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import { themeTrpcClient } from "./screens/settings/theme-selector";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
SettingsService.checkOrientation();
const App = () => {
const init = useAppEvents();
useEffect(() => {
const { appLockMode } = SettingsService.get();
let { appLockMode } = SettingsService.get();
if (appLockMode && appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
@@ -97,10 +101,10 @@ const App = () => {
let currTheme =
useThemeStore.getState().colorScheme === "dark"
? SettingsService.getProperty("darkTheme")
: SettingsService.getProperty("lighTheme");
: SettingsService.getProperty("lightTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: () => JSX.Element) => {
export const withTheme = (Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,

View File

@@ -16,17 +16,15 @@ 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";
database.host(
__DEV__
@@ -51,30 +49,15 @@ database.host(
}
);
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
database.setup(
Storage,
Platform.OS === "ios" ? EventSource : AndroidEventSource,
filesystem,
{
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 500,
sqliteOptions: {
dialect: {
createDriver: () =>
new RNSqliteDriver({ async: true, dbName: "test.db" }),
createAdapter: () => new SqliteAdapter(),
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler()
}
// journalMode: "MEMORY",
// synchronous: "normal",
// pageSize: 8192,
// cacheSize: -16000,
// lockingMode: "exclusive"
}
});
);
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();
initialize(new KV(LoggerStorage));
initalize(new KV(LoggerStorage));
export { LoggerStorage };

View File

@@ -1,123 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type { 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"));
}
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,12 +29,13 @@ import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { ToastEvent, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
import { createCacheDir } from "./io";
import { createCacheDir, exists } from "./io";
import { IOS_APPGROUPID } from "../../utils/constants";
export const FileDownloadStatus = {
Success: 1,
@@ -181,12 +182,14 @@ export default async function downloadAttachment(
silent: false,
cache: false,
throwError: false,
groupId: undefined
groupId: undefined,
base64: false,
text: false
}
) {
await createCacheDir();
let attachment = await db.attachments.attachment(hash);
let attachment = db.attachments.attachment(hash);
if (!attachment) {
console.log("attachment not found");
return;
@@ -208,16 +211,21 @@ export default async function downloadAttachment(
attachment.metadata.hash,
options.groupId
);
await db
.fs()
.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
if (
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
)
await db.fs.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
if (!(await exists(attachment.metadata.hash))) {
return;
}
if (options.base64 || options.text) {
return await db.attachments.read(
attachment.metadata.hash,
options.base64 ? "base64" : "text"
);
}
let filename = getFileNameWithExtension(
attachment.metadata.filename,
@@ -235,7 +243,8 @@ export default async function downloadAttachment(
mime: attachment.metadata.type,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
};
let fileUri = await Sodium.decryptFile(
@@ -245,7 +254,7 @@ export default async function downloadAttachment(
);
if (!options.silent) {
ToastManager.show({
ToastEvent.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"

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 { ToastManager } from "../../services/event-manager";
import { ToastEvent } 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") {
ToastManager.show({
ToastEvent.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
});
ToastManager.show({
ToastEvent.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.tokenManager.getAccessToken();
const token = await db.user.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",

View File

@@ -36,9 +36,13 @@ export async function readEncrypted(filename, key, cipherData) {
}
const attachment = db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.metadata.type);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
console.log("decrypting....");
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);
let output = await Sodium.decryptFile(
key,
{

View File

@@ -18,8 +18,7 @@ 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 { Platform, View } from "react-native";
import { enabled } from "react-native-privacy-snapshot";
import { db } from "../../common/database";
import { useAppState } from "../../hooks/use-app-state";
@@ -42,8 +41,8 @@ const AppLockedOverlay = () => {
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 passwordInputRef = useRef();
const password = useRef();
const appState = useAppState();
const biometricUnlockAwaitingUserInput = useRef(false);
@@ -56,14 +55,14 @@ const AppLockedOverlay = () => {
}
useSettingStore.getState().setRequestBiometrics(true);
const unlocked = await BiometricService.validateUser(
let unlocked = await BiometricService.validateUser(
"Unlock to access your notes",
""
);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = undefined;
password.current = null;
}
setTimeout(() => {
biometricUnlockAwaitingUserInput.current = false;
@@ -74,11 +73,11 @@ const AppLockedOverlay = () => {
const onSubmit = async () => {
if (!password.current) return;
try {
const unlocked = await db.user.verifyPassword(password.current);
let unlocked = await db.user.verifyPassword(password.current);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = undefined;
password.current = null;
}
} catch (e) {
console.error(e);
@@ -171,9 +170,7 @@ const AppLockedOverlay = () => {
secureTextEntry
placeholder="Enter account password"
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();
}}
onSubmit={onSubmit}
/>
</>
) : null}

View File

@@ -17,13 +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 { 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, { RefObject, useEffect, useState } from "react";
import React, { useCallback, 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";
@@ -31,17 +27,14 @@ 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
presentSheet,
ToastEvent
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import {
eCloseAttachmentDialog,
eCloseSheet,
eDBItemUpdate
} from "../../utils/events";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
@@ -53,33 +46,28 @@ 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,
fwdRef
}: {
attachment: Attachment;
fwdRef: RefObject<ActionSheetRef>;
}) => {
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
const { colors } = useThemeColors();
const contextId = attachment.hash;
const [filename, setFilename] = useState(attachment.filename);
const contextId = attachment.metadata.hash;
const [filename, setFilename] = useState(attachment.metadata.filename);
const [currentProgress] = useAttachmentProgress(attachment);
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState<{
name?: string;
}>({});
const [failed, setFailed] = useState(attachment.failed);
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState({
name: null
});
const actions = [
{
name: "Download",
onPress: async () => {
if (currentProgress) {
await db.fs().cancel(attachment.hash, "download");
useAttachmentStore.getState().remove(attachment.hash);
await db.fs.cancel(attachment.metadata.hash);
useAttachmentStore.getState().remove(attachment.metadata.hash);
}
downloadAttachment(attachment.hash, false);
downloadAttachment(attachment.metadata.hash, false);
eSendEvent(eCloseSheet, contextId);
},
icon: "download"
@@ -88,7 +76,7 @@ const Actions = ({
name: "Reupload",
onPress: async () => {
if (!PremiumService.get()) {
ToastManager.show({
ToastEvent.show({
heading: "Upgrade to pro",
type: "error",
context: "local"
@@ -97,9 +85,9 @@ const Actions = ({
}
await picker.pick({
reupload: true,
hash: attachment.hash,
hash: attachment.metadata.hash,
context: contextId,
type: attachment.type
type: attachment.metadata.type
});
},
icon: "upload"
@@ -110,28 +98,28 @@ const Actions = ({
setLoading({
name: "Run file check"
});
let res = await filesystem.checkAttachment(attachment.hash);
let res = await filesystem.checkAttachment(attachment.metadata.hash);
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
ToastManager.show({
ToastEvent.show({
heading: "File check failed with error: " + res.failed,
type: "error",
context: "local"
});
} else {
setFailed(undefined);
db.attachments.markAsFailed(attachment.id);
eSendEvent(eDBItemUpdate, attachment.id);
ToastManager.show({
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
}
setAttachments();
setLoading({
name: undefined
name: null
});
},
icon: "file-check"
@@ -140,19 +128,19 @@ const Actions = ({
name: "Rename",
onPress: () => {
presentDialog({
context: contextId as any,
context: contextId,
input: true,
title: "Rename file",
paragraph: "Enter a new name for the file",
defaultValue: attachment.filename,
defaultValue: attachment.metadata.filename,
positivePress: async (value) => {
if (value && value.length > 0) {
await db.attachments.add({
hash: attachment.hash,
hash: attachment.metadata.hash,
filename: value
});
setFilename(value);
eSendEvent(eDBItemUpdate, attachment.id);
setAttachments();
}
},
positiveText: "Rename"
@@ -163,22 +151,34 @@ const Actions = ({
{
name: "Delete",
onPress: async () => {
await db.attachments.remove(attachment.hash, false);
eSendEvent(eDBItemUpdate, attachment.id);
eSendEvent(eCloseSheet, contextId);
await db.attachments.remove(attachment.metadata.hash, false);
setAttachments();
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(() => {
db.relations
.to(attachment, "note")
.selector.items()
.then((items) => {
setNotes(items);
});
}, [attachment]);
setNotes(getNotes());
}, [attachment, getNotes]);
return (
<ScrollView
@@ -221,7 +221,7 @@ const Actions = ({
}}
color={colors.secondary.paragraph}
>
{attachment.type}
{attachment.metadata.type}
</Paragraph>
<Paragraph
style={{
@@ -230,10 +230,10 @@ const Actions = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{formatBytes(attachment.size)}
{formatBytes(attachment.length)}
</Paragraph>
{notes.length ? (
{attachment.noteIds ? (
<Paragraph
style={{
marginRight: 10
@@ -241,14 +241,14 @@ const Actions = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{notes.length} note
{notes.length > 1 ? "s" : ""}
{attachment.noteIds.length} note
{attachment.noteIds.length > 1 ? "s" : ""}
</Paragraph>
) : null}
<Paragraph
onPress={() => {
Clipboard.setString(attachment.hash);
ToastManager.show({
Clipboard.setString(attachment.metadata.hash);
ToastEvent.show({
type: "success",
heading: "Attachment hash copied",
context: "local"
@@ -257,7 +257,7 @@ const Actions = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{attachment.hash}
{attachment.metadata.hash}
</Paragraph>
</View>
@@ -286,11 +286,21 @@ const Actions = ({
{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 as any).type === "trash");
openNote(item, item.type === "trash");
}}
customStyle={{
paddingVertical: 12,
@@ -311,16 +321,17 @@ const Actions = ({
<Button
key={item.name}
buttonType={{
text:
item.name === "Delete" || item.name === "PermDelete"
? colors.error.paragraph
: colors.primary.paragraph
text: item.on
? colors.primary.accent
: 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="gray"
type={item.on ? "shade" : "gray"}
fontSize={SIZE.sm}
style={{
borderRadius: 0,
@@ -348,14 +359,17 @@ const Actions = ({
);
};
Actions.present = (
attachment: Attachment,
set: (attachments: VirtualizedGrouping<Attachment>) => void,
context?: string
) => {
Actions.present = (attachment, set, context) => {
presentSheet({
context: context,
component: (ref) => <Actions fwdRef={ref} attachment={attachment} />
component: (ref, close) => (
<Actions
fwdRef={ref}
setAttachments={set}
close={close}
attachment={attachment}
/>
)
});
};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatBytes } from "@notesnook/common";
import React, { useEffect, useState } from "react";
import React from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
@@ -29,47 +29,32 @@ import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useDBItem } from "../../hooks/use-db-item";
function getFileExtension(filename: string) {
function getFileExtension(filename) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
id,
attachments,
attachment,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading,
context
}: {
id: string;
attachments?: VirtualizedGrouping<Attachment>;
encryption?: boolean;
setAttachments: (attachments: any) => void;
pressable?: boolean;
hideWhenNotDownloading?: boolean;
context?: string;
}) => {
const [attachment] = useDBItem(id, "attachment", attachments?.item);
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const onPress = () => {
if (!pressable || !attachment) return;
if (!pressable) return;
Actions.present(attachment, setAttachments, context);
};
return (hideWhenNotDownloading &&
(!currentProgress || !(currentProgress as any).value)) ||
!attachment ? null : (
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
@@ -82,6 +67,7 @@ export const AttachmentItem = ({
borderRadius: 5,
backgroundColor: colors.secondary.background
}}
type="grayBg"
>
<View
style={{
@@ -107,7 +93,7 @@ export const AttachmentItem = ({
position: "absolute"
}}
>
{getFileExtension(attachment.filename).toUpperCase()}
{getFileExtension(attachment.metadata.filename).toUpperCase()}
</Paragraph>
</View>
@@ -127,14 +113,14 @@ export const AttachmentItem = ({
lineBreakMode="middle"
color={colors.primary.paragraph}
>
{attachment.filename}
{attachment.metadata.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.size)}{" "}
{(currentProgress as any)?.type
? "(" + (currentProgress as any).type + "ing - tap to cancel)"
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
@@ -146,8 +132,8 @@ export const AttachmentItem = ({
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs().cancel(attachment.hash, currentProgress?.type as any);
setCurrentProgress(undefined);
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
style={{
justifyContent: "center",

View File

@@ -21,10 +21,7 @@ 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 {
PresentSheetOptions,
presentSheet
} from "../../services/event-manager";
import { presentSheet } from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -32,19 +29,8 @@ 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
}: {
attachments: VirtualizedGrouping<Attachment>;
close?: ((ctx?: string | undefined) => void) | undefined;
isNote?: boolean;
update?: (props: PresentSheetOptions) => void;
}) => {
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
const { colors } = useThemeColors();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
@@ -53,40 +39,38 @@ const DownloadAttachments = ({
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef<string>();
const groupId = useRef();
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: number, statusText: string) =>
setProgress({ value: progress, statusText }),
(progress, statusText) => 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, "download");
await db.fs.cancel(groupId.current);
setDownloading(false);
groupId.current = undefined;
groupId.current = null;
};
const successResults = () => {
@@ -107,11 +91,11 @@ const DownloadAttachments = ({
function getResultText() {
const downloadedAttachmentsCount =
attachments?.ids?.length - failedResults().length;
attachments.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments?.ids.length
attachments.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
@@ -173,9 +157,7 @@ const DownloadAttachments = ({
width={null}
animated={true}
useNativeDriver
progress={
progress.value ? progress.value / attachments.ids?.length : 0
}
progress={progress.value ? progress.value / attachments.length : 0}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
@@ -192,7 +174,7 @@ const DownloadAttachments = ({
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments.ids : undefined}
data={downloading ? attachments : []}
ListEmptyComponent={
<View
style={{
@@ -207,15 +189,14 @@ const DownloadAttachments = ({
</Paragraph>
</View>
}
keyExtractor={(item) => item as string}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
return (
<AttachmentItem
id={item as string}
attachment={item}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
attachments={attachments}
/>
);
}}
@@ -228,9 +209,7 @@ const DownloadAttachments = ({
borderRadius: 100,
marginTop: 20
}}
onPress={() => {
close?.();
}}
onPress={close}
type="accent"
title="Done"
/>
@@ -248,9 +227,7 @@ const DownloadAttachments = ({
borderRadius: 100,
marginRight: 5
}}
onPress={() => {
close?.();
}}
onPress={close}
type="grayBg"
title="No"
/>
@@ -281,11 +258,7 @@ const DownloadAttachments = ({
);
};
DownloadAttachments.present = (
context: string,
attachments: VirtualizedGrouping<Attachment>,
isNote?: boolean
) => {
DownloadAttachments.present = (context, attachments, isNote) => {
presentSheet({
context: context,
component: (ref, close, update) => (

View File

@@ -17,25 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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 React, { 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 { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
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";
@@ -43,77 +34,82 @@ 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";
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
sortDirection: "desc"
};
export const AttachmentDialog = ({ note }: { note?: Note }) => {
export const AttachmentDialog = ({ note }) => {
const { colors } = useThemeColors();
const { height } = useSettingStore((state) => state.dimensions);
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>();
const searchTimer = useRef<NodeJS.Timeout>();
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])]
);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("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]);
const onChangeText = (text) => {
const attachments = note?.id
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])];
useEffect(() => {
refresh();
}, [note, refresh]);
const onChangeText = (text: string) => {
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
refresh();
setAttachments(filterAttachments(currentFilter));
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(async () => {
let results = await db.lookup.attachments(
attachmentSearchValue.current as string
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
attachments,
attachmentSearchValue.current
);
setAttachments(results);
if (results.length === 0) return;
setAttachments(filterAttachments(currentFilter, results));
}, 300);
};
const renderItem = ({ item }: { item: string }) => (
const renderItem = ({ item }) => (
<AttachmentItem
setAttachments={setAttachments}
attachments={attachments}
id={item}
setAttachments={() => {
setAttachments(filterAttachments(currentFilter));
}}
attachment={item}
context="attachments-list"
/>
);
const onCheck = async () => {
if (!attachments) return;
setLoading(true);
for (let id of attachments.ids) {
const attachment = await attachments.item(id as string);
if (!attachment) continue;
let result = await filesystem.checkAttachment(attachment.hash);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
if (result.failed) {
await db.attachments.markAsFailed(attachment.hash, result.failed);
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
await db.attachments.markAsFailed(id as string, undefined);
await db.attachments.markAsFailed(attachment.id, null);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
refresh();
setLoading(false);
};
@@ -140,33 +136,33 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
}
];
const filterAttachments = async (type: string) => {
let items: FilteredSelector<Attachment> = db.attachments.all;
const filterAttachments = (type, _attachments) => {
const attachments = _attachments
? _attachments
: note
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])];
switch (type) {
case "all":
items = db.attachments.all;
break;
return attachments;
case "images":
items = note
? db.attachments.ofNote(note.id, "images")
: db.attachments.images;
break;
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
case "video":
items = items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.videos;
break;
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
case "audio":
items = db.attachments.all;
break;
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
case "documents":
items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.documents;
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
}
return await items.sorted(DEFAULT_SORTING);
};
return (
@@ -224,7 +220,6 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
@@ -241,7 +236,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
onChangeText(attachmentSearchValue.current);
}}
/>
@@ -263,7 +258,10 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
style={{
borderRadius: 0,
borderBottomWidth: 1,
@@ -273,9 +271,9 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
? "transparent"
: colors.primary.accent
}}
onPress={async () => {
onPress={() => {
setCurrentFilter(item.filterBy);
setAttachments(await filterAttachments(item.filterBy));
setAttachments(filterAttachments(item.filterBy));
}}
/>
))}
@@ -306,7 +304,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
/>
}
estimatedItemSize={50}
data={attachments?.ids as string[]}
data={attachments}
renderItem={renderItem}
/>
@@ -329,7 +327,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
);
};
AttachmentDialog.present = (note?: Note) => {
AttachmentDialog.present = (note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
});

View File

@@ -17,14 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
@@ -34,7 +36,6 @@ import { IconButton } from "../ui/icon-button";
import { hideAuth, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
export const AuthMode = {
login: 0,
@@ -68,6 +69,9 @@ const AuthModal = () => {
}
const close = () => {
useUserStore.setState({
disableAppLockRequests: false
});
actionSheetRef.current?.hide();
setCurrentAuthMode(AuthMode.login);
setVisible(false);
@@ -77,6 +81,11 @@ const AuthModal = () => {
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={false}
onShow={() => {
useUserStore.setState({
disableAppLockRequests: true
});
}}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
@@ -86,6 +95,7 @@ const AuthModal = () => {
transparent={false}
animated={false}
centered={false}
enableSheetKeyboardHandler
>
<KeyboardAwareScrollView
style={{

View File

@@ -23,15 +23,18 @@ import { db } from "../../common/database";
import {
eSendEvent,
presentSheet,
ToastManager
ToastEvent
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet } from "../../utils/events";
import { eCloseSheet, eOpenRecoveryKeyDialog } 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();
@@ -45,7 +48,7 @@ export const ChangePassword = () => {
const changePassword = async () => {
if (!user?.isEmailConfirmed) {
ToastManager.show({
ToastEvent.show({
heading: "Email not confirmed",
message: "Please confirm your email to change account password",
type: "error",
@@ -54,7 +57,7 @@ export const ChangePassword = () => {
return;
}
if (error || !oldPassword.current || !password.current) {
ToastManager.show({
ToastEvent.show({
heading: "All fields required",
message: "Fill all the fields and try again.",
type: "error",
@@ -64,18 +67,23 @@ 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);
ToastManager.show({
ToastEvent.show({
heading: "Account password updated",
type: "success",
context: "global"
});
setLoading(false);
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
setLoading(false);
ToastManager.show({
ToastEvent.show({
heading: "Failed to change password",
message: e.message,
type: "error",
@@ -92,6 +100,7 @@ export const ChangePassword = () => {
padding: 12
}}
>
<Dialog context="change-password-dialog" />
<DialogHeader
title="Change password"
paragraph="Enter your old and new passwords"
@@ -129,7 +138,14 @@ export const ChangePassword = () => {
/>
<Notice
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."
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.`}
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 { ToastManager } from "../../services/event-manager";
import { ToastEvent } 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) {
ToastManager.show({
ToastEvent.show({
heading: "Account email is required.",
type: "error",
context: "local"
@@ -63,7 +63,7 @@ export const ForgotPassword = () => {
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
ToastManager.show({
ToastEvent.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);
ToastManager.show({
ToastEvent.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 {
ToastManager,
ToastEvent,
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 "";
@@ -69,6 +69,9 @@ export const SessionExpired = () => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
},
true
);
@@ -84,8 +87,11 @@ export const SessionExpired = () => {
MMKV.clearStore();
clearAllStores();
setVisible(false);
useUserStore.setState({
disableAppLockRequests: false
});
} catch (e) {
ToastManager.show({
ToastEvent.show({
heading: e.message,
type: "error",
context: "local"
@@ -102,9 +108,9 @@ export const SessionExpired = () => {
const open = React.useCallback(async () => {
try {
let res = await db.tokenManager.getToken();
let res = await db.user.tokenManager.getToken();
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
if (db.user.tokenManager._isTokenExpired(res))
throw new Error("token expired");
Sync.run("global", false, true, async (complete) => {
if (!complete) {
@@ -127,6 +133,9 @@ export const SessionExpired = () => {
email.current = user.email;
setFocused(false);
setVisible(true);
useUserStore.setState({
disableAppLockRequests: true
});
}
}, [email]);
@@ -142,6 +151,9 @@ export const SessionExpired = () => {
await sleep(300);
passwordInputRef.current?.focus();
setFocused(true);
useUserStore.setState({
disableAppLockRequests: true
});
}}
enableSheetKeyboardHandler={true}
visible={true}

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 { ToastManager } from "../../services/event-manager";
import { ToastEvent } 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) {
ToastManager.show({
ToastEvent.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);
ToastManager.show({
ToastEvent.show({
heading: "Signup failed",
message: e.message,
type: "error",

View File

@@ -24,7 +24,7 @@ import useTimer from "../../hooks/use-timer";
import {
eSendEvent,
presentSheet,
ToastManager
ToastEvent
} 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);
ToastManager.error(e, "Error sending 2FA Code", "local");
ToastEvent.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, ToastManager } from "../../services/event-manager";
import { eSendEvent, ToastEvent } 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)
) {
ToastManager.show({
ToastEvent.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);
ToastManager.show({
ToastEvent.show({
heading: "Login failed",
message: e.message,
type: "error",
@@ -135,7 +135,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastManager.show({
ToastEvent.show({
heading: "Login successful",
message: `Logged in as ${user.email}`,
type: "success",

View File

@@ -0,0 +1,40 @@
/*
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, { PropsWithChildren } from "react";
import { View } from "react-native";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
export const ContainerHeader = (props: PropsWithChildren) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
return !selectionMode ? (
<View
style={{
backgroundColor: colors.primary.background,
width: "100%",
overflow: "hidden"
}}
>
{props.children}
</View>
) : null;
};

View File

@@ -36,11 +36,10 @@ import { SIZE, normalize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import { PressableButton } from "../ui/pressable";
interface FloatingButtonProps {
title: string;
interface FloatingButton {
title?: string;
onPress: () => void;
color?: string;
shouldShow?: boolean;
alwaysVisible?: boolean;
}
@@ -49,7 +48,7 @@ const FloatingButton = ({
onPress,
color,
alwaysVisible = false
}: FloatingButtonProps) => {
}: FloatingButton) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
@@ -119,7 +118,7 @@ const FloatingButton = ({
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={color}
accentColor={colors.static[color as keyof typeof colors.static]}
customStyle={{
...getElevationStyle(5),
borderRadius: 100

View File

@@ -21,9 +21,15 @@ import React, { PropsWithChildren } from "react";
import { KeyboardAvoidingView, Platform } 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";
export const Container = ({ children }: PropsWithChildren) => {
const floating = useIsFloatingKeyboard();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const insets = useGlobalSafeAreaInsets();
return (
@@ -37,6 +43,13 @@ export const Container = ({ children }: PropsWithChildren) => {
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
}}
>
{!introCompleted ? null : (
<>
<SelectionHeader />
<Header />
</>
)}
{children}
</KeyboardAvoidingView>
);

View File

@@ -17,16 +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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme";
import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { AddTopicDialog } from "../dialogs/add-topic";
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";
@@ -37,6 +36,7 @@ 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";
const DialogProvider = () => {
const { colors } = useThemeColors();
@@ -46,6 +46,7 @@ const DialogProvider = () => {
<>
<LoadingDialog />
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
@@ -61,7 +62,6 @@ const DialogProvider = () => {
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
</>
);
};

View File

@@ -21,9 +21,11 @@ import { eSendEvent } from "../../services/event-manager";
import {
eCloseActionSheet,
eCloseAddNotebookDialog,
eCloseAddTopicDialog,
eCloseMoveNoteDialog,
eOpenActionSheet,
eOpenAddNotebookDialog,
eOpenAddTopicDialog,
eOpenMoveNoteDialog
} from "../../utils/events";
@@ -50,3 +52,9 @@ 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,9 +17,8 @@ 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, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import {
ColorValue,
KeyboardAvoidingView,
Modal,
Platform,
@@ -36,25 +35,6 @@ 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,
@@ -66,14 +46,14 @@ const BaseDialog = ({
transparent,
centered = true,
bottom = false,
background = null,
animated = true,
bounce = true,
closeOnTouch = true,
useSafeArea = true,
avoidKeyboardResize = false,
enableSheetKeyboardHandler,
background
}: BaseDialogProps) => {
enableSheetKeyboardHandler = false
}) => {
const floating = useIsFloatingKeyboard();
const appState = useAppState();
const lockEvents = useRef(false);
@@ -86,6 +66,7 @@ const BaseDialog = ({
}, []);
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.get().appLockMode === "background") {
if (appState === "background") {
setIntervalVisible(false);
@@ -167,7 +148,7 @@ const BaseDialog = ({
]}
>
<TouchableOpacity
onPress={closeOnTouch ? onRequestClose : undefined}
onPress={closeOnTouch ? onRequestClose : null}
style={styles.overlayButton}
/>
{premium}

View File

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

View File

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

View File

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

@@ -17,22 +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 { GroupHeader, Item, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
} from "react";
import { FlatList, ScrollView, View } from "react-native";
import React, { useEffect, useState } from "react";
import { 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,
@@ -43,47 +36,27 @@ 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 JumpToSectionDialog = () => {
const scrollRef = useRef<RefObject<FlatList>>();
const [data, setData] = useState<VirtualizedGrouping<Item>>();
const offsets = [];
let timeout = null;
const JumpToSectionDialog = ({ scrollRef, data, type }) => {
const { colors } = useThemeColors();
const notes = data;
const [visible, setVisible] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const offsets = useRef<number[]>([]);
const timeout = useRef<NodeJS.Timeout>();
const onPress = (item: GroupHeader) => {
const index = notes?.ids?.findIndex((i) => {
if (typeof i === "object") {
return i.title === item.title && i.type === "header";
} else {
false;
}
});
scrollRef.current?.current?.scrollToIndex({
index: index as number,
const onPress = (item) => {
let ind = notes.findIndex(
(i) => i.title === item.title && i.type === "header"
);
scrollRef.current?.scrollToIndex({
index: ind,
animated: true
});
close();
};
const open = useCallback(
({
data,
ref
}: {
data: VirtualizedGrouping<Item>;
ref: RefObject<FlatList>;
}) => {
setData(data);
scrollRef.current = ref;
setVisible(true);
},
[]
);
useEffect(() => {
eSubscribeEvent(eOpenJumpToDialog, open);
eSubscribeEvent(eCloseJumpToDialog, close);
@@ -96,52 +69,50 @@ const JumpToSectionDialog = () => {
};
}, [open]);
const onScroll = (data: { x: number; y: number }) => {
const y = data.y;
const onScroll = (data) => {
let y = data.y;
if (timeout) {
clearTimeout(timeout.current);
timeout.current = undefined;
clearTimeout(timeout);
timeout = null;
}
timeout.current = setTimeout(() => {
setCurrentIndex(
offsets.current?.findIndex(
(o, i) => o <= y && offsets.current[i + 1] > y
) || 0
);
timeout = setTimeout(() => {
let index = offsets.findIndex((o, i) => o <= y && offsets[i + 1] > y);
setCurrentIndex(index || 0);
}, 200);
};
const open = useCallback(
(_type) => {
if (_type !== type) return;
setVisible(true);
},
[type]
);
const close = () => {
setVisible(false);
};
const loadOffsets = useCallback(() => {
notes?.ids
.filter((i) => typeof i === "object" && i.type === "header")
.map((item, index) => {
if (typeof item === "string") return;
let offset = 35 * index;
let ind = notes.ids.findIndex(
(i) =>
typeof i === "object" &&
i.title === item.title &&
i.type === "header"
);
const messageState = useMessageStore.getState().message;
const msgOffset = messageState?.visible ? 60 : 10;
ind = ind + 1;
ind = ind - (index + 1);
offset = offset + ind * 100 + msgOffset;
offsets.current.push(offset);
});
}, [notes]);
useEffect(() => {
loadOffsets();
}, [loadOffsets, notes]);
const loadOffsets = useCallback(() => {
notes
.filter((i) => i.type === "header")
.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);
});
}, [notes]);
return !visible ? null : (
<BaseDialog
onShow={() => {
@@ -178,13 +149,13 @@ const JumpToSectionDialog = () => {
paddingBottom: 20
}}
>
{notes?.ids
.filter((i) => typeof i === "object" && i.type === "header")
{notes
.filter((i) => i.type === "header")
.map((item, index) => {
return typeof item === "object" && item.title ? (
return item.title ? (
<PressableButton
key={item.title}
onPress={() => onPress(item)}
onPress={() => onPress(item, index)}
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
minWidth: "20%",

View File

@@ -27,12 +27,14 @@ import { editorController } from "../../../screens/editor/tiptap/utils";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
eUnSubscribeEvent,
ToastEvent
} 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,
@@ -43,7 +45,6 @@ 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";
@@ -54,6 +55,10 @@ 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);
@@ -83,11 +88,6 @@ export class VaultDialog extends Component {
deleteVault: false,
deleteAll: false
};
this.passInputRef = createRef();
this.confirmPassRef = createRef();
this.changePassInputRef = createRef();
this.password = null;
this.confirmPassword = null;
this.newPassword = null;
@@ -148,6 +148,9 @@ 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");
@@ -196,7 +199,7 @@ export class VaultDialog extends Component {
close = () => {
if (this.state.loading) {
ToastManager.show({
ToastEvent.show({
heading: this.state.title,
message: "Please wait and do not close the app.",
type: "success",
@@ -209,6 +212,7 @@ export class VaultDialog extends Component {
this.password = null;
this.confirmPassword = null;
SearchService.updateAndSearch();
this.setState({
visible: false,
note: {},
@@ -232,7 +236,7 @@ export class VaultDialog extends Component {
if (this.state.loading) return;
if (!this.password) {
ToastManager.show({
ToastEvent.show({
heading: "Password not entered",
message: "Enter a password for the vault and try again.",
type: "error",
@@ -241,7 +245,7 @@ export class VaultDialog extends Component {
return;
}
if (this.password && this.password.length < 3) {
ToastManager.show({
ToastEvent.show({
heading: "Password too short",
message: "Password must be longer than 3 characters.",
type: "error",
@@ -253,7 +257,7 @@ export class VaultDialog extends Component {
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
ToastManager.show({
ToastEvent.show({
heading: "Passwords do not match",
type: "error",
context: "local"
@@ -279,7 +283,7 @@ export class VaultDialog extends Component {
if (this.state.biometricUnlock) {
this._enrollFingerprint(this.newPassword);
}
ToastManager.show({
ToastEvent.show({
heading: "Vault password updated successfully",
type: "success",
context: "global"
@@ -291,19 +295,17 @@ export class VaultDialog extends Component {
loading: false
});
if (e.message === db.vault.ERRORS.wrongPassword) {
ToastManager.show({
ToastEvent.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) {
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
@@ -353,7 +355,7 @@ export class VaultDialog extends Component {
});
this.close();
} else {
ToastManager.show({
ToastEvent.show({
heading: "Account password incorrect",
message: "Please enter correct password for your account.",
type: "error",
@@ -380,7 +382,7 @@ export class VaultDialog extends Component {
this.close();
eSendEvent("vaultUpdated");
} catch (e) {
ToastManager.show({
ToastEvent.show({
heading: "Vault password incorrect",
message: "Please enter correct password to clear vault.",
type: "error",
@@ -394,7 +396,7 @@ export class VaultDialog extends Component {
async _lockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
type: "error",
context: "local"
@@ -406,7 +408,7 @@ export class VaultDialog extends Component {
eSendEvent(eClearEditor);
}
this.close();
ToastManager.show({
ToastEvent.show({
message: "Note locked successfully",
type: "error",
context: "local"
@@ -419,7 +421,7 @@ export class VaultDialog extends Component {
async _unlockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
@@ -457,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.id], "note");
await deleteItems(this.state.note);
this.close();
} catch (e) {
this._takeErrorAction(e);
@@ -477,7 +479,7 @@ export class VaultDialog extends Component {
loading: false
});
eSendEvent("vaultUpdated");
ToastManager.show({
ToastEvent.show({
heading: "Biometric unlocking enabled!",
message: "Now you can unlock notes in vault with biometrics.",
type: "success",
@@ -486,7 +488,7 @@ export class VaultDialog extends Component {
this.close();
} catch (e) {
this.close();
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
message:
"Please enter the correct vault password to enable biometrics.",
@@ -515,7 +517,7 @@ export class VaultDialog extends Component {
this.setState({
loading: false
});
ToastManager.show({
ToastEvent.show({
heading: "Note added to vault",
type: "success",
context: "global"
@@ -523,7 +525,7 @@ export class VaultDialog extends Component {
this.close();
} else {
eSendEvent("vaultUpdated");
ToastManager.show({
ToastEvent.show({
heading: "Vault created successfully",
type: "success",
context: "global"
@@ -536,7 +538,7 @@ export class VaultDialog extends Component {
db.vault
.remove(this.state.note.id, this.password)
.then(() => {
ToastManager.show({
ToastEvent.show({
heading: "Note permanently unlocked.",
type: "success",
context: "global"
@@ -551,9 +553,7 @@ export class VaultDialog extends Component {
_openInEditor(note) {
this.close();
InteractionManager.runAfterInteractions(async () => {
eSendEvent(eOnLoadNote, {
item: note
});
eSendEvent(eOnLoadNote, note);
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
@@ -562,7 +562,7 @@ export class VaultDialog extends Component {
async _copyNote(note) {
Clipboard.setString(await convertNoteToText(note));
ToastManager.show({
ToastEvent.show({
heading: "Note copied",
type: "success",
message: "Note has been copied to clipboard!",
@@ -594,7 +594,7 @@ export class VaultDialog extends Component {
visible: true
});
setTimeout(() => {
ToastManager.show({
ToastEvent.show({
heading: "Incorrect password",
type: "error",
context: "local"
@@ -609,13 +609,13 @@ export class VaultDialog extends Component {
try {
await BiometricService.resetCredentials();
eSendEvent("vaultUpdated");
ToastManager.show({
ToastEvent.show({
heading: "Biometric unlocking disabled!",
type: "success",
context: "global"
});
} catch (e) {
ToastManager.show({
ToastEvent.show({
heading: "Failed to disable Biometric unlocking.",
message: e.message,
type: "success",
@@ -667,8 +667,7 @@ export class VaultDialog extends Component {
<BaseDialog
onShow={async () => {
await sleep(100);
this.passInputRef.current?.focus();
passInputRef.current?.focus();
}}
statusBarTranslucent={false}
onRequestClose={this.close}
@@ -703,7 +702,7 @@ export class VaultDialog extends Component {
!this.state.revokeFingerprintAccess ? (
<>
<Input
fwdRef={this.passInputRef}
fwdRef={passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
@@ -720,7 +719,7 @@ export class VaultDialog extends Component {
}
onSubmit={() => {
changePassword
? this.confirmPassRef.current?.focus()
? changePassInputRef.current?.focus()
: this.onPress;
}}
autoComplete="password"
@@ -772,7 +771,7 @@ export class VaultDialog extends Component {
<>
<Seperator half />
<Input
ref={this.confirmPassRef}
ref={changePassInputRef}
editable={!loading}
testID={notesnook.ids.dialogs.vault.changePwd}
autoCapitalize="none"
@@ -792,7 +791,7 @@ export class VaultDialog extends Component {
{!novault ? (
<View>
<Input
fwdRef={this.passInputRef}
fwdRef={passInputRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
@@ -803,13 +802,13 @@ export class VaultDialog extends Component {
returnKeyType="next"
secureTextEntry
onSubmit={() => {
this.confirmPassRef.current?.focus();
confirmPassRef.current?.focus();
}}
placeholder="Password"
/>
<Input
fwdRef={this.confirmPassRef}
fwdRef={confirmPassRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwdAlt}
secureTextEntry

View File

@@ -17,73 +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 { 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, {
RouteName
} from "../../stores/use-navigation-store";
import useNavigationStore 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";
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 _Header = () => {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [borderHidden, setBorderHidden] = useState(true);
const [hide, setHide] = useState(true);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
const currentScreen = useNavigationStore(
(state) => state.currentScreen?.name
);
const onScroll = useCallback(
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
(data: { x: number; y: number }) => {
if (data.y > 150) {
if (!borderHidden) return;
setBorderHidden(false);
if (!hide) return;
setHide(false);
} else {
if (borderHidden) return;
setBorderHidden(true);
if (hide) return;
setHide(true);
}
},
[borderHidden, id, renderedInRoute]
[hide]
);
useEffect(() => {
@@ -91,9 +60,9 @@ export const Header = ({
return () => {
eUnSubscribeEvent(eScrollEvent, onScroll);
};
}, [borderHidden, onScroll]);
}, [hide, onScroll]);
return selectionMode && isFocused ? null : (
return selectionMode ? null : (
<>
<View
style={[
@@ -103,42 +72,29 @@ export const Header = ({
backgroundColor: colors.primary.background,
overflow: "hidden",
borderBottomWidth: 1,
borderBottomColor: borderHidden
borderBottomColor: hide
? "transparent"
: colors.secondary.background,
justifyContent: "space-between"
}
]}
>
<>
<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}
/>
</>
{currentScreen === "Search" ? (
<SearchBar />
) : (
<>
<View style={styles.leftBtnContainer}>
<LeftMenus />
<Title />
</View>
<RightMenus />
</>
)}
</View>
</>
);
};
export const Header = React.memo(_Header, () => true);
const styles = StyleSheet.create({
container: {

View File

@@ -17,29 +17,23 @@ 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 = ({
canGoBack,
onLeftButtonPress
}: {
canGoBack?: boolean;
onLeftButtonPress?: () => void;
}) => {
export const LeftMenus = () => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const canGoBack = useNavigationStore((state) => state.canGoBack);
const isTablet = deviceMode === "tablet";
const _onLeftButtonPress = () => {
if (onLeftButtonPress) return onLeftButtonPress();
const onLeftButtonPress = () => {
if (!canGoBack) {
if (tabBarRef.current?.isDrawerOpen()) {
Navigation.closeDrawer();
@@ -60,17 +54,21 @@ 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,46 +20,43 @@ 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 {
HeaderRightButton,
RouteName
} from "../../stores/use-navigation-store";
import useNavigationStore 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 = ({
headerRightButtons,
renderedInRoute,
id,
onPressDefaultRightButton,
search,
onSearch
}: {
headerRightButtons?: HeaderRightButton[];
renderedInRoute: RouteName;
id?: string;
onPressDefaultRightButton?: () => void;
search?: boolean;
onSearch?: () => void;
}) => {
export const RightMenus = () => {
const { colors } = useThemeColors();
const { colors: contextMenuColors } = useThemeColors("contextMenu");
const deviceMode = useSettingStore((state) => state.deviceMode);
const menuRef = useRef<Menu>(null);
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;
}>();
return (
<View style={styles.rightBtnContainer}>
{search ? (
{!currentScreen.startsWith("Settings") ? (
<IconButton
onPress={onSearch}
onPress={async () => {
SearchService.prepareSearch();
Navigation.navigate(
{
name: "Search"
},
{}
);
}}
testID="icon-search"
name="magnify"
color={colors.primary.paragraph}
@@ -69,9 +66,9 @@ export const RightMenus = ({
{deviceMode !== "mobile" ? (
<Button
onPress={onPressDefaultRightButton}
onPress={buttonAction}
testID={notesnook.ids.default.addBtn}
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
icon={currentScreen === "Trash" ? "delete" : "plus"}
iconSize={SIZE.xl}
type="shade"
hitSlop={{
@@ -92,7 +89,7 @@ export const RightMenus = ({
/>
) : null}
{headerRightButtons && headerRightButtons.length > 0 ? (
{buttons && buttons.length > 0 ? (
<Menu
ref={menuRef}
animationDuration={200}
@@ -114,7 +111,7 @@ export const RightMenus = ({
/>
}
>
{headerRightButtons.map((item) => (
{buttons.map((item) => (
<Button
style={{
width: 150,

View File

@@ -20,74 +20,103 @@ 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";
export const Title = ({
title,
isHiddenOnRender,
accentColor,
isBeta,
renderedInRoute,
id
}: {
title: string;
isHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
renderedInRoute: string;
id?: string;
}) => {
const titleState: { [name: string]: boolean } = {};
export const Title = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(isHiddenOnRender);
const isTag = title.startsWith("#");
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 onScroll = useCallback(
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
(data: { x: number; y: number }) => {
if (currentScreen.name !== "Notebook") {
setHide(false);
return;
}
if (data.y > 150) {
if (!visible) return;
setVisible(false);
if (!hide) return;
titleState[currentScreen.id as string] = false;
setHide(false);
} else {
if (visible) return;
setVisible(true);
if (hide) return;
titleState[currentScreen.id as string] = true;
setHide(true);
}
},
[id, renderedInRoute, visible]
[currentScreen.id, currentScreen.name, hide]
);
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);
};
}, [visible, onScroll]);
}, [hide, onScroll]);
function navigateToNotebook() {
if (!isTopic) return;
Notebook.navigate(notebook, true);
}
return (
<>
{!visible ? (
{!hide && !isHidden ? (
<Heading
onPress={navigateToNotebook}
numberOfLines={1}
size={SIZE.lg}
style={{
flexWrap: "wrap",
marginTop: Platform.OS === "ios" ? -1 : 0
}}
color={accentColor || colors.primary.heading}
color={currentScreen.color || colors.primary.heading}
>
{isTag ? (
<Heading size={SIZE.xl} color={colors.primary.accent}>
#
</Heading>
) : null}
{isTag ? title.slice(1) : title}{" "}
{title}{" "}
<Tag
visible={isBeta}
visible={currentScreen.beta}
text="BETA"
style={{
backgroundColor: "transparent"

View File

@@ -32,22 +32,23 @@ 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<string>();
const [image, setImage] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
return () => {
eUnSubscribeEvent("ImagePreview", open);
};
}, []);
const open = async (image: ImageAttributes) => {
const open = async (image) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
@@ -62,7 +63,6 @@ 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(undefined);
setImage(null);
setVisible(false);
};
@@ -88,7 +88,7 @@ const ImagePreview = () => {
backgroundColor: "black"
}}
>
{loading || !image ? (
{loading ? (
<View
style={{
flex: 1,

View File

@@ -77,9 +77,14 @@ export const WelcomeNotice = () => {
SettingsService.set({
introCompleted: true
});
Navigation.replace("Notes", {
canGoBack: false
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
}
);
}, 1000);
}}
style={{
@@ -101,9 +106,14 @@ export const WelcomeNotice = () => {
SettingsService.set({
introCompleted: true
});
Navigation.replace("Notes", {
canGoBack: false
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
}
);
await sleep(1000);
eSendEvent(eCloseLoading);
}}

View File

@@ -17,39 +17,57 @@ 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 { useSelectionStore } from "../../../stores/use-selection-store";
import { ColorValues } from "../../../utils/colors";
import { Announcement } from "../../announcements/announcement";
import { Card } from "../../list/card";
export type ListHeaderProps = {
noAnnouncement?: boolean;
color?: string;
messageCard?: boolean;
screen?: string;
shouldShow?: boolean;
};
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 const Header = React.memo(
({
type,
messageCard = true,
color,
shouldShow = false,
noAnnouncement,
screen
}: ListHeaderProps) => {
warning
}) => {
const { colors } = useThemeColors();
const announcements = useMessageStore((state) => state.announcements);
const selectionMode = useSelectionStore((state) => state.selectionMode);
return selectionMode ? null : (
<>
{announcements.length !== 0 && !noAnnouncement ? (
{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 ? (
<Announcement color={color || colors.primary.accent} />
) : (screen as any) === "Search" ? null : !shouldShow ? (
) : type === "search" ? null : !shouldShow ? (
<View
style={{
marginBottom: 5,
@@ -60,7 +78,11 @@ export const Header = React.memo(
}}
>
{messageCard ? (
<Card color={color || colors.primary.accent} />
<Card
color={
ColorValues[color?.toLowerCase()] || colors.primary.accent
}
/>
) : null}
</View>
) : null}

View File

@@ -17,33 +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 { Notebook } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { useRef, useState } from "react";
import React from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { ToastManager } from "../../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { useMenuStore } from "../../../stores/use-menu-store";
import { ToastEvent } from "../../../services/event-manager";
import { getTotalNotes } from "@notesnook/common";
import { db } from "../../../common/database";
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,
totalNotes = 0
}: {
notebook: Notebook;
onEditNotebook: () => void;
totalNotes: number;
}) => {
export const NotebookHeader = ({ notebook, onEditNotebook }) => {
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 {
@@ -56,7 +50,7 @@ export const NotebookHeader = ({
type: "notebook"
}
});
ToastManager.show({
ToastEvent.show({
heading: "Shortcut created",
type: "success"
});
@@ -82,7 +76,7 @@ export const NotebookHeader = ({
}}
>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{getFormattedDate(notebook.dateModified, "date-time")}
{new Date(notebook.dateEdited).toLocaleString()}
</Paragraph>
<View
style={{
@@ -109,6 +103,7 @@ export const NotebookHeader = ({
name={isPinnedToMenu ? "link-variant-off" : "link-variant"}
onPress={onPinNotebook}
tooltipText={"Create shortcut in side menu"}
fwdRef={shortcutRef}
customStyle={{
marginRight: 15,
width: 40,
@@ -143,15 +138,15 @@ export const NotebookHeader = ({
style={{
marginTop: 10,
fontStyle: "italic",
fontFamily: undefined
fontFamily: null
}}
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,52 +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 { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import React, { useRef } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { presentSheet } from "../../../services/event-manager";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { RouteName } from "../../../stores/use-navigation-store";
import { useSettingStore } from "../../../stores/use-setting-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";
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) {
export const SectionHeader = React.memo(
function SectionHeader({ item, index, type, color, screen, groupOptions }) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
let groupBy = Object.keys(GROUP).find(
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
(key) => GROUP[key] === groupOptions.groupBy
);
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
const jumpToRef = useRef();
const sortRef = useRef();
const compactModeRef = useRef();
const notebooksListMode = useSettingStore(
(state) => state.settings.notebooksListMode
);
const notesListMode = useSettingStore(
(state) => state.settings.notesListMode
);
const listMode = type === "notebooks" ? notebooksListMode : notesListMode;
groupBy = !groupBy
? "Default"
@@ -85,8 +72,9 @@ export const SectionHeader = React.memo<
>
<TouchableOpacity
onPress={() => {
onOpenJumpToDialog();
eSendEvent(eOpenJumpToDialog, type);
}}
ref={jumpToRef}
activeOpacity={0.9}
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
style={{
@@ -95,10 +83,7 @@ export const SectionHeader = React.memo<
}}
>
<Heading
color={
ColorValues[color?.toLowerCase() as keyof typeof ColorValues] ||
colors.primary.accent
}
color={ColorValues[color?.toLowerCase()] || colors.primary.accent}
size={SIZE.sm}
style={{
minWidth: 60,
@@ -116,17 +101,16 @@ 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={dataType} />
component: <Sort screen={screen} type={type} />
});
}}
hidden={screen === "Reminders"}
tooltipText="Change sorting of items in list"
fwdRef={sortRef}
title={groupBy}
icon={
groupOptions.sortDirection === "asc"
@@ -139,9 +123,7 @@ export const SectionHeader = React.memo<
paddingHorizontal: 0,
backgroundColor: "transparent",
marginRight:
dataType === "note" ||
screen === "Notes" ||
dataType === "notebook"
type === "notes" || type === "home" || type === "notebooks"
? 10
: 0
}}
@@ -155,41 +137,35 @@ export const SectionHeader = React.memo<
height: 25
}}
hidden={
dataType !== "note" &&
dataType !== "notebook" &&
screen !== "Notes"
type !== "notes" && type !== "notebooks" && type !== "home"
}
testID="icon-compact-mode"
tooltipText={
isCompactModeEnabled
listMode == "compact"
? "Switch to normal mode"
: "Switch to compact mode"
}
fwdRef={compactModeRef}
color={colors.secondary.icon}
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
name={listMode == "compact" ? "view-list" : "view-list-outline"}
onPress={() => {
SettingsService.set({
[dataType !== "notebook"
? "notesListMode"
: "notebooksListMode"]: !isCompactModeEnabled
? "compact"
: "normal"
});
let settings = {};
settings[
type !== "notebooks" ? "notesListMode" : "notebooksListMode"
] = listMode === "normal" ? "compact" : "normal";
SettingsService.set(settings);
}}
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,30 +17,21 @@ 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,
TrashItem
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { EntityLevel, decode } from "entities";
import { getUpcomingReminder } from "@notesnook/core/dist/collections/reminders";
import { decode, EntityLevel } 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 { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import NotebookScreen from "../../../screens/notebook";
import { db } from "../../../common/database";
import Notebook from "../../../screens/notebook";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { useEditorStore } from "../../../stores/use-editor-store";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../../utils/colors";
import { SIZE } from "../../../utils/size";
import {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../list/list-item.wrapper";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
@@ -48,41 +39,80 @@ 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";
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
index: number;
tags?: TagsWithDateEdited;
notebooks?: NotebooksWithDateEdited;
color?: Color;
reminder?: Reminder;
attachmentsCount: number;
date: number;
isTrash?: boolean;
noOpen?: boolean;
function navigateToTag(item) {
const tag = db.tags.tag(item.id);
if (!tag) return;
TaggedNotes.navigate(tag, true);
}
const showActionSheet = (item) => {
Properties.present(item);
};
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,
date,
color,
notebooks,
reminder,
tags,
attachmentsCount,
noOpen
}: NoteItemProps) => {
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
dateBy = "dateCreated",
noOpen = false
}) => {
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const compactMode = useIsCompactModeEnabled(item);
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
const _update = useRelationStore((state) => state.updater);
const primaryColors =
currentEditingNote === item.id ? colors.selected : colors.primary;
// 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 (
<>
@@ -103,45 +133,44 @@ const NoteItem = ({
flexWrap: "wrap"
}}
>
{notebooks?.items
?.filter(
(item) =>
item.id !== useNavigationStore.getState().focusedRouteId
)
.map((item) => (
<Button
title={
item.title.length > 25
? item.title.slice(0, 25) + "..."
: item.title
{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);
}
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={color?.colorCode}
color={noteColor}
onPress={() => {
Properties.present(reminder);
}}
@@ -155,7 +184,9 @@ const NoteItem = ({
{compactMode ? (
<Paragraph
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
style={{
flexWrap: "wrap"
}}
@@ -166,7 +197,9 @@ const NoteItem = ({
) : (
<Heading
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
style={{
flexWrap: "wrap"
}}
@@ -219,11 +252,13 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
time={item[dateBy]}
updateFrequency={
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
}
/>
{attachmentsCount > 0 ? (
{attachmentCount > 0 ? (
<View
style={{
flexDirection: "row",
@@ -240,7 +275,7 @@ const NoteItem = ({
color={colors.secondary.paragraph}
size={SIZE.xs}
>
{attachmentsCount}
{attachmentCount}
</Paragraph>
</View>
) : null}
@@ -253,7 +288,10 @@ const NoteItem = ({
style={{
marginRight: 6
}}
color={color?.colorCode || primaryColors.accent}
color={
ColorValues[item.color?.toLowerCase()] ||
primaryColors.accent
}
/>
) : null}
@@ -282,10 +320,10 @@ const NoteItem = ({
) : null}
{!isTrash && !compactMode && tags
? tags.items?.map((item) =>
? tags.map((item) =>
item.id ? (
<Button
title={"#" + item.title}
title={"#" + item.alias}
key={item.id}
height={23}
type="gray"
@@ -299,9 +337,9 @@ const NoteItem = ({
paddingHorizontal: 6,
marginRight: 4,
zIndex: 10,
maxWidth: tags.items?.length > 1 ? 130 : null
maxWidth: tags.length > 1 ? 130 : null
}}
onPress={() => TaggedNotes.navigate(item, true)}
onPress={() => navigateToTag(item)}
/>
) : null
)
@@ -329,8 +367,7 @@ const NoteItem = ({
marginRight: 6
}}
>
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
)}
@@ -386,8 +423,8 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
time={item[dateBy]}
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
/>
</>
) : null}
@@ -397,7 +434,7 @@ const NoteItem = ({
color={primaryColors.paragraph}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && Properties.present(item)}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
customStyle={{
justifyContent: "center",
height: 35,

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { BaseTrashItem, Color, Note, Reminder } from "@notesnook/core";
import React from "react";
import NoteItem from ".";
import { notesnook } from "../../../../e2e/test.ids";
@@ -33,43 +32,50 @@ 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 type {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../list/list-item.wrapper";
import { presentDialog } from "../../dialog/functions";
import NotePreview from "../../note-history/preview";
import SelectionWrapper from "../selection-wrapper";
export const openNote = async (
item: Note,
isTrash?: boolean,
isSheet?: boolean
) => {
let note: Note = item;
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 = (await db.notes.note(item.id)) as Note;
_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, setSelectedItem } =
const { selectedItemsList, selectionMode, clearSelection } =
useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(note.id);
if (selectedItemsList.length > 0 && selectionMode) {
setSelectedItem && setSelectedItem(_note);
return;
} else {
clearSelection();
}
if (note.conflicted) {
eSendEvent(eShowMergeDialog, note);
if (_note.conflicted) {
eSendEvent(eShowMergeDialog, _note);
return;
}
if (note.locked) {
if (_note.locked) {
openVault({
item: note,
item: _note,
novault: true,
locked: true,
goToEditor: true,
@@ -79,81 +85,48 @@ export const openNote = async (
return;
}
if (isTrash) {
if (!note.contentId) return;
const content = await db.content.get(note.contentId as string);
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, {
item: note
});
useEditorStore.getState().setReadonly(_note?.readonly);
eSendEvent(eOnLoadNote, _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;
};
export const NoteWrapper = React.memo<
React.FunctionComponent<NoteWrapperProps>
>(
function NoteWrapper({
item,
index,
isRenderedInActionSheet,
...restProps
}: NoteWrapperProps) {
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 as Note, isTrash, isRenderedInActionSheet)}
isSheet={isRenderedInActionSheet}
onPress={() => openNote(item, isTrash, setSelectedItem, isSheet)}
isSheet={isSheet}
item={item}
color={restProps.color?.colorCode}
>
<NoteItem {...restProps} item={item} index={index} isTrash={isTrash} />
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
</SelectionWrapper>
);
},
(prev, next) => {
if (prev.date !== next.date) {
if (prev.dateBy !== next.dateBy) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
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) {
if (prev.item !== next.item) {
return false;
}

View File

@@ -17,38 +17,41 @@ 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 { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
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";
type NotebookItemProps = {
item: Notebook | BaseTrashItem<Notebook>;
totalNotes: number;
date: number;
index: number;
isTrash: boolean;
const showActionSheet = (item) => {
Properties.present(item);
};
const navigateToTopic = (topic) => {
if (useSelectionStore.getState().selectedItemsList.length > 0) return;
TopicNotes.navigate(topic, true);
};
export const NotebookItem = ({
item,
isTopic = false,
isTrash,
date,
dateBy,
totalNotes
}: NotebookItemProps) => {
}) => {
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const compactMode = useIsCompactModeEnabled(item);
const topics = item.topics?.slice(0, 3) || [];
return (
<>
@@ -80,7 +83,7 @@ export const NotebookItem = ({
</Heading>
)}
{!item.description || compactMode ? null : (
{isTopic || !item.description || compactMode ? null : (
<Paragraph
size={SIZE.sm}
numberOfLines={2}
@@ -92,6 +95,42 @@ 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={{
@@ -113,9 +152,7 @@ export const NotebookItem = ({
}}
>
{"Deleted on " +
new Date((item as TrashItem).dateDeleted)
.toISOString()
.slice(0, 10)}
new Date(item.dateDeleted).toISOString().slice(0, 10)}
</Paragraph>
<Paragraph
color={colors.primary.accent}
@@ -125,8 +162,7 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
) : (
@@ -137,7 +173,7 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{getFormattedDate(date, "date")}
{getFormattedDate(item[dateBy], "date")}
</Paragraph>
)}
<Paragraph
@@ -197,7 +233,7 @@ export const NotebookItem = ({
name="dots-horizontal"
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => Properties.present(item)}
onPress={() => showActionSheet(item)}
customStyle={{
justifyContent: "center",
height: 35,

View File

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

View File

@@ -16,21 +16,20 @@ 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 { useSelectionStore } from "../../../stores/use-selection-store";
import type { Reminder } from "../../../services/notifications";
import { useThemeColors } from "@notesnook/theme";
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(
({
@@ -44,23 +43,17 @@ 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 onPress={openReminder} item={item} isSheet={isSheet}>
<SelectionWrapper
//@ts-ignore
index={index}
height={100}
onPress={openReminder}
item={item}
isSheet={isSheet}
>
<View
style={{
flexShrink: 1,
@@ -138,11 +131,7 @@ 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,17 +22,16 @@ 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, color }: { item: Item; color?: string }) => {
export const Filler = ({ item }) => {
const { colors } = useThemeColors();
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
);
const [selected] = useIsSelected(item);
return currentEditingNote === item.id || selected ? (
return isEditingNote || selected ? (
<View
style={{
position: "absolute",
@@ -40,12 +39,11 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
height: "150%",
backgroundColor: colors.selected.background,
borderLeftWidth: 5,
borderLeftColor:
currentEditingNote === item.id
? color
? color
: colors.selected.accent
: "transparent"
borderLeftColor: isEditingNote
? item.color
? colors.static[item.color]
: colors.selected.accent
: "transparent"
}}
collapsable={false}
/>

View File

@@ -18,45 +18,37 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { PropsWithChildren, useRef } from "react";
import React, { 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,
children,
color
}: SelectionWrapperProps) => {
isSheet
}) => {
const itemId = useRef(item.id);
const { colors, isDark } = useThemeColors();
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const compactMode = useIsCompactModeEnabled(item);
if (item.id !== itemId.current) {
itemId.current = item.id;
}
const onLongPress = () => {
if (useSelectionStore.getState().selectionMode !== item.type) {
useSelectionStore.getState().setSelectionMode(item.type);
if (!useSelectionStore.getState().selectionMode) {
useSelectionStore.setState({
selectedItemsList: []
});
useSelectionStore.getState().setSelectionMode(true);
}
useSelectionStore.getState().setSelectedItem(item.id);
useSelectionStore.getState().setSelectedItem(item);
};
return (
@@ -80,8 +72,14 @@ const SelectionWrapper = ({
marginBottom: isSheet ? 12 : undefined
}}
>
{item.type === "note" ? <Filler item={item} color={color} /> : null}
<SelectionIcon item={item} />
{item.type === "note" ? (
<Filler background={background} item={item} />
) : null}
<SelectionIcon
compactMode={compactMode}
item={item}
onLongPress={onLongPress}
/>
{children}
</PressableButton>
);

View File

@@ -25,16 +25,13 @@ 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 }: { item: Item }) => {
export const SelectionIcon = ({ item }) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const [selected] = useIsSelected(item);
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const compactMode = useIsCompactModeEnabled(item);
return selectionMode ? (
<View

View File

@@ -17,58 +17,44 @@ 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 { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
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,
totalNotes
}: {
item: Tag;
index: number;
totalNotes: number;
}) => {
const { colors } = useThemeColors();
({ item, index }) => {
const { colors, isDark } = 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 (
<SelectionWrapper
<PressableButton
onPress={onPress}
item={item}
selectedColor={colors.secondary.background}
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={{
flexGrow: 1,
flexShrink: 1
maxWidth: "92%"
}}
>
<Heading size={SIZE.md}>
@@ -80,7 +66,7 @@ const TagItem = React.memo(
>
#
</Heading>
{item.title}
{item.alias}
</Heading>
<Paragraph
color={colors.secondary.paragraph}
@@ -89,10 +75,10 @@ const TagItem = React.memo(
marginTop: 5
}}
>
{totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
{item && item.noteIds.length && item.noteIds.length > 1
? item.noteIds.length + " notes"
: item.noteIds.length === 1
? item.noteIds.length + " note"
: null}
</Paragraph>
</View>
@@ -113,11 +99,14 @@ const TagItem = React.memo(
alignItems: "center"
}}
/>
</SelectionWrapper>
</PressableButton>
);
},
(prev, next) => {
if (prev.item?.dateModified !== next.item?.dateModified) {
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
return false;
}

View File

@@ -27,15 +27,14 @@ import { SIZE } from "../../utils/size";
import { PressableButton } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Card = ({ color }: { color?: string }) => {
export const Card = ({ color, warning }) => {
const { colors } = useThemeColors();
color = color ? color : colors.primary.accent;
const messageBoardState = useMessageStore((state) => state.message);
const announcements = useMessageStore((state) => state.announcements);
const announcement = useMessageStore((state) => state.announcement);
const fontScale = Dimensions.get("window").fontScale;
return !messageBoardState.visible ||
(announcements && announcements.length) ? null : (
return !messageBoardState.visible || announcement || warning ? null : (
<View
style={{
width: "95%"

View File

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

@@ -1,140 +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 { 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

@@ -17,171 +17,178 @@ 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,
GroupingKey,
Item,
VirtualizedGrouping,
isGroupHeader
} 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 { 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 { 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 { eOpenJumpToDialog, eScrollEvent } from "../../utils/events";
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 { Empty, PlaceholderData } from "./empty";
import { ListItemWrapper } from "./list-item.wrapper";
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";
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;
const renderItems = {
note: NoteWrapper,
notebook: NotebookWrapper,
topic: NotebookWrapper,
tag: TagItem,
section: SectionHeader,
header: SectionHeader,
reminder: ReminderItem
};
export default function List(props: ListProps) {
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 =
(props.dataType === "note" && notesListMode === "compact") ||
props.dataType === "notebook" ||
(type === "notes" && notesListMode === "compact") ||
type === "notebooks" ||
notebooksListMode === "compact";
const groupType =
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash"
? "trash"
: `${props.dataType}s`;
screen === "Home" ? "home" : screen === "Favorites" ? "favorites" : type;
const groupOptions = useGroupOptions(groupType);
const _onRefresh = async () => {
Sync.run("global", false, true, () => {
props.onRefresh?.();
});
};
const dateBy =
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
const renderItem = React.useCallback(
({ item, index }: { item: string | GroupHeader; index: number }) => {
if (isGroupHeader(item)) {
return (
<SectionHeader
screen={props.renderedInRoute}
item={item}
index={index}
dataType={props.dataType}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: scrollRef,
data: props.data
});
}}
/>
);
} else {
return (
<ListItemWrapper
id={item}
index={index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
group={groupType as GroupingKey}
/>
);
}
},
({ item, index }) => (
<RenderItem
item={item}
index={index}
color={headerProps?.color}
title={headerProps?.heading}
dateBy={dateBy}
type={groupType}
screen={screen}
isSheet={isSheet}
groupOptions={groupOptions}
/>
),
[
groupOptions,
headerProps?.color,
headerProps?.heading,
screen,
isSheet,
dateBy,
groupType,
props.customAccentColor,
props.data,
props.dataType,
props.isRenderedInActionSheet,
props.renderedInRoute
groupOptions
]
);
const onListScroll = React.useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
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: event.nativeEvent.contentOffset.y,
screen: props.renderedInRoute
y,
screen
});
},
[props.renderedInRoute]
[screen]
);
useEffect(() => {
eSendEvent(eScrollEvent, {
y: 0,
screen: props.renderedInRoute
screen
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const styles = {
let styles = {
width: "100%",
minHeight: 1,
minWidth: 1
};
const ListView = props.CustomListComponent
? props.CustomListComponent
: FlashList;
const ListView = ScrollComponent ? ScrollComponent : FlashList;
return (
<>
<Animated.View
style={{
flex: 1
}}
entering={props.renderedInRoute === "Search" ? undefined : FadeInDown}
entering={type === "search" ? undefined : FadeInDown}
>
<ListView
{...handlers}
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
data={props.data?.ids || []}
data={listData}
renderItem={renderItem}
onScroll={onListScroll}
onScroll={_onScroll}
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
tabBarRef.current?.unlock();
onMomentumScrollEnd?.();
}}
getItemType={(item: any) => (isGroupHeader(item) ? "header" : "item")}
getItemType={(item) => item.itemType || item.type}
estimatedItemSize={isCompactModeEnabled ? 60 : 100}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"
@@ -196,32 +203,44 @@ export default function List(props: ListProps) {
/>
}
ListEmptyComponent={
props.placeholder ? (
placeholderData ? (
<Empty
loading={props.loading}
title={props.headerTitle}
dataType={props.dataType}
color={props.customAccentColor}
placeholder={props.placeholder}
screen={props.renderedInRoute}
loading={loading}
placeholderData={placeholderData}
headerProps={headerProps}
type={type}
screen={screen}
/>
) : null
}
ListFooterComponent={<Footer />}
ListHeaderComponent={
<>
{props.CustomLisHeader ? (
props.CustomLisHeader
) : !props.headerTitle ? null : (
{ListHeader ? (
ListHeader
) : !headerProps ? null : (
<Header
color={props.customAccentColor}
screen={props.renderedInRoute}
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

@@ -1,269 +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 { getSortValue } from "@notesnook/core/dist/utils/grouping";
import {
GroupingKey,
Item,
VirtualizedGrouping,
Color,
Reminder,
Notebook,
Tag,
TrashItem,
ItemType,
Note
} from "@notesnook/core";
import { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { NoteWrapper } from "../list-items/note/wrapper";
import { db } from "../../common/database";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
export type WithDateEdited<T> = { items: T[]; dateEdited: number };
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
export type TagsWithDateEdited = WithDateEdited<Tag>;
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
id: string;
isSheet: boolean;
index: number;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
const { id, items, group, isSheet, index } = 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);
useEffect(() => {
(async function () {
const { item, data } = (await items?.item(id, resolveItems)) || {};
if (!item) return;
if (item.type === "note" && isNoteResolvedData(data)) {
tags.current = data.tags;
notebooks.current = data.notebooks;
reminder.current = data.reminder;
color.current = data.color;
attachmentsCount.current = data.attachmentsCount;
} else if (item.type === "notebook" && typeof data === "number") {
totalNotes.current = data;
} else if (item.type === "tag" && typeof data === "number") {
totalNotes.current = data;
}
setItem(item);
})();
}, [id, items]);
if (!item) return <View style={{ height: 100, width: "100%" }} />;
const type = ((item as TrashItem).itemType || item.type) as ItemType;
switch (type) {
case "note": {
return (
<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}
/>
);
}
case "notebook":
return (
<NotebookWrapper
item={item as Notebook}
totalNotes={totalNotes.current}
date={getDate(item, group)}
index={index}
/>
);
case "reminder":
return (
<ReminderItem item={item as Reminder} index={index} isSheet={isSheet} />
);
case "tag":
return (
<TagItem
item={item as Tag}
index={index}
totalNotes={totalNotes.current}
/>
);
default:
return null;
}
}
function withDateEdited<
T extends { dateEdited: number } | { dateModified: number }
>(items: T[]): WithDateEdited<T> {
let latestDateEdited = 0;
items.forEach((item) => {
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
if (latestDateEdited < date) latestDateEdited = date;
});
return { dateEdited: latestDateEdited, items };
}
function getDate(item: Item, groupType?: GroupingKey): number {
return (
getSortValue(
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},
item
) || 0
);
}
async function resolveItems(ids: string[], items: Record<string, Item>) {
const { type } = items[ids[0]];
if (type === "note") return resolveNotes(ids);
else if (type === "notebook") {
const data: Record<string, number> = {};
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
return data;
} else if (type === "tag") {
const data: Record<string, number> = {};
for (const id of ids)
data[id] = await db.relations.from({ id, type: "tag" }, "note").count();
return data;
}
return {};
}
type NoteResolvedData = {
notebooks?: NotebooksWithDateEdited;
reminder?: Reminder;
color?: Color;
tags?: TagsWithDateEdited;
attachmentsCount: number;
};
async function resolveNotes(ids: string[]) {
console.time("relations");
const relations = [
...(await db.relations
.to({ type: "note", ids }, ["notebook", "tag", "color"])
.get()),
...(await db.relations.from({ type: "note", ids }, "reminder").get())
];
console.timeEnd("relations");
const relationIds: {
notebooks: Set<string>;
colors: Set<string>;
tags: Set<string>;
reminders: Set<string>;
} = {
colors: new Set(),
notebooks: new Set(),
tags: new Set(),
reminders: new Set()
};
const grouped: Record<
string,
{
notebooks: string[];
color?: string;
tags: string[];
reminder?: string;
}
> = {};
for (const relation of relations) {
const noteId =
relation.toType === "relation" ? relation.fromId : relation.toId;
const data = grouped[noteId] || {
notebooks: [],
tags: []
};
if (relation.toType === "relation" && !data.reminder) {
data.reminder = relation.fromId;
relationIds.reminders.add(relation.fromId);
} else if (relation.fromType === "notebook" && data.notebooks.length < 2) {
data.notebooks.push(relation.fromId);
relationIds.notebooks.add(relation.fromId);
} else if (relation.fromType === "tag" && data.tags.length < 3) {
data.tags.push(relation.fromId);
relationIds.tags.add(relation.fromId);
} else if (relation.fromType === "color" && !data.color) {
data.color = relation.fromId;
relationIds.colors.add(relation.fromId);
}
grouped[relation.toId] = data;
}
console.time("resolve");
const resolved = {
notebooks: await db.notebooks.all.records(
Array.from(relationIds.notebooks)
),
tags: await db.tags.all.records(Array.from(relationIds.tags)),
colors: await db.colors.all.records(Array.from(relationIds.colors)),
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
};
console.timeEnd("resolve");
const data: Record<string, NoteResolvedData> = {};
for (const noteId in grouped) {
const group = grouped[noteId];
data[noteId] = {
color: group.color ? resolved.colors[group.color] : undefined,
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
tags: withDateEdited(group.tags.map((id) => resolved.tags[id])),
notebooks: withDateEdited(
group.notebooks.map((id) => resolved.notebooks[id])
),
attachmentsCount:
(await db.attachments?.ofNote(noteId, "all").ids())?.length || 0
};
}
return data;
}
function isNoteResolvedData(data: unknown): data is NoteResolvedData {
return (
typeof data === "object" &&
!!data &&
"notebooks" in data &&
"reminder" in data &&
"color" in data &&
"tags" in data
);
}

View File

@@ -91,7 +91,7 @@ const MergeConflicts = () => {
if (editorController.current?.note?.id === note.id) {
// reload the note in editor
eSendEvent(eOnLoadNote, {
item: editorController.current?.note,
...editorController.current?.note,
forced: true
});
}
@@ -324,12 +324,10 @@ const MergeConflicts = () => {
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictPrimary", {
item: {
...note,
content: {
...content.current,
isPreview: true
}
...note,
content: {
...content.current,
isPreview: true
}
});
}}
@@ -361,10 +359,8 @@ const MergeConflicts = () => {
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictSecondary", {
item: {
...note,
content: { ...content.current.conflicted, isPreview: true }
}
...note,
content: { ...content.current.conflicted, isPreview: true }
});
}}
/>

View File

@@ -17,29 +17,26 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React 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, ToastManager } from "../../services/event-manager";
import { eSendEvent, ToastEvent } 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";
@@ -49,7 +46,7 @@ export default function NotePreview({ session, content, note }) {
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
ToastEvent.show({
heading: "Restore successful",
type: "success"
});
@@ -60,7 +57,7 @@ export default function NotePreview({ session, content, note }) {
if (useEditorStore.getState()?.currentEditingNote === session?.noteId) {
if (editorController.current?.note) {
eSendEvent(eOnLoadNote, {
item: editorController.current?.note,
...editorController.current?.note,
forced: true
});
}
@@ -69,7 +66,7 @@ export default function NotePreview({ session, content, note }) {
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
ToastManager.show({
ToastEvent.show({
heading: "Note restored successfully",
type: "success"
});
@@ -86,7 +83,7 @@ export default function NotePreview({ session, content, note }) {
await db.trash.delete(note.id);
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
ToastEvent.show({
heading: "Permanently deleted items",
type: "success",
context: "local"
@@ -120,12 +117,10 @@ export default function NotePreview({ session, content, note }) {
onLoad={async () => {
const _note = note || db.notes.note(session?.noteId)?.data;
eSendEvent(eOnLoadNote + editorId, {
item: {
..._note,
content: {
...content,
isPreview: true
}
..._note,
content: {
...content,
isPreview: true
}
});
}}

View File

@@ -25,7 +25,7 @@ import { usePricing } from "../../hooks/use-pricing";
import {
eSendEvent,
presentSheet,
ToastManager
ToastEvent
} 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");
ToastManager.show({
ToastEvent.show({
heading: "Discount applied!",
type: "success",
context: "local"
@@ -525,7 +525,7 @@ export const PricingPlans = ({
setBuying(false);
} catch (e) {
setBuying(false);
ToastManager.show({
ToastEvent.show({
heading: "Promo code invalid or expired",
message: (e as Error).message,
type: "error",

View File

@@ -23,11 +23,6 @@ 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

@@ -17,10 +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 { DefaultColors } from "@notesnook/core/dist/collections/colors";
import { Color, ItemReference, Note } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
@@ -28,56 +25,45 @@ 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 { 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 }: { item: Note }) => {
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 updater = useRelationStore((state) => state.updater);
const getColorInfo = async (colorCode: string) => {
const dbColor = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", colorCode)])
);
let isLinked = false;
if (dbColor) {
const hasRelation = await db.relations.from(dbColor, "note").has(item.id);
if (hasRelation) {
isLinked = true;
}
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);
}
return {
linked: isLinked,
item: dbColor
};
let _note = db.notes.note(note.id).data;
setNote({ ..._note });
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
const ColorItem = ({ name }: { name: keyof typeof DefaultColors }) => {
const color = DefaultColors[name];
const [colorInfo, setColorInfo] = useState<{
linked: boolean;
item: Color | undefined;
}>();
useEffect(() => {
getColorInfo(color).then((info) => setColorInfo(info));
}, [color]);
const _renderColor = (c) => {
const color = {
name: c,
value: ColorValues[c?.toLowerCase()]
};
return (
<PressableButton
type="accent"
accentColor={color}
accentColor={colors.static[color.name?.toLowerCase()]}
accentText={colors.static.white}
testID={notesnook.ids.dialogs.actionsheet.color(name)}
key={color}
onPress={() => changeColor(name)}
testID={notesnook.ids.dialogs.actionsheet.color(c)}
key={color.value}
onPress={() => changeColor(color)}
customStyle={{
width: 30,
height: 30,
@@ -87,40 +73,13 @@ export const ColorTags = ({ item }: { item: Note }) => {
marginRight: isTablet ? 10 : undefined
}}
>
{colorInfo?.linked ? (
{note.color?.toLowerCase() === color.name ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
);
};
const changeColor = async (color: string) => {
const colorInfo = await getColorInfo(DefaultColors[color]);
if (colorInfo.item) {
if (colorInfo.linked) {
await db.relations.unlink(colorInfo.item, item);
} else {
await db.relations.add(colorInfo.item, item);
}
} else {
const colorId = await db.colors.add({
title: color,
colorCode: DefaultColors[color]
});
const dbColor = await db.colors.color(colorId);
if (dbColor) {
await db.relations.add(dbColor as unknown as ItemReference, item);
}
}
useRelationStore.getState().update();
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
return (
<View
style={{
@@ -133,9 +92,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
justifyContent: isTablet ? "center" : "space-between"
}}
>
{Object.keys(DefaultColors).map((name: keyof typeof DefaultColors) => {
return <ColorItem key={name} name={name} />;
})}
{Object.keys(ColorValues).map(_renderColor)}
</View>
);
};

View File

@@ -53,24 +53,23 @@ export const DateMeta = ({ item }) => {
return keys.filter((key) => key.startsWith("date") && key !== "date");
}
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>
);
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>
);
return (
<View

View File

@@ -16,13 +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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Platform, View } from "react-native";
import React, { useRef } from "react";
import { Dimensions, Platform, View, useWindowDimensions } 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";
@@ -33,8 +34,7 @@ import { DateMeta } from "./date-meta";
import { Items } from "./items";
import Notebooks from "./notebooks";
import { Synced } from "./synced";
import { TagStrip, Tags } from "./tags";
import { Tags, TagStrip } from "./tags";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
return (
@@ -52,6 +52,7 @@ 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 (
@@ -89,13 +90,9 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
#
</Heading>
) : null}
{item.title}
{alias}
</Heading>
{item.type === "notebook" && item.description ? (
<Paragraph>{item.description}</Paragraph>
) : null}
{item.type === "note" ? (
<TagStrip close={close} item={item} />
) : null}
@@ -133,6 +130,9 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
buttons={buttons}
close={() => {
close();
setTimeout(() => {
SearchService.updateAndSearch();
}, 1000);
}}
/>
@@ -152,7 +152,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
);
};
Properties.present = async (item, buttons = [], isSheet) => {
Properties.present = (item, buttons = [], isSheet) => {
if (!item) return;
let type = item?.type;
let props = [];
@@ -164,7 +164,7 @@ Properties.present = async (item, buttons = [], isSheet) => {
break;
case "note":
android = Platform.OS === "android" ? ["pin-to-notifications"] : [];
props[0] = await db.notes.note(item.id);
props[0] = db.notes.note(item.id).data;
props.push([
"notebooks",
"add-reminder",
@@ -189,34 +189,33 @@ Properties.present = async (item, buttons = [], isSheet) => {
]);
break;
case "notebook":
props[0] = await db.notebooks.notebook(item.id);
props[0] = db.notebooks.notebook(item.id).data;
props.push([
"edit-notebook",
"pin",
"add-shortcut",
"trash",
"default-notebook",
"add-notebook"
"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"
]);
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"
// ]);
// break;
case "tag":
props[0] = await db.tags.tag(item.id);
props[0] = db.tags.tag(item.id);
props.push(["add-shortcut", "trash", "rename-tag"]);
break;
case "reminder": {
props[0] = await db.reminders.reminder(item.id);
props[0] = db.reminders.reminder(item.id);
props.push(["edit-reminder", "trash", "disable-reminder"]);
break;
}

View File

@@ -28,7 +28,6 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { PressableButton } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Items = ({ item, buttons, close }) => {
const { colors } = useThemeColors();
const dimensions = useSettingStore((state) => state.dimensions);
@@ -178,37 +177,17 @@ export const Items = ({ item, buttons, close }) => {
"export",
"lock-unlock"
];
if (!shouldShrink) {
topBarItemsList.push("publish");
}
const topBarItems = data
.filter((item) => topBarItemsList.indexOf(item.id) > -1)
.sort((a, b) =>
topBarItemsList.indexOf(a.id) > topBarItemsList.indexOf(b.id) ? 1 : -1
);
const topBarItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) > -1
);
const bottomBarItemsList = [
"notebooks",
"add-reminder",
"pin-to-notifications",
"duplicate",
"read-only",
"local-only",
"history",
"reminders",
"attachments",
"trash"
];
const bottomGridItems = data
.filter((item) => bottomBarItemsList.indexOf(item.id) > -1)
.sort((a, b) =>
bottomBarItemsList.indexOf(a.id) > bottomBarItemsList.indexOf(b.id)
? 1
: -1
);
const bottomGridItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) === -1
);
const topBarItemWidth =
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;

View File

@@ -17,46 +17,76 @@ 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, useState } from "react";
import { View } from "react-native";
import React from "react";
import { ScrollView, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eClearEditor } from "../../utils/events";
import Notebook from "../../screens/notebook";
import { TopicNotes } from "../../screens/notes/topic-notes";
import {
eSendEvent,
presentSheet,
ToastEvent
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useNotebookStore } from "../../stores/use-notebook-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { PressableButton } from "../ui/pressable";
import { eClearEditor } from "../../utils/events";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
async function getNotebooks() {
let filteredNotebooks = await db.relations.to(note, "notebook").resolve();
return filteredNotebooks || [];
}
const [noteNotebooks, setNoteNotebooks] = useState([]);
useEffect(() => {
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
});
const notebooks = useNotebookStore((state) => state.notebooks);
function getNotebooks(item) {
let filteredNotebooks = [];
const relations = db.relations.to(note, "notebook");
filteredNotebooks.push(
...relations.map((notebook) => ({
...notebook,
topics: []
}))
);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
const navigateNotebook = async (id) => {
let item = await db.notebooks.notebook(id);
for (let notebookReference of item.notebooks) {
let notebook = {
...(notebooks.find((item) => item.id === notebookReference.id) || {})
};
if (notebook.id) {
notebook.topics = notebook.topics.filter((topic) => {
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
});
const index = filteredNotebooks.findIndex(
(item) => item.id === notebook.id
);
if (index > -1) {
filteredNotebooks[index].topics = notebook.topics;
} else {
filteredNotebooks.push(notebook);
}
}
}
return filteredNotebooks;
}
const noteNotebooks = getNotebooks(note);
const navigateNotebook = (id) => {
let item = db.notebooks.notebook(id)?.data;
if (!item) return;
NotebookScreen.navigate(item, true);
Notebook.navigate(item, true);
};
const navigateTopic = (id, notebookId) => {
let item = db.notebooks.notebook(notebookId)?.topics?.topic(id)?._topic;
if (!item) return;
TopicNotes.navigate(item, true);
};
const renderItem = (item) => (
<PressableButton
<View
key={item.id}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
type={full ? "transparent" : "grayBg"}
customStyle={{
style={{
justifyContent: "flex-start",
paddingHorizontal: 12,
flexDirection: "row",
@@ -64,7 +94,10 @@ export default function Notebooks({ note, close, full }) {
flexShrink: 1,
flexGrow: 1,
padding: 6,
borderWidth: full ? 0 : 1,
borderColor: colors.primary.background,
borderRadius: 10,
backgroundColor: full ? "transparent" : colors.secondary.background,
minHeight: 42
}}
>
@@ -82,10 +115,65 @@ export default function Notebooks({ note, close, full }) {
maxWidth: "50%"
}}
size={SIZE.sm}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
>
{item.title}
</Heading>
</PressableButton>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
style={{
flexDirection: "row",
marginLeft: 8,
borderLeftColor: colors.primary.hover,
borderLeftWidth: 1,
paddingLeft: 8
}}
>
{item.topics.map((topic) => (
<Button
key={topic.id}
onPress={() => {
navigateTopic(topic.id, item.id);
eSendEvent(eClearEditor);
close();
}}
onLongPress={async () => {
await db.notes.removeFromNotebook(
{
id: item.id,
topic: topic.id
},
note
);
useNotebookStore.getState().setNotebooks();
Navigation.queueRoutesForUpdate();
ToastEvent.show({
heading: "Note removed from topic",
context: "local",
type: "success"
});
}}
title={topic.title}
type="gray"
height={30}
fontSize={SIZE.xs}
icon="bookmark-outline"
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
))}
<View style={{ width: 10 }} />
</ScrollView>
</View>
);
return noteNotebooks.length === 0 ? null : (
@@ -102,7 +190,7 @@ export default function Notebooks({ note, close, full }) {
{noteNotebooks.length > 1 && !full ? (
<Button
title={`View all linked notebooks`}
title={`See all linked notebooks`}
fontSize={SIZE.xs}
style={{
alignSelf: "flex-end",

View File

@@ -27,7 +27,6 @@ import { sleep } from "../../utils/time";
import ManageTagsSheet from "../sheets/manage-tags";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();
@@ -48,7 +47,7 @@ export const Tags = ({ item, close }) => {
>
<Button
onPress={async () => {
ManageTagsSheet.present([item.id]);
ManageTagsSheet.present([item]);
}}
buttonType={{
text: colors.primary.accent

View File

@@ -0,0 +1,70 @@
/*
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 { View } from "react-native";
import { TopicNotes } from "../../screens/notes/topic-notes";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
export const Topics = ({ item, close }) => {
const open = (topic) => {
close();
TopicNotes.navigate(topic, true);
};
const renderItem = (topic) => (
<Button
key={topic.id}
title={topic.title}
type="grayBg"
// buttonType={{
// text: colors.primary.accent
// }}
height={30}
onPress={() => open(topic)}
icon="bookmark-outline"
fontSize={SIZE.xs}
style={{
marginRight: 5,
paddingHorizontal: 8,
borderRadius: 100,
marginVertical: 5
}}
/>
);
return item &&
item.type === "notebook" &&
item.topics &&
item.topics.length > 0 ? (
<View
style={{
flexDirection: "row",
marginTop: 5,
width: "100%",
flexWrap: "wrap"
}}
>
{item.topics
.sort((a, b) => a.dateEdited - b.dateEdited)
.slice(0, 6)
.map(renderItem)}
</View>
) : null;
};

View File

@@ -0,0 +1,365 @@
/*
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 } from "react";
import { BackHandler, Platform, View } from "react-native";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { deleteItems } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import ManageTagsSheet from "../sheets/manage-tags";
export const SelectionHeader = React.memo(() => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const setSelectionMode = useSelectionStore((state) => state.setSelectionMode);
const clearSelection = useSelectionStore((state) => state.clearSelection);
const currentScreen = useNavigationStore((state) => state.currentScreen);
const screen = currentScreen.name;
const insets = useGlobalSafeAreaInsets();
SearchService.prepareSearch?.();
const allItems = SearchService.getSearchInformation()?.get() || [];
const allSelected = allItems.length === selectedItemsList.length;
useEffect(() => {
if (selectionMode) {
tabBarRef.current?.lock();
} else {
tabBarRef.current?.unlock();
}
}, [selectionMode]);
const addToFavorite = async () => {
if (selectedItemsList.length > 0) {
selectedItemsList.forEach((item) => {
db.notes.note(item.id).favorite();
});
Navigation.queueRoutesForUpdate();
clearSelection();
}
};
const restoreItem = async () => {
if (selectedItemsList.length > 0) {
let noteIds = [];
selectedItemsList.forEach((item) => {
noteIds.push(item.id);
});
await db.trash.restore(...noteIds);
Navigation.queueRoutesForUpdate();
clearSelection();
ToastEvent.show({
heading: "Restore successful",
type: "success"
});
}
};
const deleteItem = async () => {
presentDialog({
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
paragraph: `Are you sure you want to delete ${
selectedItemsList.length > 1
? "these items permanently?"
: "this item permanently?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
if (selectedItemsList.length > 0) {
let noteIds = [];
selectedItemsList.forEach((item) => {
noteIds.push(item.id);
});
await db.trash.delete(...noteIds);
Navigation.queueRoutesForUpdate();
clearSelection();
}
},
positiveType: "errorShade"
});
};
const onBackPress = useCallback(() => {
clearSelection();
return true;
}, [clearSelection]);
useEffect(() => {
if (selectionMode) {
BackHandler.addEventListener("hardwareBackPress", onBackPress);
} else {
BackHandler.removeEventListener("hardwareBackPress", onBackPress);
}
}, [onBackPress, selectionMode]);
return !selectionMode ? null : (
<View
style={{
width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50,
paddingTop: Platform.OS === "android" ? insets.top : null,
backgroundColor: colors.primary.background,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
zIndex: 999,
paddingHorizontal: 12,
marginVertical: 10
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
borderRadius: 100
}}
>
<IconButton
customStyle={{
justifyContent: "center",
alignItems: "center",
height: 40,
width: 40,
borderRadius: 100,
marginRight: 10
}}
type="grayBg"
onPress={() => {
setSelectionMode(!selectionMode);
}}
size={SIZE.xl}
color={colors.primary.icon}
name="close"
/>
<View
style={{
backgroundColor: colors.secondary.background,
height: 40,
borderRadius: 100,
paddingHorizontal: 16,
justifyContent: "center",
flexDirection: "row",
alignItems: "center"
}}
>
<Heading size={SIZE.md} color={colors.primary.accent}>
{selectedItemsList.length}
</Heading>
</View>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center"
}}
>
<IconButton
onPress={async () => {
useSelectionStore
.getState()
.setAll(allSelected ? [] : [...allItems]);
}}
tooltipText="Select all"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="select-all"
size={SIZE.xl}
/>
{screen === "Trash" ||
screen === "Notebooks" ||
screen === "Reminders" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.primary.icon}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
}}
tooltipText="Export"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="export"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
//setSelectionMode(false);
await sleep(100);
MoveNoteSheet.present();
}}
customStyle={{
marginLeft: 10
}}
tooltipText="Add to notebooks"
tooltipPosition={4}
color={colors.primary.paragraph}
name="plus"
size={SIZE.xl}
/>
</>
)}
{screen === "TopicNotes" || screen === "Notebook" ? (
<IconButton
onPress={async () => {
if (selectedItemsList.length > 0) {
const currentScreen =
useNavigationStore.getState().currentScreen;
if (screen === "Notebook") {
for (const item of selectedItemsList) {
await db.relations.unlink(
{ type: "notebook", id: currentScreen.id },
item
);
}
} else {
await db.notes.removeFromNotebook(
{
id: currentScreen.notebookId,
topic: currentScreen.id
},
...selectedItemsList.map((item) => item.id)
);
}
Navigation.queueRoutesForUpdate();
clearSelection();
}
}}
customStyle={{
marginLeft: 10
}}
tooltipText={`Remove from ${
screen === "Notebook" ? "notebook" : "topic"
}`}
tooltipPosition={4}
testID="select-minus"
color={colors.primary.paragraph}
name="minus"
size={SIZE.xl}
/>
) : null}
{screen === "Favorites" ? (
<IconButton
onPress={addToFavorite}
customStyle={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
tooltipPosition={4}
color={colors.primary.paragraph}
name="star-off"
size={SIZE.xl}
/>
) : null}
{screen === "Trash" ? null : (
<IconButton
customStyle={{
marginLeft: 10
}}
onPress={() => {
deleteItems();
}}
tooltipText="Move to trash"
tooltipPosition={1}
color={colors.primary.paragraph}
name="delete"
size={SIZE.xl}
/>
)}
{screen === "Trash" ? (
<>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={restoreItem}
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>
</>
) : null}
</View>
</View>
);
});
SelectionHeader.displayName = "SelectionHeader";
export default SelectionHeader;

View File

@@ -1,363 +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 { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect } from "react";
import {
BackHandler,
NativeEventSubscription,
Platform,
View
} from "react-native";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { deleteItems } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { updateNotebook } from "../../utils/notebooks";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import ManageTagsSheet from "../sheets/manage-tags";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import Animated, { FadeInUp } from "react-native-reanimated";
export const SelectionHeader = React.memo(
({
items,
type,
id,
renderedInRoute
}: {
items?: VirtualizedGrouping<Item>;
id?: string;
type?: ItemType;
renderedInRoute?: string;
}) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const clearSelection = useSelectionStore((state) => state.clearSelection);
const insets = useGlobalSafeAreaInsets();
const allSelected =
items?.ids?.filter((id) => typeof id === "string").length ===
selectedItemsList.length;
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
useEffect(() => {
if (selectionMode) {
tabBarRef.current?.lock();
} else {
tabBarRef.current?.unlock();
}
}, [selectionMode]);
const addToFavorite = async () => {
if (!selectedItemsList.length) return;
db.notes.favorite(true, ...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
};
const restoreItem = async () => {
if (!selectedItemsList.length) return;
await db.trash.restore(...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
ToastManager.show({
heading: "Restore successful",
type: "success"
});
};
const deleteItem = async () => {
presentDialog({
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
paragraph: `Are you sure you want to delete ${
selectedItemsList.length > 1
? "these items permanently?"
: "this item permanently?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
if (!selectedItemsList.length) return;
await db.trash.delete(...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
},
positiveType: "errorShade"
});
};
useEffect(() => {
const onBackPress = () => {
clearSelection();
return true;
};
let sub: NativeEventSubscription | undefined;
if (selectionMode) {
sub = BackHandler.addEventListener("hardwareBackPress", onBackPress);
}
return () => {
sub?.remove();
};
}, [clearSelection, selectionMode]);
return selectionMode !== type || focusedRouteId !== id ? null : (
<Animated.View
entering={FadeInUp}
style={{
width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50,
paddingTop: Platform.OS === "android" ? insets.top : null,
backgroundColor: colors.primary.background,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
zIndex: 999,
paddingHorizontal: 12,
marginVertical: 10
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
borderRadius: 100
}}
>
<IconButton
customStyle={{
justifyContent: "center",
alignItems: "center",
height: 40,
width: 40,
borderRadius: 100,
marginRight: 10
}}
type="grayBg"
onPress={() => {
clearSelection();
}}
size={SIZE.xl}
color={colors.primary.icon}
name="close"
/>
<View
style={{
backgroundColor: colors.secondary.background,
height: 40,
borderRadius: 100,
paddingHorizontal: 16,
justifyContent: "center",
flexDirection: "row",
alignItems: "center"
}}
>
<Heading size={SIZE.md} color={colors.primary.accent}>
{selectedItemsList.length}
</Heading>
</View>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center"
}}
>
<IconButton
onPress={async () => {
useSelectionStore
.getState()
.setAll(
allSelected
? []
: [
...((items?.ids.filter(
(id) => typeof id !== "object"
) as string[]) || [])
]
);
}}
tooltipText="Select all"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={
allSelected ? colors.primary.accent : colors.primary.paragraph
}
name="select-all"
size={SIZE.xl}
/>
{type !== "note" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.primary.icon}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
}}
tooltipText="Export"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="export"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
MoveNoteSheet.present();
}}
customStyle={{
marginLeft: 10
}}
tooltipText="Add to notebooks"
tooltipPosition={4}
color={colors.primary.paragraph}
name="plus"
size={SIZE.xl}
/>
</>
)}
{renderedInRoute === "Notebook" ? (
<IconButton
onPress={async () => {
if (selectedItemsList.length > 0) {
if (!id) return;
await db.notes.removeFromNotebook(id, ...selectedItemsList);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
}
}}
customStyle={{
marginLeft: 10
}}
tooltipText={`Remove from Notebook`}
tooltipPosition={4}
testID="select-minus"
color={colors.primary.paragraph}
name="minus"
size={SIZE.xl}
/>
) : null}
{focusedRouteId === "Favorites" ? (
<IconButton
onPress={addToFavorite}
customStyle={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
tooltipPosition={4}
color={colors.primary.paragraph}
name="star-off"
size={SIZE.xl}
/>
) : null}
{type === "trash" ? null : (
<IconButton
customStyle={{
marginLeft: 10
}}
onPress={() => {
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
);
}}
tooltipText="Move to trash"
tooltipPosition={1}
color={colors.primary.paragraph}
name="delete"
size={SIZE.xl}
/>
)}
{type === "trash" ? (
<>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={restoreItem}
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>
</>
) : null}
</View>
</Animated.View>
);
}
);
SelectionHeader.displayName = "SelectionHeader";
export default SelectionHeader;

View File

@@ -101,6 +101,7 @@ const SheetProvider = ({ context = "global" }) => {
setVisible(false);
setData(null);
}}
keyboardHandlerDisabled={data?.keyboardHandlerDisabled}
bottomPadding={!data.noBottomPadding}
enableGesturesInScrollView={
typeof data.enableGesturesInScrollView === "undefined"

View File

@@ -0,0 +1,553 @@
/*
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 {
Keyboard,
StyleSheet,
TextInput,
TouchableOpacity,
View
} from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time";
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 { MoveNotes } from "../move-notes/movenote";
let refs = [];
export class AddNotebookSheet extends React.Component {
constructor(props) {
super(props);
refs = [];
this.state = {
notebook: props.notebook,
topics:
props.notebook?.topics?.map((item) => {
return item.title;
}) || [],
description: null,
titleFocused: false,
descFocused: false,
count: 0,
topicInputFocused: false,
editTopic: false,
loading: false
};
this.title = props.notebook?.title;
this.description = props.notebook?.description;
this.listRef;
this.prevItem = null;
this.prevIndex = null;
this.currentSelectedInput = null;
this.id = props.notebook?.id;
this.backPressCount = 0;
this.currentInputValue = null;
this.titleRef;
this.descriptionRef;
this.topicsToDelete = [];
this.hiddenInput = createRef();
this.topicInputRef = createRef();
this.addingTopic = false;
this.actionSheetRef = props.actionSheetRef;
}
componentWillUnmount() {
refs = [];
}
componentDidMount() {
sleep(300).then(() => {
!this.state.notebook && this.titleRef?.focus();
});
}
close = () => {
refs = [];
this.props.close(true);
};
onDelete = (index) => {
let { topics } = this.state;
let prevTopics = topics;
refs = [];
prevTopics.splice(index, 1);
let edit = this.state.notebook;
if (edit && edit.id) {
let topicToDelete = edit.topics[index];
if (topicToDelete) {
this.topicsToDelete.push(topicToDelete.id);
}
}
let nextTopics = [...prevTopics];
if (this.prevIndex === index) {
this.prevIndex = null;
this.prevItem = null;
this.currentInputValue = null;
this.topicInputRef.current?.setNativeProps({
text: null
});
}
this.setState({
topics: nextTopics
});
};
addNewNotebook = async () => {
this.setState({
loading: true
});
let { topics, notebook } = this.state;
if (!this.title || this.title?.trim().length === 0) {
ToastEvent.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
this.setState({
loading: false
});
return;
}
let toEdit = null;
if (notebook) {
toEdit = db.notebooks.notebook(notebook.id).data;
}
let prevTopics = [...topics];
if (this.currentInputValue && this.currentInputValue.trim().length !== 0) {
if (this.prevItem != null) {
prevTopics[this.prevIndex] = this.currentInputValue;
} else {
prevTopics.push(this.currentInputValue);
this.currentInputValue = null;
}
}
let newNotebookId = null;
if (notebook) {
if (this.topicsToDelete?.length > 0) {
await db.notebooks
.notebook(toEdit.id)
.topics.delete(...this.topicsToDelete);
toEdit = db.notebooks.notebook(toEdit.id).data;
}
await db.notebooks.add({
title: this.title,
description: this.description,
id: notebook.id
});
let nextTopics = toEdit.topics.map((topic, index) => {
let copy = { ...topic };
copy.title = prevTopics[index];
return copy;
});
prevTopics.forEach((title, index) => {
if (!nextTopics[index]) {
nextTopics.push(title);
}
});
await db.notebooks.notebook(toEdit.id).topics.add(...nextTopics);
this.close();
} else {
newNotebookId = await db.notebooks.add({
title: this.title,
description: this.description,
topics: prevTopics,
id: null
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
};
onSubmit = (forward = true) => {
this.hiddenInput.current?.focus();
let willFocus = true;
let { topics } = this.state;
if (!this.currentInputValue || this.currentInputValue?.trim().length === 0)
return;
let prevTopics = [...topics];
if (this.prevItem === null) {
prevTopics.push(this.currentInputValue);
this.setState({
topics: prevTopics
});
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
this.currentInputValue = null;
} else {
prevTopics[this.prevIndex] = this.currentInputValue;
this.setState({
topics: prevTopics
});
this.currentInputValue = null;
if (this.state.editTopic) {
this.topicInputRef.current?.blur();
Keyboard.dismiss();
this.setState({
editTopic: false
});
willFocus = false;
}
this.prevItem = null;
this.prevIndex = null;
this.currentInputValue = null;
if (forward) {
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
}, 30);
}
}
this.topicInputRef.current?.setNativeProps({
text: ""
});
willFocus && this.topicInputRef.current?.focus();
};
renderTopicItem = ({ item, index }) => (
<TopicItem
item={item}
onPress={(item, index) => {
this.prevIndex = index;
this.prevItem = item;
this.topicInputRef.current?.setNativeProps({
text: item
});
this.topicInputRef.current?.focus();
this.currentInputValue = item;
this.setState({
editTopic: true
});
}}
onDelete={this.onDelete}
index={index}
colors={this.props.colors}
/>
);
render() {
const { colors } = this.props;
const { topics, topicInputFocused, notebook } = this.state;
return (
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
>
<TextInput
ref={this.hiddenInput}
style={{
width: 1,
height: 1,
opacity: 0,
position: "absolute"
}}
blurOnSubmit={false}
/>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>
{notebook && notebook.dateCreated
? "Edit Notebook"
: "New Notebook"}
</Heading>
<Button
title="Save"
type="accent"
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24
}}
onPress={this.addNewNotebook}
/>
</View>
<Seperator />
<Input
fwdRef={(ref) => (this.titleRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
this.title = value;
}}
placeholder="Enter a title"
onSubmit={() => {
this.descriptionRef.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : null}
/>
<Input
fwdRef={(ref) => (this.descriptionRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
this.description = value;
}}
placeholder="Describe your notebook."
onSubmit={() => {
this.topicInputRef.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : null}
/>
<Input
fwdRef={this.topicInputRef}
testID={notesnook.ids.dialogs.notebook.inputs.topic}
onChangeText={(value) => {
this.currentInputValue = value;
if (this.prevItem !== null) {
refs[this.prevIndex].setNativeProps({
text: value,
style: {
borderBottomColor: colors.primary.accent
}
});
}
}}
returnKeyLabel="Done"
returnKeyType="done"
onSubmit={() => {
this.onSubmit();
}}
blurOnSubmit={false}
button={{
testID: "topic-add-button",
icon: this.state.editTopic ? "check" : "plus",
onPress: this.onSubmit,
color: topicInputFocused
? colors.primary.accent
: colors.secondary.icon
}}
placeholder="Add a topic"
/>
<FlatList
data={topics}
ref={(ref) => (this.listRef = ref)}
nestedScrollEnabled
keyExtractor={(item, index) => item + index.toString()}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListFooterComponent={
topics.length === 0 ? null : <View style={{ height: 50 }} />
}
renderItem={this.renderTopicItem}
/>
</View>
);
}
}
AddNotebookSheet.present = (notebook) => {
presentSheet({
component: (ref, close, _update, colors) => (
<AddNotebookSheet
actionSheetRef={ref}
notebook={notebook}
close={close}
colors={colors}
/>
)
});
};
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
const topicRef = (ref) => (refs[index] = ref);
return (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: colors.secondary.background,
borderRadius: 5,
marginVertical: 5
}}
>
<TouchableOpacity
style={{
width: "80%",
backgroundColor: "transparent",
zIndex: 10,
position: "absolute",
height: 30
}}
onPress={() => {
onPress(item, index);
}}
/>
<TextInput
ref={topicRef}
editable={false}
style={[
styles.topicInput,
{
color: colors.primary.paragraph
}
]}
defaultValue={item}
placeholderTextColor={colors.primary.placeholder}
/>
<View
style={{
width: 80,
position: "absolute",
right: 0,
alignItems: "center",
flexDirection: "row",
justifyContent: "flex-end"
}}
>
<IconButton
onPress={() => {
onPress(item, index);
}}
name="pencil"
size={SIZE.lg - 5}
color={colors.secondary.icon}
/>
<IconButton
onPress={() => {
onDelete(index);
}}
name="minus"
size={SIZE.lg}
color={colors.secondary.icon}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.3)",
justifyContent: "center",
alignItems: "center"
},
overlay: {
width: "100%",
height: "100%",
position: "absolute"
},
headingContainer: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center"
},
headingText: {
marginLeft: 5,
fontSize: SIZE.xl
},
input: {
paddingRight: 12,
paddingHorizontal: 0,
borderRadius: 0,
minHeight: 45,
fontSize: SIZE.md,
padding: pv - 2,
borderBottomWidth: 1,
marginTop: 10,
marginBottom: 5
},
addBtn: {
width: "12%",
minHeight: 45,
justifyContent: "center",
alignItems: "center",
position: "absolute",
right: 0
},
buttonContainer: {
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
width: "100%",
marginTop: 20
},
topicContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: 10
},
topicInput: {
padding: pv - 5,
fontSize: SIZE.sm,
//fontFamily: "sans-serif",
paddingHorizontal: ph,
paddingRight: 40,
paddingVertical: 10,
width: "100%",
maxWidth: "100%"
},
topicBtn: {
borderRadius: 5,
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center",
position: "absolute",
right: 0
}
});

View File

@@ -1,189 +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 { Notebook } from "@notesnook/core";
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
import { eOnNotebookUpdated } from "../../../utils/events";
import { getParentNotebookId } from "../../../utils/notebooks";
export const AddNotebookSheet = ({
notebook,
parentNotebook,
close
}: {
notebook?: Notebook;
parentNotebook?: Notebook;
close: ((context?: string | undefined) => void) | undefined;
}) => {
const title = useRef(notebook?.title);
const description = useRef(notebook?.description);
const titleInput = useRef<TextInput>(null);
const descriptionInput = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const onSaveChanges = async () => {
if (loading) return;
setLoading(true);
if (!title.current || title?.current.trim().length === 0) {
ToastManager.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
setLoading(false);
return;
}
const id = await db.notebooks.add({
title: title.current,
description: description.current,
id: notebook?.id
});
if (parentNotebook) {
await db.relations.add(parentNotebook, {
type: "notebook",
id: id
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
const parent =
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
eSendEvent(eOnNotebookUpdated, parent);
if (notebook) {
setImmediate(() => {
console.log(parent, notebook.id);
eSendEvent(eOnNotebookUpdated, notebook.id);
});
}
if (!notebook) {
MoveNotes.present(await db.notebooks.notebook(id));
} else {
close?.();
}
};
return (
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>
{notebook ? "Edit Notebook" : "New Notebook"}
</Heading>
<Button
title={notebook ? "Save" : "Add"}
type="accent"
height={40}
style={{
paddingHorizontal: 24
}}
onPress={onSaveChanges}
/>
</View>
<Seperator />
<Input
fwdRef={titleInput}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
title.current = value;
}}
onLayout={() => {
setImmediate(() => {
titleInput?.current?.focus();
});
}}
placeholder="Enter a title"
onSubmit={() => {
descriptionInput.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : ""}
/>
<Input
fwdRef={descriptionInput}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
description.current = value;
}}
placeholder="Describe your notebook."
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
</View>
);
};
AddNotebookSheet.present = (
notebook?: Notebook,
parentNotebook?: Notebook,
context?: string
) => {
presentSheet({
context: context,
component: (ref, close) => (
<AddNotebookSheet
notebook={notebook}
parentNotebook={parentNotebook}
close={close}
/>
)
});
};

View File

@@ -0,0 +1,29 @@
/*
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 { createContext, useContext } from "react";
export const SelectionContext = createContext({
toggleSelection: (item) => null,
deselect: (item) => null,
select: (item) => null,
deselectAll: () => null
});
export const SelectionProvider = SelectionContext.Provider;
export const useSelectionContext = () => useContext(SelectionContext);

View File

@@ -0,0 +1,76 @@
/*
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, useState } from "react";
import { FlashList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { ListHeaderInputItem } from "./list-header-item.js";
export const FilteredList = ({
data,
itemType,
onAddItem,
hasHeaderSearch,
listRef,
...restProps
}) => {
const [filtered, setFiltered] = useState(data);
const query = useRef();
const onChangeText = useCallback(
(value) => {
query.current = value;
try {
if (!value) return setFiltered(data);
const results = db.lookup[itemType + "s"]([...data], value);
setFiltered(results);
} catch (e) {
console.warn(e.message);
}
},
[data, itemType]
);
const onSubmit = async (value) => {
return await onAddItem(value);
};
useEffect(() => {
onChangeText(query.current);
}, [data, onChangeText]);
return (
<FlashList
{...restProps}
data={filtered}
ref={listRef}
ListHeaderComponent={
hasHeaderSearch ? (
<ListHeaderInputItem
onSubmit={onSubmit}
onChangeText={onChangeText}
itemType={itemType}
testID={"list-input" + itemType}
placeholder={`Search or add a new ${itemType}`}
/>
) : null
}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
/>
);
};

View File

@@ -0,0 +1,486 @@
/*
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, useMemo } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
eSendEvent,
presentSheet,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { eOnTopicSheetUpdate } from "../../../utils/events";
import { useThemeColors } from "@notesnook/theme";
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";
import { SelectionProvider } from "./context";
import { FilteredList } from "./filtered-list";
import { ListItem } from "./list-item";
import { useItemSelectionStore } from "./store";
import { useRelationStore } from "../../../stores/use-relation-store";
const MoveNoteSheet = ({ note, actionSheetRef }) => {
const { colors } = useThemeColors();
const notebooks = useNotebookStore((state) =>
state.notebooks.filter((n) => n?.type === "notebook")
);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const onAddNotebook = async (title) => {
if (!title || title.trim().length === 0) {
ToastEvent.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
return false;
}
await db.notebooks.add({
title: title,
description: null,
topics: [],
id: null
});
setNotebooks();
return true;
};
const openAddTopicDialog = (item) => {
presentDialog({
context: "move_note",
input: true,
inputPlaceholder: "Enter title",
title: "New topic",
paragraph: "Add a new topic in " + item.title,
positiveText: "Add",
positivePress: (value) => {
return onAddTopic(value, item);
}
});
};
const onAddTopic = useCallback(
async (value, item) => {
if (!value || value.trim().length === 0) {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return false;
}
await db.notebooks.notebook(item.id).topics.add(value);
setNotebooks();
return true;
},
[setNotebooks]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const getSelectedNotesCountInItem = React.useCallback(
(item) => {
switch (item.type) {
case "notebook": {
const notes = db.relations.from(item, "note");
if (notes.length === 0) return 0;
let count = 0;
selectedItemsList.forEach((item) =>
notes.findIndex((note) => note.id === item.id) > -1
? count++
: undefined
);
return count;
}
case "topic": {
const noteIds = db.notes?.topicReferences.get(item.id);
let count = 0;
selectedItemsList.forEach((item) =>
noteIds.indexOf(item.id) > -1 ? count++ : undefined
);
return count;
}
}
},
[selectedItemsList]
);
useEffect(() => {
resetItemState();
return () => {
useItemSelectionStore.getState().setMultiSelect(false);
useItemSelectionStore.getState().setItemState({});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const resetItemState = useCallback(
(state) => {
const itemState = {};
const notebooks = db.notebooks.all;
let count = 0;
for (let notebook of notebooks) {
itemState[notebook.id] = state
? state
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[notebook.id] === "selected") {
count++;
}
for (let topic of notebook.topics) {
itemState[topic.id] = state
? state
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
getSelectedNotesCountInItem(topic, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[topic.id] === "selected") {
count++;
}
}
}
if (count > 1) {
useItemSelectionStore.getState().setMultiSelect(true);
} else {
useItemSelectionStore.getState().setMultiSelect(false);
}
useItemSelectionStore.getState().setItemState(itemState);
},
[getSelectedNotesCountInItem, selectedItemsList]
);
const getItemsForItem = (item) => {
switch (item.type) {
case "notebook":
return item.topics?.filter((t) => t.type === "topic");
}
};
function areAllSelectedItemsInNotebook(notebook, items) {
const notes = db.relations.from(notebook, "note");
if (notes.length === 0) return false;
return items.every((item) => {
return notes.find((note) => note.id === item.id);
});
}
function areAllSelectedItemsInTopic(topic, items) {
return items.every((item) => {
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
});
}
const updateItemState = useCallback(function (item, state) {
const itemState = { ...useItemSelectionStore.getState().itemState };
const mergeState = {
[item.id]: state
};
useItemSelectionStore.getState().setItemState({
...itemState,
...mergeState
});
}, []);
const contextValue = useMemo(
() => ({
toggleSelection: (item) => {
const itemState = useItemSelectionStore.getState().itemState;
if (itemState[item.id] === "selected") {
updateItemState(item, "deselected");
} else {
updateItemState(item, "selected");
}
},
deselect: (item) => {
updateItemState(item, "deselected");
},
select: (item) => {
updateItemState(item, "selected");
},
deselectAll: (state) => {
resetItemState(state);
}
}),
[resetItemState, updateItemState]
);
const getItemFromId = (id) => {
for (const nb of notebooks) {
if (nb.id === id) return nb;
for (const tp of nb.topics) {
if (tp.id === id) return tp;
}
}
};
const onSave = async () => {
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
const itemState = useItemSelectionStore.getState().itemState;
for (const id in itemState) {
const item = getItemFromId(id);
if (itemState[id] === "selected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.add(item, { id: noteId, type: "note" });
}
} else {
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
}
} else if (itemState[id] === "deselected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.unlink(item, { id: noteId, type: "note" });
}
} else {
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id,
rebuildCache: true
},
...noteIds
);
}
}
}
Navigation.queueRoutesForUpdate();
setNotebooks();
eSendEvent(eOnTopicSheetUpdate);
SearchService.updateAndSearch();
useRelationStore.getState().update();
actionSheetRef.current?.hide();
};
return (
<>
<Dialog context="move_note" />
<View>
<TouchableOpacity
style={{
width: "100%",
height: "100%",
position: "absolute"
}}
onPress={() => {
Keyboard.dismiss();
}}
/>
<View
style={{
paddingHorizontal: 12,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "flex-start"
}}
>
<DialogHeader
style={{
minHeight: 10,
flexShrink: 1
}}
title="Select notebooks"
paragraph={
!multiSelect
? "Long press to enable multi-select."
: "Select topics you want to add note(s) to."
}
/>
<Button
height={35}
style={{
borderRadius: 100,
paddingHorizontal: 24,
alignSelf: "flex-start"
}}
title="Save"
type={"accent"}
onPress={onSave}
/>
</View>
<View
style={{
paddingHorizontal: 12
}}
>
<Button
title="Reset selection"
height={30}
style={{
alignSelf: "flex-start",
paddingHorizontal: 0,
width: "100%",
marginTop: 6
}}
type="grayAccent"
onPress={() => {
resetItemState();
}}
/>
</View>
<SelectionProvider value={contextValue}>
<View
style={{
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85,
height: 50 * (notebooks.length + 2)
}}
>
<FilteredList
ListEmptyComponent={
notebooks.length > 0 ? null : (
<View
style={{
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center"
}}
>
<Icon
name="book-outline"
color={colors.primary.icon}
size={100}
/>
<Paragraph style={{ marginBottom: 10 }}>
You do not have any notebooks.
</Paragraph>
</View>
)
}
estimatedItemSize={50}
data={notebooks}
hasHeaderSearch={true}
renderItem={({ item, index }) => (
<ListItem
item={item}
key={item.id}
index={index}
hasNotes={getSelectedNotesCountInItem(item) > 0}
sheetRef={actionSheetRef}
infoText={
<>
{item.topics.length === 1
? item.topics.length + " topic"
: item.topics.length + " topics"}
</>
}
getListItems={getItemsForItem}
getSublistItemProps={(topic) => ({
hasNotes: getSelectedNotesCountInItem(topic) > 0,
style: {
marginBottom: 0,
height: 40
},
onPress: (item) => {
const itemState =
useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
} else {
contextValue.deselect(item);
}
},
key: item.id,
type: "transparent"
})}
icon={(expanded) => ({
name: expanded ? "chevron-up" : "chevron-down",
color: expanded
? colors.primary.accent
: colors.primary.paragraph
})}
onScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
hasSubList={true}
hasHeaderSearch={false}
type="grayBg"
sublistItemType="topic"
onAddItem={(title) => {
return onAddTopic(title, item);
}}
onAddSublistItem={(item) => {
openAddTopicDialog(item);
}}
onPress={(item) => {
const itemState =
useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
} else {
contextValue.deselect(item);
}
}}
/>
)}
itemType="notebook"
onAddItem={async (title) => {
return await onAddNotebook(title);
}}
ListFooterComponent={<View style={{ height: 20 }} />}
/>
</View>
</SelectionProvider>
</View>
</>
);
};
MoveNoteSheet.present = (note) => {
presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false,
noBottomPadding: true
});
};
export default MoveNoteSheet;

View File

@@ -1,257 +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 { GroupHeader, Note } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback, useEffect } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import { ActionSheetRef, FlashList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { ItemSelection } from "../../../stores/item-selection-store";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { updateNotebook } from "../../../utils/notebooks";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import SheetProvider from "../../sheet-provider";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Paragraph from "../../ui/typography/paragraph";
import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
.to(
{
type: "note",
ids: items
},
"notebook"
)
.get();
const initialSelectionState: ItemSelection = {};
const notebookIds = [
...new Set(relations.map((relation) => relation.fromId))
];
for (const id of notebookIds) {
const all = items.every((noteId) => {
return (
relations.findIndex(
(relation) => relation.fromId === id && relation.toId === noteId
) > -1
);
});
if (all) {
initialSelectionState[id] = "selected";
} else {
initialSelectionState[id] = "intermediate";
}
}
useNotebookItemSelectionStore.setState({
initialState: initialSelectionState,
selection: { ...initialSelectionState },
multiSelect: relations.length > 1
});
}
const MoveNoteSheet = ({
note,
actionSheetRef
}: {
note: Note | undefined;
actionSheetRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const notebooks = useNotebookStore((state) => state.notebooks);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const multiSelect = useNotebookItemSelectionStore(
(state) => state.multiSelect
);
useEffect(() => {
const items = note ? [note.id] : selectedItemsList;
updateInitialSelectionState(items);
return () => {
useNotebookItemSelectionStore.setState({
initialState: {},
selection: {},
multiSelect: false,
canEnableMultiSelectMode: true
});
};
}, [note, selectedItemsList]);
const onSave = async () => {
const noteIds = note ? [note.id] : selectedItemsList;
const changedNotebooks = useNotebookItemSelectionStore.getState().selection;
for (const id in changedNotebooks) {
const item = await db.notebooks.notebook(id);
if (!item) continue;
if (changedNotebooks[id] === "selected") {
for (const id of noteIds) {
await db.relations.add(item, { id: id, type: "note" });
updateNotebook(item.id);
}
} else if (changedNotebooks[id] === "deselected") {
for (const id of noteIds) {
await db.relations.unlink(item, { id: id, type: "note" });
updateNotebook(item.id);
}
}
}
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
actionSheetRef.current?.hide();
};
const renderNotebook = useCallback(
({ item, index }: { item: string | GroupHeader; index: number }) =>
(item as GroupHeader).type === "header" ? null : (
<NotebookItem items={notebooks} id={item as string} index={index} />
),
[notebooks]
);
return (
<>
<Dialog context="move_note" />
<SheetProvider context="link-notebooks" />
<View>
<TouchableOpacity
style={{
width: "100%",
height: "100%",
position: "absolute"
}}
onPress={() => {
Keyboard.dismiss();
}}
/>
<View
style={{
paddingHorizontal: 12,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "flex-start"
}}
>
<DialogHeader
style={{
minHeight: 10,
flexShrink: 1
}}
title="Select notebooks"
paragraph={
!multiSelect
? "Long press to enable multi-select."
: "Select topics you want to add note(s) to."
}
/>
<View
style={{
flexDirection: "row",
columnGap: 10
}}
>
<IconButton
name="restore"
type="grayAccent"
onPress={() => {
const items = note ? [note.id] : selectedItemsList;
updateInitialSelectionState(items);
}}
customStyle={{
width: 40,
height: 40
}}
/>
<Button
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24,
alignSelf: "flex-start"
}}
title="Save"
type={"accent"}
onPress={onSave}
/>
</View>
</View>
<View
style={{
paddingHorizontal: 0,
maxHeight: dimensions.height * 0.85,
height: dimensions.height * 0.85,
paddingTop: 6
}}
>
<FlashList
data={notebooks?.ids?.filter((id) => typeof id === "string")}
style={{
width: "100%"
}}
estimatedItemSize={50}
keyExtractor={(item) => item as string}
renderItem={renderNotebook}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
<Paragraph color={colors.primary.icon}>No notebooks</Paragraph>
</View>
}
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</View>
</View>
</>
);
};
MoveNoteSheet.present = (note?: Note) => {
presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false,
noBottomPadding: true
});
};
export default MoveNoteSheet;

View File

@@ -0,0 +1,88 @@
/*
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, { useRef, useState } from "react";
import { View } from "react-native";
import Input from "../../ui/input";
import Paragraph from "../../ui/typography/paragraph";
import { useThemeColors } from "@notesnook/theme";
export const ListHeaderInputItem = ({
onSubmit,
onChangeText,
placeholder,
testID
}) => {
const [focused, setFocused] = useState(false);
const { colors } = useThemeColors("sheet");
const [inputValue, setInputValue] = useState();
const inputRef = useRef();
return (
<View
style={{
width: "100%",
marginTop: 10,
marginBottom: 5
}}
>
<Input
fwdRef={inputRef}
onChangeText={(value) => {
setInputValue(value);
onChangeText?.(value);
}}
testID={testID}
blurOnSubmit={false}
onFocusInput={() => {
setFocused(true);
}}
onBlurInput={() => {
setFocused(false);
}}
button={{
icon: inputValue ? "plus" : "magnify",
color: focused ? colors.selected.icon : colors.secondary.icon,
onPress: async () => {
const result = await onSubmit(inputValue);
if (result) {
inputRef.current?.blur();
}
}
}}
placeholder={placeholder}
marginBottom={5}
/>
{inputValue ? (
<View
style={{
backgroundColor: colors.primary.shade,
padding: 5,
borderRadius: 5,
marginBottom: 10
}}
>
<Paragraph color={colors.primary.accent}>
Tap on + to add {`"${inputValue}"`}
</Paragraph>
</View>
) : null}
</View>
);
};

View File

@@ -0,0 +1,293 @@
/*
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, useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useSelectionContext } from "./context";
import { FilteredList } from "./filtered-list";
import { useItemSelectionStore } from "./store";
const SelectionIndicator = ({
item,
hasNotes,
selectItem,
onPress,
onChange
}) => {
const itemState = useItemSelectionStore((state) => state.itemState[item.id]);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const isSelected = itemState === "selected";
const isIntermediate = itemState === "intermediate";
const isRemoved = !isSelected && hasNotes;
const { colors } = useThemeColors("sheet");
useEffect(() => {
onChange?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemState]);
return (
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
isRemoved
? colors.static.red
: isIntermediate || isSelected
? colors.selected.icon
: colors.primary.icon
}
onPress={() => {
if (multiSelect) return selectItem();
onPress?.(item);
}}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
testID={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
);
};
export const ListItem = ({
item,
index,
icon,
infoText,
hasSubList,
onPress,
onScrollEnd,
getListItems,
style,
type,
sublistItemType,
onAddItem,
getSublistItemProps,
hasHeaderSearch,
onAddSublistItem,
hasNotes,
onChange,
sheetRef
}) => {
const { toggleSelection } = useSelectionContext();
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
const { colors } = useThemeColors("sheet");
const [expanded, setExpanded] = useState(false);
function selectItem() {
toggleSelection(item);
}
const getSelectedNotesCountInNotebookTopics = (item) => {
if (item.type === "topic") return;
let count = 0;
const noteIds = [];
for (let topic of item.topics) {
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
if (useItemSelectionStore.getState().itemState[topic.id] === "selected") {
count++;
}
}
useSelectionStore.getState().selectedItemsList.forEach((item) => {
if (noteIds.indexOf(item.id) > -1) {
count++;
}
});
return count;
};
useEffect(() => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
}, [item]);
const onChangeSubItem = () => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
};
return (
<View
style={{
overflow: "hidden",
marginBottom: 10,
...style
}}
>
<PressableButton
onPress={() => {
if (hasSubList) return setExpanded(!expanded);
if (multiSelect) return selectItem();
onPress?.(item);
}}
type={type}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
customStyle={{
height: style?.height || 50,
width: "100%",
alignItems: "flex-start"
}}
>
<View
style={{
width: "100%",
height: 50,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<SelectionIndicator
hasNotes={hasNotes}
onPress={onPress}
item={item}
onChange={onChange}
selectItem={selectItem}
/>
<View>
{hasSubList && expanded ? (
<Heading size={SIZE.md}>{item.title}</Heading>
) : (
<Paragraph size={SIZE.sm}>{item.title}</Paragraph>
)}
{infoText ? (
<Paragraph size={SIZE.xs} color={colors.primary.icon}>
{infoText}
</Paragraph>
) : null}
</View>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{showSelectedIndicator ? (
<View
style={{
backgroundColor: colors.primary.accent,
width: 7,
height: 7,
borderRadius: 100,
marginRight: 12
}}
/>
) : null}
{onAddSublistItem ? (
<IconButton
name={"plus"}
testID="add-item-icon"
color={colors.primary.paragraph}
size={SIZE.xl}
onPress={() => {
onAddSublistItem(item);
}}
/>
) : null}
{icon ? (
<IconButton
name={icon(expanded).name}
color={icon(expanded).color}
size={icon(expanded).size || SIZE.xl}
onPress={
hasSubList
? () => setExpanded(!expanded)
: icon(expanded).onPress
}
/>
) : null}
</View>
</View>
</PressableButton>
{expanded && hasSubList ? (
<FilteredList
nestedScrollEnabled
data={getListItems(item)}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
onMomentumScrollEnd={onScrollEnd}
style={{
width: "95%",
alignSelf: "flex-end",
maxHeight: 250
}}
estimatedItemSize={40}
itemType={sublistItemType}
hasHeaderSearch={hasHeaderSearch}
renderItem={({ item, index }) => (
<ListItem
item={item}
{...getSublistItemProps(item)}
index={index}
onChange={onChangeSubItem}
onScrollEnd={onScrollEnd}
/>
)}
onAddItem={onAddItem}
/>
) : null}
</View>
);
};

View File

@@ -1,247 +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 { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useMemo } from "react";
import { View, useWindowDimensions } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { useTotalNotes } from "../../../hooks/use-db-item";
import { useNotebook } from "../../../hooks/use-notebook";
import useNavigationStore from "../../../stores/use-navigation-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import {
useNotebookExpandedStore,
useNotebookItemSelectionStore
} from "./store";
type NotebookParentProp = {
parent?: NotebookParentProp;
item?: Notebook;
};
export const NotebookItem = ({
id,
currentLevel = 0,
index,
parent,
items
}: {
id: string;
currentLevel?: number;
index: number;
parent?: NotebookParentProp;
items?: VirtualizedGrouping<Notebook>;
}) => {
const { nestedNotebooks, notebook: item } = useNotebook(id, items);
const ids = useMemo(() => (id ? [id] : []), [id]);
const { totalNotes: totalNotes } = useTotalNotes(ids, "notebook");
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
const { colors } = useThemeColors("sheet");
const selection = useNotebookItemSelectionStore((state) =>
id ? state.selection[id] : undefined
);
const isSelected = selection === "selected";
const isFocused = focusedRouteId === id;
const { fontScale } = useWindowDimensions();
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
const onPress = () => {
if (!item) return;
const state = useNotebookItemSelectionStore.getState();
if (isSelected) {
state.markAs(item, !state.initialState[id] ? undefined : "deselected");
return;
}
if (!state.multiSelect) {
const keys = Object.keys(state.selection);
const nextState: any = {};
for (const key in keys) {
nextState[key] = !state.initialState[key] ? undefined : "deselected";
}
console.log("Single item selection");
state.setSelection({
[item.id]: "selected",
...nextState
});
} else {
console.log("Multi item selection");
state.markAs(item, "selected");
}
};
return (
<View
style={{
paddingLeft: currentLevel > 0 && currentLevel < 6 ? 15 : undefined,
width: "100%"
}}
>
<PressableButton
type={"transparent"}
onLongPress={() => {
if (!item) return;
useNotebookItemSelectionStore.setState({
multiSelect: true
});
const state = useNotebookItemSelectionStore.getState();
useNotebookItemSelectionStore
.getState()
.markAs(
item,
!isSelected
? "selected"
: !state.initialState[id]
? undefined
: "deselected"
);
}}
testID={`add-to-notebook-item-${currentLevel}-${index}`}
onPress={onPress}
customStyle={{
justifyContent: "space-between",
width: "100%",
alignItems: "center",
flexDirection: "row",
paddingLeft: 12,
paddingRight: 12,
borderRadius: 0,
height: 45
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{nestedNotebooks?.ids.length ? (
<IconButton
size={SIZE.xl}
color={isSelected ? colors.selected.icon : colors.primary.icon}
onPress={() => {
useNotebookExpandedStore.getState().setExpanded(id);
}}
top={0}
left={0}
bottom={0}
right={0}
customStyle={{
width: 35,
height: 35
}}
name={expanded ? "chevron-down" : "chevron-right"}
/>
) : null}
<IconButton
size={SIZE.xl}
color={
isSelected
? colors.selected.icon
: selection === "deselected"
? colors.error.accent
: colors.primary.icon
}
onPress={onPress}
top={0}
left={0}
bottom={0}
right={0}
customStyle={{
width: 40,
height: 40
}}
name={
selection === "deselected"
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<Paragraph
color={
isFocused ? colors.selected.paragraph : colors.secondary.paragraph
}
size={SIZE.sm}
>
{item?.title}
</Paragraph>
</View>
<View
style={{
flexDirection: "row",
columnGap: 10,
alignItems: "center"
}}
>
{totalNotes?.(id) ? (
<Paragraph size={SIZE.sm} color={colors.secondary.paragraph}>
{totalNotes(id)}
</Paragraph>
) : null}
<IconButton
name="plus"
customStyle={{
width: 40 * fontScale,
height: 40 * fontScale
}}
testID={notesnook.ids.notebook.menu}
onPress={() => {
if (!item) return;
AddNotebookSheet.present(undefined, item, "link-notebooks");
}}
left={0}
right={0}
bottom={0}
top={0}
color={colors.primary.icon}
size={SIZE.xl}
/>
</View>
</PressableButton>
{!expanded
? null
: nestedNotebooks?.ids.map((id, index) => (
<NotebookItem
key={id as string}
id={id as string}
index={index}
currentLevel={currentLevel + 1}
items={nestedNotebooks}
parent={{
parent: parent,
item: item
}}
/>
))}
</View>
);
};

View File

@@ -16,24 +16,27 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import create from "zustand";
import { createItemSelectionStore } from "../../../stores/item-selection-store";
import create, { State } from "zustand";
export const useNotebookExpandedStore = create<{
expanded: {
[id: string]: boolean;
};
setExpanded: (id: string) => void;
}>((set, get) => ({
expanded: {},
setExpanded(id: string) {
type SelectionItemState = Record<
string,
"intermediate" | "selected" | "deselected"
>;
export interface SelectionStore extends State {
itemState: SelectionItemState;
setItemState: (state: SelectionItemState) => void;
multiSelect: boolean;
setMultiSelect: (multiSelect: boolean) => void;
}
export const useItemSelectionStore = create<SelectionStore>((set) => ({
itemState: {},
setItemState: (itemState) => {
set({
expanded: {
...get().expanded,
[id]: !get().expanded[id]
}
itemState
});
}
},
multiSelect: false,
setMultiSelect: (multiSelect) => set({ multiSelect })
}));
export const useNotebookItemSelectionStore = createItemSelectionStore(true);

View File

@@ -24,7 +24,7 @@ import {
eSendEvent,
presentSheet,
PresentSheetOptions,
ToastManager
ToastEvent
} from "../../../services/event-manager";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
@@ -87,7 +87,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
);
eSendEvent(eUserLoggedIn);
close?.();
ToastManager.show({
ToastEvent.show({
heading: `Email changed`,
message: `Your account email has been updated to ${emailChangeData.current.email}`,
type: "success",
@@ -96,7 +96,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
}
} catch (e) {
setLoading(false);
ToastManager.error(e as Error);
ToastEvent.error(e as Error);
}
};

View File

@@ -17,27 +17,20 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { Fragment, useState } from "react";
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
import { Platform, StyleSheet, View, ActivityIndicator } from "react-native";
import FileViewer from "react-native-file-viewer";
import Share from "react-native-share";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { requestInAppReview } from "../../../services/app-review";
import {
PresentSheetOptions,
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import { presentSheet, ToastEvent } from "../../../services/event-manager";
import Exporter from "../../../services/exporter";
import PremiumService from "../../../services/premium";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../../stores/use-user-store";
import { getElevationStyle } from "../../../utils/elevation";
import { eCloseSheet } from "../../../utils/events";
import { SIZE, ph, pv } from "../../../utils/size";
import { ph, pv, SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import DialogHeader from "../../dialog/dialog-header";
import { ProTag } from "../../premium/pro-tag";
@@ -47,48 +40,38 @@ import { PressableButton } from "../../ui/pressable";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { eSendEvent } from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { requestInAppReview } from "../../../services/app-review";
import { Dialog } from "../../dialog";
const ExportNotesSheet = ({
ids,
update
}: {
ids: string[];
update: ((props: PresentSheetOptions) => void) | undefined;
}) => {
const ExportNotesSheet = ({ notes, update }) => {
const { colors } = useThemeColors();
const [exporting, setExporting] = useState(false);
const [complete, setComplete] = useState(false);
const [result, setResult] = useState<
| {
fileName: string;
filePath: string;
name: string;
type: string;
}
| undefined
>();
const [result, setResult] = useState({});
const [status, setStatus] = useState(null);
const premium = useUserStore((state) => state.premium);
const save = async (type: "pdf" | "txt" | "md" | "html") => {
const save = async (type) => {
if (exporting) return;
if (!PremiumService.get() && type !== "txt") return;
setExporting(true);
update?.({ disableClosing: true } as PresentSheetOptions);
update({ disableClosing: true });
setComplete(false);
let result;
if (ids.length > 1) {
result = await Exporter.bulkExport(ids, type, setStatus);
if (notes.length > 1) {
result = await Exporter.bulkExport(notes, type, setStatus);
} else {
result = await Exporter.exportNote(ids[0], type);
result = await Exporter.exportNote(notes[0], type);
await sleep(1000);
}
if (!result) {
update?.({ disableClosing: false } as PresentSheetOptions);
update({ disableClosing: false });
return setExporting(false);
}
setResult(result as any);
update?.({ disableClosing: false } as PresentSheetOptions);
setResult(result);
update({ disableClosing: false });
setComplete(true);
setExporting(false);
requestInAppReview();
@@ -159,7 +142,9 @@ const ExportNotesSheet = ({
<DialogHeader
icon="export"
title={
ids.length > 1 ? `Export ${ids.length} Notes` : "Export Note"
notes.length > 1
? `Export ${notes.length} Notes`
: "Export Note"
}
paragraph={`All exports are saved in ${
Platform.OS === "android"
@@ -173,6 +158,8 @@ const ExportNotesSheet = ({
</>
) : null}
<Dialog context="export-notes" />
<View style={styles.buttonContainer}>
{!exporting && !complete ? (
actions.map((item) => (
@@ -245,7 +232,7 @@ const ExportNotesSheet = ({
<>
<ActivityIndicator />
<Paragraph>
{ids.length === 1
{notes.length === 1
? "Exporting note... Please wait"
: `Exporting notes${
status ? ` (${status})` : ``
@@ -270,8 +257,8 @@ const ExportNotesSheet = ({
}}
color={colors.secondary.heading}
>
{ids.length > 1
? `${ids.length} Notes exported`
{notes.length > 1
? `${notes.length} Notes exported`
: "Note exported"}
</Heading>
<Paragraph
@@ -279,8 +266,8 @@ const ExportNotesSheet = ({
textAlign: "center"
}}
>
Your {ids.length > 1 ? "notes are" : "note is"} exported
successfully as {result?.fileName}
Your {notes.length > 1 ? "notes are" : "note is"} exported
successfully as {result.fileName}
</Paragraph>
<Button
title="Open"
@@ -292,15 +279,14 @@ const ExportNotesSheet = ({
borderRadius: 100
}}
onPress={async () => {
if (!result?.filePath) return;
eSendEvent(eCloseSheet);
await sleep(500);
FileViewer.open(result?.filePath, {
FileViewer.open(result.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
console.log(e);
ToastManager.show({
ToastEvent.show({
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
type: "success",
@@ -319,7 +305,6 @@ const ExportNotesSheet = ({
borderRadius: 100
}}
onPress={async () => {
if (!result?.filePath) return;
if (Platform.OS === "ios") {
Share.open({
url: result.filePath
@@ -329,7 +314,7 @@ const ExportNotesSheet = ({
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
} as any).catch(console.log);
}).catch(console.log);
}
}}
/>
@@ -344,7 +329,7 @@ const ExportNotesSheet = ({
}}
onPress={async () => {
setComplete(false);
setResult(undefined);
setResult(null);
setExporting(false);
}}
/>
@@ -357,12 +342,15 @@ const ExportNotesSheet = ({
);
};
ExportNotesSheet.present = async (ids?: string[], allNotes?: boolean) => {
const exportNoteIds = allNotes ? await db.notes.all?.ids() : ids || [];
ExportNotesSheet.present = (notes, allNotes) => {
presentSheet({
component: (ref, close, update) => (
<ExportNotesSheet ids={exportNoteIds} update={update} />
)
<ExportNotesSheet
notes={allNotes ? db.notes.all : notes}
update={update}
/>
),
keyboardHandlerDisabled: true
});
};

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { View } from "react-native";
import FileViewer from "react-native-file-viewer";
import { ToastManager } from "../../../services/event-manager";
import { ToastEvent } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
export const ShareComponent = ({ uri, name, padding }) => {
@@ -40,7 +40,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch(() => {
ToastManager.show({
ToastEvent.show({
heading: "Cannot open",
message: `No application found to open ${name} file.`,
type: "success",

View File

@@ -22,7 +22,7 @@ import React, { useRef, useState } from "react";
import { Linking, Platform, Text, TextInput, View } from "react-native";
import { getVersion } from "react-native-device-info";
import { db } from "../../../common/database";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import { eSendEvent, ToastEvent } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../../stores/use-user-store";
@@ -100,7 +100,7 @@ Logged in: ${user ? "yes" : "no"}`,
positiveText: "Copy link",
positivePress: () => {
Clipboard.setString(issue_url);
ToastManager.show({
ToastEvent.show({
heading: "Issue url copied!",
type: "success",
context: "global"
@@ -110,8 +110,8 @@ Logged in: ${user ? "yes" : "no"}`,
});
} catch (e) {
setLoading(false);
ToastManager.show({
heading: "An error occured",
ToastEvent.show({
heading: "An error occurred",
message: e.message,
type: "error"
});

View File

@@ -0,0 +1,287 @@
/*
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, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useTagStore } from "../../../stores/use-tag-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size";
import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { IconButton } from "../../ui/icon-button";
const ManageTagsSheet = (props) => {
const { colors } = useThemeColors();
const [notes, setNotes] = useState(props.notes || []);
const allTags = useTagStore((state) => state.tags);
const [tags, setTags] = useState([]);
const [query, setQuery] = useState(null);
const inputRef = useRef();
const [focus, setFocus] = useState(false);
useEffect(() => {
sortTags();
}, [allTags, notes, query, sortTags]);
const sortTags = useCallback(() => {
let _tags = [...allTags];
_tags = _tags.filter((t) => t.type === "tag");
_tags = _tags.sort((a, b) => a.title.localeCompare(b.title));
if (query) {
_tags = db.lookup.tags(_tags, query);
}
const tagsMerged = [...notes.map((note) => note.tags || []).flat()];
if (!tagsMerged || !tagsMerged.length) {
setTags(_tags);
return;
}
let noteTags = [];
for (let tag of tagsMerged) {
let index = _tags.findIndex((t) => t.title === tag);
if (index !== -1) {
noteTags.push(_tags[index]);
_tags.splice(index, 1);
}
}
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
let combinedTags = [...noteTags, ..._tags];
setTags(combinedTags);
}, [allTags, notes, query]);
useEffect(() => {
useTagStore.getState().setTags();
}, []);
const onSubmit = async () => {
let _query = query;
if (!_query || _query === "" || _query.trimStart().length == 0) {
ToastEvent.show({
heading: "Tag field is empty",
type: "error",
context: "local"
});
return;
}
let tag = _query;
setNotes(
notes.map((note) => ({
...note,
tags: note.tags ? [...note.tags, tag] : [tag]
}))
);
setQuery(null);
inputRef.current?.setNativeProps({
text: ""
});
try {
for (let note of notes) {
await db.notes.note(note.id).tag(tag);
}
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
} catch (e) {
ToastEvent.show({
heading: "Cannot add tag",
type: "error",
message: e.message,
context: "local"
});
}
Navigation.queueRoutesForUpdate();
};
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
minHeight: focus ? "100%" : "60%"
}}
>
<Input
button={{
icon: "magnify",
color: colors.primary.accent,
size: SIZE.lg
}}
testID="tag-input"
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(v) => {
setQuery(db.tags.sanitize(v));
}}
onFocusInput={() => {
setFocus(true);
}}
onBlurInput={() => {
setFocus(false);
}}
onSubmit={onSubmit}
placeholder="Search or add a tag"
/>
<ScrollView
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
{query && query !== tags[0]?.title ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="selected"
>
<Heading size={SIZE.sm} color={colors.selected.heading}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
</PressableButton>
) : null}
{!allTags || allTags.length === 0 ? (
<View
style={{
width: "100%",
height: 200,
justifyContent: "center",
alignItems: "center"
}}
>
<Heading size={50} color={colors.secondary.heading}>
#
</Heading>
<Paragraph
textBreakStrategy="balanced"
color={colors.secondary.paragraph}
>
You do not have any tags.
</Paragraph>
</View>
) : null}
{tags.map((item) => (
<TagItem
key={item.title}
tag={item}
notes={notes}
setNotes={setNotes}
/>
))}
</ScrollView>
</View>
);
};
ManageTagsSheet.present = (notes) => {
presentSheet({
component: (ref) => {
return <ManageTagsSheet actionSheetRef={ref} notes={notes} />;
}
});
};
export default ManageTagsSheet;
const TagItem = ({ tag, notes, setNotes }) => {
const { colors } = useThemeColors();
const someNotesTagged = notes.some(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const allNotesTagged = notes.every(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const onPress = async () => {
for (let note of notes) {
try {
if (someNotesTagged) {
await db.notes
.note(note.id)
.untag(note.tags[note.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
}
} catch (e) {
console.error(e);
}
}
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
};
return (
<PressableButton
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "flex-start",
height: 40
}}
onPress={onPress}
type="gray"
>
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
someNotesTagged || allNotesTagged
? colors.selected.icon
: colors.primary.icon
}
testID={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<Paragraph size={SIZE.sm}>{"#" + tag.alias}</Paragraph>
</PressableButton>
);
};

View File

@@ -1,416 +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 { VirtualizedGrouping } from "@notesnook/core";
import { Tags } from "@notesnook/core/dist/collections/tags";
import { Tag } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from "react";
import { TextInput, View, useWindowDimensions } from "react-native";
import { ActionSheetRef, FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import {
ItemSelection,
createItemSelectionStore
} from "../../../stores/item-selection-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useTagStore } from "../../../stores/use-tag-store";
import { SIZE } from "../../../utils/size";
import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
.to(
{
type: "note",
ids: items
},
"tag"
)
.get();
const initialSelectionState: ItemSelection = {};
const tagId = [...new Set(relations.map((relation) => relation.fromId))];
for (const id of tagId) {
const all = items.every((noteId) => {
return (
relations.findIndex(
(relation) => relation.fromId === id && relation.toId === noteId
) > -1
);
});
if (all) {
initialSelectionState[id] = "selected";
} else {
initialSelectionState[id] = "intermediate";
}
}
return initialSelectionState;
}
const useTagItemSelection = createItemSelectionStore(true);
const ManageTagsSheet = (props: {
ids?: string[];
actionSheetRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const ids = useMemo(() => props.ids || [], [props.ids]);
const [tags, setTags] = useState<VirtualizedGrouping<Tag>>();
const [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null);
const [focus, setFocus] = useState(false);
const [queryExists, setQueryExists] = useState(false);
const dimensions = useWindowDimensions();
const refreshSelection = useCallback(() => {
updateInitialSelectionState(ids).then((selection) => {
useTagItemSelection.setState({
initialState: selection,
selection: { ...selection }
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ids, tags]);
const refreshTags = useCallback(() => {
if (query && query.trim() !== "") {
db.lookup
.tags(query)
.sorted()
.then((items) => {
setTags(items);
console.log("searched tags");
});
} else {
db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => {
console.log("items loaded tags");
setTags(items);
});
}
}, [query]);
useEffect(() => {
refreshTags();
}, [refreshTags, query]);
useEffect(() => {
refreshSelection();
}, [refreshSelection]);
const checkQueryExists = (query: string) => {
db.tags.all
.find((v) => v.and([v(`title`, "==", query)]))
.then((exists) => setQueryExists(!!exists));
};
const onSubmit = async () => {
if (!query || query === "" || query.trimStart().length == 0) {
ToastManager.show({
heading: "Tag field is empty",
type: "error",
context: "local"
});
return;
}
const tag = query;
inputRef.current?.setNativeProps({
text: ""
});
try {
const exists = await db.tags.all.find((v) =>
v.and([v(`title`, "==", tag)])
);
const id = exists
? exists?.id
: await db.tags.add({
title: tag
});
if (id) {
for (const noteId of ids) {
await db.relations.add(
{
id: id,
type: "tag"
},
{
id: noteId,
type: "note"
}
);
}
}
useRelationStore.getState().update();
useTagStore.getState().setTags();
setQuery(undefined);
} catch (e) {
ToastManager.show({
heading: "Cannot add tag",
type: "error",
message: (e as Error).message,
context: "local"
});
}
Navigation.queueRoutesForUpdate();
};
const onPress = useCallback(
async (id: string) => {
for (const noteId of ids) {
try {
if (!id) return;
const isSelected =
useTagItemSelection.getState().initialState[id] === "selected";
if (isSelected) {
await db.relations.unlink(
{
id: id,
type: "tag"
},
{
id: noteId,
type: "note"
}
);
} else {
await db.relations.add(
{
id: id,
type: "tag"
},
{
id: noteId,
type: "note"
}
);
}
} catch (e) {
console.error(e);
}
}
useTagStore.getState().setTags();
useRelationStore.getState().update();
refreshTags();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
refreshSelection();
},
[ids, refreshSelection, refreshTags]
);
const renderTag = useCallback(
({ item }: { item: string; index: number }) => (
<TagItem
key={item as string}
tags={tags as VirtualizedGrouping<Tag>}
id={item as string}
onPress={onPress}
/>
),
[onPress, tags]
);
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85
}}
>
<Input
button={{
icon: "magnify",
color: colors.primary.accent,
size: SIZE.lg,
onPress: () => {}
}}
testID="tag-input"
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(v) => {
setQuery(Tags.sanitize(v));
checkQueryExists(Tags.sanitize(v));
}}
onFocusInput={() => {
setFocus(true);
}}
onBlurInput={() => {
setFocus(false);
}}
onSubmit={() => {
onSubmit();
}}
placeholder="Search or add a tag"
/>
{query && !queryExists ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="selected"
>
<Heading size={SIZE.sm} color={colors.selected.heading}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
</PressableButton>
) : null}
<FlatList
data={tags?.ids?.filter((id) => typeof id === "string") as string[]}
style={{
width: "100%"
}}
keyboardShouldPersistTaps
keyboardDismissMode="interactive"
keyExtractor={(item) => item as string}
renderItem={renderTag}
ListEmptyComponent={
<View
style={{
width: "100%",
height: 200,
justifyContent: "center",
alignItems: "center"
}}
>
<Heading size={50} color={colors.secondary.heading}>
#
</Heading>
<Paragraph
textBreakStrategy="balanced"
color={colors.secondary.paragraph}
>
You do not have any tags.
</Paragraph>
</View>
}
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</View>
);
};
ManageTagsSheet.present = (ids?: string[]) => {
presentSheet({
component: (ref) => {
return <ManageTagsSheet actionSheetRef={ref} ids={ids} />;
}
});
};
export default ManageTagsSheet;
const TagItem = ({
id,
tags,
onPress
}: {
id: string;
tags: VirtualizedGrouping<Tag>;
onPress: (id: string) => void;
}) => {
const { colors } = useThemeColors();
const [tag] = useDBItem(id, "tag", tags);
const selection = useTagItemSelection((state) => state.selection[id]);
return (
<PressableButton
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "flex-start",
height: 40
}}
onPress={() => onPress(id)}
type="gray"
>
{!tag ? null : (
<Icon
size={22}
onPress={() => onPress(id)}
color={
selection === "selected" || selection === "intermediate"
? colors.selected.icon
: colors.primary.icon
}
style={{
marginRight: 6
}}
testID={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
)}
{tag ? (
<Paragraph size={SIZE.sm}>{"#" + tag?.title}</Paragraph>
) : (
<View
style={{
width: 200,
height: 30,
// backgroundColor: colors.secondary.background,
borderRadius: 5
}}
/>
)}
</PressableButton>
);
};

View File

@@ -26,7 +26,7 @@ import BackupService from "../../../services/backup";
import {
eSendEvent,
presentSheet,
ToastManager
ToastEvent
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useThemeColors } from "@notesnook/theme";
@@ -94,7 +94,7 @@ export default function Migrate() {
await sleep(1000);
const backupSaved = await BackupService.run(false, "local");
if (!backupSaved) {
ToastManager.show({
ToastEvent.show({
heading: "Migration failed",
message: "You must download a backup of your data before migrating.",
context: "local"

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