Compare commits

...

116 Commits

Author SHA1 Message Date
Ammar Ahmed
d4d7eac240 mobile: fix loading indicator causes cancel button to jump 2026-05-08 10:44:05 +05:00
Ammar Ahmed
e7fb246c25 mobile: show errors under input in rename dialog 2026-05-08 10:44:04 +05:00
Ammar Ahmed
03defb3a48 mobile: fix defaultValue is used if input is empty in dialog input 2026-05-08 10:42:25 +05:00
Ammar Ahmed
73c44d476b mobile: fix rename attachment does not work on iOS 2026-05-08 10:41:53 +05:00
Abdullah Atta
0aaabfab62 web: exclude hidden settings from setting search (#9772)
* web: exclude hidden settings from search

* web: add support for searching setting section groups
2026-05-08 08:53:00 +05:00
Ammar Ahmed
1312ce7d50 Merge pull request #9818 from streetwriters/fix/284
Trim title before checking if title is valid in add reminder screen
2026-05-07 12:58:46 +05:00
Ammar Ahmed
d16cd510b3 mobile: cleanup 2026-05-07 12:58:05 +05:00
Ammar Ahmed
af08ed5174 mobile: show title is required error under the input 2026-05-07 12:58:05 +05:00
Ammar Ahmed
365e1d7029 mobile: trim title before checking if title is valid 2026-05-07 12:57:25 +05:00
Ammar Ahmed
80b75fc28c Merge pull request #9821 from streetwriters/fix/287
Show confirm dialog before deleting attachment in attachment manager
2026-05-07 12:54:10 +05:00
Ammar Ahmed
6de4b4ac2d mobile: show toast message after attachment deleted 2026-05-07 12:53:44 +05:00
Ammar Ahmed
39bfff6b14 mobile: fix base dialog background is always transparent 2026-05-07 12:53:44 +05:00
Ammar Ahmed
e381b62e5c mobile: show confirm dialog before deleting attachment in attachment manager 2026-05-07 12:53:43 +05:00
Ammar Ahmed
ffef5affce Merge pull request #9822 from streetwriters/fix/309
mobile: fix handling invalid gift code input
2026-05-07 12:28:27 +05:00
Ammar Ahmed
780276b30e mobile: fix pro user walk through makes app get stuck 2026-05-07 11:12:49 +05:00
Ammar Ahmed
276239dd2b mobile: add form validation support in dialog inputs 2026-05-07 08:49:21 +05:00
Ammar Ahmed
5ef3bdcad4 mobile: fix handling invalid gift code 2026-05-07 08:03:45 +05:00
Ammar Ahmed
63076d37f5 Merge pull request #9808 from streetwriters/fix/270
Improve error handling when signing up on mobile
2026-05-06 13:21:56 +05:00
Ammar Ahmed
724ad58824 mobile: show errors under the email input 2026-05-06 11:28:58 +05:00
Ammar Ahmed
7fcdb3b55d mobile: skip showing toast message 2026-05-06 08:16:35 +05:00
Ammar Ahmed
47ba40d7cd mobile: fix ts errors in build 2026-05-05 21:00:17 +05:00
Ammar Ahmed
0801e73242 mobile: fix login and signup flows 2026-05-05 19:06:42 +05:00
Ammar Ahmed
c82487983c Merge pull request #9806 from streetwriters/fix/268
Fix No error message shown for missing or incorrect 2FA code on mobile
2026-05-05 14:13:20 +05:00
Ammar Ahmed
2ad39048da mobile: fix form validation in login, signup and forgot password 2026-05-05 13:47:37 +05:00
Ammar Ahmed
3421627f31 mobile: show error if passwords do not match or fields empty 2026-05-05 10:34:44 +05:00
Ammar Ahmed
cf6262e995 mobile: fix showing 2fa error when field is empty and show error below input field 2026-05-05 10:32:27 +05:00
Ammar Ahmed
8a325bf7c0 mobile: cleanup 2026-05-05 10:11:19 +05:00
Ammar Ahmed
883667b411 mobile: show error message on failed two-factor auth 2026-05-05 10:11:19 +05:00
Ammar Ahmed
06b88e74ae Merge pull request #9807 from streetwriters/fix/269
Remove password length check on signup on mobile
2026-05-05 10:10:03 +05:00
Ammar Ahmed
7800a25775 Merge pull request #9803 from streetwriters/mobile/release-3.3.22
mobile: release v3.3.22
2026-05-05 07:46:27 +05:00
Ammar Ahmed
01d0de3909 mobile: remove password length check on signup 2026-05-04 13:42:05 +05:00
Ammar Ahmed
d61f9472c8 mobile: fix search in archived notes returns no results 2026-05-04 13:08:05 +05:00
Ammar Ahmed
2465337148 mobile: fix show results count in search 2026-05-04 13:05:54 +05:00
01zulfi
af4638ac50 Merge pull request #9802 from streetwriters/release/3.3.17
web: bump version to 3.3.17
2026-05-04 12:33:11 +05:00
Ammar Ahmed
5e6be09aab mobile: release v3.3.22 2026-05-04 10:41:36 +05:00
Ammar Ahmed
f626953e0a Merge pull request #9781 from streetwriters/fix/archive-note-time
Fix Archive screen does not show correct date/time on note based on Sorting Type
2026-05-04 10:30:02 +05:00
01zulfi
4d4bce301d web: bump version to 3.3.17
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-04 10:21:03 +05:00
Ammar Ahmed
d45b18afd6 Merge pull request #9783 from streetwriters/feat/compact-view-search
mobile: add compact-view support in search results
2026-05-04 09:51:01 +05:00
Ammar Ahmed
4f212453a6 mobile: fix sort options not working in trash 2026-05-02 11:04:11 +05:00
Ammar Ahmed
2c1b9081bf mobile: fix group type usage
Explicitly pass group type to list and sort sheet.
2026-05-02 10:49:08 +05:00
01zulfi
7c5dc1615c web: remove testimonial with broken link (#9789)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-02 08:48:34 +05:00
Ammar Ahmed
3fb07369bb mobile: fix navigating from Monographs to Search screen 2026-05-01 10:09:50 +05:00
Ammar Ahmed
9b480950da mobile: fix typo in properties key 2026-05-01 09:59:44 +05:00
01zulfi
9f20d243fa web: disable expand sidebar button on small screens (#9769)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-01 09:07:28 +05:00
01zulfi
13e6573342 web: show copied text when monograph link is copied (#9780)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-30 17:31:08 +05:00
Ammar Ahmed
f37869cb5a mobile: add compact-view support in search results 2026-04-30 13:55:19 +05:00
Ammar Ahmed
46d802548b mobile: fix Archive screen does not show correct date/time on note based on sorting 2026-04-30 13:27:52 +05:00
Ammar Ahmed
2bf42c207d Merge pull request #9776 from streetwriters/mobile/release-3.3.21
mobile: v3.3.21 release
2026-04-30 10:05:04 +05:00
Ammar Ahmed
65e42e5d8b mobile: prepare for v3.3.21 release 2026-04-29 16:36:09 +05:00
Ammar Ahmed
c85007f0dd Merge pull request #9705 from streetwriters/fix/screenguard
mobile: fix screenguard on mobile & privacy mode
2026-04-29 16:20:06 +05:00
Ammar Ahmed
36058145b5 mobile: disable privacy mode on user logout 2026-04-29 16:19:18 +05:00
Ammar Ahmed
a376a6b497 mobile: fix screenguard on mobile & privacy mode 2026-04-29 16:19:18 +05:00
Ammar Ahmed
cd62d51e45 Merge pull request #9752 from streetwriters/mobile/fix-locked-note-merge-conflict
mobile: fix saving incoming note in merge conflict dialog does not work
2026-04-29 16:16:38 +05:00
Abdullah Atta
3f6fcbacbc global: fix dialog on successful bug report (#9750)
* core: handle different issue report responses

* web: better handling for issue report responses

* mobile: update issue report dialog to show correct info of where issue is reported

* mobile: close sheet on clicking "done"

* web: do not include email in issue success dialog

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-04-29 14:19:29 +05:00
01zulfi
2a2bf67fea web: use expire verbiage in subscription subtitle when provider is gift card (#9759)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-29 08:45:52 +05:00
01zulfi
b8fca5cd98 Merge pull request #9765 from streetwriters/release/3.3.16
web: bump version to 3.3.16
2026-04-28 15:48:34 +05:00
01zulfi
4d87155fdd web: bump version to 3.3.16
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-28 10:17:27 +05:00
01zulfi
ed38bb6299 editor: fix crash in backspace handler (#9740)
* editor: fix crash in backspace handler
* replace nextNode with .nodeAfter

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: fix position of node when deleting nextNode

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-28 09:08:31 +05:00
Abdullah Atta
7c9e3e79df web: small refactor to make the build pass (#9761) 2026-04-27 12:54:44 +05:00
Chloe Oletto
43b90070a9 core: Limit note subtitle/headline length. (#9732)
Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-04-23 22:27:33 +05:00
Ammar Ahmed
04f6e1068b mobile: fix saving incoming note in merge conflict dialog does not work 2026-04-23 10:43:07 +05:00
01zulfi
9bea4adcf9 web: show titlebar on auth pages (#9093)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-22 13:51:21 +05:00
Chloe Oletto
a5790de23c desktop: Stop urgent reminder causes segfault & app freeze on Linux. (#9737)
* desktop: Stop urgent reminder causes segfault & app freeze on Linux.

Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>

* desktop: Linux segfault
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Apply suggestion from @thecodrr

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-04-22 11:56:47 +05:00
Abdullah Atta
eb4edeea8f web: fix crash on non-paddle related iframe event (#9747)
Stack trace:
```
undefined is not an object (evaluating 'w.data')
E@https://app.notesnook.com/assets/paddle-Z43isEce-h-M.js:23:45428
E@https://app.notesnook.com/assets/paddle-Z43isEce-h-M.js:23:45846
```
2026-04-22 09:46:29 +05:00
01zulfi
e3ccd504da ui: fix submenu cutting off when there are a lot of lazy items (#9739)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-22 09:11:28 +05:00
Ammar Ahmed
4627d7d5df Merge pull request #9736 from streetwriters/mobile/release-3.3.20
mobile: release 3.3.20
2026-04-22 08:46:43 +05:00
01zulfi
f216c1b65d web: fix scrollbar blocking date in compact note list item (#9719)
* web: fix scrollbar blocking date in compact note list item
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: adjust notes list scrollbar width, compact item padding, & group header padding
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-21 11:08:30 +05:00
Abdullah Atta
4692ba7183 desktop: use newer appimage bundler (#9744)
this should fix libfuse and libz errors
2026-04-21 10:55:52 +05:00
Ammar Ahmed
543825918c mobile: fix useEffect deps in use-feature-manager hook 2026-04-21 08:46:19 +05:00
Ammar Ahmed
9f995b861b mobile: fix toast position incorrect with keyboard opened 2026-04-21 08:45:45 +05:00
Ammar Ahmed
9963a6c655 mobile: delete vault action keeps loading when entering incorrect password 2026-04-21 08:45:29 +05:00
Ammar Ahmed
1696ec5c72 mobile: fix unable to switch between yearly/monthly plans 2026-04-21 08:14:38 +05:00
Ammar Ahmed
7c95faad21 mobile: fix clear trash option is disabled on login 2026-04-20 15:43:39 +05:00
Ammar Ahmed
ca80441c14 mobile: fix trash count does not update on calling clear trash 2026-04-20 15:30:26 +05:00
01zulfi
9c497fd2bb Merge pull request #9724 from streetwriters/release/3.3.15
web: bump version to 3.3.15
2026-04-20 12:21:46 +05:00
Ammar Ahmed
ef5e5e5b4c mobile: release 3.3.20 2026-04-20 12:03:12 +05:00
Abdullah Atta
ede900fdbd ci: add linux arm64 preview builds (#9735) 2026-04-20 11:56:10 +05:00
01zulfi
1758f03f93 intl: regen locale files (#9734)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-20 11:39:13 +05:00
01zulfi
f18db748e0 web: bump version to 3.3.15
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-20 10:38:18 +05:00
01zulfi
2fba2af126 web: fix pdf viewer's search dialog unreadable UI in dark mode (#9720)
* web: fix pdf viewer's search dialog unreadable UI in dark mode
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: fix pdf search dialog ui

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-18 10:46:09 +05:00
Ammar Ahmed
1ef3d712e1 mobile: fix parent notebook can be moved into it's child notebooks. (#9691)
* mobile: fix parent notebook can be moved into it's child notebooks.

1. Ensure notebook that is being moved and all it's children are not visible in move-notebook dialog
2. Ensure child cannot be moved into it's parent again
3. Ensure child can be moved into it's sibling notebooks

* mobile: add e2e tests

* mobile: fix toast position
2026-04-18 09:28:39 +05:00
Ammar Ahmed
405adff8f4 mobile: fix linting not working in vscode and fix lint errors (#9710) 2026-04-18 08:52:39 +05:00
Ammar Ahmed
9a21ca1cf4 mobile: fix discount value shows as undefined on iOS when buying plan (#9711)
* mobile: fix discount value is undefined

* mobile: fix discount calculation and price formatting

Respect spacing when writing price in a currency.
Fix incorrect discount calculation when comparing products of a plan based on int values, use parseFloat to parse correct values for comparision

* mobile: fix ui inconsistencies

* mobile: conditionally include trial plan conditions based on GitHub release status
2026-04-18 08:40:50 +05:00
01zulfi
49252e66ce web: fix RCE via stored XSS in note html export (#9709)
* sandbox the export iframe
* escape fields when building html template

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-17 18:10:06 +05:00
Abdullah Atta
4ba2e900c5 desktop: disable autoInstallOnAppQuit (#9726) 2026-04-17 17:07:21 +05:00
01zulfi
b6714b559e web: make dates text reactive to date format setting (#9707)
the following dates text are now reactive:
* dates in note list item
* date in notebook header
* dates in properties panel
* date in reminder list item

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-16 12:42:10 +05:00
Ammar Ahmed
a6c1757cf6 mobile: fix list-items do not show correct notes count in side menu (#9697)
* mobile: fix list-items do not show correct notes count in side menu

* mobile: fix notebook note count not updated on deleting note

* mobile: fix monograph count does not update on unpublishing note in side-menu

* mobile: fix notebook and tag shortcut count does not update in sidemenu
2026-04-15 21:43:45 +05:00
01zulfi
3d847ea00c web: fix time field not aligning in reminder dialog (#9202)
* also capitalize month string

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-15 12:49:22 +05:00
Abdullah Atta
63453942ee Merge pull request #9686 from streetwriters/web/clear-trash
web: add clear trash option in trash nav item context menu
2026-04-15 12:48:50 +05:00
Ammar Ahmed
cb5a7904d3 mobile: add clear trash option in Trash properties
mobile: show clear trash success message

mobile: add e2e tests

mobile: fix clear trash option remains disabled after moving item to trash after login

mobile: Disable "Move to trash" for deleted notes.
2026-04-15 12:48:24 +05:00
01zulfi
f2c088fb18 web: add clear trash option in trash nav item context menu
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: disable clear trash if empty && update confirm message
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-15 12:48:10 +05:00
01zulfi
4e2fd9dde6 web: show error text on settings number input's out of range (#9171)
* web: show error text on settings number input's out of range
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: minor refactor and fixes

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-15 08:04:58 +05:00
Ammar Ahmed
92c1ca7b6f mobile: fix configure toolbar do not render properly on adding/removing groups (#9699)
* editor: make editor tools object a function

Required for string localization to work properly

* mobile: fix tools and groups do not re-render properly on changing preset or adding tools

* mobile: fix empty toast message shows on removing tools from group
2026-04-14 22:46:41 +05:00
01zulfi
5fcad35cdc Merge pull request #9712 from streetwriters/release/3.3.14
web: bump version to 3.3.14
2026-04-14 15:14:39 +05:00
01zulfi
2b11fcea41 web: bump version to 3.3.14
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-14 13:14:49 +05:00
Chloe Oletto
3f0c24d27b core: duplicating a note should copy all the flags (#9250)
Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-04-14 08:23:50 +05:00
01zulfi
71088fca99 editor: handle crash on inserting incorrect syntax in math nodes (#9690)
* editor: handle crash on inserting incorrect syntax in math nodes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: fix 'math error' text shown on newly empty math node
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-13 20:35:57 +05:00
01zulfi
d6f7f0c0be editor: fix crash when app is unlocked after a relock (#9698)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-13 20:35:16 +05:00
01zulfi
4044b68701 webclipper: fix notebooks getting unselected on tag select (#9706)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-13 14:55:20 +05:00
Abdullah Atta
610ced00af global: whitelist postinstall packages on bootstrap (#9629)
* global: enable npm ignore scripts by default

* config: disable ignore-scripts in .npmrc

* mobile: update lockfiles

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-04-13 14:27:32 +05:00
Abdullah Atta
f9fecda1f1 ci: enable CSC_FOR_PULL_REQUEST for desktop preview builds (#9695) 2026-04-10 14:45:24 +05:00
Ammar Ahmed
74921ae1b3 mobile: release 3.3.19 (#9683)
* mobile: release 3.3.19

* mobile: fix ios production xcconfig
2026-04-08 09:37:56 +05:00
01zulfi
40f2c88ead Merge pull request #9682 from streetwriters/release/3.3.13
web: bump version to 3.3.13
2026-04-07 15:49:17 +05:00
01zulfi
7fe410ae89 web: bump version to 3.3.13
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-07 14:00:37 +05:00
Ammar Ahmed
4cbdd085d6 mobile: fix editor stops updating notes after first two notes edited (#9681) 2026-04-07 13:18:50 +05:00
01zulfi
24556dab83 web: show better error message on invalid recovery key (#9680)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-07 13:00:02 +05:00
Abdullah Atta
dd8116eba8 Merge pull request #9677 from streetwriters/fix/table-cell-selection-mobile
Fix table cell selection on mobile
2026-04-06 23:55:18 +05:00
Abdullah Atta
ae82a8c0a0 editor: only allow resizing column from resize handle 2026-04-06 23:49:04 +05:00
Chloe Oletto
af4dc5e9ea web: fix all tabs closed on sync when any note is deleted from another device (#9252)
* web: All tabs closed when any note is deleted.

Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>

* web: remove redundant noteID check, fix check to ensure locked notes are reopened.

Signed-off By: Chloe Oletto <NeedsChloesure@riseup.net>
2026-04-06 23:02:12 +05:00
Abdullah Atta
71b03a90dd editor: disable scroll during column resize 2026-04-06 21:01:25 +05:00
Abdullah Atta
728e50cd13 editor: fix awkward selection behaviour in table cells
this includes fixes to multiple UX bugs:

1. scroll now stops when user starts cell selection
2. moving cell selection to either edge auto scrolls the table
3. text selection does not trigger cell selection
2026-04-06 21:00:23 +05:00
01zulfi
c8e8935b34 web: add back button in recovery forms (#9676)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-06 16:27:33 +05:00
01zulfi
21f4b906d7 web: fix html entities shown in match result (#9645)
* web: fix non-breaking space shown as encoded in match result
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: decode matches in db.lookup.notesWithHighlighting
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: decode entities at parse time

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-06 14:20:10 +05:00
01zulfi
d49d42b736 common: guard against undefined backStack/forwardStack in TabSessionHistory (#9674)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-06 12:42:00 +05:00
SahilAslam001
267fe4e453 mobile: remove restoring app state on app launch #6294 (#9586)
Signed-off-by: Sahil Aslam <sahil.aslam011@gmail.com>
2026-04-06 11:20:30 +05:00
01zulfi
413ca61ef5 core: $headline$ format should get 60 chars from first p or h tag (#9658)
* core: note title headline format should get content from first p or h tag
* also decrease character limit to 60

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* Apply suggestion from @thecodrr

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-06 09:20:21 +05:00
216 changed files with 5713 additions and 7592 deletions

View File

@@ -101,12 +101,12 @@ jobs:
- name: Build dmg
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
CSC_FOR_PULL_REQUEST: true
run: |
npm run tx @notesnook/desktop:release
cd apps/desktop
@@ -152,8 +152,6 @@ jobs:
run: npm run tx @notesnook/desktop:release
- name: Build AppImage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
working-directory: ./apps/desktop
@@ -164,6 +162,49 @@ jobs:
name: linux-x64-build
path: apps/desktop/output/*.AppImage
build-linux-arm64:
name: Build for Linux arm64
needs: build
runs-on: ubuntu-22.04-arm
outputs:
linux-arm64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build AppImage
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
working-directory: ./apps/desktop
- name: Upload AppImage artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: linux-arm64-build
path: apps/desktop/output/*.AppImage
build-windows:
name: Build for Windows
needs: build
@@ -215,7 +256,8 @@ jobs:
post-pr-comment:
name: Post PR comment with preview URLs
needs: [build, build-macos, build-linux-x64, build-windows]
needs:
[build, build-macos, build-linux-x64, build-linux-arm64, build-windows]
runs-on: ubuntu-latest
steps:
- name: Post or update PR comment
@@ -223,6 +265,7 @@ jobs:
env:
macos_artifact_url: ${{ needs.build-macos.outputs.macos-artifact-url }}
linux_x64_artifact_url: ${{ needs.build-linux-x64.outputs.linux-x64-artifact-url }}
linux_arm64_artifact_url: ${{ needs.build-linux-arm64.outputs.linux-arm64-artifact-url }}
windows_artifact_url: ${{ needs.build-windows.outputs.windows-artifact-url }}
with:
script: |
@@ -231,6 +274,7 @@ jobs:
const previewUrl = [
{ platform: 'macOS', url: process.env.macos_artifact_url },
{ platform: 'Linux x64', url: process.env.linux_x64_artifact_url },
{ platform: 'Linux arm64', url: process.env.linux_arm64_artifact_url },
{ platform: 'Windows x64', url: process.env.windows_artifact_url },
].filter(u => u.url).map(u => `- [${u.platform}](${u.url})`).join('\n');
const body = `${marker}\n**Desktop Previews**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
min-release-age=7 # days
# ignore-scripts=true

View File

@@ -198,6 +198,9 @@ module.exports = {
}
}
},
toolsets: {
appimage: "1.0.2"
},
snap: {
autoStart: false,
confinement: "strict",

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.12",
"version": "3.3.17",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
@@ -48,7 +48,7 @@
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^37.0.0",
"electron-builder": "^26.0.12",
"electron-builder": "^26.8.1",
"esbuild": "0.21.5",
"node-abi": "^4.5.0",
"node-gyp-build": "^4.8.4",

View File

@@ -27,6 +27,7 @@ import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = path.join(__dirname, "..");
const RUNNING_PROCESSES = [];
const RESTARTABLE_PROCESSES = [];
let lastBundleHash = null;
@@ -36,7 +37,7 @@ const ENV = {
FORCE_COLOR: "false",
COLOR: "0"
};
process.chdir(path.join(__dirname, ".."));
process.chdir(root);
await onChange(true);
@@ -55,13 +56,16 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npm rebuild electron --verbose --foreground-scripts");
await exec(
"npm run postinstall --verbose",
path.join(root, "node_modules", "electron")
);
await exec("yarn electron-builder install-app-deps");
await exec("yarn electron-builder install-app-deps", root);
}
await exec(`yarn run bundle`);
await exec(`yarn run build`);
await exec(`yarn run bundle`, root);
await exec(`yarn run build`, root);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
@@ -111,7 +115,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
async function exec(cmd, cwd) {
try {
console.log(">", cmd);
console.log(">", cmd, cwd);
return execSync(cmd, {
env: ENV,

View File

@@ -178,7 +178,9 @@ export const osIntegrationRouter = t.router({
})
});
notification.show();
if (input.urgency === "critical") {
if (input.urgency === "critical" && process.platform !== "linux") {
// due to an Electron bug in versions below 40.x, shell.beep() causes a segfault on Linux.
// TODO: Remove when migrating to Electron 40.x or newer.
shell.beep();
}

View File

@@ -37,7 +37,11 @@ async function configureAutoUpdater() {
config.releaseTrack === "stable" &&
autoUpdater.currentVersion.prerelease.length > 0;
autoUpdater.allowPrerelease = false;
autoUpdater.autoInstallOnAppQuit = true;
// Do NOT auto-install on quit. On Windows, if the system shuts down while
// the NSIS installer is running, it first removes all old files and then
// gets killed before copying new ones — leaving an empty install directory.
// Updates should only be installed when the user explicitly triggers it.
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.disableWebInstaller = true;
}

View File

@@ -140,7 +140,7 @@ android {
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3098
versionCode 3103
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')

View File

@@ -199,11 +199,7 @@
</intent-filter>
</activity>
<service
android:name=".OnClearFromRecentService"
android:enabled="true"
android:exported="true"
android:stopWithTask="false" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
<service

View File

@@ -28,10 +28,6 @@ public class MainActivity extends ReactActivity {
WebView.setWebContentsDebuggingEnabled(true);
}
try {
startService(new Intent(getBaseContext(), OnClearFromRecentService.class));
} catch (Exception ignored) {}
}
/**

View File

@@ -1,36 +0,0 @@
package com.streetwriters.notesnook;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
import android.util.Log;
public class OnClearFromRecentService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
try {
SharedPreferences appStateDetails = getApplicationContext().getSharedPreferences("appStateDetails", MODE_PRIVATE);
SharedPreferences.Editor edit = appStateDetails.edit();
edit.remove("appState");
edit.apply();
stopSelf();
} catch (UnsatisfiedLinkError | Exception e) {
}
}
}

View File

@@ -93,21 +93,6 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void setAppState(final String appState) {
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = appStateDetails.edit();
edit.putString("appState", appState);
edit.apply();
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String getAppState() {
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
String appStateValue = appStateDetails.getString("appState", "");
return appStateValue.isEmpty() ? null : appStateValue;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public int getWidgetId() {
return NotePreviewConfigureActivity.appWidgetId;

View File

@@ -1,3 +1,3 @@
- Minor bug fixes and performance improvements
- Bug fixes and improvements
Thank you for using Notesnook!
Thank you for using Notesnook!

View File

@@ -23,7 +23,7 @@ import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import React, { PropsWithChildren, useEffect } from "react";
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -66,6 +66,9 @@ const App = (props: { configureMode: "note-preview" }) => {
useEffect(() => {
SettingsService.onFirstLaunch();
changeSystemBarColors();
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
setTimeout(async () => {
await Notifications.get();
if (SettingsService.get().notifNotes) {
@@ -106,8 +109,10 @@ let currTheme =
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: (props: any) => JSX.Element) => {
return function AppWithThemeProvider(props: any) {
export const withTheme = (
Element: (props: PropsWithChildren) => JSX.Element
) => {
return function AppWithThemeProvider(props: PropsWithChildren) {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,
state.darkTheme,

View File

@@ -25,7 +25,6 @@ import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
// Database key cipher is persisted across different user sessions hence it has
@@ -205,9 +204,8 @@ export async function getDatabaseKey(appLockPassword?: string) {
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
const userKeyCredentials =
await Keychain.getInternetCredentials("notesnook");
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(

View File

@@ -72,7 +72,7 @@ export async function setupDatabase(password?: string) {
({
compress: Gzip.deflate,
decompress: Gzip.inflate
} as ICompressor),
}) as ICompressor,
batchSize: 50,
sqliteOptions: {
dialect: (name) => ({

View File

@@ -109,8 +109,8 @@ class RNSqliteConnection implements DatabaseConnection {
query.kind === "SelectQueryNode"
? "query"
: query.kind === "RawNode"
? "raw"
: "exec";
? "raw"
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);

View File

@@ -112,8 +112,8 @@ export async function writeEncryptedBase64(
async function deleteLocalFile(filename: string) {
try {
await createCacheDir();
let path = cacheDir + `/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
const path = cacheDir + `/${filename}`;
const exists = await RNFetchBlob.fs.exists(path);
if (Platform.OS === "ios" && !exists) {
const iosAppGroup =
Platform.OS === "ios"
@@ -309,7 +309,9 @@ export async function deleteDCacheFiles() {
});
}
}
} catch (e) {}
} catch (e) {
/** Empty */
}
}
export async function getCachePathForFile(filename: string) {

View File

@@ -36,7 +36,7 @@ export const Announcement = () => {
state.announcements,
state.remove
]);
let announcement = announcements.length > 0 ? announcements[0] : null;
const announcement = announcements.length > 0 ? announcements[0] : null;
const selectionMode = useSelectionStore((state) => state.selectionMode);
return !announcement || selectionMode ? null : (

View File

@@ -32,7 +32,7 @@ import { Action } from "../../stores/use-message-store";
export const Cta = (props: BodyItemProps) => {
const { colors } = useThemeColors();
let buttons =
const buttons =
props.item.actions.filter((item) => allowedOnPlatform(item.platforms)) ||
[];

View File

@@ -53,7 +53,6 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { strings } from "@notesnook/intl";
import { AppFontSize } from "../../utils/size";
import { editorController } from "../../screens/editor/tiptap/utils";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
@@ -239,8 +238,8 @@ const AppLocked = () => {
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,

View File

@@ -22,7 +22,7 @@ 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 { View } from "react-native";
import { TextInput, View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { ScrollView } from "react-native-gesture-handler";
import { db } from "../../common/database";
@@ -46,7 +46,6 @@ import {
eOnLoadNote
} from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { openNote } from "../list-items/note/wrapper";
@@ -60,6 +59,7 @@ import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import Navigation from "../../services/navigation";
import { createFormRef, validators } from "../ui/input/form-input";
const Actions = ({
attachment,
@@ -154,50 +154,100 @@ const Actions = ({
{
name: strings.rename(),
onPress: () => {
presentDialog({
input: true,
title: strings.renameFile(),
defaultValue: attachment.filename,
positivePress: async (value) => {
if (value && value.length > 0) {
await db.attachments.add({
hash: attachment.hash,
filename: value
});
setFilename(value);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
}
return true;
},
positiveText: strings.rename()
});
close?.();
setTimeout(() => {
presentDialog({
title: strings.renameFile(),
form: {
formRef: createFormRef({
name: attachment.filename
}),
items: [
{
name: "name",
defaultValue: attachment.filename,
placeholder: strings.enterTitle(),
ref: React.createRef<TextInput | null>(),
validators: [validators.required(strings.nameIsRequired())]
}
],
onFormSubmit: async (form) => {
try {
const value = form.getValue("name");
await db.attachments.add({
hash: attachment.hash,
filename: value
});
setFilename(value);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
ToastManager.show({
message: `Attachment renamed to ${value}`,
type: "success"
});
return true;
} catch (e) {
form.setError("name", (e as Error).message);
return false;
}
}
},
positiveText: strings.rename()
});
}, 500);
},
icon: "form-textbox"
},
{
name: strings.delete(),
onPress: async () => {
const relations = await db.relations.to(attachment, "note").get();
await db.attachments.remove(attachment.hash, false);
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
relations
.map((relation) => relation.fromId)
.forEach(async (id) => {
useTabStore.getState().forEachNoteTab(id, async (tab) => {
const isFocused = useTabStore.getState().currentTab === tab.id;
if (isFocused) {
eSendEvent(eOnLoadNote, {
item: await db.notes.note(id),
forced: true
});
} else {
editorController.current.commands.setLoading(true, tab.id);
}
});
});
close?.();
setTimeout(() => {
presentDialog({
title: strings.deleteAttachment(),
paragraph: strings.deleteAttachmentConfirm(),
positiveText: strings.yes(),
negativeText: strings.no(),
positiveType: "errorShade",
positivePress: async () => {
try {
const relations = await db.relations
.to(attachment, "note")
.get();
await db.attachments.remove(attachment.hash, false);
ToastManager.show({
type: "success",
message: strings.attachmentDeleted()
});
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
relations
.map((relation) => relation.fromId)
.forEach(async (id) => {
useTabStore.getState().forEachNoteTab(id, async (tab) => {
const isFocused =
useTabStore.getState().currentTab === tab.id;
if (isFocused) {
eSendEvent(eOnLoadNote, {
item: await db.notes.note(id),
forced: true
});
} else {
editorController.current.commands.setLoading(
true,
tab.id
);
}
});
});
return true;
} catch (e) {
return false;
}
}
});
}, 500);
},
icon: "delete-outline"
}

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 { LegendList } from "@legendapp/list";
import {
Attachment,
FilteredSelector,

View File

@@ -27,7 +27,7 @@ import { useThemeColors } from "@notesnook/theme";
import DialogHeader from "../dialog/dialog-header";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -36,31 +36,32 @@ import { DefaultAppStyles } from "../../utils/styles";
export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
const { colors } = useThemeColors("sheet");
const email = useRef<string>(userEmail);
const formRef = useRef(
createFormRef({
email: userEmail || ""
})
);
const emailInputRef = useRef<TextInput>(null);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [sent, setSent] = useState(false);
const sendRecoveryEmail = async () => {
if (!email.current || error) {
ToastManager.show({
heading: strings.emailRequired(),
type: "error",
context: "local"
});
if (formRef.current.validateField("email")) {
return;
}
const values = formRef.current.getValues();
setLoading(true);
try {
let lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
const lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
if (
lastRecoveryEmailTime &&
Date.now() - lastRecoveryEmailTime < 60000 * 3
) {
throw new Error(strings.pleaseWaitBeforeSendEmail());
}
await db.user.recoverAccount(email.current.toLowerCase());
await db.user.recoverAccount(values.email.toLowerCase());
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
@@ -75,12 +76,7 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
setSent(true);
} catch (e) {
setLoading(false);
ToastManager.show({
heading: strings.recoveryEmailFailed(),
message: (e as Error).message,
type: "error",
context: "local"
});
formRef.current.setError("email", (e as Error).message);
}
};
@@ -126,22 +122,25 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
<DialogHeader title={strings.accountRecovery()} />
<Seperator />
<Input
<FormInput
name="email"
formRef={formRef}
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
defaultValue={email.current}
onErrorCheck={(e) => setError(e)}
loading={loading}
returnKeyLabel={strings.next()}
returnKeyType="next"
autoComplete="email"
validationType="email"
keyboardType="email-address"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
onSubmit={() => {}}
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
sendRecoveryEmail();
}}
/>
<Button

View File

@@ -22,14 +22,9 @@ import { useThemeColors } from "@notesnook/theme";
import { RouteProp, useRoute } from "@react-navigation/native";
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -42,8 +37,9 @@ import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import { Progress } from "../sheets/progress";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import Input from "../ui/input";
import FormInput, { validators } from "../ui/input/form-input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
@@ -68,14 +64,13 @@ export const Login = ({
const {
step,
setStep,
password,
email,
emailInputRef,
passwordInputRef,
loading,
setLoading,
setError,
login
login,
error,
formRef
} = useLogin(async () => {
eSendEvent(eUserLoggedIn, true);
await sleep(500);
@@ -98,6 +93,11 @@ export const Login = ({
});
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const onContinue = () => {
login();
};
useEffect(() => {
async () => {
setStep(LoginSteps.emailAuth);
@@ -195,35 +195,35 @@ export const Login = ({
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Input
<FormInput
name="email"
formRef={formRef}
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
keyboardType="email-address"
marginBottom={0}
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading}
onSubmit={() => {
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
if (step === LoginSteps.emailAuth) {
login();
onContinue();
} else {
passwordInputRef.current?.focus();
}
@@ -232,11 +232,10 @@ export const Login = ({
{step === LoginSteps.passwordAuth && (
<>
<Input
<FormInput
name="password"
formRef={formRef}
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
testID="input.password"
returnKeyLabel={strings.done()}
returnKeyType="done"
@@ -247,9 +246,9 @@ export const Login = ({
placeholder={strings.password()}
marginBottom={0}
editable={!loading}
defaultValue={password.current}
onSubmit={() => {
login();
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
onContinue();
}}
/>
<Button
@@ -260,9 +259,13 @@ export const Login = ({
paddingHorizontal: 0
}}
onPress={() => {
if (loading || !email.current) return;
if (loading) return;
presentSheet({
component: <ForgotPassword userEmail={email.current} />
component: (
<ForgotPassword
userEmail={formRef.current.getValue("email")}
/>
)
});
}}
textStyle={{
@@ -278,8 +281,7 @@ export const Login = ({
<Button
loading={loading}
onPress={() => {
if (loading) return;
login();
onContinue();
}}
style={{
width: "100%"
@@ -333,6 +335,25 @@ export const Login = ({
</Paragraph>
</TouchableOpacity>
) : null}
{error ? (
<Paragraph
numberOfLines={4}
onPress={() => {}}
color={colors.error.accent}
style={{
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
<AppIcon
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{error.message}
</Paragraph>
) : null}
</View>
</View>
</View>

View File

@@ -43,29 +43,26 @@ import SheetProvider from "../sheet-provider";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
import { strings } from "@notesnook/intl";
import { getObfuscatedEmail } from "../../utils/functions";
import { DefaultAppStyles } from "../../utils/styles";
import FormInput, { validators } from "../ui/input/form-input";
export const SessionExpired = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [focused, setFocused] = useState(false);
const { step, password, email, passwordInputRef, loading, login } = useLogin(
() => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
},
true
);
const { step, passwordInputRef, loading, login, formRef } = useLogin(() => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
}, true);
const logout = async () => {
try {
@@ -92,7 +89,7 @@ export const SessionExpired = () => {
const open = React.useCallback(async () => {
try {
let res = await db.tokenManager.getToken();
const res = await db.tokenManager.getToken();
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
@@ -102,9 +99,9 @@ export const SessionExpired = () => {
Sync.run("global", false, "full", async (complete) => {
if (!complete) {
let user = await db.user.getUser();
const user = await db.user.getUser();
if (!user) return;
email.current = user.email;
formRef.current.setValue("email", user.email);
setVisible(true);
setFocused(false);
return;
@@ -115,16 +112,16 @@ export const SessionExpired = () => {
setVisible(false);
});
} catch (e) {
let user = await db.user.getUser();
const user = await db.user.getUser();
if (!user) return;
email.current = user.email;
formRef.current.setValue("email", user.email);
setFocused(false);
setVisible(true);
useUserStore.setState({
disableAppLockRequests: true
});
}
}, [email]);
}, [formRef]);
useEffect(() => {
const sub = eSubscribeEvent(eLoginSessionExpired, open);
@@ -155,6 +152,7 @@ export const SessionExpired = () => {
enableSheetKeyboardHandler={true}
visible={true}
>
<Dialog context="two_factor_verify" />
<View
style={{
width: focused ? "100%" : "99.9%",
@@ -192,17 +190,17 @@ export const SessionExpired = () => {
}}
>
{strings.sessionExpiredDesc(
getObfuscatedEmail(email.current as string)
getObfuscatedEmail(formRef.current.getValue("email") as string)
)}
</Paragraph>
</View>
{step === LoginSteps.passwordAuth ? (
<Input
<FormInput
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
formRef={formRef}
name="password"
validators={[validators.required(strings.passwordRequired())]}
returnKeyLabel={strings.done()}
returnKeyType="next"
secureTextEntry
@@ -210,7 +208,7 @@ export const SessionExpired = () => {
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
onSubmit={() => {
onSubmitEditing={() => {
login();
}}
/>

View File

@@ -30,7 +30,6 @@ import {
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
import Navigation from "../../services/navigation";
import { useUserStore } from "../../stores/use-user-store";
@@ -39,13 +38,14 @@ import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Loading } from "../loading";
import { Button } from "../ui/button";
import Input from "../ui/input";
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AuthHeader } from "./header";
import { SignupContext } from "./signup-context";
import { RouteParams } from "../../stores/use-navigation-store";
import SettingsService from "../../services/settings";
import AppIcon from "../ui/AppIcon";
const SignupSteps = {
signup: 0,
@@ -62,13 +62,17 @@ export const Signup = ({
}) => {
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
const { colors } = useThemeColors();
const email = useRef<string>(undefined);
const formRef = useRef(
createFormRef({
email: "",
password: "",
confirmPassword: ""
})
);
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>(undefined);
const confirmPasswordInputRef = useRef<TextInput>(null);
const confirmPassword = useRef<string>(undefined);
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState<string>();
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const setLastSynced = useUserStore((state) => state.setLastSynced);
@@ -76,30 +80,18 @@ export const Signup = ({
const isTablet = width > 600;
const route = useRoute<RouteProp<RouteParams, "Auth">>();
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastManager.show({
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
type: "error",
context: "local"
});
return false;
}
return true;
};
const signup = async () => {
if (!validateInfo() || error) return;
setErrorMessage(undefined);
if (!formRef.current.validate()) return;
if (loading) return;
const values = formRef.current.getValues();
setLoading(true);
try {
setCurrentStep(SignupSteps.createAccount);
await db.user.signup(email.current!.toLowerCase(), password.current!);
let user = await db.user.getUser();
await db.user.signup(values.email.toLowerCase(), values.password);
const user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
clearMessage();
@@ -115,12 +107,14 @@ export const Signup = ({
} catch (e) {
setCurrentStep(SignupSteps.signup);
setLoading(false);
ToastManager.show({
heading: strings.signupFailed(),
message: (e as Error).message,
type: "error",
context: "local"
});
if (
(e as Error).message === "Unable to create an account on this email."
) {
formRef.current.setError("email", (e as Error).message);
} else {
setErrorMessage((e as Error).message);
}
return false;
}
};
@@ -215,60 +209,55 @@ export const Signup = ({
alignSelf: "center"
}}
>
<Input
<FormInput
name="email"
formRef={formRef}
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
defaultValue={email.current}
loading={loading}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
keyboardType="email-address"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
blurOnSubmit={false}
onSubmit={() => {
if (!email.current) return;
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
/>
<Input
<FormInput
name="password"
formRef={formRef}
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
defaultValue={password.current}
loading={loading}
testID="input.password"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
blurOnSubmit={false}
validationType="password"
autoCorrect={false}
placeholder={strings.password()}
onSubmit={() => {
if (!password.current) return;
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
confirmPasswordInputRef.current?.focus();
}}
/>
<Input
<FormInput
name="confirmPassword"
formRef={formRef}
fwdRef={confirmPasswordInputRef}
onChangeText={(value) => {
confirmPassword.current = value;
}}
defaultValue={confirmPassword.current}
loading={loading}
testID="input.confirmPassword"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Signup"
returnKeyType="done"
secureTextEntry
@@ -276,17 +265,22 @@ export const Signup = ({
autoCapitalize="none"
autoCorrect={false}
blurOnSubmit={false}
validationType="confirmPassword"
customValidator={() => password.current!}
placeholder={strings.confirmPassword()}
marginBottom={12}
onSubmit={() => {
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField(
"password",
strings.passwordNotMatched()
)
]}
onSubmitEditing={() => {
signup();
}}
/>
<Button
title={!loading ? "Continue" : null}
title={!loading ? strings.continue() : null}
type="accent"
loading={loading}
onPress={() => {
@@ -320,6 +314,25 @@ export const Signup = ({
</Paragraph>
</Paragraph>
</TouchableOpacity>
{errorMessage ? (
<Paragraph
numberOfLines={4}
onPress={() => {}}
color={colors.error.accent}
style={{
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
<AppIcon
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{errorMessage}
</Paragraph>
) : null}
</View>
<View

View File

@@ -27,14 +27,15 @@ import useTimer from "../../hooks/use-timer";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { eCloseSimpleDialog } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { presentDialog } from "../dialog/functions";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
import { presentDialog } from "../dialog/functions";
type MFAInfo = {
primaryMethod: string;
@@ -52,7 +53,8 @@ const TwoFactorVerification = ({
method: string;
code: string;
},
callback: (result: any) => void
callback: (result: any) => void,
onerror: (e: Error) => void
) => Promise<void>;
mfaInfo: MFAInfo;
onCancel: () => void;
@@ -66,15 +68,26 @@ const TwoFactorVerification = ({
method: mfaInfo?.primaryMethod,
isPrimary: true
});
const { seconds, start, reset } = useTimer(currentMethod.method!);
const { seconds, start, reset, secondsRef } = useTimer(currentMethod.method!);
const [loading, setLoading] = useState(false);
const inputRef = useRef<TextInput>(null);
const [sending, setSending] = useState(false);
const [error, setError] = useState<Error | undefined>(undefined);
const onNext = async () => {
if (!code.current || code.current.length < 6 || !currentMethod.method)
if (!code.current || code.current.length < 6) {
setError(
new Error("Please provide a valid multi-factor authentication code.")
);
return;
}
if (!currentMethod.method) {
return;
}
setLoading(true);
setError(undefined);
inputRef.current?.blur();
await onMfaLogin(
{
@@ -86,6 +99,9 @@ const TwoFactorVerification = ({
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
}
setLoading(false);
},
(e) => {
setError(e);
}
);
setLoading(false);
@@ -131,7 +147,7 @@ const TwoFactorVerification = ({
};
const onSendCode = useCallback(async () => {
if (seconds || sending) return;
if (secondsRef.current || sending) return;
setSending(true);
try {
await db.mfa.sendCode(currentMethod.method as "sms" | "email");
@@ -139,15 +155,18 @@ const TwoFactorVerification = ({
setSending(false);
} catch (e) {
setSending(false);
ToastManager.error(e as Error, "Error sending 2FA Code", "local");
setError(
new Error(`Error sending 2FA Code. Tap "Send code" to try again `)
);
}
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
}, [currentMethod.method, secondsRef, sending, start]);
useEffect(() => {
if (currentMethod.method === "sms" || currentMethod.method === "email") {
onSendCode();
}
}, [currentMethod.method, onSendCode]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentMethod.method]);
return (
<ScrollView
@@ -234,6 +253,7 @@ const TwoFactorVerification = ({
fwdRef={inputRef}
textAlign="center"
onChangeText={(value) => {
setError(undefined);
code.current = value;
}}
cursorColor={colors.selected.accent}
@@ -241,6 +261,7 @@ const TwoFactorVerification = ({
selectionColor={colors.selected.accent}
onSubmitEditing={onNext}
height={60}
marginBottom={0}
inputStyle={{
fontSize: AppFontSize.lg,
textAlign: "center",
@@ -254,10 +275,26 @@ const TwoFactorVerification = ({
containerStyle={{
minWidth: "50%"
}}
wrapperStyle={{
height: 60
}}
/>
{error ? (
<Paragraph
numberOfLines={4}
onPress={() => {}}
color={colors.error.accent}
style={{
textAlign: "center",
marginVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
maxWidth: 250
}}
>
<AppIcon
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{error?.message}
</Paragraph>
) : null}
<Button
title={loading ? null : strings.next()}
@@ -338,7 +375,8 @@ TwoFactorVerification.present = (
method: string;
code: string;
},
callback: (result: any) => void
callback: (result: any) => void,
onerror: (e: Error) => void
) => Promise<void>,
data: MFAInfo,
onCancel: () => void,

View File

@@ -28,6 +28,7 @@ import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSimpleDialog } from "../../utils/events";
import TwoFactorVerification from "./two-factor";
import { createFormRef } from "../ui/input/form-input";
export const LoginSteps = {
emailAuth: 1,
@@ -39,49 +40,37 @@ export const useLogin = (
onFinishLogin?: () => void,
sessionExpired = false
) => {
const [error, setError] = useState(false);
const [error, setError] = useState<Error>();
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const [step, setStep] = useState(LoginSteps.emailAuth);
const email = useRef<string>(undefined);
const password = useRef<string>(undefined);
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const validateInfo = () => {
if (
(!password.current && step === LoginSteps.passwordAuth) ||
(!email.current && step === LoginSteps.emailAuth)
) {
ToastManager.show({
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
type: "error",
context: "local"
});
return false;
}
return true;
};
const formRef = useRef(
createFormRef({
email: "",
password: ""
})
);
const login = async () => {
if (!validateInfo() || error) return;
try {
if (loading) return;
setError(undefined);
setLoading(true);
switch (step) {
case LoginSteps.emailAuth: {
if (!email.current) {
if (formRef.current.validateField("email")) {
setLoading(false);
return;
}
const mfaInfo = await db.user.authenticateEmail(email.current);
const mfaInfo = await db.user.authenticateEmail(
formRef.current.getValue("email")
);
if (mfaInfo) {
TwoFactorVerification.present(
async (mfa: any, callback: (success: boolean) => void) => {
async (mfa: any, callback: (success: boolean) => void, onerror: (e: Error) => void) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
@@ -103,6 +92,9 @@ export const useLogin = (
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
ToastManager.error(new Error("Token expired, try logging in again"));
} else {
onerror(e as Error);
}
}
},
@@ -119,13 +111,14 @@ export const useLogin = (
break;
}
case LoginSteps.passwordAuth: {
if (!email.current || !password.current) {
if (!formRef.current.validate()) {
setLoading(false);
return;
}
const values = formRef.current.getValues();
await db.user.authenticatePassword(
email.current,
password.current,
values.email,
values.password,
undefined,
sessionExpired
);
@@ -142,12 +135,11 @@ export const useLogin = (
const finishWithError = async (e: Error) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
ToastManager.show({
heading: strings.loginFailed(),
message: e.message,
type: "error",
context: "local"
});
if (e.message === "Password is incorrect.") {
formRef.current.setError("password", e.message);
} else {
setError(e);
}
};
const finishLogin = async () => {
@@ -174,13 +166,12 @@ export const useLogin = (
login,
step,
setStep,
email,
password,
passwordInputRef,
emailInputRef,
loading,
setLoading,
error,
setError
setError,
formRef
};
};

View File

@@ -1,3 +1,22 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import { useRef } from "react";
import { View } from "react-native";

View File

@@ -23,7 +23,6 @@ import {
ColorValue,
KeyboardAvoidingView,
Modal,
Platform,
SafeAreaView,
StyleSheet,
TouchableOpacity,
@@ -136,8 +135,8 @@ const BaseDialog = ({
backgroundColor: background
? background
: transparent
? "transparent"
: "rgba(0,0,0,0.3)"
? "transparent"
: "rgba(0,0,0,0.1)"
}}
>
<KeyboardAvoidingView
@@ -155,8 +154,8 @@ const BaseDialog = ({
justifyContent: centered
? "center"
: bottom
? "flex-end"
: "flex-start"
? "flex-end"
: "flex-start"
}
]}
>

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { StyleSheet, View } from "react-native";
import { ActivityIndicator, StyleSheet, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { getColorLinearShade } from "../../utils/colors";
@@ -79,6 +79,11 @@ const DialogButtons = ({
/>
<Paragraph color={colors.primary.accent}>{" " + doneText}</Paragraph>
</View>
) : loading ? (
<ActivityIndicator
size={AppFontSize.lg}
color={colors.primary.accent}
/>
) : (
<View />
)}
@@ -105,7 +110,6 @@ const DialogButtons = ({
style={{
marginLeft: 10
}}
loading={loading}
bold
type={positiveType || "transparent"}
title={positiveTitle}

View File

@@ -22,7 +22,6 @@ import { Text, View, ViewStyle } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { Button, ButtonProps } from "../ui/button";
import { PressableProps } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";

View File

@@ -17,10 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { KeyboardTypeOptions } from "react-native";
import { KeyboardTypeOptions, TextInput } from "react-native";
import { eSendEvent } from "../../services/event-manager";
import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events";
import { ButtonProps } from "../ui/button";
import { FieldValidator, FormRef } from "../ui/input/form-input";
import { RefObject } from "react";
export type DialogInfo = {
title?: string;
@@ -43,6 +45,18 @@ export type DialogInfo = {
| "errorShade";
icon?: string;
paragraphColor: string;
form?: {
formRef: FormRef;
items: {
name: string;
placeholder: string;
label?: string;
validators: FieldValidator[];
defaultValue?: string;
ref: RefObject<TextInput | null>;
}[];
onFormSubmit?: (form: FormRef) => Promise<boolean>;
};
input: boolean;
inputPlaceholder: string;
defaultValue: string;

View File

@@ -18,7 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, {
useCallback,
useEffect,
useRef,
useState,
RefObject
} from "react";
import { TextInput, View, ViewStyle } from "react-native";
import { DDS } from "../../services/device-detection";
import {
@@ -34,6 +40,7 @@ import { sleep } from "../../utils/time";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { FormInput, type FormRef } from "../ui/input/form-input";
import { Notice } from "../ui/notice";
import Seperator from "../ui/seperator";
import BaseDialog from "./base-dialog";
@@ -53,15 +60,38 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
});
const inputRef = useRef<TextInput>(null);
const [dialogInfo, setDialogInfo] = useState<DialogInfo>();
const formRef = useRef(dialogInfo?.form?.formRef);
formRef.current = dialogInfo?.form?.formRef;
const onPressPositive = async () => {
if (dialogInfo?.positivePress) {
// Handle form submission if form is available
if (dialogInfo?.form && formRef.current) {
inputRef.current?.blur();
try {
const isValid = await formRef.current.validate();
if (!isValid) {
return;
}
if (dialogInfo.form.onFormSubmit) {
setLoading(true);
const result = await dialogInfo.form.onFormSubmit(formRef.current);
if (result === false) {
setLoading(false);
return;
}
}
} catch (e) {
/** Empty */
}
setLoading(false);
} else if (dialogInfo?.positivePress) {
// Handle old input-based submission
inputRef.current?.blur();
setLoading(true);
let result = false;
try {
result = await dialogInfo.positivePress(
values.current.inputValue || dialogInfo.defaultValue,
values.current.inputValue,
checked
);
} catch (e) {
@@ -76,6 +106,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
setChecked(false);
values.current.inputValue = undefined;
formRef.current = undefined;
setVisible(false);
};
@@ -85,6 +116,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
if (data.context !== context) return;
setDialogInfo(data);
setChecked(data.check?.defaultValue);
formRef.current = data?.form?.formRef;
values.current.inputValue = data.defaultValue;
setVisible(true);
},
@@ -94,6 +126,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
const hide = React.useCallback(() => {
setChecked(false);
values.current.inputValue = undefined;
formRef.current = undefined;
setVisible(false);
setDialogInfo(undefined);
dialogInfo?.onClose?.();
@@ -134,19 +167,30 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
? false
: dialogInfo.statusBarTranslucent
}
bounce={!dialogInfo.input}
bounce={!dialogInfo.input && !dialogInfo.form}
closeOnTouch={!dialogInfo.disableBackdropClosing}
background={dialogInfo.background}
transparent={
dialogInfo.transparent === undefined ? true : dialogInfo.transparent
dialogInfo.transparent === undefined ? false : dialogInfo.transparent
}
onShow={async () => {
if (dialogInfo.input) {
if (dialogInfo.input && !dialogInfo.form) {
inputRef.current?.setNativeProps({
text: dialogInfo.defaultValue
});
await sleep(300);
inputRef.current?.focus();
} else if (dialogInfo.form) {
const items = dialogInfo.form?.items;
const firstItem = items[0];
for (const item of items) {
if (item.defaultValue) {
item.ref.current?.setNativeProps({
text: dialogInfo.defaultValue
});
}
}
firstItem?.ref?.current?.focus();
}
}}
visible={true}
@@ -170,7 +214,36 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
/>
<Seperator half />
{dialogInfo.input ? (
{dialogInfo.form ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP / 2
}}
>
{dialogInfo.form.items.map((item, index) => (
<FormInput
key={item.name}
fwdRef={item.ref}
name={item.name}
autoFocus={index === 0}
placeholder={item.placeholder}
formRef={formRef as RefObject<FormRef>}
validators={item.validators}
defaultValue={item.defaultValue}
secureTextEntry={dialogInfo.secureTextEntry}
onSubmitEditing={() => {
const nextItem = dialogInfo?.form?.items?.[index + 1];
if (nextItem) {
nextItem?.ref.current?.focus();
} else {
onPressPositive();
}
}}
/>
))}
</View>
) : dialogInfo.input ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
@@ -184,7 +257,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}}
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
//defaultValue={dialogInfo.defaultValue}
defaultValue={dialogInfo.defaultValue}
onSubmit={() => {
onPressPositive();
}}
@@ -237,7 +310,10 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={dialogInfo.positivePress && onPressPositive}
onPressPositive={
(dialogInfo.positivePress || dialogInfo.form?.onFormSubmit) &&
onPressPositive
}
loading={loading}
positiveTitle={dialogInfo.positiveText}
negativeTitle={dialogInfo.negativeText}

View File

@@ -201,12 +201,14 @@ export const AppLockPassword = () => {
accountPass
? strings.enterAccountPassword()
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin" ? strings.pin() : strings.password()
}`
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin"
? strings.pin()
: strings.password()
}`
}
/>
@@ -364,7 +366,9 @@ export const AppLockPassword = () => {
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(SettingsService.get());
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
ToastManager.show({
message: strings.applockDisabled(),
type: "success"
@@ -383,8 +387,8 @@ export const AppLockPassword = () => {
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
? strings.change()
: strings.save()
}
negativeTitle={strings.cancel()}
positiveType="transparent"

View File

@@ -110,61 +110,6 @@ export const VaultDialog: React.FC = () => {
const confirmPasswordRef = useRef<string | null>(null);
const newPasswordRef = useRef<string | null>(null);
const open = useCallback(async (data: Vault) => {
const biometry = await BiometricService.isBiometryAvailable();
const available = !!biometry;
const fingerprint = await BiometricService.hasInternetCredentials();
if (data.item) {
const locked = await db.vaults.itemExists(data.item);
noteLockedRef.current = locked;
if (!locked) {
const content = await db.content.findByNoteId(data.item!.id);
if (content && isEncryptedContent(content)) {
noteLockedRef.current = true;
}
}
}
// Set refs
noteRef.current = data.item;
titleRef.current = data.title || strings.goToEditor();
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
onUnlockRef.current = data.onUnlock;
requestTypeRef.current = data.requestType;
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setLoading(false);
// Auto-unlock with fingerprint if applicable
const canAutoUnlock =
fingerprint &&
data.requestType !== VaultRequestType.EnableFingerprint &&
data.requestType !== VaultRequestType.RevokeFingerprint &&
data.requestType !== VaultRequestType.ChangePassword &&
data.requestType !== VaultRequestType.ClearVault &&
data.requestType !== VaultRequestType.DeleteVault &&
data.requestType !== VaultRequestType.CustomAction &&
data.requestType !== VaultRequestType.PermanentUnlock;
if (canAutoUnlock) {
await onPressFingerprintAuth(data.title, data.description);
} else {
setVisible(true);
}
}, []);
const close = useCallback(() => {
if (loading) {
ToastManager.show({
@@ -249,6 +194,7 @@ export const VaultDialog: React.FC = () => {
close();
}, 100);
} else {
setLoading(false);
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
@@ -282,6 +228,10 @@ export const VaultDialog: React.FC = () => {
setLoading(false);
close();
eSendEvent("vaultUpdated");
ToastManager.show({
message: strings.vaultCleared(),
type: "success"
});
} catch (e) {
ToastManager.show({
heading: strings.passwordIncorrect(),
@@ -292,6 +242,45 @@ export const VaultDialog: React.FC = () => {
setLoading(false);
}, [close]);
const enrollFingerprint = useCallback(
async (password: string) => {
setLoading(true);
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
setLoading(false);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
close();
} catch (e) {
close();
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setLoading(false);
}
},
[close]
);
const takeErrorAction = useCallback(() => {
setWrongPassword(true);
setVisible(true);
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
}, []);
const lockNote = useCallback(async () => {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
@@ -333,7 +322,13 @@ export const VaultDialog: React.FC = () => {
.catch((e) => {
takeErrorAction();
});
}, [close, biometricUnlock, isBiometryEnrolled]);
}, [
biometricUnlock,
isBiometryEnrolled,
close,
enrollFingerprint,
takeErrorAction
]);
const openInEditor = useCallback(
(note: Note & { content?: NoteContent<false> }) => {
@@ -387,19 +382,7 @@ export const VaultDialog: React.FC = () => {
} catch (e) {
takeErrorAction();
}
}, [close]);
const takeErrorAction = useCallback(() => {
setWrongPassword(true);
setVisible(true);
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
}, []);
}, [close, takeErrorAction]);
const openNote = useCallback(async () => {
try {
@@ -440,6 +423,7 @@ export const VaultDialog: React.FC = () => {
}, [
biometricUnlock,
isBiometryEnrolled,
enrollFingerprint,
openInEditor,
shareNote,
deleteNote,
@@ -464,33 +448,6 @@ export const VaultDialog: React.FC = () => {
}
}, [permanantUnlock, openNote]);
const enrollFingerprint = useCallback(
async (password: string) => {
setLoading(true);
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
setLoading(false);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
close();
} catch (e) {
close();
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setLoading(false);
}
},
[close]
);
const createVault = useCallback(async () => {
await db.vault.create(passwordRef.current || "");
@@ -536,31 +493,6 @@ export const VaultDialog: React.FC = () => {
}
}, []);
const onPressFingerprintAuth = useCallback(
async (title?: string, description?: string) => {
try {
const credentials = await BiometricService.getCredentials(
title || titleRef.current,
description || descriptionRef.current || ""
);
if (!credentials) throw new Error("Failed to get user credentials");
if (credentials?.password) {
passwordRef.current = credentials.password;
onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
setVisible(true);
}
} catch (e) {
console.error(e);
}
},
[]
);
const onPress = useCallback(async () => {
const requestType = requestTypeRef.current;
@@ -689,6 +621,89 @@ export const VaultDialog: React.FC = () => {
deleteVault
]);
const onPressFingerprintAuth = useCallback(
async (title?: string, description?: string) => {
try {
const credentials = await BiometricService.getCredentials(
title || titleRef.current,
description || descriptionRef.current || ""
);
if (!credentials) throw new Error("Failed to get user credentials");
if (credentials?.password) {
passwordRef.current = credentials.password;
onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
setVisible(true);
}
} catch (e) {
console.error(e);
}
},
[onPress]
);
const open = useCallback(
async (data: Vault) => {
const biometry = await BiometricService.isBiometryAvailable();
const available = !!biometry;
const fingerprint = await BiometricService.hasInternetCredentials();
if (data.item) {
const locked = await db.vaults.itemExists(data.item);
noteLockedRef.current = locked;
if (!locked) {
const content = await db.content.findByNoteId(data.item!.id);
if (content && isEncryptedContent(content)) {
noteLockedRef.current = true;
}
}
}
// Set refs
noteRef.current = data.item;
titleRef.current = data.title || strings.goToEditor();
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
onUnlockRef.current = data.onUnlock;
requestTypeRef.current = data.requestType;
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setLoading(false);
// Auto-unlock with fingerprint if applicable
const canAutoUnlock =
fingerprint &&
data.requestType !== VaultRequestType.EnableFingerprint &&
data.requestType !== VaultRequestType.RevokeFingerprint &&
data.requestType !== VaultRequestType.ChangePassword &&
data.requestType !== VaultRequestType.ClearVault &&
data.requestType !== VaultRequestType.DeleteVault &&
data.requestType !== VaultRequestType.CustomAction &&
data.requestType !== VaultRequestType.PermanentUnlock;
if (canAutoUnlock) {
await onPressFingerprintAuth(data.title, data.description);
} else {
setVisible(true);
}
},
[onPressFingerprintAuth]
);
useEffect(() => {
eSubscribeEvent(eOpenVaultDialog, open);
eSubscribeEvent(eCloseVaultDialog, close);
@@ -731,7 +746,8 @@ export const VaultDialog: React.FC = () => {
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
paddingTop: 12,
overflow: "hidden"
}}
>
<DialogHeader
@@ -798,7 +814,8 @@ export const VaultDialog: React.FC = () => {
!isBiometryAvailable ||
isCreateVault ||
isChangePassword ||
isCustomAction ? null : (
isCustomAction ||
isDeleteVault ? null : (
<Button
onPress={() =>
onPressFingerprintAuth(strings.unlockNote(), "")

View File

@@ -22,7 +22,6 @@ import React, {
RefObject,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState
} from "react";
@@ -39,8 +38,6 @@ import Animated, {
WithSpringConfig,
withTiming
} from "react-native-reanimated";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { getAppState } from "../../screens/editor/tiptap/utils";
import { eSendEvent } from "../../services/event-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { eClearEditor } from "../../utils/events";
@@ -84,18 +81,9 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
}: TabProps,
ref
) {
const appState = useMemo(() => getAppState(), []);
const deviceMode = useSettingStore((state) => state.deviceMode);
const fullscreen = useSettingStore((state) => state.fullscreen);
const translateX = useSharedValue(
widths
? appState &&
appState?.movedAway === false &&
useTabStore.getState().getCurrentNoteId()
? widths.sidebar + widths.list
: widths.sidebar
: 0
);
const translateX = useSharedValue(widths ? widths.sidebar : 0);
const startX = useSharedValue(0);
const currentTab = useSharedValue(1);
const previousTab = useSharedValue(1);
@@ -125,10 +113,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
translateX.value = 0;
} else {
if (prevWidths.current?.sidebar !== widths.sidebar) {
translateX.value =
appState && appState?.movedAway === false
? editorPosition
: widths.sidebar;
translateX.value = widths.sidebar;
if (translateX.value === editorPosition) {
onChangeTab?.({ i: 2, from: 1 });
}
@@ -136,15 +121,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
}
isLoaded.current = true;
prevWidths.current = widths;
}, [
deviceMode,
widths,
fullscreen,
translateX,
editorPosition,
appState,
onChangeTab
]);
}, [deviceMode, widths, fullscreen, translateX, editorPosition, onChangeTab]);
useEffect(() => {
const sub = BackHandler.addEventListener("hardwareBackPress", () => {

View File

@@ -17,7 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import {
GroupHeader,
GroupingKey,
GroupOptions,
ItemType
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
@@ -40,7 +45,9 @@ type SectionHeaderProps = {
color?: string;
screen?: RouteName;
groupOptions: GroupOptions;
group: GroupingKey;
onOpenJumpToDialog: () => void;
itemCount?: number;
};
export const SectionHeader = React.memo<
@@ -53,11 +60,13 @@ export const SectionHeader = React.memo<
color,
screen,
groupOptions,
onOpenJumpToDialog
group,
onOpenJumpToDialog,
itemCount
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
dataType as "note" | "notebook" | "searchResult"
);
return (
@@ -104,7 +113,9 @@ export const SectionHeader = React.memo<
color={color || colors.primary.accent}
>
{!item.title || item.title === ""
? strings.pinned().toUpperCase()
? screen === "Search"
? strings.results(itemCount || 0)
: strings.pinned().toUpperCase()
: item.title.toUpperCase()}
</Heading>
</Pressable>
@@ -133,6 +144,7 @@ export const SectionHeader = React.memo<
<Sort
screen={screen}
type={dataType}
group={group}
hideGroupOptions={
screen === "Reminders" || screen === "Search"
}
@@ -150,7 +162,8 @@ export const SectionHeader = React.memo<
hidden={
dataType !== "note" &&
dataType !== "notebook" &&
screen !== "Notes"
screen !== "Notes" &&
screen !== "Search"
}
style={{
width: 25,
@@ -163,9 +176,11 @@ export const SectionHeader = React.memo<
}
onPress={() => {
SettingsService.set({
[dataType !== "notebook"
? "notesListMode"
: "notebooksListMode"]: !isCompactModeEnabled
[dataType === "notebook"
? "notebooksListMode"
: dataType === "searchResult"
? "searchListMode"
: "notesListMode"]: !isCompactModeEnabled
? "compact"
: "normal"
});
@@ -191,6 +206,7 @@ export const SectionHeader = React.memo<
},
(prev, next) => {
if (prev.item.title !== next.item.title) return false;
if (prev.itemCount !== next.itemCount) return false;
if (prev.groupOptions?.groupBy !== next.groupOptions.groupBy) return false;
if (prev.groupOptions?.sortDirection !== next.groupOptions.sortDirection)
return false;

View File

@@ -31,6 +31,7 @@ import { eSendEvent } from "../../../services/event-manager";
import { eOnLoadNote } from "../../../utils/events";
import { IconButton } from "../../ui/icon-button";
import { fluidTabsRef } from "../../../utils/global-refs";
import { useSettingStore } from "../../../stores/use-setting-store";
type SearchResultProps = {
item: HighlightedResult;
};
@@ -38,6 +39,9 @@ type SearchResultProps = {
export const SearchResult = (props: SearchResultProps) => {
const [expanded, setExpanded] = React.useState(true);
const { colors } = useThemeColors();
const compactMode = useSettingStore(
(state) => state.settings.searchListMode === "compact"
);
const openNote = async (index?: number) => {
const note = await db.notes.note(props.item.id);
@@ -87,7 +91,7 @@ export const SearchResult = (props: SearchResultProps) => {
flexShrink: 1
}}
>
{props.item.content?.length ? (
{props.item.content?.length && !compactMode ? (
<IconButton
name={!expanded ? "chevron-right" : "chevron-down"}
onPress={() => setExpanded((prev) => !prev)}
@@ -130,6 +134,7 @@ export const SearchResult = (props: SearchResultProps) => {
</View>
{expanded &&
!compactMode &&
props.item.content.map((content, index) => (
<Pressable
key={props.item.id + index}

View File

@@ -53,6 +53,7 @@ type ListProps = {
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
groupType: GroupingKey;
id?: string;
};
@@ -73,16 +74,7 @@ export default function List(props: ListProps) {
props.dataType === "notebook" ||
notebooksListMode === "compact";
const groupType =
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;
const groupOptions = useGroupOptions(groupType);
const groupOptions = useGroupOptions(props.groupType);
const _onRefresh = async () => {
Sync.run("global", false, "full", () => {
@@ -94,7 +86,7 @@ export default function List(props: ListProps) {
(item: number | boolean, index: number) => {
return props.data?.type(index);
},
[]
[props.data]
);
const renderItem = React.useCallback(
@@ -105,7 +97,7 @@ export default function List(props: ListProps) {
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupOptions={groupOptions}
group={groupType as GroupingKey}
group={props.groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
@@ -115,7 +107,7 @@ export default function List(props: ListProps) {
},
[
groupOptions,
groupType,
props.groupType,
props.customAccentColor,
props.data,
props.dataType,

View File

@@ -53,7 +53,7 @@ import TagItem from "../list-items/tag";
import { SearchResult } from "../list-items/search-result";
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
group: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
isSheet: boolean;
index: number;
@@ -182,6 +182,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -218,6 +219,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -245,6 +247,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
@@ -271,6 +274,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
@@ -297,9 +301,11 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
itemCount={items?.placeholders.length}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,

View File

@@ -30,7 +30,7 @@ import { fluidTabsRef } from "../../utils/global-refs";
import { AppFontSize } from "../../utils/size";
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
import { IconButton } from "../ui/icon-button";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
import { strings } from "@notesnook/intl";
import { ToastManager } from "../../services/event-manager";
@@ -142,7 +142,7 @@ function ReorderableList<T extends { id: string }>({
items.push(...data.filter((i) => !itemOrderState.includes(i.id)));
return items;
}, [data, customizableSidebarFeature?.isAllowed]);
}, [customizableSidebarFeature?.isAllowed, data, itemOrderState]);
return (
<View style={styles.container}>

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { getFormattedDate } from "@notesnook/common";
import {
EncryptedContentItem,
isEncryptedContent,
Note,
UnencryptedContentItem
} from "@notesnook/core";
@@ -75,9 +74,11 @@ const MergeConflicts = () => {
const { height } = useSettingStore((state) => state.dimensions);
const applyChanges = async () => {
let contentToSave = selectedContent;
const contentToSave = selectedContent;
if (!contentToSave) return;
let note = await db.notes.note(contentToSave.noteId);
const note = await db.notes.note(
(content.current as UnencryptedContentItem).noteId
);
if (!note) return;
await db.notes.add({
id: note.id,
@@ -462,7 +463,8 @@ const MergeConflicts = () => {
<ReadonlyEditor
editorId="conflictSecondary"
onLoad={async (loadContent) => {
const note = await db.notes.note(content.current?.noteId!);
if (!content.current?.noteId) return;
const note = await db.notes.note(content.current?.noteId);
if (!note) return;
loadContent({
id: note.id,

View File

@@ -63,22 +63,25 @@ const HistoryItem = ({
}${_end_time}`;
};
const preview = useCallback(async (item: HistorySession) => {
const content = await db.noteHistory.content(item.id);
presentSheet({
component: (
<NotePreview
session={{
...item,
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
note={note}
/>
),
context: "note_history"
});
}, []);
const preview = useCallback(
async (item: HistorySession) => {
const content = await db.noteHistory.content(item.id);
presentSheet({
component: (
<NotePreview
session={{
...item,
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
note={note}
/>
),
context: "note_history"
});
},
[note]
);
return (
<Pressable
@@ -138,7 +141,7 @@ export default function NoteHistory({
({ index }: { index: number }) => (
<HistoryItem index={index} items={history} note={note} />
),
[history]
[history, note]
);
return (

View File

@@ -42,7 +42,6 @@ import {
isEncryptedContent,
Note,
NoteContent,
SessionContentItem,
TrashOrItem
} from "@notesnook/core";

View File

@@ -18,14 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFeaturesTable } from "@notesnook/common";
import {
EV,
EVENTS,
Plan,
SKUResponse,
SubscriptionPlan,
User
} from "@notesnook/core";
import { EVENTS, Plan, SubscriptionPlan, User } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
@@ -55,6 +48,7 @@ import {
TECHLORE_SVG,
XDA_SVG
} from "../../assets/images/assets";
import { db } from "../../common/database";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import usePricingPlans, {
PlanOverView,
@@ -76,7 +70,6 @@ import { IconButton } from "../ui/icon-button";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { db } from "../../common/database";
const Steps = {
select: 1,
@@ -119,7 +112,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
}
setStep(Steps.buy);
}
}, [routeParams.state]);
}, [pricingPlans, routeParams.state]);
useEffect(() => {
let listener: NativeEventSubscription;
@@ -137,7 +130,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
return () => {
listener?.remove();
};
}, [isFocused, step]);
}, [isFocused, routeParams.context, step]);
useEffect(() => {
const sub = db.eventManager.subscribe(
@@ -154,7 +147,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
return () => {
sub?.unsubscribe();
};
}, []);
}, [routeParams.context]);
const is5YearPlanSelected = (
isGithubRelease
@@ -936,7 +929,10 @@ const PricingPlanCard = ({
setStep: (step: number) => void;
}) => {
const { colors } = useThemeColors();
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
const regionalDiscount =
annualBilling && plan.id === "pro"
? pricingPlans?.regionalDiscount
: undefined;
const { width } = useWindowDimensions();
const isTablet = width > 600;
@@ -959,26 +955,6 @@ const PricingPlanCard = ({
annualBilling
);
useEffect(() => {
if (pricingPlans?.isGithubRelease || !annualBilling) return;
pricingPlans
?.getRegionalDiscount(
plan.id,
pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: `notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
)
.then((value) => {
setRegionaDiscount(value);
});
}, [annualBilling]);
useEffect(() => {
if (!annualBilling) {
setRegionaDiscount(undefined);
}
}, [annualBilling]);
const isSubscribed =
product?.productId &&
pricingPlans?.user?.subscription?.productId?.includes(plan.id) &&
@@ -1008,8 +984,8 @@ const PricingPlanCard = ({
: "monthly"
}`
: pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: (product?.productId as string)
? (WebPlan?.period as string)
: (product?.productId as string)
);
setStep(Steps.buy);
}}

View File

@@ -27,7 +27,11 @@ import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import {
eSendEvent,
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useRelationStore } from "../../stores/use-relation-store";
@@ -63,6 +67,7 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
useRelationStore.getState().update();
setColorNotes();
Navigation.queueRoutesForUpdate();
sendItemUpdateEvent(item.id, "color");
eSendEvent(refreshNotesPage);
};
@@ -127,7 +132,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
}
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}, []);
}, [colorFeature]);
return (
<>
@@ -140,6 +145,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
Navigation.queueRoutesForUpdate();
sendItemUpdateEvent(color.id, "color");
eSendEvent(refreshNotesPage);
}}
/>

View File

@@ -35,7 +35,7 @@ export const DateMeta = ({ item }: { item: Item }) => {
const [dateCreated, setDateCreated] = useState(item.dateCreated);
function getDateMeta() {
let keys = Object.keys(item);
const keys = Object.keys(item);
if (keys.includes("dateEdited"))
keys.splice(
keys.findIndex((k) => k === "dateModified"),

View File

@@ -173,15 +173,15 @@ export const Items = ({
DDS.isTab
? AppFontSize.xxl
: shouldShrink
? AppFontSize.xxl
: AppFontSize.lg
? AppFontSize.xxl
: AppFontSize.lg
}
color={
item.checked
? item.activeColor || colors.primary.accent
: item.id.match(/(delete|trash)/g)
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
</Pressable>
@@ -212,8 +212,8 @@ export const Items = ({
text: item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.paragraph
: colors.primary.paragraph
? colors.error.paragraph
: colors.primary.paragraph
}}
testID={"icon-" + item.id}
onPress={item.onPress}
@@ -277,8 +277,8 @@ export const Items = ({
item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
@@ -318,7 +318,9 @@ export const Items = ({
[
colors.error.icon,
colors.primary.accent,
colors.primary.border,
colors.secondary.icon,
colors.static.orange,
columnItemWidth,
topBarSorting
]

View File

@@ -77,7 +77,7 @@ export const TagStrip = ({ item, close }) => {
.then((tags) => {
setTags(tags);
});
}, []);
}, [item]);
return tags?.length > 0 ? (
<View

View File

@@ -42,8 +42,6 @@ import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import ExportNotesSheet from "../sheets/export-notes";
import { MoveNotebook } from "../../screens/move-notebook";
import { IconButton } from "../ui/icon-button";
import NativeTooltip from "../../utils/tooltip";
import ManageTags from "../../screens/manage-tags";

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 { Plan, SKUResponse } from "@notesnook/core";
import { Plan } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import {
Linking,
Platform,
ScrollView,
Text,
TouchableOpacity,
@@ -31,7 +32,6 @@ import {
import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { WebView } from "react-native-webview";
import { db } from "../../../common/database";
import usePricingPlans from "../../../hooks/use-pricing-plans";
import { ToastManager } from "../../../services/event-manager";
@@ -211,16 +211,19 @@ export const BuyPlan = (props: {
? strings["5yearPlanConditions"]()
: [
strings.trialPlanConditions[0](
billingDuration?.duration as number
billingDuration?.duration as number as never
),
strings.trialPlanConditions[1](0)
...(isGithubRelease
? []
: [strings.trialPlanConditions[1](Platform.OS as never)])
]
).map((item) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 10
gap: 10,
flex: 1
}}
key={item}
>
@@ -229,7 +232,13 @@ export const BuyPlan = (props: {
size={AppFontSize.lg}
name="check"
/>
<Paragraph>{item}</Paragraph>
<Paragraph
style={{
flexShrink: 1
}}
>
{item}
</Paragraph>
</View>
))}
</View>
@@ -243,8 +252,8 @@ export const BuyPlan = (props: {
is5YearPlanSelected
? strings.purchase()
: pricingPlans?.userCanRequestTrial
? strings.subscribeAndStartTrial()
: strings.subscribe()
? strings.subscribeAndStartTrial()
: strings.subscribe()
}
onPress={async () => {
if (isGithubRelease) {
@@ -321,7 +330,10 @@ const ProductItem = (props: {
productId: string;
}) => {
const { colors } = useThemeColors();
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
const regionalDiscount =
props.productId === "notesnook.pro.yearly"
? props.pricingPlans.regionalDiscount
: undefined;
const product =
props.pricingPlans?.currentPlan?.subscriptions?.[
regionalDiscount?.sku || props.productId
@@ -358,27 +370,19 @@ const ProductItem = (props: {
props.pricingPlans.user?.subscription?.productId ===
(product as Plan)?.id);
useEffect(() => {
props.pricingPlans
?.getRegionalDiscount(
props.pricingPlans.currentPlan?.id as string,
props.pricingPlans.isGithubRelease
? ((product as Plan)?.period as string)
: props.productId
)
.then((value) => {
if (
value &&
value.sku?.startsWith(
(props.pricingPlans.selectedProduct as RNIap.Subscription)
?.productId
)
) {
props.pricingPlans.selectProduct(value?.sku as string);
}
setRegionaDiscount(value);
});
}, []);
const discountValue =
(isAnnual && !isGithubRelease) ||
(isGithubRelease && (product as Plan)?.discount?.amount)
? regionalDiscount
? regionalDiscount.discount
: isGithubRelease
? (product as Plan).discount?.amount
: props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
)
: undefined;
return (
<TouchableOpacity
@@ -420,12 +424,11 @@ const ProductItem = (props: {
{isAnnual
? strings.yearly()
: is5YearProduct
? strings.fiveYearPlan()
: strings.monthly()}
? strings.fiveYearPlan()
: strings.monthly()}
</Heading>
{(isAnnual && !isGithubRelease) ||
(isGithubRelease && (product as Plan)?.discount?.amount) ? (
{discountValue ? (
<View
style={{
backgroundColor: colors.static.red,
@@ -436,18 +439,7 @@ const ProductItem = (props: {
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.bestValue()} -{" "}
{strings.percentOff(
(regionalDiscount
? regionalDiscount.discount
: isGithubRelease
? (product as Plan).discount?.amount
: props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
)) as string
)}
{strings.bestValue()} - {strings.percentOff(`${discountValue}`)}
</Heading>
</View>
) : null}

View File

@@ -21,7 +21,6 @@ import React from "react";
import { View } from "react-native";
import FileViewer from "react-native-file-viewer";
import { ToastManager } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { Button } from "../../ui/button";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";

View File

@@ -17,18 +17,18 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Debug } from "@notesnook/core";
import { Debug, IssueReportResponse } from "@notesnook/core";
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { Linking, Platform, Text, TextInput, View } from "react-native";
import { getVersion } from "react-native-device-info";
import { useStoredRef } from "../../../hooks/use-stored-ref";
import { ToastManager } from "../../../services/event-manager";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store";
import { openLinkInBrowser } from "../../../utils/functions";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size/index";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
@@ -37,17 +37,26 @@ import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import Config from "react-native-config";
import { eCloseSheet } from "../../../utils/events";
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
export const Issue = ({
defaultTitle,
defaultBody,
issueTitle
}: {
defaultTitle?: string;
defaultBody?: string;
issueTitle?: string;
}) => {
const { colors } = useThemeColors();
const body = useStoredRef("issueBody");
const body = useStoredRef("issueBody", "");
const title = useStoredRef("issueTitle", defaultTitle);
const [done, setDone] = useState(false);
const user = useUserStore((state) => state.user);
const [loading, setLoading] = useState(false);
const bodyRef = useRef();
const bodyRef = useRef<TextInput>(null);
const initialLayout = useRef(false);
const issueUrl = useRef();
const issueReportResponse = useRef<IssueReportResponse>(undefined);
const onPress = async () => {
if (loading) return;
@@ -57,7 +66,7 @@ export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
try {
setLoading(true);
issueUrl.current = await Debug.report({
issueReportResponse.current = await Debug.report({
title: title.current,
body:
body.current +
@@ -72,7 +81,7 @@ Logged in: ${user ? "yes" : "no"}
Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
userId: user?.id
});
if (!issueUrl.current) {
if (!issueReportResponse.current) {
setLoading(false);
ToastManager.show({
heading: "Failed to report issue on github",
@@ -88,12 +97,46 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
} catch (e) {
setLoading(false);
ToastManager.show({
heading: e.message,
heading: (e as Error).message,
type: "error"
});
}
};
function getResponseInfo(response?: IssueReportResponse) {
if (!response || "error" in response) return;
switch (response.type) {
case "email": {
return {
title: strings.yourSupportRequestHasBeenForwarded(),
message: strings.supportEmailMessage()
};
}
case "discussion": {
const url = response.url;
return {
title: strings.thankYouForFeedback(),
positiveButtonText: strings.copyLink(),
message: strings.featureRequestMessage(url),
url: url
};
}
case "issue": {
const url = response.url;
return {
title: strings.thankYouForReporting(),
positiveButtonText: strings.copyLink(),
message: strings.bugReportMessage(url),
url: url
};
}
}
}
const responseInfo = getResponseInfo(issueReportResponse.current);
console.log(responseInfo, issueReportResponse.current);
return (
<View
style={{
@@ -105,38 +148,28 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
<>
<View
style={{
height: 250,
justifyContent: "center",
alignItems: "center",
gap: 10
}}
>
<Heading>{strings.issueCreatedHeading()}</Heading>
<Heading>{responseInfo?.title}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
selectable={true}
>
{strings.issueCreatedDesc[0]()}
<Paragraph
style={{
textDecorationLine: "underline",
color: colors.primary.accent
}}
onPress={() => {
Linking.openURL(issueUrl.current);
}}
>
{issueUrl.current}
</Paragraph>
. {strings.issueCreatedDesc[1]()}
{responseInfo?.message}
</Paragraph>
<Button
title={strings.openIssue()}
title={responseInfo?.positiveButtonText || "Done"}
onPress={() => {
Linking.openURL(issueUrl.current);
if (responseInfo?.url) {
Linking.openURL(responseInfo?.url);
}
eSendEvent(eCloseSheet);
}}
type="accent"
width="100%"
@@ -248,10 +281,7 @@ Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
}}
onPress={async () => {
try {
await openLinkInBrowser(
"https://discord.gg/zQBK97EE22",
colors
);
await openLinkInBrowser("https://discord.gg/zQBK97EE22");
} catch (e) {
console.error(e);
}

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useAreFeaturesAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import React, { useEffect } from "react";
import { View } from "react-native";
import {
eSendEvent,
@@ -28,7 +28,11 @@ import {
ToastManager
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { eCloseSheet } from "../../../utils/events";
import {
eAfterSync,
eCloseSheet,
eMenuItemUpdate
} from "../../../utils/events";
import { SideMenuItem } from "../../../utils/menu-items";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
@@ -37,12 +41,25 @@ import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import PaywallSheet from "../paywall";
import { presentDialog } from "../../dialog/functions";
import { db } from "../../../common/database";
import { useTrashStore } from "../../../stores/use-trash-store";
import { useSettingStore } from "../../../stores/use-setting-store";
export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
const { colors } = useThemeColors();
const featuresAvailable = useAreFeaturesAvailable([
"customHomepage",
"customizableSidebar"
]);
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const trash = useTrashStore((state) => state.items);
useEffect(() => {
if (!isAppLoading) {
useTrashStore.getState().refresh();
}
}, [isAppLoading]);
return !featuresAvailable ? null : (
<View
style={{
@@ -100,7 +117,38 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
},
icon: "sort-ascending",
locked: !featuresAvailable?.customizableSidebar.isAllowed
}
},
...(item.id === "Trash"
? [
{
title: strings.clearTrash(),
onPress: async () => {
if (!trash || trash?.length === 0) return;
eSendEvent(eCloseSheet);
setTimeout(() => {
presentDialog({
title: strings.clearTrashConfirm(),
paragraph: strings.clearTrashDesc(),
positiveText: strings.clear(),
positivePress: async () => {
await db.trash.clear();
useTrashStore.getState().clear();
eSendEvent(eMenuItemUpdate);
eSendEvent(eAfterSync);
ToastManager.show({
message: strings.trashCleared(),
type: "success"
});
return true;
}
});
}, 500);
},
icon: "delete-sweep-outline",
disabled: !trash || trash?.length === 0
}
]
: [])
].map((item) => (
<Pressable
key={item.title}
@@ -112,7 +160,7 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
gap: DefaultAppStyles.GAP_SMALL,
borderRadius: 0,
paddingHorizontal: DefaultAppStyles.GAP,
opacity: item.locked ? 0.6 : 1
opacity: item.disabled || item.locked ? 0.6 : 1
}}
onPress={() => {
item.onPress();

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { Notebook } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";

View File

@@ -1,3 +1,22 @@
/*
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 { FeatureId, FeatureResult } from "@notesnook/common";
import { SubscriptionPlan, SubscriptionProvider } from "@notesnook/core";
import { strings } from "@notesnook/intl";

View File

@@ -1,3 +1,22 @@
/*
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 {
FeatureUsage,
formatBytes,
@@ -76,10 +95,10 @@ export function PlanLimits() {
{item.total === Infinity
? strings.unlimited()
: item.id === "storage"
? `${formatBytes(item.used)}/${formatBytes(
item.total
)} ${strings.used()}`
: `${item.used}/${item.total} ${strings.used()}`}
? `${formatBytes(item.used)}/${formatBytes(
item.total
)} ${strings.used()}`
: `${item.used}/${item.total} ${strings.used()}`}
</Paragraph>
</View>
</View>

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { hosts, Monograph, Note } from "@notesnook/core";
import { hosts, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
@@ -28,12 +28,15 @@ import {
TouchableOpacity,
View
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import ToggleSwitch from "toggle-switch-react-native";
import { db } from "../../../common/database";
import { requestInAppReview } from "../../../services/app-review";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useAttachmentStore } from "../../../stores/use-attachment-store";
import { openLinkInBrowser } from "../../../utils/functions";
@@ -46,22 +49,17 @@ import Input from "../../ui/input";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useAsync } from "react-async-hook";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { eMenuItemUpdate } from "../../../utils/events";
import { useIsFeatureAvailable } from "@notesnook/common";
async function fetchMonographData(noteId: string) {
const monographId = db.monographs.monograph(noteId);
const monograph = monographId
? await db.monographs.get(monographId)
: undefined;
const analyticsFeature = await isFeatureAvailable("monographAnalytics");
const analytics =
monographId && analyticsFeature
? await db.monographs.analytics(monographId)
: undefined;
return {
monograph,
monographId,
analytics
monographId
};
}
@@ -118,6 +116,7 @@ const PublishNoteSheet = ({
await monographData.execute();
Navigation.queueRoutesForUpdate();
eSendEvent(eMenuItemUpdate);
setPublishLoading(false);
}
requestInAppReview();
@@ -144,6 +143,7 @@ const PublishNoteSheet = ({
await db.monographs.unpublish(note.id);
monographData.execute();
Navigation.queueRoutesForUpdate();
eSendEvent(eMenuItemUpdate);
setPublishLoading(false);
}
} catch (e) {
@@ -381,9 +381,6 @@ const PublishNoteSheet = ({
}}
>
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
<Paragraph>
{monographData?.result?.analytics?.totalViews || 0}
</Paragraph>
</View>
</View>
</View>

View File

@@ -121,6 +121,17 @@ export const RelationsList = ({
<List
data={items}
loading={false}
groupType={
referenceType === "note"
? "notes"
: referenceType === "tag"
? "tags"
: referenceType === "notebook"
? "notebooks"
: referenceType === "reminder"
? "reminders"
: "notes"
}
dataType={referenceType as any}
isRenderedInActionSheet={true}
/>

View File

@@ -181,6 +181,7 @@ export default function ReminderNotify({
data={references}
loading={false}
dataType="note"
groupType="notes"
isRenderedInActionSheet={true}
/>
</View>

View File

@@ -45,25 +45,16 @@ import Paragraph from "../../ui/typography/paragraph";
const Sort = ({
type,
screen,
hideGroupOptions
hideGroupOptions,
group: groupType
}: {
type: ItemType;
screen?: RouteName;
group: GroupingKey;
hideGroupOptions?: boolean;
}) => {
const { colors } = useThemeColors();
const groupType =
screen === "Archive"
? "archive"
: screen === "Search"
? "search"
: screen === "Notes"
? "home"
: screen === "Trash" || type === "trash"
? "trash"
: ((type + "s") as GroupingKey);
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(groupType)
);
@@ -157,10 +148,15 @@ const Sort = ({
>
{Object.keys(SORT).map((item) => {
const sortOptionVisibility = {
relevance: screen === "Search",
dueDate: screen === "Reminders",
dateModified: screen === "Tags" || screen === "Reminders",
dateEdited: screen !== "Tags" && screen !== "Reminders"
dateCreated: groupType !== "trash",
relevance: groupType === "search",
dueDate: groupType === "reminders",
dateModified: groupType === "reminders" || groupType === "tags",
dateEdited:
groupType !== "tags" &&
groupType !== "reminders" &&
groupType !== "trash",
dateDeleted: groupType === "trash"
};
// Check if this sort option should be skipped for the current screen
@@ -188,10 +184,7 @@ const Sort = ({
onPress={async () => {
const _groupOptions: GroupOptions = {
...groupOptions,
sortBy:
type === "trash"
? "dateDeleted"
: (item as SortOptions["sortBy"])
sortBy: item as SortOptions["sortBy"]
};
await updateGroupOptions(_groupOptions);
}}

View File

@@ -79,6 +79,7 @@ type SimpleTabViewProps = {
const createSceneMap = (
scenes: Record<string, React.ComponentType<any>>
): ((props: { route: SimpleRoute }) => React.ReactNode) => {
// eslint-disable-next-line react/display-name
return ({ route }: { route: SimpleRoute }) => {
const SceneComponent = scenes[route.key];
if (!SceneComponent) return null;
@@ -136,8 +137,8 @@ const SimpleTabView = ({
{loadedKeysRef.current.has(route.key)
? getSceneForRoute(route)
: navigationState.index === routeIndex
? getSceneForRoute(route)
: null}
? getSceneForRoute(route)
: null}
</View>
))}
</View>
@@ -432,12 +433,12 @@ const TabBar = (props: SimpleTabBarProps) => {
name="plus"
testID="sidebar-add-button"
size={AppFontSize.lg - 2}
top={10}
color={colors.primary.icon}
onPress={async () => {
if (props.navigationState.index === 1) {
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
const notebooksFeature =
await isFeatureAvailable("notebooks");
if (!notebooksFeature.isAllowed) {
PaywallSheet.present(notebooksFeature);
return;
@@ -485,6 +486,7 @@ const TabBar = (props: SimpleTabBarProps) => {
? "sort-ascending"
: "sort-descending"
}
top={10}
testID="sidebar-sort-button"
color={colors.primary.icon}
onPress={() => {
@@ -496,6 +498,11 @@ const TabBar = (props: SimpleTabBarProps) => {
? "notebook"
: "tag"
}
group={
props.navigationState.index === 1
? "notebooks"
: "tags"
}
hideGroupOptions
/>
)
@@ -520,6 +527,7 @@ const TabBar = (props: SimpleTabBarProps) => {
width: 28,
height: 28
}}
top={10}
testID="sidebar-theme-button"
color={colors.primary.icon}
name={isDark ? "weather-night" : "weather-sunny"}

View File

@@ -24,18 +24,22 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useTotalNotes } from "../../hooks/use-db-item";
import { db } from "../../common/database";
import { eSubscribeEvent } from "../../services/event-manager";
import {
eSubscribeEvent,
subscribeToItemUpdate
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import useNavigationStore, {
RouteParams
} from "../../stores/use-navigation-store";
import { eAfterSync } from "../../utils/events";
import { eAfterSync, eMenuItemUpdate } from "../../utils/events";
import { SideMenuItem } from "../../utils/menu-items";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { useSideBarDraggingStore } from "./dragging-store";
import { useRelationStore } from "../../stores/use-relation-store";
export function MenuItem({
item,
@@ -56,13 +60,19 @@ export function MenuItem({
const totalNotes = useTotalNotes(
item.dataType as "notebook" | "tag" | "color"
);
const update = useRelationStore((state) => state.updater);
const getTotalNotesRef = useRef(totalNotes.getTotalNotes);
getTotalNotesRef.current = totalNotes.getTotalNotes;
const menuItemCount = !item.data
? itemCount
: totalNotes.totalNotes(item.data.id);
useEffect(() => {
if (item.data) {
getTotalNotesRef.current([item.data?.id]);
}
}, [item.data, update]);
useEffect(() => {
const onSyncComplete = async () => {
try {
@@ -84,7 +94,9 @@ export function MenuItem({
setItemCount(await db.notes.archived.count());
break;
case "Trash":
setItemCount((await db.trash.all()).length);
{
setItemCount((await db.trash.all()).length);
}
break;
}
} else {
@@ -94,10 +106,20 @@ export function MenuItem({
/** Empty */
}
};
const event = eSubscribeEvent(eAfterSync, onSyncComplete);
const events = [eSubscribeEvent(eAfterSync, onSyncComplete)];
if (!item.data) {
events.push(eSubscribeEvent(eMenuItemUpdate, onSyncComplete));
}
if (item.data?.id) {
events.push(
subscribeToItemUpdate(item?.data?.id, item?.data?.type, onSyncComplete)
);
}
onSyncComplete();
return () => {
event?.unsubscribe();
events?.forEach((e) => e?.unsubscribe());
};
}, [item.data, item.id]);
@@ -151,8 +173,8 @@ export function MenuItem({
item.icon === "crown"
? colors.static.yellow
: isFocused
? colors.selected.icon
: colors.secondary.icon
? colors.selected.icon
: colors.secondary.icon
}
size={AppFontSize.md}
/>

View File

@@ -24,7 +24,8 @@ import { StoreApi, UseBoundStore } from "zustand";
import { useTotalNotes } from "../../hooks/use-db-item";
import {
eSubscribeEvent,
eUnSubscribeEvent
eUnSubscribeEvent,
ToastManager
} from "../../services/event-manager";
import { TreeItem } from "../../stores/create-notebook-tree-stores";
import { SelectionStore } from "../../stores/item-selection-store";
@@ -35,6 +36,7 @@ import AppIcon from "../ui/AppIcon";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { useRelationStore } from "../../stores/use-relation-store";
export const NotebookItem = ({
index,
@@ -70,13 +72,14 @@ export const NotebookItem = ({
const notebook = item.notebook;
const isFocused = focused;
const { totalNotes, getTotalNotes } = useTotalNotes("notebook");
const updater = useRelationStore(state => state.updater);
const getTotalNotesRef = React.useRef(getTotalNotes);
getTotalNotesRef.current = getTotalNotes;
const { colors } = useThemeColors();
useEffect(() => {
getTotalNotesRef.current([item.notebook.id]);
}, [item.notebook]);
}, [item.notebook, updater]);
useEffect(() => {
const onNotebookUpdate = (id?: string) => {
@@ -101,7 +104,8 @@ export const NotebookItem = ({
? 15 * item.depth
: 15 * 5,
width: "100%",
marginTop: 2
marginTop: 2,
opacity: item.disabled ? 0.5 : 1
}}
>
<Pressable

View File

@@ -76,7 +76,7 @@ export const PinnedSection = React.memo(
onPress: onPress,
onLongPress: onLongPress
})) as SideMenuItem[],
[menuPins, onPress]
[menuPins, onLongPress, onPress]
);
const renderItem = React.useCallback(({ item }: { item: SideMenuItem }) => {

View File

@@ -40,6 +40,7 @@ import {
useSideMenuNotebookTreeStore
} from "./stores";
import { LegendList } from "@legendapp/list";
import { useRelationStore } from "../../stores/use-relation-store";
useSideMenuNotebookSelectionStore.setState({
multiSelect: true
});
@@ -52,6 +53,7 @@ export const SideMenuNotebooks = () => {
const [filteredNotebooks, setFilteredNotebooks] = React.useState(notebooks);
const searchTimer = React.useRef<NodeJS.Timeout>(undefined);
const lastQuery = React.useRef<string>(undefined);
const updater = useRelationStore(state => state.updater);
const loadRootNotebooks = React.useCallback(async () => {
if (!filteredNotebooks) return;
const _notebooks: Notebook[] = [];
@@ -66,9 +68,6 @@ export const SideMenuNotebooks = () => {
const updateNotebooks = React.useCallback(() => {
if (lastQuery.current) {
// useSideMenuNotebookTreeStore.setState({
// isSearching: true
// });
db.lookup
.notebooks(lastQuery.current)
.sorted(db.settings.getGroupOptions("notebooks"))
@@ -76,16 +75,13 @@ export const SideMenuNotebooks = () => {
setFilteredNotebooks(filtered);
});
} else {
// useSideMenuNotebookTreeStore.setState({
// isSearching: false
// });
setFilteredNotebooks(notebooks);
}
}, [notebooks]);
useEffect(() => {
updateNotebooks();
}, [updateNotebooks]);
}, [updateNotebooks,updater]);
useEffect(() => {
(async () => {

View File

@@ -38,6 +38,7 @@ import { SideMenuHeader } from "./side-menu-header";
import { SideMenuListEmpty } from "./side-menu-list-empty";
import { useSideMenuTagsSelectionStore } from "./stores";
import { LegendList, LegendListRenderItemProps } from "@legendapp/list";
import { useRelationStore } from "../../stores/use-relation-store";
const TagItem = (props: {
tags: VirtualizedGrouping<Tag>;
@@ -55,12 +56,13 @@ const TagItem = (props: {
const totalNotes = useTotalNotes("tag");
const totalNotesRef = React.useRef(totalNotes);
totalNotesRef.current = totalNotes;
const updater = useRelationStore(state => state.updater);
useEffect(() => {
if (item?.id) {
totalNotesRef.current?.getTotalNotes([item?.id]);
}
}, [item]);
}, [item, updater]);
return (
<View

View File

@@ -38,10 +38,10 @@ import {
import { getElevationStyle } from "../../utils/elevation";
import { eHideToast, eShowToast } from "../../utils/events";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
export const Toast = ({ context = "global" }) => {
const { colors, isDark } = useThemeColors();
@@ -119,11 +119,7 @@ export const Toast = ({ context = "global" }) => {
width: DDS.isTab ? dimensions.width / 2 : "100%",
alignItems: "center",
alignSelf: "center",
bottom:
Platform.OS === "android"
? Math.max(insets.bottom, 40)
: Math.max(insets.bottom, 40) +
(keyboard.keyboardShown ? keyboard.keyboardHeight : 0),
bottom: insets.bottom + 15,
position: "absolute",
zIndex: 999,
elevation: 15
@@ -158,22 +154,22 @@ export const Toast = ({ context = "global" }) => {
toastOptions.icon
? toastOptions.icon
: toastOptions.type === "success"
? "check"
: toastOptions.type === "info"
? "information"
: "close"
? "check"
: toastOptions.type === "info"
? "information"
: "close"
}
size={isFullToastMessage ? AppFontSize.xxxl : AppFontSize.xl}
color={
toastOptions?.icon
? toastOptions?.icon
: toastOptions.type === "error"
? colors.error.icon
: toastOptions.type === "info"
? isDark
? colors.static.white
: colors.static.black
: colors.success.icon
? colors.error.icon
: toastOptions.type === "info"
? isDark
? colors.static.white
: colors.static.black
: colors.success.icon
}
/>

View File

@@ -0,0 +1,408 @@
/*
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, { RefObject, useEffect, useMemo, useState } from "react";
import {
ColorValue,
TextInput,
TextInputProps,
TouchableOpacity,
View,
ViewStyle
} from "react-native";
import isEmail from "validator/lib/isEmail";
import isURL from "validator/lib/isURL";
import { useThemeColors } from "@notesnook/theme";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { IconButton } from "../icon-button";
import Paragraph from "../typography/paragraph";
import AppIcon from "../AppIcon";
export type FormValues = Record<string, string>;
export type FormErrors = Partial<Record<string, string>>;
export type FieldValidator = (
value: string,
values: FormValues
) => string | undefined;
export type ValidationSchema = Partial<Record<string, FieldValidator[]>>;
export interface FormRef {
values: FormValues;
errors: FormErrors;
setValue: (name: string, value: string) => void;
getValue: (name: string) => string;
getValues: () => FormValues;
setError: (name: string, error?: string) => void;
getError: (name: string) => string | undefined;
clearErrors: () => void;
registerField: (name: string, validators?: FieldValidator[]) => void;
unregisterField: (name: string) => void;
validateField: (name: string) => string | undefined;
validate: () => boolean;
subscribe: (listener: () => void) => () => void;
}
export function createFormRef(initialValues: FormValues = {}): FormRef {
const listeners = new Set<() => void>();
const values: FormValues = { ...initialValues };
const errors: FormErrors = {};
const schema: ValidationSchema = {};
const notify = () => {
listeners.forEach((listener) => listener());
};
return {
values,
errors,
setValue(name, value) {
values[name] = value;
if (errors[name]) {
delete errors[name];
notify();
}
},
getValue(name) {
return values[name] ?? "";
},
getValues() {
return { ...values };
},
setError(name, error) {
if (!error) {
delete errors[name];
} else {
errors[name] = error;
}
notify();
},
getError(name) {
return errors[name];
},
clearErrors() {
Object.keys(errors).forEach((key) => delete errors[key]);
notify();
},
registerField(name, validators = []) {
schema[name] = validators;
if (values[name] === undefined) values[name] = "";
},
unregisterField(name) {
delete schema[name];
delete errors[name];
notify();
},
validateField(name) {
const fieldValidators = schema[name] || [];
const value = values[name] ?? "";
for (const validator of fieldValidators) {
const error = validator(value, values);
if (error) {
errors[name] = error;
notify();
return error;
}
}
delete errors[name];
notify();
return undefined;
},
validate() {
const nextErrors = validateForm(values, schema);
Object.keys(errors).forEach((key) => delete errors[key]);
Object.keys(nextErrors).forEach((key) => {
errors[key] = nextErrors[key];
});
notify();
return !hasFormErrors(nextErrors);
},
subscribe(listener) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
};
}
export function validateForm(
values: FormValues,
schema: ValidationSchema
): FormErrors {
const errors: FormErrors = {};
Object.keys(schema).forEach((field) => {
const fieldValidators = schema[field] || [];
const value = values[field] ?? "";
for (const validator of fieldValidators) {
const error = validator(value, values);
if (error) {
errors[field] = error;
break;
}
}
});
return errors;
}
export function hasFormErrors(errors: FormErrors) {
return Object.keys(errors).length > 0;
}
export const validators = {
required:
(message = "This field is required") =>
(value: string) =>
value?.trim() ? undefined : message,
email:
(message = "Please enter a valid email") =>
(value: string) =>
!value?.trim() || isEmail(value.trim()) ? undefined : message,
minLength: (length: number, message?: string) => (value: string) =>
!value || value.length >= length
? undefined
: message || `Must be at least ${length} characters`,
url:
(message = "Please enter a valid URL") =>
(value: string) =>
!value?.trim() || isURL(value.trim(), { allow_underscores: true })
? undefined
: message,
matchField:
(fieldName: string, message = "Values do not match") =>
(value: string, values: FormValues) =>
value === values[fieldName] ? undefined : message
};
interface FormInputProps extends TextInputProps {
name: string;
formRef: RefObject<FormRef>;
validators?: FieldValidator[];
fwdRef?: RefObject<TextInput | null>;
loading?: boolean;
error?: string;
customColor?: ColorValue;
marginBottom?: number;
marginRight?: number;
button?: {
icon: string;
color: ColorValue;
onPress: () => void;
testID?: string;
size?: number;
};
buttons?: React.ReactNode;
buttonLeft?: React.ReactNode;
height?: number;
fontSize?: number;
inputStyle?: TextInputProps["style"];
containerStyle?: ViewStyle;
wrapperStyle?: ViewStyle;
}
export function FormInput({
name,
formRef,
validators: fieldValidators = [],
fwdRef,
loading,
error,
secureTextEntry,
customColor,
marginBottom = 10,
marginRight,
button,
buttonLeft,
buttons,
height = 45,
fontSize = AppFontSize.sm,
inputStyle = {},
containerStyle = {},
wrapperStyle = {},
onFocus,
onBlur,
onPress,
onChangeText,
...restProps
}: FormInputProps) {
const { colors, isDark } = useThemeColors();
const [focused, setFocused] = useState(false);
const [secureEntry, setSecureEntry] = useState(true);
const [, setVersion] = useState(0);
useEffect(() => {
const form = formRef.current;
form.registerField(name, fieldValidators);
const unsubscribe = form.subscribe(() => {
setVersion((v) => v + 1);
});
return () => {
unsubscribe();
form.unregisterField(name);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formRef, name]);
const value = formRef.current.getValue(name);
const fieldError = error || formRef.current.getError(name);
const borderColor = useMemo(() => {
if (fieldError) return colors.error.accent;
if (focused) return customColor || colors.selected.border;
return colors.primary.border;
}, [colors, customColor, fieldError, focused]);
const style: ViewStyle = {
borderWidth: 1,
borderRadius: defaultBorderRadius,
borderColor,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
...containerStyle
};
const textStyle: TextInputProps["style"] = {
paddingHorizontal: 0,
fontSize,
color:
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
flexGrow: 1,
flexShrink: 1,
paddingBottom: DefaultAppStyles.GAP_VERTICAL,
paddingTop: DefaultAppStyles.GAP_VERTICAL,
fontFamily: "Inter-Regular",
...(inputStyle as ViewStyle)
};
const handleChangeText = (nextValue: string) => {
formRef.current.setValue(name, nextValue);
onChangeText?.(nextValue);
};
return (
<View
importantForAccessibility="yes"
style={{
marginBottom,
marginRight,
...wrapperStyle
}}
>
<TouchableOpacity
disabled={!loading}
onPress={onPress}
activeOpacity={1}
style={style}
>
{buttonLeft}
<TextInput
{...restProps}
defaultValue={value}
ref={fwdRef}
editable={!loading && restProps.editable !== false}
onChangeText={handleChangeText}
onFocus={(e) => {
setFocused(true);
onFocus?.(e);
}}
onBlur={(e) => {
setFocused(false);
onBlur?.(e);
}}
keyboardAppearance={isDark ? "dark" : "light"}
style={textStyle}
secureTextEntry={secureTextEntry && secureEntry}
placeholderTextColor={colors.primary.placeholder}
/>
<View
style={{
flexDirection: "row",
justifyContent: "center",
height: 35 > height ? height : 35,
alignItems: "center"
}}
>
{secureTextEntry && (
<IconButton
name="eye"
size={20}
top={10}
bottom={10}
onPress={() => {
fwdRef?.current?.blur();
setSecureEntry(!secureEntry);
}}
style={{
width: 25,
marginLeft: 5
}}
color={
secureEntry ? colors.secondary.icon : colors.primary.accent
}
/>
)}
{buttons}
{button && (
<IconButton
testID={button.testID}
name={button.icon}
size={button.size || AppFontSize.xl}
top={10}
bottom={10}
onPress={button.onPress}
color={button.color}
style={{
marginRight: -8
}}
/>
)}
</View>
</TouchableOpacity>
{fieldError ? (
<Paragraph
size={AppFontSize.xs}
style={{ marginTop: 5, color: colors.error.icon }}
>
<AppIcon
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{fieldError}
</Paragraph>
) : null}
</View>
);
}
export default FormInput;

View File

@@ -121,8 +121,8 @@ const Input = ({
const color = error
? colors.error.border
: focus
? customColor || colors.selected.border
: colors.primary.border;
? customColor || colors.selected.border
: colors.primary.border;
const validate = async (value: string) => {
if (!validationType) return;

View File

@@ -83,12 +83,13 @@ const SheetWrapper = ({
: 0
};
}, [
colors.primary.background,
colors.primary.border,
largeTablet,
smallTablet,
width,
insets.bottom
colors.primary.background,
colors.primary.border,
bottomInsets,
isGestureNavigationEnabled
]);
const _onOpen = () => {

View File

@@ -17,33 +17,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { SubscriptionPlan } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Linking, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import {
COMMUNITY_SVG,
LAUNCH_ROCKET,
SUPPORT_SVG,
WELCOME_SVG
} from "../../assets/images/assets";
import useRotator from "../../hooks/use-rotator";
import { eSendEvent } from "../../services/event-manager";
import { getContainerBorder } from "../../utils/colors";
import { getElevationStyle } from "../../utils/elevation";
import { eOpenAddNotebookDialog } from "../../utils/events";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
import { WELCOME_SVG } from "../../assets/images/assets";
import { useUserStore } from "../../stores/use-user-store";
import { planToId, SubscriptionPlan } from "@notesnook/core";
import { planToDisplayName } from "../../utils/constants";
import AppIcon from "../ui/AppIcon";
import { SvgView } from "../ui/svg";
export type TStep = {
text?: string;

View File

@@ -33,7 +33,7 @@ import { useThemeColors } from "@notesnook/theme";
import { DisplayedNotification } from "@notifee/react-native";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { InteractionManager, Platform, View } from "react-native";
import { InteractionManager, Platform } from "react-native";
import Share from "react-native-share";
import { DatabaseLogger, db } from "../common/database";
import { AttachmentDialog } from "../components/attachments";
@@ -72,8 +72,8 @@ import { useUserStore } from "../stores/use-user-store";
import { eCloseSheet, eUpdateNoteInEditor } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import DatePickerComponent from "../components/date-picker";
@@ -196,7 +196,7 @@ export const useActions = ({
"expiringNotes"
]);
const [item, setItem] = useState(propItem);
const { colors, isDark } = useThemeColors();
const { colors } = useThemeColors();
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
db.shortcuts.exists(item.id)
@@ -368,7 +368,14 @@ export const useActions = ({
inputPlaceholder: strings.name(),
defaultValue: item.title,
positivePress: async (value) => {
if (!value || value.trim().length === 0) return;
if (!value || value.trim().length === 0) {
ToastManager.error(
new Error(strings.nameIsRequired()),
undefined,
"local"
);
return;
}
await db.colors.add({
id: item.id,
title: value
@@ -382,6 +389,16 @@ export const useActions = ({
}
const deleteItem = async () => {
if (isPublished) {
ToastManager.show({
heading: strings.notePublished(),
message: strings.unpublishToDelete(),
type: "error",
context: "local"
});
return;
}
close();
await sleep(300);
@@ -1237,7 +1254,8 @@ export const useActions = ({
: strings.moveToTrash(),
icon: "delete-outline",
type: "error",
onPress: deleteItem
onPress: deleteItem,
locked: isPublished
});
}
@@ -1275,7 +1293,10 @@ export const useActions = ({
(item as Note).headline || (item as Notebook).description || "",
(item as Color).colorCode
);
} catch (e) {}
} catch (e) {
/**
empty */
}
}
});
}

View File

@@ -30,6 +30,7 @@ import {
import { strings } from "@notesnook/intl";
import notifee from "@notifee/react-native";
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
import dayjs from "dayjs";
import React, { useCallback, useEffect, useRef } from "react";
import {
AppState,
@@ -48,6 +49,7 @@ import * as RNIap from "react-native-iap";
import { DatabaseLogger, db, setupDatabase } from "../common/database";
import { initializeLogger } from "../common/database/logger";
import { MMKV } from "../common/database/mmkv";
import { deleteDCacheFiles } from "../common/filesystem/io";
import { endProgress, startProgress } from "../components/dialogs/progress";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
@@ -58,11 +60,7 @@ import {
resetTabStore,
useTabStore
} from "../screens/editor/tiptap/use-tab-store";
import {
clearAppState,
editorController,
editorState
} from "../screens/editor/tiptap/utils";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
import { useDragState } from "../screens/settings/editor/state";
import BackupService from "../services/backup";
import BiometricService from "../services/biometrics";
@@ -89,6 +87,7 @@ import { clearAllStores, initAfterSync } from "../stores";
import { refreshAllStores } from "../stores/create-db-collection-store";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useMessageStore } from "../stores/use-message-store";
import { useRelationStore } from "../stores/use-relation-store";
import { useSettingStore } from "../stores/use-setting-store";
import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors";
@@ -96,6 +95,7 @@ import { BETA } from "../utils/constants";
import {
eAfterSync,
eCloseSheet,
eCloseSimpleDialog,
eEditorReset,
eLoginSessionExpired,
eOnLoadNote,
@@ -105,12 +105,8 @@ import {
} from "../utils/events";
import { getGithubVersion } from "../utils/github-version";
import { fluidTabsRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import useFeatureManager from "./use-feature-manager";
import { deleteDCacheFiles } from "../common/filesystem/io";
import dayjs from "dayjs";
import { useRelationStore } from "../stores/use-relation-store";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -173,7 +169,6 @@ const onAppOpenedFromURL = async (event: {
if (url.startsWith("https://app.notesnook.com/account/verified")) {
await onUserEmailVerified();
} else if (url.startsWith("ShareMedia://QuickNoteWidget")) {
clearAppState();
editorState().movedAway = false;
eSendEvent(eOnLoadNote, { newNote: true });
fluidTabsRef.current?.goToPage("editor", false);
@@ -292,7 +287,10 @@ const onUserSubscriptionStatusChanged = async (
subscription: subscription
}
});
Walkthrough.present("prouser", false, true);
eSendEvent(eCloseSimpleDialog);
setTimeout(() => {
Walkthrough.present("prouser", false, true);
}, 500);
}
await PremiumService.setPremiumStatus();
useMessageStore.getState().setAnnouncement();
@@ -358,23 +356,9 @@ async function checkForShareExtensionLaunchedInBackground() {
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened");
}
} catch (e) {}
}
async function saveEditorState() {
if (!editorState().movedAway) {
const id = useTabStore.getState().getCurrentNoteId();
const note = id ? await db.notes.note(id) : undefined;
const locked = note && (await db.vaults.itemExists(note));
if (locked) return;
const state = JSON.stringify({
editing: editorState().currentlyEditing,
movedAway: editorState().movedAway,
timestamp: Date.now()
});
NotesnookModule.setAppState(state);
} else {
NotesnookModule.setAppState("");
} catch (e) {
/**
empty */
}
}
@@ -592,13 +576,13 @@ export const useAppEvents = () => {
}, [isAppLoading, onSyncComplete]);
useEffect(() => {
if (initialUrl) {
if (initialUrl && !isAppLoading) {
onAppOpenedFromURL({
url: initialUrl!,
isInitialUrl: true
});
}
}, [initialUrl]);
}, [initialUrl, isAppLoading]);
const subscribeToPurchaseListeners = useCallback(async () => {
if (Platform.OS === "android") {
@@ -838,7 +822,6 @@ export const useAppEvents = () => {
Sync.run("global", false, "full");
reconnectSSE();
await checkForShareExtensionLaunchedInBackground();
NotesnookModule.setAppState("");
let user = await db.user.getUser();
if (user && !user?.isEmailConfirmed) {
try {
@@ -861,7 +844,6 @@ export const useAppEvents = () => {
eSendEvent(eEditorReset);
}
} else {
await saveEditorState();
if (
SettingsService.canLockAppInBackground() &&
!useSettingStore.getState().requestBiometrics &&

View File

@@ -135,7 +135,7 @@ export const useNoteLocked = (noteId: string | undefined) => {
//@ts-ignore
!globalThis["IS_SHARE_EXTENSION"]
) {
let unsub = useSettingStore.subscribe((state) => {
const unsub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
unsub();
db.vaults

View File

@@ -1,15 +1,28 @@
/*
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 { useAreFeaturesAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useEffect } from "react";
import { db } from "../common/database";
import { presentDialog } from "../components/dialog/functions";
import { useDragState } from "../screens/settings/editor/state";
import { eSendEvent } from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
import SettingsService from "../services/settings";
import { useUserStore } from "../stores/use-user-store";
import { eCloseSimpleDialog } from "../utils/events";
export default function useFeatureManager() {
const features = useAreFeaturesAvailable([
@@ -24,11 +37,10 @@ export default function useFeatureManager() {
"disableTrashCleanup",
"fullOfflineMode"
]);
const user = useUserStore((state) => state.user);
const plan = useUserStore((state) => state.user?.subscription?.plan);
useEffect(() => {
if (!user || !features) return;
if (!useUserStore.getState().user || !features) return;
if (!features.createNoteFromNotificationDrawer.isAllowed) {
SettingsService.setProperty("notifNotes", false);
@@ -69,6 +81,7 @@ export default function useFeatureManager() {
db.settings.setDefaultTag(undefined);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [features, plan]);
return true;

View File

@@ -20,14 +20,27 @@ import { ItemType } from "@notesnook/core";
import { useSettingStore } from "../stores/use-setting-store";
export function useIsCompactModeEnabled(dataType: ItemType) {
const [notebooksListMode, notesListMode] = useSettingStore((state) => [
state.settings.notebooksListMode,
state.settings.notesListMode
]);
const [notebooksListMode, notesListMode, searchListMode] = useSettingStore(
(state) => [
state.settings.notebooksListMode,
state.settings.notesListMode,
state.settings.searchListMode
]
);
if (dataType !== "note" && dataType !== "notebook") return false;
if (
dataType !== "note" &&
dataType !== "notebook" &&
dataType !== "searchResult"
)
return false;
const listMode = dataType === "notebook" ? notebooksListMode : notesListMode;
const listMode =
dataType === "notebook"
? notebooksListMode
: dataType === "searchResult"
? searchListMode
: notesListMode;
return listMode === "compact";
}

View File

@@ -16,16 +16,17 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Plan, SubscriptionPlan, SubscriptionPlanId } from "@notesnook/core";
import { useEffect, useState } from "react";
import { Plan, SubscriptionPlan } from "@notesnook/core";
import React, { useEffect, useState } from "react";
import { useAsync } from "react-async-hook";
import { Platform } from "react-native";
import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import { DatabaseLogger, db } from "../common/database";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
import { useSettingStore } from "../stores/use-setting-store";
import { useUserStore } from "../stores/use-user-store";
import SettingsService from "../services/settings";
function numberWithCommas(x: string) {
const parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -155,9 +156,18 @@ const planIdToIndex = (planId: string) => {
return planIndex;
};
let WebPlanCache: Plan[];
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const usePricingPlans = (options?: PricingPlansOptions) => {
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const user = useUserStore((state) => state.user);
const regionalDiscount = useAsync(
() =>
db.pricing.sku(
Platform.OS === "android" ? "google" : "apple",
"yearly",
"pro"
),
[]
);
const [currentPlan, setCurrentPlan] = useState<string>(
options?.planId || pricingPlans[2].id
);
@@ -172,17 +182,20 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
const [userCanRequestTrial, setUserCanRequestTrial] = useState(false);
const [webPricingPlans, setWebPricingPlans] = useState<Plan[]>([]);
const getProduct = (planId: string, skuId: string) => {
if (isGithubRelease)
return webPricingPlans.find(
(plan) => planIdToIndex(planId) === plan.plan && skuId === plan.period
);
const getProduct = React.useCallback(
(planId: string, skuId: string) => {
if (isGithubRelease)
return webPricingPlans.find(
(plan) => planIdToIndex(planId) === plan.plan && skuId === plan.period
);
return (
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
plans.find((p) => p.id === planId)?.products?.[skuId]
);
};
return (
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
plans.find((p) => p.id === planId)?.products?.[skuId]
);
},
[plans, webPricingPlans]
);
const getProductAndroid = (planId: string, skuId: string) => {
if (isGithubRelease)
@@ -196,37 +209,45 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
return getProduct(planId, skuId) as RNIap.SubscriptionIOS;
};
const hasTrialOffer = (planId?: string, productId?: string) => {
if (!selectedProductSku && !productId) return false;
const hasTrialOffer = React.useCallback(
(planId?: string, productId?: string) => {
if (!selectedProductSku && !productId) return false;
if (productId?.includes("5year")) return false;
if (isGithubRelease) {
if (
user?.subscription?.trialsAvailed?.some(
(plan) => plan === planIdToIndex(planId || currentPlan)
)
) {
return false;
} else {
return true;
if (productId?.includes("5year")) return false;
if (isGithubRelease) {
if (
user?.subscription?.trialsAvailed?.some(
(plan) => plan === planIdToIndex(planId || currentPlan)
)
) {
return false;
} else {
return true;
}
}
}
return Platform.OS === "ios"
? (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionIOS
)?.introductoryPricePaymentModeIOS === "FREETRIAL"
: (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionAndroid
)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList
?.length > 1;
};
return Platform.OS === "ios"
? (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionIOS
)?.introductoryPricePaymentModeIOS === "FREETRIAL"
: (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionAndroid
)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList
?.length > 1;
},
[
currentPlan,
getProduct,
selectedProductSku,
user?.subscription?.trialsAvailed
]
);
// user && (!user.subscription || !user.subscription.expiry) ? true : false;
@@ -255,12 +276,15 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
const products = WebPlanCache || (await db.pricing.products());
WebPlanCache = products;
setWebPricingPlans(products);
} catch (e) {}
} catch (e) {
/**
empty */
}
}
setLoadingPlans(false);
};
loadPlans();
}, [options?.promoOffer, cancelPromo]);
}, [options?.promoOffer, cancelPromo, hasTrialOffer]);
function getLocalizedPrice(
product: RNIap.Subscription | RNIap.Product | Plan
@@ -276,8 +300,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(product as Plan).period === "yearly"
? (product as Plan).price.gross
: (product as Plan).period === "5-year"
? (product as Plan).price.gross
: (product as Plan).price.gross
? (product as Plan).price.gross
: (product as Plan).price.gross
}`;
}
@@ -432,8 +456,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
return period.endsWith("W")
? "week"
: period.endsWith("M")
? "month"
: "year";
? "month"
: "year";
} else {
const unit = (product as RNIap.SubscriptionIOS)
?.subscriptionPeriodUnitIOS;
@@ -475,8 +499,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
type: phase.billingPeriod.endsWith("W")
? "week"
: phase.billingPeriod.endsWith("M")
? "month"
: "year"
? "month"
: "year"
};
} else {
const productIos = product as RNIap.SubscriptionIOS;
@@ -530,14 +554,15 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
const formattedPrice = numberWithCommas(monthlyPrice.toFixed(2));
return isAtLeft
? `${symbol} ${formattedPrice}`
: `${formattedPrice} ${symbol}`;
? `${symbol}${formattedPrice}`
: `${formattedPrice}${symbol}`;
};
const getDiscountValue = (p1: string, p2: string, splitToMonth?: boolean) => {
let price1 = Platform.OS === "ios" ? parseInt(p1) : parseInt(p1) / 1000000;
let price1 =
Platform.OS === "ios" ? parseFloat(p1) : parseFloat(p1) / 1000000;
const price2 =
Platform.OS === "ios" ? parseInt(p2) : parseInt(p2) / 1000000;
Platform.OS === "ios" ? parseFloat(p2) : parseFloat(p2) / 1000000;
price1 = splitToMonth ? price1 / 12 : price1;
@@ -587,7 +612,7 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
} else {
priceValue = price / 1000000;
}
const priceSymbol = localizedPrice.replace(/[\s\d,.]+/, "");
const priceSymbol = localizedPrice.replace(/[\d,.]+/, "");
return { priceValue, priceSymbol, localizedPrice };
};
@@ -607,8 +632,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(product as Plan).period === "yearly"
? ((product as Plan).price.gross / 12).toFixed(2)
: (product as Plan).period === "5-year"
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
: (product as Plan).price.gross
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
: (product as Plan).price.gross
}`;
}
@@ -645,21 +670,6 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
);
};
async function getRegionalDiscount(plan: string, productId: string) {
if (productId !== "notesnook.pro.yearly") {
return;
}
try {
return await db.pricing.sku(
Platform.OS === "android" ? "google" : "apple",
"yearly",
plan as SubscriptionPlanId
);
} catch (e) {
console.log(e);
}
}
function isSubscribedToPlan(planId: string) {
if (!PremiumService.get()) return false;
return user?.subscription?.productId?.includes(planId);
@@ -730,7 +740,7 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(plan) => plan.plan === planIndex && plan.period === period
);
},
getRegionalDiscount,
regionalDiscount: regionalDiscount.result,
isGithubRelease: isGithubRelease,
isSubscribed: () => user?.subscription?.plan !== SubscriptionPlan.FREE,
finish: () => options?.onBuy?.()

View File

@@ -21,7 +21,6 @@ import { useEffect } from "react";
import { NativeEventEmitter, NativeModule } from "react-native";
import { useRef } from "react";
import { Platform } from "react-native";
import { Linking } from "react-native";
import deviceInfoModule from "react-native-device-info";
import { strings } from "@notesnook/intl";
const ShortcutsEmitter = new NativeEventEmitter(
@@ -54,14 +53,9 @@ export const useShortcutManager = ({
}, [shortcuts]);
useEffect(() => {
Linking.getInitialURL().then((url) => {
if (url?.startsWith("ShareMedia://QuickNoteWidget")) {
onShortcutPressed(defaultShortcuts[0]);
}
});
if (!isSupported()) return;
Shortcuts.getInitialShortcut().then((shortcut) => {
if (initialShortcutRecieved.current) return;
if (initialShortcutRecieved.current || !shortcut) return;
onShortcutPressed(shortcut);
initialShortcutRecieved.current = true;
});

View File

@@ -32,6 +32,8 @@ const useTimer = (initialId?: string) => {
const [id, setId] = useState(initialId);
const [seconds, setSeconds] = useState(getSecondsLeft(id));
const interval = useRef<NodeJS.Timeout>(undefined);
const secondsRef = useRef(seconds);
secondsRef.current = seconds;
const start = (sec: number, currentId = id) => {
if (!currentId) return;
@@ -59,7 +61,7 @@ const useTimer = (initialId?: string) => {
}
};
return { seconds, setId, start, reset };
return { seconds, setId, start, reset, secondsRef: secondsRef };
};
export default useTimer;

View File

@@ -47,12 +47,7 @@ import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
import { useShortcutManager } from "../hooks/use-shortcut-manager";
import { hideAllTooltips } from "../hooks/use-tooltip";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
clearAppState,
editorController,
editorState,
getAppState
} from "../screens/editor/tiptap/utils";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
import { DDS } from "../services/device-detection";
import {
eSendEvent,
@@ -118,13 +113,9 @@ export const FluidPanelsView = React.memo(
useShortcutManager({
onShortcutPressed: async (item) => {
if (!item && getAppState()) {
editorState().movedAway = false;
fluidTabsRef.current?.goToPage("editor", false);
return;
}
if (!item) return;
if (item?.type === "notesnook.action.newnote") {
clearAppState();
if (!fluidTabsRef.current) {
setTimeout(() => {
eSendEvent(eOnLoadNote, { newNote: true });
@@ -148,7 +139,7 @@ export const FluidPanelsView = React.memo(
if (deviceMode === "smallTablet") {
fluidTabsRef.current?.openDrawer(false);
}
}, [deviceMode, dimensions.width, setFullscreen]);
}, [deviceMode, setFullscreen]);
const closeFullScreenEditor = useCallback(
(current: string) => {
@@ -167,7 +158,7 @@ export const FluidPanelsView = React.memo(
fluidTabsRef.current?.goToIndex(2, false);
}
},
[deviceMode, dimensions.width, setFullscreen]
[deviceMode, setFullscreen]
);
const toggleView = useCallback(
@@ -205,7 +196,6 @@ export const FluidPanelsView = React.memo(
eSendEvent(eCloseFullscreenEditor, current);
}
const state = getAppState();
setTimeout(() => {
switch (current) {
case "tablet":
@@ -217,23 +207,15 @@ export const FluidPanelsView = React.memo(
}
break;
case "mobile":
if (
state &&
editorState().movedAway === false &&
useTabStore.getState().getCurrentNoteId()
) {
fluidTabsRef.current?.goToPage("editor", false);
} else {
fluidTabsRef.current?.goToPage(
fluidTabsRef.current?.page(),
false
);
}
fluidTabsRef.current?.goToPage(
fluidTabsRef.current?.page(),
false
);
break;
}
}, 0);
},
[deviceMode, fullscreen, setDeviceModeState]
[fullscreen, setDeviceModeState]
);
const checkDeviceType = React.useCallback(
@@ -243,18 +225,18 @@ export const FluidPanelsView = React.memo(
const nextDeviceMode = DDS.isLargeTablet()
? "tablet"
: DDS.isSmallTab
? "smallTablet"
: "mobile";
? "smallTablet"
: "mobile";
setDeviceMode(nextDeviceMode, size);
},
[orientation, setDeviceMode, setDimensions]
[orientation, setDeviceMode]
);
useEffect(() => {
if (orientation !== "UNKNOWN") {
checkDeviceType(dimensions);
}
}, [orientation, dimensions]);
}, [orientation, dimensions, checkDeviceType]);
const _onLayout = React.useCallback(
(event: LayoutChangeEvent) => {
@@ -269,13 +251,7 @@ export const FluidPanelsView = React.memo(
setOrientation(OrientationType["PORTRAIT"]);
}
},
[
checkDeviceType,
deviceMode,
dimensions.width,
orientation,
setDeviceMode
]
[setDimensions]
);
const PANE_OFFSET = useMemo(

View File

@@ -16,7 +16,6 @@ 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 { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

View File

@@ -35,7 +35,6 @@ import { db } from "../../common/database";
import { Dialog } from "../../components/dialog";
import { Header } from "../../components/header";
import { Button } from "../../components/ui/button";
import Input from "../../components/ui/input";
import { ReminderTime } from "../../components/ui/reminder-time";
import Paragraph from "../../components/ui/typography/paragraph";
import { DDS } from "../../services/device-detection";
@@ -59,6 +58,10 @@ import { TimeSince } from "../../components/ui/time-since";
import Heading from "../../components/ui/typography/heading";
import { eOnLoadNote } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
import FormInput, {
createFormRef,
validators
} from "../../components/ui/input/form-input";
const ReminderModes =
Platform.OS === "ios"
@@ -113,7 +116,12 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const [repeatFrequency, setRepeatFrequency] = useState(1);
const referencedItem = reference ? (reference as Note) : null;
const recurringReminderFeature = useIsFeatureAvailable("recurringReminders");
const formRef = useRef(
createFormRef({
title: reminder?.title || referencedItem?.title || "",
description: reminder?.description || referencedItem?.headline || ""
})
);
const title = useRef<string | undefined>(
!reminder ? referencedItem?.title : reminder?.title
);
@@ -172,9 +180,15 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
async function saveReminder() {
try {
if (!(await Notifications.checkAndRequestPermissions(true)))
throw new Error(strings.noNotificationPermission());
if (!date && reminderMode !== ReminderModes.Permanent) return;
if (!formRef.current.validate()) return;
if (
date.getTime() < Date.now() &&
reminderMode === "once" &&
!props.route.params.reminder
) {
throw new Error(strings.dateError());
}
if (
reminderMode === ReminderModes.Repeat &&
recurringMode !== "day" &&
@@ -183,14 +197,9 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
)
throw new Error(strings.selectDayError());
if (!title.current) throw new Error(strings.setTitleError());
if (
date.getTime() < Date.now() &&
reminderMode === "once" &&
!props.route.params.reminder
) {
throw new Error(strings.dateError());
}
if (!(await Notifications.checkAndRequestPermissions(true)))
throw new Error(strings.noNotificationPermission());
if (!date && reminderMode !== ReminderModes.Permanent) return;
date.setSeconds(0, 0);
@@ -261,7 +270,10 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
>
<Input
<FormInput
name="title"
validators={[validators.required(strings.titleIsRequired())]}
formRef={formRef}
fwdRef={titleRef}
defaultValue={reminder?.title || referencedItem?.title}
placeholder={strings.remindeMeOf()}
@@ -270,12 +282,15 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
wrapperStyle={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
onSubmit={() => {
onSubmitEditing={() => {
descriptionRef.current?.focus();
}}
/>
<Input
<FormInput
name="description"
validators={[]}
formRef={formRef}
defaultValue={
reminder ? reminder?.description : referencedItem?.headline
}
@@ -400,17 +415,45 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
recurringMode === RecurringModes.Year
? null
: recurringMode === RecurringModes.Week
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
<Button
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
type={
selectedDays.indexOf(item) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
return [...days];
}
days.push(item);
return [...days];
});
}}
/>
)
)
: MonthDays.map((item, index) => (
<Button
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
key={index + "monthday"}
title={index + 1 + ""}
type={
selectedDays.indexOf(item) > -1
selectedDays.indexOf(index + 1) > -1
? "selected"
: "plain"
}
@@ -422,44 +465,16 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
if (days.indexOf(index + 1) > -1) {
days.splice(days.indexOf(index + 1), 1);
return [...days];
}
days.push(item);
days.push(index + 1);
return [...days];
});
}}
/>
)
)
: MonthDays.map((item, index) => (
<Button
key={index + "monthday"}
title={index + 1 + ""}
type={
selectedDays.indexOf(index + 1) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(index + 1) > -1) {
days.splice(days.indexOf(index + 1), 1);
return [...days];
}
days.push(index + 1);
return [...days];
});
}}
/>
))}
))}
</ScrollView>
</View>
) : null}
@@ -542,24 +557,24 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
dayjs(date).format("hh:mm A")
)
: recurringMode === RecurringModes.Year
? strings.reminderRepeatStrings.year(
dayjs(date).format("dddd, MMMM D, h:mm A")
)
: selectedDays.length === 7 &&
recurringMode === RecurringModes.Week
? strings.reminderRepeatStrings.week.daily(
dayjs(date).format("hh:mm A")
)
: selectedDays.length === 0
? strings.reminderRepeatStrings[
recurringMode as "week" | "month"
].selectDays()
: strings.reminderRepeatStrings.repeats(
repeatFrequency,
recurringMode as string,
getSelectedDaysText(selectedDays),
dayjs(date).format("hh:mm A")
)}
? strings.reminderRepeatStrings.year(
dayjs(date).format("dddd, MMMM D, h:mm A")
)
: selectedDays.length === 7 &&
recurringMode === RecurringModes.Week
? strings.reminderRepeatStrings.week.daily(
dayjs(date).format("hh:mm A")
)
: selectedDays.length === 0
? strings.reminderRepeatStrings[
recurringMode as "week" | "month"
].selectDays()
: strings.reminderRepeatStrings.repeats(
repeatFrequency,
recurringMode as string,
getSelectedDaysText(selectedDays),
dayjs(date).format("hh:mm A")
)}
</Paragraph>
</>
</View>
@@ -587,8 +602,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
mode === "Silent"
? "minus-circle"
: mode === "Vibrate"
? "vibrate"
: "volume-high"
? "vibrate"
: "volume-high"
}
fontSize={AppFontSize.xs}
height={35}
@@ -636,6 +651,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
<Heading size={AppFontSize.md}>{strings.referencedIn()}</Heading>
{referencedNotes.value.map((item) => (
<Pressable
key={item.id}
style={{
justifyContent: "space-between",
flexDirection: "row",

View File

@@ -70,6 +70,7 @@ export const Archive = ({ navigation, route }: NavigationProps<"Archive">) => {
onRefresh={() => {
refresh();
}}
groupType="archive"
renderedInRoute="Archive"
loading={loading}
placeholder={{

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import Sodium from "@ammarahmed/react-native-sodium";
import { isFeatureAvailable } from "@notesnook/common";
import { isImage } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
DocumentPickerOptions,
@@ -34,6 +33,7 @@ import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import { compressToFile } from "../../../common/filesystem/compress";
import { santizeUri } from "../../../common/filesystem/utils";
import AttachImage from "../../../components/dialogs/attach-image-dialog";
import { ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
@@ -41,7 +41,6 @@ import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
import { santizeUri } from "../../../common/filesystem/utils";
type PickerOptions = {
noteId?: string;
@@ -103,7 +102,7 @@ const file = async (fileOptions: PickerOptions) => {
return;
}
let uri = santizeUri(fileCopyUri.localUri);
const uri = santizeUri(fileCopyUri.localUri);
const hash = await Sodium.hashFile({
uri: uri,
type: "url"

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import type { ToolbarGroupDefinition } from "@notesnook/editor";
import { useEditor } from "./use-editor";
import { FeatureId, FeatureResult } from "@notesnook/common";
import { DayFormat } from "@notesnook/core";
export type useEditorType = ReturnType<typeof useEditor>;

View File

@@ -207,7 +207,7 @@ export const useEditorEvents = (
return () => {
subscriptions.forEach((subscription) => subscription.remove());
};
}, [editor.commands, editor.postMessage]);
}, [editor, editor.commands, editor.postMessage]);
useEffect(() => {
if (loading) return;
if (typeof defaultFontFamily === "object") {
@@ -471,9 +471,8 @@ export const useEditorEvents = (
relationType: "from",
title: strings.dataTypesPluralCamelCase.reminder(),
onAdd: async () => {
const reminderFeature = await isFeatureAvailable(
"activeReminders"
);
const reminderFeature =
await isFeatureAvailable("activeReminders");
if (!reminderFeature.isAllowed) {
ToastManager.show({
type: "info",
@@ -560,7 +559,7 @@ export const useEditorEvents = (
.then((data: any) => {
console.log(
"Got attachment data:",
data,
!!data,
editorMessage.resolverId
);
editor.postMessage(NativeEvents.resolve, {
@@ -765,7 +764,7 @@ export const useEditorEvents = (
let filePath: string;
let fileName: string;
if (Platform.OS === "android") {
let file = await ScopedStorage.createDocument(
const file = await ScopedStorage.createDocument(
"table.csv",
"text/csv",
csv,

View File

@@ -38,15 +38,14 @@ import { strings } from "@notesnook/intl";
import { useThemeEngineStore } from "@notesnook/theme";
import { Mutex } from "async-mutex";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Linking } from "react-native";
import WebView from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
eSubscribeEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { NotePreviewWidget } from "../../../services/note-preview-widget";
@@ -69,15 +68,7 @@ import Commands from "./commands";
import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types";
import { TabSessionItem, syncTabs, useTabStore } from "./use-tab-store";
import {
clearAppState,
defaultState,
getAppState,
isContentInvalid,
isEditorLoaded,
post
} from "./utils";
import { Linking } from "react-native";
import { defaultState, isContentInvalid, isEditorLoaded, post } from "./utils";
const loadNoteMutex = new Mutex();
@@ -167,7 +158,7 @@ export const useEditor = (
const tags = useTagStore((state) => state.items);
const insets = useGlobalSafeAreaInsets();
const isDefaultEditor = editorId === "";
const saveCount = useRef(0);
const saveCount = useRef<Record<string, number>>({});
const lastContentChangeTime = useRef<Record<string, number>>({});
const lock = useRef(false);
const currentLoadingNoteId = useRef<string>(undefined);
@@ -253,9 +244,9 @@ export const useEditor = (
clearTimeout(timers.current["loading-images" + noteId]);
}
saveCount.current = 0;
currentLoadingNoteId.current = undefined;
lock.current = false;
saveCount.current[tabId] = 0;
resetContent && postMessage(NativeEvents.title, "", tabId);
resetContent && (await commands.clearContent(tabId));
resetContent && (await commands.clearTags(tabId));
@@ -436,10 +427,9 @@ export const useEditor = (
lastContentChangeTime.current[id] = note.dateEdited;
if (
saveCount.current < 2 ||
saveCount.current[tabId] < 2 ||
currentNotes.current[id]?.title !== note.title ||
currentNotes.current[id]?.headline?.slice(0, 200) !==
note.headline?.slice(0, 200)
currentNotes.current[id]?.headline !== note.headline
) {
Navigation.queueRoutesForUpdate();
}
@@ -455,7 +445,11 @@ export const useEditor = (
currentNotes.current[id] = note;
}
saveCount.current++;
if (!saveCount.current[tabId]) {
saveCount.current[tabId] = 1;
} else {
saveCount.current[tabId]++;
}
clearTimeout(timers.current.onsave);
timers.current.onsave = setTimeout(async () => {
@@ -564,6 +558,7 @@ export const useEditor = (
useTabStore.getState().newTabSession(tabId, {});
}
}
saveCount.current[tabId] = 0;
setTimeout(() => {
if (state.current?.ready && !state.current.movedAway)
@@ -696,6 +691,7 @@ export const useEditor = (
currentNotes.current[item.id] = item;
editorSessionHistory.newSession(item.id);
saveCount.current[tabId] = 0;
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
@@ -1025,22 +1021,11 @@ export const useEditor = (
);
const restoreEditorState = useCallback(async () => {
const appState = getAppState();
if (!appState) return;
state.current.isRestoringState = true;
state.current.currentlyEditing = true;
if (fluidTabsRef.current?.page() === "editor") {
state.current.movedAway = false;
}
if (!state.current.editorStateRestored) {
state.current.isRestoringState = true;
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor", false);
}
}
clearAppState();
state.current.isRestoringState = false;
}, []);
const onContentChanged = (noteId?: string) => {

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { parseInternalLink } from "@notesnook/core";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { createRef, MutableRefObject, RefObject } from "react";
import { TextInput } from "react-native";
import WebView from "react-native-webview";
@@ -28,10 +29,8 @@ import {
eUnSubscribeEvent
} from "../../../services/event-manager";
import { eOnLoadNote } from "../../../utils/events";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { AppState, EditorState, useEditorType } from "./types";
import { EditorState, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
export const textInput = createRef<TextInput>();
export const editorController =
@@ -141,34 +140,6 @@ export function isContentInvalid(content: string | undefined) {
);
}
const canRestoreAppState = (appState: AppState) => {
return appState.editing && Date.now() < appState.timestamp + 3600000;
};
let appState: AppState | undefined;
export function setAppState(state: AppState) {
appState = state;
}
export function getAppState() {
if (appState && canRestoreAppState(appState)) return appState as AppState;
const json = NotesnookModule.getAppState();
if (json) {
appState = JSON.parse(json) as AppState;
if (canRestoreAppState(appState)) {
return appState;
} else {
clearAppState();
return null;
}
}
return null;
}
export function clearAppState() {
appState = undefined;
NotesnookModule.setAppState("");
}
export async function openInternalLink(url: string) {
const data = parseInternalLink(url);
if (!data?.id) return false;

View File

@@ -70,6 +70,7 @@ export const Favorites = ({
<List
data={favorites}
dataType="note"
groupType="favorites"
onRefresh={() => {
refresh();
}}

View File

@@ -70,6 +70,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
<List
data={notes}
dataType="note"
groupType="home"
renderedInRoute={route.name}
loading={loading || !isFocused}
headerTitle={strings.routes[route.name]()}

View File

@@ -45,7 +45,10 @@ import { AddNotebookSheet } from "../../components/sheets/add-notebook";
import { Notice } from "../../components/ui/notice";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { isFeatureAvailable } from "@notesnook/common";
import { ToastManager } from "../../services/event-manager";
import {
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import PaywallSheet from "../../components/sheets/paywall";
const {
@@ -175,6 +178,7 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
updateNotebook(item.id);
}
}
sendItemUpdateEvent(id, "notebook");
}
Navigation.queueRoutesForUpdate();
@@ -237,9 +241,8 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
button={{
icon: "plus",
onPress: async () => {
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
const notebooksFeature =
await isFeatureAvailable("notebooks");
if (!notebooksFeature.isAllowed) {
ToastManager.show({
message: notebooksFeature.error,
@@ -388,8 +391,8 @@ const NotebookItemWrapper = React.memo(
!selected
? "selected"
: !state.initialState[item.notebook.id]
? undefined
: "deselected"
? undefined
: "deselected"
);
}}
canDisableSelectionMode={false}

View File

@@ -33,7 +33,10 @@ import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { ToastManager } from "../../services/event-manager";
import {
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import {
ItemSelection,
@@ -80,7 +83,10 @@ const useTagItemSelection = createItemSelectionStore(true);
const ManageTags = (props: NavigationProps<"ManageTags">) => {
const { colors } = useThemeColors();
const ids = props.route.params.ids || [];
const ids = React.useMemo(
() => props.route.params.ids || [],
[props.route.params.ids]
);
const [tags, setTags] = useState<Tag[]>();
const [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null);
@@ -121,7 +127,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
tags.splice(0, 0, ...noteTags);
setTags(tags);
},
[]
[ids]
);
const refreshTags = useCallback(() => {
@@ -135,7 +141,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
.sorted(db.settings.getGroupOptions("tags"))
.then(sortAndSetTags);
}
}, [query]);
}, [query, sortAndSetTags]);
useEffect(() => {
refreshTags();
@@ -199,6 +205,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
}
useRelationStore.getState().update();
sendItemUpdateEvent(id, "tag");
useTagStore.getState().refresh();
setQuery(undefined);
} catch (e) {
@@ -242,6 +249,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
console.error(e);
}
}
sendItemUpdateEvent(id, "tag");
useTagStore.getState().refresh();
useRelationStore.getState().update();
refreshTags();
@@ -417,15 +425,15 @@ const TagItem = ({
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
)}

View File

@@ -46,6 +46,7 @@ import {
import {
checkParentSelected,
findRootNotebookId,
getAllNotebookChildren,
getParentNotebookId
} from "../../utils/notebooks";
import { DefaultAppStyles } from "../../utils/styles";
@@ -124,17 +125,25 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
useEffect(() => {
async function filterNotebooks() {
const excludedItems: string[] = [];
const disabledItems: string[] = [];
for (const notebook of selectedNotebooks) {
const children = await getAllNotebookChildren(notebook.id, []);
excludedItems.push(...children);
const parent = await getParentNotebookId(notebook.id);
if (!excludedItems.includes(parent)) {
excludedItems.push(parent);
if (!disabledItems.includes(parent)) {
disabledItems.push(parent);
}
excludedItems.push(notebook.id);
}
const filtered = tree.filter(
(treeItem) => !excludedItems.includes(treeItem.notebook.id)
// Exclude and disable items as needed
const filtered = tree.filter(item => !excludedItems.includes(item.notebook.id)).map(
(treeItem) => {
return {
...treeItem,
disabled: disabledItems.includes(treeItem.notebook.id)
}
}
);
return filtered;
}
filterNotebooks().then((filtered) => {
@@ -149,6 +158,13 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
index={index}
item={item}
onPress={async () => {
if (item.disabled) {
ToastManager.show({
type: "info",
"message": "You cannot move the selected notebook(s) here"
})
return;
}
const selectedNotebook = item.notebook;
presentDialog({
title: strings.moveNotebooks(selectedNotebooks.length),
@@ -239,6 +255,7 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
updateNotebooks();
}, 300);
}}
testID="move-notebook-search"
button={{
icon: "plus",
onPress: async () => {

View File

@@ -39,6 +39,7 @@ import { createItemSelectionStore } from "../../stores/item-selection-store";
import { updateNotebook } from "../../utils/notebooks";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sendItemUpdateEvent } from "../../services/event-manager";
const useItemSelectionStore = createItemSelectionStore(true);
export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
@@ -198,6 +199,7 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
);
updateNotebook(currentNotebook.id);
sendItemUpdateEvent(currentNotebook.id, "notebook");
Navigation.queueRoutesForUpdate();
Navigation.goBack();
}}
@@ -281,8 +283,8 @@ const SelectableNoteItem = React.memo(
selected
? colors.selected.icon
: exists && !selected
? colors.static.red
: colors.primary.icon
? colors.static.red
: colors.primary.icon
}
/>
</>

View File

@@ -185,6 +185,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
<List
data={notes}
dataType="note"
groupType="notes"
onRefresh={() => {
onRequestUpdate();
}}
@@ -202,7 +203,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
) : undefined
}
placeholder={{
title: notebook?.title!,
title: notebook?.title || "",
paragraph: strings.notesEmpty(),
button: strings.addFirstNote(),
action: openEditor,

View File

@@ -193,11 +193,14 @@ const NotesPage = ({
hasSearch={true}
id={route.name === "Monographs" ? "Monographs" : params?.current?.id}
onSearch={() => {
if (!item) return;
if (route.name !== "Monographs" && !item) return;
const selector =
route.name === "Monographs"
? db.monographs.all
: db.relations.from(item, "note").selector;
: item && db.relations.from(item, "note").selector;
if (!selector) return;
Navigation.push("Search", {
placeholder: strings.searchInRoute(title || route.name),
@@ -214,6 +217,7 @@ const NotesPage = ({
<List
data={notes}
dataType="note"
groupType="notes"
onRefresh={onRequestUpdate}
loading={false}
renderedInRoute={route.name}

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