Compare commits

...

348 Commits

Author SHA1 Message Date
Ammar Ahmed
521afe90ca mobile: show backup complete sheet after 500ms 2026-03-13 09:39:03 +05:00
Ammar Ahmed
ee3b7b5d7d mobile: fix ios app remains stuck when restoring backup from list 2026-03-13 09:21:49 +05:00
Abdullah Atta
b2bac3808b Merge pull request #9505 from streetwriters/release/vnext
Release vNext
2026-03-12 10:19:47 +05:00
Abdullah Atta
ebf58e68cf desktop: disable db connecting closing 2026-03-12 10:00:50 +05:00
Ammar Ahmed
dc3e909dfe mobile: release v3.3.16 2026-03-12 09:50:21 +05:00
Abdullah Atta
bf09e9615a web: bump version to 3.3.10 2026-03-12 09:48:22 +05:00
Abdullah Atta
b2d30d47d6 Merge pull request #9504 from streetwriters/fix/crash-on-app-close
Fix sql crash on app close
2026-03-12 09:43:34 +05:00
Abdullah Atta
b1296fd24f Merge pull request #9503 from streetwriters/fix/disable-auto-orphaned-attachment-deletion
Disable automatic orphaned attachment deletion
2026-03-12 09:43:09 +05:00
Abdullah Atta
62d8c89300 desktop: fix sql crash on app close 2026-03-12 09:40:39 +05:00
Abdullah Atta
80c44eee89 mobile: disable automatic orphaned attachment removal 2026-03-12 09:34:15 +05:00
Abdullah Atta
153eb126fc web: disable automatic orphaned attachment removal 2026-03-12 09:34:04 +05:00
Abdullah Atta
dda36bb036 Merge pull request #9478 from streetwriters/add-pr-template
Add pull request template for contributions
2026-03-10 10:31:28 +05:00
Ammar Ahmed
843ad08813 global: add platform selection to pull request template
Added a section to specify related platforms for the PR.

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2026-03-10 10:23:40 +05:00
Abdullah Atta
a0e3c923e4 Apply suggestion from @thecodrr
Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
2026-03-10 10:14:57 +05:00
Abdullah Atta
3095b03b09 Update .github/pull_request_template.md
Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
2026-03-10 10:00:11 +05:00
Ammar Ahmed
ea5a0e0a7c Add pull request template for contributions
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2026-03-10 09:29:38 +05:00
Abdullah Atta
2b4ec5b03b Merge pull request #9472 from NeedsChloesure/feat/update-docs
docs: hotfix: Fix copilot's document formatting error that I didn't notice.
2026-03-09 14:14:51 +05:00
Chloe Oletto
59f7add065 docs: hotfix: Fix copilot's document formatting error that I didn't notice.
Signed-off-by: Chloe Oletto <needschloesure@riseup.net>
2026-03-09 01:39:25 -07:00
Abdullah Atta
9c7bfc2347 Merge pull request #9450 from streetwriters/release/vnext
Release v3.3.9
2026-03-09 13:10:05 +05:00
Abdullah Atta
ef90f40be4 web: add version highlights 2026-03-09 12:39:42 +05:00
Ammar Ahmed
bca6a4f22c mobile: add feature highlights 2026-03-09 11:47:34 +05:00
Ammar Ahmed
1d3e2b56b2 mobile: fix icon colors not rendering correctly 2026-03-09 11:30:17 +05:00
Ammar Ahmed
a93a20ed5b mobile: fix color of icon note move-notebook 2026-03-09 11:30:17 +05:00
Ammar Ahmed
971a80c183 mobile: release 3.3.15 2026-03-09 11:30:17 +05:00
Abdullah Atta
125b60facc core: disable sync stress tests 2026-03-09 09:50:29 +05:00
Abdullah Atta
ef99b47178 web: fix edit creation date test 2026-03-09 08:57:53 +05:00
Abdullah Atta
d65f870213 core: fix tests 2026-03-09 08:48:08 +05:00
Abdullah Atta
5b3f1c9de2 web: bump version to 3.3.9 2026-03-08 20:55:25 +05:00
Abdullah Atta
4afb090cb1 web: update snapshots 2026-03-08 13:40:20 +05:00
Abdullah Atta
8cf6e65ed8 web: upgrade otplib 2026-03-08 13:11:49 +05:00
Abdullah Atta
c85c51ec08 web: improve tests reliability 2026-03-07 12:47:44 +05:00
Abdullah Atta
dbeabea6c5 web: always use prod urls for testing 2026-03-07 12:47:44 +05:00
Abdullah Atta
dfbe2d0f0f web: don't crash on web extension errors 2026-03-07 12:47:44 +05:00
Abdullah Atta
ea2277ea4e web: fix characters missing sometimes when creating a new note 2026-03-07 12:47:44 +05:00
Abdullah Atta
ad78105a6e web: update playwright 2026-03-07 12:47:44 +05:00
Abdullah Atta
36584175e2 web: add support for test throttling 2026-03-07 12:47:44 +05:00
Ammar Ahmed
084b1fdfd2 mobile: fix blank space under sheet 2026-03-07 09:22:22 +05:00
Ammar Ahmed
bed091e20f mobile: fix properties topbar pagination 2026-03-07 09:22:10 +05:00
Ammar Ahmed
4420663687 mobile: fix use-db-item hook 2026-03-06 13:07:57 +05:00
Ammar Ahmed
677ae82ffa Merge pull request #9435 from streetwriters/fix/crash-theme-switch-animation
Fix native crashes on android found on google developer console android vitals
2026-03-05 12:34:30 +05:00
Ammar Ahmed
48a3fac9e0 Merge pull request #9434 from streetwriters/fix/reachabilty-url
mobile: fix reachability url for checking network access
2026-03-05 12:33:24 +05:00
Ammar Ahmed
e797d37b01 Merge pull request #9418 from streetwriters/fix/read-only-title
mobile: fix title is editable when note has readonly mode enabled
2026-03-05 12:32:24 +05:00
Abdullah Atta
ffafd2e787 editor: fix marks lost on node split 2026-03-05 12:26:59 +05:00
Abdullah Atta
8500c971eb editor: fix font family not reflected in toolbar 2026-03-05 12:26:59 +05:00
Ammar Ahmed
46a67a56fb mobile: fix reachability url 2026-03-03 08:45:51 +05:00
Chloe Oletto
5dc62e6df2 docs: Update all docs to be current. (#9430)
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-03-02 12:34:20 +05:00
Abdullah Atta
4479d6cf56 Merge branch 'master' into beta 2026-03-02 12:30:52 +05:00
01zulfi
1faaf1f6b1 web: fix vault not marked as created on login if already exists for user (#9407)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-02 10:37:49 +05:00
Copilot
1f33ef191a docs(help): add Archive help documentation (#9412)
* Initial plan

* Add help documentation for Archive feature

Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>

* Fix Desktop/Web instructions to use right-click only, not three-dot button

Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>
2026-03-02 10:31:58 +05:00
Copilot
d5bf8a6e3c editor: fix backticks create inline code even with Markdown Shortcuts disabled (#9415) 2026-03-02 10:29:21 +05:00
Copilot
5c252c7843 desktop: fix external links opening in-app instead of default browser on desktop (#9417)
* Initial plan

* fix: open external links in default browser on Linux desktop

Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>

* desktop: fix sql db closure on app close

---------

Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-03-02 10:05:29 +05:00
Abdullah Atta
d2e10ccaf6 Merge pull request #9414 from streetwriters/copilot/add-opensans-font
monograph: Use Inter font instead of missing OpenSans in OG image generation
2026-03-02 09:37:39 +05:00
Abdullah Atta
89d2b6870f monograph: bump version to 1.2.8 2026-03-02 09:37:24 +05:00
Copilot
26a9086d96 editor: fix TOC active heading detection (#9413)
* Initial plan

* fix: compute heading offsets relative to editor root for correct TOC highlighting

Headings inside callout blocks had incorrect `offsetTop` values because
`offsetTop` is relative to the nearest positioned ancestor (the callout),
not the editor root. This caused the TOC to highlight wrong sections.

Fix: walk the `offsetParent` chain from the heading up to the editor
content element, accumulating `offsetTop` values to get the correct
absolute position within the editor.

Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>

* editor: ignore callouts from toc

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-03-02 09:34:53 +05:00
01zulfi
9fc8e6b1e5 web: reduce note title size & improve padding (#9420)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-27 22:11:59 +05:00
Ammar Ahmed
6718650619 mobile: update libraries
react-native-background-fetch
notifee-react-native
2026-02-27 12:03:02 +05:00
Ammar Ahmed
a622949f23 mobile: fix react-native-tooltips crash 2026-02-27 12:02:34 +05:00
Ammar Ahmed
5a2df5b2c1 mobile: fix crash when switching theme animation 2026-02-27 12:01:18 +05:00
Ammar Ahmed
ae751cdd65 mobile: fix readonly title editable 2026-02-27 09:24:00 +05:00
Ammar Ahmed
0d00bb2866 mobile: update @ammarahmed/react-native-upload dep 2026-02-27 08:54:35 +05:00
Ammar Ahmed
4299965064 mobile: update README.md 2026-02-27 08:47:23 +05:00
copilot-swe-agent[bot]
6f3fc8db3d Use Inter font instead of OpenSans in monograph OG image generation
Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>
2026-02-26 18:26:26 +00:00
copilot-swe-agent[bot]
db731856f8 Fix missing OpenSans fonts in monograph Docker image
Co-authored-by: thecodrr <7473959+thecodrr@users.noreply.github.com>
2026-02-26 17:21:36 +00:00
copilot-swe-agent[bot]
217f1dd6d4 Initial plan 2026-02-26 17:15:33 +00:00
Abdullah Atta
cf87929476 web: bump version to v3.3.9-beta.2 2026-02-26 12:55:37 +05:00
Ammar Ahmed
d343a0720e mobile: update README.md 2026-02-26 12:46:53 +05:00
Ammar Ahmed
51bd58edb2 mobile: release 3.3.15-beta.1 2026-02-26 12:37:22 +05:00
Ammar Ahmed
f778d83316 editor: prevent audio element request attachment data multiple times 2026-02-26 10:53:32 +05:00
Ammar Ahmed
b169206c68 mobile: fix audio playback in editor 2026-02-26 10:34:28 +05:00
Ammar Ahmed
4cea69e71d core: remove group from queueDownloads when in progress 2026-02-26 10:34:04 +05:00
Ammar Ahmed
04d422b411 core: fix audio attachments not linked to note when attached 2026-02-26 10:32:17 +05:00
Ammar Ahmed
49dec52aa0 mobile: fix multiple unlock with biometrics buttons rendering in vault dialog 2026-02-26 09:44:06 +05:00
Ammar Ahmed
b83a62814d mobile: use home group options when configuring note preview widget 2026-02-26 09:13:34 +05:00
Ammar Ahmed
ce5449f25a intl: fix add tag description 2026-02-26 08:31:02 +05:00
Abdullah Atta
aa769a8921 Merge pull request #9301 from streetwriters/core/orphaned-attachment-cleanup
core: remove orphaned attachments in trash cleanup method
2026-02-25 12:53:17 +05:00
01zulfi
a8d4e6c754 web: fix vault not marked as created when vault is created from locking note (#9394)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-25 11:36:32 +05:00
Abdullah Atta
f2402cf086 web: remove orphaned attachments on app launch 2026-02-25 11:24:31 +05:00
Ammar Ahmed
07a481f002 mobile: remove orphaned attachments on app launch 2026-02-24 12:55:31 +05:00
Ammar Ahmed
f41638db6d mobile: attachment items not rendering on switching list 2026-02-24 12:48:34 +05:00
Ammar Ahmed
82baf41d8e mobile: fix bulk delete call 2026-02-24 12:48:10 +05:00
Abdullah Atta
e60370d4dc editor: improve hr styling (#9377) 2026-02-24 11:08:45 +05:00
01zulfi
3e412323ee web: handle crash when drag-n-drop file in flatpak build (#9379)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-24 11:08:24 +05:00
Abdullah Atta
d635c1a056 Merge pull request #9364 from streetwriters/editor/audio-playback
editor: support audio attachment playback
2026-02-24 11:06:50 +05:00
01zulfi
f02db2a024 editor: fix checklist & blockquote indent so text starts at same pos as other lists (#9358)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-24 11:04:38 +05:00
Abdullah Atta
8c74324ff8 editor: handle attachments based on mime type 2026-02-24 10:29:58 +05:00
01zulfi
f2230eb39b editor: disallow inserting empty embed urls/code (#9392)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-24 09:50:38 +05:00
Abdullah Atta
ab900ae5da editor: various fixes and improvements related to audio widget 2026-02-24 09:36:47 +05:00
Ammar Ahmed
1cf28c56c2 mobile: add bulk delete support in file storage 2026-02-24 09:17:29 +05:00
Ammar Ahmed
08cd33a0cf Merge pull request #9371 from streetwriters/fix-sort-tags-by-note
mobile: sort assigned tags on top when viewing note tags
2026-02-24 08:28:10 +05:00
Ammar Ahmed
7b0d2a239d Merge pull request #9367 from streetwriters/fix/vault-biometrics-ux
Fix vault with biometrics enabled does not lock the note
2026-02-24 08:27:54 +05:00
Abdullah Atta
5d922d934c Merge branch 'master' into beta 2026-02-21 10:36:37 +05:00
Abdullah Atta
5bab9ea914 Merge pull request #9385 from streetwriters/fix/url-crash-in-editor
Fix web/desktop app crashes
2026-02-21 10:34:29 +05:00
Abdullah Atta
da06d7e636 misc: add stack trace deobfuscation script 2026-02-21 10:30:56 +05:00
Abdullah Atta
aece140473 web: fix crash on rewriting error message 2026-02-21 10:30:39 +05:00
Abdullah Atta
656f87e3c6 editor: fix crash if embed url is invalid or empty 2026-02-21 10:30:18 +05:00
Ammar Ahmed
36a95616d9 mobile: cleanup imports 2026-02-20 08:57:49 +05:00
Ammar Ahmed
60bdc4715d mobile: sort assigned tags on top when viewing note tags 2026-02-20 08:50:08 +05:00
Ammar Ahmed
7da168ab52 Merge pull request #9307 from streetwriters/fix-offline-mode-cache-attachments
mobile: fix switching app from background to foreground starts a new attachment cache download group
2026-02-20 08:12:57 +05:00
Ammar Ahmed
b20ad48b31 mobile: release 3.3.15-beta.0 2026-02-20 08:04:53 +05:00
Ammar Ahmed
8b17495f0e mobile: fix vault with biometrics enabled does not lock note 2026-02-19 13:16:09 +05:00
Abdullah Atta
e113e1b59c web: bump version to 3.3.9-beta.1 2026-02-19 11:38:30 +05:00
Abdullah Atta
6bd4924c0b web: add logging about persistence type 2026-02-19 11:37:59 +05:00
Abdullah Atta
a2939aa183 web: use in memory db only for account recovery routes 2026-02-19 11:34:17 +05:00
Abdullah Atta
effa726b65 web: bump version to 3.3.9-beta.0 2026-02-19 10:43:37 +05:00
Abdullah Atta
20d9bf1f23 core: ensure user is fetched after sign up 2026-02-19 10:42:55 +05:00
Abdullah Atta
09e5d014df web: fix page state persistence on account recovery pages 2026-02-19 10:42:55 +05:00
01zulfi
add1b8a68a editor: support audio attachment playback
Co-authored-by: Hamish <133548095+Hamster45105@users.noreply.github.com>
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-18 11:24:45 +05:00
Ammar Ahmed
fe143f66b7 core: remove log 2026-02-16 15:04:05 +05:00
Ammar Ahmed
75c8ca29ef web: publish vaultLocked event from db eventManager 2026-02-16 14:42:43 +05:00
Ammar Ahmed
c52779df23 Merge branch 'master' into beta 2026-02-16 14:40:45 +05:00
Ammar Ahmed
b7b77646e6 Merge pull request #9357 from streetwriters/mobile/release-3.3.14
mobile: release 3.3.14
2026-02-16 14:36:42 +05:00
01zulfi
1157c6b7cd Merge pull request #9351 from streetwriters/web/server-url-from-environment
web: prioritize environment variables for server urls configuration
2026-02-16 14:28:08 +05:00
Abdullah Atta
ab824d7f66 web: fix server urls 2026-02-16 14:27:50 +05:00
Abdullah Atta
702e28a6e6 Merge pull request #9323 from streetwriters/fix/password-changing
Fix account password changing
2026-02-16 14:15:39 +05:00
Ammar Ahmed
0e0ab58959 mobile: release 3.3.14 2026-02-16 13:39:41 +05:00
Ammar Ahmed
e5cdc9ea3d Merge pull request #9355 from streetwriters/fix/use-db-ev-manager
core: use db event manager
2026-02-16 13:34:31 +05:00
Ammar Ahmed
d3c9744e65 core: use db event manager 2026-02-16 13:11:45 +05:00
Ammar Ahmed
6b02144b30 Merge pull request #9354 from streetwriters/fix-loading-note-bug
mobile: loading note gets stuck
2026-02-16 13:07:27 +05:00
Abdullah Atta
91cbc5ddf0 core: simplify user signup 2026-02-16 13:06:58 +05:00
Ammar Ahmed
93aa1ca5aa Merge pull request #9352 from streetwriters/fix-attachment-upload-error
mobile: fix attachment upload error Failed to delete file
2026-02-16 12:59:15 +05:00
Ammar Ahmed
6ae13ebb2f mobile: fix attachment upload error Failed to delete file 2026-02-16 11:43:55 +05:00
Ammar Ahmed
73cdbf64c7 mobile: fix note loading is slow in editor after login/signup 2026-02-13 14:31:13 +05:00
01zulfi
0aeea965c0 core: add support for bulk delete files
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-13 13:01:56 +05:00
Abdullah Atta
a63b15637f core: fix types 2026-02-13 11:33:09 +05:00
Ammar Ahmed
5153b43bc6 mobile: enable password change and forgot password 2026-02-13 11:27:58 +05:00
Ammar Ahmed
9ea7d09722 core: fix imports 2026-02-13 09:45:08 +05:00
01zulfi
b7b474b245 core: fix archived notes not showing in attachment's linked notes (#9001)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-12 14:19:59 +05:00
ash
e2184a8bdf web: fix text not selectable from entire height of editor (#8972)
Signed-off-by: Ash Holland <x-hv2j5m@keysmash.solutions>
2026-02-12 14:05:49 +05:00
01zulfi
9ea84ca9ae editor: reduce horizontal rule height for large screens (#9040)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-12 14:02:01 +05:00
01zulfi
50136e3689 editor: use last set code language as default (#7086)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-02-12 13:49:52 +05:00
01zulfi
e87f5e5f89 editor: escape html in twitter embed src (#9326)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-10 12:33:56 +05:00
01zulfi
d44792d132 web: align check icon inside note's color in properties panel (#9308)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-09 12:33:28 +05:00
01zulfi
50b01e0b1b web: fix attachment not marked as uploaded when app locked & unlocked (#9306)
When RouteWrapper was unmounted, it would unsub all events including the ones subbed during db initialization (db.init()). Since db isn't initialized again when app is locked & unlocked, those events weren't subbed again. One of these event subscription (fileUploaded event) is responsible for marking an attachment as uploaded. There is no need to unsubscribe all events on RouteWrapper unmount.

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-09 12:32:29 +05:00
Abdullah Atta
122c3a96c7 web: fix account recovery 2026-02-09 12:27:36 +05:00
Abdullah Atta
4d407bd646 web: enable account recovery 2026-02-09 11:10:41 +05:00
Abdullah Atta
16b6b37b94 web: enable password changing 2026-02-09 11:08:00 +05:00
Abdullah Atta
8c766ef4f0 core: add initial DEK support 2026-02-09 11:07:53 +05:00
Ammar Ahmed
b7d2865435 mobile: fix switching app from background to foreground starts
a new attachment cache download group
2026-02-05 13:07:27 +05:00
Ammar Ahmed
8d9df5030c mobile: loading note gets stuck 2026-02-05 11:41:54 +05:00
01zulfi
4a158b2547 core: remove orphaned attachments in trash cleanup method
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-04 13:13:56 +05:00
Ammar Ahmed
af014cf8c1 Merge pull request #9297 from streetwriters/fix-toast-context-color-picker
mobile: fix toast context in color picker
2026-02-03 11:48:33 +05:00
Ammar Ahmed
1ddd7b1e6e mobile: fix toast context in color picker 2026-02-03 11:46:32 +05:00
Ammar Ahmed
9ef0179566 Merge pull request #9296 from streetwriters/fix-notebook-header-error
mobile: fix crash when opening notebook (happens randomly)
2026-02-03 11:11:51 +05:00
Ammar Ahmed
38690d18d4 mobile: fix crash when opening notebook (happens randomly) 2026-02-03 11:10:59 +05:00
Arjun Vijay Prakash
e381e54bb7 web: fix toolbar item indentation when moving between groups (#8624)
* web: adjust item depth calculation during move operation

Signed-off-by: Arjun Vijay Prakash <arjunv.prakash12345@gmail.com>

* web: add tests for customize-toolbar.tsx

Signed-off-by: Arjun Vijay Prakash <arjunv.prakash12345@gmail.com>

* refactor(toolbar): export types and functions in customize-toolbar.tsx

Signed-off-by: Arjun Vijay Prakash <arjunv.prakash12345@gmail.com>

* refactor(tests): simplify and import types for customize-toolbar tests

Signed-off-by: Arjun Vijay Prakash <arjunv.prakash12345@gmail.com>

* refactor(toolbar): remove export from internal functions in customize-toolbar.tsx

Signed-off-by: Arjun Vijay Prakash <arjunv.prakash12345@gmail.com>

---------

Signed-off-by: Arjun Vijay Prakash <arjunv.prakash12345@gmail.com>
2026-02-03 10:21:46 +05:00
little Kitchen
7001f98353 fix: unable to change zoom level back to 1.0 (#9283)
Fixed two issues with number input settings:

1. Added onBlur handler to immediately commit values when user
   clicks away, bypassing the 500ms debounce delay. This ensures
   values are saved even if the user navigates away quickly.

2. Added key prop based on current value to force re-render
   when the stored value changes, ensuring the input always
   reflects the actual persisted value.

Fixes #4035
2026-02-02 13:05:24 +05:00
Abdullah Atta
a836642d74 Merge pull request #7901 from 01zulfi/web/delete-data-for-not-logged-in-user
web: add delete data option for not logged in user
2026-02-02 12:31:01 +05:00
Abdullah Atta
a3d3add48f Merge pull request #7303 from 01zulfi/web/reminder-dialog-improvements
web: reminder dialogs improvements
2026-02-02 12:30:40 +05:00
01zulfi
994060cce2 editor: allow attaching image as file (#8752)
* editor: allow attaching image as file
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: fix image previews for regular attachments

* mobile: allow images to be added as attachment without preview

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

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-02-02 10:26:03 +05:00
Abdullah Atta
75103dc706 Merge pull request #9237 from streetwriters/web/dont-render-properties-on-locked-note
web: don't render properties panel for locked sessions
2026-02-02 09:56:20 +05:00
Abdullah Atta
cf63c2fdc5 Merge branch 'master' into beta 2026-01-28 12:44:31 +05:00
Abdullah Atta
c07f5393f6 Merge pull request #9266 from streetwriters/release/v3.3.8
Release vNext
2026-01-28 12:42:38 +05:00
Abdullah Atta
406c63721d web: bump version to 3.3.8 2026-01-27 17:24:36 +05:00
Abdullah Atta
97f3e02f59 core: remove .only from note history tests 2026-01-27 12:25:39 +05:00
Abdullah Atta
5c6564f00b core: fix monograph sync tests 2026-01-27 12:20:54 +05:00
Ammar Ahmed
605e75efb3 mobile: fix and update tests 2026-01-27 12:02:20 +05:00
Ammar Ahmed
496f9ba340 ci: update build tools version 2026-01-27 10:59:49 +05:00
Ammar Ahmed
f98283f2dd mobile: update changelog 2026-01-27 10:08:17 +05:00
Ammar Ahmed
2d32e85a64 mobile: refactor 2026-01-27 09:59:43 +05:00
Ammar Ahmed
1a56a22a33 mobile: fix feature not locked 2026-01-26 13:40:51 +05:00
Ammar Ahmed
c85204d0c1 mobile: bump version to 3.3.13 2026-01-26 13:32:28 +05:00
Abdullah Atta
1d8a958eaf editor: update test snapshots 2026-01-26 13:07:35 +05:00
Abdullah Atta
b52f70d57d core: fix reminders tests 2026-01-26 12:49:50 +05:00
Chloe Oletto
12687dae7b core: Fix reminder sorting. (#9242)
Signed off by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-01-22 09:26:27 +05:00
Chloe Oletto
c53c3167d8 web: Fix reminder dialog subtext formatting. (#9241)
Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-01-22 09:25:20 +05:00
Ammar Ahmed
f15b4cf634 mobile: show note references in reminder page 2026-01-20 10:17:57 +05:00
01zulfi
6430611e3e web: reminder dialogs improvements
* add note references (if any) in reminder edit dialog
* wrap snooze option buttons in reminder preview dialog

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-20 09:29:52 +05:00
01zulfi
afe57e41ce web: add confirmation dialog for delete data option
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-19 16:18:29 +05:00
Ammar Ahmed
86f3f90143 mobile: delete all data option in settings 2026-01-19 14:24:21 +05:00
01zulfi
d3ea51cc01 web: add delete data option for not logged in user
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-19 12:58:48 +05:00
01zulfi
60df472d53 editor: fix search scroll not working on switching editors or opening new editor (#8061)
* editor: fix search scroll not working on switching editors or opening new editor
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: initialize search right after editor is created
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-19 11:59:48 +05:00
01zulfi
67fe25bc8b common: make command palette keybinding same across web & desktop (#9235)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-17 21:18:52 +05:00
01zulfi
94dbec4486 web: don't render properties panel for locked sessions
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-17 14:04:38 +05:00
01zulfi
44a7aed308 intl: fix strings with '<<< HEAD' in pseudo-LOCALE.po (#9236)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-17 12:32:48 +05:00
Ammar Ahmed
f22937e1eb ci: update build tools version 2026-01-17 10:54:44 +05:00
Ammar Ahmed
c816f8b15b mobile: fix ts errors 2026-01-17 09:39:20 +05:00
01zulfi
fa4f4e7bea web: add command palette button in status bar (#7977)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-16 19:24:21 +05:00
01zulfi
076cea6d6d editor: add scrollbar to color picker (#8065)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-16 19:22:08 +05:00
01zulfi
d951b97bed web: fix getting active note commands in command palette (#9234)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-16 19:20:38 +05:00
Ammar Ahmed
bd888e57e5 mobile: release 3.3.13-beta.1 2026-01-16 14:46:15 +05:00
Abdullah Atta
3ba1234056 core: fix note expiry date index not created 2026-01-16 14:44:53 +05:00
Ammar Ahmed
4c5a668114 global: update deps 2026-01-16 14:31:53 +05:00
Abdullah Atta
06d1891389 web: bump version to 3.3.8-beta.1 2026-01-16 14:27:31 +05:00
Ammar Ahmed
c97a228ec8 Merge branch 'master' into beta 2026-01-16 14:26:17 +05:00
Ammar Ahmed
d11a200ebb Merge pull request #9233 from streetwriters/mobile/fix-web-clip
Fix web clip on mobile does not include url & date clipped
2026-01-16 14:24:46 +05:00
01zulfi
bfedc1f43e editor: replace simplebar with simple css styling (#9085)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-16 14:24:11 +05:00
Ammar Ahmed
b148083750 Merge pull request #9232 from streetwriters/mobile/fix-shortcuts-update
Fix shortcuts do not update in side menu
2026-01-16 14:24:01 +05:00
Ammar Ahmed
cdfd7333cd Merge pull request #9231 from streetwriters/mobile/fix-share-padding
Fix share sheet bottom padding
2026-01-16 14:23:44 +05:00
Ammar Ahmed
2ed32efefb Merge pull request #9210 from streetwriters/fix-editor-features-lock
mobile: fix editor features not updated on login or user change
2026-01-16 14:23:23 +05:00
01zulfi
e3331e6dee web: temporarily disable change&forgot password
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-16 14:21:01 +05:00
Ammar Ahmed
b3406be1ad mobile: disable password changing and forgot password 2026-01-16 14:21:01 +05:00
01zulfi
751d0ca861 editor: fix hidden collapse/expand icon when callout title is empty (#9220)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-16 13:06:43 +05:00
Ammar Ahmed
a614693dfa mobile: fix web clip does not include url & date clipped 2026-01-16 13:00:53 +05:00
01zulfi
c4bbceef67 web: add option to preview attachments in attachment manager (#9186)
* web: add option to preview attachments in attachment manager
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: open pdf preview dialog in full width&height
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: minor ui adjustment for pdf preview dialog

* web: fix pdf text selection not visible

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-01-16 13:00:14 +05:00
Abdullah Atta
d39e7c990d Merge pull request #9192 from streetwriters/core/note-history-title
Add support for saving title in note history sessions
2026-01-16 12:41:49 +05:00
Abdullah Atta
e248dbfa5b intl: update po files 2026-01-16 12:41:19 +05:00
Abdullah Atta
29f789493e web: fix previewing note history where only title has changed 2026-01-16 12:41:12 +05:00
Abdullah Atta
2b0e5c41c8 core: remove note content if history item has no content 2026-01-16 12:40:42 +05:00
Ammar Ahmed
e241b2fe72 mobile: fix showing properties from shortcuts 2026-01-16 11:49:08 +05:00
01zulfi
a91983340c web: support note title from session history in diff viewer
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-16 11:21:52 +05:00
Ammar Ahmed
2aa3aaf2f8 mobile: cleanup 2026-01-16 11:21:52 +05:00
Ammar Ahmed
6b2bf369ff core: ensure session content remains consistent when new session is created
New sessions can be created with title or content only, hence we need to pull the missing data from the note itself to keep session history consistent with previous sessions
2026-01-16 11:21:52 +05:00
Ammar Ahmed
fea5818951 core: update note history tests 2026-01-16 11:21:52 +05:00
Ammar Ahmed
e6eb71a484 core: restoring note history with title 2026-01-16 11:21:52 +05:00
Ammar Ahmed
1e023500be mobile: add support for setting note history session title 2026-01-16 11:21:34 +05:00
Ammar Ahmed
2c078de2c0 core: save note title in note session history item 2026-01-16 11:21:34 +05:00
Abdullah Atta
b373881b8b Merge pull request #9169 from streetwriters/editor/table-export-import-csv
editor: add options to export/import table <> csv
2026-01-16 11:19:10 +05:00
Abdullah Atta
6b37cf0dda intl: update locales 2026-01-16 11:18:02 +05:00
Abdullah Atta
1025c4ec6e editor: minor refactors 2026-01-16 11:17:49 +05:00
Abdullah Atta
c44f9f685d editor: parse csv directly to prosemirror nodes 2026-01-16 11:17:33 +05:00
Ammar Ahmed
5633d4504f mobile: fix shortcuts not updating 2026-01-16 11:10:45 +05:00
Abdullah Atta
c7aa49e541 common: move import/export csv to Pro plan 2026-01-16 10:58:17 +05:00
Ammar Ahmed
ee139463d2 mobile: export table to csv 2026-01-16 10:58:17 +05:00
01zulfi
2cf7f0cc27 editor: add options to export/import table <> csv
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-16 10:58:17 +05:00
Ammar Ahmed
210549445e global: add support for expiring notes (#9167)
* core: expiring notes

* core: add tests

* mobile: support setting not expiry date

* web: support note expiry dates
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: add tests for expiring notes

* core: fix tests

* mobile: delete expiring notes at startup

* core: create index on expiry date

* core: minor refactor

* core: remove `.only` sync test

* web: refactors

* mobile: set limit on expiring notes

* web: improve expiry date menu option && note item ui
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: add premium check for setting expiry for notes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: move note expiry date dialog into its own file && minor changes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: delete expired notes on startup

* web: minor refactors

---------

Co-authored-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-01-16 10:55:40 +05:00
Ammar Ahmed
5b85158a7c mobile: fix share sheet bottom padding 2026-01-16 10:38:55 +05:00
01zulfi
09cba1a1df editor: fix youtube embeds erroring with code 153 (#9180)
* web: add referer header with content origin
* youtube embeds aren't working without adding this header

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

* editor: use corsHost for youtube embeds

* editor: make youtube embed detection more robust

* web: remove referrer meta tag

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-01-15 19:22:46 +05:00
01zulfi
d75e2e85c9 desktop: fix start minimized on autostartup (#8492)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-15 10:41:36 +05:00
01zulfi
966673dfd1 editor: add clear current node keyboard shortcut (#7272)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-15 10:38:33 +05:00
Abdullah Atta
b8ad9265b9 Merge pull request #5726 from streetwriters/feature/conflict-on-locked-notes
Add support for showing conflicts on locked notes
2026-01-15 10:36:07 +05:00
Ammar Ahmed
b2f1fda641 mobile: show merge conflict on locked notes 2026-01-15 10:35:36 +05:00
Abdullah Atta
1544b656e0 web: fix conflict on locked note auto resolving 2026-01-15 10:34:39 +05:00
Abdullah Atta
d97314c484 core: enable conflicts on locked notes 2026-01-15 10:34:39 +05:00
Ammar Ahmed
1432553897 mobile: fix editor features not updated on login 2026-01-15 09:29:48 +05:00
Abdullah Atta
f585fb0381 Merge pull request #9159 from streetwriters/web/change-note-created-date
web: allow editing note's created date from properties panel
2026-01-14 22:06:15 +05:00
01zulfi
98557be52d web: replace note creation date edit popover with dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-14 14:14:14 +05:00
Ammar Ahmed
e7da0a35d3 mobile: limit changing created date to note's edited date 2026-01-14 14:13:42 +05:00
Ammar Ahmed
965a7f91ed mobile: support changing date-created 2026-01-14 14:13:42 +05:00
01zulfi
2b1c973177 web: allow editing note's created date from properties panel
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-14 14:13:42 +05:00
Abdullah Atta
7e87cf7fdc Merge pull request #9170 from streetwriters/editor/fix-line-height-css-issues
editor: fix icons not aligning in outline,task,check lists & callouts because of custom line height
2026-01-14 13:52:36 +05:00
Ammar Ahmed
c8cd5a4dc8 mobile: fix line height not applied to paragraphs 2026-01-14 13:36:37 +05:00
Abdullah Atta
d00f7f6255 Merge pull request #9081 from streetwriters/fix/improve-sync-reliability
Improve sync reliability
2026-01-14 11:48:34 +05:00
01zulfi
e8690e3419 web: fix toc headings alignment (#9143)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-14 11:48:04 +05:00
Abdullah Atta
72ff45c722 Merge pull request #9182 from streetwriters/web/week-format
web: add setting to change week format
2026-01-14 11:40:26 +05:00
01zulfi
22a6271423 editor: fix duplicate block-id on splitting node (#9198)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-14 10:56:31 +05:00
01zulfi
a6032ffd72 web: fix empty command palette/quick open when locked note active (#9200)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-14 10:53:00 +05:00
01zulfi
69ea9992ce web: enable toc toggle on new notes (#9201)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-14 10:52:24 +05:00
Ammar Ahmed
36b4d69c2d mobile: week format settings 2026-01-09 11:22:03 +05:00
01zulfi
2129533aa9 web: add setting to change week format
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-08 15:49:13 +05:00
Abdullah Atta
e635ace338 Merge pull request #9174 from streetwriters/fix/theme-server-crashing
Fix theme server crashing on fetching total installs
2026-01-08 01:03:18 +05:00
Abdullah Atta
e729086015 server(themes): bump version to 1.0.4 2026-01-08 01:02:25 +05:00
Abdullah Atta
3fbb7ce548 server(themes): fix crash when fetching install count 2026-01-08 01:01:59 +05:00
01zulfi
e0c31d6fdd editor: fix icons not aligning in outline,task,check lists & callouts because of custom line height
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-07 14:05:57 +05:00
Ammar Ahmed
d19c5b9775 mobile: release 3.3.13-beta.0 2026-01-07 10:00:58 +05:00
Abdullah Atta
f01b88083f Merge branch 'master' into beta 2026-01-07 09:40:38 +05:00
Abdullah Atta
d3ab1d1d2a web: bump version to 3.3.8-beta.0 2026-01-07 09:39:11 +05:00
Ammar Ahmed
7855442d33 Merge pull request #9161 from streetwriters/mobile-multipart-uploads
mobile: add support for multipart uploads
2026-01-06 13:56:42 +05:00
Ammar Ahmed
bbaa6c76b4 mobile: add support for multipart uploads
Files >100MB fail currently using simple file upload methods.
2026-01-06 13:08:53 +05:00
Abdullah Atta
cf71809f6c Merge pull request #9132 from streetwriters/fix/themes-not-instaling
Fix themes not installing
2026-01-03 14:40:18 +05:00
Abdullah Atta
3b7d0ddb36 server(themes): remove _test key suffix 2026-01-03 14:40:06 +05:00
Abdullah Atta
482fea91f3 server(themes): bump version to 1.0.3 2026-01-03 14:39:20 +05:00
Abdullah Atta
4bace10bab server(themes): make counter increment faster 2026-01-03 14:38:45 +05:00
Abdullah Atta
603b7da9e6 server(theme): update theme's install count immediately on install 2026-01-03 14:38:00 +05:00
Abdullah Atta
bf4d168c96 server(theme): sort themes by total installs 2026-01-03 14:37:19 +05:00
Abdullah Atta
756bb82fb7 server(themes): sync themes every 1 hour (roughly) 2026-01-03 14:36:41 +05:00
Abdullah Atta
81b1005b02 Merge pull request #9128 from streetwriters/fix/themes-server-fetch-perf
Fix themes server fetch perf
2026-01-02 13:04:40 +05:00
Abdullah Atta
7ba52d1358 server(themes): bump version to 1.0.2 2026-01-02 13:03:36 +05:00
Abdullah Atta
fcfb2c7e01 server(themes): fetch installs once on sync instead of on every fetch 2026-01-02 13:03:19 +05:00
Abdullah Atta
fa0a5afa63 Merge pull request #9113 from streetwriters/editor/collapsible-headings-perf
editor: improve collapsible heading performance
2026-01-02 09:43:06 +05:00
01zulfi
b881b236a0 editor: export & use AttributeUpdate interface
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-01 15:23:27 +05:00
Ammar Ahmed
ec9c40bd64 Merge pull request #9125 from streetwriters/fix-subscription-wording
mobile: fix wrong trial end date shown in settings
2026-01-01 12:53:04 +05:00
Ammar Ahmed
f0f7857355 mobile: fix wrong trial end date shown in settings 2026-01-01 12:51:05 +05:00
Abdullah Atta
9c59cbbc40 Merge pull request #8944 from streetwriters/web/fix-resizing-editor
web: fix editor & note title resizing when window resizes
2026-01-01 11:19:40 +05:00
Abdullah Atta
6dce41a070 web: fix editor & note title resizing when window resizes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-01 11:19:23 +05:00
Abdullah Atta
dffeb05f86 Merge pull request #8488 from 01zulfi/global/day-format-setting
global: support day format options & $day$ title format & /day command
2026-01-01 10:51:53 +05:00
Abdullah Atta
ecfbfd83b9 Merge branch 'master' into beta 2026-01-01 10:44:45 +05:00
Ammar Ahmed
0aea1deea0 Merge pull request #9124 from streetwriters/fix-invalid-file-crash-on-restore
mobile: fix app crash when trying to restore an invalid file
2026-01-01 10:26:28 +05:00
Ammar Ahmed
f041b92360 mobile: fix app crash when trying to restore an invalid file 2026-01-01 10:15:54 +05:00
Abdullah Atta
64074030dd Merge pull request #9123 from streetwriters/fix/make-themes-host-configurable
Make themes server host configurable
2026-01-01 09:48:49 +05:00
Abdullah Atta
3387dda22b server(themes): bump version to 1.0.1 2026-01-01 09:46:50 +05:00
Abdullah Atta
534b810350 server(themes): make themes server port configurable 2026-01-01 09:46:25 +05:00
01zulfi
50666c1307 editor: add a collapsible heading test case
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-31 15:33:21 +05:00
01zulfi
200c9bd9eb editor: improve checks to not unecessary call toggle fn in heading update plugin
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-31 15:01:14 +05:00
Ammar Ahmed
2f93c6fdc8 mobile: support day format options 2025-12-31 14:57:16 +05:00
01zulfi
5c21c0a498 global: support day format options & $day$ title format & /day command
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-31 14:54:44 +05:00
Abdullah Atta
93c07d6842 Merge pull request #9122 from streetwriters/fix/themes-server-ci
Fix themes server docker image build & publish
2025-12-31 12:09:19 +05:00
Abdullah Atta
5d9f18ed77 ci: set build context to root for themes server 2025-12-31 12:08:11 +05:00
Abdullah Atta
313d47d9ff Merge pull request #8108 from StLyn4/docker-themes-server
docker: theme server
2025-12-31 11:57:11 +05:00
Abdullah Atta
304a9f3218 ci: add workflow for publishing theme server 2025-12-31 11:56:15 +05:00
Abdullah Atta
a160725acb server(themes): use bun in production 2025-12-31 11:53:22 +05:00
Abdullah Atta
05535b50da server(themes): no need for default env 2025-12-31 11:38:57 +05:00
Abdullah Atta
70fdcf2b44 server(theme): add support for cloudflare kv for counter persistance 2025-12-31 11:08:27 +05:00
Ammar Ahmed
67d47c283e mobile: fix default type homepage not loading 2025-12-31 09:42:53 +05:00
Ammar Ahmed
dd59aab328 Merge branch 'master' into beta 2025-12-31 09:34:23 +05:00
Ammar Ahmed
509b308419 Merge pull request #9119 from streetwriters/fix-navigation-unresponsive-ios
mobile: add simple tab bar implementation
2025-12-31 09:23:45 +05:00
01zulfi
7a12fd701e editor: batch update hidden attribute when toggling headings
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-30 16:53:02 +05:00
01zulfi
9396a06a89 core: support customizable monograph title (#8491)
* core: support customizable monograph title
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* mobile: set custom title for monograph

* web: minor ui changes

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2025-12-30 14:29:01 +05:00
Ammar Ahmed
9d9c3b7db5 mobile: add simple tab bar implementation
This fixes most bugs faced by users in side-menu on ios like swipe gesture working when it is disabled and navigation screen stuck on older ios versions.
2025-12-30 14:23:39 +05:00
Abdullah Atta
4cfb10db7c Merge pull request #8284 from 01zulfi/web/editor-line-height-setting
web: add editor line height setting
2025-12-30 14:09:26 +05:00
Abdullah Atta
50024b62cb intl: update locales 2025-12-30 14:08:30 +05:00
Ammar Ahmed
f31084baa7 mobile: configurable default line height setting 2025-12-30 14:07:45 +05:00
01zulfi
2688becb05 web: add editor line height setting
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-30 14:07:44 +05:00
01zulfi
8298900f76 editor: support ctrl+c for images (#8386)
* editor: support ctrl+c for images
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: add test for copy image functionality
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-30 13:46:24 +05:00
01zulfi
b0c18a8ece web: allow saving note from status icon click & tab menu (#8316)
* web: allow saving note from status icon click & tab menu
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: directly save content instead of using events

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2025-12-30 13:27:10 +05:00
Abdullah Atta
9659a12d70 Merge pull request #9117 from streetwriters/fix/monograph-ui
Fix monograph ui
2025-12-30 10:06:31 +05:00
Abdullah Atta
6abc25f58a monograph: bump version to 1.2.7 2025-12-30 10:05:51 +05:00
Abdullah Atta
a164ac67ce monograph: fix monograph ui 2025-12-30 10:04:27 +05:00
01zulfi
255677d98e editor: improve headingUpdatePlugin performance
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-29 14:23:16 +05:00
Ammar Ahmed
2a92ea4efd mobile: update export options 2025-12-29 10:25:04 +05:00
Ammar Ahmed
608b4f3009 mobile: update ios provisioning 2025-12-29 10:25:04 +05:00
Ammar Ahmed
a69cb1bf9c mobile: release 3.3.12 2025-12-29 10:25:04 +05:00
Abdullah Atta
be40b3d160 monograph: fix build 2025-12-29 10:25:04 +05:00
Abdullah Atta
51147b17cd monograph: bump version to 1.2.6 2025-12-29 10:25:04 +05:00
Abdullah Atta
5d0c2ae0b9 monograph: add support for reading secrets from file 2025-12-29 10:25:04 +05:00
Ammar Ahmed
8435e69cb5 Merge pull request #9110 from streetwriters/mobile-release/3.3.12
Mobile release/3.3.12
2025-12-29 10:24:05 +05:00
Ammar Ahmed
1d6a33e3be mobile: update export options 2025-12-27 11:47:31 +05:00
Ammar Ahmed
3f4d78af7d mobile: update ios provisioning 2025-12-27 10:19:31 +05:00
Abdullah Atta
f94012d184 Merge pull request #9103 from streetwriters/fix/monograph-build
monograph: fix build
2025-12-27 00:40:06 +05:00
Abdullah Atta
ca73c0c2ce monograph: fix build 2025-12-27 00:39:13 +05:00
Abdullah Atta
742bf31038 Merge pull request #9102 from streetwriters/feat/monograph/secrets-support
Add support for reading secrets from file
2025-12-27 00:28:45 +05:00
Abdullah Atta
1369b64cb1 monograph: bump version to 1.2.6 2025-12-27 00:27:15 +05:00
Abdullah Atta
bc3488512f monograph: add support for reading secrets from file 2025-12-27 00:26:51 +05:00
Ammar Ahmed
51f87911e1 mobile: release 3.3.12 2025-12-26 14:37:49 +05:00
Ammar Ahmed
4d4af5c278 Merge branch 'master' into beta 2025-12-26 11:24:47 +05:00
Ammar Ahmed
a6fcff4d86 Merge pull request #9099 from streetwriters/fix-file-errors
Fix bugs in file picker, pdf preview, backup restoration and vault unlock with biometrics
2025-12-26 11:23:39 +05:00
Ammar Ahmed
50b7f9eb68 mobile: refactor 2025-12-26 11:11:57 +05:00
Ammar Ahmed
a81c534258 mobile: fix ios crash when using biometrics 2025-12-25 22:03:20 +05:00
Ammar Ahmed
67cd2e6504 mobile: fix keyboard opens on app launch if locked note is opened in current tab 2025-12-25 15:25:49 +05:00
Ammar Ahmed
ed5c84b069 mobile: cleanup downloaded cache files on startup 2025-12-25 13:52:52 +05:00
Ammar Ahmed
f6c442ba88 mobile: fix pdf previews not working on ios 2025-12-25 13:51:13 +05:00
Ammar Ahmed
19e59b778d mobile: fix picking files on ios and android 2025-12-25 13:47:35 +05:00
Ammar Ahmed
0643a74ade Merge pull request #9084 from streetwriters/fix-notification-notes
mobile: fix loading note in editor from notifications
2025-12-25 11:41:39 +05:00
Ammar Ahmed
1babbb567c Merge pull request #9086 from streetwriters/fix-sidebar-gesture-close
mobile: fix sidebar close gesture on ios
2025-12-25 11:41:25 +05:00
Ammar Ahmed
11899501f2 Merge pull request #9087 from streetwriters/fix-color-picker-wheel-image
mobile: fix color picker wheel images not loaded
2025-12-25 11:41:02 +05:00
Ammar Ahmed
9982d7b7b4 Merge pull request #9088 from streetwriters/feat-home-shortcuts
Feat Android home launcher shortcuts
2025-12-25 11:40:27 +05:00
Ammar Ahmed
22497199c5 mobile: catch errors 2025-12-25 11:40:05 +05:00
Ammar Ahmed
873b180dab mobile: enable feature for pro users 2025-12-25 11:37:27 +05:00
Ammar Ahmed
ca8e505cd0 mobile: add support for color shortcuts on homescreen 2025-12-24 14:17:52 +05:00
Ammar Ahmed
465e1af838 mobile: keep shortcuts synced with the app 2025-12-24 13:11:08 +05:00
Ammar Ahmed
a829594fad mobile: fix color picker wheel images not loaded 2025-12-23 13:57:57 +05:00
Ammar Ahmed
1427518e4b mobile: fix sidebar close gesture on ios 2025-12-23 13:43:15 +05:00
01zulfi
41094c80d5 editor: add move line up/down keybinding (#7078)
* editor: add move line up/down keybinding
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* docs: update keyboard shortcuts

* editor: add missing changes

* editor: add move parent node up/down keyboard shortcut
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: refactor moveNode fn && add a test case
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: improve move node logic
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2025-12-23 12:39:30 +05:00
Abdullah Atta
09b12c6d7e Merge pull request #9083 from streetwriters/release/3.3.7
Release v3.3.7
2025-12-23 12:33:43 +05:00
Abdullah Atta
c560685285 web: bump version to 3.3.7 2025-12-23 12:15:04 +05:00
Abdullah Atta
48d2099308 Merge pull request #9082 from streetwriters/fix/checklist-txt-export 2025-12-23 12:08:21 +05:00
Abdullah Atta
ca3604129d Merge pull request #9082 from streetwriters/fix/checklist-txt-export
Fix check list text export
2025-12-23 12:05:21 +05:00
Abdullah Atta
583a68cabe core: fix merger tests 2025-12-23 11:53:43 +05:00
Abdullah Atta
924eaf8486 core: fix check list text export 2025-12-23 11:33:14 +05:00
Abdullah Atta
9c1a02dbd5 mobile: use db's event manager instead of global 2025-12-23 11:04:06 +05:00
Abdullah Atta
637a19ee21 web: use db's event manager instead of global 2025-12-23 11:03:59 +05:00
Abdullah Atta
896420064f core: improve sync reliability 2025-12-23 11:03:36 +05:00
01zulfi
f6140e8396 editor: fix crash on task list toggle & drag (#9078)
* make sure column resizing's handleMouseDown correctly checks for handle element

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-23 10:55:28 +05:00
Ammar Ahmed
b59a3fb5de mobile: improved navigation for deep links 2025-12-23 10:54:46 +05:00
Ammar Ahmed
0a7f8631b6 mobile: add home shortcuts
Add ability to create shortcuts on launcher homescreen for notes, notebooks and tags.
2025-12-22 18:59:36 +05:00
Ammar Ahmed
de37cb0da1 Merge branch 'master' into beta 2025-12-19 10:48:12 +05:00
Ammar Ahmed
167ce97dd9 Merge branch 'mobile-release/3.3.10' into beta 2025-12-18 09:47:30 +05:00
Abdullah Atta
a218ee1c02 Merge branch 'release/3.3.6' into beta 2025-12-18 00:50:54 +05:00
Vsevolod Volkov
c6d471cec9 docker: ability to build container with theme server 2025-11-26 19:24:04 +01:00
412 changed files with 16284 additions and 69015 deletions

30
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,30 @@
## Description
<!-- Add a detailed summary of what this feature/bugfix does -->
## Type of Change
- [ ] Bug fix
- [ ] Feature
## Visuals
- [ ] Attached relevant screenshots / screen recording / GIF
- [ ] N/A (not a feature or no UI changes)
## Testing
- [ ] Ran all E2E tests
- [ ] Ran all integration tests
- [ ] Added/updated tests for this change (if needed)
- [ ] N/A (tests not needed — explanation provided below)
### If tests were not added, explain why
<!-- explanation -->
## Platform
<!-- Describe which platforms this PR is related to -->
- [ ] Web
- [ ] Mobile
- [ ] Desktop
## Sign-off
- [ ] QA passed
- [ ] UI/UX passed

View File

@@ -113,7 +113,7 @@ jobs:
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
BUILD_TOOLS_VERSION: "36.0.0"
- name: Build apks for Github release
run: yarn release:android
@@ -128,7 +128,7 @@ jobs:
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
BUILD_TOOLS_VERSION: "36.0.0"
- name: Rename apk files
run: |

View File

@@ -113,7 +113,7 @@ jobs:
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
BUILD_TOOLS_VERSION: "36.0.0"
- name: Build apks for Github release
run: yarn release:android
@@ -128,7 +128,7 @@ jobs:
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
BUILD_TOOLS_VERSION: "36.0.0"
- name: Rename apk files
run: |

78
.github/workflows/themes.publish.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: Publish @notesnook/themes-server
on: workflow_dispatch
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-22.04
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Collect package metadata
id: package_metadata
working-directory: ./servers/themes
run: |
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
# Setup Buildx
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
ecr: auto
logout: true
# Pull previous image from docker hub to use it as cache to improve the image build time.
- name: docker pull cache image
continue-on-error: true
run: docker pull streetwriters/themes-server:latest
# Setup QEMU
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: streetwriters/themes-server
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: servers/themes/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: streetwriters/themes-server:${{ steps.package_metadata.outputs.app_version }},streetwriters/themes-server:latest
cache-from: streetwriters/themes-server:latest
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: index.docker.io/streetwriters/themes-server
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.6",
"version": "3.3.10",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -113,7 +113,8 @@ export class SQLite {
};
}
} catch (e) {
if (e instanceof Error) e.message += ` (query: ${sql})`;
if (e instanceof Error)
throw rewriteError(e, `${e.message} (query: ${sql})`);
throw e;
} finally {
// Since SQLite 3.48.0 (SQLite3MC v2.0.2) it's not possible to load fts5
@@ -212,3 +213,11 @@ function getExtensionPath(extensionName: string, entryPoint: string) {
}
return loadablePath;
}
function rewriteError(e: Error, message: string) {
const error = new Error(message);
error.stack = e.stack;
error.name = e.name;
error.cause = e.cause;
return error;
}

View File

@@ -34,6 +34,17 @@ export const windowRouter = t.router({
}),
maximized: t.procedure.query(() => globalThis.window?.isMaximized()),
fullscreen: t.procedure.query(() => globalThis.window?.isFullScreen()),
onClose: t.procedure.subscription(() => {
return observable<void>((emit) => {
function listener() {
emit.next();
}
globalThis.window?.addListener("close", listener);
return () => {
globalThis.window?.removeListener("close", listener);
};
});
}),
onWindowStateChanged: t.procedure.subscription(() => {
return observable<{ maximized: boolean; fullscreen: boolean }>((emit) => {
function listener() {

View File

@@ -52,6 +52,9 @@ locale.then(({ default: locale }) => {
});
setI18nGlobal(i18n);
const appHostnames = isDevelopment()
? ["localhost", "127.0.0.1"]
: ["app.notesnook.com"];
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
console.log("Another instance is already running!");
@@ -173,6 +176,19 @@ async function createWindow() {
return { action: "deny" };
});
mainWindow.webContents.on("will-navigate", (event, url) => {
try {
const parsedUrl = new URL(url);
if (!appHostnames.includes(parsedUrl.hostname)) {
event.preventDefault();
shell.openExternal(url);
}
} catch (e) {
console.error("will-navigate: failed to parse URL", url, e);
event.preventDefault();
}
});
nativeTheme.on("updated", () => {
setupTray();
setupJumplist();

View File

@@ -39,7 +39,7 @@ const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
"autostart"
);
const STARTUP_ARGS = ["--hidden"];
const HIDDEN_ARG = "--hidden";
export class AutoLaunch {
static enable(hidden: boolean) {
@@ -54,13 +54,13 @@ export class AutoLaunch {
);
} else {
const loginItemSettings = app.getLoginItemSettings({
args: STARTUP_ARGS
args: hidden ? [HIDDEN_ARG] : undefined
});
if (loginItemSettings.openAtLogin) return;
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: hidden,
args: STARTUP_ARGS
args: hidden ? [HIDDEN_ARG] : undefined
});
}
}

View File

@@ -3,10 +3,8 @@
</p>
<h1 align="center">Notesnook Mobile</h1>
<h3 align="center">The mobile app is built using React Native, Typescript & Javascript for both iOS & Android.</h3>
<p align="center">
<a href="#developer-guide">Developer guide</a> | <a href="#build-instructions">How to build?</a>
</p>
<h3 align="center">The mobile app is built with React Native for both iOS and Android.</h3>
<p align="center"><a href="#build-instructions">Build instructions</a> | <a href="#developer-guide">Developer guide</a> | <a href="#running-e2e-tests-detox">E2E tests</a></p>
<p align="center">
<a href="https://play.google.com/store/apps/details?id=com.streetwriters.notesnook">
@@ -25,21 +23,21 @@
Requirements:
1. [Node.js](https://nodejs.org/en/download/)
1. [Node.js](https://nodejs.org/en/download/) 20+ (the repo is pinned to Node `22.20.0` via Volta)
2. [git](https://git-scm.com/downloads)
3. NPM (not yarn or pnpm)
4. [React Native](https://reactnative.dev/docs/environment-setup)
3. `npm`
4. [React Native environment setup](https://reactnative.dev/docs/set-up-your-environment)
To run the app locally, you will need to setup React Native on your system:
To run the app locally, first complete React Native native tooling setup:
1. Open the official [environment setup guide here](https://reactnative.dev/docs/environment-setup)
1. Open [React Native environment setup](https://reactnative.dev/docs/set-up-your-environment)
2. Select `React Native CLI Quickstart`
3. Select your OS & the platform to run the app on (iOS or Android)
3. Select your OS and target platform(s): iOS and/or Android
4. Follow the steps listed.
> Please keep in mind that **Expo is not supported**.
> Expo is not used in this project.
Once you have completed the setup, the first step is to `clone` the monorepo:
Clone the monorepo:
```bash
git clone https://github.com/streetwriters/notesnook.git
@@ -48,26 +46,27 @@ git clone https://github.com/streetwriters/notesnook.git
cd notesnook
```
Once you are inside the `./notesnook` directory, run the preparation step:
Install dependencies and bootstrap the mobile workspace:
```bash
# this might take a while to complete
npm install
npm run bootstrap -- --scope=mobile
```
### Running the app on Android
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
[Set up an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) (or connect a physical device), then run:
```bash
npm run start:android
```
If you want to run the app on your phone, make sure to [enable USB debugging](https://developer.android.com/studio/debug/dev-options).
If you are using a physical device, enable [USB debugging](https://developer.android.com/studio/debug/dev-options).
### Running the app on iOS
To run the app on iOS:
Install CocoaPods dependencies first, then run the iOS app:
```bash
# this might take a while to complete
@@ -76,90 +75,87 @@ npm run prepare:ios
npm run start:ios
```
### Useful development commands
```bash
# start Metro only
npm run start:metro
# start Re.Pack bundler
npm run start:repack
```
## Developer guide
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
> The mobile app is a mixed TypeScript/JavaScript codebase.
### The tech stack
We try to keep the stack as lean as possible:
1. React Native
2. Typescript/Javascript
3. Zustand: State management
4. Detox: Runs all our e2e tests
5. React Native MMKV: Database & persistence
6. libsodium: Encryption
1. React Native `0.82`
2. React `19`
3. TypeScript + JavaScript
4. Zustand (state management)
5. Detox (end-to-end testing)
6. libsodium (encryption)
### Project structure
The app codebase is distributed over two primary directories. `native/` and `app/`.
Top-level directories in `apps/mobile/`:
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
- `app/`: Main React Native app source (`components`, `common`, `hooks`, `navigation`, `screens`, `services`, `stores`, `utils`, etc.)
- `android/`: Android native project
- `ios/`: iOS native project
- `e2e/`: Detox test suite and config
- `patches/`: `patch-package` patches
- `scripts/`: Mobile-specific scripts
- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
- `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
- `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
- `hooks/`: Hooks for different app logic
- `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
- `screens`: Navigator screens.
- `services`: Parts of code that do a specific function. For example, the `sync` service runs Sync from anywhere in the app.
- `stores`: We use `zustand` for global state management in the app. Multiple stores provide the state for different parts of the app.
- `utils`: General purpose stuff such as constant values, utility functions, etc.
## Running E2E tests (Detox)
There are several other folders at the root:
Detox device defaults in this repo:
- `share/`: Code for the iOS Share Extension and Android widget.
- `e2e/`: Detox End to end tests
- `patches/`: Patches for various react native dependencies.
### Running the tests
When you are done making the required changes, you must run the tests to ensure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
- Android emulator: `Pixel_5_API_36`
- iOS simulator: `iPhone 17 Pro Max`
### Android
To run the tests on Android, you will need to create an emulator device on your system:
Build and run Android Detox tests:
```
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
```
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
Once you have created an emulator device, build the Android apks:
```
```bash
npm run build:android
```
Finally, run the tests:
```
npm run test:android
```
For debug configuration:
```bash
npm run build:android:debug
npm run start:metro
npm run test:android:debug
```
### iOS
To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
Build and run iOS Detox tests:
```bash
npm run build:ios
npm run test:ios
```
If simulator tooling is missing, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
```bash
brew tap wix/brew
brew install applesimutils
```
Now build the iOS app for testing:
## Release commands
```
npm run build:ios
```
Finally, run the tests:
Android release helpers:
```bash
npm run release:android
npm run release:android:bundle
```
npm run test:ios
```
All tests on iOS are configured to run on `iPhone 8` simulator.

View File

@@ -125,7 +125,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3087
versionCode 3096
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -39,28 +39,32 @@ public class BootTaskService extends HeadlessJsTaskService {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("com.streetwriters.notesnook",
"Default",
NotificationManager.IMPORTANCE_DEFAULT);
try {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("com.streetwriters.notesnook",
"Default",
NotificationManager.IMPORTANCE_DEFAULT);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
notification = new NotificationCompat.Builder(this, channel.getId())
.setContentTitle("Sync on boot")
.setContentText("")
.setSmallIcon(R.drawable.ic_stat_name)
.build();
if (android.os.Build.VERSION.SDK_INT >= 34) {
this.startForeground(
1,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else {
this.startForeground(
1,
notification);
}
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
notification = new NotificationCompat.Builder(this, channel.getId())
.setContentTitle("Sync on boot")
.setContentText("")
.setSmallIcon(R.drawable.ic_stat_name)
.build();
if (android.os.Build.VERSION.SDK_INT >= 34) {
this.startForeground(
1,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else {
this.startForeground(
1,
notification);
}
} catch (Exception ignored) {
stopSelf(startId);
}
return super.onStartCommand(intent, flags, startId);

View File

@@ -6,7 +6,14 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
@@ -29,6 +36,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class RCTNNativeModule extends ReactContextBaseJavaModule {
@@ -237,6 +245,221 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void addShortcut(final String id, final String type, final String title, final String description, final String color, Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
return;
}
try {
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
promise.reject("ERROR", "ShortcutManager not available");
return;
}
String uri = "https://app.notesnook.com/open_" + type + "?id=" + id;
Intent intent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(uri));
intent.setPackage(mContext.getPackageName());
Icon icon = createLetterIcon(type, title, color);
ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, id)
.setShortLabel(title)
.setLongLabel(description != null && !description.isEmpty() ? description : title)
.setIcon(icon)
.setCategories(Set.of(type))
.setIntent(intent)
.build();
shortcutManager.requestPinShortcut(shortcut, null);
promise.resolve(true);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void removeShortcut(final String id, Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
return;
}
try {
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
promise.reject("ERROR", "ShortcutManager not available");
return;
}
shortcutManager.disableShortcuts(java.util.Collections.singletonList(id));
promise.resolve(true);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void updateShortcut(final String id, final String type, final String title, final String description,final String color, Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
return;
}
try {
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
promise.reject("ERROR", "ShortcutManager not available");
return;
}
// Get existing shortcut to preserve icon and intent
List<ShortcutInfo> shortcuts = shortcutManager.getPinnedShortcuts();
ShortcutInfo existingShortcut = null;
for (ShortcutInfo s : shortcuts) {
if (s.getId().equals(id)) {
existingShortcut = s;
break;
}
}
if (existingShortcut == null) {
return;
}
Icon icon = createLetterIcon(type, title, color);
ShortcutInfo updatedShortcut = new ShortcutInfo.Builder(mContext, id)
.setShortLabel(title)
.setIcon(icon)
.setLongLabel(description != null && !description.isEmpty() ? description : title)
.setIntent(existingShortcut.getIntent())
.build();
shortcutManager.updateShortcuts(java.util.Collections.singletonList(updatedShortcut));
promise.resolve(true);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void removeAllShortcuts(Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
return;
}
try {
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
promise.reject("ERROR", "ShortcutManager not available");
return;
}
List<ShortcutInfo> pinnedShortcuts = shortcutManager.getPinnedShortcuts();
if (!pinnedShortcuts.isEmpty()) {
List<String> ids = new ArrayList<>();
for (ShortcutInfo shortcut : pinnedShortcuts) {
ids.add(shortcut.getId());
}
shortcutManager.disableShortcuts(ids);
}
promise.resolve(true);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void getAllShortcuts(Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
WritableArray shortcuts = Arguments.createArray();
List<ShortcutInfo> infos = shortcutManager.getPinnedShortcuts();
for (ShortcutInfo info: infos) {
WritableMap data = Arguments.createMap();
data.putString("id", info.getId());
if (info.getShortLabel() != null) {
data.putString("title", info.getShortLabel().toString());
}
if (info.getLongLabel() != null) {
data.putString("description", info.getLongLabel().toString());
}
if (!Objects.requireNonNull(info.getCategories()).isEmpty()) {
if (info.getCategories().contains("note")) {
data.putString("type", "note");
} else if (info.getCategories().contains("notebook")) {
data.putString("type", "notebook");
} else if (info.getCategories().contains("tag")) {
data.putString("type", "tag");
} else if (info.getCategories().contains("color")) {
data.putString("type", "color");
}
}
shortcuts.pushMap(data);
}
promise.resolve(shortcuts);
}
private Icon createLetterIcon(String type, String title, String colorCode) {
String letter = type.contains("tag") ? "#" : title != null && !title.isEmpty()
? title.substring(0, 1).toUpperCase()
: "?";
int color = type.equals("color") ? Color.parseColor(colorCode) : getColorForLetter(letter);
if (type.equals("notebook")) return Icon.createWithResource(mContext, R.drawable.ic_notebook);
// Use a larger canvas and fill it completely to avoid white borders from launcher masking.
int iconSize = 256;
Bitmap bitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (type.equals("color")) {
Paint backgroundPaint = new Paint();
backgroundPaint.setColor(color);
backgroundPaint.setAntiAlias(true);
canvas.drawCircle(iconSize / 2, iconSize / 2, iconSize/2, backgroundPaint);
} else {
Paint textPaint = new Paint();
textPaint.setColor(color);
textPaint.setTextSize(130);
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTypeface(android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD));
float x = iconSize / 2f;
float y = (iconSize / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f);
canvas.drawText(letter, x, y, textPaint);
}
return Icon.createWithBitmap(bitmap);
}
private int getColorForLetter(String letter) {
int[] colors = {
0xFF1976D2, // Blue
0xFFD32F2F, // Red
0xFF388E3C, // Green
0xFFF57C00, // Orange
0xFF7B1FA2, // Purple
0xFF0097A7, // Cyan
0xFFC2185B, // Pink
0xFF455A64, // Blue Grey
0xFF6A1B9A, // Deep Purple
0xFF00796B, // Teal
0xFF512DA8, // Indigo
0xFF1565C0 // Dark Blue
};
int hash = Math.abs(letter.hashCode());
return colors[hash % colors.length];
}
}

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#008837"
android:alpha="1">
<path
android:fillColor="@android:color/white"
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,160Q720,160 720,160Q720,160 720,160L640,160L640,440L540,380L440,440L440,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM440,440L540,380L640,440L640,440L540,380L440,440Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

View File

@@ -1,6 +1,8 @@
- Get an overview of your year with Notesnook with Wrapped 2025
- Cell selection and column resizing in tables
- Added support for uploading large files in background
- Bug fixes and minor improvements
- Add support audio playback in editor
- Sort tags assigned to a note on top when viewing and managing note tags
- Fix account password changing and account recovery
- Fix title is editable in readonly mode
- Allow attaching image as file
- Minor bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -24,7 +24,7 @@ import {
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { Appearance, I18nManager, StatusBar } from "react-native";
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
@@ -43,7 +43,7 @@ import { changeSystemBarColors, useThemeStore } from "./stores/use-theme-store";
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";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
@@ -54,13 +54,18 @@ if (appLockEnabled || appLockMode !== "none") {
RNBootSplash.hide({
fade: true
});
Linking.getInitialURL().then((url) => {
useSettingStore.setState({
initialUrl: url
});
});
const App = (props: { configureMode: "note-preview" }) => {
useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
useEffect(() => {
changeSystemBarColors();
SettingsService.onFirstLaunch();
changeSystemBarColors();
setTimeout(async () => {
await Notifications.get();
if (SettingsService.get().notifNotes) {
@@ -122,8 +127,12 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
.then((theme) => {
if (theme) {
theme.colorScheme === "dark"
? useThemeStore.getState().setDarkTheme(theme)
: useThemeStore.getState().setLightTheme(theme);
? useThemeStore.setState({
darkTheme: theme
})
: useThemeStore.setState({
lightTheme: theme
});
}
})
.catch(() => {
@@ -133,9 +142,9 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
const listener = Appearance.addChangeListener(({ colorScheme }) => {
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
useThemeStore
.getState()
.setColorScheme(colorScheme as "light" | "dark");
useThemeStore.setState({
colorScheme: colorScheme as "light" | "dark"
});
}
});
return () => {

View File

@@ -30,7 +30,8 @@ import {
getCacheSize,
hashBase64,
readEncrypted,
writeEncryptedBase64
writeEncryptedBase64,
bulkDeleteFiles
} from "./io";
import { uploadFile } from "./upload";
import {
@@ -61,5 +62,6 @@ export const FileStorage: IFileStorage = {
exists,
clearFileStorage,
getUploadedFileSize,
bulkExists
bulkExists,
bulkDeleteFiles
};

View File

@@ -30,7 +30,14 @@ import RNFetchBlob from "react-native-blob-util";
import { eSendEvent } from "../../services/event-manager";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
import {
ABYTES,
cacheDir,
cacheDirOld,
getRandomId,
isSuccessStatusCode,
parseS3Error
} from "./utils";
export async function readEncrypted<TOutputFormat extends DataFormat>(
filename: string,
@@ -102,16 +109,41 @@ export async function writeEncryptedBase64(
};
}
async function deleteLocalFile(filename: string) {
try {
await createCacheDir();
let path = cacheDir + `/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
if (Platform.OS === "ios" && !exists) {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
if (await RNFetchBlob.fs.exists(appGroupPath)) {
RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
/* empty */
});
return;
}
}
if (exists) {
RNFetchBlob.fs.unlink(path).catch(() => {
/* empty */
});
}
} catch (e) {
DatabaseLogger.error(e as Error, "deleteLocalFile");
}
}
export async function deleteFile(
filename: string,
requestOptions?: RequestOptions
): Promise<boolean> {
await createCacheDir();
const localFilePath = cacheDir + `/${filename}`;
if (!requestOptions) {
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
/* empty */
});
deleteLocalFile(filename);
return true;
}
@@ -122,9 +154,7 @@ export async function deleteFile(
const status = response.info().status;
const ok = status >= 200 && status < 300;
if (ok) {
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
/* empty */
});
deleteLocalFile(filename);
}
return ok;
} catch (e) {
@@ -135,6 +165,49 @@ export async function deleteFile(
}
}
export async function bulkDeleteFiles(
filenames: string[],
requestOptions?: RequestOptions
) {
await createCacheDir();
if (!requestOptions) {
filenames.forEach((filename) => {
deleteLocalFile(filename);
});
return true;
}
try {
const { url, headers } = requestOptions;
const response = await fetch(url, {
method: "POST",
headers: {
...headers,
"Content-Type": "application/json"
},
body: JSON.stringify({
names: filenames
})
});
const result = isSuccessStatusCode(response.status);
if (result) {
filenames.forEach((filename) => {
deleteLocalFile(filename);
});
} else {
throw await response.text();
}
return result;
} catch (e) {
DatabaseLogger.error(
typeof e === "string" ? parseS3Error(e as string) : (e as Error),
"Could not bulk delete files"
);
return false;
}
}
export async function clearFileStorage() {
try {
await createCacheDir();
@@ -222,15 +295,21 @@ export async function deleteCacheFileByName(name: string) {
}
export async function deleteDCacheFiles() {
await createCacheDir();
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (file.includes("_dcache") || file.startsWith("NN_")) {
await RNFetchBlob.fs.unlink(file).catch(() => {
/* empty */
});
try {
await createCacheDir();
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (
file.includes("_dcache") ||
file.startsWith("NN_") ||
file.endsWith(".pdf")
) {
await RNFetchBlob.fs.unlink(file).catch(() => {
/* empty */
});
}
}
}
} catch (e) {}
}
export async function getCachePathForFile(filename: string) {

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { isImage, RequestOptions } from "@notesnook/core";
import { isImage, RequestOptions, hosts } from "@notesnook/core";
import { PermissionsAndroid, Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
@@ -32,6 +32,133 @@ import {
getUploadedFileSize
} from "./utils";
import Upload from "@ammarahmed/react-native-upload";
import { CloudUploader } from "react-native-nitro-cloud-uploader";
// Upload constants
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
const MINIMUM_MULTIPART_FILE_SIZE = 25 * 1024 * 1024; // 25MB
interface InitiateMultipartResponse {
uploadId: string;
parts: string[];
error?: string;
}
async function initiateMultipartUpload(
filename: string,
fileSize: number,
headers: Record<string, string>
): Promise<InitiateMultipartResponse> {
const totalParts = Math.ceil(fileSize / CHUNK_SIZE);
const url = `${hosts.API_HOST}/s3/multipart?name=${filename}&parts=${totalParts}&uploadId=`;
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(
`Failed to initiate multipart upload: ${response.statusText}`
);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
if (!data.uploadId || !data.parts) {
throw new Error("Failed to initiate multipart upload: invalid response.");
}
DatabaseLogger.info(
`Initiated multipart upload for ${filename} with upload ID: ${data.uploadId}`
);
return data;
}
async function multipartUploadFile(
filename: string,
filePath: string,
fileSize: number,
requestOptions: RequestOptions,
cancelToken: { cancel: (reason?: string) => Promise<void> }
): Promise<Response> {
const { headers } = requestOptions;
try {
const uploadData = await initiateMultipartUpload(
filename,
fileSize,
headers
);
const { uploadId, parts } = uploadData;
DatabaseLogger.info(
`Starting upload for ${filename} with ${parts.length} parts`
);
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
await CloudUploader.cancelUpload(uploadId);
};
CloudUploader.addListener("upload-progress", (event) => {
useAttachmentStore
.getState()
.setProgress(
event.bytesUploaded || 0,
event.totalBytes || fileSize,
filename,
0,
"upload"
);
DatabaseLogger.info(
`File upload progress: ${filename}, ${event.bytesUploaded}/${
event.totalBytes || fileSize
}, chunk: ${event.chunkIndex}, progress: ${event.progress}`
);
});
// CloudUploader handles chunking and uploading all parts internally
const result = await CloudUploader.startUpload(
filename,
filePath,
parts,
3, // maxParallel
true // showNotification
);
CloudUploader.removeListener("upload-progress");
if (!result.success) {
throw new Error("Failed to upload multipart file");
}
DatabaseLogger.info(
`Multipart upload completed for ${filename} with upload ID: ${uploadId}`
);
const response = await fetch(`${hosts.API_HOST}/s3/multipart`, {
method: "POST",
body: JSON.stringify({
Key: filename,
UploadId: uploadId,
PartETags: result.etags.map((etag, index) => ({
partNumber: index + 1,
etag: etag
}))
}),
headers: { ...headers, "Content-Type": "application/json" }
});
return response;
} catch (error) {
DatabaseLogger.error(error, "Multipart upload failed", { filename });
CloudUploader.removeListener("upload-progress");
useAttachmentStore.getState().remove(filename);
throw error;
}
}
export async function uploadFile(
filename: string,
@@ -96,72 +223,111 @@ export async function uploadFile(
}
}
const upload = Upload.create({
customUploadId: filename,
path: Platform.OS === "ios" ? "file://" + fileInfo.path : fileInfo.path,
url: url,
method: "PUT",
headers: {
...headers,
"content-type": "application/octet-stream"
},
appGroup: IOS_APPGROUPID,
notification: {
filename:
attachmentInfo && isImage(attachmentInfo?.mimeType)
? "image"
: attachmentInfo?.filename || "file",
enabled: true,
enableRingTone: true,
autoClear: true
}
}).onChange((event) => {
switch (event.status) {
case "running":
case "pending":
useAttachmentStore
.getState()
.setProgress(
event.uploadedBytes || 0,
event.totalBytes || fileInfo.size,
filename,
0,
"upload"
);
DatabaseLogger.info(
`File upload progress: ${filename}, ${event.uploadedBytes}/${
event.totalBytes || fileInfo.size
}`
);
break;
case "completed":
console.log("Upload completed");
break;
}
});
const result = await upload.start();
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
upload.cancel();
};
let uploaded = false;
const status = result.responseCode || 0;
const uploaded = status >= 200 && status < 300;
// Use multipart upload for files larger than MINIMUM_MULTIPART_FILE_SIZE
if (fileInfo.size >= MINIMUM_MULTIPART_FILE_SIZE) {
DatabaseLogger.info(
`Using multipart upload for large file: ${filename} (${fileInfo.size} bytes)`
);
const result = await multipartUploadFile(
filename,
filePath,
fileInfo.size,
requestOptions,
cancelToken
);
const status = result.status || 0;
uploaded = status >= 200 && status < 300;
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(await result.text())}`
);
}
} else {
// Use single-part upload for smaller files
DatabaseLogger.info(
`Using single-part upload for file: ${filename} (${fileInfo.size} bytes)`
);
const upload = Upload.create({
customUploadId: filename,
path: Platform.OS === "ios" ? "file://" + fileInfo.path : fileInfo.path,
url: url,
method: "PUT",
headers: {
...headers,
"content-type": "application/octet-stream"
},
appGroup: IOS_APPGROUPID,
notification: {
filename:
attachmentInfo && isImage(attachmentInfo?.mimeType)
? "image"
: attachmentInfo?.filename || "file",
enabled: true,
enableRingTone: true,
autoClear: true
}
}).onChange((event) => {
switch (event.status) {
case "running":
case "pending":
useAttachmentStore
.getState()
.setProgress(
event.uploadedBytes || 0,
event.totalBytes || fileInfo.size,
filename,
0,
"upload"
);
DatabaseLogger.info(
`File upload progress: ${filename}, ${event.uploadedBytes}/${
event.totalBytes || fileInfo.size
}`
);
break;
case "completed":
DatabaseLogger.info("Upload completed");
break;
}
});
const result = await upload.start();
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
upload.cancel();
};
const status = result.responseCode || 0;
uploaded = status >= 200 && status < 300;
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(result.error)}`
);
}
}
useAttachmentStore.getState().remove(filename);
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(result.error)}`
if (uploaded) {
attachmentInfo = await db.attachments.attachment(filename);
if (!attachmentInfo) return false;
await checkUpload(
filename,
requestOptions.chunkSize,
attachmentInfo.size
);
DatabaseLogger.info(`File upload status: ${filename}, success`);
}
attachmentInfo = await db.attachments.attachment(filename);
if (!attachmentInfo) return false;
await checkUpload(filename, requestOptions.chunkSize, attachmentInfo.size);
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
return uploaded;
} catch (e) {
useAttachmentStore.getState().remove(filename);

View File

@@ -161,10 +161,10 @@ export async function checkUpload(
size === 0
? `File size is 0.`
: size === -1
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
if (error) throw new Error(error);
}
@@ -189,3 +189,11 @@ export async function checkAndCreateDir(path: string) {
}
return dir;
}
export const santizeUri = (uri: string) => {
return Platform.OS === "ios" ? decodeURI(uri).replace("file:///", "/") : uri;
};
export function isSuccessStatusCode(statusCode: number) {
return statusCode >= 200 && statusCode <= 299;
}

View File

@@ -28,7 +28,7 @@ import {
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, View } from "react-native";
import { ActivityIndicator, FlatList, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create from "zustand";
@@ -313,6 +313,10 @@ export const AttachmentDialog = ({
});
};
db.attachments.orphaned.items().then((r) => {
console.log(r);
});
return (
<>
{isSheet ? (
@@ -509,7 +513,7 @@ export const AttachmentDialog = ({
</ScrollView>
</View>
<LegendList
<FlatList
renderScrollComponent={(props) => <ScrollView {...props} />}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
@@ -543,7 +547,6 @@ export const AttachmentDialog = ({
}}
/>
}
estimatedItemSize={50}
data={loading ? [] : attachments?.placeholders || []}
extraData={attachments}
renderItem={renderItem}

View File

@@ -73,7 +73,15 @@ export const ChangePassword = () => {
throw new Error(strings.backupFailed() + `: ${result.error}`);
}
await db.user.changePassword(oldPassword.current, password.current);
const passwordChanged = await db.user.changePassword(
oldPassword.current,
password.current
);
if (!passwordChanged) {
throw new Error("Could not change user account password.");
}
ToastManager.show({
heading: strings.passwordChangedSuccessfully(),
type: "success",

View File

@@ -39,7 +39,7 @@ export function hideAuth(context?: AuthParams["context"]) {
initialAuthMode.current === AuthMode.welcomeLogin ||
context === "intro"
) {
Navigation.replace("FluidPanelsView", {});
Navigation.navigate("FluidPanelsView", {});
} else {
Navigation.goBack();
}

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
@@ -35,9 +34,9 @@ import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
export const ForgotPassword = () => {
export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
const { colors } = useThemeColors("sheet");
const email = useRef<string>(undefined);
const email = useRef<string>(userEmail);
const emailInputRef = useRef<TextInput>(null);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
@@ -87,94 +86,76 @@ export const ForgotPassword = () => {
return (
<>
<ActionSheet
onBeforeShow={(data) => (email.current = data)}
onClose={() => {
setSent(false);
setLoading(false);
}}
onOpen={() => {
emailInputRef.current?.setNativeProps({
text: email.current
});
}}
indicatorStyle={{
width: 100
}}
gestureEnabled
id="forgotpassword_sheet"
>
{sent ? (
<View
{sent ? (
<View
style={{
padding: DefaultAppStyles.GAP,
justifyContent: "center",
alignItems: "center",
paddingBottom: 50
}}
>
<IconButton
style={{
padding: DefaultAppStyles.GAP,
justifyContent: "center",
alignItems: "center",
paddingBottom: 50
width: null,
height: null
}}
color={colors.primary.accent}
name="email"
size={50}
/>
<Heading>{strings.recoveryEmailSent()}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
<IconButton
style={{
width: null,
height: null
}}
color={colors.primary.accent}
name="email"
size={50}
/>
<Heading>{strings.recoveryEmailSent()}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
{strings.recoveryEmailSentDesc()}
</Paragraph>
</View>
) : (
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
padding: DefaultAppStyles.GAP
{strings.recoveryEmailSentDesc()}
</Paragraph>
</View>
) : (
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
padding: DefaultAppStyles.GAP
}}
>
<DialogHeader title={strings.accountRecovery()} />
<Seperator />
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
>
<DialogHeader title={strings.accountRecovery()} />
<Seperator />
defaultValue={email.current}
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
onSubmit={() => {}}
/>
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
defaultValue={email.current}
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
onSubmit={() => {}}
/>
<Button
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
loading={loading}
onPress={sendRecoveryEmail}
type="accent"
title={loading ? null : strings.next()}
/>
</View>
)}
</ActionSheet>
<Button
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
loading={loading}
onPress={sendRecoveryEmail}
type="accent"
title={loading ? null : strings.next()}
/>
</View>
)}
</>
);
};

View File

@@ -25,7 +25,11 @@ import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -110,7 +114,6 @@ export const Login = ({
return (
<>
<AuthHeader />
<ForgotPassword />
<Dialog context="two_factor_verify" />
<KeyboardAwareScrollView
style={{
@@ -258,7 +261,9 @@ export const Login = ({
}}
onPress={() => {
if (loading || !email.current) return;
SheetManager.show("forgotpassword_sheet");
presentSheet({
component: <ForgotPassword userEmail={email.current} />
});
}}
textStyle={{
textDecorationLine: "underline"

View File

@@ -97,7 +97,7 @@ export const SessionExpired = () => {
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
const key = await db.user.getEncryptionKey();
const key = await db.user.getDataEncryptionKeys();
if (!key) throw new Error("No encryption key found.");
Sync.run("global", false, "full", async (complete) => {

View File

@@ -0,0 +1,66 @@
import { useThemeColors } from "@notesnook/theme";
import { useRef } from "react";
import { 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, "day").toDate());
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
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

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

View File

@@ -167,14 +167,16 @@ const ColorPicker = ({
);
if (!title.current)
return ToastManager.error(
new Error(strings.allFieldsRequired())
new Error(strings.allFieldsRequired()),
"color-picker"
);
const exists = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", selectedColor)])
);
if (exists)
return ToastManager.error(
new Error(strings.colorExists(selectedColor))
new Error(strings.colorExists(selectedColor)),
"color-picker"
);
const id = await db.colors.add({
title: title.current,

View File

@@ -23,6 +23,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import Orientation from "react-native-orientation-locker";
import Pdf from "react-native-pdf";
import FileViewer from "react-native-file-viewer";
import { MMKV } from "../../../common/database/mmkv";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import { deleteCacheFileByPath, exists } from "../../../common/filesystem/io";
@@ -43,6 +44,7 @@ import SheetProvider from "../../sheet-provider";
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";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
@@ -117,8 +119,11 @@ const PDFPreview = () => {
setVisible(false);
return;
}
const path = `${cacheDir}/${uri}`;
let path = `${cacheDir}/${attachment.filename}`;
snapshotValue.current = snapshot.current;
await ReactNativeBlobUtil.fs
.mv(`${cacheDir}/${uri}`, path)
.catch(console.log);
setPDFSource("file://" + path);
setLoading(false);
}, 100);
@@ -127,7 +132,7 @@ const PDFPreview = () => {
);
const close = () => {
deleteCacheFileByPath(pdfSource);
deleteCacheFileByPath(pdfSource.replace("file://", ""));
setPDFSource(null);
setVisible(false);
setPassword("");
@@ -157,7 +162,12 @@ const PDFPreview = () => {
return (
visible && (
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
<BaseDialog
animation="fade"
visible={true}
onRequestClose={close}
useSafeArea={false}
>
<SheetProvider context={attachment?.hash} />
<Dialog context={attachment?.hash} />
@@ -223,45 +233,48 @@ const PDFPreview = () => {
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: 12
gap: DefaultAppStyles.GAP_SMALL
}}
>
<TextInput
ref={inputRef}
defaultValue={currentPage + ""}
<View
style={{
color: colors.primary.paragraph,
padding: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
paddingVertical: 0,
height: 25,
backgroundColor: colors.secondary.background,
width: 40,
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "Inter-Regular"
flexDirection: "row",
alignItems: "center"
}}
selectTextOnFocus
keyboardType="decimal-pad"
onSubmitEditing={(event) => {
setCurrentPage(event.nativeEvent.text);
pdfRef.current?.setPage(parseInt(event.nativeEvent.text));
}}
blurOnSubmit
/>
<Paragraph color={colors.static.white}>/{numPages}</Paragraph>
</View>
<View
style={{
flexDirection: "row"
}}
>
>
<TextInput
ref={inputRef}
defaultValue={currentPage + ""}
style={{
color: colors.primary.paragraph,
padding: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
paddingVertical: 0,
height: 25,
backgroundColor: colors.secondary.background,
width: 40,
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "Inter-Regular"
}}
selectTextOnFocus
keyboardType="decimal-pad"
onSubmitEditing={(event) => {
setCurrentPage(event.nativeEvent.text);
pdfRef.current?.setPage(
parseInt(event.nativeEvent.text)
);
}}
blurOnSubmit
/>
<Paragraph color={colors.static.white}>
/{numPages}
</Paragraph>
</View>
<IconButton
color={colors.static.white}
name="download"
@@ -269,6 +282,16 @@ const PDFPreview = () => {
downloadAttachment(attachment.hash, false);
}}
/>
<IconButton
color={colors.static.white}
name="open-in-new"
onPress={() => {
FileViewer.open(pdfSource, {
showOpenWithDialog: true,
showAppsSuggestions: true
});
}}
/>
</View>
</View>
{pdfSource ? (

View File

@@ -1,900 +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 Clipboard from "@react-native-clipboard/clipboard";
import React, { Component, createRef } from "react";
import { InteractionManager, View } from "react-native";
import Share from "react-native-share";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseActionSheet,
eCloseVaultDialog,
eOnLoadNote,
eOpenVaultDialog,
eUpdateNoteInEditor
} from "../../../utils/events";
import { deleteItems } from "../../../utils/functions";
import { fluidTabsRef } from "../../../utils/global-refs";
import { convertNoteToText } from "../../../utils/note-to-text";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
export class VaultDialog extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
wrongPassword: false,
loading: false,
note: {},
vault: false,
locked: true,
permanant: false,
goToEditor: false,
share: false,
passwordsDontMatch: false,
deleteNote: false,
focusIndex: null,
biometricUnlock: false,
isBiometryEnrolled: false,
isBiometryAvailable: false,
fingerprintAccess: false,
changePassword: false,
copyNote: false,
revokeFingerprintAccess: false,
title: strings.goToEditor(),
description: null,
clearVault: false,
deleteVault: false,
deleteAll: false,
noteLocked: false
};
this.passInputRef = createRef();
this.confirmPassRef = createRef();
this.changePassInputRef = createRef();
this.password = null;
this.confirmPassword = null;
this.newPassword = null;
this.title = !this.state.novault
? strings.createVault()
: this.state.fingerprintAccess
? strings.vaultFingerprintUnlock()
: this.state.revokeFingerprintAccess
? strings.revokeVaultFingerprintUnlock()
: this.state.changePassword
? strings.changeVaultPassword()
: this.state.noteLocked
? this.state.deleteNote
? strings.deleteNote()
: this.state.share
? strings.shareNote()
: this.state.copyNote
? strings.copyNote()
: this.state.goToEditor
? strings.goToEditor()
: strings.goToEditor()
: strings.lockNote();
}
componentDidMount() {
eSubscribeEvent(eOpenVaultDialog, this.open);
eSubscribeEvent(eCloseVaultDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenVaultDialog, this.open);
eUnSubscribeEvent(eCloseVaultDialog, this.close);
}
/**
*
* @param {import('../../../services/event-manager').vaultType} data
*/
open = async (data) => {
let biometry = await BiometricService.isBiometryAvailable();
let available = false;
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
if (biometry) {
available = true;
}
this.setState({
note: data.item,
novault: data.novault,
locked: data.locked,
permanant: data.permanant,
goToEditor: data.goToEditor,
share: data.share,
deleteNote: data.deleteNote,
copyNote: data.copyNote,
isBiometryAvailable: available,
biometricUnlock: fingerprint,
isBiometryEnrolled: fingerprint,
fingerprintAccess: data.fingerprintAccess,
changePassword: data.changePassword,
revokeFingerprintAccess: data.revokeFingerprintAccess,
title: data.title,
description: data.description,
clearVault: data.clearVault,
deleteVault: data.deleteVault,
noteLocked: data.item && (await db.vaults.itemExists(data.item))
});
if (
fingerprint &&
data.novault &&
!data.fingerprintAccess &&
!data.revokeFingerprintAccess &&
!data.changePassword &&
!data.clearVault &&
!data.deleteVault
) {
await this._onPressFingerprintAuth(data.title, data.description);
} else {
this.setState({
visible: true
});
}
};
close = () => {
if (this.state.loading) {
ToastManager.show({
heading: this.state.title,
message: strings.pleaseWait() + "...",
type: "success",
context: "local"
});
return;
}
Navigation.queueRoutesForUpdate();
this.password = null;
this.confirmPassword = null;
this.setState({
visible: false,
note: {},
locked: false,
permanant: false,
goToEditor: false,
share: false,
novault: false,
deleteNote: false,
passwordsDontMatch: false
});
};
onPress = async () => {
if (this.state.revokeFingerprintAccess) {
await this._revokeFingerprintAccess();
this.close();
return;
}
if (this.state.loading) return;
if (!this.password) {
ToastManager.show({
heading: strings.passwordNotEntered(),
type: "error",
context: "local"
});
return;
}
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
ToastManager.show({
heading: strings.passwordNotMatched(),
type: "error",
context: "local"
});
this.setState({
passwordsDontMatch: true
});
return;
}
this._createVault();
} else if (this.state.changePassword) {
this.setState({
loading: true
});
db.vault
.changePassword(this.password, this.newPassword)
.then(() => {
this.setState({
loading: false
});
if (this.state.biometricUnlock) {
this._enrollFingerprint(this.newPassword);
}
ToastManager.show({
heading: strings.passwordUpdated(),
type: "success",
context: "global"
});
this.close();
})
.catch((e) => {
this.setState({
loading: false
});
if (e.message === db.vault.ERRORS.wrongPassword) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
} else {
ToastManager.error(e);
}
});
} else if (this.state.locked) {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
this.setState({
wrongPassword: true
});
return;
}
if (this.state.noteLocked) {
await this._unlockNote();
} else {
db.vault
.unlock(this.password)
.then(async () => {
this.setState({
wrongPassword: false
});
await this._lockNote();
})
.catch((e) => {
this._takeErrorAction(e);
});
}
} else if (this.state.fingerprintAccess) {
this._enrollFingerprint(this.password);
} else if (this.state.clearVault) {
await this.clearVault();
} else if (this.state.deleteVault) {
await this.deleteVault();
}
};
deleteVault = async () => {
this.setState({
loading: true
});
try {
let verified = true;
if (await db.user.getUser()) {
verified = await db.user.verifyPassword(this.password);
}
if (verified) {
let noteIds = [];
if (this.state.deleteAll) {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault, "note").get();
noteIds = relations.map((item) => item.toId);
}
await db.vault.delete(this.state.deleteAll);
if (this.state.deleteAll) {
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
}
eSendEvent("vaultUpdated");
this.setState({
loading: false
});
setTimeout(() => {
this.close();
}, 100);
} else {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}
} catch (e) {
console.error(e);
}
};
clearVault = async () => {
this.setState({
loading: true
});
try {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault, "note").get();
const noteIds = relations.map((item) => item.toId);
await db.vault.clear(this.password);
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
this.setState({
loading: false
});
this.close();
eSendEvent("vaultUpdated");
} catch (e) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}
this.setState({
loading: false
});
};
async _lockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
return;
} else {
await db.vault.add(this.state.note.id);
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.close();
ToastManager.show({
message: strings.noteLocked(),
type: "error",
context: "local"
});
this.setState({
loading: false
});
}
}
async _unlockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
return;
}
if (this.state.permanant) {
this._permanantUnlock();
} else {
await this._openNote();
}
}
_openNote = async () => {
try {
let note = await db.vault.open(this.state.note.id, this.password);
if (this.state.biometricUnlock && !this.state.isBiometryEnrolled) {
await this._enrollFingerprint(this.password);
}
if (this.state.goToEditor) {
this._openInEditor(note);
} else if (this.state.share) {
await this._shareNote(note);
} else if (this.state.deleteNote) {
await this._deleteNote();
} else if (this.state.copyNote) {
await this._copyNote(note);
}
} catch (e) {
this._takeErrorAction(e);
}
};
async _deleteNote() {
try {
await db.vault.remove(this.state.note.id, this.password);
await deleteItems("note", [this.state.note.id]);
this.close();
} catch (e) {
this._takeErrorAction(e);
}
}
async _enrollFingerprint(password) {
this.setState(
{
loading: true
},
async () => {
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
this.setState({
loading: false
});
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
this.close();
} catch (e) {
this.close();
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
this.setState({
loading: false
});
}
}
);
}
async _createVault() {
await db.vault.create(this.password);
if (this.state.biometricUnlock) {
await this._enrollFingerprint(this.password);
}
if (this.state.note?.id) {
await db.vault.add(this.state.note.id);
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.setState({
loading: false
});
ToastManager.show({
heading: strings.noteLocked(),
type: "success",
context: "global"
});
this.close();
} else {
ToastManager.show({
heading: strings.vaultCreated(),
type: "success",
context: "global"
});
this.close();
}
eSendEvent("vaultUpdated");
}
_permanantUnlock() {
db.vault
.remove(this.state.note.id, this.password)
.then(() => {
ToastManager.show({
heading: strings.noteUnlocked(),
type: "success",
context: "global"
});
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.close();
})
.catch((e) => {
this._takeErrorAction(e);
});
}
_openInEditor(note) {
this.close();
InteractionManager.runAfterInteractions(async () => {
eSendEvent(eOnLoadNote, {
item: note
});
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor");
}
});
}
async _copyNote(note) {
Clipboard.setString((await convertNoteToText(note, true)) || "");
ToastManager.show({
heading: strings.noteCopied(),
type: "success",
context: "global"
});
this.close();
}
async _shareNote(note) {
this.close();
try {
await Share.open({
heading: note.title,
failOnCancel: false,
message: (await convertNoteToText(note)) || ""
});
} catch (e) {
console.error(e);
}
}
_takeErrorAction(e) {
this.setState({
wrongPassword: true,
visible: true
});
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
}
_revokeFingerprintAccess = async () => {
try {
await BiometricService.resetCredentials();
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockDisabled(),
type: "success",
context: "global"
});
} catch (e) {
ToastManager.show({
heading: e.message,
type: "success",
context: "global"
});
}
};
_onPressFingerprintAuth = async (title, description) => {
try {
let credentials = await BiometricService.getCredentials(
title || this.state.title,
description || this.state.description
);
if (credentials?.password) {
this.password = credentials.password;
this.onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
this.setState({
visible: true
});
}
} catch (e) {
console.error(e);
}
};
render() {
const { colors } = this.props;
const {
note,
visible,
novault,
deleteNote,
share,
goToEditor,
fingerprintAccess,
changePassword,
loading,
deleteVault,
clearVault
} = this.state;
if (!visible) return null;
return (
<BaseDialog
onShow={async () => {
await sleep(100);
this.passInputRef.current?.focus();
}}
statusBarTranslucent={false}
onRequestClose={this.close}
visible={true}
>
<View
style={{
...getElevationStyle(5),
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
}}
>
<DialogHeader title={this.state.title} icon="shield" padding={12} />
<Seperator half />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{(novault ||
changePassword ||
this.state.clearVault ||
this.state.deleteVault) &&
!this.state.revokeFingerprintAccess ? (
<>
<Input
fwdRef={this.passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
this.password = value;
}}
marginBottom={
!this.state.biometricUnlock ||
!this.state.isBiometryEnrolled ||
!novault ||
changePassword
? 0
: 10
}
onSubmit={() => {
changePassword
? this.confirmPassRef.current?.focus()
: this.onPress;
}}
autoComplete="password"
returnKeyLabel={
changePassword ? strings.next() : this.state.title
}
returnKeyType={changePassword ? "next" : "done"}
secureTextEntry
placeholder={
changePassword
? strings.currentPassword()
: strings.password()
}
/>
{!this.state.biometricUnlock ||
!this.state.isBiometryEnrolled ||
!novault ||
changePassword ? null : (
<Button
onPress={() =>
this._onPressFingerprintAuth(strings.unlockNote(), "")
}
icon="fingerprint"
width="100%"
title={strings.unlockWithBiometrics()}
type="transparent"
/>
)}
</>
) : null}
{this.state.deleteVault && (
<Button
onPress={() =>
this.setState({
deleteAll: !this.state.deleteAll
})
}
icon={
this.state.deleteAll
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
width="100%"
title={strings.deleteAllNotes()}
type="errorShade"
/>
)}
{changePassword ? (
<>
<Seperator half />
<Input
ref={this.confirmPassRef}
editable={!loading}
testID={notesnook.ids.dialogs.vault.changePwd}
autoCapitalize="none"
onChangeText={(value) => {
this.newPassword = value;
}}
autoComplete="password"
onSubmit={this.onPress}
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={strings.newPassword()}
/>
</>
) : null}
{!novault ? (
<View>
<Input
fwdRef={this.passInputRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
this.password = value;
}}
autoComplete="password"
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
onSubmit={() => {
this.confirmPassRef.current?.focus();
}}
placeholder={strings.password()}
/>
<Input
fwdRef={this.confirmPassRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwdAlt}
secureTextEntry
validationType="confirmPassword"
customValidator={() => this.password}
errorMessage="Passwords do not match."
onErrorCheck={() => null}
marginBottom={0}
autoComplete="password"
returnKeyLabel="Create"
returnKeyType="done"
onChangeText={(value) => {
this.confirmPassword = value;
if (value !== this.password) {
this.setState({
passwordsDontMatch: true
});
} else {
this.setState({
passwordsDontMatch: false
});
}
}}
onSubmit={this.onPress}
placeholder={strings.confirmPassword()}
/>
</View>
) : null}
{this.state.biometricUnlock &&
!this.state.isBiometryEnrolled &&
novault ? (
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
) : null}
{this.state.isBiometryAvailable &&
!this.state.fingerprintAccess &&
!this.state.clearVault &&
!this.state.deleteVault &&
((!this.state.biometricUnlock && !changePassword) || !novault) ? (
<Button
onPress={() => {
this.setState({
biometricUnlock: !this.state.biometricUnlock
});
}}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
icon="fingerprint"
width="100%"
title={strings.unlockWithBiometrics()}
iconColor={
this.state.biometricUnlock
? colors.selected.accent
: colors.primary.icon
}
type={this.state.biometricUnlock ? "transparent" : "plain"}
/>
) : null}
</View>
<DialogButtons
onPressNegative={this.close}
onPressPositive={this.onPress}
loading={loading}
positiveType={
deleteVault || clearVault ? "errorShade" : "transparent"
}
positiveTitle={
deleteVault
? strings.delete()
: clearVault
? strings.clear()
: fingerprintAccess
? strings.enable()
: this.state.revokeFingerprintAccess
? strings.revoke()
: changePassword
? strings.change()
: this.state.noteLocked
? deleteNote
? strings.delete()
: share
? strings.share()
: goToEditor
? strings.open()
: strings.unlock()
: !note.id
? strings.create()
: strings.lock()
}
/>
</View>
<Toast context="local" />
</BaseDialog>
);
}
}

View File

@@ -0,0 +1,933 @@
/*
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 Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { InteractionManager, View, TextInput } from "react-native";
import Share from "react-native-share";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
Vault,
VaultRequestType,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseActionSheet,
eCloseVaultDialog,
eOnLoadNote,
eOpenVaultDialog,
eUpdateNoteInEditor
} from "../../../utils/events";
import { deleteItems } from "../../../utils/functions";
import { fluidTabsRef } from "../../../utils/global-refs";
import { convertNoteToText } from "../../../utils/note-to-text";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import { Note, NoteContent, VAULT_ERRORS } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
export const VaultDialog: React.FC = () => {
const { colors } = useThemeColors();
// UI State
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [wrongPassword, setWrongPassword] = useState(false);
const [passwordsDontMatch, setPasswordsDontMatch] = useState(false);
const [deleteAll, setDeleteAll] = useState(false);
const [biometricUnlock, setBiometricUnlock] = useState(false);
const [isBiometryAvailable, setIsBiometryAvailable] = useState(false);
const [isBiometryEnrolled, setIsBiometryEnrolled] = useState(false);
// Refs for non-UI state
const requestTypeRef = useRef<VaultRequestType | null>(null);
const noteRef = useRef<Note | undefined>(undefined);
const titleRef = useRef<string>(strings.goToEditor());
const descriptionRef = useRef<string | null>(null);
const paragraphRef = useRef<string | null>(null);
const buttonTitleRef = useRef<string | null>(null);
const positiveButtonTypeRef = useRef<"errorShade" | "transparent" | "accent">(
"transparent"
);
const customActionTitleRef = useRef<string | null>(null);
const customActionParagraphRef = useRef<string | null>(null);
const noteLockedRef = useRef(false);
const onUnlockRef = useRef<
| ((
item: Note & {
content?: NoteContent<false>;
},
password: string
) => void)
| undefined
>(undefined);
// Input refs
const passInputRef = useRef<TextInput>(null);
const confirmPassRef = useRef<TextInput>(null);
const changePassInputRef = useRef<TextInput>(null);
// Password refs
const passwordRef = useRef<string | null>(null);
const confirmPasswordRef = useRef<string | null>(null);
const newPasswordRef = useRef<string | null>(null);
const open = useCallback(async (data: Vault) => {
const biometry = await BiometricService.isBiometryAvailable();
const available = !!biometry;
const fingerprint = await BiometricService.hasInternetCredentials();
const noteLocked = data.item
? await db.vaults.itemExists(data.item)
: false;
// Set refs
noteRef.current = data.item;
titleRef.current = data.title || strings.goToEditor();
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
noteLockedRef.current = noteLocked;
onUnlockRef.current = data.onUnlock;
requestTypeRef.current = data.requestType;
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setLoading(false);
// Auto-unlock with fingerprint if applicable
const canAutoUnlock =
fingerprint &&
data.requestType !== VaultRequestType.EnableFingerprint &&
data.requestType !== VaultRequestType.RevokeFingerprint &&
data.requestType !== VaultRequestType.ChangePassword &&
data.requestType !== VaultRequestType.ClearVault &&
data.requestType !== VaultRequestType.DeleteVault &&
data.requestType !== VaultRequestType.CustomAction &&
data.requestType !== VaultRequestType.PermanentUnlock;
if (canAutoUnlock) {
await onPressFingerprintAuth(data.title, data.description);
} else {
setVisible(true);
}
}, []);
const close = useCallback(() => {
if (loading) {
ToastManager.show({
heading: titleRef.current,
message: strings.pleaseWait() + "...",
type: "success",
context: "local"
});
return;
}
Navigation.queueRoutesForUpdate();
// Reset password refs
passwordRef.current = null;
confirmPasswordRef.current = null;
newPasswordRef.current = null;
// Reset refs
requestTypeRef.current = null;
noteRef.current = undefined;
titleRef.current = strings.goToEditor();
descriptionRef.current = null;
paragraphRef.current = null;
buttonTitleRef.current = null;
positiveButtonTypeRef.current = "transparent";
customActionTitleRef.current = null;
customActionParagraphRef.current = null;
noteLockedRef.current = false;
onUnlockRef.current = undefined;
// Reset UI state
setVisible(false);
setLoading(false);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setBiometricUnlock(false);
setIsBiometryAvailable(false);
setIsBiometryEnrolled(false);
}, [loading]);
const deleteVault = useCallback(async () => {
setLoading(true);
try {
let verified = true;
if (await db.user.getUser()) {
verified = await db.user.verifyPassword(passwordRef.current || "");
}
if (verified) {
let noteIds: string[] = [];
if (deleteAll) {
const vault = await db.vaults.default();
const relations = await db.relations
.from(
{
type: "vault",
id: vault!.id
},
"note"
)
.get();
noteIds = relations.map((item) => item.toId);
}
await db.vault.delete(deleteAll);
if (deleteAll) {
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
}
eSendEvent("vaultUpdated");
setLoading(false);
setTimeout(() => {
close();
}, 100);
} else {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}
} catch (e) {
console.error(e);
}
}, [deleteAll, close]);
const clearVault = useCallback(async () => {
setLoading(true);
try {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault!, "note").get();
const noteIds = relations.map((item) => item.toId);
await db.vault.clear(passwordRef.current || "");
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
setLoading(false);
close();
eSendEvent("vaultUpdated");
} catch (e) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}
setLoading(false);
}, [close]);
const lockNote = useCallback(async () => {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
return;
} else {
await db.vault.add(noteRef.current!.id);
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
close();
ToastManager.show({
message: strings.noteLocked(),
type: "error",
context: "local"
});
setLoading(false);
}
}, [close]);
const permanantUnlock = useCallback(() => {
db.vault
.remove(noteRef.current!.id, passwordRef.current || "")
.then(async () => {
ToastManager.show({
heading: strings.noteUnlocked(),
type: "success",
context: "global"
});
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
if (biometricUnlock && !isBiometryEnrolled) {
await enrollFingerprint(passwordRef.current || "");
}
close();
})
.catch((e) => {
takeErrorAction();
});
}, [close, biometricUnlock, isBiometryEnrolled]);
const openInEditor = useCallback(
(note: Note & { content?: NoteContent<false> }) => {
close();
InteractionManager.runAfterInteractions(async () => {
eSendEvent(eOnLoadNote, {
item: note
});
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor");
}
});
},
[close]
);
const copyNote = useCallback(
async (note: Note & { content?: NoteContent<false> }) => {
Clipboard.setString((await convertNoteToText(note, true)) || "");
ToastManager.show({
heading: strings.noteCopied(),
type: "success",
context: "global"
});
close();
},
[close]
);
const shareNote = useCallback(
async (note: Note & { content?: NoteContent<false> }) => {
close();
try {
await Share.open({
title: note.title,
failOnCancel: false,
message: (await convertNoteToText(note)) || ""
});
} catch (e) {
console.error(e);
}
},
[close]
);
const deleteNote = useCallback(async () => {
try {
await db.vault.remove(noteRef.current!.id, passwordRef.current || "");
await deleteItems("note", [noteRef.current!.id]);
close();
} catch (e) {
takeErrorAction();
}
}, [close]);
const takeErrorAction = useCallback(() => {
setWrongPassword(true);
setVisible(true);
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
}, []);
const openNote = useCallback(async () => {
try {
if (!passwordRef.current) throw new Error("Invalid password");
const note = await db.vault.open(
noteRef.current!.id,
passwordRef.current
);
if (!note) throw new Error("Failed to unlock note.");
if (biometricUnlock && !isBiometryEnrolled) {
await enrollFingerprint(passwordRef.current || "");
}
const requestType = requestTypeRef.current;
if (requestType === VaultRequestType.GoToEditor) {
openInEditor(note);
} else if (requestType === VaultRequestType.ShareNote) {
await shareNote(note);
} else if (requestType === VaultRequestType.DeleteNote) {
await deleteNote();
} else if (requestType === VaultRequestType.CopyNote) {
await copyNote(note);
} else if (
requestType === VaultRequestType.CustomAction &&
onUnlockRef.current
) {
const password = passwordRef.current;
close();
await sleep(300);
onUnlockRef.current(note, password);
}
} catch (e) {
takeErrorAction();
}
}, [
biometricUnlock,
isBiometryEnrolled,
openInEditor,
shareNote,
deleteNote,
copyNote,
close,
takeErrorAction
]);
const unlockNote = useCallback(async () => {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
return;
}
if (requestTypeRef.current === VaultRequestType.PermanentUnlock) {
permanantUnlock();
} else {
await openNote();
}
}, [permanantUnlock, openNote]);
const enrollFingerprint = useCallback(
async (password: string) => {
setLoading(true);
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
setLoading(false);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
close();
} catch (e) {
close();
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setLoading(false);
}
},
[close]
);
const createVault = useCallback(async () => {
await db.vault.create(passwordRef.current || "");
if (biometricUnlock) {
await enrollFingerprint(passwordRef.current || "");
}
if (noteRef.current?.id) {
await db.vault.add(noteRef.current.id);
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
setLoading(false);
ToastManager.show({
heading: strings.noteLocked(),
type: "success",
context: "global"
});
close();
} else {
ToastManager.show({
heading: strings.vaultCreated(),
type: "success",
context: "global"
});
close();
}
eSendEvent("vaultUpdated");
}, [biometricUnlock, enrollFingerprint, close]);
const revokeFingerprintAccess = useCallback(async () => {
try {
await BiometricService.resetCredentials();
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockDisabled(),
type: "success",
context: "global"
});
} catch (e: any) {
ToastManager.show({
heading: e.message,
type: "success",
context: "global"
});
}
}, []);
const onPressFingerprintAuth = useCallback(
async (title?: string, description?: string) => {
try {
const credentials = await BiometricService.getCredentials(
title || titleRef.current,
description || descriptionRef.current || ""
);
if (!credentials) throw new Error("Failed to get user credentials");
if (credentials?.password) {
passwordRef.current = credentials.password;
onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
setVisible(true);
}
} catch (e) {
console.error(e);
}
},
[]
);
const onPress = useCallback(async () => {
const requestType = requestTypeRef.current;
if (requestType === VaultRequestType.RevokeFingerprint) {
await revokeFingerprintAccess();
close();
return;
}
if (loading) return;
if (!passwordRef.current) {
ToastManager.show({
heading: strings.passwordNotEntered(),
type: "error",
context: "local"
});
return;
}
if (requestType === VaultRequestType.CreateVault) {
if (passwordRef.current !== confirmPasswordRef.current) {
ToastManager.show({
heading: strings.passwordNotMatched(),
type: "error",
context: "local"
});
setPasswordsDontMatch(true);
return;
}
createVault();
} else if (requestType === VaultRequestType.ChangePassword) {
setLoading(true);
db.vault
.changePassword(passwordRef.current, newPasswordRef.current || "")
.then(() => {
setLoading(false);
if (biometricUnlock) {
enrollFingerprint(newPasswordRef.current || "");
}
ToastManager.show({
heading: strings.passwordUpdated(),
type: "success",
context: "global"
});
close();
})
.catch((e) => {
setLoading(false);
if (e.message === VAULT_ERRORS.wrongPassword) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
} else {
ToastManager.error(e);
}
});
} else if (requestType === VaultRequestType.LockNote) {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setWrongPassword(true);
return;
}
db.vault
.unlock(passwordRef.current)
.then(async (unlocked) => {
if (unlocked) {
setWrongPassword(false);
await lockNote();
} else {
takeErrorAction();
}
})
.catch((e) => {
takeErrorAction();
});
} else if (
requestType === VaultRequestType.UnlockNote ||
requestType === VaultRequestType.PermanentUnlock ||
requestType === VaultRequestType.GoToEditor ||
requestType === VaultRequestType.ShareNote ||
requestType === VaultRequestType.CopyNote ||
requestType === VaultRequestType.DeleteNote ||
requestType === VaultRequestType.CustomAction
) {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setWrongPassword(true);
return;
}
if (noteLockedRef.current) {
await unlockNote();
} else {
console.log("Error: Note should be locked for this operation");
}
} else if (requestType === VaultRequestType.EnableFingerprint) {
enrollFingerprint(passwordRef.current);
} else if (requestType === VaultRequestType.ClearVault) {
await clearVault();
} else if (requestType === VaultRequestType.DeleteVault) {
await deleteVault();
}
}, [
loading,
biometricUnlock,
revokeFingerprintAccess,
close,
createVault,
enrollFingerprint,
unlockNote,
lockNote,
takeErrorAction,
clearVault,
deleteVault
]);
useEffect(() => {
eSubscribeEvent(eOpenVaultDialog, open);
eSubscribeEvent(eCloseVaultDialog, close);
return () => {
eUnSubscribeEvent(eOpenVaultDialog, open);
eUnSubscribeEvent(eCloseVaultDialog, close);
};
}, [open, close]);
if (!visible) return null;
const requestType = requestTypeRef.current;
const isCreateVault = requestType === VaultRequestType.CreateVault;
const isChangePassword = requestType === VaultRequestType.ChangePassword;
const isClearVault = requestType === VaultRequestType.ClearVault;
const isDeleteVault = requestType === VaultRequestType.DeleteVault;
const isRevokeFingerprint =
requestType === VaultRequestType.RevokeFingerprint;
const isEnableFingerprint =
requestType === VaultRequestType.EnableFingerprint;
const isCustomAction = requestType === VaultRequestType.CustomAction;
const isDeleteNote = requestType === VaultRequestType.DeleteNote;
const isShareNote = requestType === VaultRequestType.ShareNote;
const isGoToEditor = requestType === VaultRequestType.GoToEditor;
return (
<BaseDialog
onShow={async () => {
await sleep(100);
passInputRef.current?.focus();
}}
statusBarTranslucent={false}
onRequestClose={close}
visible={true}
>
<View
style={{
...getElevationStyle(5),
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
}}
>
<DialogHeader
title={titleRef.current}
paragraph={
paragraphRef.current || customActionParagraphRef.current || ""
}
icon="shield"
padding={12}
/>
<Seperator half />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{(isChangePassword ||
isClearVault ||
!isCreateVault ||
isDeleteVault ||
isCustomAction) &&
!isRevokeFingerprint ? (
<>
<Input
fwdRef={passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
passwordRef.current = value;
}}
marginBottom={
!biometricUnlock ||
!isBiometryEnrolled ||
isCreateVault ||
isChangePassword ||
isCustomAction
? 0
: 10
}
onSubmit={() => {
if (isChangePassword) {
confirmPassRef.current?.focus();
} else {
onPress();
}
}}
autoComplete="password"
returnKeyLabel={
isChangePassword ? strings.next() : titleRef.current
}
returnKeyType={isChangePassword ? "next" : "done"}
secureTextEntry
placeholder={
isChangePassword
? strings.currentPassword()
: strings.password()
}
/>
{!biometricUnlock ||
!isBiometryEnrolled ||
!isBiometryAvailable ||
isCreateVault ||
isChangePassword ||
isCustomAction ? null : (
<Button
onPress={() =>
onPressFingerprintAuth(strings.unlockNote(), "")
}
icon="fingerprint"
width="100%"
title={strings.unlockWithBiometrics()}
type="transparent"
/>
)}
</>
) : null}
{isDeleteVault && (
<Button
onPress={() => setDeleteAll(!deleteAll)}
icon={
deleteAll
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
width="100%"
title={strings.deleteAllNotes()}
type="errorShade"
/>
)}
{isChangePassword ? (
<>
<Seperator half />
<Input
fwdRef={confirmPassRef}
editable={!loading}
testID={notesnook.ids.dialogs.vault.changePwd}
autoCapitalize="none"
onChangeText={(value) => {
newPasswordRef.current = value;
}}
autoComplete="password"
onSubmit={() => {
onPress();
}}
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={strings.newPassword()}
/>
</>
) : null}
{isCreateVault ? (
<View>
<Input
fwdRef={passInputRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
passwordRef.current = value;
}}
autoComplete="password"
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
onSubmit={() => {
confirmPassRef.current?.focus();
}}
placeholder={strings.password()}
/>
<Input
fwdRef={confirmPassRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwdAlt}
secureTextEntry
validationType="confirmPassword"
customValidator={() => passwordRef.current || ""}
errorMessage="Passwords do not match."
onErrorCheck={() => null}
marginBottom={0}
autoComplete="password"
returnKeyLabel="Create"
returnKeyType="done"
onChangeText={(value) => {
confirmPasswordRef.current = value;
if (value !== passwordRef.current) {
setPasswordsDontMatch(true);
} else {
setPasswordsDontMatch(false);
}
}}
onSubmit={() => {
onPress();
}}
placeholder={strings.confirmPassword()}
/>
</View>
) : null}
{biometricUnlock && !isBiometryEnrolled && !isCreateVault ? (
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
) : null}
{!biometricUnlock &&
!isBiometryEnrolled &&
isBiometryAvailable &&
(requestType === VaultRequestType.CopyNote ||
requestType === VaultRequestType.DeleteNote ||
requestType === VaultRequestType.ShareNote ||
requestType === VaultRequestType.CustomAction ||
requestType === VaultRequestType.GoToEditor ||
requestType === VaultRequestType.PermanentUnlock ||
requestType === VaultRequestType.LockNote) ? (
<Button
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>
<DialogButtons
onPressNegative={close}
onPressPositive={onPress}
loading={loading}
positiveType={positiveButtonTypeRef.current}
positiveTitle={buttonTitleRef.current || strings.unlock()}
/>
</View>
<Toast context="local" />
</BaseDialog>
);
};

View File

@@ -50,7 +50,7 @@ export const Header = ({
onLeftMenuButtonPress?: () => void;
renderedInRoute?: RouteName;
id?: string;
title: string;
title?: string;
canGoBack?: boolean;
onPressDefaultRightButton?: () => void;
hasSearch?: boolean;
@@ -114,7 +114,16 @@ export const Header = ({
onLeftButtonPress={onLeftMenuButtonPress}
/>
{hasSearch ? (
{!title ? (
<View
style={{
width: 100,
backgroundColor: colors.primary.hover,
height: 10,
borderRadius: 100
}}
/>
) : hasSearch ? (
<Paragraph>
{selectionMode
? `${selectedItemsList.length} selected`

View File

@@ -252,14 +252,16 @@ const NoteItem = ({
{reminder ? (
<ReminderTime
reminder={reminder}
disabled
color={color?.colorCode}
textStyle={{
fontSize: AppFontSize.xxxs
fontSize: AppFontSize.xxs
}}
iconSize={AppFontSize.xxxs}
short
iconSize={AppFontSize.xxs}
style={{
height: "auto"
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
alignSelf: "flex-start"
}}
/>
) : null}

View File

@@ -131,7 +131,7 @@ function ReorderableList<T extends { id: string }>({
]
);
function getOrderedItems() {
const getOrderedItems = React.useCallback(() => {
if (!customizableSidebarFeature?.isAllowed) return data;
const items: T[] = [];
itemOrderState.forEach((id) => {
@@ -142,7 +142,7 @@ function ReorderableList<T extends { id: string }>({
items.push(...data.filter((i) => !itemOrderState.includes(i.id)));
return items;
}
}, [data, customizableSidebarFeature?.isAllowed]);
return (
<View style={styles.container}>

View File

@@ -18,8 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFormattedDate } from "@notesnook/common";
import {
EncryptedContentItem,
isEncryptedContent,
Note,
UnencryptedContentItem
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import KeepAwake from "@sayem314/react-native-keep-awake";
import { diff } from "diffblazer";
import React, { useEffect, useRef, useState } from "react";
import { SafeAreaView, Text, View } from "react-native";
import Animated from "react-native-reanimated";
@@ -32,13 +40,17 @@ import { DDS } from "../../services/device-detection";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
eUnSubscribeEvent,
openVault,
VaultRequestType
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync";
import { useSettingStore } from "../../stores/use-setting-store";
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import BaseDialog from "../dialog/base-dialog";
import DialogButtons from "../dialog/dialog-buttons";
import DialogContainer from "../dialog/dialog-container";
@@ -47,46 +59,55 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import { presentDialog } from "../dialog/functions";
const MergeConflicts = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [keep, setKeep] = useState(null);
const [copy, setCopy] = useState(null);
const [selectedContent, setSelectedContent] =
useState<UnencryptedContentItem>();
const [copyOfDiscardedContent, setCopyOfDiscardedContent] =
useState<UnencryptedContentItem>();
const [dialogVisible, setDialogVisible] = useState(false);
const insets = useGlobalSafeAreaInsets();
const content = useRef(null);
const isKeepingConflicted = !keep?.conflicted;
const isKeeping = !!keep;
const content = useRef<UnencryptedContentItem>(null);
const { height } = useSettingStore((state) => state.dimensions);
const applyChanges = async () => {
let _content = keep;
let note = await db.notes.note(_content.noteId);
let contentToSave = selectedContent;
if (!contentToSave) return;
let note = await db.notes.note(contentToSave.noteId);
if (!note) return;
await db.notes.add({
id: note.id,
conflicted: false,
dateEdited: _content.dateEdited
dateEdited: contentToSave.dateEdited
});
await db.content.add({
id: note.contentId,
data: _content.data,
type: _content.type,
dateResolved: content.current?.conflicted?.dateModified || Date.now(),
sessionId: Date.now(),
conflicted: false
});
const noteLocked = await db.vaults.itemExists(note);
if (copy) {
if (noteLocked) {
await db.vault.save({
...contentToSave,
sessionId: `${Date.now()}`
});
} else {
await db.content.add({
id: note.contentId,
data: contentToSave.data,
type: contentToSave.type,
dateResolved: content.current?.conflicted?.dateModified || Date.now(),
sessionId: `${Date.now()}`,
conflicted: undefined
});
}
if (copyOfDiscardedContent) {
await db.notes.add({
title: note.title + " (Copy)",
content: {
data: copy.data,
type: copy.type
data: copyOfDiscardedContent.data,
type: copyOfDiscardedContent.type
}
});
}
@@ -103,15 +124,88 @@ const MergeConflicts = () => {
Sync.run();
};
const show = async (item) => {
let noteContent = await db.content.get(item.contentId);
content.current = { ...noteContent };
if (__DEV__) {
if (!noteContent.conflicted) {
content.current.conflicted = { ...noteContent };
const show = async (item: Note) => {
const isLocked = await db.vaults.itemExists(item);
let noteContent: UnencryptedContentItem;
if (isLocked) {
openVault({
requestType: VaultRequestType.CustomAction,
item: item,
title: strings.unlockNote(),
customActionTitle: strings.unlockNote(),
customActionParagraph: strings.unlockNoteToMergeConflicts(),
buttonTitle: strings.unlock(),
onUnlock: async (item, password) => {
if (!item || !password) return;
const currentContent = await db.content.get(item.contentId!);
try {
noteContent = {
...(await db.content.get(item.contentId!)),
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
content.current = noteContent;
if (__DEV__) {
if (!noteContent?.conflicted) {
content.current.conflicted = noteContent;
}
}
setVisible(true);
} catch (e) {
presentDialog({
input: true,
inputPlaceholder: strings.enterPassword(),
title: strings.unlockIncomingNote(),
paragraph: strings.unlockIncomingNoteDesc(),
positiveText: "Unlock",
positivePress: async (password) => {
try {
noteContent = {
...(await db.content.get(item.contentId!)),
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
content.current = noteContent;
if (__DEV__) {
if (!noteContent?.conflicted) {
content.current.conflicted = noteContent;
}
}
setVisible(true);
return true;
} catch (e) {
return false;
}
}
});
}
}
});
} else {
noteContent = (await db.content.get(
item.contentId!
)) as UnencryptedContentItem;
content.current = noteContent;
if (__DEV__) {
if (!noteContent?.conflicted) {
content.current.conflicted = noteContent;
}
}
setVisible(true);
}
setVisible(true);
};
useEffect(() => {
@@ -123,8 +217,8 @@ const MergeConflicts = () => {
const close = () => {
setVisible(false);
setCopy(null);
setKeep(null);
setCopyOfDiscardedContent(undefined);
setSelectedContent(undefined);
setDialogVisible(false);
};
@@ -134,6 +228,12 @@ const MergeConflicts = () => {
back,
isCurrent,
contentToKeep
}: {
isDiscarded: boolean;
keeping: boolean;
back: boolean;
isCurrent: boolean;
contentToKeep: UnencryptedContentItem;
}) => {
return (
<View
@@ -194,7 +294,7 @@ const MergeConflicts = () => {
{isDiscarded ? (
<Button
onPress={() => {
setCopy(contentToKeep);
setCopyOfDiscardedContent(contentToKeep);
setDialogVisible(true);
}}
title={strings.saveACopy()}
@@ -222,7 +322,6 @@ const MergeConflicts = () => {
paddingHorizontal: DefaultAppStyles.GAP
}}
fontSize={AppFontSize.xs}
color={colors.error.paragraph}
onPress={() => {
setDialogVisible(true);
}}
@@ -244,7 +343,9 @@ const MergeConflicts = () => {
keeping && !isDiscarded ? strings.undo() : strings.keep()
}
onPress={() => {
setKeep(keeping && !isDiscarded ? null : contentToKeep);
setSelectedContent(
keeping && !isDiscarded ? undefined : contentToKeep
);
}}
/>
</>
@@ -258,7 +359,6 @@ const MergeConflicts = () => {
<BaseDialog
statusBarTranslucent
transparent={false}
animationType="slide"
animated={false}
bounce={false}
onRequestClose={() => {
@@ -266,15 +366,9 @@ const MergeConflicts = () => {
}}
centered={false}
background={colors?.primary.background}
supportedOrientations={[
"portrait",
"portrait-upside-down",
"landscape",
"landscape-left",
"landscape-right"
]}
visible={true}
>
<Dialog context="merge-conflicts" />
<SafeAreaView
style={{
backgroundColor: colors.primary.background,
@@ -301,15 +395,15 @@ const MergeConflicts = () => {
style={{
height: "100%",
width: "100%",
backgroundColor: DDS.isLargeTablet() ? "rgba(0,0,0,0.3)" : null
backgroundColor: DDS.isLargeTablet() ? "rgba(0,0,0,0.3)" : undefined
}}
>
<ConfigBar
back={true}
isCurrent={true}
isDiscarded={isKeeping && isKeepingConflicted}
keeping={isKeeping}
contentToKeep={content.current}
isDiscarded={!!selectedContent && !selectedContent.conflicted}
keeping={!!selectedContent}
contentToKeep={content.current!}
/>
<Animated.View
@@ -323,15 +417,18 @@ const MergeConflicts = () => {
<ReadonlyEditor
editorId="conflictPrimary"
onLoad={async (loadContent) => {
const note = await db.notes.note(content.current?.noteId);
const note = await db.notes.note(content.current!.noteId);
if (!note) return;
loadContent({
id: note.id,
data: diff(
content.current.conflicted.data,
content.current.data
)
});
if (content.current && content.current.conflicted) {
loadContent({
id: note.id,
data: diff(
(content.current.conflicted as UnencryptedContentItem)
.data,
content.current.data
)
});
}
}}
/>
</Animated.View>
@@ -339,9 +436,11 @@ const MergeConflicts = () => {
<ConfigBar
back={false}
isCurrent={false}
isDiscarded={isKeeping && !isKeepingConflicted}
keeping={isKeeping}
contentToKeep={content.current.conflicted}
isDiscarded={!!selectedContent && !!selectedContent.conflicted}
keeping={!!selectedContent}
contentToKeep={
content.current!.conflicted! as UnencryptedContentItem
}
/>
<Animated.View
@@ -354,11 +453,12 @@ const MergeConflicts = () => {
<ReadonlyEditor
editorId="conflictSecondary"
onLoad={async (loadContent) => {
const note = await db.notes.note(content.current?.noteId);
const note = await db.notes.note(content.current?.noteId!);
if (!note) return;
loadContent({
id: note.id,
data: content.current.conflicted.data
data: (content.current!.conflicted as UnencryptedContentItem)
.data
});
}}
/>

View File

@@ -47,7 +47,7 @@ const HistoryItem = ({
}: {
index: number;
items?: VirtualizedGrouping<HistorySession>;
note?: Note;
note: Note;
}) => {
const [item] = useDBItem(index, "noteHistory", items);
const { colors } = useThemeColors();
@@ -73,6 +73,7 @@ const HistoryItem = ({
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
note={note}
/>
),
context: "note_history"
@@ -135,7 +136,7 @@ export default function NoteHistory({
const renderItem = useCallback(
({ index }: { index: number }) => (
<HistoryItem index={index} items={history} />
<HistoryItem index={index} items={history} note={note} />
),
[history]
);

View File

@@ -37,21 +37,43 @@ 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,
SessionContentItem,
TrashOrItem
} from "@notesnook/core";
/**
*
* @param {any} param0
* @returns
*/
export default function NotePreview({ session, content, note }) {
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") {
if ((await db.trash.restore(note.id)) === false) return;
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
useSelectionStore.getState().setSelectionMode();
ToastManager.show({
heading: strings.noteRestored(),
type: "success"
@@ -59,24 +81,26 @@ export default function NotePreview({ session, content, note }) {
eSendEvent(eCloseSheet);
return;
}
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
});
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();
eSendEvent(eCloseSheet, "note_history");
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: strings.noteRestoredFromHistory(),
type: "success"
});
ToastManager.show({
heading: strings.noteRestoredFromHistory(),
type: "success"
});
}
}
useEffect(() => {
@@ -91,15 +115,17 @@ export default function NotePreview({ session, content, note }) {
negativeText: strings.cancel(),
context: "local",
positivePress: async () => {
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
heading: strings.noteDeleted(),
type: "success",
context: "local"
});
eSendEvent(eCloseSheet);
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"
});
@@ -113,8 +139,11 @@ export default function NotePreview({ session, content, note }) {
}}
>
<Dialog context="local" />
<DialogHeader padding={12} title={note?.title || session?.session} />
{!session?.locked && !locked ? (
<DialogHeader
padding={12}
title={content?.title || note.title || session?.session}
/>
{!session?.locked && !locked && content?.data ? (
<View
style={{
flex: 1,
@@ -125,17 +154,27 @@ export default function NotePreview({ session, content, note }) {
editorId="historyPreview"
onLoad={async (loadContent) => {
try {
if (content.data) {
const _note = note || (await db.notes.note(session?.noteId));
const currentContent = await db.content.get(_note.contentId);
loadContent({
data: diff(currentContent.data, content.data),
id: _note.id
});
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,
e as Error,
"Failed to load history preview",
"local"
);
@@ -153,7 +192,9 @@ export default function NotePreview({ session, content, note }) {
}}
>
<Paragraph color={colors.secondary.paragraph}>
{strings.encryptedNoteHistoryNotice()}
{!content?.data
? strings.noContent()
: strings.encryptedNoteHistoryNotice()}
</Paragraph>
</View>
)}

View File

@@ -76,6 +76,7 @@ import { IconButton } from "../ui/icon-button";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { db } from "../../common/database";
const Steps = {
select: 1,
@@ -139,12 +140,12 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
}, [isFocused, step]);
useEffect(() => {
const sub = EV.subscribe(
const sub = db.eventManager.subscribe(
EVENTS.userSubscriptionUpdated,
(sub: User["subscription"]) => {
if (sub.plan === SubscriptionPlan.FREE) return;
if (routeParams.context === "signup") {
Navigation.replace("FluidPanelsView", {});
Navigation.navigate("FluidPanelsView", {});
} else {
Navigation.goBack();
}
@@ -182,8 +183,9 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
>
<IconButton
name="close"
color={colors.primary.icon}
onPress={() => {
Navigation.replace("FluidPanelsView", {});
Navigation.navigate("FluidPanelsView", {});
}}
/>
</View>
@@ -196,7 +198,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
return;
}
if (routeParams.context === "signup") {
Navigation.replace("FluidPanelsView", {});
Navigation.navigate("FluidPanelsView", {});
} else {
Navigation.goBack();
}
@@ -654,7 +656,7 @@ After trying all the privacy security oriented note taking apps, for the price a
type="accent"
onPress={() => {
if (routeParams.context === "signup") {
Navigation.replace("FluidPanelsView", {});
Navigation.navigate("FluidPanelsView", {});
} else {
Navigation.goBack();
}
@@ -1006,8 +1008,8 @@ const PricingPlanCard = ({
: "monthly"
}`
: pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: (product?.productId as string)
? (WebPlan?.period as string)
: (product?.productId as string)
);
setStep(Steps.buy);
}}

View File

@@ -1,72 +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 { 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";
export const DateMeta = ({ item }) => {
const { colors } = useThemeColors();
function getDateMeta() {
let keys = Object.keys(item);
if (keys.includes("dateEdited"))
keys.splice(
keys.findIndex((k) => k === "dateModified"),
1
);
return keys.filter((key) => key.startsWith("date") && key !== "date");
}
const renderItem = (key) =>
!item[key] ? null : (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2
}}
>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{strings.dateDescFromKey(key)}
</Paragraph>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{getFormattedDate(item[key], "date-time")}
</Paragraph>
</View>
);
return (
<View
style={{
borderTopWidth: 1,
borderTopColor: colors.primary.border,
paddingHorizontal: DefaultAppStyles.GAP,
paddingTop: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
{getDateMeta().map(renderItem)}
</View>
);
};

View File

@@ -0,0 +1,130 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useState } from "react";
import { View } from "react-native";
import { 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 AppIcon from "../ui/AppIcon";
export const DateMeta = ({ item }: { item: Item }) => {
const { colors, isDark } = useThemeColors();
const [isDatePickerVisible, setIsDatePickerVisible] = useState(false);
const [dateCreated, setDateCreated] = useState(item.dateCreated);
function getDateMeta() {
let keys = Object.keys(item);
if (keys.includes("dateEdited"))
keys.splice(
keys.findIndex((k) => k === "dateModified"),
1
);
return keys.filter((key) => key.startsWith("date") && key !== "date");
}
const renderItem = (key: string) =>
!item[key as keyof Item] ? null : (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2
}}
>
<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);
}
}
>
{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>
);
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}
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 File

@@ -39,6 +39,7 @@ 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();
@@ -203,6 +204,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
/>
) : null}
<SheetProvider context="properties" />
<Dialog context="properties" />
</View>
)}
/>

View File

@@ -57,6 +57,8 @@ const BOTTOM_BAR_ITEMS: ActionId[] = [
"export",
"copy-link",
"duplicate",
"launcher-shortcut",
"expiry-date",
"trash"
];
@@ -76,6 +78,7 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
"reorder",
"rename-color",
"rename-tag",
"launcher-shortcut",
"restore",
"trash",
"delete"
@@ -170,15 +173,15 @@ export const Items = ({
DDS.isTab
? AppFontSize.xxl
: shouldShrink
? AppFontSize.xxl
: AppFontSize.lg
? AppFontSize.xxl
: AppFontSize.lg
}
color={
item.checked
? item.activeColor || colors.primary.accent
: item.id.match(/(delete|trash)/g)
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
</Pressable>
@@ -209,8 +212,8 @@ export const Items = ({
text: item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.paragraph
: colors.primary.paragraph
? colors.error.paragraph
: colors.primary.paragraph
}}
testID={"icon-" + item.id}
onPress={item.onPress}
@@ -274,8 +277,8 @@ export const Items = ({
item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
@@ -371,7 +374,11 @@ export const Items = ({
style={{
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP,
gap: 5
gap: 5,
/**
* ((columnItemWidth + gap) * noOfItems) - padding
*/
width: (columnItemWidth + 5) * 5 - 12
}}
>
{item.map(renderTopBarItem)}

View File

@@ -102,9 +102,17 @@ const TabItemComponent = (props: {
{props.tab.session?.noteLocked ? (
<>
{props.tab.session?.locked ? (
<Icon size={AppFontSize.md} name="lock" />
<Icon
size={AppFontSize.md}
name="lock"
color={colors.primary.icon}
/>
) : (
<Icon size={AppFontSize.md} name="lock-open-outline" />
<Icon
size={AppFontSize.md}
name="lock-open-outline"
color={colors.primary.icon}
/>
)}
</>
) : null}

View File

@@ -78,12 +78,15 @@ const PublishNoteSheet = ({
const isFeatureAvailable = useIsFeatureAvailable("monographAnalytics");
const [isLocked, setIsLocked] = useState(false);
const [publishing, setPublishing] = useState(false);
const customTitle = useRef<string>("");
const pwdInput = useRef<TextInput>(null);
const titleInput = useRef<TextInput>(null);
const passwordValue = useRef<string>(undefined);
const monographData = useAsync(async () => {
return fetchMonographData(note?.id);
}, []);
const monograph = monographData.result?.monograph;
customTitle.current = monograph?.title || note.title || "";
const publishUrl = monograph && `${hosts.MONOGRAPH_HOST}/${monograph?.id}`;
const isPublished = db.monographs.monograph(note?.id);
@@ -108,7 +111,7 @@ const PublishNoteSheet = ({
try {
if (note?.id) {
if (isLocked && !passwordValue.current) return;
await db.monographs.publish(note.id, {
await db.monographs.publish(note.id, customTitle.current, {
selfDestruct: selfDestruct,
password: isLocked ? passwordValue.current : undefined
});
@@ -129,7 +132,6 @@ const PublishNoteSheet = ({
setPublishLoading(false);
};
const setPublishLoading = (value: boolean) => {
setPublishing(value);
};
@@ -245,6 +247,13 @@ const PublishNoteSheet = ({
</TouchableOpacity>
) : null}
<Input
fwdRef={titleInput}
onChangeText={(value) => (customTitle.current = value)}
defaultValue={customTitle.current}
placeholder={strings.noteTitle()}
/>
<TouchableOpacity
onPress={() => {
if (publishing) return;

View File

@@ -169,7 +169,7 @@ class RecoveryKeySheet extends React.Component {
};
onOpen = async () => {
let k = await db.user.getEncryptionKey();
let k = await db.user.getMasterKey();
this.user = await db.user.getUser();
if (k) {
this.setState({

View File

@@ -30,6 +30,7 @@ import ReorderableList from "../list/reorderable-list";
import { Properties } from "../properties";
import { useSideBarDraggingStore } from "./dragging-store";
import { MenuItem } from "./menu-item";
import { Default_Drag_Action } from "../../hooks/use-actions";
export const ColorSection = React.memo(
function ColorSection() {
@@ -59,7 +60,7 @@ export const ColorSection = React.memo(
const onLongPress = React.useCallback((item: SideMenuItem) => {
if (useSideBarDraggingStore.getState().dragging) return;
Properties.present(item.data as Color);
Properties.present(item.data as Color, false, [Default_Drag_Action]);
}, []);
const renderIcon = React.useCallback((item: SideMenuItem, size: number) => {

View File

@@ -21,15 +21,6 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import {
NavigationState,
Route,
SceneMap,
SceneRendererProps,
TabDescriptor,
TabView
} from "react-native-tab-view";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useGroupOptions } from "../../hooks/use-group-options";
@@ -59,7 +50,104 @@ import SettingsService from "../../services/settings";
import { isFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
const renderScene = SceneMap({
/**
* Simple Tab View Implementation for the Side bar
*/
type SimpleRoute = {
key: string;
title?: string;
};
type SimpleNavigationState = {
index: number;
routes: SimpleRoute[];
};
type SimpleTabBarProps = {
navigationState: SimpleNavigationState;
jumpTo: (key: string) => void;
};
type SimpleTabViewProps = {
navigationState: SimpleNavigationState;
renderScene: ({ route }: { route: SimpleRoute }) => React.ReactNode;
renderTabBar?: (props: SimpleTabBarProps) => React.ReactNode;
onIndexChange?: (index: number) => void;
};
const createSceneMap = (
scenes: Record<string, React.ComponentType<any>>
): ((props: { route: SimpleRoute }) => React.ReactNode) => {
return ({ route }: { route: SimpleRoute }) => {
const SceneComponent = scenes[route.key];
if (!SceneComponent) return null;
return <SceneComponent />;
};
};
const SimpleTabView = ({
navigationState,
renderScene,
renderTabBar,
onIndexChange
}: SimpleTabViewProps) => {
const loadedKeysRef = React.useRef(new Set<string>());
const scenesRef = React.useRef(new Map<string, React.ReactNode>());
const jumpTo = React.useCallback(
(key: string) => {
const nextIndex = navigationState.routes.findIndex(
(route) => route.key === key
);
if (nextIndex !== -1 && nextIndex !== navigationState.index) {
onIndexChange?.(nextIndex);
}
},
[navigationState.index, navigationState.routes, onIndexChange]
);
const activeKey = navigationState.routes[navigationState.index]?.key;
if (activeKey && !loadedKeysRef.current.has(activeKey)) {
loadedKeysRef.current.add(activeKey);
}
const getSceneForRoute = React.useCallback(
(route: SimpleRoute) => {
const cached = scenesRef.current.get(route.key);
if (cached) return cached;
const created = renderScene({ route });
scenesRef.current.set(route.key, created);
return created;
},
[renderScene]
);
return (
<View style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
{navigationState.routes.map((route, routeIndex) => (
<View
key={route.key}
style={{
flex: 1,
display: navigationState.index === routeIndex ? "flex" : "none"
}}
>
{loadedKeysRef.current.has(route.key)
? getSceneForRoute(route)
: navigationState.index === routeIndex
? getSceneForRoute(route)
: null}
</View>
))}
</View>
{renderTabBar ? renderTabBar({ navigationState, jumpTo }) : null}
</View>
);
};
const renderScene = createSceneMap({
home: SideMenuHome,
notebooks: SideMenuNotebooks,
tags: SideMenuTags,
@@ -73,7 +161,7 @@ export const SideMenu = React.memo(
const [index, setIndex] = React.useState(
SettingsService.getProperty("defaultSidebarTab")
);
const [routes] = React.useState<Route[]>([
const [routes] = React.useState<SimpleRoute[]>([
{
key: "home",
title: "Home"
@@ -98,15 +186,11 @@ export const SideMenu = React.memo(
paddingLeft: insets.left
}}
>
<TabView
<SimpleTabView
navigationState={{ index, routes }}
renderTabBar={(props) => <TabBar {...props} />}
tabBarPosition="bottom"
renderScene={renderScene}
onIndexChange={setIndex}
swipeEnabled={false}
animationEnabled={false}
lazy
/>
</View>
);
@@ -114,12 +198,7 @@ export const SideMenu = React.memo(
() => true
);
const TabBar = (
props: SceneRendererProps & {
navigationState: NavigationState<Route>;
options: Record<string, TabDescriptor<Route>> | undefined;
}
) => {
const TabBar = (props: SimpleTabBarProps) => {
const dragging = useSideBarDraggingStore((state) => state.dragging);
const { colors, isDark } = useThemeColors();
const groupOptions = useGroupOptions(

View File

@@ -37,7 +37,7 @@ import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { useSideBarDraggingStore } from "./dragging-store";
function _MenuItem({
export function MenuItem({
item,
index,
testID,
@@ -179,12 +179,3 @@ function _MenuItem({
</Pressable>
);
}
export const MenuItem = React.memo(_MenuItem, (prev, next) => {
if (
prev.item.id !== next.item.id &&
prev.item.data?.dateModified !== next.item.data?.dateModified
)
return false;
return true;
});

View File

@@ -241,6 +241,7 @@ export const NotebookItem = ({
name="plus"
size={AppFontSize.md}
testID={`add-notebook-${index}`}
color={colors.primary.icon}
top={0}
left={0}
bottom={0}

View File

@@ -31,6 +31,9 @@ import ReorderableList from "../list/reorderable-list";
import { MenuItem } from "./menu-item";
import { useThemeColors } from "@notesnook/theme";
import { DefaultAppStyles } from "../../utils/styles";
import { useSideBarDraggingStore } from "./dragging-store";
import { Properties } from "../properties";
import { Default_Drag_Action } from "../../hooks/use-actions";
export const PinnedSection = React.memo(
function PinnedSection() {
@@ -57,6 +60,11 @@ export const PinnedSection = React.memo(
});
}, []);
const onLongPress = React.useCallback((item: SideMenuItem) => {
if (useSideBarDraggingStore.getState().dragging) return;
Properties.present(item.data as Notebook, false, [Default_Drag_Action]);
}, []);
const menuItems = useMemo(
() =>
menuPins.map((item) => ({
@@ -65,7 +73,8 @@ export const PinnedSection = React.memo(
icon: item.type === "notebook" ? "notebook-outline" : "pound",
dataType: item.type,
data: item,
onPress: onPress
onPress: onPress,
onLongPress: onLongPress
})) as SideMenuItem[],
[menuPins, onPress]
);
@@ -98,6 +107,7 @@ export const PinnedSection = React.memo(
flexGrow: 1,
width: "100%"
}}
disableDefaultDrag
contentContainerStyle={{
flexGrow: 1
}}

View File

@@ -85,6 +85,7 @@ export function SideMenuHome() {
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
keyExtractor={() => "scroll-items"}
bounces={false}
renderItem={() => (
<>
<ReorderableList

View File

@@ -303,7 +303,7 @@ export const SideMenuTags = () => {
} catch (e) {
DatabaseLogger.error(e);
}
}, 500);
}, 100);
}}
placeholderTextColor={colors.primary.placeholder}
/>

View File

@@ -107,7 +107,7 @@ export const IconButton = ({
color={
restProps.disabled
? RGB_Linear_Shade(-0.05, hexToRGBA(colors.secondary.background))
: colors.static[color as never] || color
: colors.static[color as never] || color || colors.primary.icon
}
size={size}
/>

View File

@@ -41,7 +41,9 @@ export const ReminderTime = ({
} & ButtonProps) => {
const { colors } = useThemeColors();
const reminder = props.reminder;
const time = !reminder ? undefined : getFormattedReminderTime(reminder);
const time = !reminder
? undefined
: getFormattedReminderTime(reminder, props.short || false);
const isTodayOrTomorrow =
(time?.includes("Today") || time?.includes("Tomorrow")) &&
!time?.includes("Last");

View File

@@ -159,7 +159,7 @@ const SheetWrapper = ({
{bottomPadding ? (
<View
style={{
height: bottomInsets
height: 10
}}
/>
) : null}

View File

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

View File

@@ -17,12 +17,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-inner-declarations */
import { useAreFeaturesAvailable } from "@notesnook/common";
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
import {
Color,
createInternalLink,
Item,
ItemReference,
Note,
Notebook,
VAULT_ERRORS
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
@@ -30,7 +32,7 @@ import { useThemeColors } from "@notesnook/theme";
import { DisplayedNotification } from "@notifee/react-native";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { InteractionManager, Platform } from "react-native";
import { InteractionManager, Platform, View } from "react-native";
import Share from "react-native-share";
import { DatabaseLogger, db } from "../common/database";
import { AttachmentDialog } from "../components/attachments";
@@ -52,7 +54,8 @@ import {
eSubscribeEvent,
openVault,
presentSheet,
ToastManager
ToastManager,
VaultRequestType
} from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
@@ -65,11 +68,13 @@ import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { useTagStore } from "../stores/use-tag-store";
import { useUserStore } from "../stores/use-user-store";
import { eUpdateNoteInEditor } from "../utils/events";
import { eCloseSheet, eUpdateNoteInEditor } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
import { resetStoredState } from "./use-stored-state";
import { NotesnookModule } from "../utils/notesnook-module";
import DatePickerComponent from "../components/date-picker";
export type ActionId =
| "select"
@@ -114,7 +119,9 @@ export type ActionId =
| "remove-from-notebook"
| "trash"
| "default-homepage"
| "default-tag";
| "default-tag"
| "launcher-shortcut"
| "expiry-date";
export type Action = {
id: ActionId;
@@ -130,6 +137,31 @@ export type Action = {
locked?: boolean;
};
export const Default_Drag_Action: Action = {
id: "reorder",
title: strings.reorder(),
icon: "sort-ascending",
onPress: async () => {
const feature = await isFeatureAvailable("customizableSidebar");
if (feature && !feature.isAllowed) {
ToastManager.show({
message: feature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(feature);
}
});
return;
}
useSideBarDraggingStore.setState({
dragging: true
});
eSendEvent(eCloseSheet);
}
};
function isNotePinnedInNotifications(item: Item) {
const pinned = Notifications.getPinnedNotes();
if (!pinned || pinned.length === 0) {
@@ -158,10 +190,12 @@ export const useActions = ({
"shortcuts",
"notebooks",
"customizableSidebar",
"customHomepage"
"customHomepage",
"androidLauncherShortcuts",
"expiringNotes"
]);
const [item, setItem] = useState(propItem);
const { colors } = useThemeColors();
const { colors, isDark } = useThemeColors();
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
db.shortcuts.exists(item.id)
@@ -297,7 +331,6 @@ export const useActions = ({
id: item.id,
title: value
});
eSendEvent(Navigation.routeNames.TaggedNotes);
InteractionManager.runAfterInteractions(() => {
useTagStore.getState().refresh();
@@ -376,12 +409,12 @@ export const useActions = ({
if (item.type === "note" && (await db.vaults.itemExists(item))) {
openVault({
deleteNote: true,
novault: true,
locked: true,
requestType: VaultRequestType.DeleteNote,
item: item,
title: strings.deleteNote(),
description: strings.unlockToDelete()
description: strings.unlockToDelete(),
buttonTitle: strings.delete(),
positiveButtonType: "errorShade"
});
} else {
try {
@@ -435,31 +468,6 @@ export const useActions = ({
icon: "square-edit-outline",
onPress: renameColor
});
actions.push({
id: "reorder",
title: strings.reorder(),
icon: "sort-ascending",
onPress: async () => {
if (features && !features.customizableSidebar.isAllowed) {
ToastManager.show({
message: features.customizableSidebar.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features.customizableSidebar);
}
});
return;
}
useSideBarDraggingStore.setState({
dragging: true
});
close();
},
locked: !features?.customizableSidebar.isAllowed
});
}
if (item.type === "reminder") {
@@ -706,26 +714,6 @@ export const useActions = ({
type: item.type
}
);
resetStoredState(
"app-home-navigtion-key",
isHomepage
? undefined
: {
name:
item.type === "notebook"
? "Notebook"
: item.type === "tag"
? "TaggedNotes"
: item.type === "color"
? "ColorNotes"
: undefined,
params: {
item: item,
id: item.id
}
}
);
}
});
}
@@ -880,11 +868,10 @@ export const useActions = ({
close();
await sleep(300);
openVault({
requestType: VaultRequestType.ShareNote,
item: item,
novault: true,
locked: true,
share: true,
title: strings.shareNote()
title: strings.shareNote(),
buttonTitle: strings.share()
});
} else {
processingId.current = "shareNote";
@@ -909,11 +896,10 @@ export const useActions = ({
close();
await sleep(300);
openVault({
requestType: VaultRequestType.PermanentUnlock,
item: item,
novault: true,
locked: true,
permanant: true,
title: strings.unlockNote()
title: strings.unlockNote(),
buttonTitle: strings.unlock()
});
return;
}
@@ -931,17 +917,18 @@ export const useActions = ({
switch ((e as Error).message) {
case VAULT_ERRORS.noVault:
openVault({
requestType: VaultRequestType.CreateVault,
item: item,
novault: false,
title: strings.createVault()
title: strings.createVault(),
buttonTitle: strings.lock()
});
break;
case VAULT_ERRORS.vaultLocked:
openVault({
requestType: VaultRequestType.LockNote,
item: item,
novault: true,
locked: true,
title: strings.lockNote()
title: strings.lockNote(),
buttonTitle: strings.lock()
});
break;
}
@@ -962,11 +949,10 @@ export const useActions = ({
close();
await sleep(300);
openVault({
copyNote: true,
novault: true,
locked: true,
item: item,
title: strings.copyNote()
requestType: VaultRequestType.CopyNote,
item: item as Note,
title: strings.copyNote(),
buttonTitle: strings.copy()
});
} else {
processingId.current = "copyContent";
@@ -1177,6 +1163,45 @@ export const useActions = ({
},
checked: item.archived,
isToggle: true
},
{
id: "expiry-date",
title: item.expiryDate ? strings.unsetExpiry() : strings.setExpiry(),
icon: item.expiryDate ? "bomb-off" : "bomb",
locked: !features?.expiringNotes?.isAllowed,
onPress: async () => {
if (item.expiryDate) {
await db.notes.setExpiryDate(null, item.id);
setItem((await db.notes.note(item.id)) as Item);
} else {
if (features && !features?.expiringNotes.isAllowed) {
ToastManager.show({
message: features?.expiringNotes.error,
type: "info",
actionText: strings.upgrade(),
context: "local",
func: () => {
PaywallSheet.present(features?.expiringNotes);
}
});
return;
}
presentDialog({
context: "properties",
component: (close) => (
<DatePickerComponent
onCancel={() => close?.()}
onConfirm={async (date) => {
close?.();
await db.notes.setExpiryDate(date.getTime(), item.id);
setItem((await db.notes.note(item.id)) as Item);
}}
/>
)
});
}
}
}
);
@@ -1207,5 +1232,44 @@ export const useActions = ({
});
}
if (
Platform.OS === "android" &&
(item.type === "tag" ||
item.type === "note" ||
item.type === "notebook" ||
item.type === "color")
) {
actions.push({
id: "launcher-shortcut",
title: strings.addToHome(),
icon: "cellphone-arrow-down",
locked: !features?.androidLauncherShortcuts.isAllowed,
onPress: async () => {
if (features && !features?.androidLauncherShortcuts.isAllowed) {
ToastManager.show({
message: features?.androidLauncherShortcuts.error,
type: "info",
actionText: strings.upgrade(),
context: "local",
func: () => {
PaywallSheet.present(features?.androidLauncherShortcuts);
}
});
return;
}
try {
await NotesnookModule.addShortcut(
item.id,
item.type,
item.title,
(item as Note).headline || (item as Notebook).description || "",
(item as Color).colorCode
);
} catch (e) {}
}
});
}
return actions;
};

View File

@@ -85,7 +85,7 @@ import Notifications from "../services/notifications";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
import Sync from "../services/sync";
import { clearAllStores, initAfterSync, initialize } from "../stores";
import { clearAllStores, initAfterSync } from "../stores";
import { refreshAllStores } from "../stores/create-db-collection-store";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useMessageStore } from "../stores/use-message-store";
@@ -94,6 +94,7 @@ import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors";
import { BETA } from "../utils/constants";
import {
eAfterSync,
eCloseSheet,
eEditorReset,
eLoginSessionExpired,
@@ -107,6 +108,9 @@ import { fluidTabsRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import useFeatureManager from "./use-feature-manager";
import { deleteDCacheFiles } from "../common/filesystem/io";
import dayjs from "dayjs";
import { useRelationStore } from "../stores/use-relation-store";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -159,7 +163,10 @@ const onUserSessionExpired = async () => {
eSendEvent(eLoginSessionExpired);
};
const onAppOpenedFromURL = async (event: { url: string }) => {
const onAppOpenedFromURL = async (event: {
url: string;
isInitialUrl?: boolean;
}) => {
const url = event.url;
try {
@@ -171,7 +178,7 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
eSendEvent(eOnLoadNote, { newNote: true });
fluidTabsRef.current?.goToPage("editor", false);
return;
} else if (url.startsWith("https://app.notesnook.com/open_note")) {
} else if (url.startsWith("https://app.notesnook.com/open_note?")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const note = await db.notes.note(id);
@@ -183,6 +190,53 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
fluidTabsRef.current?.goToPage("editor", false);
}
}
} else if (
url.startsWith("https://app.notesnook.com/open_notebook?") &&
!event.isInitialUrl
) {
const id = new URL(url).searchParams.get("id");
if (id) {
const notebook = await db.notebooks.notebook(id);
if (notebook) {
Navigation.navigate("Notebook", {
id: notebook.id,
canGoBack: true,
item: notebook
});
}
}
} else if (
url.startsWith("https://app.notesnook.com/open_tag?") &&
!event.isInitialUrl
) {
const id = new URL(url).searchParams.get("id");
if (id) {
const tag = await db.tags.tag(id);
if (tag) {
Navigation.navigate("TaggedNotes", {
type: "tag",
id: tag.id,
item: tag,
canGoBack: true
});
}
}
} else if (
url.startsWith("https://app.notesnook.com/open_color?") &&
!event.isInitialUrl
) {
const id = new URL(url).searchParams.get("id");
if (id) {
const color = await db.colors.color(id);
if (color) {
Navigation.navigate("ColoredNotes", {
type: "color",
id: color.id,
item: color,
canGoBack: true
});
}
}
} else if (url.startsWith("https://app.notesnook.com/open_reminder")) {
const id = new URL(url).searchParams.get("id");
if (id) {
@@ -279,6 +333,7 @@ const onLogout = async (reason: string) => {
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
eSendEvent(eAfterSync);
};
async function checkForShareExtensionLaunchedInBackground() {
@@ -435,6 +490,28 @@ const IsDatabaseMigrationRequired = () => {
return true;
};
let timer: NodeJS.Timeout | null = null;
let initialDate = -1;
async function expiringNotesTimer() {
if (timer != null) clearInterval(timer);
if (initialDate === -1) {
DatabaseLogger.info("Deleting expired notes at startup");
db.notes.deleteExpiredNotes();
initialDate = dayjs().date();
}
timer = setInterval(() => {
if (dayjs().date() != initialDate) {
DatabaseLogger.info("Deleting expired notes");
db.notes.deleteExpiredNotes();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
initialDate = dayjs().date();
}
}, 1000 * 60);
}
const initializeDatabase = async (password?: string) => {
if (useUserStore.getState().appLocked) return;
if (!db.isInitialized) {
@@ -454,15 +531,21 @@ const initializeDatabase = async (password?: string) => {
if (db.isInitialized) {
await setAppMessage();
useSettingStore.getState().setAppLoading(false);
useSettingStore.getState().refresh();
Notifications.setupReminders(true);
DatabaseLogger.info("Database initialized");
Notifications.restorePinnedNotes();
expiringNotesTimer();
deleteDCacheFiles();
}
Walkthrough.init();
};
export const useAppEvents = () => {
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const [isAppLoading, initialUrl] = useSettingStore((state) => [
state.isAppLoading,
state.initialUrl
]);
const [setLastSynced, setUser, appLocked, syncing] = useUserStore((state) => [
state.setLastSynced,
state.setUser,
@@ -480,6 +563,7 @@ export const useAppEvents = () => {
initialUrl: string;
backupDidWait: boolean;
isConnectingSSE: boolean;
attachmentsCachedOfflineMode: boolean;
}>
>({});
@@ -507,6 +591,15 @@ export const useAppEvents = () => {
};
}, [isAppLoading, onSyncComplete]);
useEffect(() => {
if (initialUrl) {
onAppOpenedFromURL({
url: initialUrl!,
isInitialUrl: true
});
}
}, [initialUrl]);
const subscribeToPurchaseListeners = useCallback(async () => {
if (Platform.OS === "android") {
try {
@@ -565,7 +658,11 @@ export const useAppEvents = () => {
}
}
if (SettingsService.getProperty("offlineMode")) {
if (
SettingsService.getProperty("offlineMode") &&
!refValues.current.attachmentsCachedOfflineMode
) {
refValues.current.attachmentsCachedOfflineMode = true;
db.attachments.cacheAttachments().catch(() => {
/* empty */
});
@@ -632,24 +729,39 @@ export const useAppEvents = () => {
useEffect(() => {
const subscriptions = [
EV.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
EV.subscribe(EVENTS.syncAborted, onSyncAborted),
EV.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
EV.subscribe(EVENTS.userLoggedOut, onLogout),
EV.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
EV.subscribe(EVENTS.userSessionExpired, onUserSessionExpired),
EV.subscribe(
db.eventManager.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
db.eventManager.subscribe(EVENTS.syncAborted, onSyncAborted),
db.eventManager.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
db.eventManager.subscribe(EVENTS.userLoggedOut, onLogout),
db.eventManager.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
db.eventManager.subscribe(
EVENTS.userSessionExpired,
onUserSessionExpired
),
db.eventManager.subscribe(
EVENTS.userSubscriptionUpdated,
onUserSubscriptionStatusChanged
),
EV.subscribe(EVENTS.fileDownload, onDownloadingAttachmentProgress),
EV.subscribe(EVENTS.fileUpload, onUploadingAttachmentProgress),
EV.subscribe(EVENTS.fileDownloaded, onDownloadedAttachmentProgress),
EV.subscribe(EVENTS.fileUploaded, onUploadedAttachmentProgress),
EV.subscribe(EVENTS.downloadCanceled, (data) => {
db.eventManager.subscribe(
EVENTS.fileDownload,
onDownloadingAttachmentProgress
),
db.eventManager.subscribe(
EVENTS.fileUpload,
onUploadingAttachmentProgress
),
db.eventManager.subscribe(
EVENTS.fileDownloaded,
onDownloadedAttachmentProgress
),
db.eventManager.subscribe(
EVENTS.fileUploaded,
onUploadedAttachmentProgress
),
db.eventManager.subscribe(EVENTS.downloadCanceled, (data) => {
useAttachmentStore.getState().setDownloading(data);
}),
EV.subscribe(EVENTS.uploadCanceled, (data) => {
db.eventManager.subscribe(EVENTS.uploadCanceled, (data) => {
useAttachmentStore.getState().setUploading(data);
}),
EV.subscribe(EVENTS.migrationStarted, (name) => {
@@ -675,7 +787,7 @@ export const useAppEvents = () => {
return;
endProgress();
}),
EV.subscribe(EVENTS.vaultLocked, async () => {
db.eventManager.subscribe(EVENTS.vaultLocked, async () => {
// Lock all notes in all tabs...
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
@@ -707,7 +819,6 @@ export const useAppEvents = () => {
return () => {
emitterSubscriptions.forEach((sub) => sub?.remove?.());
subscriptions.forEach((sub) => sub?.unsubscribe?.());
EV.unsubscribeAll();
};
}, [onSyncComplete, onUserUpdated]);
@@ -722,6 +833,7 @@ export const useAppEvents = () => {
if (state === "active") {
notifee.setBadgeCount(0);
updateStatusBarColor();
expiringNotesTimer();
checkAutoBackup();
Sync.run("global", false, "full");
reconnectSSE();
@@ -772,23 +884,10 @@ export const useAppEvents = () => {
}
};
if (!refValues.current.initialUrl) {
Linking.getInitialURL().then((url) => {
if (url) {
refValues.current.initialUrl = url;
}
});
}
let sub: NativeEventSubscription;
if (!isAppLoading && !appLocked) {
setTimeout(() => {
sub = AppState.addEventListener("change", onAppStateChanged);
if (refValues.current.initialUrl) {
onAppOpenedFromURL({
url: refValues.current.initialUrl!
});
refValues.current.initialUrl = undefined;
}
}, 1000);
refValues.current.removeInternetStateListener = NetInfo.addEventListener(

View File

@@ -60,23 +60,17 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
): [ItemTypeKey[T] | undefined, () => void] => {
const [item, setItem] = useState<ItemTypeKey[T]>();
const itemIdRef = useRef<string>(undefined);
const prevIdOrIndexRef = useRef<string | number>(undefined);
if (prevIdOrIndexRef.current !== idOrIndex) {
itemIdRef.current = undefined;
prevIdOrIndexRef.current = idOrIndex;
}
const itemsRef = useRef(item);
itemsRef.current = item;
useEffect(() => {
const onUpdateItem = async (itemId?: string) => {
if (typeof itemId === "string" && itemId !== itemIdRef.current) return;
if (!isValidIdOrIndex(idOrIndex)) return;
let item: ItemTypeKey[T] | undefined = undefined;
if (items && typeof idOrIndex === "number") {
const item = (await items.item(idOrIndex))?.item;
setItem(item);
itemIdRef.current = item?.id;
onItemUpdated?.(item);
item = (await items.item(idOrIndex))?.item;
} else {
if (!(db as any)[type + "s"][type]) {
console.warn(
@@ -84,24 +78,30 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
`db.${type}s.${type}(id: string)`
);
} else {
const item = await (db as any)[type + "s"]?.[type]?.(
idOrIndex as string
);
setItem(item);
itemIdRef.current = item.id;
onItemUpdated?.(item);
item = await (db as any)[type + "s"]?.[type]?.(idOrIndex as string);
}
}
};
if (
itemsRef.current === item ||
itemsRef.current?.dateModified === item?.dateModified
)
return;
setItem(item);
itemIdRef.current = item?.id;
onItemUpdated?.(item);
};
let unsub: (() => void) | undefined;
if (
useSettingStore.getState().isAppLoading &&
//@ts-ignore
!globalThis["IS_SHARE_EXTENSION"]
) {
useSettingStore.subscribe((state) => {
unsub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
onUpdateItem();
unsub?.();
unsub = undefined;
}
});
} else {
@@ -109,9 +109,10 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
}
eSubscribeEvent(eDBItemUpdate, onUpdateItem);
return () => {
unsub?.();
eUnSubscribeEvent(eDBItemUpdate, onUpdateItem);
};
}, [idOrIndex, type, items, onItemUpdated]);
}, [idOrIndex, type, items]);
return [
isValidIdOrIndex(idOrIndex) ? (item as ItemTypeKey[T]) : undefined,
@@ -134,8 +135,9 @@ export const useNoteLocked = (noteId: string | undefined) => {
//@ts-ignore
!globalThis["IS_SHARE_EXTENSION"]
) {
useSettingStore.subscribe((state) => {
let unsub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
unsub();
db.vaults
.itemExists({
type: "note",
@@ -181,25 +183,27 @@ export const useTotalNotes = (type: "notebook" | "tag" | "color") => {
const [totalNotesById, setTotalNotesById] = useState<{
[id: string]: number;
}>({});
const totalNotesRef = useRef(0);
const getTotalNotes = React.useCallback(
(ids: string[]) => {
if (!ids || !ids.length || !type) return;
db.relations
.from({ type: type, ids: ids as string[] }, ["note"])
.get()
.then((relations) => {
const totalNotesById: any = {};
for (const id of ids) {
totalNotesById[id] = relations.filter(
(relation) => relation.fromId === id && relation.toType === "note"
)?.length;
}
setTotalNotesById(totalNotesById);
});
},
[type]
);
const getTotalNotes = React.useCallback((ids: string[]) => {
if (!ids || !ids.length || !type) return;
db.relations
.from({ type: type, ids: ids as string[] }, ["note"])
.get()
.then((relations) => {
const totalNotesById: any = {};
for (const id of ids) {
totalNotesById[id] = relations.filter(
(relation) => relation.fromId === id && relation.toType === "note"
)?.length;
}
if (totalNotesById === totalNotesRef.current) return;
totalNotesRef.current = totalNotesById;
setTotalNotesById(totalNotesById);
});
}, []);
return {
totalNotes: (id: string) => {

View File

@@ -29,7 +29,6 @@ export type SyncProgressEventType = {
const useSyncProgress = () => {
const [progress, setProgress] = useState<SyncProgressEventType>();
const EV = db.eventManager;
const onProgress = useCallback(
({ type, current, total }: SyncProgressEventType) => {
@@ -42,13 +41,13 @@ const useSyncProgress = () => {
setProgress(undefined);
};
useEffect(() => {
EV?.subscribe(EVENTS.syncProgress, onProgress);
EV?.subscribe(EVENTS.syncCompleted, onSyncComplete);
db.eventManager.subscribe(EVENTS.syncProgress, onProgress);
db.eventManager.subscribe(EVENTS.syncCompleted, onSyncComplete);
return () => {
EV?.unsubscribe(EVENTS.syncProgress, onProgress);
EV?.unsubscribe(EVENTS.syncCompleted, onSyncComplete);
db.eventManager.unsubscribe(EVENTS.syncProgress, onProgress);
db.eventManager.unsubscribe(EVENTS.syncCompleted, onSyncComplete);
};
}, [EV, onProgress]);
}, [onProgress]);
return {
progress

View File

@@ -31,8 +31,6 @@ import { useSettingStore } from "../stores/use-setting-store";
import { rootNavigatorRef } from "../utils/global-refs";
import Navigation from "../services/navigation";
import { isFeatureAvailable } from "@notesnook/common";
import { useStoredValue } from "../hooks/use-stored-state";
import { db } from "../common/database";
const RootStack = createNativeStackNavigator();
const AppStack = createNativeStackNavigator();
@@ -58,105 +56,117 @@ const AppNavigation = React.memo(
() => {
const { colors } = useThemeColors();
const homepageV2 = useSettingStore((state) => state.settings.homepageV2);
const home = useStoredValue(
"app-home-navigtion-key",
!homepageV2 || homepageV2.id === DEFAULT_HOME.name
? DEFAULT_HOME
: undefined
);
const loading = useSettingStore((state) => state.isAppLoading);
const [home, setHome] = React.useState<
{ name: string; params: any } | undefined
>(undefined);
if (!home.value && !homepageV2) {
home.value = DEFAULT_HOME;
}
React.useEffect(() => {
if (!home) {
if (useSettingStore.getState().initialUrl) {
const url = useSettingStore.getState().initialUrl;
if (url?.startsWith("https://app.notesnook.com/open_notebook?")) {
const id = new URL(url).searchParams.get("id");
if (id) {
setHome({
name: "Notebook",
params: {
id: id
}
});
return;
}
} else if (url?.startsWith("https://app.notesnook.com/open_tag?")) {
const id = new URL(url).searchParams.get("id");
if (id) {
setHome({
name: "TaggedNotes",
params: {
type: "tag",
id: id
}
});
return;
}
} else if (url?.startsWith("https://app.notesnook.com/open_color?")) {
const id = new URL(url).searchParams.get("id");
if (id) {
setHome({
name: "ColoredNotes",
params: {
type: "color",
id: id
}
});
return;
}
}
}
if (homepageV2) {
switch (homepageV2.type) {
case "notebook": {
setHome({
name: "Notebook",
params: {
id: homepageV2.id
}
});
return;
}
case "color": {
setHome({
name: "ColoredNotes",
params: {
type: "color",
id: homepageV2.id
}
});
return;
}
case "tag": {
setHome({
name: "TaggedNotes",
params: {
type: "tag",
id: homepageV2.id
}
});
return;
}
case "default":
setHome({
name: homepageV2.id,
params: undefined
});
return;
}
} else {
setHome(DEFAULT_HOME);
}
}
}, []);
React.useEffect(() => {
if (!homepageV2 || loading) return;
(async () => {
isFeatureAvailable("customHomepage").then((value) => {
if (!value.isAllowed) {
home.value = DEFAULT_HOME;
SettingsService.setProperty("homepageV2", undefined);
}
});
if (!home.value) {
switch (homepageV2.type) {
case "notebook":
{
const notebook = await db.notebooks.notebook(homepageV2.id);
if (notebook) {
home.value = {
name: "Notebook",
params: {
item: notebook,
id: notebook.id,
title: notebook.title
}
};
return;
}
}
break;
case "color": {
const color = await db.colors.color(homepageV2.id);
if (color) {
home.value = {
name: "ColoredNotes",
params: {
item: color,
id: color.id,
title: color.title
}
};
return;
}
break;
}
case "tag": {
const tag = await db.tags.tag(homepageV2.id);
if (tag) {
home.value = {
name: "TaggedNotes",
params: {
item: tag,
id: tag.id,
title: tag.title
}
};
return;
}
break;
}
case "default":
{
home.value = DEFAULT_HOME;
}
return;
}
home.value = undefined;
setTimeout(() => {
home.value = DEFAULT_HOME;
});
isFeatureAvailable("customHomepage").then((value) => {
if (!value.isAllowed) {
SettingsService.setProperty("homepageV2", undefined);
}
})();
}, [homepageV2, loading, home.value]);
});
}, [homepageV2, loading]);
React.useEffect(() => {
if (!home) return;
useNavigationStore.getState().update(home?.name as keyof RouteParams);
useNavigationStore
.getState()
.update(home.value?.name as keyof RouteParams);
useNavigationStore
.getState()
.setFocusedRouteId(home.value?.params?.id || home.value?.name);
.setFocusedRouteId(home?.params?.id || home?.name);
}, [home]);
return !home.value ? null : (
return !home ? null : (
<AppStack.Navigator
initialRouteName={home.value?.name}
initialRouteName={home?.name}
screenOptions={{
headerShown: false,
animation: "none",
@@ -197,7 +207,7 @@ const AppNavigation = React.memo(
return TaggedNotes;
}}
initialParams={
home.value?.name === "TaggedNotes" ? home.value?.params : undefined
home?.name === "TaggedNotes" ? home?.params : undefined
}
/>
@@ -209,7 +219,7 @@ const AppNavigation = React.memo(
return ColoredNotes;
}}
initialParams={
home.value?.name === "ColoredNotes" ? home.value?.params : undefined
home?.name === "ColoredNotes" ? home?.params : undefined
}
/>
@@ -244,9 +254,7 @@ const AppNavigation = React.memo(
Notebook = Notebook || require("../screens/notebook").default;
return Notebook;
}}
initialParams={
home.value?.name === "Notebook" ? home.value?.params : undefined
}
initialParams={home?.name === "Notebook" ? home?.params : undefined}
/>
<AppStack.Screen

View File

@@ -39,17 +39,26 @@ import Input from "../../components/ui/input";
import { ReminderTime } from "../../components/ui/reminder-time";
import Paragraph from "../../components/ui/typography/paragraph";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import Notifications from "../../services/notifications";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { getFormattedDate, useIsFeatureAvailable } from "@notesnook/common";
import {
getFormattedDate,
useIsFeatureAvailable,
usePromise
} from "@notesnook/common";
import PaywallSheet from "../../components/sheets/paywall";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { Pressable } from "../../components/ui/pressable";
import { TimeSince } from "../../components/ui/time-since";
import Heading from "../../components/ui/typography/heading";
import { eOnLoadNote } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
const ReminderModes =
Platform.OS === "ios"
@@ -70,17 +79,9 @@ const RecurringModes = {
Year: "year"
};
const WeekDays = new Array(7).fill(true);
const WeekDays = [0, 1, 2, 3, 4, 5, 6];
const WeekDaysMon = [1, 2, 3, 4, 5, 6, 0];
const MonthDays = new Array(31).fill(true);
const WeekDayNames = {
0: "Sunday",
1: "Monday",
2: "Tuesday",
3: "Wednesday",
4: "Thursday",
5: "Friday",
6: "Saturday"
};
const ReminderNotificationModes = {
Silent: "silent",
@@ -92,6 +93,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const { reminder, reference } = props.route.params;
useNavigationFocus(props.navigation, { focusOnInit: true });
const { colors, isDark } = useThemeColors();
const weekFormat = useSettingStore((state) => state.weekFormat);
const [reminderMode, setReminderMode] = useState<Reminder["mode"]>(
reminder?.mode || "once"
);
@@ -121,6 +123,15 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const titleRef = useRef<TextInput>(null);
const descriptionRef = useRef<TextInput>(null);
const timer = useRef<NodeJS.Timeout>(undefined);
const referencedNotes = usePromise(
() =>
reminder?.id
? db.relations
.to({ id: reminder.id, type: "reminder" }, "note")
.resolve()
: null,
[reminder?.id]
);
const showDatePicker = () => {
setDatePickerVisibility(true);
@@ -151,7 +162,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const isSecondLast = index === selectedDays.length - 2;
const joinWith = isSecondLast ? " & " : isLast ? "" : ", ";
return recurringMode === RecurringModes.Week
? WeekDayNames[day as keyof typeof WeekDayNames] + joinWith
? strings.weekDayNames[day as keyof typeof strings.weekDayNames]() +
joinWith
: `${day}${nth(day)} ${joinWith}`;
})
.join("");
@@ -243,6 +255,9 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
marginBottom: DDS.isTab ? 25 : undefined,
paddingHorizontal: DefaultAppStyles.GAP
}}
contentContainerStyle={{
gap: DefaultAppStyles.GAP_VERTICAL
}}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
>
@@ -277,15 +292,11 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
height={80}
wrapperStyle={{
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
/>
<ScrollView
style={{
flexDirection: "row",
marginBottom: DefaultAppStyles.GAP_VERTICAL
flexDirection: "row"
}}
horizontal
>
@@ -341,8 +352,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
style={{
backgroundColor: colors.secondary.background,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius,
marginBottom: DefaultAppStyles.GAP_VERTICAL
borderRadius: defaultBorderRadius
}}
>
<View
@@ -390,37 +400,39 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
recurringMode === RecurringModes.Year
? null
: recurringMode === RecurringModes.Week
? WeekDays.map((item, index) => (
<Button
key={strings.weekDayNamesShort[
index as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
index as keyof typeof strings.weekDayNamesShort
]()}
type={
selectedDays.indexOf(index) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(index) > -1) {
days.splice(days.indexOf(index), 1);
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
<Button
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
type={
selectedDays.indexOf(item) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
return [...days];
}
days.push(item);
return [...days];
}
days.push(index);
return [...days];
});
}}
/>
))
});
}}
/>
)
)
: MonthDays.map((item, index) => (
<Button
key={index + "monthday"}
@@ -458,7 +470,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
width: "100%",
flexDirection: "column",
justifyContent: "center",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
alignItems: "center"
}}
>
@@ -468,6 +479,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
onConfirm={handleConfirm}
onCancel={hideDatePicker}
isDarkModeEnabled={isDark}
firstDayOfWeek={weekFormat === "Mon" ? 1 : 0}
is24Hour={db.settings.getTimeFormat() === "24-hour"}
date={date || new Date(Date.now())}
/>
@@ -516,10 +528,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
style={{
borderRadius: defaultBorderRadius,
flexDirection: "row",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
alignItems: "center",
justifyContent: "flex-start",
marginBottom: DefaultAppStyles.GAP_VERTICAL
justifyContent: "flex-start"
}}
>
<>
@@ -555,22 +565,10 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
</View>
)}
<ReminderTime
reminder={reminder}
style={{
width: "100%",
justifyContent: "flex-start",
borderWidth: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
alignSelf: "flex-start"
}}
/>
{reminderMode === ReminderModes.Permanent ? null : (
<ScrollView
style={{
flexDirection: "row",
marginTop: DefaultAppStyles.GAP_VERTICAL,
height: 50
}}
horizontal
@@ -615,6 +613,60 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
))}
</ScrollView>
)}
<ReminderTime
reminder={reminder}
style={{
width: "100%",
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
alignSelf: "flex-start"
}}
/>
{referencedNotes &&
referencedNotes.status === "fulfilled" &&
referencedNotes.value !== null &&
referencedNotes.value?.length > 0 ? (
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Heading size={AppFontSize.md}>{strings.referencedIn()}</Heading>
{referencedNotes.value.map((item) => (
<Pressable
style={{
justifyContent: "space-between",
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
onPress={() => {
Navigation.navigate("FluidPanelsView");
fluidTabsRef.current?.goToPage("editor");
eSendEvent(eOnLoadNote, {
item: item
});
}}
type="secondary"
>
<Paragraph>{item.title}</Paragraph>
<TimeSince
style={{
fontSize: AppFontSize.xxs,
color: colors.secondary.paragraph,
marginRight: 6
}}
time={item.dateEdited}
updateFrequency={
Date.now() - item.dateEdited < 60000 ? 2000 : 60000
}
/>
</Pressable>
))}
</View>
) : null}
</ScrollView>
</KeyboardViewIOS>
</SafeAreaView>

View File

@@ -25,7 +25,8 @@ import React, {
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef
useRef,
useState
} from "react";
import { Platform, ViewStyle } from "react-native";
import WebView from "react-native-webview";
@@ -109,17 +110,20 @@ const Editor = React.memo(
noToolbar,
noHeader
});
const renderKey = useRef(randId("editor-id") + editorId);
const [renderKey, setRenderKey] = useState(
randId("editor-id") + editorId
);
useImperativeHandle(ref, () => ({
get: () => editor
}));
useLockedNoteHandler();
const onError = useCallback(() => {
renderKey.current = randId("editor-id") + editorId;
setRenderKey(randId("editor-id") + editorId);
editor.state.current.ready = false;
editor.state.current.initialLoadCalled = false;
editor.setLoading(true);
}, [editor, editorId]);
}, [editor]);
useEffect(() => {
const sub = [eSubscribeEvent(eEditorReset, onError)];
@@ -144,7 +148,7 @@ const Editor = React.memo(
<WebView
testID={notesnook.editor.id}
ref={editor.ref}
key={renderKey.current}
key={renderKey}
onRenderProcessGone={onError}
nestedScrollEnabled
onError={onError}
@@ -179,6 +183,7 @@ const Editor = React.memo(
hideKeyboardAccessoryView={false}
allowsFullscreenVideo={true}
allowFileAccessFromFileURLs={true}
allowsInlineMediaPlayback
allowUniversalAccessFromFileURLs={true}
originWhitelist={["*"]}
source={{

View File

@@ -22,12 +22,12 @@ import { isFeatureAvailable } from "@notesnook/common";
import { isImage } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
pick as pickFile,
DocumentPickerOptions,
DocumentPickerResponse,
keepLocalCopy
keepLocalCopy,
KeepLocalCopyResponse,
pick as pickFile
} from "@react-native-documents/picker";
import { basename } from "pathe";
import { basename, dirname } from "pathe";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
@@ -35,31 +35,13 @@ import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import { compressToFile } from "../../../common/filesystem/compress";
import AttachImage from "../../../components/dialogs/attach-image-dialog";
import {
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import { ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
const showEncryptionSheet = (file: DocumentPickerResponse) => {
presentSheet({
title: strings.encryptingAttachment(),
paragraph: strings.encryptingAttachmentDesc(file.name || ""),
icon: "attachment"
});
};
const santizeUri = (uri: string) => {
uri = decodeURI(uri);
uri = Platform.OS === "ios" ? uri.replace("file:///", "/") : uri;
return uri;
};
import { santizeUri } from "../../../common/filesystem/utils";
type PickerOptions = {
noteId?: string;
@@ -80,7 +62,7 @@ const file = async (fileOptions: PickerOptions) => {
await db.attachments.generateKey();
let file;
let fileCopyUri;
let fileCopyUri: KeepLocalCopyResponse[0];
let fileName;
try {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
@@ -97,12 +79,10 @@ const file = async (fileOptions: PickerOptions) => {
});
fileCopyUri = result[0];
} catch (e) {
DatabaseLogger.error(e as Error, "Error picking file");
return;
}
let uri =
Platform.OS === "ios" ? fileCopyUri.sourceUri || file.uri : file.uri;
const featureResult = await isFeatureAvailable("fileSize", file.size || 0);
if (!featureResult.isAllowed) {
ToastManager.show({
@@ -122,8 +102,7 @@ const file = async (fileOptions: PickerOptions) => {
return;
}
uri = Platform.OS === "ios" ? santizeUri(uri) : uri;
showEncryptionSheet(file);
let uri = santizeUri(fileCopyUri.localUri);
const hash = await Sodium.hashFile({
uri: uri,
type: "url"
@@ -139,44 +118,31 @@ const file = async (fileOptions: PickerOptions) => {
) {
throw new Error("Failed to attach file");
}
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
RNFetchBlob.fs.unlink(dirname(fileCopyUri.localUri)).catch((e) => {
console.log(e, "error");
});
if (
fileOptions.tabId !== undefined &&
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
fileOptions.noteId
) {
if (isImage(file.type || "application/octet-stream")) {
editorController.current?.commands.insertImage(
{
hash: hash,
filename: fileName,
mime: file.type || "application/octet-stream",
size: file.size || 0,
dataurl: (await db.attachments.read(hash, "base64")) as string,
type: "image"
},
fileOptions.tabId
);
} else {
editorController.current?.commands.insertAttachment(
{
hash: hash,
filename: fileName,
mime: file.type || "application/octet-stream",
size: file.size || 0,
type: "file"
},
fileOptions.tabId
);
}
editorController.current?.commands.insertAttachment(
{
hash: hash,
filename: fileName,
mime: file.type || "application/octet-stream",
size: file.size || 0,
type: "file"
},
fileOptions.tabId
);
} else {
throw new Error("Failed to attach file, no tabId is set");
}
eSendEvent(eCloseSheet);
} catch (e) {
eSendEvent(eCloseSheet);
console.log("ERROR OCCURED HERE.", e);
ToastManager.show({
heading: (e as Error).message,
type: "error",
@@ -287,7 +253,6 @@ const handleImageResponse = async (
for (const image of response) {
const isPng = /(png)/g.test(image.mime);
const isJpeg = /(jpeg|jpg)/g.test(image.mime);
if (compress && (isPng || isJpeg)) {
image.path = await compressToFile(
Platform.OS === "ios" ? "file://" + image.path : image.path,
@@ -327,7 +292,7 @@ const handleImageResponse = async (
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
RNFetchBlob.fs.unlink(uri).catch((e) => {});
if (
options.tabId !== undefined &&
@@ -412,9 +377,7 @@ export async function attachFile(
return true;
} catch (e) {
DatabaseLogger.error(e);
if (Platform.OS === "ios") {
await RNFetchBlob.fs.unlink(uri);
}
RNFetchBlob.fs.unlink(uri).catch((e) => {});
return false;
}
}

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import type { ToolbarGroupDefinition } from "@notesnook/editor";
import { useEditor } from "./use-editor";
import { FeatureId, FeatureResult } from "@notesnook/common";
import { DayFormat } from "@notesnook/core";
export type useEditorType = ReturnType<typeof useEditor>;
export type EditorState = {
@@ -60,6 +61,8 @@ export type Settings = {
markdownShortcuts: boolean;
features: Record<any, any>;
loggedIn: boolean;
defaultLineHeight: number;
dayFormat: DayFormat;
};
export type EditorProps = {

View File

@@ -28,11 +28,13 @@ import { getDefaultPresets } from "@notesnook/editor/dist/cjs/toolbar/tool-defin
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef } from "react";
import * as ScopedStorage from "react-native-scoped-storage";
import {
BackHandler,
Keyboard,
KeyboardEventListener,
NativeEventSubscription,
Platform,
useWindowDimensions
} from "react-native";
import { WebViewMessageEvent } from "react-native-webview";
@@ -82,6 +84,9 @@ import { useDragState } from "../../settings/editor/state";
import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { editorState, openInternalLink } from "./utils";
import filesystem from "../../../common/filesystem";
import ReactNativeBlobUtil from "react-native-blob-util";
import { ShareComponent } from "../../../components/sheets/export-notes/share";
const publishNote = async () => {
const user = useUserStore.getState().user;
@@ -153,16 +158,22 @@ export const useEditorEvents = (
const features = useAreFeaturesAvailable([
"callout",
"outlineList",
"taskList"
"taskList",
"importCsvToTable",
"exportTableAsCsv"
]);
const deviceMode = useSettingStore((state) => state.deviceMode);
const fullscreen = useSettingStore((state) => state.fullscreen);
const corsProxy = useSettingStore((state) => state.settings.corsProxy);
const loading = useSettingStore((state) => state.isAppLoading);
const [dateFormat, timeFormat] = useSettingStore((state) => [
state.dateFormat,
state.timeFormat
]);
const [dateFormat, timeFormat, defaultLineHeight, dayFormat] =
useSettingStore((state) => [
state.dateFormat,
state.timeFormat,
state.settings.defaultLineHeight,
state.dayFormat
]);
const handleBack = useRef<NativeEventSubscription>(undefined);
const loggedIn = useUserStore((state) => !!state.user);
const { fontScale } = useWindowDimensions();
@@ -222,10 +233,12 @@ export const useEditorEvents = (
fontFamily: SettingsService.get().defaultFontFamily,
dateFormat: db.settings?.getDateFormat(),
timeFormat: db.settings?.getTimeFormat(),
dayFormat: db.settings?.getDayFormat(),
fontScale,
markdownShortcuts,
features,
loggedIn
loggedIn,
defaultLineHeight
});
}, [
fullscreen,
@@ -241,10 +254,13 @@ export const useEditorEvents = (
defaultFontFamily,
dateFormat,
timeFormat,
dayFormat,
loading,
fontScale,
markdownShortcuts,
loggedIn
loggedIn,
defaultLineHeight,
features
]);
const onBackPress = useCallback(async () => {
@@ -518,15 +534,24 @@ export const useEditorEvents = (
}
case EditorEvents.getAttachmentData: {
const attachment = (editorMessage.value as any)
?.attachment as Attachment;
const data = (editorMessage.value as any)?.attachment as Attachment;
const attachment = await db.attachments.attachment(data.hash);
if (!attachment) {
editor.postMessage(NativeEvents.resolve, {
resolverId: editorMessage.resolverId,
data: undefined
});
break;
}
DatabaseLogger.log(
`Getting attachment data: ${attachment?.hash} ${attachment?.type}`
`Getting attachment data: ${attachment.mimeType} ${attachment.hash} ${data.type}`
);
downloadAttachment(attachment.hash, true, {
base64: attachment.type === "image",
text: attachment.type === "web-clip",
base64:
data.type === "image" || attachment.mimeType?.startsWith("audio"),
text: data.type === "web-clip",
silent: true,
groupId: editor.note.current?.id,
cache: true
@@ -534,7 +559,7 @@ export const useEditorEvents = (
.then((data: any) => {
console.log(
"Got attachment data:",
!!data,
data,
editorMessage.resolverId
);
editor.postMessage(NativeEvents.resolve, {
@@ -733,6 +758,62 @@ export const useEditorEvents = (
}
break;
}
case EditorEvents.downloadCsv: {
try {
const csv = editorMessage.value;
let filePath: string;
let fileName: string;
if (Platform.OS === "android") {
let file = await ScopedStorage.createDocument(
"table.csv",
"text/csv",
csv,
"utf8"
);
if (!file) return;
filePath = file.path || file.uri;
fileName = file.name;
} else {
const path = await filesystem.checkAndCreateDir("/");
let possibleFileName = "table.csv";
let counter = 1;
// Check if file exists and find available filename
while (
await ReactNativeBlobUtil.fs.exists(path + possibleFileName)
) {
possibleFileName = `table (${counter}).csv`;
counter++;
}
await ReactNativeBlobUtil.fs.writeFile(
path + possibleFileName,
csv,
"utf8"
);
filePath = path + possibleFileName;
fileName = possibleFileName;
}
await sleep(500);
presentSheet({
title: "Table saved to csv",
paragraph: strings.fileSaved(fileName, Platform.OS),
icon: "download",
context: "global",
component: (
<ShareComponent uri={filePath} name={fileName} padding={12} />
)
});
} catch (e) {
ToastManager.show({
type: "info",
message: "Could not save table to csv"
});
DatabaseLogger.error(e as Error);
}
break;
}
default:
break;

View File

@@ -298,7 +298,7 @@ export const useEditor = (
return;
}
if (isContentInvalid(data) && id) {
if (!title && isContentInvalid(data) && id) {
// Create a new history session if recieved empty or invalid content
// To ensure that history is preserved for correct content.
currentSessionHistoryId = editorSessionHistory.newSession(id);
@@ -677,15 +677,13 @@ export const useEditor = (
state.current.currentlyEditing = true;
if (!tabLocked) {
await loadContent(item);
} else {
} else if (fluidTabsRef.current?.page() === "editor") {
commands.focus(tabId!);
}
lastContentChangeTime.current[item.id] = item.dateEdited;
currentNotes.current[item.id] = item;
if (!currentNotes.current[item.id]) return;
editorSessionHistory.newSession(item.id);
await commands.setStatus(
@@ -1034,13 +1032,6 @@ export const useEditor = (
state.current.isRestoringState = false;
}, []);
useEffect(() => {
eSubscribeEvent(eOnLoadNote + editorId, loadNote);
return () => {
eUnSubscribeEvent(eOnLoadNote + editorId, loadNote);
};
}, [editorId, loadNote, restoreEditorState, isDefaultEditor]);
const onContentChanged = (noteId?: string) => {
if (noteId) {
lastContentChangeTime.current[noteId] = Date.now();
@@ -1097,10 +1088,15 @@ export const useEditor = (
if (!state.current?.initialLoadCalled) {
const url = await Linking.getInitialURL();
let noteId = url && new URL(url).searchParams.get("id");
let noteId =
url &&
url.startsWith("https://app.notesnook.com/open_note?") &&
new URL(url).searchParams.get("id");
if (noteId) {
const note = await db.notes?.note(noteId);
fluidTabsRef.current?.goToPage("editor");
loadNoteMutex.cancel();
loadNoteMutex.release();
if (note) {
loadNote({
item: note
@@ -1108,6 +1104,8 @@ export const useEditor = (
}
} else {
noteId = useTabStore.getState().getCurrentNoteId() || null;
loadNoteMutex.cancel();
loadNoteMutex.release();
if (!noteId) {
loadNote({ newNote: true });
if (fluidTabsRef.current?.page() === "editor") {

View File

@@ -17,24 +17,22 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { VirtualizedGrouping } from "@notesnook/core";
import { sanitizeTag } from "@notesnook/core";
import { Tag } from "@notesnook/core";
import { LegendList } from "@legendapp/list";
import { isFeatureAvailable } from "@notesnook/common";
import { Tag, VirtualizedGrouping, sanitizeTag } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { TextInput, View, useWindowDimensions } from "react-native";
import { ActionSheetRef } 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 { Header } from "../../components/header";
import Input from "../../components/ui/input";
import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { ToastManager } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import {
@@ -43,17 +41,8 @@ import {
} from "../../stores/item-selection-store";
import { useRelationStore } from "../../stores/use-relation-store";
import { useTagStore } from "../../stores/use-tag-store";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import Input from "../../components/ui/input";
import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Header } from "../../components/header";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { isFeatureAvailable } from "@notesnook/common";
import { LegendList } from "@legendapp/list";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
@@ -92,13 +81,12 @@ const useTagItemSelection = createItemSelectionStore(true);
const ManageTags = (props: NavigationProps<"ManageTags">) => {
const { colors } = useThemeColors();
const ids = props.route.params.ids || [];
const [tags, setTags] = useState<VirtualizedGrouping<Tag>>();
const [tags, setTags] = useState<Tag[]>();
const [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null);
const [focus, setFocus] = useState(false);
useNavigationFocus(props.navigation, { focusOnInit: true });
const timerRef = useRef<NodeJS.Timeout>(undefined);
const [queryExists, setQueryExists] = useState(false);
const dimensions = useWindowDimensions();
const refreshSelection = useCallback(() => {
updateInitialSelectionState(ids).then((selection) => {
useTagItemSelection.setState({
@@ -109,18 +97,43 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ids, tags]);
const sortAndSetTags = useCallback(
async (items: VirtualizedGrouping<Tag>) => {
const tags = [];
const noteTags = [];
const assignedTags =
ids.length > 1
? []
: await db.relations
.to({ type: "note", id: ids[0] }, "tag")
.resolve();
for (let i = 0; i < items.placeholders.length; i++) {
const item = (await items.item(i)).item;
if (item) {
if (assignedTags.find((tag) => tag.id === item.id)) {
noteTags.push(item);
} else {
tags.push(item);
}
}
}
tags.splice(0, 0, ...noteTags);
setTags(tags);
},
[]
);
const refreshTags = useCallback(() => {
if (query && query.trim() !== "") {
db.lookup
.tags(query)
.sorted(db.settings.getGroupOptions("tags"))
.then((items) => {
setTags(items);
});
.then(sortAndSetTags);
} else {
db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => {
setTags(items);
});
db.tags.all
.sorted(db.settings.getGroupOptions("tags"))
.then(sortAndSetTags);
}
}, [query]);
@@ -241,14 +254,10 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
);
const renderTag = useCallback(
({ index }: { item: boolean; index: number }) => (
<TagItem
tags={tags as VirtualizedGrouping<Tag>}
id={index}
onPress={onPress}
/>
({ index, item }: { item: Tag; index: number }) => (
<TagItem tag={item} onPress={onPress} />
),
[onPress, tags]
[onPress]
);
return (
@@ -280,14 +289,11 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(v) => {
setQuery(sanitizeTag(v));
checkQueryExists(sanitizeTag(v));
}}
onFocusInput={() => {
setFocus(true);
}}
onBlurInput={() => {
setFocus(false);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setQuery(sanitizeTag(v));
checkQueryExists(sanitizeTag(v));
}, 300);
}}
onSubmit={() => {
onSubmit();
@@ -324,7 +330,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
}}
>
<LegendList
data={tags?.placeholders || []}
data={tags || []}
extraData={tags}
keyboardShouldPersistTaps
keyboardDismissMode="interactive"
@@ -367,16 +373,13 @@ ManageTags.present = (ids?: string[]) => {
export default ManageTags;
const TagItem = ({
id,
tags,
onPress
onPress,
tag
}: {
id: string | number;
tags: VirtualizedGrouping<Tag>;
tag: Tag;
onPress: (id: string) => void;
}) => {
const { colors } = useThemeColors();
const [tag] = useDBItem(id, "tag", tags);
const selection = useTagItemSelection((state) =>
tag?.id ? state.selection[tag?.id] : false
);

View File

@@ -99,7 +99,7 @@ export const NotePreviewConfigure = () => {
useEffect(() => {
useSettingStore.getState().setDeviceMode("mobile");
if (loading) return;
db.notes.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
db.notes.all.sorted(db.settings.getGroupOptions("home")).then((notes) => {
setItems(notes);
});
}, [loading]);
@@ -141,7 +141,7 @@ export const NotePreviewConfigure = () => {
bounceRef.current = setTimeout(() => {
if (!value) {
db.notes.all
.sorted(db.settings.getGroupOptions("notes"))
.sorted(db.settings.getGroupOptions("home"))
.then((notes) => {
setItems(notes);
});
@@ -149,7 +149,7 @@ export const NotePreviewConfigure = () => {
}
db.lookup
.notes(value)
.sorted()
.sorted(db.settings.getGroupOptions("home"))
.then((notes) => {
setItems(notes);
});

View File

@@ -38,7 +38,6 @@ import { eUpdateNotebookRoute } from "../../utils/events";
import { findRootNotebookId } from "../../utils/notebooks";
import { openEditor, setOnFirstSave } from "../notes/common";
import { View } from "react-native";
import { DefaultAppStyles } from "../../utils/styles";
import { Notebooks } from "../../components/sheets/notebooks";
import { useSettingStore } from "../../stores/use-setting-store";
import { rootNavigatorRef } from "../../utils/global-refs";
@@ -47,6 +46,9 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const params = useRef<NotebookScreenParams>(route?.params);
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const [notebook, setNotebook] = useState<Notebook | undefined>(
params.current.item
);
const [loading, setLoading] = useState(true);
const updateOnFocus = useRef(false);
const [breadcrumbs, setBreadcrumbs] = useState<
@@ -75,10 +77,10 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
});
const syncWithNavigation = React.useCallback(() => {
useNavigationStore.getState().setFocusedRouteId(params?.current?.item?.id);
useNavigationStore.getState().setFocusedRouteId(params?.current?.id);
setOnFirstSave({
type: "notebook",
id: params.current.item.id
id: params.current.id
});
}, []);
@@ -86,23 +88,20 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
async (data?: NotebookScreenParams) => {
if (useSettingStore.getState().isAppLoading) return;
if (
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
useNavigationStore.getState().focusedRouteId !== params.current.id &&
!data
) {
updateOnFocus.current = true;
return;
}
if (data?.item?.id && params.current.item?.id !== data?.item?.id) {
const nextRootNotebookId = await findRootNotebookId(data?.item?.id);
const currentNotebookRoot = await findRootNotebookId(
params.current.item.id
);
if (data?.id && params.current?.id !== data?.id) {
const nextRootNotebookId = await findRootNotebookId(data?.id);
const currentNotebookRoot = await findRootNotebookId(params.current.id);
if (
nextRootNotebookId !== currentNotebookRoot ||
nextRootNotebookId === params.current?.item?.id
nextRootNotebookId === params.current?.id
) {
// Never update notebook in route if root is different or if the root is current notebook.
return;
@@ -112,14 +111,13 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
if (data) params.current = data;
try {
const notebook = await db.notebooks?.notebook(
params?.current?.item?.id
);
const notebook = await db.notebooks?.notebook(params?.current?.id);
setNotebook(notebook);
params.current.item = notebook;
if (notebook) {
const breadcrumbs = await db.notebooks.breadcrumbs(notebook.id);
setBreadcrumbs(breadcrumbs.slice(0, breadcrumbs.length - 1));
params.current.item = notebook;
params.current.id = notebook.id;
const notes = await db.relations
.from(notebook, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
@@ -160,29 +158,27 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
<>
<Header
renderedInRoute={route.name}
title={params.current.item?.title}
title={notebook?.title}
canGoBack={params?.current?.canGoBack}
rightButton={{
name: "dots-vertical",
onPress: () => {
Properties.present(params.current.item);
Properties.present(notebook);
}
}}
hasSearch={true}
onSearch={() => {
const selector = db.relations.from(
params.current.item,
"note"
).selector;
if (!notebook) return;
const selector = db.relations.from(notebook, "note").selector;
Navigation.push("Search", {
placeholder: strings.searchInRoute(params.current.item?.title),
placeholder: strings.searchInRoute(notebook?.title),
type: "note",
title: params.current.item?.title,
title: notebook?.title,
route: route.name,
items: selector
});
}}
id={params.current.item?.id}
id={notebook?.id}
/>
<DelayLayout wait={loading}>
@@ -192,19 +188,21 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
onRefresh={() => {
onRequestUpdate();
}}
id={params.current.item?.id}
id={params.current?.id}
renderedInRoute="Notebook"
headerTitle={params.current.item.title}
headerTitle={notebook?.title}
loading={loading}
CustomLisHeader={
<NotebookHeader
breadcrumbs={breadcrumbs}
notebook={params.current.item}
totalNotes={notes?.placeholders.length || 0}
/>
notebook ? (
<NotebookHeader
breadcrumbs={breadcrumbs}
notebook={notebook}
totalNotes={notes?.placeholders.length || 0}
/>
) : undefined
}
placeholder={{
title: params.current.item?.title,
title: notebook?.title!,
paragraph: strings.notesEmpty(),
button: strings.addFirstNote(),
action: openEditor,
@@ -225,7 +223,8 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
testID="notebookTreeSheet"
size="small"
onPress={() => {
Notebooks.present(params.current.item);
if (!notebook) return;
Notebooks.present(notebook);
}}
style={{
position: "relative",
@@ -246,7 +245,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
/>
</View>
<SelectionHeader
id={route.params?.item?.id}
id={route.params?.id}
items={notes}
type="note"
renderedInRoute="Notebook"
@@ -260,6 +259,7 @@ NotebookScreen.navigate = async (item: Notebook, canGoBack?: boolean) => {
const { currentRoute, focusedRouteId } = useNavigationStore.getState();
if (currentRoute === "Notebooks") {
Navigation.push("Notebook", {
id: item.id,
item: item,
canGoBack
});
@@ -276,22 +276,24 @@ NotebookScreen.navigate = async (item: Notebook, canGoBack?: boolean) => {
// Update the route in place instead
eSendEvent(eUpdateNotebookRoute, {
item: item,
title: item.title,
canGoBack: canGoBack
id: item.id,
canGoBack: canGoBack,
item: item
});
} else {
// Push a new route
Navigation.push("Notebook", {
item: item,
canGoBack
id: item.id,
canGoBack,
item: item
});
}
} else {
// Push a new route anyways
Navigation.push("Notebook", {
item: item,
canGoBack
id: item.id,
canGoBack,
item: item
});
}
};

View File

@@ -25,7 +25,7 @@ import Navigation, { NavigationProps } from "../../services/navigation";
import useNavigationStore, {
NotesScreenParams
} from "../../stores/use-navigation-store";
import { PLACEHOLDER_DATA, openEditor, toCamelCase } from "./common";
import { PLACEHOLDER_DATA, openEditor } from "./common";
export const ColoredNotes = ({
navigation,
route
@@ -45,11 +45,13 @@ export const ColoredNotes = ({
ColoredNotes.get = async (params: NotesScreenParams, grouped = true) => {
if (!grouped) {
return await db.relations.from(params.item, "note").resolve();
return await db.relations
.from({ id: params.id, type: "color" }, "note")
.resolve();
}
return await db.relations
.from(params.item, "note")
.from({ id: params.id, type: "color" }, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
};
@@ -63,8 +65,10 @@ ColoredNotes.navigate = (item: Color, canGoBack: boolean) => {
}
Navigation.push<"ColoredNotes">("ColoredNotes", {
item: item,
canGoBack
type: "color",
id: item.id,
canGoBack,
item: item
});
};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { resolveItems } from "@notesnook/common";
import { VirtualizedGrouping } from "@notesnook/core";
import { Tag, VirtualizedGrouping } from "@notesnook/core";
import { Color, Note } from "@notesnook/core";
import React, { useEffect, useRef, useState } from "react";
import { db } from "../../common/database";
@@ -76,14 +76,12 @@ const NotesPage = ({
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const [loadingNotes, setLoadingNotes] = useState(true);
const isMonograph = route.name === "Monographs";
const title =
params.current?.item.type === "tag"
? "#" + params.current?.item.title
: params.current?.item.title;
const [item, setItem] = useState<Tag | Color | undefined>(
params.current.item
);
const title = item?.type === "tag" ? "#" + item?.title : item?.title;
const accentColor =
route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode
: undefined;
route.name === "ColoredNotes" ? (item as Color)?.colorCode : undefined;
const updateOnFocus = useRef(false);
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const isFocused = useNavigationFocus(navigation, {
@@ -107,31 +105,30 @@ const NotesPage = ({
});
const syncWithNavigation = React.useCallback(() => {
const { item } = params.current;
useNavigationStore
.getState()
.setFocusedRouteId(params?.current?.item?.id || route.name);
const { id } = params.current;
useNavigationStore.getState().setFocusedRouteId(id || route.name);
!isMonograph &&
setOnFirstSave({
type: getItemType(route.name),
id: item.id
id: id
});
}, [isMonograph, route.name]);
const onRequestUpdate = React.useCallback(
async (data?: NotesScreenParams) => {
if (useSettingStore.getState().isAppLoading) return;
if (
params.current.item.id &&
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
params.current.id &&
useNavigationStore.getState().focusedRouteId !== params.current.id &&
!data
) {
updateOnFocus.current = false;
return;
}
const isNew = data && data?.item?.id !== params.current?.item?.id;
const isNew = data && data?.id !== params.current?.id;
if (data) params.current = data;
try {
@@ -142,9 +139,9 @@ const NotesPage = ({
)) as VirtualizedGrouping<Note>;
if (route.name === "TaggedNotes" || route.name === "ColoredNotes") {
const item = await (db as any)[params.current.item.type + "s"][
params.current.item.type
](params.current.item.id);
const item = await (db as any)[params.current.type + "s"][
params.current.type
](params.current.id);
if (!item) {
if (rootNavigatorRef.canGoBack()) {
@@ -154,7 +151,7 @@ const NotesPage = ({
}
return;
}
setItem(item);
params.current.item = item;
}
@@ -173,15 +170,7 @@ const NotesPage = ({
useEffect(() => {
if (isAppLoading) return;
if (loadingNotes) {
get(params.current, true)
.then(async (items) => {
setNotes(items as VirtualizedGrouping<Note>);
await (items as VirtualizedGrouping<Note>).item(0, resolveItems);
setLoadingNotes(false);
})
.catch((e) => {
setLoadingNotes(false);
});
onRequestUpdate(params.current);
}
}, [loadingNotes, get, isAppLoading]);
@@ -202,19 +191,18 @@ const NotesPage = ({
}
canGoBack={params?.current?.canGoBack}
hasSearch={true}
id={
route.name === "Monographs" ? "Monographs" : params?.current.item?.id
}
id={route.name === "Monographs" ? "Monographs" : params?.current?.id}
onSearch={() => {
if (!item) return;
const selector =
route.name === "Monographs"
? db.monographs.all
: db.relations.from(params.current.item, "note").selector;
: db.relations.from(item, "note").selector;
Navigation.push("Search", {
placeholder: strings.searchInRoute(title || route.name),
type: "note",
title: title,
title: title!,
route: route.name,
items: selector
});
@@ -229,7 +217,7 @@ const NotesPage = ({
onRefresh={onRequestUpdate}
loading={false}
renderedInRoute={route.name}
id={params.current.item?.id}
id={params.current?.id}
headerTitle={title || "Monographs"}
customAccentColor={accentColor}
placeholder={placeholder}
@@ -246,7 +234,7 @@ const NotesPage = ({
) : null}
</DelayLayout>
<SelectionHeader
id={route.params?.item?.id || route.name}
id={route.params?.id || route.name}
items={notes}
type="note"
renderedInRoute={route.name}

View File

@@ -62,7 +62,8 @@ Monographs.get = async (params?: NotesScreenParams, grouped = true) => {
Monographs.navigate = (canGoBack?: boolean) => {
Navigation.navigate<"Monographs">("Monographs", {
item: { type: "monograph" } as any,
type: "monograph",
id: "monograph",
canGoBack: canGoBack as boolean
});
};

View File

@@ -46,11 +46,13 @@ export const TaggedNotes = ({
TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => {
if (!grouped) {
return await db.relations.from(params.item, "note").resolve();
return await db.relations
.from({ id: params.id, type: "tag" }, "note")
.resolve();
}
return await db.relations
.from(params.item, "note")
.from({ id: params.id, type: "tag" }, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
};
@@ -64,8 +66,10 @@ TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => {
}
Navigation.push<"TaggedNotes">("TaggedNotes", {
item: item,
canGoBack
id: item.id,
type: "tag",
canGoBack,
item: item
});
};

View File

@@ -32,6 +32,8 @@ import {
BackupReminderPicker,
BackupWithAttachmentsReminderPicker,
DateFormatPicker,
DayFormatPicker,
WeekFormatPicker,
FontPicker,
HomePicker,
SidebarTabPicker,
@@ -57,6 +59,8 @@ export const components: { [name: string]: ReactElement } = {
"title-format": <TitleFormat />,
"date-format-selector": <DateFormatPicker />,
"time-format-selector": <TimeFormatPicker />,
"day-format-selector": <DayFormatPicker />,
"week-format-selector": <WeekFormatPicker />,
"theme-selector": <ThemeSelector />,
"applock-timer": <ApplockTimerPicker />,
autobackupsattachments: <BackupWithAttachmentsReminderPicker />,

View File

@@ -61,6 +61,7 @@ const Home = ({
/>
<DelayLayout type="settings">
<LegendList
testID="settings-list"
data={settingsGroups}
keyExtractor={keyExtractor}
renderItem={renderItem}

View File

@@ -32,6 +32,18 @@ import { strings } from "@notesnook/intl";
import { isFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../../../components/sheets/paywall";
const DAY_FORMATS = ["short", "long"];
const DayFormatFormats = {
short: "ddd",
long: "dddd"
};
const WEEK_FORMATS = ["Sun", "Mon"];
const WeekFormatNames = {
Sun: "Sunday",
Mon: "Monday"
};
export const FontPicker = createSettingsPicker({
getValue: () => useSettingStore.getState().settings.defaultFontFamily,
updateValue: (item) => {
@@ -111,8 +123,8 @@ export const TrashIntervalPicker = createSettingsPicker({
return item === -1
? strings.never()
: item === 1
? strings.reminderRecurringMode.day()
: strings.days(item);
? strings.reminderRecurringMode.day()
: strings.days(item);
},
getItemKey: (item) => item.toString(),
options: [-1, 1, 7, 30, 365],
@@ -152,6 +164,42 @@ export const DateFormatPicker = createSettingsPicker({
isOptionAvailable: () => true
});
export const DayFormatPicker = createSettingsPicker({
getValue: () => db.settings.getDayFormat(),
updateValue: (item) => {
db.settings.setDayFormat(item);
useSettingStore.setState({
dayFormat: item
});
},
formatValue: (item) => {
return `${strings.dayFormat()} (${dayjs().format(DayFormatFormats[item])})`;
},
getItemKey: (item) => item,
options: DAY_FORMATS,
compareValue: (current, item) => current === item,
isFeatureAvailable: () => true,
isOptionAvailable: () => true
});
export const WeekFormatPicker = createSettingsPicker({
getValue: () => db.settings.getWeekFormat(),
updateValue: (item) => {
db.settings.setWeekFormat(item);
useSettingStore.setState({
weekFormat: item
});
},
formatValue: (item) => {
return `${WeekFormatNames[item]}`;
},
getItemKey: (item) => item,
options: WEEK_FORMATS,
compareValue: (current, item) => current === item,
isFeatureAvailable: () => true,
isOptionAvailable: () => true
});
const TimeFormats = {
"12-hour": "hh:mm A",
"24-hour": "HH:mm"
@@ -228,10 +276,10 @@ export const ApplockTimerPicker = createSettingsPicker({
return item === -1
? strings.never()
: item === 0 || item === undefined
? strings.immediately()
: item === 1
? strings.minutes(1)
: strings.minutes(item);
? strings.immediately()
: item === 1
? strings.minutes(1)
: strings.minutes(item);
},
getItemKey: (item) => item.toString(),
options: [-1, 0, 1, 5, 15, 30],

View File

@@ -95,6 +95,15 @@ const restoreBackup = async (options: {
deleteFile?: boolean;
}) => {
try {
if (
!options.uri.endsWith(".nnbackup") &&
!options.uri.endsWith(".nnbackupz")
) {
throw new Error(
`Invalid backup file selected. Only .nnbackup and .nnbackupz files can be restored.`
);
}
const isLegacyBackup = options.uri.endsWith(".nnbackup");
startProgress({
@@ -367,7 +376,6 @@ export const RestoreBackup = () => {
disableAppLockRequests: true
});
const file = await pick();
const fileCopy = await keepLocalCopy({
destination: "cachesDirectory",
files: [
@@ -390,10 +398,7 @@ export const RestoreBackup = () => {
}, 1000);
restoreBackup({
uri:
Platform.OS === "android"
? (("file://" + fileCopy[0].sourceUri) as string)
: (fileCopy[0].sourceUri as string),
uri: fileCopy[0].localUri,
deleteFile: true
});
},
@@ -565,13 +570,17 @@ const BackupItem = ({
paragraph: strings.restoreBackupConfirm(),
positiveText: strings.restore(),
negativeText: strings.cancel(),
context: "global",
positivePress: async () => {
restoreBackup({
uri:
Platform.OS === "android"
? (item as ScopedStorage.FileType).uri
: (item as ReactNativeBlobUtilStat).path
});
setTimeout(() => {
restoreBackup({
uri:
Platform.OS === "android"
? (item as ScopedStorage.FileType).uri
: (item as ReactNativeBlobUtilStat).path
});
}, 500);
return true;
}
});
}}

View File

@@ -53,7 +53,8 @@ import {
eSendEvent,
eSubscribeEvent,
openVault,
presentSheet
presentSheet,
VaultRequestType
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Notifications from "../../services/notifications";
@@ -62,7 +63,11 @@ import SettingsService from "../../services/settings";
import Sync from "../../services/sync";
import { useThemeStore } from "../../stores/use-theme-store";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
import {
eAfterSync,
eCloseSheet,
eOpenRecoveryKeyDialog
} from "../../utils/events";
import { NotesnookModule } from "../../utils/notesnook-module";
import { sleep } from "../../utils/time";
import { MFARecoveryCodes, MFASheet } from "./2fa";
@@ -71,8 +76,50 @@ import { verifyUser, verifyUserWithApplock } from "./functions";
import { logoutUser } from "./logout";
import { SettingSection } from "./types";
import { getTimeLeft } from "./user-section";
import { EDITOR_LINE_HEIGHT } from "../../utils/constants";
import { MMKV } from "../../common/database/mmkv";
import { resetTabStore } from "../editor/tiptap/use-tab-store";
import { clearAllStores } from "../../stores";
import { refreshAllStores } from "../../stores/create-db-collection-store";
export const settingsGroups: SettingSection[] = [
{
id: "account-local",
name: strings.account(),
useHook: () => useUserStore((state) => state.user),
hidden: (current) => !!current,
sections: [
{
id: "delete-data",
name: strings.deleteData(),
description: strings.deleteAccountDesc(),
modifer: () => {
presentDialog({
title: strings.deleteData(),
paragraph: strings.irreverisibleAction(),
positiveType: "errorShade",
positiveText: "Delete data",
positivePress: async () => {
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
resetTabStore();
clearAllStores();
Navigation.queueRoutesForUpdate();
SettingsService.resetSettings();
db.reset();
setImmediate(() => {
refreshAllStores();
eSendEvent(eAfterSync);
});
return true;
}
});
}
}
]
},
{
id: "account",
name: strings.account(),
@@ -143,21 +190,20 @@ export const settingsGroups: SettingSection[] = [
"dddd, MMMM D, YYYY h:mm A"
);
const trialEndDate = dayjs(user?.subscription?.start)
.add(
user?.subscription?.productId?.includes("monthly") ? 7 : 14,
"day"
)
.format("dddd, MMMM D, YYYY h:mm A");
if (
user.subscription?.plan !== SubscriptionPlan.FREE &&
user.subscription?.productId
) {
const status = user.subscription?.status;
return status === SubscriptionStatus.TRIAL
? strings.trialEndsOn(
dayjs(user?.subscription?.start)
.add(
user?.subscription?.productId?.includes("monthly")
? 7
: 14
)
.format("dddd, MMMM D, YYYY h:mm A")
)
? strings.trialOnGoing(trialEndDate)
: status === SubscriptionStatus.ACTIVE
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
@@ -716,6 +762,22 @@ export const settingsGroups: SettingSection[] = [
component: "date-format-selector",
icon: "calendar-blank"
},
{
id: "day-format",
name: strings.dayFormat(),
description: strings.dayFormatDesc(),
type: "component",
component: "day-format-selector",
icon: "calendar-today"
},
{
id: "week-format",
name: strings.weekFormat(),
description: strings.weekFormatDesc(),
type: "component",
component: "week-format-selector",
icon: "calendar"
},
{
id: "time-format",
name: strings.timeFormat(),
@@ -815,6 +877,16 @@ export const settingsGroups: SettingSection[] = [
property: "defaultFontFamily",
component: "font-selector"
},
{
id: "default-line-height",
name: strings.lineHeight(),
description: strings.lineHeightDesc(),
type: "input-selector",
property: "defaultLineHeight",
icon: "format-line-spacing",
minInputValue: EDITOR_LINE_HEIGHT.MIN,
maxInputValue: EDITOR_LINE_HEIGHT.MAX
},
{
id: "title-format",
name: strings.titleFormat(),
@@ -896,9 +968,9 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => (current as VaultStatusType)?.exists,
modifer: () => {
openVault({
item: {},
novault: false,
title: strings.createVault()
requestType: VaultRequestType.CreateVault,
title: strings.createVault(),
buttonTitle: strings.create()
});
}
},
@@ -910,10 +982,9 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () =>
openVault({
item: {},
changePassword: true,
novault: true,
title: strings.changeVaultPassword()
requestType: VaultRequestType.ChangePassword,
title: strings.changeVaultPassword(),
buttonTitle: strings.change()
})
},
{
@@ -924,10 +995,10 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
item: {},
clearVault: true,
novault: true,
title: strings.clearVault() + "?"
requestType: VaultRequestType.ClearVault,
title: strings.clearVault() + "?",
buttonTitle: strings.clear(),
positiveButtonType: "errorShade"
});
}
},
@@ -939,10 +1010,10 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
item: {},
deleteVault: true,
novault: true,
title: strings.deleteVault() + "?"
requestType: VaultRequestType.DeleteVault,
title: strings.deleteVault() + "?",
buttonTitle: strings.delete(),
positiveButtonType: "errorShade"
});
}
},
@@ -960,14 +1031,15 @@ export const settingsGroups: SettingSection[] = [
getter: (current) => (current as VaultStatusType)?.biometryEnrolled,
modifer: (current) => {
const _current = current as VaultStatusType;
const isRevoking = _current.biometryEnrolled;
openVault({
item: {},
fingerprintAccess: !_current.biometryEnrolled,
revokeFingerprintAccess: _current.biometryEnrolled,
novault: true,
title: _current.biometryEnrolled
requestType: isRevoking
? VaultRequestType.RevokeFingerprint
: VaultRequestType.EnableFingerprint,
title: isRevoking
? strings.revokeBiometricUnlock()
: strings.vaultEnableBiometrics()
: strings.vaultEnableBiometrics(),
buttonTitle: isRevoking ? strings.revoke() : strings.enable()
});
}
}

View File

@@ -16,6 +16,8 @@ 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 { LegendList } from "@legendapp/list";
import { strings } from "@notesnook/intl";
import {
THEME_COMPATIBILITY_VERSION,
ThemeDefinition,
@@ -29,7 +31,6 @@ import type {
ThemesRouter
} from "@notesnook/themes-server";
import { keepLocalCopy, pick } from "@react-native-documents/picker";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
@@ -40,24 +41,24 @@ import {
TouchableOpacity,
View
} from "react-native";
import ReactNativeBlobUtil from "react-native-blob-util";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { DatabaseLogger, db } from "../../common/database";
import { santizeUri } from "../../common/filesystem/utils";
import SheetProvider from "../../components/sheet-provider";
import { Button } from "../../components/ui/button";
import { IconButton } from "../../components/ui/icon-button";
import Input from "../../components/ui/input";
import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import { getColorLinearShade } from "../../utils/colors";
import { getElevationStyle } from "../../utils/elevation";
import { MenuItemsList } from "../../utils/menu-items";
import { IconButton } from "../../components/ui/icon-button";
import { Pressable } from "../../components/ui/pressable";
import { getColorLinearShade } from "../../utils/colors";
import { strings } from "@notesnook/intl";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { LegendList } from "@legendapp/list";
import ReactNativeBlobUtil from "react-native-blob-util";
const THEME_SERVER_URL = "https://themes-api.notesnook.com";
//@ts-ignore
@@ -429,12 +430,16 @@ function ThemeSelector() {
});
if (copiedFile[0].status !== "success") return;
const themeJsonCopiedPath = santizeUri(
copiedFile[0].localUri
);
const themeJson = await ReactNativeBlobUtil.fs.readFile(
copiedFile[0].localUri,
themeJsonCopiedPath,
"utf8"
);
ReactNativeBlobUtil.fs
.unlink(copiedFile[0].localUri)
.unlink(themeJsonCopiedPath)
.catch(() => {});
const json = JSON.parse(themeJson);
const result = validateTheme(json);

View File

@@ -319,9 +319,8 @@ async function run(
};
}
await sleep(300);
if (canShowCompletionStatus) {
await sleep(500);
presentBackupCompleteSheet(path);
}

View File

@@ -76,11 +76,11 @@ async function getCredentials(title?: string, description?: string) {
const options = Platform.select({
ios: {
fallbackEnabled: false,
description: description
description: description || title || "Unlock"
},
android: {
title: title,
description: description,
description: description || "Unlock",
deviceCredentialAllowed: false
}
});
@@ -132,11 +132,11 @@ async function validateUser(title: string, description?: string) {
Platform.select({
ios: {
fallbackEnabled: false,
description: title
description: title || "Unlock"
},
android: {
title: title,
description: description,
description: description || "Unlock",
deviceCredentialAllowed: false
}
}) as AuthenticateIOS

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { EventHandler, EventManager } from "@notesnook/core";
import { EventHandler, EventManager, Note, NoteContent } from "@notesnook/core";
import Clipboard from "@react-native-clipboard/clipboard";
import { RefObject } from "react";
import { ActionSheetRef } from "react-native-actions-sheet";
@@ -32,22 +32,39 @@ import {
} from "../utils/events";
import { strings } from "@notesnook/intl";
type Vault = {
item: unknown;
novault: boolean;
title: string;
description: string;
locked: boolean;
permanant: boolean;
goToEditor: boolean;
share: boolean;
deleteNote: boolean;
fingerprintAccess: boolean;
revokeFingerprintAccess: boolean;
changePassword: boolean;
clearVault: boolean;
deleteVault: boolean;
copyNote: boolean;
export enum VaultRequestType {
CreateVault = "createVault",
LockNote = "lockNote",
UnlockNote = "unlockNote",
PermanentUnlock = "permanentUnlock",
GoToEditor = "goToEditor",
ShareNote = "shareNote",
CopyNote = "copyNote",
DeleteNote = "deleteNote",
EnableFingerprint = "enableFingerprint",
RevokeFingerprint = "revokeFingerprint",
ChangePassword = "changePassword",
ClearVault = "clearVault",
DeleteVault = "deleteVault",
CustomAction = "customAction"
}
export type Vault = {
item?: Note;
requestType: VaultRequestType;
title?: string;
description?: string;
paragraph?: string;
buttonTitle?: string;
positiveButtonType?: "errorShade" | "transparent" | "accent";
customActionTitle?: string;
customActionParagraph?: string;
onUnlock?: (
item: Note & {
content?: NoteContent<false>;
},
password: string
) => void;
};
type NoteEdit = {
@@ -79,7 +96,7 @@ export const eSendEvent = (eventName: string, ...args: any[]) => {
eventManager.publish(eventName, ...args);
};
export const openVault = (data: Partial<Vault>) => {
export const openVault = (data: Vault) => {
eSendEvent(eOpenVaultDialog, data);
};

View File

@@ -463,9 +463,11 @@ async function loadNote(id: string, jump: boolean) {
const tab = useTabStore.getState().getTabForNote(id);
if (useTabStore.getState().currentTab !== tab) {
eSendEvent(eOnLoadNote, {
item: note
});
setTimeout(() => {
eSendEvent(eOnLoadNote, {
item: note
});
}, 300);
}
}

View File

@@ -23,6 +23,7 @@ import ShareExtension, {
import { getPreviewData } from "@flyerhq/react-native-link-preview";
import {
formatBytes,
getFormattedDate,
isFeatureAvailable,
useIsFeatureAvailable
} from "@notesnook/common";
@@ -47,7 +48,6 @@ import {
import RNFetchBlob from "react-native-blob-util";
import {
SafeAreaProvider,
SafeAreaView,
useSafeAreaInsets
} from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
@@ -70,14 +70,20 @@ import { Search } from "./search";
import { initDatabase, useShareStore } from "./store";
import { isTablet } from "react-native-device-info";
import { NotesnookModule } from "../utils/notesnook-module";
import { DefaultAppStyles } from "../utils/styles";
const getLinkPreview = (url: string) => {
return getPreviewData(url, 5000);
};
async function sanitizeHtml(site: string) {
async function sanitizeHtml(site: string, title?: string) {
try {
let html = await fetchHandle.current?.processUrl(site);
return html;
return (
html +
"<hr/>" +
`<p>Clipped from <a href="${site}">${title || site}</a></p>
<p>Date clipped ${getFormattedDate(Date.now())}</p>`
);
} catch (e) {
return "";
}
@@ -145,6 +151,7 @@ const ShareView = () => {
const noteContent = useRef<string>(undefined);
const noteTitle = useRef<string>(undefined);
const [loading, setLoading] = useState(false);
const [loadingData, setLoadingData] = useState(true);
const [loadingExtension, setLoadingExtension] = useState(true);
const fullQualityImages = useIsFeatureAvailable("fullQualityImages");
const [rawData, setRawData] = useState<{
@@ -167,6 +174,11 @@ const ShareView = () => {
globalThis["IS_SHARE_EXTENSION"] = true;
const onKeyboardDidShow = (event: KeyboardEvent) => {
let height = Dimensions.get("screen").height - event.endCoordinates.screenY;
if (height < 100) {
setKeyboardHeight(0);
keyboardHeightRef.current = 0;
return;
}
keyboardHeightRef.current = height;
setKeyboardHeight(height);
};
@@ -210,7 +222,6 @@ const ShareView = () => {
};
const onLoad = useCallback(() => {
console.log(noteContent.current, "current...");
eSendEvent(eOnLoadNote + "shareEditor", {
id: null,
content: {
@@ -226,6 +237,7 @@ const ShareView = () => {
try {
if (noteContent.current) {
onLoad();
setLoadingData(false);
return;
}
defaultNote.content.data = undefined;
@@ -242,6 +254,7 @@ const ShareView = () => {
editorRef.current?.focus();
}, 300);
}
setLoadingData(false);
return;
}
@@ -298,7 +311,9 @@ const ShareView = () => {
});
onLoad();
setNote({ ...note });
setLoadingData(false);
} catch (e) {
setLoadingData(false);
console.error(e);
}
},
@@ -311,6 +326,7 @@ const ShareView = () => {
await initDatabase();
setLoadingExtension(false);
useShareStore.getState().restore();
loadData(false);
} catch (e) {
DatabaseLogger.error(e);
}
@@ -389,7 +405,7 @@ const ShareView = () => {
if (value === 2) {
setLoadingPage(true);
setTimeout(async () => {
let html = await sanitizeHtml(rawData?.value || "");
let html = await sanitizeHtml(rawData?.value || "", note.title);
noteContent.current = html || "";
setLoadingPage(false);
onLoad();
@@ -448,11 +464,7 @@ const ShareView = () => {
overflow: "hidden",
paddingTop: insets.top,
paddingLeft: insets.left,
paddingRight: insets.right,
paddingBottom:
Platform.OS === "android" && keyboardHeight
? keyboardHeight - (insets.bottom || (gesturesEnabled ? 25 : 50))
: 0
paddingRight: insets.right
}}
>
{loadingPage ? <HtmlLoadingWebViewAgent /> : null}
@@ -500,394 +512,414 @@ const ShareView = () => {
display: searchMode ? "none" : "flex",
borderTopRightRadius: Platform.OS === "ios" ? 10 : 15,
borderTopLeftRadius: Platform.OS === "ios" ? 10 : 15,
maxHeight: Platform.OS === "android" ? undefined : "100%"
maxHeight: Platform.OS === "android" ? undefined : "100%",
paddingBottom:
Platform.OS === "android" && keyboardHeight
? keyboardHeight
: insets.bottom
}}
>
<ScrollView>
{loadingData ? (
<View
style={{
maxHeight: "100%"
justifyContent: "center",
alignItems: "center",
height: 300,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<ActivityIndicator color={colors.primary.accent} />
<Paragraph>Loading clip...</Paragraph>
</View>
) : (
<ScrollView>
<View
style={{
width: "100%"
maxHeight: "100%"
}}
>
<View
style={{
minHeight: 100,
backgroundColor: colors.primary.background,
overflow: "hidden"
width: "100%"
}}
>
<View
style={{
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
borderBottomWidth: 1,
paddingBottom: 12,
borderBottomColor: colors.secondary.background,
paddingHorizontal: 12,
gap: 10
minHeight: 100,
backgroundColor: colors.primary.background,
overflow: "hidden"
}}
>
{appendNoteId ? (
<Heading
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "Inter-SemiBold",
fontSize: AppFontSize.lg,
paddingBottom: 0,
paddingTop: 0
}}
allowFontScaling={false}
>
Save note
</Heading>
) : (
<TextInput
placeholder="Enter note title"
ref={inputRef}
allowFontScaling={false}
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "Inter-SemiBold",
fontSize: AppFontSize.lg,
paddingBottom: 0,
paddingTop: 0,
color: colors.primary.heading
}}
onChangeText={(value) => {
noteTitle.current = value;
}}
defaultValue={noteTitle.current}
blurOnSubmit={false}
onSubmitEditing={() => {
editorRef.current?.focus();
}}
/>
)}
<Button
title="Done"
type="accent"
loading={loading}
onPress={onPress}
allowFontScaling={false}
/>
</View>
{rawFiles?.length > 0 ? (
<View
style={{
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
borderBottomWidth: 1,
paddingBottom: 12,
borderBottomColor: colors.secondary.background,
paddingHorizontal: 12,
paddingVertical: 12,
backgroundColor: colors.secondary.background
gap: 10
}}
>
<Paragraph
style={{ marginBottom: 6 }}
color={colors.primary.paragraph}
{appendNoteId ? (
<Heading
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "Inter-SemiBold",
fontSize: AppFontSize.lg,
paddingBottom: 0,
paddingTop: 0
}}
allowFontScaling={false}
>
Save note
</Heading>
) : (
<TextInput
placeholder="Enter note title"
ref={inputRef}
allowFontScaling={false}
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "Inter-SemiBold",
fontSize: AppFontSize.lg,
paddingBottom: 0,
paddingTop: 0,
color: colors.primary.heading
}}
onChangeText={(value) => {
noteTitle.current = value;
}}
defaultValue={noteTitle.current}
blurOnSubmit={false}
onSubmitEditing={() => {
editorRef.current?.focus();
}}
/>
)}
<Button
title="Done"
type="accent"
loading={loading}
onPress={onPress}
allowFontScaling={false}
>
Attaching {rawFiles.length} file(s):
</Paragraph>
<ScrollView horizontal>
{rawFiles.map((item) =>
isImage(item.type) || item.value?.endsWith(".png") ? (
<TouchableOpacity
onPress={() => onRemoveFile(item)}
key={item.name}
activeOpacity={0.9}
>
<Image
source={{
uri:
Platform.OS === "android"
? `file://${item.value}`
: item.value
}}
style={{
width: 100,
height: 100,
borderRadius: defaultBorderRadius,
backgroundColor: "black",
marginRight: 6
}}
resizeMode="cover"
/>
</TouchableOpacity>
) : (
<TouchableOpacity
activeOpacity={0.9}
key={item.name}
onPress={() => onRemoveFile(item)}
style={{
borderRadius: defaultBorderRadius,
backgroundColor: colors.secondary.background,
flexDirection: "row",
borderWidth: 1,
borderColor: colors.primary.border,
alignItems: "center",
paddingVertical: 5,
paddingHorizontal: 8,
marginRight: 6
}}
>
<Icon
color={colors.primary.icon}
size={15}
name="file"
/>
/>
</View>
<Paragraph
size={AppFontSize.xs}
color={colors.primary.paragraph}
allowFontScaling={false}
style={{
marginLeft: 4,
paddingRight: 8
}}
>
{item.name || item.value.split("/").pop()}
{item.size ? `(${formatBytes(item.size)})` : ""}
</Paragraph>
</TouchableOpacity>
)
)}
</ScrollView>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
allowFontScaling={false}
{rawFiles?.length > 0 ? (
<View
style={{
marginTop: 6
paddingHorizontal: 12,
paddingVertical: 12,
backgroundColor: colors.secondary.background
}}
>
Tap to remove an attachment.
</Paragraph>
{rawFiles.some((item) => isImage(item.type)) ? (
<TouchableOpacity
activeOpacity={1}
<Paragraph
style={{ marginBottom: 6 }}
color={colors.primary.paragraph}
allowFontScaling={false}
>
Attaching {rawFiles.length} file(s):
</Paragraph>
<ScrollView horizontal>
{rawFiles.map((item) =>
isImage(item.type) || item.value?.endsWith(".png") ? (
<TouchableOpacity
onPress={() => onRemoveFile(item)}
key={item.name}
activeOpacity={0.9}
>
<Image
source={{
uri:
Platform.OS === "android"
? `file://${item.value}`
: item.value
}}
style={{
width: 100,
height: 100,
borderRadius: defaultBorderRadius,
backgroundColor: "black",
marginRight: 6
}}
resizeMode="cover"
/>
</TouchableOpacity>
) : (
<TouchableOpacity
activeOpacity={0.9}
key={item.name}
onPress={() => onRemoveFile(item)}
style={{
borderRadius: defaultBorderRadius,
backgroundColor: colors.secondary.background,
flexDirection: "row",
borderWidth: 1,
borderColor: colors.primary.border,
alignItems: "center",
paddingVertical: 5,
paddingHorizontal: 8,
marginRight: 6
}}
>
<Icon
color={colors.primary.icon}
size={15}
name="file"
/>
<Paragraph
size={AppFontSize.xs}
color={colors.primary.paragraph}
allowFontScaling={false}
style={{
marginLeft: 4,
paddingRight: 8
}}
>
{item.name || item.value.split("/").pop()}
{item.size ? `(${formatBytes(item.size)})` : ""}
</Paragraph>
</TouchableOpacity>
)
)}
</ScrollView>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
allowFontScaling={false}
style={{
flexDirection: "row",
alignSelf: "center",
alignItems: "center",
width: "100%",
marginTop: 6
}}
onPress={() => {
if (fullQualityImages?.isAllowed) {
setCompress(!compress);
}
}}
>
<Icon
size={20}
name={
compress
? "checkbox-marked"
: "checkbox-blank-outline"
}
allowFontScaling={false}
color={
compress && fullQualityImages?.isAllowed
? colors.primary.accent
: colors.primary.icon
}
/>
<Text
Tap to remove an attachment.
</Paragraph>
{rawFiles.some((item) => isImage(item.type)) ? (
<TouchableOpacity
activeOpacity={1}
style={{
flexShrink: 1,
marginLeft: 3,
fontSize: 12
flexDirection: "row",
alignSelf: "center",
alignItems: "center",
width: "100%",
marginTop: 6
}}
onPress={() => {
if (fullQualityImages?.isAllowed) {
setCompress(!compress);
}
}}
allowFontScaling={false}
>
Compress image(s) (recommended)
</Text>
</TouchableOpacity>
) : null}
<Icon
size={20}
name={
compress
? "checkbox-marked"
: "checkbox-blank-outline"
}
allowFontScaling={false}
color={
compress && fullQualityImages?.isAllowed
? colors.primary.accent
: colors.primary.icon
}
/>
<Text
style={{
flexShrink: 1,
marginLeft: 3,
fontSize: 12
}}
allowFontScaling={false}
>
Compress image(s) (recommended)
</Text>
</TouchableOpacity>
) : null}
</View>
) : null}
<View
style={{
width: "100%",
height: rawFiles.length > 0 ? 100 : 200,
paddingBottom: 15,
marginBottom: 10,
borderBottomColor: colors.secondary.background,
borderBottomWidth: 1
}}
>
<SafeAreaProvider
style={{
flex: 1,
paddingTop: 10,
justifyContent: loadingPage ? "center" : undefined,
alignItems: loadingPage ? "center" : undefined
}}
>
{!loadingExtension && !loadingPage ? (
<>
<Editor
editorRef={editorRef}
onLoad={onLoadEditor}
onChange={(html) => {
noteContent.current = html;
}}
/>
</>
) : (
<>
{loadingPage ? (
<>
<ActivityIndicator
color={colors.primary.accent}
/>
<Paragraph>Preparing web clip...</Paragraph>
</>
) : null}
</>
)}
</SafeAreaProvider>
</View>
) : null}
<View
style={{
width: "100%",
height: rawFiles.length > 0 ? 100 : 200,
paddingBottom: 15,
marginBottom: 10,
borderBottomColor: colors.secondary.background,
borderBottomWidth: 1
}}
>
<SafeAreaProvider
{appendNoteId ? (
<AppendNote
id={appendNoteId}
onLoad={(title) => {
if (!noteTitle.current) {
noteTitle.current = title;
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
}
}}
/>
) : null}
<View
style={{
flex: 1,
paddingTop: 10,
justifyContent: loadingPage ? "center" : undefined,
alignItems: loadingPage ? "center" : undefined
flexDirection: "row",
paddingHorizontal: 12,
width: "100%",
alignItems: "center"
}}
>
{!loadingExtension && !loadingPage ? (
<>
<Editor
editorRef={editorRef}
onLoad={onLoadEditor}
onChange={(html) => {
noteContent.current = html;
}}
/>
</>
) : (
<>
{loadingPage ? (
<>
<ActivityIndicator color={colors.primary.accent} />
<Paragraph>Preparing web clip...</Paragraph>
</>
) : null}
</>
)}
</SafeAreaProvider>
</View>
{appendNoteId ? (
<AppendNote
id={appendNoteId}
onLoad={(title) => {
if (!noteTitle.current) {
noteTitle.current = title;
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
}
}}
/>
) : null}
<View
style={{
flexDirection: "row",
paddingHorizontal: 12,
width: "100%",
alignItems: "center"
}}
>
<Paragraph
style={{
marginRight: 10
}}
allowFontScaling={false}
>
Clip Mode:
</Paragraph>
{rawData.value && isURL(rawData.value) ? (
<Button
type={mode === 2 ? "inverted" : "plain"}
icon={mode === 2 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(2)}
title={modes[2].title}
height={30}
<Paragraph
style={{
marginRight: 10
}}
allowFontScaling={false}
>
Clip Mode:
</Paragraph>
{rawData.value && isURL(rawData.value) ? (
<Button
type={mode === 2 ? "inverted" : "plain"}
icon={mode === 2 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(2)}
title={modes[2].title}
height={30}
allowFontScaling={false}
style={{
paddingHorizontal: 6
}}
/>
) : null}
<Button
type={mode === 1 ? "inverted" : "plain"}
icon={mode === 1 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(1)}
allowFontScaling={false}
title={modes[1].title}
height={30}
style={{
paddingHorizontal: 6
}}
/>
) : null}
<Button
type={mode === 1 ? "inverted" : "plain"}
icon={mode === 1 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(1)}
allowFontScaling={false}
title={modes[1].title}
height={30}
style={{
paddingHorizontal: 6
}}
/>
</View>
</View>
</View>
</View>
<View
style={{
width: "100%",
borderRadius: 10,
flexDirection: "column",
marginTop: 10,
alignSelf: "center",
alignItems: "center",
paddingHorizontal: 12,
gap: 10
}}
>
<Button
icon="plus"
onPress={() => {
useShareStore.getState().setAppendNote(null);
}}
type={!appendNoteId ? "transparent" : "plain"}
title="New note"
allowFontScaling={false}
<View
style={{
paddingHorizontal: 12,
height: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "flex-start"
}}
/>
<Button
icon="text-short"
onPress={() => {
setSearchMode("appendNote");
}}
allowFontScaling={false}
type={appendNoteId ? "transparent" : "plain"}
title={`Append to a note`}
style={{
borderRadius: 10,
flexDirection: "column",
marginTop: 10,
alignSelf: "center",
alignItems: "center",
paddingHorizontal: 12,
height: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "flex-start"
gap: 10
}}
/>
{!appendNoteId ? (
<AddTags
>
<Button
icon="plus"
onPress={() => {
setSearchMode("selectTags");
useShareStore.getState().setAppendNote(null);
}}
type={!appendNoteId ? "transparent" : "plain"}
title="New note"
allowFontScaling={false}
style={{
paddingHorizontal: 12,
height: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "flex-start"
}}
/>
) : null}
{!appendNoteId ? (
<AddNotebooks
<Button
icon="text-short"
onPress={() => {
setSearchMode("selectNotebooks");
setSearchMode("appendNote");
}}
allowFontScaling={false}
type={appendNoteId ? "transparent" : "plain"}
title={`Append to a note`}
style={{
paddingHorizontal: 12,
height: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "flex-start"
}}
/>
) : null}
</View>
<View
style={{
height: isTablet() ? 150 : Platform.OS === "ios" ? 110 : 0
}}
/>
</View>
</ScrollView>
{!appendNoteId ? (
<AddTags
onPress={() => {
setSearchMode("selectTags");
}}
/>
) : null}
{!appendNoteId ? (
<AddNotebooks
onPress={() => {
setSearchMode("selectNotebooks");
}}
/>
) : null}
</View>
<View
style={{
height: isTablet() ? 150 : Platform.OS === "ios" ? 110 : 0
}}
/>
</View>
</ScrollView>
)}
</WrapperView>
</View>
);

View File

@@ -17,12 +17,13 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/database";
import { DatabaseLogger, db } from "../common/database";
import { eSendEvent } from "../services/event-manager";
import Navigation from "../services/navigation";
import { NotePreviewWidget } from "../services/note-preview-widget";
import Notifications from "../services/notifications";
import { eAfterSync } from "../utils/events";
import { NotesnookModule, ShortcutInfo } from "../utils/notesnook-module";
import { useFavoriteStore } from "./use-favorite-store";
import { useMenuStore } from "./use-menu-store";
import { useMonographStore } from "./use-monograph-store";
@@ -34,6 +35,81 @@ import { useTagStore } from "./use-tag-store";
import { useTrashStore } from "./use-trash-store";
import { useUserStore } from "./use-user-store";
async function syncShortcuts(result: ShortcutInfo[]) {
try {
for (let shortcut of result) {
switch (shortcut.type) {
case "note":
{
const note = await db.notes.note(shortcut.id);
if (!note) {
NotesnookModule.removeShortcut(shortcut.id);
} else if (note.title !== shortcut.title) {
NotesnookModule.updateShortcut(
shortcut.id,
"note",
note.title,
note.headline
);
}
}
break;
case "notebook":
{
const notebook = await db.notebooks.notebook(shortcut.id);
if (!notebook) {
NotesnookModule.removeShortcut(shortcut.id);
} else if (notebook.title !== shortcut.title) {
NotesnookModule.updateShortcut(
shortcut.id,
"notebook",
notebook.title,
notebook.description
);
}
}
break;
case "tag":
{
const tag = await db.tags.tag(shortcut.id);
if (!tag) {
NotesnookModule.removeShortcut(shortcut.id);
} else if (tag.title !== shortcut.title) {
NotesnookModule.updateShortcut(
shortcut.id,
"tag",
tag.title,
tag.title
);
}
}
break;
case "color":
{
const color = await db.colors.color(shortcut.id);
if (!color) {
NotesnookModule.removeShortcut(shortcut.id);
} else if (color.title !== shortcut.title) {
NotesnookModule.updateShortcut(
shortcut.id,
"color",
color.title,
color.title,
color.colorCode
);
}
}
break;
}
}
} catch (e) {
DatabaseLogger.error(
e as Error,
"Error while syncing homescreen shortcuts"
);
}
}
export function initAfterSync(type: "full" | "send" = "send") {
if (type === "full") {
Navigation.queueRoutesForUpdate();
@@ -46,9 +122,16 @@ export function initAfterSync(type: "full" | "send" = "send") {
profile: db.settings.getProfile()
});
}
Notifications.setupReminders(true);
NotePreviewWidget.updateNotes();
eSendEvent(eAfterSync);
NotesnookModule.getAllShortcuts()
.then(syncShortcuts)
.catch((e) => {
DatabaseLogger.log(e);
});
}
export async function initialize() {}

View File

@@ -25,8 +25,7 @@ import {
Note,
Notebook,
Reminder,
Tag,
TrashItem
Tag
} from "@notesnook/core";
import { ParamListBase } from "@react-navigation/core";
import { create } from "zustand";
@@ -36,12 +35,15 @@ export type GenericRouteParam = {
};
export type NotebookScreenParams = {
item: Notebook;
id: string;
item?: Notebook;
canGoBack?: boolean;
};
export type NotesScreenParams = {
item: Note | Notebook | Tag | Color | TrashItem | Reminder;
type: "tag" | "color" | "monograph";
id: string;
item?: Tag | Color;
canGoBack?: boolean;
};

View File

@@ -24,7 +24,9 @@ import { initialWindowMetrics } from "react-native-safe-area-context";
import { FileType } from "react-native-scoped-storage";
import { create } from "zustand";
import { ThemeDark, ThemeLight, ThemeDefinition } from "@notesnook/theme";
import { Reminder } from "@notesnook/core";
import { DayFormat, WeekFormat, Reminder } from "@notesnook/core";
import { db } from "../common/database";
import { EDITOR_LINE_HEIGHT } from "../utils/constants";
export const HostIds = [
"API_HOST",
"AUTH_HOST",
@@ -99,6 +101,7 @@ export type Settings = {
serverUrls?: Record<HostId, string>;
defaultSidebarTab: number;
checkForUpdates?: boolean;
defaultLineHeight: number;
};
type DimensionsType = {
@@ -134,8 +137,12 @@ export interface SettingStore {
setInsets: (insets: Insets) => void;
timeFormat: string;
dateFormat: string;
dayFormat: DayFormat;
weekFormat: WeekFormat;
dbPassword?: string;
isOldAppLock: () => boolean;
initialUrl: string | null;
refresh: () => void;
}
const { width, height } = Dimensions.get("window");
@@ -198,7 +205,8 @@ export const defaultSettings: SettingStore["settings"] = {
backupType: "partial",
fullBackupReminder: "never",
lastFullBackupDate: 0,
checkForUpdates: true
checkForUpdates: true,
defaultLineHeight: EDITOR_LINE_HEIGHT.DEFAULT
};
export const useSettingStore = create<SettingStore>((set, get) => ({
@@ -221,6 +229,8 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
setInsets: (insets) => set({ insets }),
timeFormat: "12-hour",
dateFormat: "DD-MM-YYYY",
dayFormat: "short",
weekFormat: "Sun",
setAppDidEnterBackgroundForAction: (value: boolean) => {
set({
appDidEnterBackgroundForAction: value
@@ -236,5 +246,14 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
},
insets: initialWindowMetrics?.insets
? initialWindowMetrics.insets
: { top: 0, right: 0, left: 0, bottom: 0 }
: { top: 0, right: 0, left: 0, bottom: 0 },
initialUrl: null,
refresh: () => {
set({
dayFormat: db.settings.getDayFormat(),
timeFormat: db.settings.getTimeFormat(),
dateFormat: db.settings?.getTimeFormat(),
weekFormat: db.settings.getWeekFormat()
});
}
}));

View File

@@ -76,18 +76,14 @@ export const useThemeStore = create<ThemeStore>((set, get) => ({
? (Appearance.getColorScheme() as "dark" | "light")
: SettingsService.get().colorScheme,
setDarkTheme: (darkTheme) => {
switchThemeWithAnimation(() => {
set({ darkTheme });
changeSystemBarColors();
SettingsService.setProperty("darkTheme", darkTheme);
});
set({ darkTheme });
changeSystemBarColors();
SettingsService.setProperty("darkTheme", darkTheme);
},
setLightTheme: (lightTheme) => {
switchThemeWithAnimation(() => {
set({ lightTheme });
changeSystemBarColors();
SettingsService.setProperty("lighTheme", lightTheme);
});
set({ lightTheme });
changeSystemBarColors();
SettingsService.setProperty("lighTheme", lightTheme);
},
setColorScheme: (colorScheme) => {
switchThemeWithAnimation(() => {

View File

@@ -122,3 +122,9 @@ export const SUBSCRIPTION_PROVIDER = {
icon: "web"
}
};
export const EDITOR_LINE_HEIGHT = {
DEFAULT: 1.2,
MAX: 10,
MIN: 1
};

View File

@@ -19,6 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { NativeModules, Platform } from "react-native";
export type ShortcutInfo = {
id: string;
title: string;
description: string;
type: "note" | "notebook" | "tag" | "color";
};
interface NotesnookModuleInterface {
getActivityName: () => Promise<string>;
setBackgroundColor: (color: string) => void;
@@ -41,6 +48,23 @@ interface NotesnookModuleInterface {
updateWidgetNote: (noteId: string, data: string) => void;
updateReminderWidget: () => void;
isGestureNavigationEnabled: () => boolean;
addShortcut: (
id: string,
type: "note" | "notebook" | "tag" | "color",
title: string,
description?: string,
color?: string
) => Promise<boolean>;
removeShortcut: (id: string) => Promise<boolean>;
updateShortcut: (
id: string,
type: "note" | "notebook" | "tag" | "color",
title: string,
description?: string,
color?: string
) => Promise<boolean>;
removeAllShortcuts: () => Promise<boolean>;
getAllShortcuts: () => Promise<ShortcutInfo[]>;
}
export const NotesnookModule: NotesnookModuleInterface = Platform.select({
@@ -61,7 +85,12 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({
hasWidgetNote: () => {},
updateWidgetNote: () => {},
updateReminderWidget: () => {},
isGestureNavigationEnabled: () => true
isGestureNavigationEnabled: () => true,
addShortcut: () => Promise.resolve(false),
removeShortcut: () => Promise.resolve(false),
updateShortcut: () => Promise.resolve(false),
removeAllShortcuts: () => Promise.resolve(false),
getAllShortcuts: () => Promise.resolve([])
},
android: NativeModules.NNativeModule
});

View File

@@ -159,7 +159,7 @@ function show(event: any, text: string, position = 2) {
duration: 2000,
autoHide: true,
clickToHide: true,
shadow: true
shadow: false
});
}

View File

@@ -134,7 +134,7 @@ describe("NOTE TESTS", () => {
.run();
});
it.only("Assign colors to a note", async () => {
it("Assign colors to a note", async () => {
await TestBuilder.create()
.prepare()
.createNote()

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect } from "detox";
import { notesnook } from "../test.ids";
import { TestBuilder, Tests } from "./utils";
import { Element, TestBuilder, Tests } from "./utils";
async function lockNote() {
await TestBuilder.create()
@@ -67,6 +67,11 @@ async function goToPrivacySecuritySettings() {
.waitAndTapById("sidemenu-settings-icon")
.wait()
.waitAndTapByText("Settings")
.addStep(async () => {
const element = new Element("id", "settings-list");
await element.element.scroll(200, "down");
})
.wait(500)
.waitAndTapByText("Vault")
.run();
}
@@ -94,7 +99,7 @@ describe("VAULT", () => {
.typeTextById(notesnook.ids.dialogs.vault.pwd, "1234")
.typeTextById(notesnook.ids.dialogs.vault.changePwd, "2362")
.waitAndTapByText("Change")
.pressBack(4)
.pressBack(3)
.addStep(async () => await openLockedNote("2362"))
.run();
});

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