Compare commits

...

147 Commits

Author SHA1 Message Date
01zulfi
fef1bd5a4a web: redesign payment pages
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-08-05 11:07:09 +05:00
Ammar Ahmed
d69a951584 Merge branch 'beta' into feat/new-ui-overhaul 2026-07-30 12:38:24 +05:00
Ammar Ahmed
e5b97abe0d Merge branch 'master' into beta 2026-07-30 12:35:39 +05:00
Abdullah Atta
f7c755f2d6 editor: sort code block languages by recently used (#10144) 2026-07-30 08:57:38 +05:00
Kashaf Ansari
10a08d5719 mobile: fix add notes to notebook selection behavior (#10104)
* mobile: fix add notes to notebook selection behavior

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: improve move notes keyboard interactions

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: fix bottom inset when keyboard is open

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-07-30 08:56:18 +05:00
Kashaf Ansari
6e10ce8d8e mobile: add sync status button to side menu (#10112)
* mobile: add sync status button to side menu

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: minor fixes

* mobile: hide sync status during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: skipped sync during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-07-30 08:54:05 +05:00
Ammar Ahmed
ab133497f7 mobile: cleanup 2026-07-29 14:58:20 +05:00
Ammar Ahmed
3209b44881 mobile: minor ui fixes 2026-07-29 14:56:38 +05:00
Ammar Ahmed
9f80758c1e mobile: fix deleteAll dialog button color 2026-07-29 14:56:38 +05:00
Ammar Ahmed
82da1aa6c6 mobile: fix 2fa auth app setup copy code/send code 2026-07-29 14:56:38 +05:00
Ammar Ahmed
e1d64a0279 mobile: ui updates 2026-07-29 14:56:36 +05:00
Ammar Ahmed
042d9818a7 global: delete agent.md file from root 2026-07-29 14:55:59 +05:00
01zulfi
443192d652 web: redesign auth pages (#10107)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: auth pages design fixes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: minor ui fixes

web: update button text in auth container
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: define & use new button variants
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: enhance MFA code component styling and layout
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

theme: set button's line height to 100% for new variants
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: retain email/password field data when navigating back from mfa screens
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-29 14:55:55 +05:00
Ammar Ahmed
620ecfd4ac mobile: update configure-toolbar design 2026-07-29 14:51:41 +05:00
Ammar Ahmed
8c3fbf832b mobile: settings-module fixes 2026-07-29 14:49:01 +05:00
Ammar Ahmed
b304ae4651 mobile: settings module fixes 2026-07-29 14:48:52 +05:00
Ammar Ahmed
75cf197e81 mobile: settings module ui fixes 2026-07-29 14:48:52 +05:00
Ammar Ahmed
4f4749be01 mobile: minor fixes in settings 2026-07-29 14:48:52 +05:00
Ammar Ahmed
ecaa5a82ab mobile: fix App version is not fully visible at the bottom of Settings screen 2026-07-29 14:48:52 +05:00
Ammar Ahmed
bc02916243 mobile: update toast message ui 2026-07-29 14:48:52 +05:00
Ammar Ahmed
8ce8b57fc5 mobile: change date-picker library in the app 2026-07-29 14:48:50 +05:00
Ammar Ahmed
59518fb863 mobile: add reminder screen 2026-07-29 14:47:28 +05:00
Ammar Ahmed
d9d5922598 mobile: fix list headers 2026-07-29 14:46:35 +05:00
Ammar Ahmed
74bf62de05 mobile: minor fixes in settings screen 2026-07-29 14:46:35 +05:00
01zulfi
ec2fb30bfe ui: rewrap @theme-ui's remaining component (#10073)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-29 14:46:35 +05:00
01zulfi
3a221baa59 ui: rewrap @theme-ui's flex, box, slider && add theme colors strict typecheck (#10067)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-29 14:46:35 +05:00
01zulfi
44628083bf ui: rewrap @theme-ui's button component (#10063)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-29 14:46:35 +05:00
01zulfi
342a1ee5ff theme: add fontsize, spacing, radius variables (#10062)
* theme: add fontsize, spacing, radius variables
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* Apply suggestion from @thecodrr

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

* 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 <abdullahatta@streetwriters.co>
2026-07-29 14:46:35 +05:00
Ammar Ahmed
357da2d850 mobile: fix trash and note item ui 2026-07-29 14:46:35 +05:00
Ammar Ahmed
31631b5168 mobile: update ui 2026-07-29 14:46:33 +05:00
Ammar Ahmed
e9d968aa8a mobile: update ui 2026-07-29 14:37:53 +05:00
Ammar Ahmed
368e9b6fe1 mobile: match recovery code screen with design 2026-07-29 14:30:59 +05:00
Ammar Ahmed
fefb4a05c0 mobile: fix text cut off on setting up your account screen 2026-07-29 14:30:59 +05:00
Ammar Ahmed
815d96a638 mobile: update ui 2026-07-29 14:30:58 +05:00
Ammar Ahmed
86334e9015 mobile: update ui 2026-07-29 14:22:33 +05:00
Ammar Ahmed
73fd802565 mobile: fix sort sheet ui 2026-07-29 14:20:25 +05:00
Ammar Ahmed
889bc32cfb mobile: fix note item ui 2026-07-29 14:20:25 +05:00
Ammar Ahmed
60e4996c89 mobile: update ui to match design 2026-07-29 14:20:25 +05:00
Ammar Ahmed
3f8b021580 mobile: cleanup 2026-07-29 14:20:25 +05:00
Ammar Ahmed
aee07c4be3 mobile: push ui updates 2026-07-29 14:20:23 +05:00
kashaf-ansari-dev
7697cfd9c2 mobile: fix color spacing notebook filter input
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-29 14:15:03 +05:00
kashaf-ansari-dev
9db2bd2b3c mobile: fix notebook filter input keyboard hiding
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-29 14:15:03 +05:00
kashaf-ansari-dev
88d080a660 mobile: ui fixes side menu items and notebook items
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-29 14:15:03 +05:00
Ammar Ahmed
dffe7c4ffa mobile: minor fixes 2026-07-29 14:15:03 +05:00
Ammar Ahmed
e24de22486 mobile: fix minor ui issues 2026-07-29 14:15:03 +05:00
Ammar Ahmed
5af77409d9 common: fix incorrect title for createNoteFromNotificationDrawer feature 2026-07-29 14:15:03 +05:00
Ammar Ahmed
98a7e5f7d5 mobile: fix 2fa method selection ui 2026-07-29 14:15:03 +05:00
Ammar Ahmed
52f0bd817b mobile: fix missing command error 2026-07-29 14:15:03 +05:00
Ammar Ahmed
79479b9347 mobile: update lockfiles 2026-07-29 14:15:01 +05:00
Ammar Ahmed
9325a4d4bb mobile: update mobile app settings ui (#9994)
* mobile: update onboarding screen ui

* mobile: update ui for paywall flow

* mobile: update ui

* mobile: update ui

* mobile: fix text capitalization

* mobile: fix Login screen text capitalization and eye icon mismatch with design

* mobile: fix two-factor ui

* mobile: ui fixes

* mobile: update basic settings ui and reorganize settings screen

* mobile: update change-password & recovery-key sheets

* mobile: update 2fa sheets design

* mobile: add force sync sheets

* mobile: replace hard coded strings

* mobile: update theme-selector and notesnook circle ui

* mobile: update behavior settings

* mobile: complete settings module

* mobile: fix errors

* mobile: settings ui update

* mobile: add app-lock password change screens

* mobile: add inbox api

* mobile: finalize settings ui
2026-07-29 14:14:38 +05:00
Ammar Ahmed
a91966d85f mobile: fix password placeholders 2026-07-29 13:36:37 +05:00
Ammar Ahmed
e0bd7ee2b5 mobile: undo hardcoded settings.introCompleted. 2026-07-29 13:36:37 +05:00
Ammar Ahmed
c54a4c3431 mobile: fix button color 2026-07-29 13:36:37 +05:00
Ammar Ahmed
ff1b3f3bb7 mobile: ui fixes 2026-07-29 13:36:36 +05:00
Ammar Ahmed
11b0032e57 mobile: fix setting up account svg colors in light mode 2026-07-29 13:35:42 +05:00
Ammar Ahmed
d782392572 mobile: minor adjustments 2026-07-29 13:35:42 +05:00
Ammar Ahmed
666e5a15d8 mobile: make intro illustration work with light mode 2026-07-29 13:35:42 +05:00
Ammar Ahmed
3550cef65b mobile: minor ui fixes 2026-07-29 13:35:42 +05:00
Ammar Ahmed
2e0bbcfec1 mobile: support dark/light mode on paywall logos 2026-07-29 13:35:42 +05:00
Ammar Ahmed
73c805dc78 mobile: show setup account screen after signup 2026-07-29 13:35:42 +05:00
Ammar Ahmed
9320c6cf4e mobile: fix pin input recovery code 2026-07-29 13:35:42 +05:00
Ammar Ahmed
4c23f889db mobile: fix two-factor ui 2026-07-29 13:35:40 +05:00
Ammar Ahmed
19b76c7aa6 mobile: fix button and text alignment in compare plans screen 2026-07-29 13:27:12 +05:00
Ammar Ahmed
4f2729bda9 mobile: fix login allowed without entering password field 2026-07-29 13:27:12 +05:00
Ammar Ahmed
48b74d4e07 mobile: fix Login screen text capitalization and eye icon mismatch with design 2026-07-29 13:27:10 +05:00
Ammar Ahmed
266fe5c570 mobile: fix Resend code button not working on 2FA screen 2026-07-29 13:25:51 +05:00
Ammar Ahmed
330fa2a5f6 mobile: update intl strings 2026-07-29 13:25:51 +05:00
Ammar Ahmed
78058dcf8e common: match feature titles with design 2026-07-29 13:25:51 +05:00
Ammar Ahmed
9d50021b36 mobile: fix Extra punctuation in “Setting up your account” screen text 2026-07-29 13:25:51 +05:00
Ammar Ahmed
c18b5a3a3d mobile: hide progress pills in setting up your account if flow is not introductory 2026-07-29 13:25:51 +05:00
Ammar Ahmed
13166f4160 mobile: fix wording if user already has an account 2026-07-29 13:25:51 +05:00
Ammar Ahmed
b4db3f1cac mobile: fix signup input secure entry icon 2026-07-29 13:25:51 +05:00
Ammar Ahmed
0941822dfc mobile: fix text capitalization 2026-07-29 13:25:51 +05:00
Ammar Ahmed
3430c469af mobile: fix colors in signup 2026-07-29 13:25:51 +05:00
Ammar Ahmed
919020d9f7 mobile: fix signup footer text and position 2026-07-29 13:25:51 +05:00
Ammar Ahmed
993ff3a622 mobile: fix illustration colors 2026-07-29 13:25:51 +05:00
Ammar Ahmed
06f7a2e59d mobile: fix errors 2026-07-29 13:25:49 +05:00
Ammar Ahmed
56ee375ee9 mobile: update ui 2026-07-29 13:25:40 +05:00
Ammar Ahmed
0902e0a58e mobile: update form input styles 2026-07-29 13:19:59 +05:00
Ammar Ahmed
dc5ea1ae86 mobile: updated colors 2026-07-29 13:19:59 +05:00
Ammar Ahmed
7ea357a324 theme: add new variant & colors 2026-07-29 13:19:59 +05:00
Ammar Ahmed
ae1f5d9352 mobile: update svg logos in paywall 2026-07-29 13:19:59 +05:00
Ammar Ahmed
09644bb4c5 mobile: enable repack commands 2026-07-29 13:19:59 +05:00
Ammar Ahmed
99ee4ed083 mobile: fix ts errors 2026-07-29 13:19:59 +05:00
Ammar Ahmed
0ca134eb32 mobile: cleanup 2026-07-29 13:19:59 +05:00
Ammar Ahmed
67dc2ddaff mobile: update ui 2026-07-29 13:19:57 +05:00
Ammar Ahmed
edd50110a7 mobile: update ui for paywall flow 2026-07-29 13:15:59 +05:00
Ammar Ahmed
0447fea3d4 mobile: update ui for auth screens 2026-07-29 13:15:27 +05:00
Ammar Ahmed
bd4ba27166 mobile: update onboarding screen ui 2026-07-29 13:15:27 +05:00
Ammar Ahmed
7cdb9b853c mobile: add style guide baseline 2026-07-29 13:10:17 +05:00
Ammar Ahmed
ae1811cc4a Merge branch 'master' into beta 2026-07-29 12:50:50 +05:00
Abdullah Atta
b5140d9947 Merge pull request #10159 from streetwriters/release/3.4.5
web: bump version to 3.4.5
2026-07-28 21:09:08 +05:00
Abdullah Atta
2288b946ad desktop: launch app with --hidden flag for tests 2026-07-28 19:53:37 +05:00
Abdullah Atta
5fd43ae08e desktop: fix auto start with minimized window on macos 2026-07-28 19:53:37 +05:00
Abdullah Atta
729c410602 global: update intl & lockfiles 2026-07-28 19:53:37 +05:00
Abdullah Atta
bb30336f5c desktop: add tests for #9942 2026-07-28 19:52:48 +05:00
01zulfi
0dc89ba945 web: bump version to 3.4.5
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-28 19:52:47 +05:00
Abdullah Atta
bb51244fa3 editor: update prosemirror packages (#10158)
* editor: update prosemirror packages

* editor: fix `__serializeForClipboard` usage
2026-07-28 19:46:51 +05:00
Abdullah Atta
b5ac41c29b web: improve "skip & go directly to app" wording (#10167) 2026-07-28 19:46:23 +05:00
01zulfi
aafb6049b2 core: delete all vaults when user triggers their vault deletion (#9939)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-28 19:45:30 +05:00
Abdullah Atta
32070327f9 core: add logs to diagnose why decryption fails after changing password (#10139) 2026-07-27 10:20:25 +05:00
Abdullah Atta
771a8bb4e3 web: fix editor undo history reset on image delete (#10143) 2026-07-25 08:55:58 +05:00
Ammar Ahmed
4f816b47ba ci: fix ios build entitlements (#10146) 2026-07-24 18:55:11 +05:00
01zulfi
1071249baa desktop: fix app not appearing in alt-tab switcher (#9764)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-24 13:05:59 +05:00
Ammar Ahmed
d4775ad340 Merge pull request #10133 from streetwriters/chore/update-workflows
ci: publish ios preview app on linux runner
2026-07-24 12:10:35 +05:00
Chloe Oletto
0d8c855cd0 desktop: If --hidden passed via cli and any system tray option enabled, don't show the window. streetwriters/notesnook#9902 flathub/com.Notesnook.notesnook#99 (#9942)
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-07-24 09:16:17 +05:00
Ammar Ahmed
d63ce5a213 Merge pull request #10126 from streetwriters/mobile-release/3.4.7
mobile: release v3.4.7
2026-07-22 11:12:45 +05:00
Ammar Ahmed
a1d6bb91e5 ci: fix ci workflow 2026-07-22 10:53:18 +05:00
Ammar Ahmed
a20f6960d7 ci: fix workflow 2026-07-22 10:45:49 +05:00
Ammar Ahmed
6b2b224f41 ci: fix ios workflow 2026-07-22 10:37:31 +05:00
Ammar Ahmed
054a023142 ci: fix intermittent ios preview build failure 2026-07-22 10:13:16 +05:00
Ammar Ahmed
441a6396be ci: publish ios preview app on linux runner 2026-07-22 10:02:18 +05:00
Chloe Oletto
987671e59c web: fix web-clipper crash on clipped file. (#10127)
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-07-21 12:45:13 +05:00
Ammar Ahmed
0fc705ce1e Merge pull request #10130 from streetwriters/chore/update-workflows
ci: update mobile workflows
2026-07-21 12:41:02 +05:00
Ammar Ahmed
b3805534d6 ci: remove authorize step in android/ios preview builds 2026-07-21 12:35:08 +05:00
Ammar Ahmed
77c009e83d mobile: fix toast message shows under the sheet in global context 2026-07-21 12:14:08 +05:00
Ammar Ahmed
c9f1cba702 ci: update mobile workflows 2026-07-21 11:36:23 +05:00
Ammar Ahmed
637cf914ee mobile: fix app gets stuck after deleting history item on iOS 2026-07-21 10:25:12 +05:00
Ammar Ahmed
64877c519b Merge pull request #10128 from streetwriters/chore/update-workflows
ci: update workflows to use actions/checkout@v5
2026-07-21 10:04:20 +05:00
Ammar Ahmed
229b620473 ci: update workflows to use actions/checkout@v5 2026-07-21 09:27:49 +05:00
Ammar Ahmed
658e915589 mobile: release v3.4.7 2026-07-20 13:12:20 +05:00
Ammar Ahmed
d4658aa032 Merge pull request #10095 from kashaf-ansari-dev/fix-reminder-notification-permission
Improve the Android reminder notification permission flow by properly handling notification and exact alarm permissions.
2026-07-17 09:10:57 +05:00
Ammar Ahmed
489c793806 Merge pull request #10041 from kashaf-ansari-dev/feat/9723-clear-note-history
mobile: add option to clear note version history
2026-07-15 09:52:04 +05:00
kashaf-ansari-dev
eeae993736 mobile: add version history deletion strings
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
c8cf4ce52a mobile: fix individual note history version deletion
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
f6c31d19d8 mobile: add missing context prop in dialog
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
a94e8d6852 mobile: render clear-history dialog with local context
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
2edbc3bf2e mobile: remove clear version history action for multiple selected notes
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:41 +05:00
kashaf-ansari-dev
e4a7d63e07 mobile: add option to clear note version history
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-15 09:49:30 +05:00
Ammar Ahmed
76fd61af4f Merge pull request #10085 from streetwriters/fix/651
mobile: fix unable to change plan on buy-plan screen
2026-07-14 08:43:53 +05:00
Ammar Ahmed
98386a6d71 Merge pull request #10094 from streetwriters/mobile-release/3.4.6
mobile: release v3.4.6
2026-07-13 22:44:13 +05:00
kashaf-ansari-dev
539a630f77 mobile: improve reminder permission settings flow on Android
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-13 18:07:40 +05:00
kashaf-ansari-dev
6b65f8031d mobile: fix reminder creation after granting notification permission
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-13 14:58:31 +05:00
01zulfi
3c85cddfe7 Merge pull request #10093 from streetwriters/release/3.4.4
web: bump version to 3.4.4
2026-07-13 14:14:25 +05:00
Ammar Ahmed
e426426468 mobile: release v3.4.6 2026-07-13 12:56:49 +05:00
01zulfi
109e46bd06 web: bump version to 3.4.4
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-13 12:28:07 +05:00
Ammar Ahmed
281a973c37 mobile: fix unable to change plan on buy-plan screen 2026-07-11 12:00:15 +05:00
Ammar Ahmed
2738207c20 Merge pull request #9998 from kashaf-ansari-dev/fix-9967-linking-note-blocks-loading-state
mobile: prevent upgrade prompt flash in note block linking list
2026-07-10 18:41:04 +05:00
01zulfi
1aa73c1f23 web: update editor's font size dynamically when default font size setting changes (#10047)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-10 09:39:21 +05:00
Masatoshi Ogiwara
9ec22724e9 fix: typo in some documents (#10071) 2026-07-09 10:34:09 +05:00
Kashaf Ansari
4a7aa27bd9 mobile: add keep-screen-on preference in settings-behaviour (#10043)
* mobile: add keep-screen-on preference in settings-behaviour

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: default Keep Screen On to true and clarify description

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-09 09:49:38 +05:00
kashaf-ansari-dev
6051614d96 mobile: add bottom spacing for create link button
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-08 18:23:23 +05:00
kashaf-ansari-dev
295a461dda mobile: wire upgrade plan button to paywall on link-note
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-08 17:55:00 +05:00
Chloe Oletto
a4cbe83211 vericrypt: Update to support DEK. (#10005)
* vericrypt: Update to support DEK.

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

* vericrypt: Clear account data on restart.

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

* Update apps/vericrypt/src/components/decrypted-result.tsx

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Update data encryption documentation to be accurate with the current client versions.

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

* docs: Explain what happens better.

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

* Fix typo in encryption key generation section

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

* Update docs/help/contents/how-is-my-data-encrypted.md

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: update wording

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

---------

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-07-08 10:24:17 +05:00
Masatoshi Ogiwara
afd63db043 docs(help): typo in documents (#10064) 2026-07-08 08:41:17 +05:00
Ammar Ahmed
774d612bf2 misc: cleanup 2026-07-07 09:50:36 +05:00
kashaf-ansari-dev
e0b9932aba mobile: prevent upgrade prompt flash in note block linking list
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-19 22:31:56 +05:00
555 changed files with 81381 additions and 26942 deletions

View File

@@ -10,7 +10,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Enable KVM
run: |

View File

@@ -0,0 +1,104 @@
name: Notesnook Android Preview Build
# UNTRUSTED stage. Runs on `pull_request`, so fork code is checked out and
# compiled with a read-only GITHUB_TOKEN and NO repository secrets. The release
# APK is built without any signing secret (the same as before). Firebase
# distribution and the PR comment happen in android.preview.publish.yml, which
# runs in the trusted `workflow_run` context and never executes fork code.
# Because no secrets are exposed here, the build runs automatically for every
# PR (including forks) with no authorization gate.
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/android.preview.build.yml"
- ".github/workflows/android.preview.publish.yml"
concurrency:
group: android-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-22.04
env:
STAGING_BUILD: true
steps:
- name: Checkout PR code
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- 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:
tool-cache: false
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: Stage build artifact
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/artifact"
cp apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk \
"$RUNNER_TEMP/artifact/app-preview.apk"
{
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
echo "HEAD_SHA=${{ github.event.pull_request.head.sha }}"
} > "$RUNNER_TEMP/artifact/pr-meta.env"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: android-preview-build
path: ${{ runner.temp }}/artifact
if-no-files-found: error
retention-days: 1
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/android/app/build/generated/sourcemaps/**/*.map

View File

@@ -1,135 +0,0 @@
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

View File

@@ -0,0 +1,72 @@
name: Notesnook Android Preview Publish
# TRUSTED stage. Runs via `workflow_run` after the build workflow finishes, so
# it has the base repo's secrets and a write-scoped token. It downloads the APK
# the build produced and distributes it + posts the PR comment. It never checks
# out or executes fork code.
on:
workflow_run:
workflows: ["Notesnook Android Preview Build"]
types: [completed]
jobs:
publish:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: android-preview-build
path: ${{ runner.temp }}/artifact
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Load PR metadata
run: cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_ENV"
- name: Publish to Firebase
id: firebase-output
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
groups: testers
file: ${{ runner.temp }}/artifact/app-preview.apk
releaseNotes: Preview for https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }}
- name: Post or update PR comment
uses: actions/github-script@v7
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
with:
script: |
const marker = '<!-- android-preview-comment -->';
const prNumber = Number(process.env.PR_NUMBER);
if (!prNumber) return;
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.HEAD_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

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -35,7 +35,7 @@ jobs:
needs: authorize
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -56,7 +56,7 @@ jobs:
macos-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -127,7 +127,7 @@ jobs:
linux-x64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -171,7 +171,7 @@ jobs:
linux-arm64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -215,7 +215,7 @@ jobs:
windows-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -142,7 +142,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -246,7 +246,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -314,7 +314,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -378,7 +378,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2

View File

@@ -24,7 +24,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -58,7 +58,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -92,7 +92,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -126,7 +126,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- uses: actions-rs/toolchain@v1
with:
toolchain: stable

152
.github/workflows/ios.preview.build.yml vendored Normal file
View File

@@ -0,0 +1,152 @@
name: Notesnook iOS Preview Build
# UNTRUSTED stage. Runs on `pull_request`, so fork code is checked out and
# compiled with a read-only GITHUB_TOKEN and NO repository secrets. It only
# produces an *unsigned* archive. Signing, Firebase distribution and PR
# comments happen in ios.preview.publish.yml, which runs in the trusted
# `workflow_run` context and never executes fork code. Because no secrets are
# exposed here, the build runs automatically for every PR (including forks)
# with no authorization gate.
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "apps/mobile/**"
- "packages/**"
- ".github/workflows/ios.preview.build.yml"
- ".github/workflows/ios.preview.publish.yml"
# A fork that spams pushes shouldn't queue up macOS builds.
concurrency:
group: ios-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
build:
runs-on: macos-26
timeout-minutes: 60
steps:
- name: Checkout PR code
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- 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: |
# GitHub runs this as `/bin/bash -e`, so `set +e` is required -- without
# it the first failure aborts the step before any retry can happen.
# Both commands hit "Unable to connect to simulator" (exit 70) when
# CoreSimulatorService is wedged, so retry each and bounce it between.
set +e
DL_DIR="$RUNNER_TEMP/ios-platform"
retry() {
for i in 1 2 3; do
"$@" && return 0
echo "Attempt $i failed ($*); resetting CoreSimulator..."
sudo killall -9 com.apple.CoreSimulator.CoreSimulatorService simdiskimaged 2>/dev/null
sleep 20
xcrun simctl list runtimes >/dev/null 2>&1
done
echo "::error::Failed after 3 attempts: $*"
exit 1
}
xcrun simctl list runtimes >/dev/null 2>&1
retry xcodebuild -downloadPlatform iOS -exportPath "$DL_DIR"
# Find the dmg; its name embeds a build number that changes.
DMG="$(find "$DL_DIR" -name '*.dmg' | head -n1)"
[ -n "$DMG" ] || { echo "::error::No dmg in $DL_DIR"; exit 1; }
retry xcodebuild -importPlatform "$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@v4
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}}
# Archive unsigned: this job has no certs/profiles (secrets), and the iOS
# device SDK forbids ad-hoc signing, so real signing happens in the trusted
# publish workflow (ios.preview.publish.yml). Do NOT pass
# CODE_SIGN_ENTITLEMENTS="" -- each target must keep its own entitlements so
# the archive records them; blanking them dropped the App Group and crashed
# MMKV on launch.
- name: Archive (unsigned)
run: |
set -euo pipefail
xcodebuild \
-workspace apps/mobile/ios/Notesnook.xcworkspace \
-scheme Notesnook \
-configuration Release \
-sdk iphoneos \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/Notesnook.xcarchive" \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_IDENTITY="" \
archive
- name: Stage build artifact
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/artifact"
tar -czf "$RUNNER_TEMP/artifact/Notesnook.xcarchive.tar.gz" \
-C "$RUNNER_TEMP" Notesnook.xcarchive
# Carry the PR context forward; workflow_run cannot see it reliably for forks.
{
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
echo "HEAD_SHA=${{ github.event.pull_request.head.sha }}"
} > "$RUNNER_TEMP/artifact/pr-meta.env"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: ios-preview-build
path: ${{ runner.temp }}/artifact
if-no-files-found: error
retention-days: 1

View File

@@ -1,173 +0,0 @@
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

@@ -0,0 +1,222 @@
name: Notesnook iOS Preview Publish
# TRUSTED stage. Runs via `workflow_run` after the build workflow finishes, so
# it has the base repo's secrets and a write-scoped token. It checks out the
# BASE repository (never fork code), downloads the unsigned archive the build
# produced, then signs + distributes it and posts the PR comment. Signing is
# done directly with `codesign` on the prebuilt archive -- no app build phases
# run here, so fork code is never executed in this trusted context.
#
# Split into two jobs on purpose: signing needs macOS, but the Firebase
# distribution action is a Docker container action and only runs on Linux.
on:
workflow_run:
workflows: ["Notesnook iOS Preview Build"]
types: [completed]
jobs:
export:
# Only publish previews for builds that actually succeeded.
if: github.event.workflow_run.conclusion == 'success'
runs-on: macos-26
timeout-minutes: 30
outputs:
pr_number: ${{ steps.meta.outputs.PR_NUMBER }}
head_sha: ${{ steps.meta.outputs.HEAD_SHA }}
steps:
- name: Checkout base repo (trusted)
uses: actions/checkout@v5
# No `ref` -> checks out the default branch, i.e. trusted base code.
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.1.1"
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: ios-preview-build
path: ${{ runner.temp }}/artifact
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Load PR metadata & extract archive
id: meta
run: |
set -euo pipefail
cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_OUTPUT"
tar -xzf "$RUNNER_TEMP/artifact/Notesnook.xcarchive.tar.gz" -C "$RUNNER_TEMP"
- name: Import signing certificate
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
- name: Install provisioning profiles
env:
PROFILE_APP: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }}
PROFILE_SHARE: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }}
PROFILE_WIDGET: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }}
run: |
set -euo pipefail
PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILE_DIR"
i=0
for p in "$PROFILE_APP" "$PROFILE_SHARE" "$PROFILE_WIDGET"; do
echo "$p" | base64 --decode > "$PROFILE_DIR/preview-$i.mobileprovision"
i=$((i + 1))
done
# Sign the unsigned archive by hand. `xcodebuild -exportArchive` re-signs
# from the archive's `archived-expanded-entitlements.xcent`, which an
# unsigned archive does not contain, so it dropped the App Group and the
# app crashed on launch. Instead we sign each bundle directly, taking the
# entitlements from each provisioning profile (exactly what Apple
# provisioned -- App Group, keychain groups with the right team prefix).
# Runs on macOS but executes NO fork build phases, so it stays trusted.
- name: Sign IPA
run: |
set -euo pipefail
PROFILES="$HOME/Library/MobileDevice/Provisioning Profiles"
APP="$(find "$RUNNER_TEMP/Notesnook.xcarchive/Products/Applications" -maxdepth 1 -name '*.app' | head -n1)"
[ -n "$APP" ] || { echo "::error::No .app in archive"; exit 1; }
# Single Apple Distribution identity from the imported keychain.
IDENTITY="$(security find-identity -v -p codesigning | awk '/Apple Distribution/{print $2; exit}')"
[ -n "$IDENTITY" ] || { echo "::error::No Apple Distribution identity found"; exit 1; }
echo "Signing identity: $IDENTITY"
TMP="$RUNNER_TEMP/sign"; mkdir -p "$TMP"
# Decode a profile's bundle id (application-identifier minus team prefix).
profile_bundle_id() {
security cms -D -i "$1" -o "$TMP/_p.plist" 2>/dev/null
local appid; appid="$(plutil -extract Entitlements.application-identifier raw -o - "$TMP/_p.plist")"
echo "${appid#*.}"
}
# Find the profile matching a bundle id; echoes its path.
profile_for() {
local want="$1" p
for p in "$PROFILES"/*.mobileprovision; do
[ -e "$p" ] || continue
[ "$(profile_bundle_id "$p")" = "$want" ] && { echo "$p"; return 0; }
done
return 1
}
# Embed the matching profile and sign a bundle with the profile's entitlements.
sign_bundle() {
local bundle="$1" bid prof ent
bid="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$bundle/Info.plist")"
prof="$(profile_for "$bid")" || { echo "::error::No provisioning profile for $bid"; exit 1; }
ent="$TMP/ent-$bid.plist"
security cms -D -i "$prof" -o "$TMP/_p.plist"
plutil -extract Entitlements xml1 -o "$ent" "$TMP/_p.plist"
cp "$prof" "$bundle/embedded.mobileprovision"
echo "Signing $(basename "$bundle") ($bid)"
codesign --force --sign "$IDENTITY" --entitlements "$ent" "$bundle"
}
# Sign inside-out: nested code first, then extensions, then the app.
if [ -d "$APP/Frameworks" ]; then
find "$APP/Frameworks" -maxdepth 1 \( -name '*.framework' -o -name '*.dylib' \) -print0 \
| while IFS= read -r -d '' f; do codesign --force --sign "$IDENTITY" "$f"; done
fi
if [ -d "$APP/PlugIns" ]; then
for appex in "$APP/PlugIns"/*.appex; do
[ -e "$appex" ] || continue
if [ -d "$appex/Frameworks" ]; then
find "$appex/Frameworks" -maxdepth 1 \( -name '*.framework' -o -name '*.dylib' \) -print0 \
| while IFS= read -r -d '' f; do codesign --force --sign "$IDENTITY" "$f"; done
fi
sign_bundle "$appex"
done
fi
sign_bundle "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
# Package into an IPA.
rm -rf "$TMP/Payload"; mkdir -p "$TMP/Payload"
cp -R "$APP" "$TMP/Payload/"
( cd "$TMP" && zip -qry "$RUNNER_TEMP/Notesnook.ipa" Payload )
# Fail fast if the App Group didn't make it into the signed app -- its
# absence is the launch crash (MMKV nil group path). Never distribute
# a build without it.
echo "::group::Signed app entitlements"
codesign -d --entitlements :- "$APP" 2>/dev/null || true
echo "::endgroup::"
codesign -d --entitlements :- "$APP" 2>/dev/null | grep -q "group.org.streetwriters.notesnook" \
|| { echo "::error::Signed app is missing the App Group entitlement; it would crash on launch."; exit 1; }
echo "App Group entitlement present."
- name: Upload signed IPA
uses: actions/upload-artifact@v4
with:
name: ios-preview-ipa
path: ${{ runner.temp }}/Notesnook.ipa
if-no-files-found: error
retention-days: 1
# Must be Linux: wzieba/Firebase-Distribution-Github-Action is a Docker
# container action, which GitHub only supports on Linux runners.
distribute:
needs: export
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Download signed IPA
uses: actions/download-artifact@v4
with:
name: ios-preview-ipa
path: ${{ runner.temp }}
- name: Publish to Firebase
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: ${{ runner.temp }}/Notesnook.ipa
releaseNotes: Preview for https://github.com/${{ github.repository }}/pull/${{ needs.export.outputs.pr_number }}
- name: Post or update PR comment
uses: actions/github-script@v7
env:
preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
PR_NUMBER: ${{ needs.export.outputs.pr_number }}
HEAD_SHA: ${{ needs.export.outputs.head_sha }}
with:
script: |
const marker = '<!-- ios-preview-comment -->';
const prNumber = Number(process.env.PR_NUMBER);
if (!prNumber) return;
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.HEAD_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

@@ -9,7 +9,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Collect package metadata
id: package_metadata

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -19,7 +19,7 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
@@ -66,7 +66,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}

View File

@@ -42,6 +42,7 @@ export interface AppContext {
export interface TestOptions {
version: string;
args?: string[];
config?: Record<string, unknown>;
}
@@ -72,7 +73,8 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version
options?.version,
options?.args
);
const ctx: AppContext = {
app,
@@ -82,7 +84,8 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version
options?.version,
options?.args
);
ctx.app = app;
ctx.userDataDir = userDataDir;
@@ -94,11 +97,12 @@ export async function buildAndLaunchApp(
async function launchApp(
executablePath: string,
userDataDir: string,
version?: string
version?: string,
args: string[] = []
) {
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [] : ["--hidden"],
args: IS_DEBUG ? [...args] : ["--hidden", ...args],
baseURL: "https://app.notesnook.com",
acceptDownloads: true,
env: {
@@ -154,6 +158,7 @@ export async function buildApp(version?: string) {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
CSC_IDENTITY_AUTO_DISCOVERY: "false",
NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,

View File

@@ -17,7 +17,65 @@ 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 { test } from "@nn/test";
import { test, expect } from "@nn/test";
import type { ElectronApplication } from "@playwright/test";
async function getMainWindowState(app: ElectronApplication) {
return await app.evaluate((window) => {
const { BrowserWindow } = window;
const mainWindow = BrowserWindow.getAllWindows()[0];
if (!mainWindow) throw new Error("Main window not found");
return {
isMinimized: mainWindow.isMinimized(),
isVisible: mainWindow.isVisible()
};
});
}
test("make sure app loads", async ({ page }) => {
await page.waitForSelector(".ProseMirror");
});
test("hidden launch minimizes when tray is disabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: false
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(true);
});
test("hidden launch does not minimize when close-to-tray is enabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: true
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(false);
});

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.4.3",
"version": "3.4.5",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -92,6 +92,17 @@ async function createWindow() {
const cliOptions = await parseArguments(process.argv);
setTheme(getTheme());
// this workaround is necessary because macos doesn't support
// the --hidden flag when launching the app on startup
if (
process.platform === "darwin" &&
app.getLoginItemSettings().wasOpenedAtLogin &&
config.desktopSettings.autoStart &&
config.desktopSettings.startMinimized
) {
cliOptions.hidden = true;
}
const mainWindowState = new WindowState({});
const mainWindow = new BrowserWindow({
show: !cliOptions.hidden,
@@ -141,7 +152,13 @@ async function createWindow() {
mainWindow.setMenuBarVisibility(false);
mainWindowState.manage(mainWindow);
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
if (
cliOptions.hidden &&
!(
config.desktopSettings.minimizeToSystemTray ||
config.desktopSettings.closeToSystemTray
)
)
mainWindow.minimize();
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);
@@ -164,6 +181,14 @@ async function createWindow() {
);
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
mainWindow.on("show", () =>
/**
* We may set `skipTaskbar` to true at startup.
* This also removes the window from the Alt-Tab switcher.
* To fix that, whenever the app is shown, we set `skipTaskbar` to false.
*/
mainWindow.setSkipTaskbar(false)
);
mainWindow.once("closed", () => {
globalThis.window = null;
});

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
with:
persist-credentials: false

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
with:
persist-credentials: false

View File

@@ -0,0 +1,9 @@
{
"iconSets": [
{
"inputDir": "../../packages/icons/svgs",
"fontFamily": "notesnook-icons",
"outputDir": "./fonts"
}
]
}

View File

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

View File

@@ -1,3 +1,4 @@
- Add option to clear note version history
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -35,7 +35,7 @@ import { Toast } from "./components/toast";
import { useAppEvents } from "./hooks/use-app-events";
import { NotePreviewConfigure } from "./screens/note-preview-configure";
import { RootNavigation } from "./navigation/navigation-stack";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import { themeTrpcClient } from "./screens/settings/components/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
@@ -44,6 +44,7 @@ import { useUserStore } from "./stores/use-user-store";
import RNBootSplash from "react-native-bootsplash";
import AppLocked from "./components/app-lock";
import { useSettingStore } from "./stores/use-setting-store";
import { THEME_DARK, THEME_LIGHT } from "./common/design/theme";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
@@ -120,30 +121,30 @@ export const withTheme = (
]);
useEffect(() => {
setTimeout(() => {
const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
if (!currentTheme) return;
themeTrpcClient.updateTheme
.query({
version: currentTheme.version,
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
id: currentTheme.id
})
.then((theme) => {
if (theme) {
theme.colorScheme === "dark"
? useThemeStore.setState({
darkTheme: theme
})
: useThemeStore.setState({
lightTheme: theme
});
}
})
.catch(() => {
/* empty */
});
}, 1000);
// setTimeout(() => {
// const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
// if (!currentTheme) return;
// themeTrpcClient.updateTheme
// .query({
// version: currentTheme.version,
// compatibilityVersion: THEME_COMPATIBILITY_VERSION,
// id: currentTheme.id
// })
// .then((theme) => {
// if (theme) {
// theme.colorScheme === "dark"
// ? useThemeStore.setState({
// darkTheme: theme
// })
// : useThemeStore.setState({
// lightTheme: theme
// });
// }
// })
// .catch(() => {
// /* empty */
// });
// }, 1000);
const listener = Appearance.addChangeListener(({ colorScheme }) => {
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
@@ -159,11 +160,11 @@ export const withTheme = (
}, []);
useEffect(() => {
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
const nextTheme = colorScheme === "dark" ? THEME_DARK : THEME_LIGHT;
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
useThemeEngineStore
.getState()
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
.setTheme(colorScheme === "dark" ? THEME_DARK : THEME_LIGHT);
currTheme = nextTheme;
}
}, [colorScheme, darkTheme, lightTheme]);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
/*
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/>.
*/
export const FontSizes = {
XXS: 10,
XS: 12,
SM: 14,
MD: 16,
LG: 18,
XL: 20,
XXL: 32
};
export const FontFamily = {
REGULAR: "Inter-Regular", // 400
MEDIUM: "Inter-Medium", // 500
SEMI_BOLD: "Inter-SemiBold", // 600
BOLD: "Inter-Bold" // 700
};
const LineHeightMultipliers = {
"100%": 1,
"110%": 1.1,
"120%": 1.2,
"130%": 1.3,
"140%": 1.4,
"150%": 1.5
};
export type LineHeightVariants =
| "100%"
| "110%"
| "120%"
| "130%"
| "140%"
| "150%";
export const getLineHeight = (
fontSize: keyof typeof FontSizes,
type: LineHeightVariants
) => {
return FontSizes[fontSize] * LineHeightMultipliers[type];
};

View File

@@ -0,0 +1,39 @@
/*
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/>.
*/
export const Spacing = {
LEVEL_0: 4,
LEVEL_1: 8,
LEVEL_2: 12,
LEVEL_3: 16,
LEVEL_4: 20,
LEVEL_5: 24,
LEVEL_6: 28,
LEVEL_7: 32,
LEVEL_8: 40
};
export const Radius = {
XXS: 4,
XS: 8,
S: 12,
MD: 16,
LG: 20,
LG_2: 32,
XXL: 40
};

View File

@@ -0,0 +1,283 @@
/*
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 { ThemeDefinition } from "@notesnook/theme";
export const THEME_LIGHT: any = {
$schema:
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json",
name: "Notesnook Light",
id: "default-light",
version: 2.1,
license: "GPL-3.0-or-later",
homepage: "https://notesnook.com",
description: "The default light theme for Notesnook app.",
colorScheme: "light",
codeBlockCSS: ``,
compatibilityVersion: 1,
authors: [
{
name: "Streetwriters",
email: "support@streetwriters.co",
url: "https://streetwriters.co"
}
],
scopes: {
base: {
primary: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#666666",
background: "#ffffff",
border: "#E2E2E2",
heading: "#181818",
icon: "#181818",
separator: "#F0F0F0",
placeholder: "#858585",
hover: "#eee",
backdrop: "#0000001a",
buttonForeground: "#181818"
},
secondary: {
accent: "#0D9E46",
accentForeground: "#ffffff",
paragraph: "#858585",
background: "#F7F7F7",
border: "#EFEFEF",
heading: "#4B4B4B",
icon: "#666666",
separator: "#E8E8E8",
placeholder: "#999999",
hover: "#dbdbdb",
backdrop: "#0000001a",
buttonForeground: "#181818"
},
tertiary: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#777",
background: "#ECECEC",
border: "#E8E8E8",
heading: "#666",
icon: "#777",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#dbdbdb",
backdrop: "#0000001a"
},
disabled: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#A6A6A6",
background: "#ffffff",
border: "#E8E8E8",
heading: "#212121",
icon: "#DADADA",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#00000010",
backdrop: "#0000001a"
},
selected: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#181818",
background: "#00883610",
border: "#008836",
heading: "#212121",
icon: "#181818",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#eee",
backdrop: "#0000001a"
},
error: {
accent: "#f54b42",
accentForeground: "#ffffff",
paragraph: "#FF242E",
background: "#FFEDEE",
border: "#F7999D",
heading: "#f54b42",
icon: "#FB2C36",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#00000010",
backdrop: "#0000001a"
},
success: {
accent: "#4F8A10",
accentForeground: "#ffffff",
paragraph: "#4F8A10",
background: "#85ce68",
border: "#E8E8E8",
heading: "#4F8A10",
icon: "#4F8A10",
separator: "#E8E8E8",
placeholder: "#a9a9a9",
hover: "#00000010",
backdrop: "#0000001a"
}
},
statusBar: {
primary: { paragraph: "#5E5E5E", background: "#f7f7f7" }
},
navigationMenu: {
primary: {
background: "#f7f7f7",
paragraph: "#666",
icon: "#666",
hover: "#eee"
},
secondary: { background: "#EEEEEE" },
selected: {
background: "#00883610",
hover: "#eee"
}
},
contextMenu: { primary: { background: "#f7f7f7" } },
editor: { selected: { background: "#e1e1e1" } },
sheet: { selected: { paragraph: "#008836" } },
list: {
secondary: {
heading: "#A6A6A6"
}
}
}
};
export const THEME_DARK: any = {
name: "Notesnook Dark",
id: "default-dark",
version: 2.1,
license: "GPL-3.0-or-later",
homepage: "https://notesnook.com",
description: "The default dark theme for the Notesnook app",
colorScheme: "dark",
codeBlockCSS: ``,
compatibilityVersion: 1,
authors: [
{
name: "Streetwriters",
email: "support@streetwriters.co",
url: "https://streetwriters.co"
}
],
scopes: {
base: {
primary: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#F2F2F2",
background: "#181818",
border: "#2A2A2A",
heading: "#FFFFFF",
icon: "#FFFFFF",
separator: "#383838",
placeholder: "#404040",
hover: "#2b2b2b",
backdrop: "#35353580",
buttonForeground: "#FFFFFF"
},
secondary: {
accent: "#0D9E46",
accentForeground: "#ffffff",
paragraph: "#A1A1A1",
background: "#252525",
border: "#0A5729",
heading: "#C0BFBF",
icon: "#808080",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580",
buttonForeground: "#BFBFBF"
},
tertiary: {
accent: "#0D9E46",
accentForeground: "#ffffff",
paragraph: "#818589",
background: "#2C2C2C",
border: "#2b2b2b",
heading: "#808080",
icon: "#808080",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
},
disabled: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#505050",
background: "#ffffff",
border: "#383838",
heading: "#212121",
icon: "#FFFFFF",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
},
selected: {
accent: "#008836",
accentForeground: "#ffffff",
paragraph: "#FBFBFB",
background: "#494949",
border: "#008836",
heading: "#e3e3e3",
icon: "#008836",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
},
error: {
accent: "#f54b42",
accentForeground: "#ffffff",
paragraph: "#f54b42",
background: "#250b0a",
border: "#F7999D",
heading: "#f54b42",
icon: "#f54b42",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
},
success: {
accent: "#4F8A10",
accentForeground: "#ffffff",
paragraph: "#4F8A10",
background: "#132204",
border: "#383838",
heading: "#4F8A10",
icon: "#4F8A10",
separator: "#383838",
placeholder: "#404040",
hover: "#ffffff10",
backdrop: "#35353580"
}
},
list: { selected: { background: "#202020" } },
contextMenu: { primary: { background: "#202020" } },
sheet: { selected: { paragraph: "#008836" } }
},
$schema:
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json"
};

View File

@@ -71,7 +71,7 @@ export const Announcement = () => {
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
borderBottomColor: colors.primary.separator,
paddingBottom: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>

View File

@@ -55,6 +55,7 @@ import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view
import { strings } from "@notesnook/intl";
import { editorController } from "../../screens/editor/tiptap/utils";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
const getUser = () => {
const user = MMKV.getString("user");
@@ -96,9 +97,6 @@ const AppLocked = () => {
const lastAppState = useRef<AppStateStatus>(appState);
const biometricUnlockAwaitingUserInput = useRef(false);
const { height } = useWindowDimensions();
const keyboardType = useSettingStore(
(state) => state.settings.applockKeyboardType
);
const appLockHasPasswordSecurity = useSettingStore(
(state) => state.settings.appLockHasPasswordSecurity
);
@@ -157,8 +155,7 @@ const AppLocked = () => {
if (!appLockHasPasswordSecurity) {
await setAppLockVerificationCipher(password.current);
SettingsService.set({
appLockHasPasswordSecurity: true,
applockKeyboardType: "default"
appLockHasPasswordSecurity: true
});
DatabaseLogger.info("App lock migrated to password security");
}
@@ -167,7 +164,7 @@ const AppLocked = () => {
password.current = undefined;
} else {
ToastManager.show({
heading: strings.invalid(keyboardType),
heading: strings.invalid("password"),
type: "error",
context: "local"
});
@@ -290,9 +287,7 @@ const AppLocked = () => {
<Input
fwdRef={passwordInputRef}
secureTextEntry
keyboardType={
appLockHasPasswordSecurity ? keyboardType : "default"
}
keyboardType={"default"}
onLayout={async () => {
if (
!biometricsAuthEnabled ||
@@ -303,13 +298,7 @@ const AppLocked = () => {
}, 32);
}
}}
placeholder={
appLockHasPasswordSecurity
? keyboardType === "numeric"
? strings.enterApplockPassword()
: strings.enterApplockPin()
: strings.enterAccountPassword()
}
placeholder={PASSWORD_PLACEHOLDER}
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { formatBytes } from "@notesnook/common";
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { RefObject, useEffect, useState } from "react";
@@ -26,6 +27,7 @@ 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";
import { Radius, Spacing } from "../../common/design/spacing";
import filesystem from "../../common/filesystem";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
@@ -37,6 +39,7 @@ import {
eSendEvent,
presentSheet
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import {
@@ -46,20 +49,19 @@ import {
eOnLoadNote
} from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { openNote } from "../list-items/note/wrapper";
import { DateMeta } from "../properties/date-meta";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import AppIcon from "../ui/AppIcon";
import { createFormRef, validators } from "../ui/input/form-input";
import { Notice } from "../ui/notice";
import { Pressable } from "../ui/pressable";
import LineSeparator from "../ui/seperator/line-separator";
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,
@@ -94,7 +96,7 @@ const Actions = ({
downloadAttachment(attachment.hash, context === "global");
fwdRef.current?.hide();
},
icon: "download"
icon: "download-simple"
},
{
name: strings.network.reupload(),
@@ -114,7 +116,7 @@ const Actions = ({
type: attachment.mimeType.startsWith("image") ? "image" : "file"
});
},
icon: "upload"
icon: "upload-simple"
},
{
name: strings.fileCheck(),
@@ -149,7 +151,7 @@ const Actions = ({
name: undefined
});
},
icon: "file-check"
icon: "file"
},
{
name: strings.rename(),
@@ -197,7 +199,7 @@ const Actions = ({
});
}, 500);
},
icon: "form-textbox"
icon: "pencil-simple"
},
{
name: strings.delete(),
@@ -249,7 +251,7 @@ const Actions = ({
});
}, 500);
},
icon: "delete-outline"
icon: "trash"
}
];
@@ -267,6 +269,7 @@ const Actions = ({
onMomentumScrollEnd={() => {
fwdRef?.current?.handleChildScrollEnd();
}}
bounces={false}
nestedScrollEnabled={true}
style={{
maxHeight: "100%"
@@ -277,42 +280,34 @@ const Actions = ({
<SheetProvider context={contextId} />
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
marginBottom:
notes && notes.length > 0 ? 0 : DefaultAppStyles.GAP_VERTICAL
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_4,
gap: Spacing.LEVEL_1
}}
>
<Heading
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
size={AppFontSize.lg}
>
{filename}
</Heading>
<Heading fontSize="LG">{filename}</Heading>
<View
style={{
flexDirection: "row",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
gap: 10
gap: Spacing.LEVEL_1,
alignItems: "center"
}}
>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{attachment.mimeType}
</Paragraph>
<Paragraph fontSize="XS"></Paragraph>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{formatBytes(attachment.size)}
</Paragraph>
<Paragraph fontSize="XS"></Paragraph>
{notes.length ? (
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{strings.notes(notes.length)}
</Paragraph>
) : null}
<Paragraph fontSize="XS"></Paragraph>
<Paragraph
onPress={() => {
Clipboard.setString(attachment.hash);
@@ -322,82 +317,123 @@ const Actions = ({
context: "local"
});
}}
size={AppFontSize.xs}
fontSize="XS"
color={colors.secondary.paragraph}
>
<AppIcon
name="copy"
size={10}
color={colors.secondary.paragraph}
iconFamily="notesnook"
/>{" "}
{attachment.hash}
</Paragraph>
</View>
<LineSeparator paddingVertical={Spacing.LEVEL_2} />
<DateMeta item={attachment} />
</View>
{notes && notes.length > 0 ? (
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<>
<Heading
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
size={AppFontSize.sm}
>
{strings.listOf()} {strings.dataTypesPlural.note()}:
</Heading>
<>
<LineSeparator
paddingHorizontal={Spacing.LEVEL_3}
paddingVertical={Spacing.LEVEL_3}
/>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
gap: Spacing.LEVEL_2
}}
>
<>
<Heading size={AppFontSize.sm}>
{strings.notes(notes.length)}
</Heading>
{notes.map((item) => (
<Pressable
onPress={async () => {
eSendEvent(eCloseSheet, contextId);
close?.();
eSendEvent(eCloseAttachmentDialog);
Navigation.navigate("FluidPanelsView");
openNote(item, (item as any).type === "trash");
}}
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
alignItems: "flex-start",
paddingHorizontal: DefaultAppStyles.GAP
}}
key={item.id}
>
<Paragraph size={AppFontSize.xs}>{item.title}</Paragraph>
</Pressable>
))}
</>
</View>
{notes.map((item) => (
<Pressable
onPress={async () => {
eSendEvent(eCloseSheet, contextId);
close?.();
eSendEvent(eCloseAttachmentDialog);
Navigation.navigate("FluidPanelsView");
openNote(item, (item as any).type === "trash");
}}
style={{
alignItems: "flex-start"
}}
key={item.id}
>
<Paragraph fontSize="XS">{item.title}</Paragraph>
</Pressable>
))}
</>
</View>
</>
) : null}
{actions.map((item) => (
<Button
key={item.name}
buttonType={{
text:
item.name === "Delete"
? colors.error.paragraph
: colors.primary.paragraph
}}
onPress={item.onPress}
title={item.name}
icon={item.icon}
loading={loading?.name === item.name}
type="plain"
fontSize={AppFontSize.sm}
style={{
borderRadius: 0,
justifyContent: "flex-start",
alignSelf: "flex-start",
width: "100%"
}}
/>
))}
<LineSeparator
paddingHorizontal={Spacing.LEVEL_3}
paddingVertical={Spacing.LEVEL_3}
/>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
{actions.map((item) => (
<Pressable
key={item.name}
onPress={item.onPress}
type="plain"
style={{
borderRadius: 0,
flexDirection: "row",
width: "100%",
gap: Spacing.LEVEL_1,
justifyContent: "flex-start",
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
width: 32,
height: 32,
backgroundColor: colors.secondary.background,
borderRadius: Radius.XS,
justifyContent: "center",
alignItems: "center"
}}
>
<AppIcon
color={
item.icon === "trash"
? colors.error.accent
: colors.primary.icon
}
name={item.icon}
iconFamily="notesnook"
size={16}
/>
</View>
<Paragraph
color={
item.icon === "trash"
? colors.error.accent
: colors.primary.heading
}
fontSize="SM"
fontFamily="MEDIUM"
>
{item.name}
</Paragraph>
</Pressable>
))}
</View>
<View
style={{

View File

@@ -17,26 +17,29 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatBytes } from "@notesnook/common";
import { formatBytes, getFormattedDate } from "@notesnook/common";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { TouchableOpacity, View } from "react-native";
import { db } from "../../common/database";
import { View } from "react-native";
import { Radius, Spacing } from "../../common/design/spacing";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useDBItem } from "../../hooks/use-db-item";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import AppIcon from "../ui/AppIcon";
import { Pressable } from "../ui/pressable";
import { ProgressBarComponent } from "../ui/svg/lazy";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
import { strings } from "@notesnook/intl";
import { Pressable } from "../ui/pressable";
import { DefaultAppStyles } from "../../utils/styles";
import { IconButton } from "../ui/icon-button";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
function getAttachmentIcon(attachment: Attachment) {
const mimeType = attachment.mimeType || "";
if (mimeType.startsWith("image/")) return "image";
if (mimeType.startsWith("video/")) return "video-camera";
if (mimeType.startsWith("audio/")) return "music-notes";
return "file-text";
}
export const AttachmentItem = ({
@@ -75,11 +78,15 @@ export const AttachmentItem = ({
onPress={onPress}
style={{
flexDirection: "row",
marginVertical: 5,
alignItems: "center",
justifyContent: "space-between",
padding: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
minHeight: 45
gap: Spacing.LEVEL_2,
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: Radius.S,
paddingHorizontal: Spacing.LEVEL_2,
paddingVertical: Spacing.LEVEL_3,
marginBottom: Spacing.LEVEL_2
}}
>
{!attachment ? null : (
@@ -89,107 +96,112 @@ export const AttachmentItem = ({
flexShrink: 1,
flexDirection: "row",
alignItems: "center",
gap: 10
gap: Spacing.LEVEL_2
}}
>
<View
style={{
width: 40,
height: 40,
borderRadius: Radius.XS,
backgroundColor: colors.secondary.background,
justifyContent: "center",
alignItems: "center",
marginLeft: -5,
borderWidth: 1,
borderColor: colors.secondary.border,
paddingHorizontal: 2,
width: 30,
height: 30,
borderRadius: defaultBorderRadius
alignItems: "center"
}}
>
<Paragraph
adjustsFontSizeToFit
size={8}
color={colors.secondary.paragraph}
style={{
maxWidth: 30
}}
numberOfLines={1}
>
{getFileExtension(attachment.filename).toUpperCase() ||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
</Paragraph>
<AppIcon
name={getAttachmentIcon(attachment)}
iconFamily="notesnook"
size={20}
color={colors.primary.icon}
/>
</View>
<View
style={{
flexShrink: 1
flexShrink: 1,
width: "100%",
gap: Spacing.LEVEL_1
}}
>
<Paragraph
size={AppFontSize.sm}
style={{
flexWrap: "wrap"
}}
numberOfLines={1}
<Heading
fontSize="MD"
lineBreakMode="middle"
color={colors.primary.paragraph}
color={colors.primary.heading}
>
{attachment.filename}
</Paragraph>
book.pdf
</Heading>
{!hideWhenNotDownloading ? (
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xxs}
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
{attachment.failed ? (
<AppIcon
size={13}
name="alert-circle-outline"
color={colors.error.paragraph}
/>
) : null}
{!hideWhenNotDownloading ? (
<Paragraph
fontSize="XS"
numberOfLines={1}
color={colors.secondary.paragraph}
>
{getFormattedDate(attachment.dateModified)} |{" "}
{formatBytes(attachment.size)}
</Paragraph>
) : null}
</View>
{currentProgress ? (
<View
style={{
width: "100%",
gap: Spacing.LEVEL_0,
flexGrow: 1
}}
>
{strings.fileSize()}: {formatBytes(attachment.size)}
</Paragraph>
<ProgressBarComponent
height={6}
width={null}
animated={true}
useNativeDriver
progress={currentProgress.value}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
<Paragraph color={colors.secondary.paragraph} fontSize="XS">
{currentProgress.percent} |{" "}
{currentProgress.type === "upload"
? strings.uploading()
: strings.downloading()}
</Paragraph>
</View>
) : null}
</View>
</View>
{currentProgress ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
db.fs().cancel(attachment.hash);
setCurrentProgress(undefined);
}}
style={{
justifyContent: "center",
marginLeft: 5,
marginTop: 5,
marginRight: -5
}}
>
<ProgressCircleComponent
size={AppFontSize.xxl}
progress={
currentProgress?.value ? currentProgress?.value / 100 : 0
}
showsText
textStyle={{
fontSize: 10
}}
color={colors.primary.accent}
formatText={(progress) => (progress * 100).toFixed(0)}
borderWidth={0}
thickness={2}
/>
</TouchableOpacity>
) : (
<>
{attachment.failed ? (
<IconButton
onPress={onPress}
name="alert-circle-outline"
color={colors.error.paragraph}
/>
) : null}
</>
)}
</>
)}
<IconButton
color={colors.secondary.icon}
name="dots-three"
iconFamily="notesnook"
size={20}
onPress={onPress}
style={{
justifyContent: "center",
height: undefined,
width: undefined,
borderRadius: 100,
alignItems: "center"
}}
/>
</Pressable>
);
};

View File

@@ -21,21 +21,23 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { Spacing } from "../../common/design/spacing";
import BackupService from "../../services/backup";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { 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 RecoveryKeySheet from "../sheets/recovery-key";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
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 { colors } = useThemeColors();
@@ -92,7 +94,7 @@ export const ChangePassword = () => {
});
setLoading(false);
Navigation.goBack();
eSendEvent(eOpenRecoveryKeyDialog);
RecoveryKeySheet.present();
} catch (e) {
const message = (e as Error).message;
setLoading(false);
@@ -106,15 +108,22 @@ export const ChangePassword = () => {
};
return (
<View
<KeyboardAwareScrollView
style={{
width: "100%",
padding: DefaultAppStyles.GAP
width: "100%"
}}
contentContainerStyle={{
paddingTop: Spacing.LEVEL_4,
paddingHorizontal: Spacing.LEVEL_3,
gap: Spacing.LEVEL_2
}}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
>
<Dialog context="change-password-dialog" />
<FormInput
name="oldPassword"
label={strings.oldPassword()}
formRef={formRef}
fwdRef={oldPasswordInputRef}
loading={loading}
@@ -125,7 +134,7 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.currentPassword()}
placeholder={"•••••••••"}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
@@ -133,6 +142,7 @@ export const ChangePassword = () => {
<FormInput
name="password"
label={strings.newPassword()}
formRef={formRef}
fwdRef={passwordInputRef}
loading={loading}
@@ -143,7 +153,28 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.newPassword()}
placeholder={"•••••••••"}
onSubmitEditing={() => {
changePassword();
}}
/>
<FormInput
name="confirmPassword"
label={strings.confirmPassword()}
formRef={formRef}
fwdRef={passwordInputRef}
loading={loading}
validators={[
validators.matchField("password", strings.confirmPasswordRequired())
]}
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={"•••••••••"}
onSubmitEditing={() => {
changePassword();
}}
@@ -168,22 +199,39 @@ export const ChangePassword = () => {
</Paragraph>
) : null}
<Notice text={strings.changePasswordNotice()} type="alert" />
<View style={{ height: 10 }} />
<Notice text={strings.changePasswordNotice2()} type="alert" />
<Notice
text={strings.changePasswordNotice()}
type="information"
size="small"
/>
<Button
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
width: "100%",
marginTop: Spacing.LEVEL_1
}}
loading={loading}
onPress={changePassword}
type="accent"
title={loading ? null : strings.changePasswordConfirm()}
/>
</View>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1,
alignSelf: "center",
marginTop: Spacing.LEVEL_0
}}
>
<AppIcon
size={15}
name="shield-check"
iconFamily="notesnook"
color={colors.secondary.icon}
/>
<Paragraph>{strings.yourSecurityIsPriority()}</Paragraph>
</View>
</KeyboardAwareScrollView>
);
};

View File

@@ -24,6 +24,9 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import { hideAuth } from "./common";
import { AuthParams } from "../../stores/use-navigation-store";
import { ProgressPills } from "../intro/progress-pills";
import { FontSizes } from "../../common/design/font";
import { Spacing } from "../../common/design/spacing";
export const AuthHeader = (props: { welcome?: boolean }) => {
const { colors } = useThemeColors();
const route = useRoute();
@@ -37,19 +40,22 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
paddingHorizontal: Spacing.LEVEL_3,
width: "100%",
height: 50,
justifyContent: !props.welcome ? "space-between" : "flex-end"
justifyContent: "space-between",
alignItems: "center"
}}
>
{props.welcome ? null : (
{props.welcome ? (
<ProgressPills activePillIndex={1} />
) : (
<IconButton
name="arrow-left"
name="arrow-back"
iconFamily="notesnook"
onPress={() => {
hideAuth((route.params as AuthParams)?.context);
}}
size={24}
color={colors.primary.paragraph}
/>
)}
@@ -60,16 +66,16 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
onPress={() => {
hideAuth();
}}
iconSize={16}
iconSize={24}
fontSize={FontSizes.SM}
type="plain"
fontFamily="REGULAR"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingHorizontal: 6
paddingRight: 0,
paddingHorizontal: 0,
paddingVertical: 0
}}
/>
)}

View File

@@ -17,15 +17,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { NavigationProps } from "../../services/navigation";
import { Toast } from "../toast";
import { AuthMode, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { useThemeColors } from "@notesnook/theme";
import { NavigationProps } from "../../services/navigation";
const Auth = ({ navigation, route }: NavigationProps<"Auth">) => {
const [currentAuthMode, setCurrentAuthMode] = useState(

View File

@@ -21,8 +21,10 @@ import { strings } from "@notesnook/intl";
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 { TouchableOpacity, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { Spacing } from "../../common/design/spacing";
import { DDS } from "../../services/device-detection";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Navigation from "../../services/navigation";
@@ -33,19 +35,20 @@ import { RouteParams } from "../../stores/use-navigation-store";
import { useUserStore } from "../../stores/use-user-store";
import { eUserLoggedIn } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import { ProgressPills } from "../intro/progress-pills";
import { Progress } from "../sheets/progress";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import FormInput, { validators } from "../ui/input/form-input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { AuthHeader } from "./header";
import TwoFactorVerification from "./two-factor";
import { useLogin } from "./use-login";
import FormInput, { validators } from "../ui/input/form-input";
import AppIcon from "../ui/AppIcon";
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
const LoginSteps = {
emailAuth: 1,
@@ -68,8 +71,9 @@ export const Login = ({
passwordInputRef,
loading,
setLoading,
login,
error,
login,
mfaData,
formRef
} = useLogin(async () => {
eSendEvent(eUserLoggedIn, true);
@@ -91,8 +95,6 @@ export const Login = ({
Progress.present();
}
});
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const onContinue = () => {
login();
@@ -114,249 +116,257 @@ export const Login = ({
return (
<>
<AuthHeader />
<Dialog context="two_factor_verify" />
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
marginTop: Spacing.LEVEL_4
}}
>
<ProgressPills
count={2}
activePillIndex={step === LoginSteps.emailAuth ? 0 : 1}
/>
</View>
<KeyboardAwareScrollView
style={{
width: "100%"
}}
contentContainerStyle={{
minHeight: "90%"
minHeight: "99%"
}}
nestedScrollEnabled
enableAutomaticScroll={true}
keyboardShouldPersistTaps="handled"
>
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
height: "100%",
alignSelf: "center"
}}
>
{step === LoginSteps.emailAuth || step === LoginSteps.passwordAuth ? (
<View
style={{
justifyContent: "flex-end",
paddingHorizontal: DefaultAppStyles.GAP,
borderBottomWidth: 0.8,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : undefined,
borderColor: isTablet ? colors.primary.border : undefined,
borderRadius: isTablet ? 20 : undefined,
marginTop: isTablet ? 50 : undefined,
width: !isTablet ? undefined : "50%",
minHeight: height * 0.4
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_6
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<Heading
style={{
marginBottom: 25,
marginTop: DefaultAppStyles.GAP_VERTICAL
paddingBottom: Spacing.LEVEL_4
}}
extraBold
size={AppFontSize.xxl}
fontSize="XL"
>
{strings.loginToYourAccount()}
</Heading>
</View>
<View
style={{
width: DDS.isTab
? focused
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<FormInput
name="email"
formRef={formRef}
fwdRef={emailInputRef}
testID="input.email"
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
keyboardType="email-address"
marginBottom={0}
autoCorrect={false}
autoCapitalize="none"
placeholder={strings.email()}
editable={step === LoginSteps.emailAuth && !loading}
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
if (step === LoginSteps.emailAuth) {
onContinue();
} else {
passwordInputRef.current?.focus();
}
<View
style={{
width: DDS.isTab
? focused
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
gap: Spacing.LEVEL_2
}}
/>
{step === LoginSteps.passwordAuth && (
<>
<FormInput
name="password"
formRef={formRef}
fwdRef={passwordInputRef}
testID="input.password"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
marginBottom={0}
editable={!loading}
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
onContinue();
}}
/>
<Button
title={strings.forgotPassword()}
style={{
alignSelf: "flex-end",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: 0
}}
onPress={() => {
if (loading) return;
presentSheet({
component: (
<ForgotPassword
userEmail={formRef.current.getValue("email")}
/>
)
});
}}
textStyle={{
textDecorationLine: "underline"
}}
fontSize={AppFontSize.xs}
type="plain"
/>
</>
)}
<View>
<Button
loading={loading}
onPress={() => {
onContinue();
>
<FormInput
name="email"
formRef={formRef}
fwdRef={emailInputRef}
testID="input.email"
label={strings.email()}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
keyboardType="email-address"
marginBottom={0}
autoCorrect={false}
autoCapitalize="none"
placeholder={strings.email()}
editable={step === LoginSteps.emailAuth && !loading}
validators={[
validators.required(strings.emailRequired()),
validators.email(strings.enterAValidEmailAddress())
]}
onSubmitEditing={() => {
passwordInputRef.current?.focus();
}}
style={{
width: "100%"
}}
type="accent"
title={!loading ? strings.continue() : null}
fontSize={AppFontSize.sm}
/>
{step === LoginSteps.passwordAuth && (
<FormInput
name="password"
formRef={formRef}
fwdRef={passwordInputRef}
label={strings.password()}
testID="input.password"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={PASSWORD_PLACEHOLDER}
marginBottom={0}
editable={!loading}
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
onContinue();
}}
/>
<Button
title={strings.forgotPassword()}
style={{
alignSelf: "flex-end",
paddingVertical: 0,
paddingHorizontal: 0
}}
onPress={() => {
if (loading || !formRef.current.getValue("email")) return;
presentSheet({
component: (
<ForgotPassword
userEmail={formRef.current.getValue("email")}
/>
)
});
}}
fontFamily="REGULAR"
fontSize={AppFontSize.sm}
type="plain"
textStyle={{
color: colors.secondary.paragraph
}}
/>
<View
style={{
marginTop: Spacing.LEVEL_1,
gap: Spacing.LEVEL_2
}}
>
<Button
title={strings.cancelLogin()}
loading={loading}
onPress={() => {
if (loading) return;
login();
}}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
onPress={() => {
if (loading) return;
setStep(LoginSteps.emailAuth);
setLoading(false);
}}
type="secondaryAccented"
type="accent"
title={!loading ? strings.continue() : null}
fontSize={AppFontSize.sm}
/>
)}
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
paddingVertical: 0
}}
>
{strings.dontHaveAccount()}{" "}
<Paragraph
size={AppFontSize.xs}
style={{ color: colors.primary.accent }}
>
{strings.signUp()}
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{strings.dontHaveAccount()}{" "}
<Paragraph
fontSize="SM"
style={{ color: colors.primary.accent }}
fontFamily="SEMI_BOLD"
>
{strings.signUp()}
</Paragraph>
</Paragraph>
</Paragraph>
</TouchableOpacity>
) : null}
</TouchableOpacity>
) : null}
{error ? (
<Paragraph
numberOfLines={4}
onPress={() => {}}
color={colors.error.accent}
style={{
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
<AppIcon
{error ? (
<Paragraph
numberOfLines={4}
onPress={() => {}}
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{error.message}
</Paragraph>
) : null}
style={{
textAlign: "center",
marginTop: Spacing.LEVEL_2
}}
>
<AppIcon
color={colors.error.accent}
name="alert-circle-outline"
size={AppFontSize.sm - 1}
/>{" "}
{error.message}
</Paragraph>
) : null}
</View>
</View>
</View>
</View>
) : (
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_6
}}
>
<TwoFactorVerification
onMfaLogin={async (
mfa: any,
callback: (success: boolean) => void,
onerror: (error: Error) => void
) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
mfa.method
);
await login();
if (success) {
setLoading(false);
callback && callback(true);
}
callback && callback(false);
} catch (e) {
callback && callback(false);
if ((e as Error).message === "invalid_grant") {
setLoading(false);
setStep(LoginSteps.emailAuth);
} else {
onerror(e as Error);
}
}
}}
mfaInfo={
mfaData.current || {
primaryMethod: "email",
secondaryMethod: "sms",
token: ""
}
}
onCancel={() => {
setLoading(false);
setStep(LoginSteps.emailAuth);
}}
/>
</View>
)}
</KeyboardAwareScrollView>
</>
);

View File

@@ -34,21 +34,21 @@ import Sync from "../../services/sync";
import { clearAllStores } from "../../stores";
import { useUserStore } from "../../stores/use-user-store";
import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import BaseDialog from "../dialog/base-dialog";
import { presentDialog } from "../dialog/functions";
import SheetProvider from "../sheet-provider";
import { Toast } from "../toast";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
import { useLogin } from "./use-login";
import { strings } from "@notesnook/intl";
import { getObfuscatedEmail } from "../../utils/functions";
import { DefaultAppStyles } from "../../utils/styles";
import { Spacing } from "../../common/design/spacing";
import FormInput, { validators } from "../ui/input/form-input";
export const SessionExpired = () => {
@@ -158,92 +158,119 @@ export const SessionExpired = () => {
width: focused ? "100%" : "99.9%",
padding: DefaultAppStyles.GAP,
justifyContent: "center",
gap: Spacing.LEVEL_7,
flex: 1,
backgroundColor: colors.primary.background
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
width: "100%",
marginBottom: 20,
borderRadius: 10,
paddingVertical: 20
}}
>
<IconButton
<View style={{ gap: Spacing.LEVEL_7, alignItems: "center" }}>
<View
style={{
width: 60,
height: 60
}}
name="alert"
color={colors.error.icon}
size={50}
/>
<Heading size={AppFontSize.xxxl} color={colors.primary.heading}>
{strings.sessionExpired()}
</Heading>
<Paragraph
style={{
textAlign: "center"
width: 100,
height: 100,
borderRadius: 50,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.error.background
}}
>
{strings.sessionExpiredDesc(
getObfuscatedEmail(formRef.current.getValue("email") as string)
)}
</Paragraph>
<View
style={{
width: 56,
height: 56,
borderRadius: 28,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.error.accent
}}
>
<AppIcon
name="warning"
iconFamily="notesnook"
size={30}
color={colors.static.white}
/>
</View>
</View>
<View style={{ width: "100%", gap: Spacing.LEVEL_3 }}>
<View
style={{
alignItems: "center",
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
<Heading
fontSize="XL"
lineHeight="100%"
color={colors.primary.heading}
>
{strings.sessionExpired()}
</Heading>
<Paragraph
fontSize="SM"
color={colors.primary.paragraph}
style={{
textAlign: "center"
}}
>
{strings.sessionExpiredDesc(
getObfuscatedEmail(
formRef.current.getValue("email") as string
)
)}
</Paragraph>
</View>
<FormInput
fwdRef={passwordInputRef}
formRef={formRef}
name="password"
label={strings.enterYourPassword()}
validators={[validators.required(strings.passwordRequired())]}
returnKeyLabel={strings.done()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
onSubmitEditing={() => {
login();
}}
/>
</View>
</View>
{step === LoginSteps.passwordAuth ? (
<FormInput
fwdRef={passwordInputRef}
formRef={formRef}
name="password"
validators={[validators.required(strings.passwordRequired())]}
returnKeyLabel={strings.done()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
onSubmitEditing={() => {
login();
<View style={{ gap: Spacing.LEVEL_2 }}>
<Button
style={{
width: "100%"
}}
loading={loading}
onPress={() => login()}
type="accent"
title={loading ? null : strings.login()}
/>
) : null}
<Button
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: 250,
borderRadius: 100
}}
loading={loading}
onPress={() => login()}
type="accent"
title={loading ? null : strings.login()}
/>
<Button
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
onPress={() => {
presentDialog({
context: "session_expiry",
title: strings.logoutFromDevice(),
paragraph: strings.logoutDesc(),
positiveText: strings.logout(),
positiveType: "errorShade",
positivePress: logout
});
}}
type="errorShade"
title={loading ? null : strings.logoutFromDevice()}
/>
<Button
style={{
width: "100%"
}}
onPress={() => {
presentDialog({
context: "session_expiry",
title: strings.logoutFromDevice(),
paragraph: strings.logoutDesc(),
positiveText: strings.logout(),
positiveType: "error-shade-outline",
positivePress: logout
});
}}
type="plain-outline"
title={strings.logoutFromDevice()}
/>
</View>
</View>
<Toast context="local" />
<Dialog context="session_expiry" />

View File

@@ -21,12 +21,7 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { RouteProp, useRoute } from "@react-navigation/native";
import React, { useRef, useState } from "react";
import {
TextInput,
TouchableOpacity,
View,
useWindowDimensions
} from "react-native";
import { TextInput, TouchableOpacity, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
@@ -46,6 +41,29 @@ import { SignupContext } from "./signup-context";
import { RouteParams } from "../../stores/use-navigation-store";
import SettingsService from "../../services/settings";
import AppIcon from "../ui/AppIcon";
import { Spacing } from "../../common/design/spacing";
import { SETTING_ACCOUNT_SVG } from "../../assets/images/assets";
import { ProgressPills } from "../intro/progress-pills";
import { sleep } from "../../utils/time";
import { PASSWORD_PLACEHOLDER } from "../../utils/constants";
const getSettingAccountSvg = (dark: boolean) => {
if (dark) return SETTING_ACCOUNT_SVG;
return SETTING_ACCOUNT_SVG.replace('stroke="#1F2722"', 'stroke="#E8F4ED"')
.replace(
'fill="#008836" fill-opacity="0.09" stroke="#233C2D"',
'fill="#008836" fill-opacity="0.04" stroke="#E3F1E8"'
)
.replace(
'fill="#008836" fill-opacity="0.09" stroke="#233C2D"',
'fill="#008836" fill-opacity="0.05" stroke="#E8F0EC"'
)
.replace(
'fill="#008836" fill-opacity="0.08" stroke="#233C2D"',
'fill="#008836" fill-opacity="0.06" stroke="#D3E8DB"'
);
};
const SignupSteps = {
signup: 0,
@@ -61,7 +79,7 @@ export const Signup = ({
welcome: boolean;
}) => {
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
const { colors } = useThemeColors();
const { colors, isDark } = useThemeColors();
const formRef = useRef(
createFormRef({
email: "",
@@ -76,8 +94,6 @@ export const Signup = ({
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const setLastSynced = useUserStore((state) => state.setLastSynced);
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const route = useRoute<RouteProp<RouteParams, "Auth">>();
const signup = async () => {
@@ -89,8 +105,9 @@ export const Signup = ({
setLoading(true);
try {
setCurrentStep(SignupSteps.createAccount);
await db.user.signup(values.email.toLowerCase(), values.password);
await sleep(1000);
setCurrentStep(SignupSteps.createAccount);
const user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
@@ -133,8 +150,9 @@ export const Signup = ({
width: "100%"
}}
contentContainerStyle={{
minHeight: "90%"
flex: 1
}}
bounces={false}
nestedScrollEnabled
keyboardShouldPersistTaps="handled"
>
@@ -145,68 +163,24 @@ export const Signup = ({
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%"
height: "100%",
paddingHorizontal: Spacing.LEVEL_3,
paddingTop: Spacing.LEVEL_6
}}
>
<View
<Heading
style={{
justifyContent: "flex-end",
paddingHorizontal: 16,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomWidth: 0.8,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : undefined,
borderColor: isTablet ? colors.primary.border : undefined,
borderRadius: isTablet ? 20 : undefined,
marginTop: isTablet ? 50 : undefined,
width: !isTablet ? undefined : "50%",
minHeight: height * 0.25
paddingBottom: Spacing.LEVEL_4
}}
fontSize="XL"
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<Heading
extraBold
style={{
marginBottom: 25,
marginTop: 10
}}
size={AppFontSize.xxl}
>
{strings.createAccount()}
</Heading>
</View>
{strings.createAccount()}
</Heading>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
paddingHorizontal: DDS.isTab ? 0 : 16,
backgroundColor: colors.primary.background,
flexGrow: 1,
alignSelf: "center"
gap: Spacing.LEVEL_2,
paddingBottom: Spacing.LEVEL_4
}}
>
<FormInput
@@ -214,6 +188,7 @@ export const Signup = ({
formRef={formRef}
fwdRef={emailInputRef}
loading={loading}
label={strings.email()}
testID="input.email"
returnKeyLabel="Next"
returnKeyType="next"
@@ -221,7 +196,7 @@ export const Signup = ({
keyboardType="email-address"
autoCorrect={false}
autoCapitalize="none"
placeholder={strings.email()}
placeholder="you@example.com"
blurOnSubmit={false}
validators={[
validators.required(strings.emailRequired()),
@@ -245,9 +220,11 @@ export const Signup = ({
autoCapitalize="none"
blurOnSubmit={false}
autoCorrect={false}
placeholder={strings.password()}
label={strings.password()}
placeholder={PASSWORD_PLACEHOLDER}
validators={[validators.required(strings.passwordRequired())]}
onSubmitEditing={() => {
if (formRef.current.validateField("password")) return;
confirmPasswordInputRef.current?.focus();
}}
/>
@@ -265,8 +242,8 @@ export const Signup = ({
autoCapitalize="none"
autoCorrect={false}
blurOnSubmit={false}
placeholder={strings.confirmPassword()}
marginBottom={12}
label={strings.confirmPassword()}
placeholder={PASSWORD_PLACEHOLDER}
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField(
@@ -278,7 +255,17 @@ export const Signup = ({
signup();
}}
/>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
backgroundColor: colors.primary.background,
flexGrow: 1,
alignSelf: "center",
gap: Spacing.LEVEL_2
}}
>
<Button
title={!loading ? strings.continue() : null}
type="accent"
@@ -296,18 +283,21 @@ export const Signup = ({
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
alignSelf: "center"
}}
>
<Paragraph
size={AppFontSize.xs + 1}
color={colors.secondary.paragraph}
style={{
marginBottom: 25,
textAlign: "center"
}}
fontSize="SM"
color={colors.primary.paragraph}
>
{strings.alreadyHaveAccount()}{" "}
<Paragraph
size={AppFontSize.xs + 1}
fontSize="SM"
fontFamily="SEMI_BOLD"
style={{ color: colors.primary.accent }}
>
{strings.login()}
@@ -347,18 +337,16 @@ export const Signup = ({
marginBottom: 25,
textAlign: "center"
}}
size={AppFontSize.xxs}
fontSize="XS"
color={colors.secondary.paragraph}
>
{strings.signupAgreement[0]()}
<Paragraph
size={AppFontSize.xxs}
fontSize="XS"
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos");
}}
style={{
textDecorationLine: "underline"
}}
fontFamily="SEMI_BOLD"
color={colors.primary.accent}
>
{" "}
@@ -366,31 +354,38 @@ export const Signup = ({
</Paragraph>{" "}
{strings.signupAgreement[2]()}
<Paragraph
size={AppFontSize.xxs}
fontSize="XS"
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy");
}}
style={{
textDecorationLine: "underline"
}}
fontFamily="SEMI_BOLD"
color={colors.primary.accent}
>
{" "}
{strings.signupAgreement[3]()}
</Paragraph>{" "}
{strings.signupAgreement[4]()}
</Paragraph>
</Paragraph>
</View>
</View>
</KeyboardAwareScrollView>
</>
) : (
<>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
{welcome ? <ProgressPills activePillIndex={2} /> : null}
<Loading
title={"Setting up your account..."}
description="Your account is almost ready, please wait..."
title={strings.settingUpYourAccount()}
svgSrc={getSettingAccountSvg(isDark)}
description={strings.accountAlmostReady()}
style={{
height: undefined,
marginTop: Spacing.LEVEL_6
}}
/>
</>
</View>
)}
</SignupContext.Provider>
);

View File

@@ -20,19 +20,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { TextInput, TouchableOpacity, View } from "react-native";
import { db } from "../../common/database/index";
import { Radius, Spacing } from "../../common/design/spacing";
import useTimer from "../../hooks/use-timer";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { eCloseSimpleDialog } from "../../utils/events";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
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 PinInput from "../ui/pin-input/index";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -73,11 +72,19 @@ const TwoFactorVerification = ({
const inputRef = useRef<TextInput>(null);
const [sending, setSending] = useState(false);
const [error, setError] = useState<Error | undefined>(undefined);
const [codeInput, setCodeInput] = useState("");
const isRecoveryCode = currentMethod.method === "recoveryCode";
const codeLength = isRecoveryCode ? 10 : 6;
const onNext = async () => {
if (!code.current || code.current.length < 6) {
setError(
new Error("Please provide a valid multi-factor authentication code.")
new Error(
currentMethod.method === "recoveryCode"
? strings.provideValidMultiFactorAuthRecoveryCode()
: strings.provideValidMultiFactorAuthCode()
)
);
return;
}
@@ -94,10 +101,7 @@ const TwoFactorVerification = ({
method: currentMethod.method,
code: code.current
},
(result) => {
if (result) {
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
}
() => {
setLoading(false);
},
(e) => {
@@ -118,12 +122,14 @@ const TwoFactorVerification = ({
{
id: "sms",
title: strings.sendCodeSms(),
icon: "message-plus-outline"
icon: "chat",
iconFamily: "notesnook"
},
{
id: "email",
title: strings.sendCodeEmail(),
icon: "email-outline"
icon: "envelope-simple",
iconFamily: "notesnook"
},
{
id: "app",
@@ -133,7 +139,8 @@ const TwoFactorVerification = ({
{
id: "recoveryCode",
title: strings.recoveryCode(),
icon: "key"
icon: "lock-simple",
iconFamily: "notesnook"
}
];
@@ -155,6 +162,7 @@ const TwoFactorVerification = ({
setSending(false);
} catch (e) {
setSending(false);
console.log("error e");
setError(
new Error(`Error sending 2FA Code. Tap "Send code" to try again `)
);
@@ -168,48 +176,56 @@ const TwoFactorVerification = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentMethod.method]);
useEffect(() => {
setCodeInput("");
code.current = "";
}, [currentMethod.method]);
return (
<ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
style={{
width: "100%",
height: "100%",
backgroundColor: colors.primary.background,
paddingTop: 60
}}
<View
onLayout={() => {
setTimeout(() => {
inputRef.current?.focus();
}, 500);
}}
style={{
alignItems: "center",
gap: Spacing.LEVEL_3
}}
>
<View
style={{
alignItems: "center",
paddingHorizontal: currentMethod.method ? 12 : 0,
gap: 12
paddingHorizontal: Spacing.LEVEL_3,
width: "100%"
}}
>
<IconButton
style={{
width: 70,
height: 70
width: 50,
height: 50,
backgroundColor: colors.primary.shade,
borderRadius: Radius.XS,
marginBottom: Spacing.LEVEL_7
}}
size={50}
name="key"
size={25}
iconFamily="notesnook"
name="shield-check"
color={colors.primary.accent}
/>
<Heading
style={{
textAlign: "center"
textAlign: "center",
marginBottom: Spacing.LEVEL_1
}}
>
{currentMethod.method ? strings["2fa"]() : strings.select2faMethod()}
{currentMethod.method === "recoveryCode"
? strings.recoveryCodeTitle()
: currentMethod.method
? strings["2fa"]()
: strings.select2faMethod()}
</Heading>
<Paragraph
style={{
width: "80%",
textAlign: "center"
}}
>
@@ -219,61 +235,35 @@ const TwoFactorVerification = ({
]?.() || strings.select2faCodeHelpText()
: strings.select2faCodeHelpText()}
</Paragraph>
</View>
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
<Button
onPress={onSendCode}
type={seconds ? "plain" : "transparent"}
title={
sending
? ""
: `${
seconds
? strings.resend2faCode(`${seconds}`)
: strings.sendCode()
}`
}
loading={sending}
height={30}
/>
) : null}
{currentMethod.method ? (
<>
<Input
placeholder={
currentMethod.method === "recoveryCode"
? "xxxxx-xxxxx"
: "xxxxxx"
}
{currentMethod.method ? (
<>
<View
style={{
borderRadius: Radius.S,
width: "100%",
gap: Spacing.LEVEL_4
}}
>
<PinInput
testID={"input.totp"}
maxLength={
currentMethod.method === "recoveryCode" ? undefined : 6
}
fwdRef={inputRef}
textAlign="center"
onChangeText={(value) => {
setError(undefined);
inputRef={inputRef}
length={codeLength}
value={codeInput}
onChangeText={(value: string) => {
setCodeInput(value);
code.current = value;
}}
cursorColor={colors.selected.accent}
selectionHandleColor={colors.selected.accent}
selectionColor={colors.selected.accent}
onSubmitEditing={onNext}
height={60}
marginBottom={0}
inputStyle={{
fontSize: AppFontSize.lg,
textAlign: "center",
letterSpacing: 7,
width: 250
}}
keyboardType={
currentMethod.method === "recoveryCode" ? "default" : "numeric"
}
enablesReturnKeyAutomatically
containerStyle={{
minWidth: "50%"
keyboardType={isRecoveryCode ? "web-search" : "number-pad"}
showDashAfterDigit={isRecoveryCode ? 5 : undefined}
sanitize={(value: string) => {
if (isRecoveryCode) {
return value.replace(/[^0-9a-zA-Z]/g, "").toUpperCase();
}
return value.replace(/[^0-9]/g, "");
}}
/>
{error ? (
@@ -284,7 +274,9 @@ const TwoFactorVerification = ({
style={{
textAlign: "center",
marginVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
maxWidth: 250
maxWidth: 250,
alignSelf: "center",
marginTop: -10
}}
>
<AppIcon
@@ -296,76 +288,144 @@ const TwoFactorVerification = ({
</Paragraph>
) : null}
<Button
title={loading ? null : strings.next()}
type="accent"
width={250}
loading={loading}
onPress={onNext}
/>
<Button
title={strings.cancel()}
type="secondaryAccented"
onPress={() => {
reset();
onCancel();
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2
}}
width={250}
/>
<Button
title={strings["2faCodeSecondaryMethodText"][
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
]()}
type="plain"
onPress={onRequestSecondaryMethod}
height={30}
/>
</>
) : (
<>
{getMethods().map((item) => (
<Pressable
key={item.title}
>
<Button
title={strings.cancel()}
type="secondary"
onPress={() => {
setCurrentMethod({
method: item.id,
isPrimary: false
});
reset();
onCancel();
}}
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
marginTop: 0,
flexDirection: "row",
borderRadius: 0,
alignItems: "center",
width: "100%",
justifyContent: "flex-start"
width: "48%"
}}
/>
<Button
title={loading ? null : strings.continue()}
type="accent"
loading={loading}
onPress={onNext}
style={{
width: "48%"
}}
/>
</View>
<View
style={{
gap: Spacing.LEVEL_1,
marginTop: -Spacing.LEVEL_0
}}
>
{currentMethod.method === "sms" ||
currentMethod.method === "email" ? (
<Button
onPress={onSendCode}
type={"plain"}
disabled={parseInt(`${seconds}`) !== 0}
title={
sending
? ""
: `${
seconds
? strings.resend2faCode(`${seconds}`)
: strings.resendCode()
}`
}
loading={sending}
fontSize={AppFontSize.sm}
fontFamily="REGULAR"
style={{
paddingVertical: 0,
alignSelf: "flex-start",
paddingHorizontal: 0
}}
/>
) : null}
<TouchableOpacity
onPress={onRequestSecondaryMethod}
activeOpacity={0.8}
style={{
alignSelf: "flex-start"
}}
>
<IconButton
type="secondaryAccented"
style={{
marginRight: 10
}}
size={15}
color={colors.primary.accent}
name={item.icon}
/>
<View
style={{
flexShrink: 1
}}
<Paragraph
fontFamily={isRecoveryCode ? "REGULAR" : "SEMI_BOLD"}
fontSize="SM"
color={
isRecoveryCode
? colors.primary.paragraph
: colors.primary.accent
}
>
<Paragraph size={AppFontSize.md}>{item.title}</Paragraph>
</View>
</Pressable>
))}
</>
)}
</View>
</ScrollView>
{strings["2faCodeSecondaryMethodText"][
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
]()}
</Paragraph>
</TouchableOpacity>
</View>
</View>
</>
) : (
<View
style={{
gap: Spacing.LEVEL_2,
width: "100%"
}}
>
{getMethods().map((item) => (
<Pressable
key={item.title}
onPress={() => {
setCurrentMethod({
method: item.id,
isPrimary: false
});
}}
style={{
padding: Spacing.LEVEL_2,
backgroundColor: colors.secondary.background,
flexDirection: "row",
borderRadius: Radius.S,
alignItems: "center",
width: "100%",
justifyContent: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<IconButton
style={{
borderRadius: Radius.XS,
padding: Spacing.LEVEL_1,
width: undefined,
height: undefined,
backgroundColor: RGB_Linear_Shade(
0.04,
hexToRGBA(colors.secondary.background)
)
}}
size={17}
color={colors.primary.icon}
name={item.icon}
iconFamily={item.iconFamily as "notesnook"}
/>
<View
style={{
flexShrink: 1
}}
>
<Paragraph size={AppFontSize.sm}>{item.title}</Paragraph>
</View>
</Pressable>
))}
</View>
)}
</View>
);
};

View File

@@ -21,13 +21,11 @@ import { strings } from "@notesnook/intl";
import { useRef, useState } from "react";
import { TextInput } from "react-native";
import { db } from "../../common/database";
import { ToastManager, eSendEvent } from "../../services/event-manager";
import { ToastManager } from "../../services/event-manager";
import { clearMessage } from "../../services/message";
import PremiumService from "../../services/premium";
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 = {
@@ -46,6 +44,7 @@ export const useLogin = (
const [step, setStep] = useState(LoginSteps.emailAuth);
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const mfaData = useRef<any>(undefined);
const formRef = useRef(
createFormRef({
email: "",
@@ -60,69 +59,43 @@ export const useLogin = (
setLoading(true);
switch (step) {
case LoginSteps.emailAuth: {
if (formRef.current.validateField("email")) {
if (!formRef.current.validate()) {
setLoading(false);
return;
}
const mfaInfo = await db.user.authenticateEmail(
formRef.current.getValue("email")
);
if (mfaInfo) {
TwoFactorVerification.present(
async (mfa: any, callback: (success: boolean) => void, onerror: (e: Error) => void) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
mfa.method
);
if (success) {
setStep(LoginSteps.passwordAuth);
setLoading(false);
setTimeout(() => {
passwordInputRef.current?.focus();
}, 500);
callback && callback(true);
}
callback && callback(false);
} catch (e) {
callback && callback(false);
if ((e as Error).message === "invalid_grant") {
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);
}
}
},
mfaInfo,
() => {
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
);
mfaData.current = mfaInfo;
setStep(LoginSteps.mfaAuth);
setLoading(false);
} else {
finishWithError(new Error(strings.unableToSend2faCode()));
}
break;
}
case LoginSteps.passwordAuth: {
case LoginSteps.passwordAuth:
case LoginSteps.mfaAuth: {
if (!formRef.current.validate()) {
setLoading(false);
return;
}
const values = formRef.current.getValues();
await db.user.authenticatePassword(
try {
await db.user.authenticatePassword(
values.email,
values.password,
undefined,
sessionExpired
);
finishLogin();
finishLogin();
} catch(e) {
formRef.current.setError("password", (e as Error).message);
setLoading(false);
setStep(LoginSteps.passwordAuth);
}
break;
}
}
@@ -172,6 +145,7 @@ export const useLogin = (
setLoading,
error,
setError,
formRef
formRef,
mfaData
};
};

View File

@@ -27,15 +27,15 @@ import Animated, {
useSharedValue,
withTiming
} from "react-native-reanimated";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { editorState } from "../../screens/editor/tiptap/utils";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, normalize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
import AppIcon from "../ui/AppIcon";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
interface FloatingButtonProps {
onPress: () => void;
@@ -47,6 +47,7 @@ interface FloatingButtonProps {
position?: "left" | "right";
size?: "small" | "large";
style?: ViewStyle;
hideOnKeyboard?: boolean;
}
const FloatingButton = ({
@@ -57,26 +58,28 @@ const FloatingButton = ({
testID,
position = "right",
size = "large",
style
style,
hideOnKeyboard = true
}: FloatingButtonProps) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const translate = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const route = useRoute();
const insets = useGlobalSafeAreaInsets();
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translate.value
},
{
translateY: translate.value
}
]
};
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
translateX: hideOnKeyboard ? translate.value : 0
},
{
translateY: hideOnKeyboard ? translate.value : 0
}
],
bottom: hideOnKeyboard ? 15 : keyboardHeight.value + 15
}));
const animate = useCallback(
(toValue: number) => {
@@ -97,12 +100,22 @@ const FloatingButton = ({
editorState().keyboardState = false;
if (deviceMode !== "mobile") return;
animate(0);
keyboardHeight.value = withTiming(0);
};
const onKeyboardShow = () => {
const onKeyboardShow = (e: any) => {
editorState().keyboardState = true;
if (deviceMode !== "mobile") return;
animate(150);
if (hideOnKeyboard) {
animate(150);
} else {
keyboardHeight.value = withTiming(
e.endCoordinates.height - insets.bottom,
{
duration: 250
}
);
}
};
const sub = [
@@ -110,9 +123,9 @@ const FloatingButton = ({
Keyboard.addListener("keyboardDidHide", onKeyboardHide)
];
return () => {
sub.forEach((sub) => sub?.remove?.());
sub.forEach((sub) => sub.remove());
};
}, [deviceMode, animate]);
}, [deviceMode, animate, hideOnKeyboard, keyboardHeight, insets.bottom]);
return deviceMode !== "mobile" && !alwaysVisible ? null : (
<Animated.View
@@ -140,7 +153,7 @@ const FloatingButton = ({
borderBottomWidth: 0,
borderLeftWidth: 0,
borderRightWidth: 0,
backgroundColor: colors.primary.background
backgroundColor: colors.primary.accent
}}
onPress={onPress}
>
@@ -148,26 +161,19 @@ const FloatingButton = ({
style={{
alignItems: "center",
justifyContent: "center",
height: normalize(size === "small" ? 40 : 60),
width: normalize(size === "small" ? 40 : 60),
height: 55,
width: 55,
backgroundColor: color
? RGB_Linear_Shade(0.87, hexToRGBA(color))
: colors.primary.shade,
: colors.primary.accent,
borderRadius: size === "small" ? 15 : 20
}}
>
<Icon
name={
icon
? icon
: route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
color={color || colors.primary.accent}
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}
<AppIcon
name={icon ? icon : route.name === "Trash" ? "trash-alt" : "plus"}
iconFamily="notesnook"
color={color || colors.primary.accentForeground}
size={24}
/>
</View>
</TouchableOpacity>

View File

@@ -1,91 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { useThemeColors } from "@notesnook/theme";
import { useRef } from "react";
import { useWindowDimensions, View } from "react-native";
import { defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import DatePicker from "react-native-date-picker";
import dayjs from "dayjs";
import { strings } from "@notesnook/intl";
import { Button } from "../ui/button";
export default function DatePickerComponent(props: {
onConfirm: (date: Date) => void;
onCancel: () => void;
}) {
const { colors, isDark } = useThemeColors();
const dateRef = useRef<Date>(dayjs().add(1, "week").toDate());
const { width } = useWindowDimensions();
return (
<View
style={{
backgroundColor: colors.primary.background,
borderRadius: defaultBorderRadius,
padding: DefaultAppStyles.GAP,
borderWidth: 0.5,
borderColor: colors.primary.border,
width: "80%",
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<DatePicker
style={{
width: width * 0.8 - DefaultAppStyles.GAP * 2
}}
theme={isDark ? "dark" : "light"}
mode="date"
minimumDate={dayjs().add(1, "day").toDate()}
onCancel={() => {
close?.();
}}
date={dateRef.current}
onDateChange={(date) => {
dateRef.current = date;
}}
/>
<Button
title={strings.setExpiry()}
type="accent"
style={{
width: "100%"
}}
onPress={async () => {
if (!dateRef.current) return;
props.onConfirm(dateRef.current);
}}
/>
<Button
title={strings.cancel()}
type="secondary"
style={{
width: "100%"
}}
onPress={async () => {
props.onCancel();
}}
/>
</View>
);
}

View File

@@ -0,0 +1,200 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import dayjs, { locale } from "dayjs";
import React, { useMemo } from "react";
import { I18nManager, ViewStyle } from "react-native";
import RNDateTimePicker, {
DateType,
useDefaultStyles
} from "react-native-ui-datepicker";
import { FontFamily } from "../../common/design/font";
import { Radius } from "../../common/design/spacing";
import { db } from "../../common/database";
import { useSettingStore } from "../../stores/use-setting-store";
import AppIcon from "../ui/AppIcon";
import DeviceInfo from "react-native-device-info";
export type DateTimePickerMode = "date" | "time" | "datetime";
export type DateTimePickerProps = {
/**
* Which parts of the value can be picked. `date` shows only the calendar,
* `time` shows only the time wheel and `datetime` shows both.
*/
mode?: DateTimePickerMode;
date: Date;
onChange: (date: Date) => void;
minDate?: Date;
maxDate?: Date;
/**
* Force 24-hour time. Defaults to the user's time format setting.
*/
is24Hour?: boolean;
/**
* First day of the week (0 = Sunday, 1 = Monday). Defaults to the user's
* week format setting.
*/
firstDayOfWeek?: number;
style?: ViewStyle;
};
const toDate = (value: DateType) =>
value ? dayjs(value).toDate() : new Date();
/**
* Themed date/time picker built on top of `react-native-ui-datepicker`.
* Renders inline; use `DateTimePicker.present()` (see index) for the sheet.
*/
export default function DateTimePicker({
mode = "date",
date,
onChange,
minDate,
maxDate,
is24Hour,
firstDayOfWeek,
style
}: DateTimePickerProps) {
const { colors, isDark } = useThemeColors();
const defaultStyles = useDefaultStyles(isDark ? "dark" : "light");
const weekFormat = useSettingStore((state) => state.weekFormat);
const use12Hours =
(is24Hour ?? db.settings.getTimeFormat() === "24-hour") === false;
const firstDay = firstDayOfWeek ?? (weekFormat === "Mon" ? 1 : 0);
const styles = useMemo(
() =>
({
...defaultStyles,
today: {
borderColor: colors.primary.accent,
borderWidth: 1,
borderRadius: Radius.S
},
today_label: {
color: colors.primary.accent
},
selected: {
backgroundColor: colors.primary.accent,
borderRadius: Radius.S
},
selected_label: {
color: colors.static.white
},
day_label: {
color: colors.primary.paragraph,
fontFamily: FontFamily.REGULAR
},
disabled_label: {
color: colors.disabled.paragraph
},
outside_label: {
color: colors.secondary.paragraph
},
weekday_label: {
color: colors.secondary.paragraph,
fontFamily: FontFamily.MEDIUM
},
month_selector_label: {
color: colors.primary.heading,
fontFamily: FontFamily.SEMI_BOLD
},
year_selector_label: {
color: colors.primary.heading,
fontFamily: FontFamily.SEMI_BOLD
},
time_selector_label: {
color: colors.primary.heading,
fontFamily: FontFamily.SEMI_BOLD
},
month_label: {
color: colors.primary.paragraph
},
year_label: {
color: colors.primary.paragraph
},
selected_month: {
backgroundColor: colors.primary.accent,
borderRadius: Radius.S
},
selected_month_label: {
color: colors.static.white
},
selected_year: {
backgroundColor: colors.primary.accent,
borderRadius: Radius.S
},
selected_year_label: {
color: colors.static.white
},
active_year: {
backgroundColor: colors.secondary.background,
borderRadius: Radius.S
},
time_label: {
color: colors.primary.heading,
fontFamily: FontFamily.SEMI_BOLD
},
time_selected_indicator: {
backgroundColor: colors.secondary.background,
borderRadius: Radius.S
}
}) as ReturnType<typeof useDefaultStyles>,
[colors, defaultStyles]
);
return (
<RNDateTimePicker
mode="single"
date={date}
timePicker={mode !== "date"}
initialView={mode === "time" ? "time" : "day"}
hideHeader={mode === "time"}
use12Hours={use12Hours}
firstDayOfWeek={firstDay}
minDate={minDate}
maxDate={maxDate}
styles={styles}
components={{
IconPrev: (
<AppIcon
name="chevron-left"
iconFamily="notesnook"
size={20}
color={colors.primary.icon}
/>
),
IconNext: (
<AppIcon
name="chevron-right"
iconFamily="notesnook"
size={20}
color={colors.primary.icon}
/>
)
}}
onChange={(params) => {
const changed = (params as { date: DateType }).date;
onChange(toDate(changed));
}}
/>
);
}

View File

@@ -0,0 +1,224 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { View, ViewStyle } from "react-native";
import { Radius, Spacing } from "../../common/design/spacing";
import { presentDialog } from "../dialog/functions";
import { presentSheet } from "../../services/event-manager";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import DateTimePicker, {
DateTimePickerMode,
DateTimePickerProps
} from "./date-time-picker";
export { default } from "./date-time-picker";
export type {
DateTimePickerMode,
DateTimePickerProps
} from "./date-time-picker";
const DEFAULT_ICONS: Record<DateTimePickerMode, string> = {
date: "calendar-dots",
time: "clock",
datetime: "calendar-dots"
};
export type PresentDateTimePickerOptions = Pick<
DateTimePickerProps,
"minDate" | "maxDate" | "is24Hour" | "firstDayOfWeek"
> & {
mode?: DateTimePickerMode;
/** Initial value the picker opens on. Defaults to now. */
date?: Date;
title?: string;
description?: string;
icon?: string;
/** Label for the confirm button. Defaults to `strings.done()`. */
confirmText?: string;
/** Dialog context to render into (dialog presentation only). */
context?: string;
onConfirm: (date: Date) => void;
onCancel?: () => void;
};
type Presentation = "dialog" | "sheet";
type DateTimePickerContentProps = PresentDateTimePickerOptions & {
presentation: Presentation;
close?: (ctx?: string) => void;
};
/**
* The shared picker body used by both the dialog and the sheet: a header,
* the themed picker, and Cancel/Confirm actions. Presentation-specific chrome
* (the dialog card vs. the sheet surface) is applied by the outer container.
*/
function DateTimePickerContent({
presentation,
mode = "date",
date,
minDate,
maxDate,
is24Hour,
firstDayOfWeek,
title,
description,
icon,
confirmText,
onConfirm,
onCancel,
close
}: DateTimePickerContentProps) {
const { colors } = useThemeColors();
const [selected, setSelected] = useState<Date>(date || new Date());
const containerStyle: ViewStyle =
presentation === "dialog"
? {
width: "90%",
maxWidth: 400,
alignSelf: "center",
backgroundColor: colors.primary.background,
borderRadius: Radius.S,
borderWidth: 0.5,
borderColor: colors.primary.border,
overflow: "hidden"
}
: {
width: "100%"
};
return (
<View style={containerStyle}>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_4,
gap: Spacing.LEVEL_3
}}
>
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 40,
height: 40,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name={icon || DEFAULT_ICONS[mode]}
iconFamily="notesnook"
size={16}
color={colors.primary.icon}
/>
</View>
<View style={{ flexShrink: 1, gap: Spacing.LEVEL_1 }}>
<Heading fontSize="XL" lineHeight="100%">
{title ||
(mode === "time"
? strings.selectTimeHeading()
: strings.selectDate())}
</Heading>
{description ? (
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
{description}
</Paragraph>
) : null}
</View>
</View>
{/* <View style={{ height: 1, backgroundColor: colors.primary.border }} /> */}
<DateTimePicker
mode={mode}
date={selected}
onChange={setSelected}
minDate={minDate}
maxDate={maxDate}
is24Hour={is24Hour}
firstDayOfWeek={firstDayOfWeek}
/>
<View style={{ flexDirection: "row", gap: Spacing.LEVEL_2 }}>
<Button
title={strings.cancel()}
type="plain-outline"
style={{ flex: 1, width: "auto" }}
onPress={() => {
close?.();
onCancel?.();
}}
/>
<Button
title={confirmText || strings.done()}
type="accent"
style={{ flex: 1, width: "auto" }}
onPress={() => {
close?.();
onConfirm(selected);
}}
/>
</View>
</View>
</View>
);
}
/**
* Show the themed date/time picker in a dialog. This is the default surface
* used across the app. Resolves the picked value through `onConfirm`;
* dismissing without confirming calls `onCancel`.
*/
export function presentDateTimePicker(options: PresentDateTimePickerOptions) {
presentDialog({
context: options.context || "global",
component: (close) => (
<DateTimePickerContent {...options} presentation="dialog" close={close} />
)
});
}
/**
* Show the same date/time picker in a bottom sheet. Preferred on the
* add-reminder screen; elsewhere use {@link presentDateTimePicker}.
*/
export function presentDateTimePickerSheet(
options: PresentDateTimePickerOptions
) {
presentSheet({
context: options.context || "global",
component: (_ref, close) => (
<DateTimePickerContent {...options} presentation="sheet" close={close} />
)
});
}

View File

@@ -30,6 +30,7 @@ interface IDelayLayoutProps extends ViewProps {
type?: "default" | "settings";
color?: string;
animated?: boolean;
disabled?: boolean;
}
const placeholder = {
@@ -39,13 +40,14 @@ const placeholder = {
export default function DelayLayout({
animated = true,
disabled,
...props
}: IDelayLayoutProps) {
const { colors } = useThemeColors();
const loading = useDelayLayout(props.delay === undefined ? 200 : props.delay);
const Placeholder = placeholder[props.type || "default"];
return loading || props.wait ? (
return (loading || props.wait) && !disabled ? (
<View
style={{
backgroundColor: colors.primary.background,

View File

@@ -31,8 +31,7 @@ import ImagePreview from "../image-preview";
import MergeConflicts from "../merge-conflicts";
import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import Progress from "../dialogs/progress";
import Progress from "../dialogs/progress/progress";
import { useSettingStore } from "../../stores/use-setting-store";
const DialogProvider = () => {
@@ -51,7 +50,6 @@ const DialogProvider = () => {
{isAppLoading ? null : (
<>
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />

View File

@@ -20,14 +20,11 @@ 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 { ActivityIndicator, StyleSheet, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import { getColorLinearShade } from "../../utils/colors";
import { Spacing } from "../../common/design/spacing";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Button, ButtonProps } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
const DialogButtons = ({
onPressPositive,
@@ -51,82 +48,41 @@ const DialogButtons = ({
return (
<View
style={[
styles.container,
{
backgroundColor: colors.secondary.background,
height: 60,
paddingHorizontal: DefaultAppStyles.GAP,
borderTopWidth: 0.7,
borderTopColor: getColorLinearShade(
colors.secondary.background,
0.05,
isDark
)
flexDirection: "row",
gap: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_3
}
]}
>
{doneText ? (
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<Icon
color={colors.primary.accent}
name="check-circle-outline"
size={AppFontSize.md}
/>
<Paragraph color={colors.primary.accent}>{" " + doneText}</Paragraph>
</View>
) : loading ? (
<ActivityIndicator
size={AppFontSize.lg}
color={colors.primary.accent}
/>
) : (
<View />
)}
<View
<Button
onPress={onPressNegative}
fontSize={AppFontSize.md}
testID={notesnook.ids.default.dialog.no}
type="plain-outline"
style={{
flexDirection: "row",
alignItems: "center"
flexGrow: 1,
flexShrink: 1
}}
>
title={negativeTitle}
/>
{onPressPositive ? (
<Button
onPress={onPressNegative}
fontSize={AppFontSize.sm}
testID={notesnook.ids.default.dialog.no}
type="plain"
onPress={onPressPositive}
fontSize={AppFontSize.md}
testID={notesnook.ids.default.dialog.yes}
style={{
flexGrow: 1,
flexShrink: 1
}}
loading={loading}
bold
title={negativeTitle}
type={positiveType || "accent"}
title={positiveTitle}
/>
{onPressPositive ? (
<Button
onPress={onPressPositive}
fontSize={AppFontSize.sm}
testID={notesnook.ids.default.dialog.yes}
style={{
marginLeft: 10
}}
bold
type={positiveType || "transparent"}
title={positiveTitle}
/>
) : null}
</View>
) : null}
</View>
);
};
export default DialogButtons;
const styles = StyleSheet.create({
container: {
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
marginTop: DefaultAppStyles.GAP_VERTICAL
}
});

View File

@@ -23,6 +23,7 @@ import { DDS } from "../../services/device-detection";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../utils/elevation";
import { getContainerBorder } from "../../utils/colors";
import { Radius } from "../../common/design/spacing";
const DialogContainer = ({
width,
@@ -44,9 +45,10 @@ const DialogContainer = ({
{
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: 10,
borderRadius: Radius.LG,
backgroundColor: colors.primary.background,
paddingTop: 12
paddingTop: 12,
marginTop: -300
},
restProps?.noBorder
? {}

View File

@@ -17,17 +17,20 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Text, View, ViewStyle } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { Radius, Spacing } from "../../common/design/spacing";
import { DefaultAppStyles } from "../../utils/styles";
import AppIcon, { IconProps } from "../ui/AppIcon";
import { Button, ButtonProps } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
type DialogHeaderProps = {
icon?: string;
iconFamily?: IconProps["iconFamily"];
iconType?: "error" | "normal";
title?: string;
paragraph?: string;
button?: ButtonProps;
@@ -46,7 +49,10 @@ const DialogHeader = ({
padding,
centered,
titlePart,
style
style,
icon,
iconFamily,
iconType
}: DialogHeaderProps) => {
const { colors } = useThemeColors();
@@ -57,61 +63,89 @@ const DialogHeader = ({
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
minHeight: 50,
paddingHorizontal: padding,
...style
}}
>
<View
style={{
width: "100%"
width: "100%",
gap: Spacing.LEVEL_4
}}
>
{icon ? (
<View
style={{
alignSelf: centered ? "center" : "flex-start",
width: 40,
height: 40,
backgroundColor: colors.error.shade,
justifyContent: "center",
alignItems: "center",
borderRadius: Radius.XS
}}
>
<AppIcon
name={icon}
iconFamily={iconFamily}
color={
iconType == "error" ? colors.static.red : colors.primary.icon
}
/>
</View>
) : null}
<View
style={{
flexDirection: "row",
justifyContent: centered ? "center" : "space-between",
alignItems: "center"
gap: Spacing.LEVEL_1
}}
>
<Heading
style={{ textAlign: centered ? "center" : "left" }}
size={AppFontSize.lg}
<View
style={{
flexDirection: "row",
justifyContent: centered ? "center" : "space-between",
alignItems: "center"
}}
>
{title}{" "}
{titlePart ? (
<Text style={{ color: colors.primary.accent }}>
{titlePart}
</Text>
) : null}
</Heading>
<Heading
style={{ textAlign: centered ? "center" : "left" }}
fontSize="XL"
>
{title}{" "}
{titlePart ? (
<Text style={{ color: colors.primary.accent }}>
{titlePart}
</Text>
) : null}
</Heading>
{button ? (
<Button
{button ? (
<Button
style={{
borderRadius: 100,
paddingHorizontal: DefaultAppStyles.GAP
}}
fontSize={13}
type={button.type || "secondary"}
height={30}
{...button}
/>
) : null}
</View>
{paragraph ? (
<Paragraph
style={{
borderRadius: 100,
paddingHorizontal: DefaultAppStyles.GAP
textAlign: centered ? "center" : "left",
maxWidth: centered ? "90%" : "100%",
alignSelf: centered ? "center" : "flex-start"
}}
fontSize={13}
type={button.type || "secondary"}
height={30}
{...button}
/>
color={paragraphColor || colors.secondary.paragraph}
>
{paragraph}
</Paragraph>
) : null}
</View>
{paragraph ? (
<Paragraph
style={{
textAlign: centered ? "center" : "left",
maxWidth: centered ? "90%" : "100%",
alignSelf: centered ? "center" : "flex-start"
}}
color={paragraphColor || colors.secondary.paragraph}
>
{paragraph}
</Paragraph>
) : null}
</View>
</View>
</>

View File

@@ -17,16 +17,19 @@ 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, TextInput } from "react-native";
import { KeyboardTypeOptions, TextInput, TextInputProps } 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";
import { IconProps } from "../ui/AppIcon";
import { PressableProps } from "../ui/pressable";
export type DialogInfo = {
title?: string;
paragraph?: string;
centered?: boolean;
positiveText: string;
negativeText: string;
background?: string;
@@ -34,16 +37,10 @@ export type DialogInfo = {
statusBarTranslucent?: boolean;
positivePress?: (...args: any[]) => Promise<any>;
onClose?: () => void;
positiveType?:
| "transparent"
| "plain"
| "secondary"
| "accent"
| "inverted"
| "shade"
| "error"
| "errorShade";
positiveType?: PressableProps["type"];
icon?: string;
iconFamily?: IconProps["iconFamily"];
iconType?: "error" | "normal";
paragraphColor: string;
form?: {
formRef: FormRef;
@@ -54,10 +51,12 @@ export type DialogInfo = {
validators: FieldValidator[];
defaultValue?: string;
ref: RefObject<TextInput | null>;
inputProps?: TextInputProps;
}[];
onFormSubmit?: (form: FormRef) => Promise<boolean>;
onFormSubmit?: (form: FormRef, checked?: boolean) => Promise<boolean>;
};
input: boolean;
inputLabel?: string;
inputPlaceholder: string;
defaultValue: string;
// eslint-disable-next-line @typescript-eslint/ban-types

View File

@@ -19,13 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState,
RefObject
useState
} from "react";
import { TextInput, View, ViewStyle } from "react-native";
import { Spacing } from "../../common/design/spacing";
import { DDS } from "../../services/device-detection";
import {
eSubscribeEvent,
@@ -38,11 +39,10 @@ import { defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import { Checkbox } from "../ui/checkbox";
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";
import DialogButtons from "./dialog-buttons";
import DialogHeader from "./dialog-header";
@@ -74,7 +74,10 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}
if (dialogInfo.form.onFormSubmit) {
setLoading(true);
const result = await dialogInfo.form.onFormSubmit(formRef.current);
const result = await dialogInfo.form.onFormSubmit(
formRef.current,
checked
);
if (result === false) {
setLoading(false);
return;
@@ -143,9 +146,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}, [hide, show]);
const onNegativePress = async () => {
if (dialogInfo?.onClose) {
await dialogInfo.onClose();
}
hide();
};
@@ -155,7 +155,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
maxHeight: 450,
borderRadius: defaultBorderRadius,
backgroundColor: colors.primary.background,
paddingTop: 12,
gap: Spacing.LEVEL_4,
...getContainerBorder(colors.primary.border, 0.5),
overflow: "hidden"
};
@@ -170,9 +170,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
bounce={!dialogInfo.input && !dialogInfo.form}
closeOnTouch={!dialogInfo.disableBackdropClosing}
background={dialogInfo.background}
transparent={
dialogInfo.transparent === undefined ? false : dialogInfo.transparent
}
transparent={dialogInfo.transparent}
onShow={async () => {
if (dialogInfo.input && !dialogInfo.form) {
inputRef.current?.setNativeProps({
@@ -201,54 +199,66 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
: dialogInfo.component}
{dialogInfo.component ? null : (
<View style={style}>
<DialogHeader
title={dialogInfo.title}
icon={dialogInfo.icon}
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
padding={12}
style={{
minHeight: 0
}}
/>
<Seperator half />
<View
style={[
{
paddingVertical: Spacing.LEVEL_4
},
style
]}
>
<View
style={[
{
paddingHorizontal: Spacing.LEVEL_3,
gap: Spacing.LEVEL_4
}
]}
>
<DialogHeader
title={dialogInfo.title}
icon={dialogInfo.icon}
iconFamily={dialogInfo.iconFamily}
iconType={dialogInfo.iconType}
centered={dialogInfo.centered}
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
style={{
minHeight: 0
}}
/>
{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
}}
>
{dialogInfo.form ? (
<View
style={{
gap: Spacing.LEVEL_2
}}
>
{dialogInfo.form.items.map((item, index) => (
<FormInput
{...item.inputProps}
key={item.name}
fwdRef={item.ref}
name={item.name}
label={item.label}
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 ? (
<Input
fwdRef={inputRef}
autoCapitalize="none"
@@ -258,6 +268,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
defaultValue={dialogInfo.defaultValue}
label={dialogInfo.inputLabel}
onSubmit={() => {
onPressPositive();
}}
@@ -266,48 +277,34 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
keyboardType={dialogInfo.keyboardType || "default"}
placeholder={dialogInfo.inputPlaceholder}
/>
</View>
) : null}
) : null}
{dialogInfo?.notice ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Notice
type={dialogInfo.notice.type || "information"}
text={dialogInfo.notice.text}
/>
</View>
) : null}
{dialogInfo?.notice ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Notice
type={dialogInfo.notice.type || "information"}
text={dialogInfo.notice.text}
/>
</View>
) : null}
{dialogInfo.check ? (
<>
<Button
{dialogInfo.check ? (
<Checkbox
checked={checked}
onPress={() => {
setChecked(!checked);
}}
icon={
checked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
iconColor={
checked ? colors.secondary.icon : colors.primary.icon
}
style={{
justifyContent: "flex-start"
}}
height={35}
iconSize={20}
width="100%"
title={dialogInfo.check.info}
type={checked ? dialogInfo.check.type || "plain" : "plain"}
style={{
marginTop: -Spacing.LEVEL_1
}}
/>
</>
) : null}
) : null}
</View>
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={

View File

@@ -16,9 +16,11 @@ 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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import { db } from "../../../common/database";
import {
clearAppLockVerificationCipher,
@@ -33,7 +35,7 @@ import {
eSubscribeEvent
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { PASSWORD_PLACEHOLDER } from "../../../utils/constants";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseAppLocKPasswordDailog,
@@ -42,61 +44,122 @@ import {
import { AppFontSize } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
import Heading from "../../ui/typography/heading";
export const AppLockPassword = () => {
const { colors } = useThemeColors();
const [mode, setMode] = useState<"create" | "change" | "remove">("create");
const [keyboardType, setKeyboardType] = useState<"pin" | "password">(
useSettingStore.getState().settings.applockKeyboardType === "default"
? "password"
: "pin"
);
const [visible, setVisible] = useState(false);
const currentPasswordInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
const values = useRef<{
currentPassword?: string;
password?: string;
confirmPassword?: string;
}>({});
const [secureTextEntry, setSecureTextEntry] = useState(true);
const formRef = useRef(
createFormRef({
currentPassword: "",
password: "",
confirmPassword: ""
})
);
const [accountPass, setAccountPass] = useState(false);
const enableApplock = useRef(false);
useEffect(() => {
const subs = [
eSubscribeEvent(
eOpenAppLockPasswordDialog,
(mode: "create" | "change" | "remove") => {
(mode: "create" | "change" | "remove", _enableApplock = false) => {
setMode(mode);
setAccountPass(false);
setVisible(true);
enableApplock.current = _enableApplock;
}
),
eSubscribeEvent(eCloseAppLocKPasswordDailog, () => {
values.current = {};
setVisible(false);
close();
})
];
return () => {
subs.forEach((sub) => sub?.unsubscribe());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const close = () => {
values.current = {};
formRef.current.setValue("currentPassword", "");
formRef.current.setValue("password", "");
formRef.current.setValue("confirmPassword", "");
formRef.current.clearErrors();
setVisible(false);
};
const onSubmit = async () => {
if (!formRef.current.validate()) return;
const currentPassword = formRef.current.getValue("currentPassword");
const password = formRef.current.getValue("password");
if (mode === "create") {
setAppLockVerificationCipher(password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
if (enableApplock.current) {
SettingsService.setProperty("appLockEnabled", true);
}
} else if (mode === "change") {
const isCurrentPasswordCorrect =
await validateAppLockPassword(currentPassword);
if (!isCurrentPasswordCorrect) {
formRef.current.setError(
"currentPassword",
strings.incorrect("password")
);
return;
}
await clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(password);
} else if (mode === "remove") {
const isCurrentPasswordCorrect = accountPass
? await db.user.verifyPassword(password)
: await validateAppLockPassword(password);
if (!isCurrentPasswordCorrect) {
formRef.current.setError(
"password",
accountPass
? strings.passwordIncorrect()
: strings.incorrect("password")
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometricService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
ToastManager.show({
message: strings.applockDisabled(),
type: "success"
});
}
}
close();
};
return !visible ? null : (
<BaseDialog
onShow={async () => {
@@ -115,286 +178,132 @@ export const AppLockPassword = () => {
style={{
...getElevationStyle(10),
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
borderRadius: Radius.MD,
backgroundColor: colors.primary.background,
paddingTop: 12
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_4,
gap: Spacing.LEVEL_4
}}
>
<DialogHeader
title={strings.changeAppLockCredentials(mode, keyboardType)}
icon="shield"
padding={12}
/>
<Seperator half />
<Heading fontSize="XL" lineHeight="100%">
{strings.changeAppLockCredentials(mode, "password")}
</Heading>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View style={{ gap: Spacing.LEVEL_2 }}>
{mode === "change" ? (
<Input
<FormInput
name="currentPassword"
formRef={formRef}
label={strings.currentPassword()}
fwdRef={currentPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.currentPassword = value;
}}
onSubmit={() => {
passwordInputRef.current?.focus();
}}
defaultValue={values.current.currentPassword}
autoComplete="password"
onSubmitEditing={() => passwordInputRef.current?.focus()}
returnKeyLabel={strings.next()}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
returnKeyType="next"
secureTextEntry={secureTextEntry}
placeholder={
keyboardType === "pin"
? strings.currentPin()
: strings.currentPassword()
}
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
containerStyle={{ borderRadius: Radius.XS }}
validators={[validators.required(strings.passwordRequired())]}
/>
) : null}
<Input
<FormInput
name="password"
formRef={formRef}
label={
mode === "change"
? strings.newPassword()
: strings.enterPassword()
}
fwdRef={passwordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.password = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.password}
keyboardType={
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
}
autoComplete="password"
onSubmitEditing={() => {
if (mode !== "remove") {
confirmPasswordInputRef.current?.focus();
} else {
onSubmit();
}
}}
returnKeyLabel={
mode !== "remove" ? strings.next() : strings.remove()
}
returnKeyType={mode !== "remove" ? "next" : "done"}
secureTextEntry={secureTextEntry}
buttonLeft={
accountPass ? null : (
<IconButton
name={keyboardType === "password" ? "numeric" : "keyboard"}
onPress={() => {
setKeyboardType(
keyboardType === "password" ? "pin" : "password"
);
setSecureTextEntry(false);
setImmediate(() => {
setSecureTextEntry(true);
});
}}
style={{
width: 25,
height: 25,
marginRight: 5
}}
size={AppFontSize.lg}
/>
)
}
placeholder={
accountPass
? strings.enterAccountPassword()
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin"
? strings.pin()
: strings.password()
}`
}
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
containerStyle={{ borderRadius: Radius.XS }}
validators={[validators.required(strings.passwordRequired())]}
/>
{mode !== "remove" ? (
<Input
fwdRef={confirmPasswordInputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.confirmPassword = value;
}}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.confirmPassword}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
customValidator={() => values.current.password || ""}
validationType="confirmPassword"
autoComplete="password"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry={secureTextEntry}
placeholder={
keyboardType === "pin"
? strings.confirmPin()
<FormInput
name="confirmPassword"
formRef={formRef}
label={
mode === "change"
? strings.confirmNewPassword()
: strings.confirmPassword()
}
fwdRef={confirmPasswordInputRef}
autoCapitalize="none"
autoComplete="password"
onSubmitEditing={() => onSubmit()}
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
placeholder={PASSWORD_PLACEHOLDER}
containerStyle={{ borderRadius: Radius.XS }}
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField("password", strings.passwordNotMatched())
]}
/>
) : null}
{mode === "remove" ? (
<>
<Button
icon={
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
}
onPress={() => {
setSecureTextEntry(false);
setAccountPass(!accountPass);
setTimeout(() => {
setSecureTextEntry(true);
});
}}
iconSize={AppFontSize.lg}
type="plain"
iconColor={
accountPass ? colors.primary.accent : colors.primary.icon
}
title={strings.useAccountPassword()}
style={{
width: "100%",
alignSelf: "flex-start",
justifyContent: "flex-start",
paddingHorizontal: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
/>
</>
<Button
icon={accountPass ? "checkbox" : "box-empty"}
iconFamily="notesnook"
onPress={() => setAccountPass(!accountPass)}
iconSize={AppFontSize.sm}
fontFamily="MEDIUM"
fontSize={AppFontSize.xs}
type="plain"
iconColor={
accountPass
? [colors.primary.accent, colors.primary.accentForeground]
: colors.primary.icon
}
textStyle={{
color: colors.primary.paragraph
}}
title={strings.useAccountPassword()}
style={{
width: "100%",
alignSelf: "flex-start",
justifyContent: "flex-start",
paddingHorizontal: 0,
paddingVertical: 0
}}
/>
) : null}
</View>
<DialogButtons
onPressNegative={close}
onPressPositive={async () => {
if (mode === "create") {
if (!values.current.password || !values.current.confirmPassword) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(strings.mismatch(keyboardType)),
undefined,
"local"
);
return;
}
const password = values.current.password;
setAppLockVerificationCipher(password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
} else if (mode === "change") {
if (
!values.current.currentPassword ||
!values.current.password ||
!values.current.confirmPassword
) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(strings.mismatch(keyboardType)),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.currentPassword
);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(strings.incorrect(keyboardType)),
undefined,
"local"
);
return;
}
const password = values.current.password;
await clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(password);
} else if (mode === "remove") {
if (!values.current.password) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = accountPass
? await db.user.verifyPassword(values.current.password)
: await validateAppLockPassword(values.current.password);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
accountPass
? strings.passwordIncorrect()
: strings.incorrect(keyboardType)
),
undefined,
"local"
);
return;
}
clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometricService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(
SettingsService.getProperty("privacyScreen")
);
ToastManager.show({
message: strings.applockDisabled(),
type: "success"
});
}
}
SettingsService.setProperty(
"applockKeyboardType",
keyboardType === "password" ? "default" : "numeric"
);
close();
}}
positiveTitle={
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
}
negativeTitle={strings.cancel()}
positiveType="transparent"
loading={false}
doneText=""
/>
<View style={{ flexDirection: "row", gap: Spacing.LEVEL_2 }}>
<Button
title={strings.cancel()}
type="plain-outline"
onPress={close}
style={{ flex: 1, paddingVertical: Spacing.LEVEL_3 }}
/>
<Button
title={mode === "remove" ? strings.remove() : strings.save()}
type="accent"
onPress={onSubmit}
style={{ flex: 1, paddingVertical: Spacing.LEVEL_3 }}
/>
</View>
</View>
<Toast context="local" />
@@ -402,6 +311,9 @@ export const AppLockPassword = () => {
);
};
AppLockPassword.present = (mode: "create" | "change" | "remove") => {
eSendEvent(eOpenAppLockPasswordDialog, mode);
AppLockPassword.present = (
mode: "create" | "change" | "remove",
enableAppLock?: boolean
) => {
eSendEvent(eOpenAppLockPasswordDialog, mode, enableAppLock);
};

View File

@@ -45,6 +45,7 @@ import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import ReactNativeBlobUtil from "react-native-blob-util";
import { useSettingStore } from "../../../stores/use-setting-store";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
@@ -286,6 +287,9 @@ const PDFPreview = () => {
color={colors.static.white}
name="open-in-new"
onPress={() => {
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
FileViewer.open(pdfSource, {
showOpenWithDialog: true,
showAppsSuggestions: true

View File

@@ -17,28 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import { eSendEvent, eSubscribeEvent } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import { Button } from "../../ui/button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
export type ProgressOptions = {
progress?: string;
cancelCallback?: () => void;
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
};
import { eSendEvent } from "../../../services/event-manager";
import { ProgressOptions } from "./progress";
export const PROGRESS_EVENTS = {
start: "startProgress",
@@ -46,146 +26,6 @@ export const PROGRESS_EVENTS = {
update: "updateProgress"
};
export default function Progress() {
const { colors } = useThemeColors();
const [progress, setProgress] = React.useState<string | undefined>();
const [visible, setVisible] = React.useState(false);
const cancelCallback = React.useRef<() => void>(undefined);
const [data, setData] = React.useState<{
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
}>();
useEffect(() => {
const events = [
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
setProgress(options.progress);
cancelCallback.current = options.cancelCallback;
setData({
title: options.title,
paragraph: options.paragraph,
fillBackground: options.fillBackground,
canHideProgress: options.canHideProgress
});
setVisible(true);
}),
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
setProgress(undefined);
setVisible(false);
setData(undefined);
cancelCallback.current?.();
cancelCallback.current = undefined;
}),
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
setProgress(options.progress);
if (options.cancelCallback) {
cancelCallback.current = options.cancelCallback;
}
const data: ProgressOptions = {};
if (options.title) data.title = options.title;
if (options.paragraph) data.paragraph = options.paragraph;
if (options.fillBackground)
data.fillBackground = options.fillBackground;
setData((current) => {
return {
...current,
...data
};
});
})
];
return () => {
events.forEach((event) => event?.unsubscribe());
cancelCallback.current = undefined;
};
}, []);
return !visible ? null : (
<BaseDialog
background={data?.fillBackground ? colors.primary.background : undefined}
visible
>
<DialogContainer
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingBottom: 10
}}
noBorder={data?.fillBackground ? true : false}
>
<Dialog context="local" />
<View
style={{
justifyContent: "center",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
gap: 10,
paddingBottom: 20
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.paragraph}
size={AppFontSize.lg}
>
{data?.title}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
>
{progress ? progress : data?.paragraph}
</Paragraph>
<View
style={{
flexDirection: "row",
width: 100,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<ProgressBarComponent
height={5}
width={100}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
{!data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="secondaryAccented"
onPress={() => {
if (cancelCallback.current) {
cancelCallback.current?.();
}
setVisible(false);
setProgress(undefined);
setData(undefined);
}}
width="100%"
/>
)}
</View>
</DialogContainer>
</BaseDialog>
);
}
export function startProgress(options: ProgressOptions) {
eSendEvent(PROGRESS_EVENTS.start, options);
}

View File

@@ -0,0 +1,210 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import { PROGRESS_EVENTS } from ".";
import { Radius, Spacing } from "../../../common/design/spacing";
import { eSubscribeEvent } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
export type ProgressOptions = {
progress?: string;
cancelCallback?: () => void;
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
icon?: string;
};
export default function Progress() {
const { colors } = useThemeColors();
const [progress, setProgress] = React.useState<string | undefined>();
const [visible, setVisible] = React.useState(false);
const cancelCallback = React.useRef<() => void>(undefined);
const [data, setData] = React.useState<{
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
icon?: string;
}>();
useEffect(() => {
const events = [
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
setProgress(options.progress);
cancelCallback.current = options.cancelCallback;
setData({
...options
});
setVisible(true);
}),
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
setProgress(undefined);
setVisible(false);
setData(undefined);
cancelCallback.current?.();
cancelCallback.current = undefined;
}),
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
setProgress(options.progress);
if (options.cancelCallback) {
cancelCallback.current = options.cancelCallback;
}
const data: ProgressOptions = {};
if (options.title) data.title = options.title;
if (options.paragraph) data.paragraph = options.paragraph;
if (options.fillBackground)
data.fillBackground = options.fillBackground;
if (options.icon) {
data.icon = options.icon;
}
setData((current) => {
return {
...current,
...data
};
});
})
];
return () => {
events.forEach((event) => event?.unsubscribe());
cancelCallback.current = undefined;
};
}, []);
return !visible ? null : (
<BaseDialog
background={data?.fillBackground ? colors.primary.background : undefined}
visible
>
<DialogContainer
style={{
padding: Spacing.LEVEL_4
}}
noBorder={data?.fillBackground ? true : false}
>
<Dialog context="local" />
<View
style={{
justifyContent: "center",
alignItems: "center",
gap: Spacing.LEVEL_4,
paddingTop: Spacing.LEVEL_1
}}
>
{data?.icon ? (
<View
style={{
width: 40,
height: 40,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<AppIcon
name={data?.icon}
iconFamily="notesnook"
size={20}
color={colors.primary.icon}
/>
</View>
) : null}
<View
style={{
gap: Spacing.LEVEL_1
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.heading}
size={AppFontSize.lg}
>
{data?.title}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
>
{progress ? progress : data?.paragraph}
</Paragraph>
<View
style={{
flexDirection: "row",
width: "100%"
}}
>
<ProgressBarComponent
height={8}
width={null}
style={{
flex: 1
}}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
</View>
{!data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="accent"
onPress={() => {
if (cancelCallback.current) {
cancelCallback.current?.();
}
setVisible(false);
setProgress(undefined);
setData(undefined);
}}
width={160}
/>
)}
</View>
</DialogContainer>
</BaseDialog>
);
}

View File

@@ -55,10 +55,7 @@ import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import {
isEncryptedContent,
Note,
@@ -67,9 +64,13 @@ import {
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../../stores/use-user-store";
import { AppFontSize } from "../../../utils/size";
import { Pressable } from "../../ui/pressable";
import AppIcon from "../../ui/AppIcon";
import { Radius, Spacing } from "../../../common/design/spacing";
import { PASSWORD_PLACEHOLDER } from "../../../utils/constants";
import { AppFontSize } from "../../../utils/size";
import { useSettingStore } from "../../../stores/use-setting-store";
import { Checkbox } from "../../ui/checkbox";
export const VaultDialog: React.FC = () => {
const { colors } = useThemeColors();
@@ -91,7 +92,7 @@ export const VaultDialog: React.FC = () => {
const paragraphRef = useRef<string | null>(null);
const buttonTitleRef = useRef<string | null>(null);
const positiveButtonTypeRef = useRef<"errorShade" | "transparent" | "accent">(
"transparent"
"accent"
);
const customActionTitleRef = useRef<string | null>(null);
const customActionParagraphRef = useRef<string | null>(null);
@@ -119,6 +120,7 @@ export const VaultDialog: React.FC = () => {
const passInputRef = useRef<TextInput>(null);
const confirmPassRef = useRef<TextInput>(null);
const newPassInputRef = useRef<TextInput>(null);
const [icon, setIcon] = useState<string>();
const close = useCallback(() => {
if (loading) {
@@ -146,7 +148,7 @@ export const VaultDialog: React.FC = () => {
descriptionRef.current = null;
paragraphRef.current = null;
buttonTitleRef.current = null;
positiveButtonTypeRef.current = "transparent";
positiveButtonTypeRef.current = "accent";
customActionTitleRef.current = null;
customActionParagraphRef.current = null;
noteLockedRef.current = false;
@@ -186,7 +188,6 @@ export const VaultDialog: React.FC = () => {
}
await db.vault.delete(deleteAll);
await BiometricService.resetCredentials();
if (deleteAll) {
noteIds.forEach((id) => {
eSendEvent(
@@ -355,6 +356,7 @@ export const VaultDialog: React.FC = () => {
async (note: Note & { content?: NoteContent<false> }) => {
close();
try {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
await Share.open({
title: note.title,
failOnCancel: false,
@@ -628,7 +630,7 @@ export const VaultDialog: React.FC = () => {
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
positiveButtonTypeRef.current = data.positiveButtonType || "accent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
onUnlockRef.current = data.onUnlock;
@@ -637,7 +639,8 @@ export const VaultDialog: React.FC = () => {
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
// setBiometricUnlock(fingerprint);
setIcon(data.icon);
setDeleteAll(false);
setLoading(false);
@@ -707,24 +710,36 @@ export const VaultDialog: React.FC = () => {
style={{
...getElevationStyle(5),
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
borderRadius: Radius.MD,
backgroundColor: colors.primary.background,
paddingTop: 12,
paddingVertical: Spacing.LEVEL_4,
overflow: "hidden"
}}
>
<DialogHeader
title={titleRef.current}
title={
requestType === VaultRequestType.PermanentUnlock
? noteRef.current?.title
: titleRef.current
}
paragraph={
paragraphRef.current || customActionParagraphRef.current || ""
}
icon="shield"
padding={12}
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
centered={!!icon}
icon={icon}
iconType="error"
iconFamily="notesnook"
/>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: Spacing.LEVEL_3,
gap: isDeleteVault ? 0 : Spacing.LEVEL_4,
marginTop: Spacing.LEVEL_4,
marginBottom: Spacing.LEVEL_4
}}
>
{(isChangePassword ||
@@ -743,20 +758,20 @@ export const VaultDialog: React.FC = () => {
<FormInput
name="password"
formRef={formRef}
label={
isDeleteVault
? strings.enterAccountPassword()
: isClearVault
? strings.enterVaultPassword()
: isChangePassword
? strings.currentPassword()
: strings.password()
}
fwdRef={passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
autoComplete="password"
marginBottom={
!biometricUnlock ||
!isBiometryEnrolled ||
isCreateVault ||
isChangePassword ||
isCustomAction
? 0
: 10
}
onSubmitEditing={() => {
if (isChangePassword) {
newPassInputRef.current?.focus();
@@ -769,11 +784,7 @@ export const VaultDialog: React.FC = () => {
}
returnKeyType={isChangePassword ? "next" : "done"}
secureTextEntry
placeholder={
isChangePassword
? strings.currentPassword()
: strings.password()
}
placeholder={PASSWORD_PLACEHOLDER}
validators={[validators.required(strings.passwordRequired())]}
/>
@@ -800,37 +811,36 @@ export const VaultDialog: React.FC = () => {
{isDeleteVault && (
<Pressable
onPress={() => setDeleteAll(!deleteAll)}
icon={deleteAll ? "checkbox" : "box-empty"}
iconFamily="notesnook"
fontFamily="MEDIUM"
iconSize={14}
fontSize={AppFontSize.sm}
width="100%"
style={{
justifyContent: "flex-start",
paddingHorizontal: 0,
paddingVertical: 0,
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL,
marginTop: isUserLoggedIn
? DefaultAppStyles.GAP_VERTICAL_SMALL
: 0,
justifyContent: "flex-start"
marginTop: Spacing.LEVEL_1
}}
>
<AppIcon
name={
deleteAll
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
color={colors.error.accent}
size={AppFontSize.md}
/>
<Paragraph color={colors.error.accent} size={AppFontSize.sm}>
{strings.deleteAllNotes()}
</Paragraph>
</Pressable>
title={strings.deleteAllNotes()}
type="transparent"
iconColor={
deleteAll
? [colors.error.accent, colors.error.accentForeground]
: colors.error.accent
}
textStyle={{
color: colors.error.accent
}}
/>
)}
{isChangePassword ? (
<>
<Seperator half />
<FormInput
name="newPassword"
label={strings.newPassword()}
formRef={formRef}
fwdRef={newPassInputRef}
editable={!loading}
@@ -843,16 +853,21 @@ export const VaultDialog: React.FC = () => {
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={strings.newPassword()}
placeholder={PASSWORD_PLACEHOLDER}
validators={[validators.required(strings.passwordRequired())]}
/>
</>
) : null}
{isCreateVault ? (
<View>
<View
style={{
gap: Spacing.LEVEL_2
}}
>
<FormInput
name="password"
label={strings.password()}
formRef={formRef}
fwdRef={passInputRef}
autoCapitalize="none"
@@ -864,12 +879,13 @@ export const VaultDialog: React.FC = () => {
onSubmitEditing={() => {
confirmPassRef.current?.focus();
}}
placeholder={strings.password()}
placeholder={PASSWORD_PLACEHOLDER}
validators={[validators.required(strings.passwordRequired())]}
/>
<FormInput
name="confirmPassword"
label={strings.confirmPassword()}
formRef={formRef}
fwdRef={confirmPassRef}
autoCapitalize="none"
@@ -882,7 +898,7 @@ export const VaultDialog: React.FC = () => {
onSubmitEditing={() => {
onPress();
}}
placeholder={strings.confirmPassword()}
placeholder={PASSWORD_PLACEHOLDER}
validators={[
validators.required(strings.confirmPasswordRequired()),
validators.matchField(
@@ -894,12 +910,7 @@ export const VaultDialog: React.FC = () => {
</View>
) : null}
{biometricUnlock && !isBiometryEnrolled && !isCreateVault ? (
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
) : null}
{!biometricUnlock &&
!isBiometryEnrolled &&
{!isBiometryEnrolled &&
isBiometryAvailable &&
(requestType === VaultRequestType.CopyNote ||
requestType === VaultRequestType.DeleteNote ||
@@ -908,20 +919,16 @@ export const VaultDialog: React.FC = () => {
requestType === VaultRequestType.GoToEditor ||
requestType === VaultRequestType.PermanentUnlock ||
requestType === VaultRequestType.LockNote) ? (
<Button
<Checkbox
title={strings.vaultEnableBiometrics()}
checked={biometricUnlock}
iconSize={12}
style={{
marginTop: -Spacing.LEVEL_1
}}
onPress={() => {
setBiometricUnlock(!biometricUnlock);
}}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
icon="fingerprint"
width="100%"
title={strings.unlockWithBiometrics()}
iconColor={
biometricUnlock ? colors.selected.accent : colors.primary.icon
}
type={biometricUnlock ? "transparent" : "plain"}
/>
) : null}
</View>

View File

@@ -17,25 +17,23 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { View } from "react-native";
import { Platform, View, ViewStyle } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import { Radius, Spacing } from "../../common/design/spacing";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { RouteName } from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { eScrollEvent } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { IconButtonProps } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import { fluidTabsRef } from "../../utils/global-refs";
import { IconButton, IconButtonProps } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
export const Header = ({
renderedInRoute,
@@ -45,7 +43,8 @@ export const Header = ({
canGoBack,
hasSearch,
onSearch,
rightButton
rightButton,
style
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute?: RouteName;
@@ -56,6 +55,7 @@ export const Header = ({
hasSearch?: boolean;
onSearch?: () => void;
rightButton?: IconButtonProps;
style?: ViewStyle;
}) => {
const { colors } = useThemeColors();
const [borderHidden, setBorderHidden] = useState(true);
@@ -64,6 +64,9 @@ export const Header = ({
state.selectionMode
]);
const deviceMode = useSettingStore((state) => state.deviceMode);
const isTablet = deviceMode === "tablet";
const onScroll = useCallback(
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
@@ -85,34 +88,60 @@ export const Header = ({
};
}, [borderHidden, onScroll]);
const HeaderWrapper = hasSearch ? Pressable : View;
const _onLeftButtonPress = () => {
if (onLeftMenuButtonPress) return onLeftMenuButtonPress();
if (!canGoBack) {
if (fluidTabsRef.current?.isDrawerOpen()) {
Navigation.closeDrawer();
} else {
Navigation.openDrawer();
}
return;
}
Navigation.goBack();
};
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: Spacing.LEVEL_3,
marginTop: Platform.OS === "android" ? 5 : 0
}}
>
<HeaderWrapper
style={{
flexDirection: "row",
justifyContent: "space-between",
borderRadius: 10,
paddingVertical: 3,
borderWidth: hasSearch ? 1 : 0,
borderColor: colors.primary.border,
paddingHorizontal: !hasSearch ? 0 : DefaultAppStyles.GAP_SMALL,
alignItems: "center"
}}
<View
style={[
{
flexDirection: "row",
justifyContent: "space-between",
borderRadius: Radius.S,
paddingVertical: Spacing.LEVEL_3,
backgroundColor: colors.secondary.background,
alignItems: "center"
},
style
]}
testID="search-header"
onPress={() => {
onSearch?.();
}}
>
<LeftMenus
canGoBack={canGoBack}
onLeftButtonPress={onLeftMenuButtonPress}
/>
{isTablet && !canGoBack ? null : (
<IconButton
testID={notesnook.ids.default.header.buttons.left}
left={40}
top={40}
onPress={_onLeftButtonPress}
onLongPress={() => {
Navigation.popToTop();
}}
style={{
width: 20,
height: 20
}}
size={20}
name={canGoBack ? "arrow-back" : "menu"}
iconFamily="notesnook"
color={colors.primary.icon}
/>
)}
{!title ? (
<View
@@ -123,18 +152,46 @@ export const Header = ({
borderRadius: 100
}}
/>
) : hasSearch ? (
<Paragraph>
{selectionMode
? `${selectedItemsList.length} selected`
: strings.searchInRoute(title)}
</Paragraph>
) : (
<Heading size={AppFontSize.lg}>{title}</Heading>
<Heading fontSize="XL">
{selectionMode ? `${selectedItemsList.length} selected` : title}
</Heading>
)}
<RightMenus rightButton={rightButton} />
</HeaderWrapper>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{rightButton ? (
<IconButton {...rightButton} color={colors.primary.icon} />
) : !hasSearch ? (
<View
style={{
width: 20,
height: 20
}}
/>
) : null}
{hasSearch ? (
<IconButton
color={colors.primary.icon}
size={20}
onPress={() => {
onSearch?.();
}}
style={{
width: 20,
height: 20
}}
iconFamily="notesnook"
name="search"
/>
) : null}
</View>
</View>
</View>
);
};

View File

@@ -1,66 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { notesnook } from "../../../e2e/test.ids";
import Navigation from "../../services/navigation";
import { useSettingStore } from "../../stores/use-setting-store";
import { fluidTabsRef } from "../../utils/global-refs";
import { IconButton } from "../ui/icon-button";
export const LeftMenus = ({
canGoBack,
onLeftButtonPress
}: {
canGoBack?: boolean;
onLeftButtonPress?: () => void;
}) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const isTablet = deviceMode === "tablet";
const _onLeftButtonPress = () => {
if (onLeftButtonPress) return onLeftButtonPress();
if (!canGoBack) {
if (fluidTabsRef.current?.isDrawerOpen()) {
Navigation.closeDrawer();
} else {
Navigation.openDrawer();
}
return;
}
Navigation.goBack();
};
return isTablet && !canGoBack ? null : (
<IconButton
testID={notesnook.ids.default.header.buttons.left}
left={40}
top={40}
onPress={_onLeftButtonPress}
onLongPress={() => {
Navigation.popToTop();
}}
name={canGoBack ? "arrow-left" : "menu"}
color={colors.primary.icon}
/>
);
};

View File

@@ -0,0 +1,314 @@
/*
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 { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSequence,
withSpring,
withTiming
} from "react-native-reanimated";
import Svg, {
Circle,
ClipPath,
Defs,
G,
Mask,
Path,
Rect
} from "react-native-svg";
import { useThemeColors } from "@notesnook/theme";
const SPRING_CONFIG = {
damping: 14,
stiffness: 120,
mass: 0.8
};
// Each icon floats independently with a different phase/speed
function useFloatAnim(delay: number, range = 6) {
const y = useSharedValue(0);
React.useEffect(() => {
const start = () => {
y.value = withSequence(
withTiming(0, { duration: delay, easing: Easing.linear }),
withRepeat(
withSequence(
withTiming(-range, {
duration: 1600,
easing: Easing.inOut(Easing.sin)
}),
withTiming(range, {
duration: 1600,
easing: Easing.inOut(Easing.sin)
})
),
-1, // infinite
true // reverse
)
);
};
start();
}, []);
return y;
}
export const IntroIllustration = () => {
const { isDark } = useThemeColors();
// Gesture-driven tilt
const tiltX = useSharedValue(0); // rotateX: finger up/down
const tiltY = useSharedValue(0); // rotateY: finger left/right
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
// Independent float offsets for each corner icon
const floatTR = useFloatAnim(0, 2); // top-right lock
const floatBR = useFloatAnim(500, 4); // bottom-right eye-off
const floatTL = useFloatAnim(260, 3); // top-left share
const floatBL = useFloatAnim(780, 2); // bottom-left document
const pan = Gesture.Pan()
.onUpdate((e) => {
// Clamp tilt to ±15 degrees and translate ±12 pts
tiltY.value = (e.translationX / 12) * 1;
tiltX.value = -(e.translationY / 12) * 1;
translateX.value = e.translationX * 0.08;
translateY.value = e.translationY * 0.08;
})
.onEnd(() => {
tiltX.value = withSpring(0, SPRING_CONFIG);
tiltY.value = withSpring(0, SPRING_CONFIG);
translateX.value = withSpring(0, SPRING_CONFIG);
translateY.value = withSpring(0, SPRING_CONFIG);
});
const containerStyle = useAnimatedStyle(() => ({
transform: [
{ perspective: 600 },
{ rotateX: `${tiltX.value}deg` },
{ rotateY: `${tiltY.value}deg` },
{ translateX: translateX.value },
{ translateY: translateY.value }
]
}));
// Derived animated styles for each floating icon
const styleTR = useAnimatedStyle(() => ({
transform: [{ translateY: floatTR.value }]
}));
const styleBR = useAnimatedStyle(() => ({
transform: [{ translateY: floatBR.value }]
}));
const styleTL = useAnimatedStyle(() => ({
transform: [{ translateY: floatTL.value }]
}));
const styleBL = useAnimatedStyle(() => ({
transform: [{ translateY: floatBL.value }]
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={containerStyle}>
<Svg width={243} height={210} viewBox="0 0 243 210" fill="none">
{/* ── Concentric background circles (static) ── */}
<Circle
cx={116}
cy={105}
r={104.75}
stroke={isDark ? "#1F2722" : "#E8F4ED"}
strokeWidth={0.5}
/>
<Circle
cx={116.5}
cy={105.5}
r={95.25}
fill="#008836"
fillOpacity={0.05}
stroke={isDark ? "#233C2D" : "#E8F0EC"}
strokeWidth={0.5}
/>
<Circle
cx={116.5}
cy={105.5}
r={82.25}
fill="#008836"
fillOpacity={0.04}
stroke={isDark ? "#233C2D" : "#E3F1E8"}
strokeWidth={0.5}
/>
<Circle
cx={116.5}
cy={105.5}
r={62.25}
fill="#008836"
fillOpacity={0.06}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<Circle cx={116.5} cy={105.5} r={44.5} fill="#008836" />
{/* ── Defs ── */}
<Defs>
<ClipPath id="clip0">
<Rect width={45} height={45} x={94.5} y={83} />
</ClipPath>
<ClipPath id="clip1">
<Rect width={21.5} height={21.5} x={202.75} y={176.75} />
</ClipPath>
<Mask
id="mask0"
maskUnits="userSpaceOnUse"
x={91}
y={80}
width={52}
height={51}
>
<Path
d="M132.504 80.055H101.496C96.007 80.055 91.5574 84.5046 91.5574 89.9935V121.002C91.5574 126.491 96.007 130.94 101.496 130.94H132.504C137.993 130.94 142.443 126.491 142.443 121.002V89.9935C142.443 84.5046 137.993 80.055 132.504 80.055Z"
fill="white"
/>
</Mask>
</Defs>
{/* ── Centre logo (static) ── */}
<G clipPath="url(#clip0)">
<G mask="url(#mask0)">
<Path
d="M127.584 113.991C126.726 116.496 125.008 118.616 122.734 119.973C120.461 121.33 117.78 121.836 115.168 121.401C112.556 120.967 110.183 119.62 108.471 117.6C106.759 115.58 105.82 113.018 105.819 110.371V104.892L109.751 106.535V110.368C109.751 111.401 109.971 112.422 110.398 113.362C110.825 114.302 111.448 115.14 112.226 115.819C112.373 115.948 112.527 116.073 112.685 116.19C113.845 117.054 115.239 117.549 116.685 117.611C116.734 117.611 116.781 117.615 116.83 117.616C116.879 117.617 116.942 117.616 116.998 117.616H117.166C117.221 117.616 117.262 117.616 117.311 117.611C118.756 117.549 120.149 117.054 121.31 116.191C121.467 116.074 121.621 115.949 121.769 115.821C122.79 114.927 123.541 113.765 123.934 112.467L127.584 113.991ZM128.181 100.628V110.371C128.181 110.497 128.181 110.623 128.173 110.749L124.249 109.108V100.628C124.249 98.7551 123.523 96.9554 122.225 95.6055C120.928 94.2557 119.158 93.4604 117.287 93.3862C115.415 93.3121 113.588 93.9649 112.188 95.2078C110.787 96.4508 109.922 98.1875 109.773 100.054C109.759 100.243 109.751 100.435 109.751 100.628V103.048L105.819 101.405V89.4469H117C119.965 89.4469 122.809 90.6249 124.906 92.7217C127.003 94.8185 128.181 97.6624 128.181 100.628Z"
fill="white"
/>
</G>
</G>
</Svg>
{/* ── Floating corner icons rendered as absolute Animated.Views over the SVG ── */}
{/* Top-left — share */}
<Animated.View
style={[{ position: "absolute", top: 0, left: -10 }, styleTL]}
>
<Svg width={47} height={47} viewBox="0 0 47 47" fill="none">
<Rect
x={0.25}
y={0.25}
width={46.5}
height={46.5}
rx={7.75}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<Rect
width={22}
height={22}
x={13}
y={12}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
/>
<Path
d="M28.125 25.75C27.6663 25.7499 27.2124 25.8417 26.7899 26.0202C26.3674 26.1987 25.9851 26.4601 25.6654 26.789L21.7037 24.2427C22.0153 23.4435 22.0153 22.5565 21.7037 21.7573L25.6654 19.211C26.2605 19.8206 27.0607 20.1875 27.911 20.2406C28.7612 20.2937 29.6008 20.0292 30.2671 19.4984C30.9334 18.9675 31.3788 18.2082 31.5171 17.3677C31.6554 16.5271 31.4766 15.6651 31.0154 14.9488C30.5542 14.2326 29.8435 13.7131 29.0211 13.4911C28.1987 13.269 27.3231 13.3603 26.5641 13.7472C25.8051 14.134 25.2169 14.7889 24.9133 15.5849C24.6098 16.3808 24.6126 17.2612 24.9212 18.0552L20.9595 20.6015C20.4824 20.1117 19.8701 19.7753 19.2009 19.6353C18.5317 19.4953 17.8359 19.5581 17.2025 19.8156C16.5691 20.0731 16.0269 20.5136 15.6452 21.0808C15.2635 21.6481 15.0596 22.3163 15.0596 23C15.0596 23.6837 15.2635 24.3519 15.6452 24.9192C16.0269 25.4864 16.5691 25.9269 17.2025 26.1844C17.8359 26.4419 18.5317 26.5047 19.2009 26.3647C19.8701 26.2247 20.4824 25.8883 20.9595 25.3985L24.9212 27.9448C24.6558 28.6294 24.6165 29.3809 24.8091 30.0893C25.0018 30.7978 25.4161 31.4259 25.9916 31.8819C26.567 32.3378 27.2733 32.5975 28.007 32.6229C28.7408 32.6484 29.4634 32.4383 30.069 32.0233C30.6747 31.6084 31.1316 31.0105 31.3729 30.3171C31.6141 29.6237 31.627 28.8712 31.4097 28.17C31.1924 27.4687 30.7562 26.8554 30.1652 26.42C29.5741 25.9845 28.8591 25.7497 28.125 25.75ZM28.125 14.75C28.5329 14.75 28.9317 14.871 29.2708 15.0976C29.61 15.3242 29.8744 15.6463 30.0305 16.0232C30.1866 16.4001 30.2274 16.8148 30.1478 17.2149C30.0683 17.615 29.8718 17.9825 29.5834 18.2709C29.2949 18.5594 28.9274 18.7558 28.5273 18.8354C28.1273 18.915 27.7126 18.8741 27.3357 18.718C26.9588 18.5619 26.6367 18.2975 26.4101 17.9584C26.1834 17.6192 26.0625 17.2204 26.0625 16.8125C26.0625 16.2655 26.2798 15.7409 26.6666 15.3541C27.0533 14.9673 27.578 14.75 28.125 14.75ZM18.5 25.0625C18.092 25.0625 17.6933 24.9415 17.3541 24.7149C17.0149 24.4883 16.7506 24.1662 16.5945 23.7893C16.4384 23.4124 16.3975 22.9977 16.4771 22.5976C16.5567 22.1975 16.7531 21.83 17.0416 21.5416C17.33 21.2532 17.6975 21.0567 18.0976 20.9771C18.4977 20.8976 18.9124 20.9384 19.2892 21.0945C19.6661 21.2506 19.9882 21.515 20.2149 21.8541C20.4415 22.1933 20.5625 22.5921 20.5625 23C20.5625 23.547 20.3452 24.0716 19.9584 24.4584C19.5716 24.8452 19.047 25.0625 18.5 25.0625ZM28.125 31.25C27.717 31.25 27.3183 31.129 26.9791 30.9024C26.6399 30.6758 26.3756 30.3537 26.2195 29.9768C26.0634 29.5999 26.0225 29.1852 26.1021 28.7851C26.1817 28.385 26.3781 28.0175 26.6666 27.7291C26.955 27.4407 27.3225 27.2442 27.7226 27.1646C28.1227 27.0851 28.5374 27.1259 28.9142 27.282C29.2911 27.4381 29.6132 27.7025 29.8399 28.0416C30.0665 28.3808 30.1875 28.7796 30.1875 29.1875C30.1875 29.7345 29.9702 30.2591 29.5834 30.6459C29.1966 31.0327 28.672 31.25 28.125 31.25Z"
fill="#008836"
/>
</Svg>
</Animated.View>
{/* Top-right — lock */}
<Animated.View
style={[{ position: "absolute", top: 28, right: -5 }, styleTR]}
>
<Svg width={32} height={32} viewBox="0 0 32 32" fill="none">
<Rect
x={0.25}
y={0.25}
width={31.5}
height={31.5}
rx={7.75}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<Path
d="M16 14.125C15.475 14.1252 14.967 14.3144 14.57 14.658C14.172 15.0017 13.912 15.4769 13.836 15.9967C13.76 16.5165 13.874 17.0462 14.157 17.4891C14.439 17.9319 14.872 18.2583 15.375 18.4086V19.75C15.375 19.9158 15.441 20.0747 15.558 20.1919C15.675 20.3092 15.834 20.375 16 20.375C16.166 20.375 16.325 20.3092 16.442 20.1919C16.559 20.0747 16.625 19.9158 16.625 19.75V18.4086C17.128 18.2583 17.561 17.9319 17.843 17.4891C18.126 17.0462 18.24 16.5165 18.164 15.9967C18.088 15.4769 17.828 15.0017 17.43 14.658C17.033 14.3144 16.525 14.1252 16 14.125ZM16 17.25C15.815 17.25 15.633 17.195 15.479 17.092C15.325 16.989 15.205 16.8426 15.134 16.6713C15.063 16.5 15.044 16.3115 15.081 16.1296C15.117 15.9477 15.206 15.7807 15.337 15.6496C15.468 15.5185 15.635 15.4292 15.817 15.393C15.999 15.3568 16.187 15.3754 16.359 15.4464C16.53 15.5173 16.676 15.6375 16.78 15.7917C16.883 15.9458 16.937 16.1271 16.937 16.3125C16.937 16.5611 16.839 16.7996 16.663 16.9754C16.487 17.1512 16.249 17.25 16 17.25ZM22.25 11.625H19.75V9.75C19.75 8.7554 19.355 7.8016 18.652 7.0983C17.948 6.3951 16.995 6 16 6C15.005 6 14.052 6.3951 13.348 7.0983C12.645 7.8016 12.25 8.7554 12.25 9.75V11.625H9.75C9.418 11.625 9.101 11.7567 8.866 11.9911C8.632 12.2255 8.5 12.5435 8.5 12.875V21.625C8.5 21.9565 8.632 22.2745 8.866 22.5089C9.101 22.7433 9.418 22.875 9.75 22.875H22.25C22.582 22.875 22.899 22.7433 23.134 22.5089C23.368 22.2745 23.5 21.9565 23.5 21.625V12.875C23.5 12.5435 23.368 12.2255 23.134 11.9911C22.899 11.7567 22.582 11.625 22.25 11.625ZM13.5 9.75C13.5 9.087 13.763 8.4511 14.232 7.9822C14.701 7.5134 15.337 7.25 16 7.25C16.663 7.25 17.299 7.5134 17.768 7.9822C18.237 8.4511 18.5 9.087 18.5 9.75V11.625H13.5V9.75ZM22.25 21.625H9.75V12.875H22.25V21.625Z"
fill="#008836"
/>
</Svg>
</Animated.View>
{/* Bottom-left — document */}
<Animated.View
style={[{ position: "absolute", bottom: 0, left: 25 }, styleBL]}
>
<Svg width={35} height={35} viewBox="0 0 35 35" fill="none">
<Rect
x={0.25}
y={0.25}
width={34.5}
height={34.5}
rx={7.75}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<Rect
width={18.3333}
height={18.3333}
x={8.3334}
y={8.333}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
/>
<Path
d="M23.6346 14.178L19.6242 10.168C19.5709 10.114 19.5077 10.072 19.4382 10.044C19.3686 10.015 19.2941 10 19.2188 10H12.3438C12.0399 10 11.7485 10.121 11.5336 10.336C11.3187 10.551 11.198 10.842 11.198 11.146V23.75C11.198 24.054 11.3187 24.345 11.5336 24.56C11.7485 24.775 12.0399 24.896 12.3438 24.896H22.6563C22.9602 24.896 23.2517 24.775 23.4666 24.56C23.6814 24.345 23.8022 24.054 23.8022 23.75V14.583C23.8022 14.508 23.7875 14.434 23.7587 14.364C23.7299 14.295 23.6878 14.231 23.6346 14.178ZM19.7917 11.956L21.8464 14.01H19.7917V11.956ZM22.6563 23.75H12.3438V11.146H18.6459V14.583C18.6459 14.735 18.7063 14.881 18.8137 14.989C18.9212 15.096 19.0669 15.156 19.2188 15.156H22.6563V23.75ZM20.3647 18.021C20.3647 18.173 20.3043 18.319 20.1969 18.426C20.0894 18.533 19.9437 18.594 19.7917 18.594H15.2084C15.0565 18.594 14.9107 18.533 14.8033 18.426C14.6959 18.319 14.6355 18.173 14.6355 18.021C14.6355 17.869 14.6959 17.723 14.8033 17.616C14.9107 17.508 15.0565 17.448 15.2084 17.448H19.7917C19.9437 17.448 20.0894 17.508 20.1969 17.616C20.3043 17.723 20.3647 17.869 20.3647 18.021ZM20.3647 20.313C20.3647 20.465 20.3043 20.61 20.1969 20.718C20.0894 20.825 19.9437 20.885 19.7917 20.885H15.2084C15.0565 20.885 14.9107 20.825 14.8033 20.718C14.6959 20.61 14.6355 20.465 14.6355 20.313C14.6355 20.161 14.6959 20.015 14.8033 19.907C14.9107 19.8 15.0565 19.74 15.2084 19.74H19.7917C19.9437 19.74 20.0894 19.8 20.1969 19.907C20.3043 20.015 20.3647 20.161 20.3647 20.313Z"
fill="#008836"
/>
</Svg>
</Animated.View>
{/* Bottom-right — eye-off */}
<Animated.View
style={[{ position: "absolute", bottom: 0, right: 5 }, styleBR]}
>
<Svg width={43} height={43} viewBox="0 0 43 43" fill="none">
<Rect
x={0.25}
y={0.25}
width={42.5}
height={42.5}
rx={7.75}
fill={isDark ? "#1A1D1B" : "#f7f7f7"}
stroke={isDark ? "#233C2D" : "#D3E8DB"}
strokeWidth={0.5}
/>
<G>
<Path
d="M15.028 3.408C14.969 3.341 14.898 3.287 14.818 3.248C14.738 3.21 14.651 3.187 14.562 3.182C14.473 3.178 14.384 3.191 14.301 3.22C14.217 3.25 14.14 3.296 14.074 3.356C14.008 3.416 13.955 3.488 13.917 3.569C13.88 3.649 13.858 3.737 13.855 3.825C13.851 3.914 13.865 4.003 13.896 4.086C13.926 4.17 13.973 4.246 14.034 4.311L15.65 6.089C12.6 7.961 11.288 10.847 11.23 10.978C11.192 11.064 11.172 11.157 11.172 11.251C11.172 11.345 11.192 11.438 11.23 11.524C11.259 11.59 11.971 13.167 13.552 14.749C15.659 16.855 18.321 17.969 21.25 17.969C22.756 17.977 24.246 17.667 25.623 17.059L27.471 19.092C27.53 19.159 27.601 19.213 27.681 19.252C27.761 19.29 27.848 19.313 27.937 19.317C28.026 19.322 28.115 19.309 28.199 19.28C28.282 19.25 28.359 19.204 28.425 19.144C28.491 19.084 28.544 19.012 28.582 18.931C28.62 18.851 28.641 18.763 28.645 18.675C28.648 18.586 28.634 18.497 28.604 18.414C28.573 18.33 28.526 18.254 28.465 18.189L15.028 3.408ZM19.003 9.777L22.503 13.628C21.976 13.905 21.372 13.999 20.785 13.896C20.199 13.793 19.663 13.497 19.263 13.057C18.862 12.616 18.62 12.055 18.573 11.461C18.526 10.868 18.677 10.275 19.003 9.777ZM21.25 16.625C18.665 16.625 16.407 15.685 14.537 13.833C13.77 13.07 13.117 12.2 12.6 11.25C12.993 10.512 14.251 8.446 16.576 7.103L18.088 8.762C17.503 9.511 17.201 10.443 17.237 11.394C17.273 12.344 17.643 13.251 18.283 13.955C18.923 14.658 19.791 15.113 20.733 15.238C21.676 15.364 22.633 15.152 23.434 14.64L24.672 16.001C23.58 16.42 22.419 16.631 21.25 16.625ZM21.754 8.61C21.579 8.577 21.424 8.475 21.324 8.328C21.224 8.181 21.186 7.999 21.22 7.824C21.253 7.649 21.355 7.495 21.502 7.394C21.65 7.294 21.831 7.257 22.006 7.29C22.862 7.456 23.642 7.895 24.229 8.54C24.815 9.185 25.178 10.003 25.262 10.871C25.279 11.049 25.224 11.225 25.11 11.363C24.997 11.5 24.833 11.586 24.656 11.603C24.635 11.604 24.614 11.604 24.593 11.603C24.425 11.603 24.263 11.541 24.138 11.428C24.014 11.316 23.936 11.16 23.921 10.993C23.864 10.416 23.623 9.872 23.232 9.442C22.842 9.013 22.323 8.721 21.754 8.61ZM31.268 11.524C31.232 11.603 30.382 13.486 28.466 15.202C28.4 15.263 28.324 15.31 28.24 15.34C28.156 15.371 28.067 15.384 27.978 15.38C27.889 15.376 27.802 15.354 27.721 15.316C27.641 15.277 27.569 15.223 27.509 15.157C27.45 15.091 27.404 15.013 27.375 14.929C27.346 14.845 27.334 14.755 27.339 14.666C27.345 14.577 27.368 14.491 27.408 14.411C27.447 14.331 27.502 14.26 27.57 14.201C28.51 13.357 29.299 12.359 29.905 11.25C29.386 10.299 28.732 9.429 27.963 8.666C26.093 6.815 23.835 5.875 21.25 5.875C20.705 5.874 20.162 5.918 19.624 6.007C19.537 6.022 19.447 6.02 19.36 6.001C19.274 5.982 19.192 5.945 19.119 5.894C19.047 5.843 18.985 5.778 18.938 5.703C18.891 5.627 18.859 5.544 18.844 5.456C18.83 5.369 18.833 5.279 18.853 5.193C18.873 5.106 18.91 5.025 18.962 4.953C19.014 4.88 19.079 4.819 19.155 4.773C19.231 4.726 19.315 4.695 19.402 4.682C20.013 4.581 20.631 4.53 21.25 4.531C24.179 4.531 26.841 5.645 28.948 7.752C30.529 9.333 31.241 10.912 31.27 10.978C31.308 11.064 31.328 11.157 31.328 11.251C31.328 11.345 31.308 11.438 31.27 11.524H31.268Z"
fill="#008836"
transform="translate(0, 10)"
/>
</G>
</Svg>
</Animated.View>
</Animated.View>
</GestureDetector>
);
};

View File

@@ -20,99 +20,93 @@ 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 { Linking, useWindowDimensions, View } from "react-native";
import { SwiperFlatList } from "react-native-swiper-flatlist";
import { Platform, useWindowDimensions, View } from "react-native";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";
import { Spacing } from "../../common/design/spacing";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useRotator from "../../hooks/use-rotator";
import Navigation from "../../services/navigation";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import SettingsService from "../../services/settings";
import { AuthMode } from "../auth/common";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import SettingsService from "../../services/settings";
import { SafeAreaView } from "react-native-safe-area-context";
import { IntroIllustration } from "./illustration";
import { ProgressPills } from "./progress-pills";
const Intro = () => {
const { colors } = useThemeColors();
const { width } = useWindowDimensions();
const insets = useGlobalSafeAreaInsets();
const isTablet = width > 600;
const rotator = useRotator([0], 10000, true);
const insets = useGlobalSafeAreaInsets();
const renderItem = React.useCallback(
({ item }: { item: (typeof strings.introData)[0] }) => (
<View
<Animated.View
entering={FadeIn}
exiting={FadeOut}
style={{
justifyContent: "center",
width: isTablet ? width / 2 : width,
paddingHorizontal: isTablet ? (width / 2) * 0.05 : width * 0.05
justifyContent: "flex-start"
}}
>
<View
style={{
flexDirection: "row"
gap: Spacing.LEVEL_2
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
{item.headings?.map((heading, index) =>
heading.bold ? (
<Heading
key={heading.value()}
fontFamily="SEMI_BOLD"
fontSize="XXL"
lineHeight="120%"
>
{heading.value()}
</Heading>
) : (
<Paragraph
key={heading.value()}
style={{
marginTop: index !== 0 ? -5 : undefined
}}
fontFamily="MEDIUM"
fontSize="XL"
>
{heading.value()}
</Paragraph>
)
)}
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<View
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
maxWidth: "90%",
width: "100%"
}}
>
{item.headings?.map((heading) => (
<Heading
key={heading()}
style={{
marginBottom: 5
}}
extraBold
size={AppFontSize.xxl}
>
{heading()}
</Heading>
))}
{item.user ? (
<Paragraph fontFamily="MEDIUM" fontSize="XL">
{item.user}
</Paragraph>
) : null}
{item.body ? (
<Paragraph size={AppFontSize.sm}>{item.body()}</Paragraph>
<Paragraph color={colors.primary.paragraph} fontSize="SM">
{item.body()}
</Paragraph>
) : null}
{item.tesimonial ? (
<Paragraph
style={{
fontStyle: "italic",
fontSize: AppFontSize.lg
}}
fontSize="SM"
color={colors.secondary.paragraph}
onPress={() => {
Linking.openURL(item.link);
// Linking.openURL(item.link);
}}
>
{item.tesimonial()} {item.user}
{item.tesimonial()}
</Paragraph>
) : null}
</View>
</View>
</Animated.View>
),
[colors.primary.accent, colors.secondary.background, isTablet, width]
[colors.primary.paragraph, colors.secondary.paragraph]
);
return (
@@ -126,17 +120,25 @@ const Intro = () => {
<View
testID="notesnook.splashscreen"
style={{
flex: 1
flex: 1,
paddingTop: Platform.OS === "android" ? 60 - insets.top : undefined
}}
>
<View
style={{
flexDirection: "row",
paddingHorizontal: Spacing.LEVEL_3
}}
>
<ProgressPills activePillIndex={0} />
</View>
<View
style={[
{
width: "100%",
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
flexGrow: 1
},
isTablet && {
@@ -149,33 +151,34 @@ const Intro = () => {
}
]}
>
<SwiperFlatList
autoplay
autoplayDelay={10}
autoplayLoop={true}
index={0}
useReactNativeGestureHandler={true}
showPagination
data={strings.introData}
paginationActiveColor={colors.primary.accent}
paginationStyleItem={{
width: 10,
height: 5,
marginRight: 4,
marginLeft: 4
<View
style={{
alignItems: "center",
alignSelf: "center",
paddingVertical: Spacing.LEVEL_7
}}
paginationDefaultColor={colors.primary.border}
renderItem={renderItem}
/>
>
<IntroIllustration />
</View>
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
{strings.introData.map((item, index) =>
index !== rotator ? null : renderItem({ item })
)}
</View>
</View>
</View>
<View
style={{
width: isTablet ? "50%" : "100%",
justifyContent: "center",
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: isTablet ? 0 : DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
gap: Spacing.LEVEL_2,
padding: Spacing.LEVEL_3,
flexShrink: 1,
alignSelf: "center"
}}
@@ -191,7 +194,7 @@ const Intro = () => {
});
}}
type="accent"
title={strings.getStarted()}
title={strings.continue()}
/>
<Button

View File

@@ -0,0 +1,53 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { Spacing } from "../../common/design/spacing";
export const ProgressPills = (props: {
activePillIndex: number;
count?: number;
}) => {
const { colors } = useThemeColors();
return (
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
{new Array(props.count || 3).fill(0).map((item, index) => (
<View
key={`${item}-progress-pill-${props.count}`}
style={{
width: props.activePillIndex === index ? 26 : 14,
height: 5,
backgroundColor:
props.activePillIndex === index
? colors.primary.accent
: colors.secondary.background,
borderRadius: 2
}}
/>
))}
</View>
);
};

View File

@@ -22,22 +22,22 @@ import {
GroupingByIdKey,
GroupingKey,
GroupOptions,
ItemType
ItemType,
Item,
VirtualizedGrouping
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import React, { RefObject } from "react";
import { FlatList, View } from "react-native";
import { Radius, Spacing } from "../../../common/design/spacing";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { RouteName } from "../../../stores/use-navigation-store";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import Sort from "../../sheets/sort";
import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
type SectionHeaderProps = {
item: GroupHeader;
@@ -49,8 +49,9 @@ type SectionHeaderProps = {
group: GroupingKey;
groupId?: string;
type?: GroupingByIdKey;
onOpenJumpToDialog: () => void;
itemCount?: number;
data?: VirtualizedGrouping<Item>;
ref?: RefObject<FlatList>;
};
export const SectionHeader = React.memo<
@@ -64,12 +65,13 @@ export const SectionHeader = React.memo<
screen,
groupOptions,
group,
onOpenJumpToDialog,
itemCount,
groupId,
type
type,
data,
ref
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const { colors } = useThemeColors("list");
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook" | "searchResult"
);
@@ -78,8 +80,9 @@ export const SectionHeader = React.memo<
<View
style={{
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP,
marginBottom: DefaultAppStyles.GAP_VERTICAL
paddingHorizontal: Spacing.LEVEL_3,
marginBottom: Spacing.LEVEL_1,
marginTop: index === 0 ? 0 : Spacing.LEVEL_3
}}
>
<View
@@ -88,58 +91,37 @@ export const SectionHeader = React.memo<
alignItems: "center",
width: "100%",
alignSelf: "center",
justifyContent: "space-between",
borderBottomWidth: 1,
borderColor: colors.primary.border,
paddingBottom: 1,
paddingTop:
index === 0
? DefaultAppStyles.GAP_VERTICAL
: DefaultAppStyles.GAP_VERTICAL_SMALL
justifyContent: "space-between"
}}
>
<Pressable
onPress={() => {
onOpenJumpToDialog();
}}
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
<Paragraph
fontSize="SM"
fontFamily="MEDIUM"
style={{
justifyContent: "flex-start",
flexDirection: "row",
width: "auto"
alignSelf: "center",
textAlignVertical: "center"
}}
color={colors.primary.accent}
>
<Heading
size={AppFontSize.xxs}
style={{
alignSelf: "center",
textAlignVertical: "center"
}}
color={color || colors.primary.accent}
>
{!item.title || item.title === ""
? screen === "Search"
? strings.results(itemCount || 0)
: strings.pinned().toUpperCase()
: item.title.toUpperCase()}
</Heading>
</Pressable>
{!item.title || item.title === ""
? screen === "Search"
? strings.results(itemCount || 0)
: strings.pinned()
: item.title}
</Paragraph>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: DefaultAppStyles.GAP_SMALL
gap: Spacing.LEVEL_1
}}
>
{index === 0 ? (
<>
<IconButton
name={
groupOptions.sortDirection === "asc"
? "sort-ascending"
: "sort-descending"
}
name={"sliders"}
iconFamily="notesnook"
color={colors.secondary.icon}
testID="icon-sort"
onPress={() => {
@@ -155,15 +137,20 @@ export const SectionHeader = React.memo<
hideGroupOptions={
screen === "Reminders" || screen === "Search"
}
data={data}
ref={ref}
/>
)
});
}}
style={{
width: 25,
height: 25
width: 30,
height: 30,
borderWidth: 1,
borderRadius: Radius.XS,
borderColor: colors.secondary.border
}}
size={AppFontSize.lg - 2}
size={16}
/>
<IconButton
hidden={
@@ -173,14 +160,16 @@ export const SectionHeader = React.memo<
screen !== "Search"
}
style={{
width: 25,
height: 25
width: 30,
height: 30,
borderWidth: 1,
borderRadius: Radius.XXS,
borderColor: colors.secondary.border
}}
testID="icon-compact-mode"
color={colors.secondary.icon}
name={
isCompactModeEnabled ? "view-list" : "view-list-outline"
}
name={"view-list"}
iconFamily="notesnook"
onPress={() => {
SettingsService.set({
[dataType === "notebook"
@@ -192,20 +181,11 @@ export const SectionHeader = React.memo<
: "normal"
});
}}
size={AppFontSize.lg - 2}
type={isCompactModeEnabled ? "shade" : undefined}
size={16}
/>
</>
) : null}
{/* <IconButton
style={{
width: 25,
height: 25
}}
color={colors.secondary.icon}
name={"chevron-down"}
size={SIZE.lg - 2}
/> */}
</View>
</View>
</View>

View File

@@ -25,10 +25,9 @@ import {
TrashItem
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { EntityLevel, decode } from "entities";
import { decode, EntityLevel } from "entities";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import useNavigationStore, {
RouteParams
@@ -42,20 +41,23 @@ import {
TagsWithDateEdited
} from "@notesnook/common";
import { strings } from "@notesnook/intl";
import dayjs from "dayjs";
import { create } from "zustand";
import { notesnook } from "../../../../e2e/test.ids";
import { FontFamily } from "../../../common/design/font";
import { Radius, Spacing } from "../../../common/design/spacing";
import useIsSelected from "../../../hooks/use-selected";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { DefaultAppStyles } from "../../../utils/styles";
import { Properties } from "../../properties";
import AppIcon from "../../ui/AppIcon";
import { ExpiryDate } from "../../ui/expiry-date";
import { IconButton } from "../../ui/icon-button";
import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import dayjs from "dayjs";
import { ExpiryDate } from "../../ui/expiry-date";
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
@@ -72,6 +74,24 @@ type NoteItemProps = {
renderedInRoute?: keyof RouteParams;
};
const useShowMoreStore = create<{
showMoreStatus: Record<string, boolean>;
show: (id: string) => void;
hide: (id: string) => void;
}>((set) => ({
showMoreStatus: {},
show(id) {
set((state) => ({
showMoreStatus: { ...state.showMoreStatus, [id]: true }
}));
},
hide(id) {
set((state) => ({
showMoreStatus: { ...state.showMoreStatus, [id]: false }
}));
}
}));
const NoteItem = ({
item,
isTrash,
@@ -98,53 +118,333 @@ const NoteItem = ({
const primaryColors = isEditingNote ? colors.selected : colors.primary;
const selectionMode = useSelectionStore((state) => state.selectionMode);
const [selected] = useIsSelected(item);
const showMore = useShowMoreStore((state) => state.showMoreStatus[item.id]);
const statusIcons = [
{
condition: item.conflicted,
name: "alert-circle",
color: colors.error.accent
},
{
condition: item.localOnly,
testID: "sync-off",
name: "sync-off",
color: primaryColors.icon
},
{
condition: item.readonly,
testID: "pencil-lock",
name: "pencil-simple-slash",
color: primaryColors.icon
},
{
condition: item.pinned,
testID: "icon-pinned",
name: "pin",
color: primaryColors.icon
},
{
condition: !!locked,
testID: "lock",
name: "lock",
color: primaryColors.icon
},
{
condition: item.favorite,
testID: "star-filled",
name: "star-filled",
color: "orange"
}
];
const activeStatusIcons = statusIcons.filter(
(statusIcon) => statusIcon.condition
);
const canShowTopStatusBar =
attachmentsCount > 0 ||
activeStatusIcons.length > 0 ||
notebooks?.items?.length ||
tags?.items.length;
return (
<>
<View
style={{
flexGrow: 1,
flexShrink: 1
flexShrink: 1,
gap: compactMode ? undefined : Spacing.LEVEL_1
}}
>
{compactMode ? null : (
<Paragraph
{compactMode || !canShowTopStatusBar || isTrash ? null : (
<View
style={{
fontSize: AppFontSize.xxxs,
color: colors.secondary.paragraph
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
width: "100%",
columnGap: Spacing.LEVEL_0,
rowGap: Spacing.LEVEL_0,
flexWrap: "wrap"
}}
>
{getFormattedDate(
date,
dayjs(date).isBefore(dayjs().subtract(1, "day").hour(23))
? "date"
: "time"
)}
</Paragraph>
{activeStatusIcons.map((statusIcon) => (
<View
key={statusIcon.testID || statusIcon.name}
style={{
borderRadius: Radius.XXS,
paddingHorizontal: 3,
paddingVertical: 3,
// borderWidth: 1,
// borderColor: primaryColors.border,
flexDirection: "row",
alignItems: "center"
}}
>
<AppIcon
testID={statusIcon.testID}
name={statusIcon.name}
size={12}
iconFamily="notesnook"
color={statusIcon.color}
/>
</View>
))}
{attachmentsCount !== 0 ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
borderRadius: Radius.XXS,
paddingHorizontal: 3,
paddingVertical: 3,
gap: Spacing.LEVEL_0
}}
>
<AppIcon
name="link"
iconFamily="notesnook"
size={12}
color={colors.secondary.icon}
/>
<Paragraph color={colors.secondary.paragraph} fontSize="XS">
{attachmentsCount}
</Paragraph>
</View>
) : null}
{notebooks?.items
?.filter(
(item) =>
renderedInRoute !== "Notebook" ||
item.id !== useNavigationStore.getState().focusedRouteId
)
.filter((_, index) => showMore || index < 1)
.map((item) => (
<View
key={item.id}
style={{
borderRadius: 100,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: 2,
backgroundColor: isEditingNote
? colors.primary.background
: colors.secondary.background,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0
}}
>
<AppIcon
name="bookmark"
iconFamily="notesnook"
size={AppFontSize.xs}
color={colors.secondary.icon}
/>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{item.title}
</Paragraph>
</View>
))}
{tags?.items
?.filter((_, index) => showMore || index < 1)
.map((item) =>
item.id ? (
<View
key={item.id}
style={{
borderRadius: 100,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: 2,
backgroundColor: isEditingNote
? colors.primary.background
: colors.secondary.background,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{item.title}
</Paragraph>
</View>
) : null
)}
{(() => {
const filteredNotebooks = (notebooks?.items || []).filter(
(nb) =>
renderedInRoute !== "Notebook" ||
nb.id !== useNavigationStore.getState().focusedRouteId
);
const filteredTags = (tags?.items || []).filter((t) => t.id);
const totalNotebooks = filteredNotebooks.length;
const totalTags = filteredTags.length;
const hasMore = totalNotebooks > 1 || totalTags > 1;
if (!hasMore) return null;
const hiddenCount =
(totalNotebooks > 1 ? totalNotebooks - 1 : 0) +
(totalTags > 1 ? totalTags - 1 : 0);
return (
<Heading
size={AppFontSize.xs}
color={colors.primary.accent}
onPress={() => {
if (showMore) {
useShowMoreStore.getState().hide(item.id);
} else {
useShowMoreStore.getState().show(item.id);
}
}}
>
{showMore ? "Show less" : `+${hiddenCount} ${strings.more()}`}
</Heading>
);
})()}
</View>
)}
{compactMode ? (
<Paragraph
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
size={AppFontSize.sm}
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
{color && !compactMode ? (
<View
style={{
width: 8,
height: 8,
borderRadius: 100,
backgroundColor: color?.colorCode
}}
/>
) : null}
<View
style={{
paddingRight: 10
flexDirection: "row",
justifyContent: "space-between",
flexGrow: 1,
alignItems: "center"
}}
>
{item.title}
</Paragraph>
) : (
<Heading
numberOfLines={1}
color={color?.colorCode || primaryColors.heading}
size={AppFontSize.sm}
style={{
paddingRight: 10
}}
>
{item.title}
</Heading>
)}
<Heading
numberOfLines={1}
color={primaryColors.heading}
fontSize="MD"
style={{
flexShrink: 1
}}
>
{item.title}
</Heading>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
{color && compactMode ? (
<View
style={{
width: 8,
height: 8,
borderRadius: 100,
backgroundColor: color?.colorCode
}}
/>
) : null}
{compactMode ? (
<>
{item.conflicted ? (
<AppIcon
name="warning-circle"
size={12}
iconFamily="notesnook"
color={colors.error.accent}
/>
) : null}
{locked ? (
<AppIcon
name="lock"
testID="lock"
iconFamily="notesnook"
size={12}
color={primaryColors.icon}
/>
) : null}
{item.favorite ? (
<AppIcon
testID="icon-star"
name="star-filled"
iconFamily="notesnook"
size={12}
color={colors.static.orange}
/>
) : null}
<TimeSince
style={{
fontSize: AppFontSize.xs,
color: colors.secondary.paragraph,
marginRight: 6
}}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
</>
) : null}
<IconButton
testID={notesnook.listitem.menu}
color={colors.secondary.icon}
name="dots-three"
iconFamily="notesnook"
size={20}
onPress={() => !noOpen && Properties.present(item)}
style={{
justifyContent: "center",
height: undefined,
width: undefined,
borderRadius: 100,
alignItems: "center"
}}
/>
</View>
</View>
</View>
{item.headline && !compactMode ? (
<Paragraph
@@ -152,6 +452,7 @@ const NoteItem = ({
flexWrap: "wrap"
}}
color={primaryColors.paragraph}
fontSize="SM"
numberOfLines={2}
>
{decode(item.headline, {
@@ -160,218 +461,96 @@ const NoteItem = ({
</Paragraph>
) : null}
{compactMode ? null : (
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
width: "100%",
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
columnGap: 8,
rowGap: 4,
flexWrap: "wrap"
}}
>
{!isTrash ? (
<>
{item.conflicted ? (
<Icon
name="alert-circle"
size={AppFontSize.sm}
color={colors.error.accent}
/>
) : null}
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
{isTrash ? (
<Paragraph
color={colors.secondary.paragraph}
fontSize="XS"
style={{
marginRight: 6
}}
>
{strings.note()}
</Paragraph>
) : null}
{item.localOnly ? (
<Icon
testID="sync-off"
name="sync-off"
size={AppFontSize.sm}
color={primaryColors.icon}
/>
) : null}
{item.readonly ? (
<Icon
testID="pencil-lock"
name="pencil-lock"
size={AppFontSize.sm}
color={primaryColors.icon}
/>
) : null}
{attachmentsCount > 0 ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 2
}}
>
<Icon
name="attachment"
size={AppFontSize.sm}
color={primaryColors.icon}
/>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xxs}
>
{attachmentsCount}
</Paragraph>
</View>
) : null}
{item.pinned ? (
<Icon
testID="icon-pinned"
name="pin-outline"
size={AppFontSize.sm}
color={color?.colorCode || primaryColors.accent}
/>
) : null}
{locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={AppFontSize.sm}
color={primaryColors.icon}
/>
) : null}
{item.favorite ? (
<Icon
testID="icon-star"
name="star-outline"
size={AppFontSize.sm}
color="orange"
/>
) : null}
{reminder ? (
<ReminderTime
reminder={reminder}
color={color?.colorCode}
textStyle={{
fontSize: AppFontSize.xxs
}}
short
iconSize={AppFontSize.xxs}
style={{
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start"
}}
/>
) : null}
{item.expiryDate?.value ? (
<ExpiryDate
note={item as Note}
color={color?.colorCode}
textStyle={{ fontSize: AppFontSize.xxs }}
short
iconSize={AppFontSize.xxs}
style={{
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start"
}}
/>
) : null}
{notebooks?.items
?.filter(
(item) =>
renderedInRoute !== "Notebook" ||
item.id !== useNavigationStore.getState().focusedRouteId
)
.map((item) => (
<View
key={item.id}
style={{
borderRadius: 4,
backgroundColor: colors.secondary.background,
paddingHorizontal: DefaultAppStyles.GAP_SMALL / 2,
borderWidth: 0.5,
borderColor: primaryColors.border,
paddingVertical: 1,
flexDirection: "row",
alignItems: "center",
gap: DefaultAppStyles.GAP_SMALL / 2
}}
>
<AppIcon
name="book-outline"
size={AppFontSize.xxxs}
color={colors.secondary.icon}
/>
<Paragraph
size={AppFontSize.xxxs}
color={colors.secondary.paragraph}
>
{item.title}
</Paragraph>
</View>
))}
{!isTrash && !compactMode && tags
? tags.items?.map((item) =>
item.id ? (
<View
key={item.id}
style={{
borderRadius: 4,
backgroundColor: colors.secondary.background,
paddingHorizontal: DefaultAppStyles.GAP_SMALL / 2,
borderWidth: 0.5,
borderColor:
color?.colorCode || primaryColors.border,
paddingVertical: 1
}}
>
<Paragraph
size={AppFontSize.xxxs}
color={colors.secondary.paragraph}
>
#{item.title}
</Paragraph>
</View>
) : null
{compactMode ? null : (
<View
style={{
gap: Spacing.LEVEL_0,
flexDirection: "row"
}}
>
<AppIcon
color={colors.secondary.icon}
size={12}
style={{
marginTop: 1
}}
name={isTrash ? "trash-alt" : "calendar"}
iconFamily="notesnook"
/>
<Paragraph
style={{
color: colors.secondary.paragraph
}}
fontSize="XS"
>
{isTrash
? strings.deletedOn(
new Date((item as TrashItem).dateDeleted)
.toISOString()
.slice(0, 10)
)
: null}
</>
) : (
<>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xxs}
style={{
marginRight: 6
}}
>
{item && item.dateDeleted
? strings.deletedOn(
new Date(item.dateDeleted).toISOString().slice(0, 10)
)
: null}
</Paragraph>
: getFormattedDate(
date,
dayjs(date).isBefore(dayjs().subtract(1, "day").hour(23))
? "date"
: "time"
)}
</Paragraph>
</View>
)}
<Paragraph
color={primaryColors.accent}
size={AppFontSize.xxs}
style={{
marginRight: 6
}}
>
{strings.note()}
</Paragraph>
</>
)}
</View>
)}
{item.expiryDate?.value ? (
<ExpiryDate
note={item as Note}
color={color?.colorCode}
textStyle={{ fontSize: AppFontSize.xxs }}
short
iconSize={AppFontSize.xxs}
style={{
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start"
}}
/>
) : null}
{reminder ? (
<ReminderTime
reminder={reminder}
color={color?.colorCode}
textStyle={{
fontSize: AppFontSize.xs,
fontFamily: FontFamily.REGULAR
}}
short
iconSize={AppFontSize.xxs}
style={{
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start",
backgroundColor: colors.primary.shade
}}
/>
) : null}
</View>
</View>
<View
style={{
@@ -379,55 +558,6 @@ const NoteItem = ({
alignItems: "center"
}}
>
{compactMode ? (
<>
{item.conflicted ? (
<Icon
name="alert-circle"
style={{
marginRight: 6
}}
size={AppFontSize.sm}
color={colors.error.accent}
/>
) : null}
{locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={AppFontSize.sm}
style={{
marginRight: 6
}}
color={primaryColors.icon}
/>
) : null}
{item.favorite ? (
<Icon
testID="icon-star"
name="star-outline"
size={AppFontSize.sm}
style={{
marginRight: 6
}}
color={colors.static.orange}
/>
) : null}
<TimeSince
style={{
fontSize: AppFontSize.xxs,
color: colors.secondary.paragraph,
marginRight: 6
}}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
</>
) : null}
{selectionMode === "note" || selectionMode === "trash" ? (
<>
<View
@@ -445,22 +575,7 @@ const NoteItem = ({
/>
</View>
</>
) : (
<IconButton
testID={notesnook.listitem.menu}
color={colors.secondary.icon}
name="dots-horizontal"
size={AppFontSize.lg}
onPress={() => !noOpen && Properties.present(item)}
style={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
/>
)}
) : null}
</View>
</>
);

View File

@@ -23,20 +23,17 @@ import NoteItem from ".";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import {
eSendEvent,
hideSheet,
presentSheet
} from "../../../services/event-manager";
import { eSendEvent, hideSheet } from "../../../services/event-manager";
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 Navigation from "../../../services/navigation";
import { RouteParams } from "../../../stores/use-navigation-store";
import NotePreview from "../../note-history/preview";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import SelectionWrapper from "../selection-wrapper";
import { selectItem } from "../../../stores/use-selection-store";
export const openNote = async (
item: Note,
@@ -59,11 +56,7 @@ export const openNote = async (
if (isTrash) {
if (!note.contentId) return;
const content = await db.content.get(note.contentId as string);
presentSheet({
component: <NotePreview note={item} content={content} />
});
Navigation.navigate("NotePreview", { note: item });
} else {
if (!useTabStore.getState().hasTabForNote(note.id!)) {
editorController.current.commands.setLoading(
@@ -94,6 +87,7 @@ type NoteWrapperProps = {
isRenderedInActionSheet: boolean;
locked?: boolean;
renderedInRoute?: keyof RouteParams;
hasGroupHeader?: boolean;
};
export const NoteWrapper = React.memo<
@@ -103,6 +97,7 @@ export const NoteWrapper = React.memo<
item,
index,
isRenderedInActionSheet,
hasGroupHeader,
...restProps
}: NoteWrapperProps) {
const isTrash = item.type === "trash";
@@ -113,6 +108,7 @@ export const NoteWrapper = React.memo<
onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)}
isSheet={isRenderedInActionSheet}
item={item}
hasGroupHeader={hasGroupHeader}
index={index}
color={restProps.color?.colorCode}
>

View File

@@ -27,7 +27,8 @@ import Navigation from "../../../services/navigation";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useTrashStore } from "../../../stores/use-trash-store";
import { presentDialog } from "../../dialog/functions";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import SelectionWrapper from "../selection-wrapper";
import { selectItem } from "../../../stores/use-selection-store";
import { strings } from "@notesnook/intl";
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {

View File

@@ -16,27 +16,28 @@ 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 { getFormattedReminderTime } from "@notesnook/common";
import { Reminder } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import React, { useState } from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { Radius, Spacing } from "../../../common/design/spacing";
import useIsSelected from "../../../hooks/use-selected";
import AddReminder from "../../../screens/add-reminder";
import { eSendEvent } from "../../../services/event-manager";
import { useSelectionStore } from "../../../stores/use-selection-store";
import {
selectItem,
useSelectionStore
} from "../../../stores/use-selection-store";
import { eCloseSheet } from "../../../utils/events";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { Properties } from "../../properties";
import AppIcon from "../../ui/AppIcon";
import { IconButton } from "../../ui/icon-button";
import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import SelectionWrapper from "../selection-wrapper";
const ReminderItem = React.memo(
({
@@ -49,6 +50,7 @@ const ReminderItem = React.memo(
isSheet: boolean;
}) => {
const { colors } = useThemeColors();
const [checked, setChecked] = useState(false);
const openReminder = () => {
if (selectItem(item)) return;
AddReminder.present(item, undefined);
@@ -58,148 +60,129 @@ const ReminderItem = React.memo(
};
const selectionMode = useSelectionStore((state) => state.selectionMode);
const [selected] = useIsSelected(item);
// Subtitle segments (reminder time · recurring mode · disabled), rendered as
// plain text separated by a dot to match the redesign.
const subtitleSegments = [
getFormattedReminderTime(item, false),
item.mode === "repeat" && item.recurringMode
? strings.reminderRecurringMode[item.recurringMode]()
: undefined,
item.disabled ? strings.disabled() : undefined
].filter(Boolean) as string[];
return (
<SelectionWrapper onPress={openReminder} item={item} isSheet={isSheet}>
<SelectionWrapper
onPress={() => {
if (selectItem(item)) return;
setChecked(!checked);
}}
item={item}
isSheet={isSheet}
hideSeparator
style={{
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: Radius.S,
marginBottom: Spacing.LEVEL_2,
backgroundColor: selected ? colors.primary.shade : undefined
}}
wrapperStyle={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
{/* Leading "done" checkbox — visual only for now, not yet wired. */}
<AppIcon
name={checked ? "checkbox" : "box-empty"}
iconFamily="notesnook"
color={
checked
? [colors.primary.accent, colors.primary.accentForeground]
: colors.primary.icon
}
size={16}
style={{
marginRight: Spacing.LEVEL_1
}}
/>
<View
style={{
opacity: item.disabled ? 0.5 : 1,
maxWidth: "80%",
flexGrow: 1
flexGrow: 1,
flexShrink: 1,
gap: Spacing.LEVEL_1,
opacity: item.disabled ? 0.5 : 1
}}
>
<Heading
numberOfLines={1}
<View
style={{
flexWrap: "wrap"
flexDirection: "row",
justifyContent: "space-between"
}}
size={AppFontSize.md}
>
{item.title}
</Heading>
<Heading
fontSize="MD"
style={{
flexShrink: 1
}}
color={colors.primary.heading}
>
{item.title}
</Heading>
<IconButton
testID={notesnook.listitem.menu}
color={colors.secondary.icon}
name="dots-three"
iconFamily="notesnook"
size={20}
onPress={() => Properties.present(item, isSheet)}
style={{
justifyContent: "center",
height: undefined,
width: undefined,
borderRadius: 100,
alignItems: "center"
}}
/>
</View>
{item.description ? (
<Paragraph
style={{
flexWrap: "wrap"
}}
numberOfLines={2}
>
{item.description}
</Paragraph>
<Paragraph fontSize="SM">{item.description}</Paragraph>
) : null}
<View
style={{
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap",
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL
gap: Spacing.LEVEL_1
}}
>
{item.disabled ? (
<View
style={{
backgroundColor: colors.secondary.background,
borderRadius: defaultBorderRadius,
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
alignItems: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
justifyContent: "flex-start",
alignSelf: "flex-start",
marginRight: 10,
height: 30
}}
>
<Icon
name="bell-off-outline"
size={AppFontSize.md}
color={colors.error.icon}
/>
{subtitleSegments.map((segment, i) => (
<React.Fragment key={segment}>
{i > 0 ? (
<View
style={{
width: 4,
height: 4,
borderRadius: 100,
backgroundColor: colors.secondary.icon
}}
/>
) : null}
<Paragraph
size={AppFontSize.xs}
fontSize="SM"
lineHeight="100%"
color={colors.secondary.paragraph}
style={{ marginLeft: 5 }}
>
{strings.disabled()}
{segment}
</Paragraph>
</View>
) : null}
{item.mode === "repeat" && item.recurringMode ? (
<View
style={{
backgroundColor: colors.secondary.background,
borderRadius: defaultBorderRadius,
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
alignItems: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
justifyContent: "flex-start",
alignSelf: "flex-start",
marginRight: DefaultAppStyles.GAP_SMALL,
height: 30
}}
>
<Icon
name="reload"
size={AppFontSize.md}
color={colors.primary.accent}
/>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{ marginLeft: 5 }}
>
{strings.reminderRecurringMode[item.recurringMode]()}
</Paragraph>
</View>
) : null}
<ReminderTime
reminder={item}
checkIsActive={false}
fontSize={AppFontSize.xxs}
style={{
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start"
}}
/>
</React.Fragment>
))}
</View>
</View>
{selectionMode === "note" || selectionMode === "trash" ? (
<>
<View
style={{
height: 35,
width: 35,
alignItems: "center",
justifyContent: "center"
}}
>
<AppIcon
name={selected ? "checkbox-outline" : "checkbox-blank-outline"}
color={selected ? colors.selected.icon : colors.primary.icon}
size={AppFontSize.lg}
/>
</View>
</>
) : (
<IconButton
testID={notesnook.listitem.menu}
color={colors.primary.paragraph}
name="dots-horizontal"
size={AppFontSize.xl}
onPress={() => Properties.present(item, isSheet)}
style={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
/>
)}
</SelectionWrapper>
);
},

View File

@@ -71,6 +71,7 @@ export const SearchResult = (props: SearchResultProps) => {
}}
onLongPress={async () => {
const note = await db.notes.note(props.item.id);
if (!note) return;
Properties.present(note);
}}
onPress={async () => openNote()}
@@ -151,6 +152,7 @@ export const SearchResult = (props: SearchResultProps) => {
}}
onLongPress={async () => {
const note = await db.notes.note(props.item.id);
if (!note) return;
Properties.present(note);
}}
onPress={() => {

View File

@@ -23,26 +23,9 @@ import React, { PropsWithChildren, useRef } from "react";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { DefaultAppStyles } from "../../../utils/styles";
import { Pressable } from "../../ui/pressable";
import { View } from "react-native";
export function selectItem(item: Item) {
if (useSelectionStore.getState().selectionMode === item.type) {
const { selectionMode, clearSelection, setSelectedItem } =
useSelectionStore.getState();
if (selectionMode === item.type) {
setSelectedItem(item.id);
}
if (useSelectionStore.getState().selectedItemsList.length === 0) {
clearSelection();
}
return true;
}
return false;
}
import { View, ViewStyle } from "react-native";
import { Spacing } from "../../../common/design/spacing";
type SelectionWrapperProps = PropsWithChildren<{
item: Item;
@@ -51,6 +34,10 @@ type SelectionWrapperProps = PropsWithChildren<{
isSheet?: boolean;
color?: string;
index?: number;
hasGroupHeader?: boolean;
style?: ViewStyle;
wrapperStyle?: ViewStyle;
hideSeparator?: boolean;
}>;
const SelectionWrapper = ({
@@ -60,7 +47,11 @@ const SelectionWrapper = ({
isSheet,
children,
color,
index = 0
hasGroupHeader,
index = 0,
style,
wrapperStyle,
hideSeparator
}: SelectionWrapperProps) => {
const itemId = useRef(item.id);
const { colors, isDark } = useThemeColors();
@@ -86,47 +77,70 @@ const SelectionWrapper = ({
};
return (
<Pressable
customColor={
isEditingNote
? colors.selected.background
: isSheet
? colors.primary.hover
: "transparent"
}
testID={testID}
onLongPress={onLongPress}
onPress={onPress}
customSelectedColor={colors.primary.hover}
customAlpha={!isDark ? -0.02 : 0.02}
customOpacity={1}
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
alignSelf: "center",
overflow: "hidden",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: compactMode ? 4 : DefaultAppStyles.GAP_VERTICAL,
borderRadius: isSheet ? 10 : 0,
marginBottom: isSheet ? DefaultAppStyles.GAP_VERTICAL : undefined
}}
>
{isEditingNote ? (
<>
{hasGroupHeader || hideSeparator ? null : (
<View
style={{
backgroundColor: color || colors.selected.accent,
position: "absolute",
bottom: 0,
top: 0,
left: 0,
width: 5
paddingHorizontal: Spacing.LEVEL_3
}}
/>
) : null}
{children}
</Pressable>
>
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.primary.separator,
width: "100%"
}}
/>
</View>
)}
<View
style={{
backgroundColor: isEditingNote
? colors.secondary.background
: undefined,
...wrapperStyle
}}
>
<Pressable
customColor={isSheet ? colors.primary.hover : "transparent"}
testID={testID}
onLongPress={onLongPress}
onPress={onPress}
customSelectedColor={colors.primary.hover}
noborder
customAlpha={!isDark ? -0.02 : 0.02}
customOpacity={1}
style={{
paddingHorizontal: Spacing.LEVEL_3,
paddingBottom: Spacing.LEVEL_2,
paddingTop: hasGroupHeader ? Spacing.LEVEL_1 : Spacing.LEVEL_2,
...style
}}
>
{isEditingNote ? (
<View
style={{
backgroundColor: color || colors.selected.accent,
position: "absolute",
bottom: 0,
top: 0,
left: 0,
width: 4
}}
/>
) : null}
<View
style={{
width: "100%",
flexDirection: "row"
}}
>
{children}
</View>
</Pressable>
</View>
</>
);
};

View File

@@ -28,7 +28,8 @@ import { Properties } from "../../properties";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import SelectionWrapper from "../selection-wrapper";
import { selectItem } from "../../../stores/use-selection-store";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";

View File

@@ -21,10 +21,11 @@ import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Dimensions, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { Radius, Spacing } from "../../common/design/spacing";
import { Message, useMessageStore } from "../../stores/use-message-store";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import AppIcon from "../ui/AppIcon";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export const Card = ({
@@ -47,15 +48,18 @@ export const Card = ({
<View
style={{
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
paddingHorizontal: Spacing.LEVEL_3,
marginBottom: Spacing.LEVEL_4
}}
>
<Pressable
onPress={messageBoardState.onPress}
type="plain"
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: Spacing.LEVEL_3,
paddingHorizontal: Spacing.LEVEL_2,
backgroundColor: colors.primary.shade,
borderRadius: Radius.S,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
@@ -66,23 +70,24 @@ export const Card = ({
style={{
flexDirection: "row",
alignItems: "center",
width: "100%"
width: "100%",
flexShrink: 1,
gap: Spacing.LEVEL_1
}}
>
<View
style={{
width: 40 * fontScale,
height: 40 * fontScale,
borderRadius: 100,
width: 34,
height: 34,
borderRadius: Radius.XS,
alignItems: "center",
justifyContent: "center"
justifyContent: "center",
backgroundColor: colors.secondary.background
}}
>
<Icon
size={AppFontSize.xxxl}
color={
messageBoardState.type === "error" ? colors.error.icon : color
}
size={16}
color={colors.primary.icon}
allowFontScaling
name={messageBoardState.icon}
/>
@@ -90,25 +95,27 @@ export const Card = ({
<View
style={{
marginLeft: 10,
marginRight: 10
gap: Spacing.LEVEL_0,
flexShrink: 1
}}
>
<Paragraph
<Heading
style={{
flexWrap: "nowrap",
flexShrink: 1
}}
size={AppFontSize.sm}
fontSize="MD"
color={colors.primary.heading}
>
{messageBoardState.actionText}
</Paragraph>
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
</Heading>
<Paragraph color={colors.secondary.paragraph} fontSize="SM">
{messageBoardState.message}
</Paragraph>
</View>
</View>
<AppIcon size={24} name="chevron-right" color={colors.primary.icon} />
</Pressable>
</View>
);

View File

@@ -21,13 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ActivityIndicator, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import { TTip, useTip } from "../../services/tip-manager";
import { Spacing } from "../../common/design/spacing";
import { RouteParams } from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { AppFontSize } from "../../utils/size";
import { Tip } from "../tip";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -58,22 +55,13 @@ export const Empty = React.memo(function Empty({
screen
}: EmptyListProps) {
const { colors } = useThemeColors();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholder?.type || ((dataType + "s") as any),
screen === "Notes" ? "notes" : "list"
);
return (
<View
style={[
{
flex: 1,
width: "80%",
width: "100%",
justifyContent: "center",
alignSelf: "center"
}
@@ -81,50 +69,53 @@ export const Empty = React.memo(function Empty({
>
{!loading ? (
<>
<Tip
color={color}
tip={
screen !== "Search"
? tip || ({ text: () => placeholder?.paragraph } as TTip)
: ({ text: () => placeholder?.paragraph } as TTip)
}
<View
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
alignItems: "center",
gap: Spacing.LEVEL_1
}}
/>
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="secondaryAccented"
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholder?.action}
>
<Heading size={AppFontSize.md} color={colors.primary.heading}>
{placeholder?.title}
</Heading>
<Paragraph
style={{
alignSelf: "flex-start"
textAlign: "center",
marginBottom: Spacing.LEVEL_2,
maxWidth: "80%"
}}
/>
)}
fontSize="SM"
color={colors.primary.paragraph}
>
{placeholder?.paragraph}
</Paragraph>
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="accent"
title={placeholder?.button}
iconPosition="right"
onPress={placeholder?.action}
/>
)}
</View>
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "flex-start",
width: "100%"
alignItems: "center",
width: "100%",
gap: Spacing.LEVEL_2
}}
>
<Heading>{placeholder?.title}</Heading>
<Paragraph size={AppFontSize.sm} textBreakStrategy="balanced">
{placeholder?.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={AppFontSize.lg}
color={color || colors.primary.accent}
/>
<Paragraph>{placeholder?.loading}</Paragraph>
</View>
</>
)}

View File

@@ -44,6 +44,8 @@ import { Header } from "../list-items/headers/header";
import { Empty, PlaceholderData } from "./empty";
import { ListItemWrapper } from "./list-item.wrapper";
import { ScrollView } from "react-native-actions-sheet";
import { Spacing } from "../../common/design/spacing";
import LineSeparator from "../ui/seperator/line-separator";
type ListProps = {
data: VirtualizedGrouping<Item> | undefined;
@@ -61,6 +63,7 @@ type ListProps = {
groupType: GroupingKey;
id?: string;
type?: GroupingByIdKey;
hideHeaderTopBorder?: boolean;
};
const onMomentumScrollEnd = () => {
@@ -212,6 +215,10 @@ export default function List(props: ListProps) {
}
ListHeaderComponent={
<>
{!props.hideHeaderTopBorder ? (
<LineSeparator paddingVertical={Spacing.LEVEL_3} />
) : null}
{props.CustomLisHeader ? (
props.CustomLisHeader
) : !props.headerTitle ? null : (

View File

@@ -43,9 +43,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
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";
import { eOpenJumpToDialog } from "../../utils/events";
import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
@@ -190,12 +188,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
ref={props.scrollRef}
data={items}
/>
) : null}
@@ -205,6 +199,9 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
color={color.current}
notebooks={notebooks.current}
reminder={reminder.current}
hasGroupHeader={
groupHeader && previousIndex.current === index && !isSheet
}
attachmentsCount={attachmentsCount.current}
date={getDate(item as Note, group)}
isRenderedInActionSheet={isSheet}
@@ -229,12 +226,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
ref={props.scrollRef}
data={items}
/>
) : null}
<NotebookWrapper
@@ -260,12 +253,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
type={props.type}
groupId={props.groupId}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
ref={props.scrollRef}
data={items}
/>
) : null}
<ReminderItem
@@ -289,12 +278,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
ref={props.scrollRef}
data={items}
/>
) : null}
<TagItem
@@ -319,12 +304,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
color={props.customAccentColor}
groupOptions={groupOptions}
itemCount={items?.placeholders.length}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
ref={props.scrollRef}
data={items}
/>
) : null}
<SearchResult item={item as HighlightedResult} />

View File

@@ -18,15 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { View, ViewStyle } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { ProgressBarComponent } from "../ui/svg/lazy";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { SvgView } from "../ui/svg";
import { Radius, Spacing } from "../../common/design/spacing";
export const Loading = (props: {
title?: string;
description?: string;
icon?: string;
svgSrc?: string;
style?: ViewStyle;
}) => {
const { colors } = useThemeColors();
return (
@@ -37,18 +41,38 @@ export const Loading = (props: {
backgroundColor: colors.primary.background,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 16
paddingHorizontal: Spacing.LEVEL_3,
...props.style
}}
>
{props.icon ? (
<Icon name={props.icon} size={80} color={colors.primary.accent} />
) : null}
{props.svgSrc ? (
<View
style={{
marginBottom: Spacing.LEVEL_7
}}
>
<SvgView
src={props.svgSrc}
style={{
width: 180,
height: 180
}}
/>
</View>
) : null}
{props.title ? (
<Heading
style={{
textAlign: "center"
textAlign: "center",
marginBottom: Spacing.LEVEL_1
}}
fontSize="XL"
lineHeight={null}
>
{props.title}
</Heading>
@@ -57,8 +81,10 @@ export const Loading = (props: {
{props.description ? (
<Paragraph
style={{
textAlign: "center"
textAlign: "center",
marginBottom: Spacing.LEVEL_4
}}
fontSize="SM"
>
{props.description}
</Paragraph>
@@ -67,15 +93,15 @@ export const Loading = (props: {
<View
style={{
flexDirection: "row",
width: 100,
marginTop: 15
width: 300
}}
>
<ProgressBarComponent
height={5}
width={100}
height={7}
width={300}
animated={true}
useNativeDriver
borderRadius={Radius.XS}
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}

View File

@@ -72,6 +72,7 @@ const MergeConflicts = () => {
const insets = useGlobalSafeAreaInsets();
const content = useRef<UnencryptedContentItem>(null);
const { height } = useSettingStore((state) => state.dimensions);
const keepScreenOn = useSettingStore((state) => state.settings.keepScreenOn);
const applyChanges = async () => {
const contentToSave = selectedContent;
@@ -155,9 +156,9 @@ const MergeConflicts = () => {
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
@@ -182,9 +183,9 @@ const MergeConflicts = () => {
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
@@ -385,7 +386,7 @@ const MergeConflicts = () => {
paddingTop: insets.top
}}
>
<KeepAwake />
{keepScreenOn && <KeepAwake />}
{dialogVisible && (
<BaseDialog visible={true}>
<DialogContainer>

View File

@@ -1,220 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { RefObject, useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { LegendList } from "@legendapp/list";
import { getFormattedDate, getTimeAgo } from "@notesnook/common";
import { HistorySession, Note, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useDBItem } from "../../hooks/use-db-item";
import { presentSheet } from "../../services/event-manager";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import NotePreview from "./preview";
const HistoryItem = ({
index,
items,
note
}: {
index: number;
items?: VirtualizedGrouping<HistorySession>;
note: Note;
}) => {
const [item] = useDBItem(index, "noteHistory", items);
const { colors } = useThemeColors();
const getDate = (start: number, end: number) => {
const _start_date = getFormattedDate(start, "date");
const _end_date = getFormattedDate(end + 60000, "date");
const _start_time = getFormattedDate(start, "time");
const _end_time = getFormattedDate(end + 60000, "time");
return `${_start_date} ${_start_time} - ${
_end_date === _start_date ? " " : _end_date + " "
}${_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"
});
},
[note]
);
return (
<Pressable
type="secondary"
onPress={() => {
if (!item) return;
preview(item);
}}
style={{
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
height: 45,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
flexDirection: "row"
}}
>
{!item ? null : (
<>
<Paragraph>{getDate(item.dateCreated, item.dateModified)}</Paragraph>
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
{getTimeAgo(item.dateModified)}
</Paragraph>
</>
)}
</Pressable>
);
};
export default function NoteHistory({
note
}: {
note: Note;
fwdRef: RefObject<ActionSheetRef>;
}) {
const [history, setHistory] = useState<VirtualizedGrouping<HistorySession>>();
const [_loading, setLoading] = useState(true);
const { colors } = useThemeColors();
useEffect(() => {
db.noteHistory
.get(note.id)
.sorted({
sortBy: "dateModified",
sortDirection: "desc"
})
.then((result) => {
setHistory(result);
setLoading(false);
})
.catch((e) => {
setLoading(false);
});
}, [note.id]);
const renderItem = useCallback(
({ index }: { index: number }) => (
<HistoryItem index={index} items={history} note={note} />
),
[history, note]
);
return (
<View>
<SheetProvider context="note_history" />
<DialogHeader title={strings.noteHistory()} padding={12} />
<Seperator />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
height: !history?.placeholders.length
? 300
: (history.placeholders.length + 1) * 55,
maxHeight: "100%"
}}
>
<LegendList
renderScrollComponent={(props) => <ScrollView {...props} />}
data={history?.placeholders || []}
estimatedItemSize={55}
extraData={history}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 300,
gap: 10
}}
>
{_loading ? (
<ActivityIndicator
size={AppFontSize.xl}
color={colors.primary.accent}
/>
) : (
<>
<Icon name="history" size={50} color={colors.primary.icon} />
<Paragraph color={colors.secondary.paragraph}>
{strings.noteHistoryPlaceholder()}
</Paragraph>
</>
)}
</View>
}
renderItem={renderItem}
/>
</View>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{
alignSelf: "center"
}}
>
{strings.noteHistoryNotice[0]()}{" "}
<Text
onPress={() => {
openLinkInBrowser(
"https://help.notesnook.com/note-version-history"
);
}}
style={{
color: colors.primary.accent,
textDecorationLine: "underline"
}}
>
{strings.noteHistoryNotice[1]()}
</Text>
</Paragraph>
</View>
);
}

View File

@@ -1,224 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import { ToastManager, eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useTrashStore } from "../../stores/use-trash-store";
import { eCloseSheet, eOnLoadNote } from "../../utils/events";
import { Dialog } from "../dialog";
import DialogHeader from "../dialog/dialog-header";
import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import {
HistorySession,
isEncryptedContent,
Note,
NoteContent,
TrashOrItem
} from "@notesnook/core";
/**
*
* @param {any} param0
* @returns
*/
export default function NotePreview({
session,
content,
note
}: {
session?: HistorySession & { session: string };
content:
| Partial<
NoteContent<boolean> & {
title: string;
}
>
| undefined;
note: TrashOrItem<Note>;
}) {
const { colors } = useThemeColors();
const [locked, setLocked] = useState(false);
async function restore() {
if (note && note.type === "trash") {
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode();
ToastManager.show({
heading: strings.noteRestored(),
type: "success"
});
eSendEvent(eCloseSheet);
return;
}
if (session) {
await db.noteHistory.restore(session.id);
if (useTabStore.getState().hasTabForNote(session?.noteId)) {
const note = editorController.current.note.current[session?.noteId];
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
}
}
eSendEvent(eCloseSheet, "note_history");
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: strings.noteRestoredFromHistory(),
type: "success"
});
}
}
useEffect(() => {
db.vaults.itemExists(note).then((locked) => setLocked(locked));
}, [note]);
const deleteNote = async () => {
presentDialog({
title: strings.deleteNote(),
paragraph: strings.deleteNoteConfirmation(),
positiveText: strings.delete(),
negativeText: strings.cancel(),
context: "local",
positivePress: async () => {
if (note) {
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode();
ToastManager.show({
heading: strings.noteDeleted(),
type: "success",
context: "local"
});
eSendEvent(eCloseSheet);
}
},
positiveType: "error"
});
};
return (
<View
style={{
height: locked || session?.locked ? null : 600,
width: "100%"
}}
>
<Dialog context="local" />
<DialogHeader
padding={12}
title={content?.title || note.title || session?.session}
/>
{!session?.locked && !locked && content?.data ? (
<View
style={{
flex: 1,
backgroundColor: colors.primary.background
}}
>
<ReadonlyEditor
editorId="historyPreview"
onLoad={async (loadContent) => {
try {
if (content?.data) {
const currentContent = note?.contentId
? await db.content.get(note.contentId)
: undefined;
if (
currentContent?.data &&
!isEncryptedContent(currentContent)
) {
loadContent({
data: diff(
currentContent?.data || "<p></p>",
content.data as string
),
id: session?.noteId || note.id
});
}
}
} catch (e) {
ToastManager.error(
e as Error,
"Failed to load history preview",
"local"
);
}
}}
/>
</View>
) : (
<View
style={{
width: "100%",
height: 100,
justifyContent: "center",
alignItems: "center"
}}
>
<Paragraph color={colors.secondary.paragraph}>
{!content?.data
? strings.noContent()
: strings.encryptedNoteHistoryNotice()}
</Paragraph>
</View>
)}
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Button
onPress={restore}
title={strings.restore()}
type="accent"
width="100%"
/>
<Button
onPress={deleteNote}
title={strings.deletePermanently()}
type="error"
width="100%"
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
/>
</View>
</View>
);
}

View File

@@ -0,0 +1,24 @@
/*
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/>.
*/
export const Steps = {
select: 1,
buy: 2,
finish: 3,
buyWeb: 4
};

View File

@@ -0,0 +1,185 @@
/*
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 { getFeaturesTable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ScrollView, useWindowDimensions, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import usePricingPlans from "../../hooks/use-pricing-plans";
import { AppFontSize } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Steps } from "./common";
import { Radius, Spacing } from "../../common/design/spacing";
export const ComparePlans = React.memo(
(props: {
pricingPlans?: ReturnType<typeof usePricingPlans>;
setStep: (step: number) => void;
}) => {
const { colors } = useThemeColors();
const { width } = useWindowDimensions();
const isTablet = width > 600;
return (
<ScrollView
horizontal
style={{
width: isTablet ? "100%" : undefined,
paddingLeft: Spacing.LEVEL_3
}}
contentContainerStyle={{
flexDirection: "column"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
gap: 10
}}
>
{["Features", "Free", "Essential", "Pro", "Believer"].map(
(plan, index) => (
<View
key={plan}
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: Spacing.LEVEL_2,
backgroundColor:
index === 0 ? colors.secondary.background : undefined,
borderBottomWidth: index === 0 ? 1 : undefined,
borderBottomColor: colors.primary.border
}}
>
<Heading size={AppFontSize.sm}>{plan}</Heading>
</View>
)
)}
</View>
{getFeaturesTable().map((item, keyIndex) => {
return (
<View
key={`${item[0] + item[1]}`}
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
gap: 10
}}
>
{item.map((featureItem, index) => (
<View
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: Spacing.LEVEL_2,
backgroundColor:
index === 0 ? colors.secondary.background : undefined,
borderBottomWidth: index === 0 ? 1 : undefined,
borderBottomColor: colors.primary.border
}}
key={item[0] + index}
>
{typeof featureItem === "string" ? (
<Heading size={AppFontSize.sm}>
{featureItem as string}
</Heading>
) : (
<>
{typeof featureItem.caption === "string" ||
typeof featureItem.caption === "number" ? (
<Paragraph>
{featureItem.caption === "infinity"
? "∞"
: featureItem.caption}
</Paragraph>
) : typeof featureItem.caption === "boolean" ? (
<>
{featureItem.caption === true ? (
<Icon
color={colors.primary.accent}
size={AppFontSize.sm}
name="check"
/>
) : (
<Paragraph color={colors.secondary.paragraph}>
-
</Paragraph>
)}
</>
) : null}
</>
)}
</View>
))}
</View>
);
})}
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
gap: 10
}}
>
{["features", "free", "essential", "pro", "believer"].map(
(plan, index) => (
<View
key={plan + "btn"}
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: Spacing.LEVEL_1
}}
>
{plan !== "free" && plan !== "features" ? (
<Button
title={strings.select()}
type="accent"
fontSize={AppFontSize.xs}
style={{
paddingVertical: Spacing.LEVEL_1,
borderRadius: Radius.XS,
alignSelf: "flex-start"
}}
onPress={() => {
props.pricingPlans?.selectPlan(plan);
props.setStep(Steps.buy);
}}
/>
) : null}
</View>
)
)}
</View>
</ScrollView>
);
},
() => true
);
ComparePlans.displayName = "ComparePlans";

View File

@@ -0,0 +1,73 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import { AppFontSize } from "../../utils/size";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export const FAQItem = (props: { question: string; answer: string }) => {
const [expanded, setExpanded] = useState(false);
const { colors } = useThemeColors();
return (
<TouchableOpacity
style={{
padding: 16,
backgroundColor: colors.secondary.background,
borderRadius: 10,
marginBottom: 10,
gap: 12
}}
activeOpacity={0.9}
onPress={() => {
setExpanded(!expanded);
}}
key={props.question}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
}}
>
<Heading
style={{
flexShrink: 1
}}
size={AppFontSize.md}
>
{props.question}
</Heading>
<Icon
name={expanded ? "chevron-up" : "chevron-down"}
color={colors.secondary.icon}
size={AppFontSize.xxl}
/>
</View>
{expanded ? (
<Paragraph size={AppFontSize.md}>{props.answer}</Paragraph>
) : null}
</TouchableOpacity>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,315 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import {
ActivityIndicator,
Text,
useWindowDimensions,
View
} from "react-native";
import * as RNIap from "react-native-iap";
//@ts-ignore
import { Radius, Spacing } from "../../common/design/spacing";
import usePricingPlans, {
PlanOverView,
PricingPlan
} from "../../hooks/use-pricing-plans";
import PremiumService from "../../services/premium";
import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Steps } from "./common";
export const PricingPlanCard = ({
plan,
pricingPlans,
annualBilling,
setStep
}: {
plan: PricingPlan;
pricingPlans?: ReturnType<typeof usePricingPlans>;
annualBilling?: boolean;
setStep: (step: number) => void;
}) => {
const { colors } = useThemeColors();
const regionalDiscount =
plan.id === "pro" && annualBilling
? pricingPlans?.regionalDiscount
: undefined;
const { width } = useWindowDimensions();
const isTablet = width > 600;
const product =
plan.subscriptions?.[
regionalDiscount?.sku ||
`notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
];
const WebPlan = pricingPlans?.getWebPlan(
plan.id,
annualBilling ? "yearly" : "monthly"
);
const price = pricingPlans?.getPrice(
pricingPlans.isGithubRelease && WebPlan
? WebPlan
: (product as RNIap.Subscription),
pricingPlans.hasTrialOffer(plan.id, product?.productId) ? 1 : 0,
annualBilling
);
const isSubscribed =
product?.productId &&
pricingPlans?.user?.subscription?.productId?.includes(plan.id) &&
pricingPlans.isSubscribed();
const isNotReady =
pricingPlans?.loadingPlans || (!price && !WebPlan?.price?.gross);
return (
<View
style={{
...getElevationStyle(3),
backgroundColor: colors.secondary.background,
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: Radius.LG,
padding: 16,
flexShrink: isTablet ? 1 : undefined,
flexDirection: "column",
justifyContent: "space-between",
gap: 6
}}
>
<View>
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
justifyContent: "space-between",
marginBottom: Spacing.LEVEL_1
}}
>
<Heading size={AppFontSize.xl}>{plan.name} </Heading>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1
}}
>
{isSubscribed ? (
<View
style={{
backgroundColor: colors.primary.accent,
borderRadius: defaultBorderRadius,
padding: Spacing.LEVEL_0,
alignItems: "center",
justifyContent: "center",
alignSelf: "flex-start",
marginBottom: Spacing.LEVEL_1
}}
>
<Heading color={colors.static.white} size={AppFontSize.xxs}>
{strings.currentPlan()}
</Heading>
</View>
) : (
<>
{regionalDiscount?.discount || WebPlan?.discount ? (
<View
style={{
backgroundColor: colors.static.red,
borderRadius: 100,
padding: Spacing.LEVEL_0,
paddingHorizontal: Spacing.LEVEL_1,
justifyContent: "center",
alignSelf: "flex-start"
}}
>
<Heading color={colors.static.white} size={AppFontSize.xxs}>
{strings.percentOff(
`${regionalDiscount?.discount || WebPlan?.discount?.amount}`
)}
</Heading>
</View>
) : null}
{plan.recommended ? (
<View
style={{
backgroundColor: colors.static.red,
borderRadius: 100,
padding: Spacing.LEVEL_0,
paddingHorizontal: Spacing.LEVEL_1
}}
>
<Text
style={{
color: colors.static.white,
fontSize: AppFontSize.xxs
}}
>
{strings.recommended()}
</Text>
</View>
) : null}
</>
)}
</View>
</View>
<Paragraph>{plan.description}</Paragraph>
<View
style={{
marginTop: Spacing.LEVEL_3
}}
>
{pricingPlans?.loadingPlans || (!price && !WebPlan?.price?.gross) ? (
<ActivityIndicator size="small" color={colors.primary.accent} />
) : (
<View>
<Heading size={24}>
{price ||
`${WebPlan?.price?.currency} ${WebPlan?.price?.gross}`}{" "}
<Paragraph color={colors.secondary.heading} fontSize="SM">
/{strings.month()}
</Paragraph>
</Heading>
{!product && !WebPlan ? null : (
<Paragraph
color={colors.primary.paragraph}
size={AppFontSize.xs}
>
{annualBilling
? strings.billedAnnually(
pricingPlans?.getStandardPrice(
(product || WebPlan) as any
) as string
)
: strings.billedMonthly(
pricingPlans?.getStandardPrice(
(product || WebPlan) as any
) as string
)}
</Paragraph>
)}
</View>
)}
</View>
<View
style={{
marginVertical: Spacing.LEVEL_4,
gap: Spacing.LEVEL_2
}}
>
{Object.keys(PlanOverView[plan.id as keyof typeof PlanOverView]).map(
(item) => (
<View
key={item + plan.id}
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
borderBottomColor: colors.primary.border,
borderBottomWidth: 1,
paddingBottom: Spacing.LEVEL_2
}}
>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
<AppIcon
name={
item === "storage"
? "cloud"
: item === "fileSize"
? "file"
: "image-outline"
}
iconFamily="notesnook"
size={AppFontSize.lg}
/>
<Paragraph size={AppFontSize.sm}>
{strings[item as "storage" | "hdImages" | "fileSize"]()}
</Paragraph>
</View>
<Heading size={AppFontSize.sm}>
{
PlanOverView[plan.id as keyof typeof PlanOverView][
item as "storage" | "hdImages" | "fileSize"
]
}
</Heading>
</View>
)
)}
</View>
<Button
title={strings.selectPlan()}
type={plan.id === "pro" ? "accent" : "tertiary"}
style={{
width: "100%"
}}
onPress={() => {
if (isNotReady) return;
const currentPlanSubscribed =
PremiumService.get() &&
(pricingPlans?.user?.subscription?.productId ===
(product as RNIap.Subscription)?.productId ||
pricingPlans?.user?.subscription?.productId?.startsWith(
(product as RNIap.Subscription)?.productId
));
pricingPlans?.selectPlan(
plan.id,
currentPlanSubscribed
? `notesnook.${plan.id}.${
!(product as RNIap.Subscription)?.productId.includes(
"yearly"
)
? "yearly"
: "monthly"
}`
: pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: (product?.productId as string)
);
setStep(Steps.buy);
}}
/>
</View>
</View>
);
};

View File

@@ -0,0 +1,84 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Image, TouchableOpacity, View } from "react-native";
//@ts-ignore
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { Radius, Spacing } from "../../common/design/spacing";
import Heading from "../ui/typography/heading";
export const ReviewItem = (props: {
review: string;
user: string;
userSource: string;
link: string;
userImage?: string;
}) => {
const { colors } = useThemeColors();
return (
<TouchableOpacity
activeOpacity={1}
onPress={() => {
openLinkInBrowser(props.link);
}}
style={{
width: "100%",
padding: Spacing.LEVEL_2,
borderWidth: 1,
backgroundColor: colors.secondary.background,
borderRadius: Radius.S,
borderColor: colors.primary.border
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
paddingBottom: Spacing.LEVEL_1
}}
>
{props.userImage ? (
<Image
source={{
uri: props.userImage
}}
style={{
width: 34,
height: 34,
borderRadius: 100
}}
/>
) : null}
<View>
<Heading size={AppFontSize.sm}>{props.user}</Heading>
<Paragraph size={AppFontSize.xs} color={colors.primary.paragraph}>
{props.userSource}
</Paragraph>
</View>
</View>
<Paragraph>{props.review}</Paragraph>
</TouchableOpacity>
);
};

View File

@@ -27,6 +27,7 @@ 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 { Spacing } from "../../common/design/spacing";
import {
eSendEvent,
sendItemUpdateEvent,
@@ -38,10 +39,9 @@ import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { refreshNotesPage } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import ColorPicker from "../dialogs/color-picker";
import PaywallSheet from "../sheets/paywall";
import { Button } from "../ui/button";
import AppIcon from "../ui/AppIcon";
import { Pressable } from "../ui/pressable";
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
@@ -80,20 +80,21 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
key={item.id}
onPress={toggleColor}
style={{
width: 35,
height: 35,
width: 40,
height: 40,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: 5
borderWidth: 0
}}
>
{isLinked ? (
<Icon
<AppIcon
testID="icon-check"
name="check"
color="white"
size={AppFontSize.lg}
name="check-small"
iconFamily="notesnook"
color={colors.static.black}
size={15}
/>
) : null}
</Pressable>
@@ -151,31 +152,10 @@ export const ColorTags = ({ item }: { item: Note }) => {
/>
<View
style={{
flexGrow: isTablet ? undefined : 1,
flexDirection: "row",
marginLeft: 5,
flexShrink: 2
flexDirection: "row"
}}
>
{!colorNotes || !colorNotes.length ? (
<Button
onPress={onPress}
buttonType={{
text: colors.primary.accent
}}
title={strings.addColor()}
type="secondary"
icon="plus"
iconPosition="right"
height={30}
fontSize={AppFontSize.xs}
style={{
marginRight: 5,
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
/>
) : (
{colorNotes?.length ? (
<LegendList
data={colorNotes}
estimatedItemSize={30}
@@ -184,11 +164,14 @@ export const ColorTags = ({ item }: { item: Note }) => {
bounces={false}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
gap: Spacing.LEVEL_1
}}
ListFooterComponent={
<Pressable
style={{
width: 35,
height: 35,
width: 40,
height: 40,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
@@ -197,16 +180,17 @@ export const ColorTags = ({ item }: { item: Note }) => {
type="secondary"
onPress={onPress}
>
<Icon
<AppIcon
testID="icon-plus"
iconFamily="notesnook"
name="plus"
color={colors.primary.icon}
color={colors.primary.accent}
size={AppFontSize.lg}
/>
</Pressable>
}
/>
)}
) : null}
</View>
</>
);

View File

@@ -17,23 +17,40 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useState } from "react";
import { View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import { db } from "../../common/database";
import { Item, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { TouchableOpacity, View } from "react-native";
import { db } from "../../common/database";
import { Radius, Spacing } from "../../common/design/spacing";
import { presentDateTimePicker } from "../date-time-picker";
import AppIcon from "../ui/AppIcon";
import Paragraph from "../ui/typography/paragraph";
export const DateMeta = ({ item }: { item: Item }) => {
const { colors, isDark } = useThemeColors();
const [isDatePickerVisible, setIsDatePickerVisible] = useState(false);
const { colors } = useThemeColors();
const [dateCreated, setDateCreated] = useState(item.dateCreated);
const editDateCreated = () => {
presentDateTimePicker({
context: "properties",
mode: "datetime",
title: strings.changeCreatedDate(),
description: strings.changeCreatedDateDesc(),
confirmText: strings.change(),
date: new Date(dateCreated),
maxDate: new Date((item as Note).dateEdited),
onConfirm: async (date) => {
await db.notes.add({
id: item.id,
dateCreated: date.getTime()
});
setDateCreated(date.getTime());
}
});
};
function getDateMeta() {
const keys = Object.keys(item);
if (keys.includes("dateEdited"))
@@ -46,86 +63,70 @@ export const DateMeta = ({ item }: { item: Item }) => {
const renderItem = (key: string) =>
!item[key as keyof Item] ? null : (
<View
<TouchableOpacity
key={key}
activeOpacity={1}
onPress={
item.type !== "note" || key !== "dateCreated"
? undefined
: editDateCreated
}
style={{
flexDirection: "row",
width: "48.5%",
justifyContent: "space-between",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2
backgroundColor: colors.secondary.background,
borderRadius: Radius.XS,
padding: Spacing.LEVEL_2,
gap: Spacing.LEVEL_1,
alignItems: "center"
}}
>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{strings.dateDescFromKey(
key as
| "dateDeleted"
| "dateEdited"
| "dateModified"
| "dateCreated"
| "dateUploaded"
)}
</Paragraph>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
onPress={
item.type !== "note"
? undefined
: () => {
setIsDatePickerVisible(true);
}
}
<View
style={{
gap: Spacing.LEVEL_0
}}
>
{getFormattedDate(
key === "dateCreated"
? dateCreated
: (item[key as keyof Item] as string),
"date-time"
)}
{key === "dateCreated" && item.type === "note" ? (
<>
{" "}
<AppIcon name="pencil" size={AppFontSize.md} />
</>
) : null}
</Paragraph>
</View>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{strings.dateDescFromKey(
key as
| "dateDeleted"
| "dateEdited"
| "dateModified"
| "dateCreated"
| "dateUploaded"
)}
</Paragraph>
<Paragraph
fontSize="XS"
color={colors.primary.heading}
fontFamily="MEDIUM"
>
{getFormattedDate(
key === "dateCreated"
? dateCreated
: (item[key as keyof Item] as string),
"date-time"
)}
</Paragraph>
</View>
{key === "dateCreated" && item.type === "note" ? (
<>
<AppIcon name="edit-pencil" size={16} iconFamily="notesnook" />
</>
) : null}
</TouchableOpacity>
);
return (
<>
{item.type === "note" ? (
<DateTimePickerModal
isVisible={isDatePickerVisible}
mode="datetime"
onConfirm={async (date: Date) => {
await db.notes.add({
id: item.id,
dateCreated: date.getTime()
});
setDateCreated(date.getTime());
setIsDatePickerVisible(false);
}}
onCancel={() => {
setIsDatePickerVisible(false);
}}
maximumDate={new Date((item as Note).dateEdited)}
isDarkModeEnabled={isDark}
themeVariant={isDark ? "dark" : "light"}
is24Hour={db.settings.getTimeFormat() === "24-hour"}
date={new Date(dateCreated)}
/>
) : null}
<View
style={{
borderTopWidth: 1,
borderTopColor: colors.primary.border,
paddingHorizontal: DefaultAppStyles.GAP,
paddingTop: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
{getDateMeta().map(renderItem)}
</View>
</>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2
}}
>
{getDateMeta().map(renderItem)}
</View>
);
};

View File

@@ -1,251 +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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eOnLoadNote } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import SheetProvider from "../sheet-provider";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import { ReminderTime } from "../ui/reminder-time";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DateMeta } from "./date-meta";
import { Items } from "./items";
import Notebooks from "./notebooks";
import { TagStrip, Tags } from "./tags";
import { Dialog } from "../dialog";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
return (
<View
style={{
height: 1,
backgroundColor: colors.primary.border,
width: "100%",
marginTop: top,
marginBottom: bottom
}}
/>
);
};
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
const { colors } = useThemeColors();
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
{strings.noNotePropertiesNotice()}
</Paragraph>
);
}
return (
<FlatList
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
style={{
backgroundColor: colors.primary.background,
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
maxHeight: "100%"
}}
nestedScrollEnabled
bounces={false}
data={[0]}
keyExtractor={() => "properties-scroll-item"}
renderItem={() => (
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
flexShrink: 1,
gap: 5
}}
>
{item.type === "color" ? (
<Pressable
type="accent"
accentColor={item.colorCode}
accentText={colors.static.white}
style={{
width: 30,
height: 30,
borderRadius: 100,
marginRight: 10
}}
/>
) : item.type === "tag" ? (
<Icon
name="pound"
size={AppFontSize.lg}
color={colors.primary.icon}
/>
) : null}
<Heading size={AppFontSize.lg}>{item.title}</Heading>
</View>
{item.type === "note" ? (
<IconButton
name="open-in-new"
type="plain"
color={colors.primary.icon}
size={AppFontSize.lg}
style={{
alignSelf: "flex-start"
}}
onPress={() => {
close();
eSendEvent(eOnLoadNote, {
item: item,
newTab: true
});
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor");
}
}}
/>
) : null}
</View>
{(item.type === "notebook" || item.type === "reminder") &&
item.description ? (
<Paragraph>{item.description}</Paragraph>
) : null}
{item.type === "note" ? (
<TagStrip close={close} item={item} />
) : null}
{item.type === "reminder" ? (
<ReminderTime
reminder={item}
style={{
justifyContent: "flex-start",
borderWidth: 0,
alignSelf: "flex-start",
backgroundColor: "transparent",
paddingHorizontal: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
fontSize={AppFontSize.xs}
/>
) : null}
</View>
<DateMeta item={item} />
<Line bottom={0} top={0} />
{item.type === "note" ? (
<>
<Tags close={close} item={item} />
<Line bottom={0} top={0} />
</>
) : null}
{item.type === "note" ? (
<Notebooks note={item} close={close} />
) : null}
<Items
item={item}
buttons={buttons}
close={() => {
close();
}}
/>
{DDS.isTab ? (
<View
style={{
height: 20
}}
/>
) : null}
<SheetProvider context="properties" />
<Dialog context="properties" />
</View>
)}
/>
);
};
Properties.present = async (item, isSheet, buttons = []) => {
if (!item) return;
let type = item?.type;
let dbItem;
switch (type) {
case "trash":
dbItem = item;
break;
case "note":
dbItem = await db.notes.note(item.id);
break;
case "notebook":
dbItem = await db.notebooks.notebook(item.id);
break;
case "tag":
dbItem = await db.tags.tag(item.id);
break;
case "color":
dbItem = await db.colors.color(item.id);
break;
case "reminder": {
dbItem = await db.reminders.reminder(item.id);
break;
}
}
presentSheet({
context: isSheet ? "local" : undefined,
component: (ref, close) => (
<Properties
close={close}
actionSheetRef={ref}
item={dbItem}
buttons={buttons}
/>
)
});
};

View File

@@ -0,0 +1,460 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
presentSheet,
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import { eOnLoadNote, refreshNotesPage } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import SheetProvider from "../sheet-provider";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import { ReminderTime } from "../ui/reminder-time";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DateMeta } from "./date-meta";
import { Items } from "./items";
import { Tags } from "./tags";
import { Dialog } from "../dialog";
import AppIcon from "../ui/AppIcon";
import { Spacing } from "../../common/design/spacing";
import { Button } from "../ui/button";
import ManageTags from "../../screens/manage-tags";
import ColorPicker from "../dialogs/color-picker";
import { useRelationStore } from "../../stores/use-relation-store";
import { useMenuStore } from "../../stores/use-menu-store";
import Navigation from "../../services/navigation";
import { useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
import { useSettingStore } from "../../stores/use-setting-store";
import { Action, useActions } from "../../hooks/use-actions";
import {
Color,
Note,
Notebook,
Reminder,
Tag,
TrashItem
} from "@notesnook/core";
import LineSeparator from "../ui/seperator/line-separator";
export type PropertiesItem =
| Note
| Notebook
| Tag
| Color
| Reminder
| TrashItem;
export const Properties = ({
close,
item,
buttons = []
}: {
close?: (ctx?: string | undefined) => void;
item: PropertiesItem;
buttons: Action[];
}) => {
const { colors } = useThemeColors();
const colorFeature = useIsFeatureAvailable("colors");
const [noteNotebooks, setNoteNotebooks] = useState<Notebook[]>([]);
const [tags, setTags] = useState<Tag[]>([]);
const [visible, setVisible] = useState(false);
const colorNotes = useMenuStore((state) => state.colorNotes);
const actions = useActions({
item,
close: () => {
close?.();
}
});
const editAction = actions.find(
(action) => action.id === "edit-notebook" || action.id === "rename-tag"
);
useEffect(() => {
async function getNotebooks() {
const filteredNotebooks = await db.relations
.to(item, "notebook")
.resolve();
return filteredNotebooks || [];
}
if (item.type === "note") {
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
db.relations
.to(item, "tag")
.resolve()
.then((tags) => {
setTags(tags);
});
}
}, [item]);
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
{strings.noNotePropertiesNotice()}
</Paragraph>
);
}
return (
<ScrollView
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
style={{
backgroundColor: colors.primary.background,
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
maxHeight: "100%",
paddingTop: Spacing.LEVEL_3
}}
nestedScrollEnabled
bounces={false}
>
<View>
{item.type === "note" ? (
<ColorPicker
visible={visible}
setVisible={setVisible}
onColorAdded={async (color) => {
await db.relations.to(item, "color").unlink();
await db.relations.add(color, item);
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
Navigation.queueRoutesForUpdate();
sendItemUpdateEvent(color.id, "color");
eSendEvent(refreshNotesPage);
}}
/>
) : null}
<View
style={{
paddingHorizontal: Spacing.LEVEL_3
}}
>
<View
style={{
gap: Spacing.LEVEL_1
}}
>
{item.type === "note" && (noteNotebooks.length || tags.length) ? (
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_1
}}
>
{noteNotebooks?.map((item) => (
<View
key={item.id}
style={{
borderRadius: 100,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: 2,
backgroundColor: colors.secondary.background,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0
}}
>
<AppIcon
name="bookmark"
iconFamily="notesnook"
size={AppFontSize.xs}
color={colors.secondary.icon}
/>
<Paragraph fontSize="XS" color={colors.secondary.paragraph}>
{item.title}
</Paragraph>
</View>
))}
{tags?.map((item) =>
item.id ? (
<View
key={item.id}
style={{
borderRadius: 100,
paddingHorizontal: Spacing.LEVEL_1,
paddingVertical: 2,
backgroundColor: colors.secondary.background,
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_0
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{item.title}
</Paragraph>
</View>
) : null
)}
</View>
) : null}
<View
style={{
flexDirection: "row",
justifyContent: "space-between"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
flexShrink: 1,
gap: Spacing.LEVEL_1
}}
>
{item.type === "color" ? (
<Pressable
type="accent"
accentColor={item.colorCode}
accentText={colors.static.white}
style={{
width: 8,
height: 8,
borderRadius: 100
}}
/>
) : null}
<Heading fontSize="XL">{item.title}</Heading>
{editAction ? (
<IconButton
name="edit-pencil"
iconFamily="notesnook"
type="plain"
color={colors.primary.icon}
size={AppFontSize.md}
onPress={editAction.onPress}
/>
) : null}
</View>
{item.type === "note" ? (
<IconButton
name="square-out"
iconFamily="notesnook"
type="plain"
color={colors.primary.icon}
size={AppFontSize.lg}
style={{
alignSelf: "flex-start"
}}
onPress={() => {
close?.();
eSendEvent(eOnLoadNote, {
item: item,
newTab: true
});
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor");
}
}}
/>
) : null}
</View>
{(item.type === "notebook" || item.type === "reminder") &&
item.description ? (
<Paragraph>{item.description}</Paragraph>
) : null}
{item.type === "reminder" ? (
<ReminderTime
reminder={item}
style={{
justifyContent: "flex-start",
borderWidth: 0,
alignSelf: "flex-start",
backgroundColor: "transparent",
paddingHorizontal: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
fontSize={AppFontSize.xs}
/>
) : null}
</View>
{item.type === "note" ? (
<LineSeparator paddingVertical={Spacing.LEVEL_3} />
) : null}
<DateMeta item={item} />.
{item.type === "note" && colorNotes.length > 0 ? (
<View>
<LineSeparator
paddingVertical={Spacing.LEVEL_3}
style={{
paddingTop: Spacing.LEVEL_2
}}
/>
<Tags close={close} item={item} />
<LineSeparator
paddingVertical={Spacing.LEVEL_3}
style={{
paddingBottom: 0
}}
/>
</View>
) : null}
{item.type === "note" ? (
<>
<View
style={{
flexDirection: "row",
gap: Spacing.LEVEL_2,
marginTop: Spacing.LEVEL_2
}}
>
<Button
onPress={async () => {
ManageTags.present([item.id]);
close?.();
}}
title={
colorNotes?.length
? strings.addTag()
: strings.dataTypesCamelCase.tag()
}
type={colorNotes?.length ? "accent-outline" : "shade-plain"}
icon="plus"
iconFamily="notesnook"
style={{
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_2,
width: colorNotes?.length > 0 ? "100%" : "48.5%"
}}
/>
{colorNotes.length > 0 ? null : (
<Button
onPress={() => {
if (colorFeature && !colorFeature.isAllowed) {
ToastManager.show({
message: colorFeature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(colorFeature);
ToastManager.hide();
}
});
return;
}
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
title={strings.dataTypesCamelCase.color()}
type="shade-plain"
icon="plus"
iconFamily="notesnook"
style={{
width: "48.5%",
paddingHorizontal: Spacing.LEVEL_3,
paddingVertical: Spacing.LEVEL_2
}}
/>
)}
</View>
</>
) : null}
</View>
<LineSeparator
paddingVertical={Spacing.LEVEL_2}
paddingHorizontal={Spacing.LEVEL_3}
/>
<Items
item={item}
buttons={buttons}
actions={actions}
close={() => {
close?.();
}}
/>
{DDS.isTab ? (
<View
style={{
height: 20
}}
/>
) : null}
<SheetProvider context="properties" />
<Dialog context="properties" />
</View>
</ScrollView>
);
};
Properties.present = async (
item: PropertiesItem,
isSheet: boolean = false,
buttons: Action[] = []
) => {
if (!item) return;
const type = item?.type;
let dbItem;
switch (type) {
case "trash":
dbItem = item;
break;
case "note":
dbItem = await db.notes.note(item.id);
break;
case "notebook":
dbItem = await db.notebooks.notebook(item.id);
break;
case "tag":
dbItem = await db.tags.tag(item.id);
break;
case "color":
dbItem = await db.colors.color(item.id);
break;
case "reminder": {
dbItem = await db.reminders.reminder(item.id);
break;
}
}
if (!dbItem) return;
presentSheet({
context: isSheet ? "local" : undefined,
component: (ref, close) => (
<Properties close={close} item={dbItem} buttons={buttons} />
)
});
};

View File

@@ -18,21 +18,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Item } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Dimensions, View } from "react-native";
import SwiperFlatList from "react-native-swiper-flatlist";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { Action, ActionId, useActions } from "../../hooks/use-actions";
import { Action, ActionId } from "../../hooks/use-actions";
import { useStoredRef } from "../../hooks/use-stored-ref";
import { DDS } from "../../services/device-detection";
import { useSettingStore } from "../../stores/use-setting-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { Radius, Spacing } from "../../common/design/spacing";
const TOP_BAR_ITEMS: ActionId[] = [
"pin",
@@ -60,13 +60,22 @@ const BOTTOM_BAR_ITEMS: ActionId[] = [
"duplicate",
"launcher-shortcut",
"expiry-date",
"disable-reminder",
"trash"
];
const PREFERENCE_ITEMS: ActionId[] = [
"add-shortcut",
"pin",
"default-notebook",
"default-tag",
"default-homepage"
];
const COLUMN_BAR_ITEMS: ActionId[] = [
"select",
"add-notebook",
"edit-notebook",
// "edit-notebook",
"move-notes",
"move-notebook",
"edit-reminder",
@@ -78,7 +87,7 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
"add-shortcut",
"reorder",
"rename-color",
"rename-tag",
// "rename-tag",
"launcher-shortcut",
"copy-id",
"copy-link",
@@ -90,11 +99,13 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
export const Items = ({
item,
close,
buttons
buttons,
actions
}: {
item: Item;
close: () => void;
buttons: Action[];
actions: Action[];
}) => {
const { colors } = useThemeColors();
const topBarSorting = useStoredRef<{ [name: string]: number }>(
@@ -102,14 +113,12 @@ export const Items = ({
{}
);
const dimensions = useSettingStore((state) => state.dimensions);
const actions = useActions({ item, close });
const selectedActions = actions.filter((i) => !i.hidden);
const deviceMode = useSettingStore((state) => state.deviceMode);
const width = Math.min(dimensions.width, 600);
const shouldShrink =
Dimensions.get("window").fontScale > 1 &&
Dimensions.get("window").width < 450;
Dimensions.get("window").fontScale > 1 && dimensions.width < 450;
const columnItemsCount = deviceMode === "tablet" ? 7 : shouldShrink ? 4 : 5;
@@ -135,16 +144,23 @@ export const Items = ({
BOTTOM_BAR_ITEMS.indexOf(a.id) > BOTTOM_BAR_ITEMS.indexOf(b.id) ? 1 : -1
);
const preferenceItems = selectedActions
.filter((item) => PREFERENCE_ITEMS.indexOf(item.id) > -1)
.sort((a, b) =>
PREFERENCE_ITEMS.indexOf(a.id) > PREFERENCE_ITEMS.indexOf(b.id) ? 1 : -1
);
const columnItems = selectedActions
.filter((item) => COLUMN_BAR_ITEMS.indexOf(item.id) > -1)
.filter(
(item) =>
COLUMN_BAR_ITEMS.indexOf(item.id) > -1 &&
PREFERENCE_ITEMS.indexOf(item.id) === -1
)
.sort((a, b) =>
COLUMN_BAR_ITEMS.indexOf(a.id) > COLUMN_BAR_ITEMS.indexOf(b.id) ? 1 : -1
);
const topBarItemHeight = Math.min(
(width - (topBarItems.length * 10 + 14)) / topBarItems.length,
60
);
const actionItems = [...buttons, ...columnItems];
const renderRowItem = React.useCallback(
({ item }: { item: Action }) => (
@@ -152,7 +168,7 @@ export const Items = ({
key={item.id}
style={{
alignItems: "center",
width: columnItemWidth - 8,
width: columnItemWidth - 10,
opacity: item.locked ? 0.5 : 1
}}
>
@@ -161,17 +177,18 @@ export const Items = ({
type={item.checked ? "shade" : "secondary"}
testID={"icon-" + item.id}
style={{
height: columnItemWidth / 1.5,
width: columnItemWidth - 8,
width: columnItemWidth - 10,
paddingVertical: Spacing.LEVEL_2,
borderRadius: 10,
justifyContent: "center",
alignItems: "center",
marginBottom: DDS.isTab ? 7 : 3.5
marginBottom: 6
}}
>
<Icon
<AppIcon
allowFontScaling
name={item.icon}
iconFamily="notesnook"
size={
DDS.isTab
? AppFontSize.xxl
@@ -207,33 +224,79 @@ export const Items = ({
]
);
const renderColumnItem = React.useCallback(
const renderPreferenceItem = React.useCallback(
(item: Action) => (
<Button
<Pressable
key={item.id}
buttonType={{
text: item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.paragraph
: colors.primary.paragraph
}}
testID={"icon-" + item.id}
onPress={item.onPress}
title={item.title}
icon={item.icon}
type={item.checked ? "inverted" : "plain"}
fontSize={AppFontSize.sm}
type={item.checked ? "shade" : "transparent"}
testID={"icon-" + item.id}
style={{
borderRadius: 0,
justifyContent: "flex-start",
alignSelf: "flex-start",
width: "100%",
opacity: item.locked ? 0.5 : 1
width: "48.5%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: Spacing.LEVEL_1,
paddingVertical: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_2,
borderRadius: Radius.XS,
borderWidth: item.checked ? 0 : 1,
borderColor: colors.primary.border,
opacity: item.locked ? 0.7 : 1
}}
/>
>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: Spacing.LEVEL_1,
flexShrink: 1
}}
>
<AppIcon
name={item.icon}
iconFamily="notesnook"
allowFontScaling
size={AppFontSize.md}
color={item.checked ? colors.primary.icon : colors.secondary.icon}
/>
<Paragraph
numberOfLines={1}
fontSize="XS"
fontFamily="MEDIUM"
color={
item.checked
? colors.primary.paragraph
: colors.secondary.paragraph
}
style={{ flexShrink: 1 }}
>
{item.title}
</Paragraph>
</View>
<AppIcon
name={item.checked ? "toggle-on" : "toggle-off"}
iconFamily="notesnook"
size={16}
color={
item.checked
? [colors.primary.accent, colors.primary.background]
: [colors.disabled.icon, colors.primary.background]
}
/>
</Pressable>
),
[colors.error.paragraph, colors.primary.accent, colors.primary.paragraph]
[
colors.disabled.icon,
colors.primary.accent,
colors.primary.background,
colors.primary.border,
colors.primary.icon,
colors.primary.paragraph,
colors.secondary.icon,
colors.secondary.paragraph
]
);
const renderTopBarItem = React.useCallback(
@@ -253,7 +316,7 @@ export const Items = ({
key={item.id}
testID={"icon-" + item.id}
style={{
width: columnItemWidth - 8,
width: columnItemWidth - 10,
alignSelf: "flex-start",
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
@@ -261,24 +324,26 @@ export const Items = ({
<View
style={{
height: columnItemWidth / 2,
width: columnItemWidth - DefaultAppStyles.GAP_SMALL,
width: 65,
justifyContent: "center",
alignItems: "center",
borderWidth: 1,
borderRadius: defaultBorderRadius,
borderColor: item.checked
? item.activeColor || colors.primary.accent
: colors.primary.border,
overflow: "hidden"
backgroundColor: item.checked
? colors.primary.shade
: colors.secondary.background,
borderRadius: Radius.XS,
overflow: "hidden",
paddingVertical: Spacing.LEVEL_2,
paddingHorizontal: Spacing.LEVEL_2
}}
>
<Icon
<AppIcon
name={item.icon}
iconFamily="notesnook"
allowFontScaling
size={DDS.isTab ? AppFontSize.xxl : AppFontSize.md + 4}
size={16}
color={
item.checked
? item.activeColor || colors.primary.accent
? colors.primary.icon
: item.id === "delete" || item.id === "trash"
? colors.error.icon
: colors.secondary.icon
@@ -310,7 +375,13 @@ export const Items = ({
<Paragraph
textBreakStrategy="simple"
size={AppFontSize.xxs}
fontSize="XXS"
fontFamily="MEDIUM"
color={
item.checked
? colors.primary.paragraph
: colors.secondary.paragraph
}
style={{ textAlign: "center" }}
>
{item.title}
@@ -321,8 +392,12 @@ export const Items = ({
[
colors.error.icon,
colors.primary.accent,
colors.primary.border,
colors.primary.icon,
colors.primary.paragraph,
colors.primary.shade,
colors.secondary.background,
colors.secondary.icon,
colors.secondary.paragraph,
colors.static.orange,
columnItemWidth,
topBarSorting
@@ -339,11 +414,7 @@ export const Items = ({
};
return (
<View
style={{
gap: DefaultAppStyles.GAP
}}
>
<View>
{item.type === "note" ? (
<>
<View>
@@ -352,22 +423,24 @@ export const Items = ({
autoplay={false}
showPagination
paginationStyleItemActive={{
borderRadius: 2,
backgroundColor: colors.selected.background,
height: 6,
marginHorizontal: 2
borderRadius: 6,
backgroundColor: colors.selected.accent,
height: 5,
width: 20,
marginHorizontal: 3
}}
paginationStyleItemInactive={{
borderRadius: 2,
borderRadius: 6,
backgroundColor: colors.secondary.background,
height: 6,
marginHorizontal: 2
height: 5,
width: 14,
marginHorizontal: 3
}}
paginationStyle={{
position: "relative",
marginHorizontal: 2,
marginBottom: -10,
marginTop: 10
marginBottom: Spacing.LEVEL_0,
marginTop: Spacing.LEVEL_2
}}
contentContainerStyle={{
justifyContent: "flex-start",
@@ -380,7 +453,7 @@ export const Items = ({
style={{
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP,
gap: 5,
gap: Spacing.LEVEL_2,
width: width
}}
>
@@ -394,7 +467,7 @@ export const Items = ({
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: 5,
gap: Spacing.LEVEL_1,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
@@ -402,9 +475,61 @@ export const Items = ({
</View>
</>
) : (
<View>
{buttons.map(renderColumnItem)}
{columnItems.map(renderColumnItem)}
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{preferenceItems.length > 0 ? (
<View style={{ gap: Spacing.LEVEL_2 }}>
<Paragraph
fontFamily="MEDIUM"
fontSize="SM"
color={colors.secondary.paragraph}
>
{strings.preferences()}
</Paragraph>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: Spacing.LEVEL_1
}}
>
{preferenceItems.map(renderPreferenceItem)}
</View>
</View>
) : null}
<View
style={{
marginVertical: Spacing.LEVEL_3,
width: "100%",
borderBottomWidth: 1,
borderColor: colors.primary.separator
}}
/>
{actionItems.length > 0 ? (
<View style={{ gap: Spacing.LEVEL_2 }}>
<Paragraph
fontFamily="MEDIUM"
fontSize="SM"
color={colors.secondary.paragraph}
>
{strings.actionsHeading()}
</Paragraph>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: Spacing.LEVEL_1
}}
>
{actionItems.map((item) => renderRowItem({ item }))}
</View>
</View>
) : null}
</View>
)}
</View>

View File

@@ -17,18 +17,17 @@ 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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import ManageTags from "../../screens/manage-tags";
import { TaggedNotes } from "../../screens/notes/tagged";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
import { Spacing } from "../../common/design/spacing";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();
@@ -37,32 +36,9 @@ export const Tags = ({ item, close }) => {
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
alignSelf: "center",
justifyContent: "space-between",
width: "100%"
}}
>
<Button
onPress={async () => {
ManageTags.present([item.id]);
close();
}}
buttonType={{
text: colors.primary.accent
}}
title={strings.addTag()}
type="secondary"
icon="plus"
iconPosition="right"
fontSize={AppFontSize.xs}
style={{
paddingHorizontal: DefaultAppStyles.GAP_SMALL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
/>
<ColorTags item={item} />
</View>
) : null;

View File

@@ -170,147 +170,147 @@ export const SelectionHeader = React.memo(
{!selectedItemsList.length
? null
: (focusedRouteId === "Monographs"
? [
{
title: strings.unpublish(),
onPress: () => {
presentDialog({
title: strings.doActions.unpublish.note(
selectedItemsList.length
),
positiveText: strings.unpublish(),
negativeText: strings.cancel(),
positivePress: async () => {
for (const id of selectedItemsList) {
await db.monographs.unpublish(id);
}
Navigation.queueRoutesForUpdate();
clearSelection();
},
positiveType: "errorShade"
});
},
visible: true,
icon: "delete"
}
]
: [
{
title: strings.manageTags(),
onPress: async () => {
ManageTags.present(selectedItemsList);
},
visible: type === "note",
icon: "pound"
},
{
title: strings.export(),
onPress: async () => {
ExportNotesSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "export"
},
{
title: strings.addToNotebook(),
onPress: async () => {
Navigation.navigate("LinkNotebooks", {
noteIds: selectedItemsList
});
},
visible: type === "note",
icon: "plus"
},
{
title: strings.unlinkNotebook(),
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(
id,
...selectedItemsList
);
updateNotebook(id);
? [
{
title: strings.unpublish(),
onPress: () => {
presentDialog({
title: strings.doActions.unpublish.note(
selectedItemsList.length
),
positiveText: strings.unpublish(),
negativeText: strings.cancel(),
positivePress: async () => {
for (const id of selectedItemsList) {
await db.monographs.unpublish(id);
}
Navigation.queueRoutesForUpdate();
clearSelection();
},
visible: renderedInRoute === "Notebook",
icon: "minus"
},
{
title: strings.unfavorite(),
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: strings.moveToTrash(),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible: type === "note" || type === "notebook",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" &&
type !== "note" &&
type !== "notebook",
icon: "delete"
},
{
title: strings.restore(),
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: strings.delete(),
onPress: deleteItem,
visible: type === "trash",
icon: "delete"
}
]
).map((item) =>
!item.visible ? null : (
<IconButton
size={AppFontSize.lg}
type="plain"
tooltipText={item.title}
tooltipPosition={NativeTooltip.POSITIONS.TOP}
testID={`select-${item.icon}`}
name={item.icon}
key={item.title}
color={colors.primary.icon}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();
}}
/>
)
)}
positiveType: "errorShade"
});
},
visible: true,
icon: "delete"
}
]
: [
{
title: strings.manageTags(),
onPress: async () => {
ManageTags.present(selectedItemsList);
},
visible: type === "note",
icon: "pound"
},
{
title: strings.export(),
onPress: async () => {
ExportNotesSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "export"
},
{
title: strings.addToNotebook(),
onPress: async () => {
Navigation.navigate("LinkNotebooks", {
noteIds: selectedItemsList
});
},
visible: type === "note",
icon: "plus"
},
{
title: strings.unlinkNotebook(),
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(
id,
...selectedItemsList
);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
},
visible: renderedInRoute === "Notebook",
icon: "minus"
},
{
title: strings.unfavorite(),
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: strings.moveToTrash(),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible: type === "note" || type === "notebook",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" &&
type !== "note" &&
type !== "notebook",
icon: "delete"
},
{
title: strings.restore(),
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: strings.delete(),
onPress: deleteItem,
visible: type === "trash",
icon: "delete"
}
]
).map((item) =>
!item.visible ? null : (
<IconButton
size={AppFontSize.lg}
type="plain"
tooltipText={item.title}
tooltipPosition={NativeTooltip.POSITIONS.TOP}
testID={`select-${item.icon}`}
name={item.icon}
key={item.title}
color={colors.primary.icon}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();
}}
/>
)
)}
</View>
<IconButton

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