Compare commits

...

273 Commits

Author SHA1 Message Date
Abdullah Atta
b7bd06c9f3 Release v3.3.22 (#9952)
* web: fix file size is 0 error on attachment upload (#9951)

* web: bump version to 3.3.22
2026-06-06 09:48:03 +05:00
Abdullah Atta
766a8f28d4 web: bump version to 3.3.22 2026-06-06 09:22:20 +05:00
Abdullah Atta
86512b5ce7 web: fix file size is 0 error on attachment upload (#9951) 2026-06-06 09:10:29 +05:00
Ammar Ahmed
a820fcde53 editor: allow any width/height value in EmbedSizeOptions when inserting embed (#9925)
* editor: allow any width/height value in EmbedSizeOptions when inserting embeds

* editor: only accept valid url as value

* editor: allow embeding normal website urls

* editor: check url if embedSource=url

* editor: use defaultValue prop for width/height
2026-06-04 13:16:51 +05:00
01zulfi
5ddf1eddf5 web: validate max date when creating reminder (#9937)
* web: validate max date when creating reminder
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: skip max date validation on repeat mode in reminder dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-04 13:16:01 +05:00
Asad Ali
4237d2eb4a desktop: fix spell check issue for non-latin languages #9423 (#9443)
* desktop: fix spell check issue for non-latin languages #9423

Signed-off-by: Asad Ali <asadali.wolf@gmail.com>

* Delete apps/desktop/src/api/spell-checker.test.ts

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

* desktop: refactor

---------

Signed-off-by: Asad Ali <asadali.wolf@gmail.com>
Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-06-04 09:36:34 +05:00
Ammar Ahmed
f67cd503d6 Merge pull request #9922 from streetwriters/fix/400
mobile: hide toc button if no headings in editor
2026-06-04 08:54:08 +05:00
Ammar Ahmed
14039d6a1e Merge pull request #9921 from streetwriters/fix/401
Fix long tag name does not wrap properly in editor
2026-06-04 08:53:50 +05:00
Ammar Ahmed
d1fed9072e Merge pull request #9890 from kashaf-ansari-dev/fix-duplicate-note-message
mobile: add success message after note duplication
2026-06-04 08:52:31 +05:00
kashaf-ansari-dev
b003a3ffed mobile: reduce translation diff noise
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-03 14:17:33 +05:00
kashaf-ansari-dev
55e10e4755 mobile: add success message after note duplication
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-03 14:14:07 +05:00
Ammar Ahmed
7c1dd0f79a Merge pull request #9915 from streetwriters/fix/359-360
mobile: fix unable to edit/delete reminder when accessing reminders from note properties
2026-06-03 14:03:41 +05:00
Ammar Ahmed
229cf195a8 Merge pull request #9920 from streetwriters/fix/357
Fix screen jerks when opening add reminder screen from note
2026-06-03 14:02:33 +05:00
Ammar Ahmed
38da6ac08b Merge pull request #9918 from streetwriters/fix/318
Fix themes do not load properly on mobile in theme selector
2026-06-03 14:01:58 +05:00
Ammar Ahmed
597dc587cd Merge pull request #9919 from streetwriters/fix/317
Fix subscription not active opens a sheet that keeps loading if there is an error
2026-06-03 14:01:32 +05:00
Ammar Ahmed
5c76b92789 Merge pull request #9893 from kashaf-ansari-dev/fix-publish-password-validation
mobile: validate password field before publishing note
2026-06-03 14:01:09 +05:00
Abdullah Atta
8418d5fa9c Merge pull request #9886 from streetwriters/web/notebook-tag-group-options
web: allow notebook/tag/color level group&sort preferences
2026-06-03 13:35:06 +05:00
01zulfi
cd071f8f58 web: ensure full year range is visible in reminder dialog's day picker (#9928)
* web: ensure full year range is visible in reminder dialog's day picker
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: include boundary years in day picker's fluid year dropdown
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-03 11:52:36 +05:00
01zulfi
32ddc3fa40 web: refresh tag & notebook filtered lists on item rename (#9923)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-02 15:27:47 +05:00
01zulfi
68d07a7862 web: fix properties panel in focus mode (#9917)
* allow properties panel to open/close when app is in focus mode

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-02 13:06:50 +05:00
01zulfi
2aa5162fc9 web: set edited date as max date in note creation date edit's day picker (#9924)
* web: set edited date as max date in note creation date edit's day picker
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: fix creation date not being able to set to same as edited date
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-02 13:02:06 +05:00
01zulfi
cf5565d09d web: don't allow note expiry to be set beyond 1 year in the future (#9901)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-02 08:56:21 +05:00
Ammar Ahmed
f129e0c411 mobile: hide toc button if no headings in note 2026-06-01 13:18:13 +05:00
01zulfi
bb43d80e05 Merge pull request #9916 from streetwriters/release/3.3.21
web: bump version to 3.3.21
2026-06-01 12:52:57 +05:00
Ammar Ahmed
4e4d3866df mobile: limit tag to single line only 2026-06-01 12:43:41 +05:00
Ammar Ahmed
b2088ef782 mobile: fix long tag name does not wrap properly in editor 2026-06-01 12:27:37 +05:00
Ammar Ahmed
07db14f6bf mobile: fix screen jerks when opening add reminder screen from note 2026-06-01 11:53:00 +05:00
Ammar Ahmed
82d37701c5 mobile: fix gap in buttons 2026-06-01 11:09:29 +05:00
Ammar Ahmed
b48ab3c85d mobile: fix query error on fetching themes in theme-selector 2026-06-01 10:56:35 +05:00
01zulfi
52d7d90b68 web: bump version to 3.3.21
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-01 10:21:57 +05:00
Ammar Ahmed
bb7ff4f376 mobile: show error if no active subscription is found 2026-06-01 10:13:31 +05:00
Ammar Ahmed
43b35917ef mobile: fix subscription not active screen gets stuck on loading on iOS 2026-06-01 09:32:05 +05:00
Ammar Ahmed
bcfa34ca05 mobile: fix unable to edit/delete reminder when accessing reminders from note properties 2026-06-01 08:40:30 +05:00
Abdullah Atta
aab6a36067 Do not refetch user before decrypting each chunk in sync (#9805)
* core: do not refetch user before decrypting each chunk in sync

* web: fix editor readonly state not updating in realtime
2026-05-25 19:42:03 +05:00
01zulfi
c7e0cd35e7 editor: expand collapsed nodes on search scroll if search text inside callout (#9745)
* editor: expand callout on search scroll if search text inside callout
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: expand outline list && headings on search scroll
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: expand parents & scroll into view on type

* editor: fix parents expanding on every transaction

* editor: add a small debounce to delay search

* editor: fix editor not scrolling to selected search result

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-05-25 19:41:06 +05:00
01zulfi
bd3edc535a web: use getGroupOptionsById & setGroupOptionsById
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-25 15:04:17 +05:00
01zulfi
1dff7a121d core: fix group options by id tests
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-25 14:50:57 +05:00
Ammar Ahmed
3f3467e3a7 mobile: add support for sorting/grouping items based on notebook/tag/color id 2026-05-25 13:21:48 +05:00
Ammar Ahmed
39bb9afa0e core: add getGroupOptionsById/setGroupOptionsById 2026-05-25 13:21:00 +05:00
kashaf-ansari-dev
886e757715 mobile: removed redundant isLocked check for password field in publish form
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-05-25 11:21:55 +05:00
Ammar Ahmed
f1124c4112 Merge pull request #9896 from streetwriters/ci-fix-ios-workflow
ci: fix ios workflow
2026-05-25 10:18:54 +05:00
Ammar Ahmed
a1575ab5ea ci: fix 2026-05-25 10:18:16 +05:00
Ammar Ahmed
d31eaef6d4 ci: update 2026-05-25 10:17:22 +05:00
Ammar Ahmed
930e31d109 ci: update 2026-05-25 08:44:08 +05:00
Ammar Ahmed
2a6cc90592 ci: fix missing ios platform error 2026-05-25 08:43:46 +05:00
Ammar Ahmed
a13962f8c9 ci: fix ios workflow 2026-05-23 13:49:15 +05:00
kashaf-ansari-dev
5c93645969 mobile: migrate publish inputs to FormInput with validation
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-05-23 11:03:00 +05:00
01zulfi
a4c8259a02 web: bump version to 3.3.20 (#9888)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-22 20:28:04 +05:00
kashaf-ansari-dev
07defdfa78 mobile: validate password field before publishing note
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-05-22 17:15:38 +05:00
01zulfi
fc93a39164 web: fix weird scroll when toggling switch in attach files dialog (#9891)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-22 15:46:36 +05:00
01zulfi
9b588c50bf web: fix day picker to allow selecting all months within min/max range (#9879)
* web: fix day picker to allow selecting all months within min/max range
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: disable month nav buttons if they exceed min/max range in DayPicker
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-22 11:07:21 +05:00
01zulfi
27b00d4ef9 web: allow notebook/tag level group&sort preferences
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-22 10:48:27 +05:00
Ammar Ahmed
91e0437554 Merge pull request #9872 from streetwriters/mobile/release-3.3.24
mobile: release v3.3.24
2026-05-20 09:46:10 +05:00
01zulfi
4dcf14fce1 core: handle unknown collection types in sync (#9878)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-19 13:38:20 +05:00
01zulfi
8f8ac25d20 desktop: add support for opening file links (#9784)
* desktop: add support for opening file links
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: unescape paths with spaces

* web: wrap message in confirm dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* desktop: show message box if path doesn't exist when opening file link
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* desktop: disable opening file links on flatpak
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* desktop: wrap long file paths in electron dialog
* on linux, long messages in dialog aren't wrapped, so we need to wrap manually

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

* desktop: fix persistent "path not found" error

* intl: update strings

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-05-19 13:27:46 +05:00
01zulfi
b594d58d31 web: improve attachments UX (#9718)
* web: improve attaching attachments UX
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: don't couple editor logic in attach files dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: close attach file dialog after single file && remove countdown
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: show file size exceeded message in attach file dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: add 'processing' status message when file picker is opened
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: show "processing" modal after picking files

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-05-19 13:25:17 +05:00
01zulfi
20921df3f1 Merge pull request #9869 from streetwriters/release/3.3.19
web: bump version to 3.3.19
2026-05-18 15:23:49 +05:00
01zulfi
43d3036525 web: fix subscription settings not showing
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-18 13:19:33 +05:00
Ammar Ahmed
f725499039 mobile: release 3.3.24 2026-05-18 12:47:36 +05:00
01zulfi
cd743a92cb web: bump version to 3.3.19
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-18 10:31:21 +05:00
Abdullah Atta
ea0550cb05 desktop: disable updates for portable desktop app (#9855)
* desktop: disable updates for portable desktop app

* web: hide setting group if all settings under it are hidden

* web: hide check for updates button on flatpak/snap/portable apps

* desktop: disable auto updates for flatpak/snap/portable apps
2026-05-15 18:59:01 +05:00
Ammar Ahmed
d9d5cb2168 Merge pull request #9832 from streetwriters/fix/310
mobile: fix form validation in set your name dialog
2026-05-15 08:42:32 +05:00
Ammar Ahmed
69e509da1c Merge pull request #9857 from streetwriters/fix/crash-on-upgrade
mobile: fix app crashes on upgrade from v2.6.x to v3
2026-05-15 08:41:43 +05:00
Ammar Ahmed
6ba1fda2f2 mobile: fix app crashes on upgrade from v2.6.x to v3 2026-05-14 13:43:20 +05:00
Ammar Ahmed
93efecd6c3 Merge pull request #9839 from streetwriters/mobile/release-3.3.23
mobile: release 3.3.23
2026-05-14 10:42:25 +05:00
Ammar Ahmed
70d1119cf0 mobile: fix crash on tap set your name 2026-05-14 09:39:01 +05:00
Ammar Ahmed
3f11b76811 mobile: fix form validation in set your name dialog 2026-05-14 09:38:15 +05:00
01zulfi
d912c93f81 web: fix validation error message on add notebook dialog (#9852)
* only title is required field

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-14 09:25:21 +05:00
01zulfi
e635bf628a web: fix expiring notes option on free plan (#9853)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-14 09:25:13 +05:00
01zulfi
8048c90a89 web: disable months/year menu options where applicable (#9846)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-13 11:46:23 +05:00
01zulfi
8093f44a70 web: skip attachments restore from backup if user not logged in (#9778)
* web: skip attachments restore from backup if user not logged in
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: show login message when downloading attachments without login
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: show backup restored success toast
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-12 09:08:26 +05:00
Ammar Ahmed
d0bfc21b49 mobile: release 3.3.23 2026-05-11 14:04:02 +05:00
01zulfi
f67f90cecd Merge pull request #9838 from streetwriters/release/3.3.18
web: bump version to 3.3.18
2026-05-11 13:47:06 +05:00
Ammar Ahmed
ff4e2631cb Merge pull request #9828 from streetwriters/fix/313
Add input validation for 2fa method configuration
2026-05-11 13:38:44 +05:00
Ammar Ahmed
9779685102 mobile: fix no success message on copying 2fa codes 2026-05-11 13:38:19 +05:00
Ammar Ahmed
f2925725b8 mobile: add input validation for 2fa method configuration 2026-05-11 13:38:17 +05:00
Ammar Ahmed
9ca9fe3f99 Merge pull request #9827 from streetwriters/fix/312
Fix copy button text is cut in 2fa settings page
2026-05-11 13:36:49 +05:00
Ammar Ahmed
8e5039fa7f Merge pull request #9831 from streetwriters/fix/289
Add form validation and error handling in change email on Mobile
2026-05-11 13:36:15 +05:00
Ammar Ahmed
2f47fe331d Merge pull request #9825 from streetwriters/fix/288
Add form validation in change password screen
2026-05-11 13:35:45 +05:00
Ammar Ahmed
238abee126 mobile: add form validation in change password screen 2026-05-11 13:35:28 +05:00
Ammar Ahmed
a8d0fa3b94 Merge pull request #9820 from streetwriters/fix/286
Fix rename attachment does not work on iOS
2026-05-11 13:33:00 +05:00
Ammar Ahmed
27b3cfcd23 Merge pull request #9819 from streetwriters/fix/285
Fix Reminder allows past time in edit mode without validation error
2026-05-11 13:32:39 +05:00
01zulfi
d3f1690dca web: bump version to 3.3.18
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-11 10:19:20 +05:00
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
Ammar Ahmed
61aaaba452 mobile: cleanup 2026-05-08 10:02:57 +05:00
Ammar Ahmed
9992bf8c32 mobile: add form validation in change email 2026-05-08 09:26:11 +05:00
Ammar Ahmed
b8b7d37799 core: remap Invalid token error to Invalid code in changeEmail 2026-05-08 09:24:39 +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
14d680c092 mobile: show all errors inline with inputs/data-entry items 2026-05-07 18:42:47 +05:00
Ammar Ahmed
28f95b0061 mobile: fix plans keep loading on paywall screen 2026-05-07 13:01:38 +05:00
Ammar Ahmed
419dd555f4 mobile: fix reminder allows past time in edit mode without validation error 2026-05-07 13:01:37 +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
e023f11cd9 mobile: fix copy button text is cut in 2fa settings page 2026-05-07 09:22:59 +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
Ammar Ahmed
02b96470bc Merge pull request #9631 from streetwriters/fix/file-size-check-fail
mobile: fix file size check shows error but continues uploading the file
2026-04-02 13:13:56 +05:00
Ammar Ahmed
7234128f78 docs(help): add reminders help page (#9637)
* docs: add remindershelp page

* Apply suggestions from code review

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

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-04-02 12:38:28 +05:00
01zulfi
27e070e555 web: redirect automatically after final step in reset password flow (#9642)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-02 12:29:16 +05:00
01zulfi
539950bdc5 core: perform fuzzy search again if query contains non-alphanumeric chars (#9634)
* core: simplify fuzzy separator regex
* remove lookahead and lookbehind

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

* core: do 2 pass fuzzy search if query contains non-alphanumeric chars

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-02 12:13:06 +05:00
Ammar Ahmed
83280e7cee docs: clarify what happens on account deletion (#9636)
* docs: clarify what happens on account deletion

* intl: clarify what happens when account is deleted
2026-04-01 15:23:57 +05:00
01zulfi
cee0275565 web: handle crash on preview attachment (#9626)
* web: handle crash on preview attachment
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: better error message on attachment preview failure

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-01 13:07:17 +05:00
Ammar Ahmed
87bddf0037 mobile: fix file size check shows error but continues uploading the file 2026-04-01 11:06:33 +05:00
SahilAslam001
e736e3d810 mobile: do not auto fill auto generated title when user clears title field in editor (#9623)
Signed-off-by: Sahil Aslam <sahil.aslam011@gmail.com>
2026-04-01 09:31:21 +05:00
01zulfi
092f15406c editor: fix table column toolbar not showing in correct position (#9583)
* editor: fix table column toolbar not showing in correct position
* also scroll to the newly created column if it is not visible

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

* editor: use tr.scrollIntoView instead of manually adjusting position on new column
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: add tr.scrollIntoView() for table's selectRow()
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-31 13:28:03 +05:00
01zulfi
befcd0f112 web: refresh user on change email & run timer on first email sent (#9620)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-31 09:53:15 +05:00
Ammar Ahmed
8b92936d05 Add workflow to build android/ios previews on each PR (#9604)
Builds and publishes iOS and Android Preview apps that are instantly available to registered testers and quality assurance teams. We are using Firebase App Distribution for this purpose that works seamlessly on both android and iOS.

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2026-03-31 09:42:24 +05:00
01zulfi
aec135de83 web: fix plans page being cut-off from top (#9594)
* web: add top padding to plans page
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: fix plans page being cut-off from top
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-31 09:41:28 +05:00
01zulfi
f9738168e5 editor: fix callout first heading being collapsed/expanded if clicked towards right of it (#9547)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-30 20:15:03 +05:00
Ammar Ahmed
e873b88dbd Merge pull request #9618 from streetwriters/mobile-release/3.3.18
mobile: release 3.3.18
2026-03-30 09:43:53 +05:00
Abdullah Atta
214f5b8a98 ci: fix desktop pr preview workflow (#9612)
* ci: fix desktop pr preview workflow

Removed dependency on build-linux-arm64 for posting PR comments.

Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>

* ci: do not sign windows staging build

* clipper(web): regen package-lock.json

* ci: fix `Unknown target: x64`

---------

Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-03-28 14:05:43 +05:00
Abdullah Atta
70cf17fdd5 ci: add workflow for desktop pr preview (#9607)
* ci: add workflow for desktop pr preview

* ci: workflow didn't run

* ci: limit workflow to specific paths
2026-03-28 13:25:55 +05:00
Abdullah Atta
7d557810cb ci: add workflow for notesnook web pr preview (#9593)
* ci: add workflow for notesnook web pr preview

* ci: remove benchmarks workflow

* ci: use `setup-node-with-cache`

* ci: only publish pr preview when packages/** or apps/web/** changes
2026-03-28 12:47:17 +05:00
Abdullah Atta
ed24282703 web: bump version to 3.3.12 (#9602) 2026-03-28 11:09:19 +05:00
Ammar Ahmed
e2cf3e81af mobile: release 3.3.18 2026-03-28 10:12:23 +05:00
Ammar Ahmed
28fa133b6d Merge pull request #9597 from SahilAslam001/fix/undefined-id-issue
mobile: resolve undefined id issue in subscription product items
2026-03-28 10:04:03 +05:00
Ammar Ahmed
37893ced80 mobile: resolve undefine id issue in subscription product items
Signed-off-by: Sahil Aslam <sahil.aslam011@gmail.com>
2026-03-28 10:03:03 +05:00
Ammar Ahmed
900c48292c Merge pull request #9601 from streetwriters/chore/fix-prettier
mobile: format all files
2026-03-28 09:58:39 +05:00
Ammar Ahmed
302ae5ddb4 mobile: format all files
It seems prettier was not working properly. Pin prettier version to 2.8.8 in apps/mobile and ran prettier on all files to fix formatting issues.
2026-03-28 09:57:51 +05:00
Abdullah Atta
24fca08fdc core: generate all user keys on sign up (#9600) 2026-03-28 09:20:53 +05:00
01zulfi
91077dc132 core: fix ciphertext cannot be decrypted... error on attachment upload after sign up (#9596)
* core: fix attachment upload on new user signup
* multiple attachment keys were being created because null cache was set in the key manager, a check is added so only defined key values are set in cache

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

* core: use `cache.get` instead of `cache.has` for checking key existence

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-03-28 00:21:02 +05:00
01zulfi
273bbea938 web: don't collapse nav bar if a menu is still open (#9588)
* web: don't uncollapse nav bar if a menu is still open
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: handle native menu case

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-03-26 23:44:52 +05:00
01zulfi
36c44ad201 web: add toasts in email verification dialog (#9584)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-26 23:22:59 +05:00
01zulfi
46f5a2d7a7 editor: check for permission before pasting images (#9587)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-26 23:19:31 +05:00
Ammar Ahmed
9b359a91cc Merge pull request #9582 from streetwriters/mobile-release/3.3.17
Mobile release/3.3.17
2026-03-26 12:16:02 +05:00
Ammar Ahmed
0af831d50b mobile: release 3.3.17 2026-03-25 13:08:31 +05:00
Ammar Ahmed
3efaeaeeb9 mobile: fix failing tests 2026-03-25 12:31:54 +05:00
01zulfi
00a20dda48 Merge pull request #9569 from streetwriters/release/v3.3.11
web: bump version to 3.3.11
2026-03-25 11:48:02 +05:00
01zulfi
ec45160b27 web: add content in feature dialog for v3.3.11
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-25 11:29:34 +05:00
Ammar Ahmed
eda284ca25 Merge pull request #9568 from streetwriters/fix/disable-app-lock-permission-request
mobile: fix app gets locked when requesting notification permission for upload progress
2026-03-25 10:41:23 +05:00
Ammar Ahmed
1ed331b4bb Merge pull request #9567 from streetwriters/fix/ca-certificates-self-hosted-mobile
mobile: fix app does not use user defined CA certificates
2026-03-25 10:40:43 +05:00
01zulfi
ee74bb4fbf web: fix edit note creation date test
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-24 16:30:18 +05:00
01zulfi
df104669a3 web: bump version to 3.3.11
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-24 14:28:55 +05:00
01zulfi
9621b43bf2 web: fix command palette highlighting being escaped (#9565)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-24 14:22:49 +05:00
Ammar Ahmed
503c1a5c8d mobile: fix app gets locked when requesting notification permission for file upload progress 2026-03-24 13:17:01 +05:00
Ammar Ahmed
a2fa1d4d78 mobile: fix app does not use user defined CA certificates 2026-03-24 12:45:58 +05:00
Ammar Ahmed
6c6ad9a766 Merge pull request #9516 from streetwriters/fix/9496
Fix iOS app remains stuck when restoring backup from list
2026-03-24 09:59:02 +05:00
Ammar Ahmed
fc8807f74d Merge pull request #9519 from streetwriters/fix/GHSA-f27j-fqc6-v7pm
mobile: escape title when creating web-clip for shared metadata
2026-03-24 09:57:41 +05:00
Ammar Ahmed
edec48f9d4 Merge pull request #9523 from streetwriters/fix/mobile-merge-conflicts
mobile: fix merge conflicts for locked notes
2026-03-24 09:56:58 +05:00
Ammar Ahmed
ce98551057 Merge pull request #9532 from streetwriters/fix/note-open-glitch
mobile: fix glitch when opening note, flickery behaviour
2026-03-24 09:56:10 +05:00
Ammar Ahmed
74a2cc5b19 Merge pull request #9550 from streetwriters/fix/attachment-nav-flow
mobile: fix linked note not opening from attachment properties
2026-03-24 09:54:43 +05:00
01zulfi
9628a940e9 core: normalize special separators in fuzzy search (#9541)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-24 09:50:27 +05:00
01zulfi
d48643cbb9 web: fix editor title temporary flicker on tab switch (#9548)
* also remove debounce on editor's resize observer

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-24 09:46:02 +05:00
Abdullah Atta
558beaf216 desktop: fix error 153 in youtube embeds (#9510)
* desktop: fix error 153 in youtube embeds

* editor: update @social-embed/lib
2026-03-20 16:47:44 +05:00
01zulfi
6cc5d2452b web: fix typo in import result text (#9513)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-20 12:46:21 +05:00
Ammar Ahmed
e934ef7ffd mobile: allow lock-unlock a note even if it's not in any vault (#9508) 2026-03-20 12:43:40 +05:00
Ammar Ahmed
e2d8032ef4 Merge pull request #9484 from streetwriters/fix/9459
mobile: fix top bar item page width and count
2026-03-20 12:21:31 +05:00
Ammar Ahmed
81e6101fb5 mobile: add missing telegram icon in settings (#9507) 2026-03-20 12:17:57 +05:00
01zulfi
070354920b web: fix feature title margin in feature dialog (#9551)
* also fix typo

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-20 12:06:18 +05:00
Ammar Ahmed
404168fb86 mobile: fix linked note not opening from attachment properties 2026-03-19 13:38:30 +05:00
01zulfi
d5fa9fd02b editor: remove vertical offset on link hover popup (#9487)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-19 09:47:51 +05:00
01zulfi
6cc66d91be docs: clarify internal note links limitation in Evernote importer (#9509)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-19 09:42:59 +05:00
01zulfi
20a3b3b95e core: fix diacritics not getting ignored in search (#9479)
* core: fix diacritics search in db.lookup.notesWithHighlighting
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: remove diacritic in transformTokens
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: remove redundant `textContainsTokens` check

* core: fix wrong edge case handling

* core: create & use highlightRegexMatches
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-03-18 11:24:13 +05:00
01zulfi
9145ea4234 web: escape command title in command palette (#9539)
* Because we use dangerouslySetInnerHTML

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-17 11:24:16 +05:00
Abdullah Atta
0fa79433bd clipper(web): remove node-sass and sass-loader (#9536) 2026-03-17 09:16:27 +05:00
01zulfi
5185273b38 editor: sandbox clip iframe (#9521)
* editor: sandbox clip iframe
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: render webclips via bloburl with strict sandboxing

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-03-17 09:15:57 +05:00
01zulfi
eadd23de4d web: escape html in note title in diff viewer (#9518)
* web: escape html in note title in diff viewer
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: use entities package for escaping note title in diff viewer
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-16 13:07:56 +05:00
Ammar Ahmed
71d9738980 mobile: fix glitch when opening note, flickery behaviour 2026-03-16 09:07:18 +05:00
Ammar Ahmed
80328af888 mobile: fix merge conflicts for locked notes 2026-03-13 12:48:29 +05:00
Ammar Ahmed
5e83da5575 mobile: escape title when creating web-clip for shared metadata
fixes https://github.com/streetwriters/notesnook/security/advisories/GHSA-f27j-fqc6-v7pm
2026-03-13 10:54:07 +05:00
01zulfi
95a9775c26 core: fix crash because of bracket in note search query (#9477)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-13 09:18:53 +05:00
01zulfi
93d1319b25 desktop: disable update checker if auto updates off (#9302)
* web: disable desktop update checker if auto updates off
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: use setting store listener instead of directly using autoUpdates value
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-13 09:16:57 +05:00
Ammar Ahmed
f1fc166713 mobile: fix top bar item page width and count 2026-03-11 12:00:36 +05:00
334 changed files with 21509 additions and 22177 deletions

View File

@@ -0,0 +1,135 @@
name: Notesnook Android Preview
on:
pull_request_target:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/android.preview.firebase.yml"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
build:
needs: authorize
runs-on: ubuntu-22.04
env:
STAGING_BUILD: true
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
architecture: "x64"
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Make Gradlew Executable
run: cd apps/mobile/android && chmod +x ./gradlew
- name: Get build number
id: build-number
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Build arm64-v8a apk
run: |
cd apps/mobile/android
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
- name: Get app version
id: package-version
uses: saionaro/extract-package-version@master
with:
path: apps/mobile
- name: Publish to firebase CLI
id: firebase-output
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_APP_ID}}
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
groups: testers
file: apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
with:
script: |
const marker = '<!-- android-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/android/app/build/generated/sourcemaps/**/*.map

301
.github/workflows/desktop.preview.yml vendored Normal file
View File

@@ -0,0 +1,301 @@
name: Preview @notesnook/desktop
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/desktop/**"
- "apps/web/**"
- "packages/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.preview.yml"
jobs:
build:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
name: Build
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Generate desktop build (stable)
run: npm run tx @notesnook/web:build:desktop
- name: Build desktop bundle
working-directory: ./apps/desktop
run: npm run bundle
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
build-macos:
name: Build for macOS
needs: build
runs-on: macos-14
outputs:
macos-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: Install setuptools
run: brew install python-setuptools
- name: Setup notarization
run: |
mkdir -p ~/private_keys/
echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
- name: Collect app metadata
id: app_metadata
working-directory: ./apps/desktop
run: |
echo ::set-output name=apple_app_id::$(cat package.json | jq -r .appAppleId)
echo ::set-output name=app_bundle_id::$(cat package.json | jq -r .build.appId)
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
echo ::set-output name=bundle_version::$(cat package.json | jq -r .build.mac.bundleVersion)
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
- name: Build dmg
env:
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
yarn electron-builder --config=electron-builder.config.js --mac dmg --arm64 -p never
- name: Upload dmg artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: macos-build
path: apps/desktop/output/*.dmg
build-linux-x64:
name: Build for Linux x64
needs: build
runs-on: ubuntu-22.04
outputs:
linux-x64-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 x64 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:x64 -p never
working-directory: ./apps/desktop
- name: Upload AppImage artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
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
runs-on: windows-latest
outputs:
windows-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
npm i --cpu x64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Build
run: node scripts/execute.mjs @notesnook/desktop:release
- name: Publish
env:
NOTESNOOK_STAGING: true
run: yarn electron-builder --config=electron-builder.config.js --win nsis:x64 --publish never
working-directory: ./apps/desktop
- name: Upload exe artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: windows-build
path: apps/desktop/output/*.exe
post-pr-comment:
name: Post PR comment with preview URLs
needs:
[build, build-macos, build-linux-x64, build-linux-arm64, build-windows]
runs-on: ubuntu-latest
steps:
- name: Post or update PR comment
uses: actions/github-script@v6
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: |
const marker = '<!-- desktop-preview-comment -->';
const prNumber = context.issue.number;
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`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

View File

@@ -0,0 +1,173 @@
name: Notesnook iOS Preview
on:
pull_request_target:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/ios.preview.firebase.yml"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
build:
needs: authorize
runs-on: macos-26
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.1.1"
- name: Setup iOS Platform
run: |
xcodebuild -downloadPlatform iOS -exportPath ~/Downloads
xcodebuild -importPlatform ~/Downloads/iphonesimulator_26.1_23B86.dmg
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Build packages
run: npm run tx @notesnook/mobile:build
- name: Cache Pods
uses: actions/cache@v3
id: pods-cache
with:
path: apps/mobile/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
- name: Install Pods
run: |
cd apps/mobile/ios
pod install
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Get marketing version
id: marketing-version
run: echo "version=$(grep "IOS_MARKETING_VERSION" apps/mobile/ios/build-configs/ios-build.staging.xcconfig | awk -F'=' '{print $2}' | xargs)" >> $GITHUB_OUTPUT
- name: Get build number
id: build-number
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
- name: Make staging xcconfig active
run: |
cd apps/mobile/ios/build-configs
./use-ios-build-config.sh staging --marketing-version ${{steps.marketing-version.outputs.version}} --build-number ${{steps.build-number.outputs.timestamp}}
- name: Build iOS App
uses: ammarahm-ed/ios-build-action@master
with:
bundle-identifier: org.streetwriters.notesnook
scheme: Notesnook
configuration: "Release"
export-options: apps/mobile/ios/ExportOptionsStaging.plist
export-method: "ad-hoc"
project-path: apps/mobile/ios/Notesnook.xcodeproj
workspace-path: apps/mobile/ios/Notesnook.xcworkspace
update-targets: |
Notesnook
Make Note
NotesWidgetExtension
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
code-signing-identity: Apple Distribution
team-id: ${{ secrets.APPLE_TEAM_ID }}
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
output-path: Notesnook.ipa
mobileprovision-base64: |
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }}
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }}
${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }}
- name: Upload IPA artifact
uses: actions/upload-artifact@v4
with:
name: ios-preview-ipa
path: Notesnook.ipa
if-no-files-found: error
retention-days: 1
publish:
runs-on: ubuntu-latest
needs: build
timeout-minutes: 20
steps:
- name: Download IPA artifact
uses: actions/download-artifact@v4
with:
name: ios-preview-ipa
path: .
- name: Publish to firebase CLI
id: firebase-output
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_IOS_APP_ID}}
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
groups: testers
file: Notesnook.ipa
releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
with:
script: |
const marker = '<!-- ios-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**iOS App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

View File

@@ -19,6 +19,11 @@ jobs:
with:
xcode-version: "26.1.1"
- name: Setup iOS Platform
run: |
xcodebuild -downloadPlatform iOS -exportPath ~/Downloads
xcodebuild -importPlatform ~/Downloads/iphonesimulator_26.1_23B86.dmg
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit

View File

@@ -1,40 +0,0 @@
name: Benchmark @notesnook/web
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.benchmarks.yml"
pull_request:
jobs:
bench:
name: Bench
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Build
run: npm run build:web
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: apps/web
- name: Run benchmarks
id: benchmark
run: node __bench__/app.bench.mjs
working-directory: apps/web

80
.github/workflows/web.preview.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Notesnook Web PR Preview
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/web/**"
- "packages/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.preview.yml"
jobs:
build-and-deploy:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install dependencies
run: npm ci
- name: Build web
run: npm run build:web
- name: Deploy to Cloudflare Pages
id: deploy
working-directory: ./apps/web
run: |
set -euo pipefail
BRANCH=pr-${{ github.event.number }}-$(echo "${{ github.sha }}" | cut -c1-7)
echo "Deploying branch: $BRANCH"
DEPLOY_OUT=$(npx --yes wrangler pages deploy --project-name=notesnook-app --branch="$BRANCH" ./build 2>&1) || { echo "$DEPLOY_OUT"; exit 1; }
echo "$DEPLOY_OUT"
PREVIEW_URL=$(printf "%s" "$DEPLOY_OUT" | grep -Eo 'https?://[^ ]+' | head -1 || true)
if [ -z "$PREVIEW_URL" ]; then
echo "WARNING: could not parse preview URL from wrangler output"
fi
echo "preview_url=$PREVIEW_URL" >> $GITHUB_ENV
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ env.preview_url }}
with:
script: |
const marker = '<!-- pages-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**Cloudflare Pages Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

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.10",
"version": "3.3.22",
"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

@@ -33,16 +33,17 @@ import { AutoLaunch } from "../utils/autolaunch";
import { config, DesktopIntegration } from "../utils/config";
import { bringToFront } from "../utils/bring-to-front";
import { getTheme, setTheme, Theme } from "../utils/theme";
import { mkdirSync, writeFileSync } from "fs";
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { dirname } from "path";
import { resolvePath } from "../utils/resolve-path";
import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak, isSnap } from "../utils";
import { isFlatpak, isPortable, isSnap } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
import type { MenuItem as NNMenuItem } from "@notesnook/ui";
import { platform } from "os";
const t = initTRPC.create();
@@ -60,6 +61,7 @@ const NotificationOptions = z.object({
export const osIntegrationRouter = t.router({
isFlatpak: t.procedure.query(() => isFlatpak()),
isSnap: t.procedure.query(() => isSnap()),
isPortable: t.procedure.query(() => isPortable()),
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
@@ -178,7 +180,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();
}
@@ -189,9 +193,28 @@ export const osIntegrationRouter = t.router({
}),
openPath: t.procedure
.input(z.object({ type: z.literal("path"), link: z.string() }))
.query(({ input }) => {
.query(async ({ input }) => {
if (isFlatpak()) return;
const { type, link } = input;
if (type === "path") return shell.openPath(resolvePath(link));
if (type !== "path") return;
const resolvedPath = resolvePath(
// remove leading slash from path on windows
platform() === "win32" ? link.slice(1) : link
);
if (!existsSync(resolvedPath)) {
if (globalThis.window) {
await dialog.showMessageBox(globalThis.window, {
type: "error",
title: "Path not found",
message: `The path does not exist:\n${wrapPath(resolvedPath)}`
});
}
return;
}
await shell.openPath(resolvedPath);
}),
bringToFront: t.procedure.query(() => bringToFront()),
changeTheme: t.procedure
@@ -252,6 +275,7 @@ export const osIntegrationRouter = t.router({
if (menuItem) menu.append(menuItem);
}
if (menu.items.length > 0) menu.popup();
menu.on("menu-will-close", () => emit.next([]));
return () => {
menu.removeAllListeners();
menu.closePopup();
@@ -295,3 +319,7 @@ function toMenuItem(
}
}
}
function wrapPath(path: string, maxLineLength = 100): string {
return path.replace(new RegExp(`(.{${maxLineLength}})`, "g"), "$1\n");
}

View File

@@ -84,35 +84,55 @@ const LANGUAGES: Record<string, string> = {
type Language = { code: string; name: string };
const LANGUAGE_REDIRECT_MAP: Record<string, string> = {
es: "es-MX",
"es-419": "es-MX",
"es-ES": "es-AR"
};
export const spellCheckerRouter = t.router({
isEnabled: t.procedure.query(() => config.isSpellCheckerEnabled),
languages: t.procedure.query(
() =>
<Language[]>(
globalThis.window?.webContents.session.availableSpellCheckerLanguages.map(
(code) => ({
code,
name: LANGUAGES[code]
})
)
)
),
enabledLanguages: t.procedure.query(
() =>
<Language[]>(
globalThis.window?.webContents.session
.getSpellCheckerLanguages()
.map((code) => ({
code,
name: LANGUAGES[code]
}))
)
),
setLanguages: t.procedure
.input(z.array(z.string()))
.mutation(({ input: languages }) =>
globalThis.window?.webContents.session.setSpellCheckerLanguages(languages)
),
languages: t.procedure.query(() => {
const available =
globalThis.window?.webContents.session.availableSpellCheckerLanguages ||
[];
return <Language[]>available
.map((code) => ({
code,
name: LANGUAGES[code] || code
}))
.sort((a, b) => a.name.localeCompare(b.name));
}),
enabledLanguages: t.procedure.query(() => {
const enabled =
globalThis.window?.webContents.session.getSpellCheckerLanguages() || [];
const available =
globalThis.window?.webContents.session.availableSpellCheckerLanguages ||
[];
const resolved = enabled
.map((code) => resolveLanguage(code, available))
.filter(Boolean) as string[];
return <Language[]>resolved.map((code) => ({
code,
name: LANGUAGES[code] || code
}));
}),
setLanguages: t.procedure.input(z.array(z.string())).mutation(({ input }) => {
const available =
globalThis.window?.webContents.session.availableSpellCheckerLanguages ||
[];
const resolved = input
.map((code) => resolveLanguage(code, available))
.filter(Boolean) as string[];
globalThis.window?.webContents.session.setSpellCheckerLanguages(resolved);
}),
toggle: t.procedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input: { enabled } }) => {
@@ -128,3 +148,16 @@ export const spellCheckerRouter = t.router({
);
})
});
function resolveLanguage(code: string, available: string[]) {
if (LANGUAGE_REDIRECT_MAP[code]) {
const working = LANGUAGE_REDIRECT_MAP[code];
return available.includes(working) ? working : code;
}
const fallback = code.split("-")[0];
return available.includes(code)
? code
: available.includes(fallback)
? fallback
: undefined;
}

View File

@@ -24,6 +24,7 @@ import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
import { z } from "zod";
import { config } from "../utils/config";
import { app } from "electron";
import { isFlatpak, isPortable, isSnap } from "../utils";
type UpdateInfo = { version: string };
type Progress = { percent: number };
@@ -31,13 +32,15 @@ type Progress = { percent: number };
const t = initTRPC.create();
let cancellationToken: CancellationToken | undefined = undefined;
let downloadTimeout: NodeJS.Timeout | undefined = undefined;
const updatesSupported = !isFlatpak() && !isSnap() && !isPortable();
export const updaterRouter = t.router({
autoUpdates: t.procedure.query(() => config.automaticUpdates),
autoUpdates: t.procedure.query(
() => updatesSupported && config.automaticUpdates
),
releaseTrack: t.procedure.query(() => config.releaseTrack),
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
download: t.procedure.query(async () => {
if (cancellationToken) return;
if (!updatesSupported || cancellationToken) return;
clearTimeout(downloadTimeout);
await new Promise<string[]>((resolve, reject) => {
downloadTimeout = setTimeout(async () => {
@@ -52,7 +55,7 @@ export const updaterRouter = t.router({
});
}),
check: t.procedure.query(async () => {
if (cancellationToken) return;
if (!updatesSupported || cancellationToken) return;
clearTimeout(downloadTimeout);
await new Promise<void>((resolve) => {
downloadTimeout = setTimeout(async () => {

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

@@ -33,3 +33,7 @@ export function isFlatpak() {
export function isSnap() {
return process.env.SNAP !== undefined;
}
export function isPortable() {
return process.env.PORTABLE_EXECUTABLE_DIR !== undefined;
}

View File

@@ -62,7 +62,17 @@ function registerProtocol() {
headers: { "Content-Type": extensionToMimeType[fileExtension] }
});
} else {
return net.fetch(request, { bypassCustomProtocolHandlers: true });
const requestHeaders = new Headers(request.headers);
if (!requestHeaders.has("referer"))
requestHeaders.set("referer", PROTOCOL_URL);
return await net.fetch(
new Request(request, {
headers: requestHeaders
}),
{
bypassCustomProtocolHandlers: true
}
);
}
});
console.info(`${SCHEME} protocol inteception "successful"`);

View File

@@ -5,6 +5,7 @@ apply plugin: 'kotlin-parcelize'
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
import com.android.build.OutputFile
import groovy.json.JsonSlurper
import org.apache.tools.ant.taskdefs.condition.Os
@@ -94,6 +95,18 @@ def fdroidBuild() {
return project.hasProperty("fdroidBuild") && project.fdroidBuild == "true"
}
def prBuildNumber() {
def value = project.getProperties().get("prBuildNumber")
return value ? value : "1000"
}
def stagingReleaseBuild() {
// Enable with STAGING_BUILDtrue or -PstagingReleaseBuild=true
def fromEnv = System.getenv("STAGING_BUILD") == "true"
def fromProp = project.hasProperty("stagingReleaseBuild") && project.stagingReleaseBuild == "true"
return fromEnv || fromProp
}
def getNpmVersion() {
def inputFile = file("$rootDir/../package.json")
def jsonPackage = new JsonSlurper().parseText(inputFile.text)
@@ -118,14 +131,17 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
namespace "com.streetwriters.notesnook"
defaultConfig {
applicationId "com.streetwriters.notesnook"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3096
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3105
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -192,6 +208,8 @@ android {
output.versionCodeOverride =
defaultConfig.versionCode * 5 + versionCodes.get(abi)
println("Fdroid Version code: ${output.versionCodeOverride} for abi ${versionCodes.get(abi)}");
} else if (stagingReleaseBuild()) {
output.versionCodeOverride = defaultConfig.versionCode + versionCodes.get(abi)
} else {
if (isBuildingAAB) {
output.versionCodeOverride = 4 * 1048576 + defaultConfig.versionCode
@@ -233,7 +251,6 @@ dependencies {
} else {
implementation jscFlavor
}
}

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

@@ -4,4 +4,10 @@
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
<base-config>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -36,6 +36,8 @@ allprojects {
url "$rootDir/../node_modules/detox/Detox-android"
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}

View File

@@ -1,8 +1,3 @@
- Add support audio playback in editor
- Sort tags assigned to a note on top when viewing and managing note tags
- Fix account password changing and account recovery
- Fix title is editable in readonly mode
- Allow attaching image as file
- Minor bug fixes and performance improvements
- Bug fixes and improvements
Thank you for using Notesnook!
Thank you for using Notesnook!

View File

@@ -1,4 +1,4 @@
{
"name": "Notesnook",
"displayName": "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

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

@@ -33,6 +33,10 @@ import {
} from "./utils";
import Upload from "@ammarahmed/react-native-upload";
import { CloudUploader } from "react-native-nitro-cloud-uploader";
import { useUserStore } from "../../stores/use-user-store";
import { sleep } from "../../utils/time";
import { isFeatureAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
// Upload constants
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
@@ -197,6 +201,20 @@ export async function uploadFile(
const remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === FileSizeResult.Error) return false;
const featureResult = await isFeatureAvailable(
"fileSize",
fileInfo.size || 0
);
if (!featureResult.isAllowed) {
ToastManager.show({
heading: strings.fileTooLarge(),
message: featureResult.error,
type: "error"
});
return false;
}
if (
remoteFileSize > FileSizeResult.Empty &&
remoteFileSize === fileInfo.size
@@ -212,6 +230,9 @@ export async function uploadFile(
);
if (Platform.OS === "android") {
useUserStore.setState({
disableAppLockRequests: true
});
const status = await PermissionsAndroid.request(
"android.permission.POST_NOTIFICATIONS"
);
@@ -221,6 +242,10 @@ export async function uploadFile(
type: "info"
});
}
await sleep(500);
useUserStore.setState({
disableAppLockRequests: false
});
}
let uploaded = false;

View File

@@ -161,10 +161,10 @@ export async function checkUpload(
size === 0
? `File size is 0.`
: size === -1
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
if (error) throw new Error(error);
}

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

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";
@@ -59,6 +58,8 @@ import Heading from "../ui/typography/heading";
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,
@@ -153,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"
}
@@ -304,9 +355,9 @@ const Actions = ({
<Pressable
onPress={async () => {
eSendEvent(eCloseSheet, contextId);
await sleep(150);
close?.();
eSendEvent(eCloseAttachmentDialog);
await sleep(300);
Navigation.navigate("FluidPanelsView");
openNote(item, (item as any).type === "trash");
}}
style={{

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

@@ -18,6 +18,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, { useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
@@ -26,42 +27,44 @@ import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useUserStore } from "../../stores/use-user-store";
import { eOpenRecoveryKeyDialog } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import Input from "../ui/input";
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
import { Notice } from "../ui/notice";
import Paragraph from "../ui/typography/paragraph";
import { TextInput } from "react-native-gesture-handler";
export const ChangePassword = () => {
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>(undefined);
const { colors } = useThemeColors();
const formRef = useRef(
createFormRef({
oldPassword: "",
password: ""
})
);
const oldPasswordInputRef = useRef<TextInput>(null);
const oldPassword = useRef<string>(undefined);
const [error, setError] = useState(false);
const passwordInputRef = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const user = useUserStore((state) => state.user);
const changePassword = async () => {
setError(undefined);
formRef.current.clearErrors();
if (!user?.isEmailConfirmed) {
ToastManager.show({
heading: strings.emailNotConfirmed(),
message: strings.emailNotConfirmedDesc(),
type: "error",
context: "local"
});
setError(strings.emailNotConfirmedDesc());
return;
}
if (error || !oldPassword.current || !password.current) {
ToastManager.show({
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
type: "error",
context: "local"
});
if (!formRef.current.validate()) {
return;
}
const values = formRef.current.getValues();
setLoading(true);
try {
const result = await BackupService.run(
@@ -74,8 +77,8 @@ export const ChangePassword = () => {
}
const passwordChanged = await db.user.changePassword(
oldPassword.current,
password.current
values.oldPassword,
values.password
);
if (!passwordChanged) {
@@ -91,15 +94,15 @@ export const ChangePassword = () => {
Navigation.goBack();
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
const message = (e as Error).message;
setLoading(false);
ToastManager.show({
heading: strings.passwordChangeFailed(),
message: (e as Error).message,
type: "error",
context: "local"
});
if (/old password/i.test(message)) {
formRef.current.setError("oldPassword", message);
} else {
setError(message);
}
}
setLoading(false);
};
return (
@@ -110,36 +113,61 @@ export const ChangePassword = () => {
}}
>
<Dialog context="change-password-dialog" />
<Input
<FormInput
name="oldPassword"
formRef={formRef}
fwdRef={oldPasswordInputRef}
onChangeText={(value) => {
oldPassword.current = value;
}}
loading={loading}
validators={[validators.required(strings.currentPasswordRequired())]}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.oldPassword()}
placeholder={strings.currentPassword()}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
/>
<Input
<FormInput
name="password"
formRef={formRef}
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
onErrorCheck={(e) => setError(e)}
loading={loading}
validators={[validators.required(strings.passwordRequired())]}
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
validationType="password"
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.newPassword()}
onSubmitEditing={() => {
changePassword();
}}
/>
{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}
</Paragraph>
) : null}
<Notice text={strings.changePasswordNotice()} type="alert" />
<View style={{ height: 10 }} />

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);
@@ -203,27 +203,27 @@ export const Login = ({
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

@@ -161,10 +161,10 @@ const FloatingButton = ({
icon
? icon
: route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
color={color || colors.primary.accent}
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}

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,
@@ -137,7 +136,7 @@ const BaseDialog = ({
? background
: transparent
? "transparent"
: "rgba(0,0,0,0.3)"
: "rgba(0,0,0,0.1)"
}}
>
<KeyboardAvoidingView

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

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

View File

@@ -56,7 +56,12 @@ import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import { Note, NoteContent, VAULT_ERRORS } from "@notesnook/core";
import {
isEncryptedContent,
Note,
NoteContent,
VAULT_ERRORS
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
export const VaultDialog: React.FC = () => {
@@ -105,54 +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();
const noteLocked = data.item
? await db.vaults.itemExists(data.item)
: false;
// 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;
noteLockedRef.current = noteLocked;
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({
@@ -237,6 +194,7 @@ export const VaultDialog: React.FC = () => {
close();
}, 100);
} else {
setLoading(false);
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
@@ -270,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(),
@@ -280,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({
@@ -321,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> }) => {
@@ -375,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 {
@@ -417,9 +412,10 @@ export const VaultDialog: React.FC = () => {
onUnlockRef.current
) {
const password = passwordRef.current;
const unlock = onUnlockRef.current;
close();
await sleep(300);
onUnlockRef.current(note, password);
await sleep(500);
unlock(note, password);
}
} catch (e) {
takeErrorAction();
@@ -427,6 +423,7 @@ export const VaultDialog: React.FC = () => {
}, [
biometricUnlock,
isBiometryEnrolled,
enrollFingerprint,
openInEditor,
shareNote,
deleteNote,
@@ -451,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 || "");
@@ -523,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;
@@ -676,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);
@@ -718,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
@@ -785,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,13 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import {
GroupHeader,
GroupingByIdKey,
GroupingKey,
GroupOptions,
ItemType
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
@@ -40,7 +46,11 @@ type SectionHeaderProps = {
color?: string;
screen?: RouteName;
groupOptions: GroupOptions;
group: GroupingKey;
groupId?: string;
type?: GroupingByIdKey;
onOpenJumpToDialog: () => void;
itemCount?: number;
};
export const SectionHeader = React.memo<
@@ -53,11 +63,15 @@ export const SectionHeader = React.memo<
color,
screen,
groupOptions,
onOpenJumpToDialog
group,
onOpenJumpToDialog,
itemCount,
groupId,
type
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
dataType as "note" | "notebook" | "searchResult"
);
return (
@@ -104,7 +118,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>
@@ -132,7 +148,10 @@ export const SectionHeader = React.memo<
component: (
<Sort
screen={screen}
type={dataType}
dataType={dataType}
type={type}
group={group}
groupId={groupId}
hideGroupOptions={
screen === "Reminders" || screen === "Search"
}
@@ -150,7 +169,8 @@ export const SectionHeader = React.memo<
hidden={
dataType !== "note" &&
dataType !== "notebook" &&
screen !== "Notes"
screen !== "Notes" &&
screen !== "Search"
}
style={{
width: 25,
@@ -163,9 +183,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 +213,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

@@ -32,9 +32,11 @@ import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { fluidTabsRef } from "../../../utils/global-refs";
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../../screens/editor/tiptap/utils";
import { RouteParams } from "../../../stores/use-navigation-store";
import NotePreview from "../../note-history/preview";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import { RouteParams } from "../../../stores/use-navigation-store";
export const openNote = async (
item: Note,
@@ -63,11 +65,19 @@ export const openNote = async (
component: <NotePreview note={item} content={content} />
});
} else {
if (!useTabStore.getState().hasTabForNote(note.id!)) {
editorController.current.commands.setLoading(
true,
useTabStore.getState().currentTab
);
}
eSendEvent(eOnLoadNote, {
item: note
});
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor");
setTimeout(() => {
fluidTabsRef.current?.goToPage("editor");
}, 32);
}
}
};

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

@@ -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 { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core";
import {
GroupingByIdKey,
GroupingKey,
Item,
VirtualizedGrouping
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { LegendList, LegendListRenderItemProps } from "@legendapp/list";
import React, { useEffect, useRef } from "react";
@@ -53,7 +58,9 @@ type ListProps = {
isRenderedInActionSheet?: boolean;
CustomListComponent?: React.JSX.ElementType;
placeholder?: PlaceholderData;
groupType: GroupingKey;
id?: string;
type?: GroupingByIdKey;
};
const onMomentumScrollEnd = () => {
@@ -73,16 +80,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, props.id, props.type);
const _onRefresh = async () => {
Sync.run("global", false, "full", () => {
@@ -94,7 +92,7 @@ export default function List(props: ListProps) {
(item: number | boolean, index: number) => {
return props.data?.type(index);
},
[]
[props.data]
);
const renderItem = React.useCallback(
@@ -104,23 +102,27 @@ export default function List(props: ListProps) {
index={itemProps.index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupId={props.id}
groupOptions={groupOptions}
group={groupType as GroupingKey}
group={props.groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
type={props.type}
scrollRef={scrollRef}
/>
);
},
[
groupOptions,
groupType,
props.customAccentColor,
props.data,
props.dataType,
props.isRenderedInActionSheet,
props.renderedInRoute
props.data,
props.id,
props.groupType,
props.renderedInRoute,
props.customAccentColor,
props.dataType,
groupOptions,
props.type
]
);

View File

@@ -26,6 +26,7 @@ import {
Color,
GroupHeader,
GroupOptions,
GroupingByIdKey,
GroupingKey,
HighlightedResult,
Item,
@@ -40,7 +41,7 @@ import {
} from "@notesnook/core";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { getGroupOptions } from "../../hooks/use-group-options";
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
import { eSendEvent } from "../../services/event-manager";
import { RouteName } from "../../stores/use-navigation-store";
@@ -49,11 +50,11 @@ import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
import { SearchResult } from "../list-items/search-result";
import TagItem from "../list-items/tag";
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
group: GroupingKey;
items: VirtualizedGrouping<TItem> | undefined;
isSheet: boolean;
index: number;
@@ -62,6 +63,8 @@ type ListItemWrapperProps<TItem = Item> = {
dataType: string;
scrollRef: any;
groupOptions: GroupOptions;
groupId?: string;
type?: GroupingByIdKey;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
@@ -182,6 +185,9 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
groupId={props.groupId}
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -218,6 +224,9 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
item={groupHeader}
index={index}
dataType={item.type}
group={group}
groupId={props.groupId}
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -245,8 +254,11 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
color={props.customAccentColor}
type={props.type}
groupId={props.groupId}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
@@ -271,7 +283,10 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
groupId={props.groupId}
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -297,9 +312,13 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
screen={props.renderedInRoute}
item={groupHeader}
index={index}
group={group}
dataType={item.type}
groupId={props.groupId}
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
itemCount={items?.placeholders.length}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
@@ -316,11 +335,16 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
}
}
function getDate(item: Notebook | Note, groupType?: GroupingKey): number {
function getDate(
item: Notebook | Note,
groupType?: GroupingKey,
id?: string,
type?: GroupingByIdKey
): number {
return (
getSortValue(
groupType
? db.settings.getGroupOptions(groupType)
? getGroupOptions(groupType, id, type)
: {
sortBy: "dateEdited",
sortDirection: "desc"

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";
@@ -60,6 +59,7 @@ import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import { presentDialog } from "../dialog/functions";
import { Cipher } from "@notesnook/crypto";
const MergeConflicts = () => {
const { colors } = useThemeColors();
@@ -74,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,
@@ -84,12 +86,21 @@ const MergeConflicts = () => {
dateEdited: contentToSave.dateEdited
});
const noteLocked = await db.vaults.itemExists(note);
const noteContent = await db.content.findByNoteId(note.id);
if (noteLocked) {
await db.vault.save({
...contentToSave,
sessionId: `${Date.now()}`
if (noteContent?.locked) {
const selectedContent = contentToSave.conflicted
? noteContent
: noteContent.conflicted;
await db.content.add({
id: note.contentId,
dateResolved: noteContent?.conflicted?.dateModified || Date.now(),
sessionId: `${Date.now()}`,
data: selectedContent?.data as Cipher<"base64">,
type: selectedContent?.type,
locked: true,
conflicted: undefined
});
} else {
await db.content.add({
@@ -138,7 +149,6 @@ const MergeConflicts = () => {
onUnlock: async (item, password) => {
if (!item || !password) return;
const currentContent = await db.content.get(item.contentId!);
try {
noteContent = {
...(await db.content.get(item.contentId!)),
@@ -453,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

@@ -318,7 +318,9 @@ export const Items = ({
[
colors.error.icon,
colors.primary.accent,
colors.primary.border,
colors.secondary.icon,
colors.static.orange,
columnItemWidth,
topBarSorting
]
@@ -326,8 +328,9 @@ export const Items = ({
const getTopBarItemChunksOfFour = () => {
const chunks = [];
for (let i = 0; i < topBarItems.length; i += 5) {
chunks.push(topBarItems.slice(i, i + 5));
const itemCount = shouldShrink ? 4 : 5;
for (let i = 0; i < topBarItems.length; i += itemCount) {
chunks.push(topBarItems.slice(i, i + itemCount));
}
return chunks;
};
@@ -375,10 +378,7 @@ export const Items = ({
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP,
gap: 5,
/**
* ((columnItemWidth + gap) * noOfItems) - padding
*/
width: (columnItemWidth + 5) * 5 - 12
width: width
}}
>
{item.map(renderTopBarItem)}

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
@@ -356,29 +368,21 @@ const ProductItem = (props: {
(product as RNIap.Subscription)?.productId
) ||
props.pricingPlans.user?.subscription?.productId ===
(product as Plan).id);
(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

@@ -118,8 +118,8 @@ const ListBlockItem = ({
{item?.content.length > 200
? item?.content.slice(0, 200) + "..."
: !item.content || item.content.trim() === ""
? strings.linkNoteEmptyBlock()
: item.content}
? strings.linkNoteEmptyBlock()
: item.content}
</Paragraph>
<View

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";
@@ -42,26 +45,24 @@ import { DefaultAppStyles } from "../../../utils/styles";
import DialogHeader from "../../dialog/dialog-header";
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 { useAsync } from "react-async-hook";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { eMenuItemUpdate } from "../../../utils/events";
import { useIsFeatureAvailable } from "@notesnook/common";
import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
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
};
}
@@ -78,26 +79,31 @@ const PublishNoteSheet = ({
const isFeatureAvailable = useIsFeatureAvailable("monographAnalytics");
const [isLocked, setIsLocked] = useState(false);
const [publishing, setPublishing] = useState(false);
const customTitle = useRef<string>("");
const pwdInput = useRef<TextInput>(null);
const titleInput = useRef<TextInput>(null);
const passwordValue = useRef<string>(undefined);
const monographData = useAsync(async () => {
return fetchMonographData(note?.id);
}, []);
const monograph = monographData.result?.monograph;
customTitle.current = monograph?.title || note.title || "";
const publishUrl = monograph && `${hosts.MONOGRAPH_HOST}/${monograph?.id}`;
const isPublished = db.monographs.monograph(note?.id);
const formRef = useRef(
createFormRef({
title: monograph?.title || note.title || "",
password: ""
})
);
useEffect(() => {
(async () => {
if (monograph) {
setSelfDestruct(!!monograph?.selfDestruct);
if (monograph.password) {
passwordValue.current = await db.monographs.decryptPassword(
const password = await db.monographs.decryptPassword(
monograph?.password
);
formRef.current.setValue("password", password);
setIsLocked(!!monograph?.password);
}
}
@@ -106,19 +112,23 @@ const PublishNoteSheet = ({
const publishNote = async () => {
if (publishing) return;
formRef.current.clearErrors();
if (!formRef.current.validate()) return;
const values = formRef.current.getValues();
setPublishLoading(true);
try {
if (note?.id) {
if (isLocked && !passwordValue.current) return;
await db.monographs.publish(note.id, customTitle.current, {
selfDestruct: selfDestruct,
password: isLocked ? passwordValue.current : undefined
await db.monographs.publish(note.id, values.title, {
selfDestruct,
password: isLocked ? values.password : undefined
});
await monographData.execute();
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
eSendEvent(eMenuItemUpdate);
}
requestInAppReview();
} catch (e) {
@@ -128,9 +138,9 @@ const PublishNoteSheet = ({
type: "error",
context: "local"
});
} finally {
setPublishLoading(false);
}
setPublishLoading(false);
};
const setPublishLoading = (value: boolean) => {
setPublishing(value);
@@ -144,6 +154,7 @@ const PublishNoteSheet = ({
await db.monographs.unpublish(note.id);
monographData.execute();
Navigation.queueRoutesForUpdate();
eSendEvent(eMenuItemUpdate);
setPublishLoading(false);
}
} catch (e) {
@@ -247,11 +258,12 @@ const PublishNoteSheet = ({
</TouchableOpacity>
) : null}
<Input
<FormInput
name="title"
formRef={formRef}
fwdRef={titleInput}
onChangeText={(value) => (customTitle.current = value)}
defaultValue={customTitle.current}
placeholder={strings.noteTitle()}
validators={[validators.required(strings.titleIsRequired())]}
/>
<TouchableOpacity
@@ -298,13 +310,16 @@ const PublishNoteSheet = ({
{isLocked ? (
<>
<Input
<FormInput
name="password"
formRef={formRef}
fwdRef={pwdInput}
onChangeText={(value) => (passwordValue.current = value)}
blurOnSubmit
secureTextEntry
defaultValue={passwordValue.current}
placeholder={strings.enterPassword()}
validators={[
validators.required(strings.passwordRequired())
]}
containerStyle={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
@@ -381,9 +396,6 @@ const PublishNoteSheet = ({
}}
>
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
<Paragraph>
{monographData?.result?.analytics?.totalViews || 0}
</Paragraph>
</View>
</View>
</View>

View File

@@ -104,8 +104,8 @@ const ListBlockItem = ({
{item?.content.length > 200
? item?.content.slice(0, 200) + "..."
: !item.content || item.content.trim() === ""
? strings.linkNoteEmptyBlock()
: item.content}
? strings.linkNoteEmptyBlock()
: item.content}
</Paragraph>
<View

View File

@@ -1,162 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Item, ItemReference, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
PresentSheetOptions,
presentSheet
} from "../../../services/event-manager";
import { useRelationStore } from "../../../stores/use-relation-store";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import DialogHeader from "../../dialog/dialog-header";
import List from "../../list";
import SheetProvider from "../../sheet-provider";
import { Button, ButtonProps } from "../../ui/button";
import Paragraph from "../../ui/typography/paragraph";
type RelationsListProps = {
actionSheetRef: RefObject<ActionSheetRef>;
close?: () => void;
update?: (options: PresentSheetOptions) => void;
item: { id: string; type: string };
referenceType: string;
relationType: "to" | "from";
title: string;
button?: ButtonProps;
onAdd: () => void;
};
const IconsByType = {
reminder: "bell"
};
export const RelationsList = ({
actionSheetRef,
item,
referenceType,
relationType,
title,
button,
onAdd
}: RelationsListProps) => {
const updater = useRelationStore((state) => state.updater);
const { colors } = useThemeColors();
const [items, setItems] = useState<VirtualizedGrouping<Item>>();
const hasNoRelations = !items || items?.placeholders?.length === 0;
useEffect(() => {
db.relations?.[relationType]?.(
{ id: item?.id, type: item?.type } as ItemReference,
referenceType as any
)
.selector.sorted({
sortBy: "dateEdited",
sortDirection: "desc"
})
.then((grouped) => {
setTimeout(() => {
setItems(grouped);
}, 300);
});
}, [relationType, referenceType, item?.id, item?.type, updater]);
return (
<View style={{ paddingHorizontal: DefaultAppStyles.GAP, height: "100%" }}>
<SheetProvider context="local" />
<DialogHeader
title={title}
button={hasNoRelations ? undefined : button}
/>
{hasNoRelations ? (
<View
style={{
height: "85%",
justifyContent: "center",
alignItems: "center"
}}
>
<Icon
name={IconsByType[referenceType as keyof typeof IconsByType]}
size={60}
color={colors.primary.icon}
/>
<Paragraph>{strings.noLinksFound()}</Paragraph>
<Button
onPress={() => {
onAdd?.();
}}
fontSize={AppFontSize.sm}
// width="100%"
type="inverted"
icon="plus"
title={strings.addItem(
referenceType as "notebook" | "tag" | "reminder" | "note"
)}
/>
</View>
) : (
<List
data={items}
loading={false}
dataType={referenceType as any}
isRenderedInActionSheet={true}
/>
)}
</View>
);
};
RelationsList.present = ({
reference,
referenceType,
relationType,
title,
button,
onAdd
}: {
reference: { id: string; type: string };
referenceType: string;
relationType: "to" | "from";
title: string;
button?: ButtonProps;
onAdd: () => void;
}) => {
presentSheet({
component: (ref, close, update) => (
<RelationsList
actionSheetRef={ref}
close={close}
update={update}
item={reference}
referenceType={referenceType}
relationType={relationType}
title={title}
button={button}
onAdd={onAdd}
/>
)
});
};

View File

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

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
GroupingByIdKey,
GroupingKey,
GroupOptions,
ItemType,
@@ -27,7 +28,10 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import {
getGroupOptions,
setGroupOptionsById
} from "../../../hooks/use-group-options";
import { eSendEvent } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { RouteName } from "../../../stores/use-navigation-store";
@@ -43,29 +47,24 @@ import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const Sort = ({
type,
dataType,
screen,
hideGroupOptions
hideGroupOptions,
group: groupType,
groupId,
type
}: {
type: ItemType;
dataType: ItemType;
type?: GroupingByIdKey;
screen?: RouteName;
group: GroupingKey;
hideGroupOptions?: boolean;
groupId?: string;
}) => {
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)
getGroupOptions(groupType, groupId, type)
);
const getSortButtonTitle = () => {
@@ -88,16 +87,17 @@ const Sort = ({
};
const updateGroupOptions = async (_groupOptions: GroupOptions) => {
await db.settings.setGroupOptions(groupType, _groupOptions);
console.log(groupId, type);
setGroupOptionsById(groupType, _groupOptions, groupId, type);
setGroupOptions(_groupOptions);
setTimeout(() => {
if (screen) Navigation.queueRoutesForUpdate(screen);
if (type === "notebook") {
if (dataType === "notebook") {
useNotebookStore.getState().refresh();
} else if (type === "tag") {
} else if (dataType === "tag") {
useTagStore.getState().refresh();
}
eSendEvent(eGroupOptionsUpdated, groupType);
eSendEvent(eGroupOptionsUpdated, groupType, groupId, type);
eSendEvent(refreshNotesPage);
}, 1);
};
@@ -157,10 +157,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 +193,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

@@ -114,8 +114,8 @@ export const UserSheet = () => {
progress ? `(${progress.current})` : ""
}`
: lastSyncStatus === SyncStatus.Failed
? strings.syncFailed()
: strings.synced()}{" "}
? strings.syncFailed()
: strings.synced()}{" "}
{!syncing ? (
<TimeSince
style={{
@@ -139,8 +139,8 @@ export const UserSheet = () => {
!user || lastSyncStatus === SyncStatus.Failed
? colors.error.icon
: isOffline
? colors.static.orange
: colors.success.icon
? colors.static.orange
: colors.success.icon
}
/>
</Paragraph>

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;
@@ -432,6 +433,7 @@ 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) {
@@ -484,17 +486,23 @@ const TabBar = (props: SimpleTabBarProps) => {
? "sort-ascending"
: "sort-descending"
}
top={10}
testID="sidebar-sort-button"
color={colors.primary.icon}
onPress={() => {
presentSheet({
component: (
<Sort
type={
dataType={
props.navigationState.index === 1
? "notebook"
: "tag"
}
group={
props.navigationState.index === 1
? "notebooks"
: "tags"
}
hideGroupOptions
/>
)
@@ -519,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]);

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) => {
@@ -98,10 +101,11 @@ export const NotebookItem = ({
item.depth === 0
? undefined
: item.depth < 6
? 15 * item.depth
: 15 * 5,
? 15 * item.depth
: 15 * 5,
width: "100%",
marginTop: 2
marginTop: 2,
opacity: item.disabled ? 0.5 : 1
}}
>
<Pressable
@@ -187,8 +191,8 @@ export const NotebookItem = ({
!item.hasChildren || disableExpand
? "book-outline"
: expanded
? "chevron-down"
: "chevron-right"
? "chevron-down"
: "chevron-right"
}
/>

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

View File

@@ -20,6 +20,7 @@ import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ColorValue, TextProps } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import EvilIcon from "react-native-vector-icons/EvilIcons";
import { AppFontSize } from "../../../utils/size";
export interface IconProps extends TextProps {
@@ -43,11 +44,22 @@ export interface IconProps extends TextProps {
*
*/
color?: ColorValue | number | undefined;
iconFamily?: "evilicons" | "material";
}
export default function AppIcon(props: IconProps) {
export default function AppIcon({
iconFamily = "material",
...props
}: IconProps) {
const { colors } = useThemeColors();
return (
return iconFamily === "evilicons" ? (
<EvilIcon
size={AppFontSize.md}
color={colors.primary.icon}
{...(props as any)}
/>
) : (
<Icon
size={AppFontSize.md}
color={colors.primary.icon}

View File

@@ -0,0 +1,411 @@
/*
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,
TextStyle,
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;
errorStyle?: TextStyle;
}
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,
errorStyle,
...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 }, errorStyle]}
>
<AppIcon
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{fieldError}
</Paragraph>
) : null}
</View>
);
}
export default FormInput;

View File

@@ -268,8 +268,8 @@ export const Pressable = ({
const opacity = customOpacity
? customOpacity
: type === "accent"
? 1
: colorOpacity;
? 1
: colorOpacity;
const alpha = customAlpha ? customAlpha : isDark ? 0.03 : -0.03;
const { fontScale } = useWindowDimensions();
const growFactor = 1 + (fontScale - 1) / 8;

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

@@ -19,13 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [
{
title: "Play audio files",
body: "You can now play audio attachments in the editor"
},
{
title: "Password change & account recovery re-enabled",
body: "We have re-enabled password change and account recovery after making some improvements"
}
];
export const features: FeatureType[] = [];

View File

@@ -21,6 +21,7 @@ import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
import {
Color,
createInternalLink,
isEncryptedContent,
Item,
ItemReference,
Note,
@@ -32,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";
@@ -44,11 +45,11 @@ import ExportNotesSheet from "../components/sheets/export-notes";
import PaywallSheet from "../components/sheets/paywall";
import PublishNoteSheet from "../components/sheets/publish-note";
import { ReferencesList } from "../components/sheets/references";
import { RelationsList } from "../components/sheets/relations-list/index";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
import { ButtonProps } from "../components/ui/button";
import AddReminder from "../screens/add-reminder";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import RelationsList from "../screens/relations-list";
import {
eSendEvent,
eSubscribeEvent,
@@ -71,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";
@@ -195,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)
@@ -221,7 +222,15 @@ export const useActions = ({
useEffect(() => {
if (item.type === "note") {
db.vaults.itemExists(item).then((locked) => setLocked(locked));
db.vaults.itemExists(item).then(async (locked) => {
setLocked(locked);
if (!locked) {
const content = await db.content.findByNoteId(item.id);
if (content && isEncryptedContent(content)) {
setLocked(true);
}
}
});
}
}, [item]);
@@ -359,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
@@ -373,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);
@@ -758,6 +784,12 @@ export const useActions = ({
const duplicateNote = async () => {
await db.notes.duplicate(item.id);
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: strings.noteDuplicated(),
type: "success",
context: "local"
});
await sleep(500);
close();
};
@@ -1021,8 +1053,9 @@ export const useActions = ({
title: strings.dataTypesPluralCamelCase.reminder(),
icon: "clock-outline",
onPress: async () => {
close();
RelationsList.present({
reference: item,
item,
referenceType: "reminder",
relationType: "from",
title: strings.dataTypesPluralCamelCase.reminder(),
@@ -1039,26 +1072,6 @@ export const useActions = ({
}
AddReminder.present(undefined, item);
close();
},
button: {
type: "plain",
onPress: async () => {
if (features && !features.activeReminders.isAllowed) {
ToastManager.show({
type: "info",
message: features.activeReminders.error,
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features.activeReminders);
}
});
return;
}
AddReminder.present(undefined, item);
close();
},
icon: "plus",
iconSize: 20
}
});
},
@@ -1102,8 +1115,9 @@ export const useActions = ({
id: "add-reminder",
title: strings.remindMe(),
icon: "clock-plus-outline",
onPress: () => {
onPress: async () => {
close();
await sleep(100);
AddReminder.present(undefined, item);
}
},
@@ -1228,7 +1242,8 @@ export const useActions = ({
: strings.moveToTrash(),
icon: "delete-outline",
type: "error",
onPress: deleteItem
onPress: deleteItem,
locked: isPublished
});
}
@@ -1266,7 +1281,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;

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