Compare commits

...

151 Commits

Author SHA1 Message Date
01zulfi
e38610e02c clipper: remove javascript handler attributes from clip
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-13 12:32:44 +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
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
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
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
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
215 changed files with 5929 additions and 2139 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

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.3.8",
"version": "3.3.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.3.8",
"version": "3.3.10",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.8",
"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

@@ -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 3091
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

@@ -1,13 +1,8 @@
- Add notebooks, tags and colors to Home Screen Shortcuts
- Change day format and use /day in notes
- Add Setting to change default editor line height
- Set a custom title for monographs
- Add webpage title and date clipped to web clips
- Configure Week to start from Sunday or Monday
- Change Note's creation date
- Set expiry date on notes
- Temporarily disable password change and recovery options
- Note history now includes note title
- Minor bug fixes
- 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

@@ -127,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(() => {
@@ -138,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();

View File

@@ -193,3 +193,7 @@ export async function checkAndCreateDir(path: string) {
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, ToastManager } 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={{
@@ -257,13 +260,10 @@ export const Login = ({
paddingHorizontal: 0
}}
onPress={() => {
ToastManager.show({
type: "info",
message:
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
if (loading || !email.current) return;
presentSheet({
component: <ForgotPassword userEmail={email.current} />
});
// if (loading || !email.current) return;
// SheetManager.show("forgotpassword_sheet");
}}
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

@@ -28,6 +28,7 @@ import { DDS } from "../../../services/device-detection";
import {
ToastManager,
Vault,
VaultRequestType,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
@@ -58,148 +59,104 @@ import { DefaultAppStyles } from "../../../utils/styles";
import { Note, NoteContent, VAULT_ERRORS } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
type VaultDialogData = {
item: Note;
} & Partial<Omit<Vault, "item">>;
interface VaultDialogState {
visible: boolean;
wrongPassword: boolean;
loading: boolean;
note?: Note;
vault: boolean;
locked: boolean;
permanant: boolean;
goToEditor: boolean;
share: boolean;
passwordsDontMatch: boolean;
deleteNote: boolean;
focusIndex: number | null;
biometricUnlock: boolean;
isBiometryEnrolled: boolean;
isBiometryAvailable: boolean;
fingerprintAccess: boolean;
changePassword: boolean;
copyNote: boolean;
revokeFingerprintAccess: boolean;
title: string;
description: string | null;
clearVault: boolean;
deleteVault: boolean;
deleteAll: boolean;
noteLocked: boolean;
novault: boolean;
customActionTitle: string | null;
customActionParagraph: string | null;
customAction: boolean;
onUnlock?: (
item: Note & {
content?: NoteContent<false>;
},
password: string
) => void;
}
export const VaultDialog: React.FC = () => {
const { colors } = useThemeColors();
const [state, setState] = useState<VaultDialogState>({
visible: false,
wrongPassword: false,
loading: false,
note: undefined,
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,
novault: false,
customActionTitle: null,
customActionParagraph: null,
customAction: false,
onUnlock: undefined
});
// 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: VaultDialogData) => {
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;
setState((prev) => ({
...prev,
note: data.item,
novault: data.novault || false,
locked: data.locked || false,
permanant: data.permanant || false,
goToEditor: data.goToEditor || false,
share: data.share || false,
deleteNote: data.deleteNote || false,
copyNote: data.copyNote || false,
isBiometryAvailable: available,
biometricUnlock: fingerprint,
isBiometryEnrolled: fingerprint,
fingerprintAccess: data.fingerprintAccess || false,
changePassword: data.changePassword || false,
revokeFingerprintAccess: data.revokeFingerprintAccess || false,
title: data.title || strings.goToEditor(),
description: data.description || null,
clearVault: data.clearVault || false,
deleteVault: data.deleteVault || false,
noteLocked,
customActionTitle: data.customActionTitle || null,
customActionParagraph: data.customActionParagraph || null,
customAction: !!(data.customActionTitle && data.customActionParagraph),
onUnlock: data.onUnlock
}));
// 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;
if (
// 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.novault &&
!data.fingerprintAccess &&
!data.revokeFingerprintAccess &&
!data.changePassword &&
!data.clearVault &&
!data.deleteVault &&
!data.customActionTitle
) {
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 {
setState((prev) => ({ ...prev, visible: true }));
setVisible(true);
}
}, []);
const close = useCallback(() => {
if (state.loading) {
if (loading) {
ToastManager.show({
heading: state.title,
heading: titleRef.current,
message: strings.pleaseWait() + "...",
type: "success",
context: "local"
@@ -209,46 +166,37 @@ export const VaultDialog: React.FC = () => {
Navigation.queueRoutesForUpdate();
// Reset password refs
passwordRef.current = null;
confirmPasswordRef.current = null;
newPasswordRef.current = null;
setState({
visible: false,
wrongPassword: false,
loading: false,
note: undefined,
vault: false,
locked: false,
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,
novault: false,
customActionTitle: null,
customActionParagraph: null,
customAction: false,
onUnlock: undefined
});
}, [state.loading, state.title]);
// 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 () => {
setState((prev) => ({ ...prev, loading: true }));
setLoading(true);
try {
let verified = true;
if (await db.user.getUser()) {
@@ -256,7 +204,7 @@ export const VaultDialog: React.FC = () => {
}
if (verified) {
let noteIds: string[] = [];
if (state.deleteAll) {
if (deleteAll) {
const vault = await db.vaults.default();
const relations = await db.relations
.from(
@@ -269,9 +217,9 @@ export const VaultDialog: React.FC = () => {
.get();
noteIds = relations.map((item) => item.toId);
}
await db.vault.delete(state.deleteAll);
await db.vault.delete(deleteAll);
if (state.deleteAll) {
if (deleteAll) {
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
@@ -284,7 +232,7 @@ export const VaultDialog: React.FC = () => {
});
}
eSendEvent("vaultUpdated");
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
setTimeout(() => {
close();
}, 100);
@@ -298,10 +246,10 @@ export const VaultDialog: React.FC = () => {
} catch (e) {
console.error(e);
}
}, [state.deleteAll, close]);
}, [deleteAll, close]);
const clearVault = useCallback(async () => {
setState((prev) => ({ ...prev, loading: true }));
setLoading(true);
try {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault!, "note").get();
@@ -319,7 +267,7 @@ export const VaultDialog: React.FC = () => {
true
);
});
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
close();
eSendEvent("vaultUpdated");
} catch (e) {
@@ -329,7 +277,7 @@ export const VaultDialog: React.FC = () => {
context: "local"
});
}
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
}, [close]);
const lockNote = useCallback(async () => {
@@ -341,9 +289,9 @@ export const VaultDialog: React.FC = () => {
});
return;
} else {
await db.vault.add(state.note!.id);
await db.vault.add(noteRef.current!.id);
eSendEvent(eUpdateNoteInEditor, state.note, true);
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
close();
ToastManager.show({
@@ -351,26 +299,29 @@ export const VaultDialog: React.FC = () => {
type: "error",
context: "local"
});
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
}
}, [state.note, close]);
}, [close]);
const permanantUnlock = useCallback(() => {
db.vault
.remove(state.note!.id, passwordRef.current || "")
.then(() => {
.remove(noteRef.current!.id, passwordRef.current || "")
.then(async () => {
ToastManager.show({
heading: strings.noteUnlocked(),
type: "success",
context: "global"
});
eSendEvent(eUpdateNoteInEditor, state.note, true);
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
if (biometricUnlock && !isBiometryEnrolled) {
await enrollFingerprint(passwordRef.current || "");
}
close();
})
.catch((e) => {
takeErrorAction();
});
}, [state.note, close]);
}, [close, biometricUnlock, isBiometryEnrolled]);
const openInEditor = useCallback(
(note: Note & { content?: NoteContent<false> }) => {
@@ -418,20 +369,17 @@ export const VaultDialog: React.FC = () => {
const deleteNote = useCallback(async () => {
try {
await db.vault.remove(state.note!.id, passwordRef.current || "");
await deleteItems("note", [state.note!.id]);
await db.vault.remove(noteRef.current!.id, passwordRef.current || "");
await deleteItems("note", [noteRef.current!.id]);
close();
} catch (e) {
takeErrorAction();
}
}, [state.note, close]);
}, [close]);
const takeErrorAction = useCallback(() => {
setState((prev) => ({
...prev,
wrongPassword: true,
visible: true
}));
setWrongPassword(true);
setVisible(true);
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
@@ -445,39 +393,40 @@ export const VaultDialog: React.FC = () => {
try {
if (!passwordRef.current) throw new Error("Invalid password");
const note = await db.vault.open(state.note!.id, passwordRef.current);
const note = await db.vault.open(
noteRef.current!.id,
passwordRef.current
);
if (!note) throw new Error("Failed to unlock note.");
if (state.biometricUnlock && !state.isBiometryEnrolled) {
if (biometricUnlock && !isBiometryEnrolled) {
await enrollFingerprint(passwordRef.current || "");
}
if (state.goToEditor) {
const requestType = requestTypeRef.current;
if (requestType === VaultRequestType.GoToEditor) {
openInEditor(note);
} else if (state.share) {
} else if (requestType === VaultRequestType.ShareNote) {
await shareNote(note);
} else if (state.deleteNote) {
} else if (requestType === VaultRequestType.DeleteNote) {
await deleteNote();
} else if (state.copyNote) {
} else if (requestType === VaultRequestType.CopyNote) {
await copyNote(note);
} else if (state.customAction && state.onUnlock) {
} else if (
requestType === VaultRequestType.CustomAction &&
onUnlockRef.current
) {
const password = passwordRef.current;
close();
await sleep(300);
state.onUnlock(note, password);
onUnlockRef.current(note, password);
}
} catch (e) {
takeErrorAction();
}
}, [
state.note,
state.biometricUnlock,
state.isBiometryEnrolled,
state.goToEditor,
state.share,
state.deleteNote,
state.copyNote,
state.customAction,
state.onUnlock,
biometricUnlock,
isBiometryEnrolled,
openInEditor,
shareNote,
deleteNote,
@@ -495,20 +444,20 @@ export const VaultDialog: React.FC = () => {
});
return;
}
if (state.permanant) {
if (requestTypeRef.current === VaultRequestType.PermanentUnlock) {
permanantUnlock();
} else {
await openNote();
}
}, [state.permanant, permanantUnlock, openNote]);
}, [permanantUnlock, openNote]);
const enrollFingerprint = useCallback(
async (password: string) => {
setState((prev) => ({ ...prev, loading: true }));
setLoading(true);
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
@@ -523,7 +472,7 @@ export const VaultDialog: React.FC = () => {
type: "error",
context: "local"
});
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
}
},
[close]
@@ -532,13 +481,13 @@ export const VaultDialog: React.FC = () => {
const createVault = useCallback(async () => {
await db.vault.create(passwordRef.current || "");
if (state.biometricUnlock) {
if (biometricUnlock) {
await enrollFingerprint(passwordRef.current || "");
}
if (state.note?.id) {
await db.vault.add(state.note.id);
eSendEvent(eUpdateNoteInEditor, state.note, true);
setState((prev) => ({ ...prev, loading: false }));
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",
@@ -554,7 +503,7 @@ export const VaultDialog: React.FC = () => {
close();
}
eSendEvent("vaultUpdated");
}, [state.biometricUnlock, state.note, enrollFingerprint, close]);
}, [biometricUnlock, enrollFingerprint, close]);
const revokeFingerprintAccess = useCallback(async () => {
try {
@@ -578,8 +527,8 @@ export const VaultDialog: React.FC = () => {
async (title?: string, description?: string) => {
try {
const credentials = await BiometricService.getCredentials(
title || state.title,
description || state.description || ""
title || titleRef.current,
description || descriptionRef.current || ""
);
if (!credentials) throw new Error("Failed to get user credentials");
@@ -590,22 +539,25 @@ export const VaultDialog: React.FC = () => {
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
setState((prev) => ({ ...prev, visible: true }));
setVisible(true);
}
} catch (e) {
console.error(e);
}
},
[state.title, state.description]
[]
);
const onPress = useCallback(async () => {
if (state.revokeFingerprintAccess) {
const requestType = requestTypeRef.current;
if (requestType === VaultRequestType.RevokeFingerprint) {
await revokeFingerprintAccess();
close();
return;
}
if (state.loading) return;
if (loading) return;
if (!passwordRef.current) {
ToastManager.show({
@@ -616,26 +568,26 @@ export const VaultDialog: React.FC = () => {
return;
}
if (!state.novault) {
if (requestType === VaultRequestType.CreateVault) {
if (passwordRef.current !== confirmPasswordRef.current) {
ToastManager.show({
heading: strings.passwordNotMatched(),
type: "error",
context: "local"
});
setState((prev) => ({ ...prev, passwordsDontMatch: true }));
setPasswordsDontMatch(true);
return;
}
createVault();
} else if (state.changePassword) {
setState((prev) => ({ ...prev, loading: true }));
} else if (requestType === VaultRequestType.ChangePassword) {
setLoading(true);
db.vault
.changePassword(passwordRef.current, newPasswordRef.current || "")
.then(() => {
setState((prev) => ({ ...prev, loading: false }));
if (state.biometricUnlock) {
setLoading(false);
if (biometricUnlock) {
enrollFingerprint(newPasswordRef.current || "");
}
ToastManager.show({
@@ -646,7 +598,7 @@ export const VaultDialog: React.FC = () => {
close();
})
.catch((e) => {
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
if (e.message === VAULT_ERRORS.wrongPassword) {
ToastManager.show({
heading: strings.passwordIncorrect(),
@@ -657,50 +609,62 @@ export const VaultDialog: React.FC = () => {
ToastManager.error(e);
}
});
} else if (state.locked) {
} else if (requestType === VaultRequestType.LockNote) {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setState((prev) => ({ ...prev, wrongPassword: true }));
setWrongPassword(true);
return;
}
if (state.noteLocked) {
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 {
db.vault
.unlock(passwordRef.current)
.then(async () => {
setState((prev) => ({ ...prev, wrongPassword: false }));
await lockNote();
})
.catch((e) => {
takeErrorAction();
});
console.log("Error: Note should be locked for this operation");
}
} else if (state.fingerprintAccess) {
} else if (requestType === VaultRequestType.EnableFingerprint) {
enrollFingerprint(passwordRef.current);
} else if (state.clearVault) {
} else if (requestType === VaultRequestType.ClearVault) {
await clearVault();
} else if (state.deleteVault) {
} else if (requestType === VaultRequestType.DeleteVault) {
await deleteVault();
} else if (state.customAction) {
await unlockNote();
}
}, [
state.revokeFingerprintAccess,
state.loading,
state.novault,
state.changePassword,
state.locked,
state.noteLocked,
state.fingerprintAccess,
state.clearVault,
state.deleteVault,
state.customAction,
state.biometricUnlock,
loading,
biometricUnlock,
revokeFingerprintAccess,
close,
createVault,
@@ -722,23 +686,21 @@ export const VaultDialog: React.FC = () => {
};
}, [open, close]);
if (!state.visible) return null;
if (!visible) return null;
const {
note,
novault,
deleteNote: shouldDeleteNote,
share,
goToEditor,
fingerprintAccess,
changePassword,
loading,
deleteVault: shouldDeleteVault,
clearVault: shouldClearVault,
customAction,
customActionTitle,
customActionParagraph
} = state;
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
@@ -760,8 +722,10 @@ export const VaultDialog: React.FC = () => {
}}
>
<DialogHeader
title={state.title}
paragraph={customActionParagraph || ""}
title={titleRef.current}
paragraph={
paragraphRef.current || customActionParagraphRef.current || ""
}
icon="shield"
padding={12}
/>
@@ -772,12 +736,12 @@ export const VaultDialog: React.FC = () => {
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{(novault ||
changePassword ||
shouldClearVault ||
shouldDeleteVault ||
customAction) &&
!state.revokeFingerprintAccess ? (
{(isChangePassword ||
isClearVault ||
!isCreateVault ||
isDeleteVault ||
isCustomAction) &&
!isRevokeFingerprint ? (
<>
<Input
fwdRef={passInputRef}
@@ -788,37 +752,40 @@ export const VaultDialog: React.FC = () => {
passwordRef.current = value;
}}
marginBottom={
!state.biometricUnlock ||
!state.isBiometryEnrolled ||
!novault ||
changePassword ||
customAction
!biometricUnlock ||
!isBiometryEnrolled ||
isCreateVault ||
isChangePassword ||
isCustomAction
? 0
: 10
}
onSubmit={() => {
if (changePassword) {
if (isChangePassword) {
confirmPassRef.current?.focus();
} else {
onPress();
}
}}
autoComplete="password"
returnKeyLabel={changePassword ? strings.next() : state.title}
returnKeyType={changePassword ? "next" : "done"}
returnKeyLabel={
isChangePassword ? strings.next() : titleRef.current
}
returnKeyType={isChangePassword ? "next" : "done"}
secureTextEntry
placeholder={
changePassword
isChangePassword
? strings.currentPassword()
: strings.password()
}
/>
{!state.biometricUnlock ||
!state.isBiometryEnrolled ||
!novault ||
changePassword ||
customAction ? null : (
{!biometricUnlock ||
!isBiometryEnrolled ||
!isBiometryAvailable ||
isCreateVault ||
isChangePassword ||
isCustomAction ? null : (
<Button
onPress={() =>
onPressFingerprintAuth(strings.unlockNote(), "")
@@ -832,16 +799,11 @@ export const VaultDialog: React.FC = () => {
</>
) : null}
{shouldDeleteVault && (
{isDeleteVault && (
<Button
onPress={() =>
setState((prev) => ({
...prev,
deleteAll: !prev.deleteAll
}))
}
onPress={() => setDeleteAll(!deleteAll)}
icon={
state.deleteAll
deleteAll
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
@@ -854,7 +816,7 @@ export const VaultDialog: React.FC = () => {
/>
)}
{changePassword ? (
{isChangePassword ? (
<>
<Seperator half />
<Input
@@ -877,7 +839,7 @@ export const VaultDialog: React.FC = () => {
</>
) : null}
{!novault ? (
{isCreateVault ? (
<View>
<Input
fwdRef={passInputRef}
@@ -912,12 +874,9 @@ export const VaultDialog: React.FC = () => {
onChangeText={(value) => {
confirmPasswordRef.current = value;
if (value !== passwordRef.current) {
setState((prev) => ({ ...prev, passwordsDontMatch: true }));
setPasswordsDontMatch(true);
} else {
setState((prev) => ({
...prev,
passwordsDontMatch: false
}));
setPasswordsDontMatch(false);
}
}}
onSubmit={() => {
@@ -928,22 +887,23 @@ export const VaultDialog: React.FC = () => {
</View>
) : null}
{state.biometricUnlock && !state.isBiometryEnrolled && novault ? (
{biometricUnlock && !isBiometryEnrolled && !isCreateVault ? (
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
) : null}
{state.isBiometryAvailable &&
!state.fingerprintAccess &&
!shouldClearVault &&
!shouldDeleteVault &&
!customAction &&
((!state.biometricUnlock && !changePassword) || !novault) ? (
{!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={() => {
setState((prev) => ({
...prev,
biometricUnlock: !prev.biometricUnlock
}));
setBiometricUnlock(!biometricUnlock);
}}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
@@ -952,11 +912,9 @@ export const VaultDialog: React.FC = () => {
width="100%"
title={strings.unlockWithBiometrics()}
iconColor={
state.biometricUnlock
? colors.selected.accent
: colors.primary.icon
biometricUnlock ? colors.selected.accent : colors.primary.icon
}
type={state.biometricUnlock ? "transparent" : "plain"}
type={biometricUnlock ? "transparent" : "plain"}
/>
) : null}
</View>
@@ -965,34 +923,8 @@ export const VaultDialog: React.FC = () => {
onPressNegative={close}
onPressPositive={onPress}
loading={loading}
positiveType={
shouldDeleteVault || shouldClearVault ? "errorShade" : "transparent"
}
positiveTitle={
shouldDeleteVault
? strings.delete()
: shouldClearVault
? strings.clear()
: fingerprintAccess
? strings.enable()
: state.revokeFingerprintAccess
? strings.revoke()
: changePassword
? strings.change()
: customAction && customActionTitle
? customActionTitle
: state.noteLocked
? shouldDeleteNote
? strings.delete()
: share
? strings.share()
: goToEditor
? strings.open()
: strings.unlock()
: !note?.id
? strings.create()
: strings.lock()
}
positiveType={positiveButtonTypeRef.current}
positiveTitle={buttonTitleRef.current || strings.unlock()}
/>
</View>
<Toast context="local" />

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

@@ -41,7 +41,8 @@ import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
openVault
openVault,
VaultRequestType
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync";
@@ -128,10 +129,12 @@ const MergeConflicts = () => {
let noteContent: UnencryptedContentItem;
if (isLocked) {
openVault({
requestType: VaultRequestType.CustomAction,
item: item,
novault: true,
customActionTitle: "Unlock note",
customActionParagraph: "Unlock note to merge conflicts",
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!);

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

@@ -33,7 +33,6 @@ import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { Dialog } from "../dialog";
const TOP_BAR_ITEMS: ActionId[] = [
"pin",
@@ -375,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

@@ -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

@@ -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

@@ -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

@@ -54,7 +54,8 @@ import {
eSubscribeEvent,
openVault,
presentSheet,
ToastManager
ToastManager,
VaultRequestType
} from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
@@ -408,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 {
@@ -867,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";
@@ -896,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;
}
@@ -918,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;
}
@@ -949,11 +949,10 @@ export const useActions = ({
close();
await sleep(300);
openVault({
copyNote: true,
novault: true,
locked: true,
requestType: VaultRequestType.CopyNote,
item: item as Note,
title: strings.copyNote()
title: strings.copyNote(),
buttonTitle: strings.copy()
});
} else {
processingId.current = "copyContent";

View File

@@ -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,
@@ -332,6 +333,7 @@ const onLogout = async (reason: string) => {
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
eSendEvent(eAfterSync);
};
async function checkForShareExtensionLaunchedInBackground() {
@@ -561,6 +563,7 @@ export const useAppEvents = () => {
initialUrl: string;
backupDidWait: boolean;
isConnectingSSE: boolean;
attachmentsCachedOfflineMode: boolean;
}>
>({});
@@ -655,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 */
});
@@ -722,24 +729,39 @@ export const useAppEvents = () => {
useEffect(() => {
const subscriptions = [
EV.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
EV.subscribe(EVENTS.syncAborted, onSyncAborted),
EV.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
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),
EV.subscribe(EVENTS.userSessionExpired, onUserSessionExpired),
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) => {
@@ -765,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;
@@ -797,7 +819,6 @@ export const useAppEvents = () => {
return () => {
emitterSubscriptions.forEach((sub) => sub?.remove?.());
subscriptions.forEach((sub) => sub?.unsubscribe?.());
EV.unsubscribeAll();
};
}, [onSyncComplete, onUserUpdated]);

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

@@ -39,7 +39,7 @@ 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 SettingsService from "../../services/settings";
@@ -47,9 +47,18 @@ 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"
@@ -114,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);
@@ -237,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"
>
@@ -271,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
>
@@ -335,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
@@ -454,7 +470,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
width: "100%",
flexDirection: "column",
justifyContent: "center",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
alignItems: "center"
}}
>
@@ -513,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"
}}
>
<>
@@ -552,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
@@ -612,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

@@ -183,6 +183,7 @@ const Editor = React.memo(
hideKeyboardAccessoryView={false}
allowsFullscreenVideo={true}
allowFileAccessFromFileURLs={true}
allowsInlineMediaPlayback
allowUniversalAccessFromFileURLs={true}
originWhitelist={["*"]}
source={{

View File

@@ -27,7 +27,7 @@ import {
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";
@@ -119,41 +119,30 @@ const file = async (fileOptions: PickerOptions) => {
throw new Error("Failed to attach file");
}
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");
}
} catch (e) {
console.log("ERROR OCCURED HERE.", e);
ToastManager.show({
heading: (e as Error).message,
type: "error",
@@ -264,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,
@@ -304,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 &&
@@ -389,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

@@ -534,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
@@ -550,7 +559,7 @@ export const useEditorEvents = (
.then((data: any) => {
console.log(
"Got attachment data:",
!!data,
data,
editorMessage.resolverId
);
editor.postMessage(NativeEvents.resolve, {

View File

@@ -1032,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();

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

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

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";
@@ -72,8 +77,49 @@ 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(),
@@ -286,17 +332,10 @@ export const settingsGroups: SettingSection[] = [
{
id: "change-password",
name: strings.changePassword(),
// type: "screen",
type: "screen",
description: strings.changePasswordDesc(),
// component: "change-password",
icon: "form-textbox-password",
modifer: () => {
ToastManager.show({
type: "info",
message:
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
});
}
component: "change-password",
icon: "form-textbox-password"
},
{
id: "change-email",
@@ -929,8 +968,9 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => (current as VaultStatusType)?.exists,
modifer: () => {
openVault({
novault: false,
title: strings.createVault()
requestType: VaultRequestType.CreateVault,
title: strings.createVault(),
buttonTitle: strings.create()
});
}
},
@@ -942,9 +982,9 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () =>
openVault({
changePassword: true,
novault: true,
title: strings.changeVaultPassword()
requestType: VaultRequestType.ChangePassword,
title: strings.changeVaultPassword(),
buttonTitle: strings.change()
})
},
{
@@ -955,9 +995,10 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
clearVault: true,
novault: true,
title: strings.clearVault() + "?"
requestType: VaultRequestType.ClearVault,
title: strings.clearVault() + "?",
buttonTitle: strings.clear(),
positiveButtonType: "errorShade"
});
}
},
@@ -969,9 +1010,10 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
deleteVault: true,
novault: true,
title: strings.deleteVault() + "?"
requestType: VaultRequestType.DeleteVault,
title: strings.deleteVault() + "?",
buttonTitle: strings.delete(),
positiveButtonType: "errorShade"
});
}
},
@@ -989,13 +1031,15 @@ export const settingsGroups: SettingSection[] = [
getter: (current) => (current as VaultStatusType)?.biometryEnrolled,
modifer: (current) => {
const _current = current as VaultStatusType;
const isRevoking = _current.biometryEnrolled;
openVault({
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

@@ -32,25 +32,34 @@ import {
} from "../utils/events";
import { strings } from "@notesnook/intl";
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;
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;
customActionTitle: string;
customActionParagraph: string;
onUnlock: (
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>;
},
@@ -87,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

@@ -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

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

View File

@@ -21,7 +21,7 @@ import { notesnook } from "../test.ids";
import { TestBuilder } from "./utils";
describe("Sort & filter", () => {
it.only("Sort by date-edited/date-created", async () => {
it("Sort by date-edited/date-created", async () => {
await TestBuilder.create()
.prepare()
.createNote("Note 1", "Note 1")
@@ -34,7 +34,7 @@ describe("Sort & filter", () => {
.element(by.web.className("ProseMirror"))
.typeText("Edited ", true);
})
.pressBack(1)
.pressBack(2)
.waitAndTapById("icon-sort")
.wait(500)
.waitAndTapByText("Date created")

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();
}

View File

@@ -17,9 +17,10 @@ Notifications.init();
enableFreeze(true);
NetInfo.configure({
reachabilityUrl: "https://notesnook.com",
reachabilityUrl: "https://api.notesnook.com/health",
reachabilityTest: (response) => {
if (!response) return false;
console.log("reachabilty test", response.status);
return response?.status >= 200 && response?.status < 300;
}
});

View File

@@ -1029,7 +1029,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2176;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1104,7 +1104,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.16;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1135,7 +1135,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2176;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1210,7 +1210,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.16;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1367,7 +1367,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2176;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1379,7 +1379,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.16;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1410,7 +1410,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2176;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1423,7 +1423,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.16;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1453,7 +1453,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2176;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1534,7 +1534,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.16;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1565,7 +1565,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2171;
CURRENT_PROJECT_VERSION = 2176;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1647,7 +1647,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.16;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -1,22 +1,22 @@
{
"name": "@notesnook/mobile",
"version": "3.3.13-beta.1",
"version": "3.3.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.13-beta.1",
"version": "3.3.15",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@ammarahmed/notifee-react-native": "^7.4.11",
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"@ammarahmed/notifee-react-native": "^7.4.12",
"@ammarahmed/react-native-background-fetch": "^4.2.3",
"@ammarahmed/react-native-eventsource": "1.1.0",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
"@ammarahmed/react-native-share-extension": "^2.9.5",
"@ammarahmed/react-native-sodium": "^1.6.8",
"@ammarahmed/react-native-upload": "^6.31.0",
"@ammarahmed/react-native-upload": "^6.33.0",
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@bam.tech/react-native-image-resizer": "3.0.11",
"@callstack/repack": "~5.2.1",
@@ -490,18 +490,18 @@
}
},
"node_modules/@ammarahmed/notifee-react-native": {
"version": "7.4.11",
"resolved": "https://registry.npmjs.org/@ammarahmed/notifee-react-native/-/notifee-react-native-7.4.11.tgz",
"integrity": "sha512-9zKhONgir2FN/sMj/rsKAhDwfxadDuxB6LJILKxpGwzqvcIuG70oCKWmBBG9LJJpSfcVpNTdxBoH9RtNZTZSsQ==",
"version": "7.4.12",
"resolved": "https://registry.npmjs.org/@ammarahmed/notifee-react-native/-/notifee-react-native-7.4.12.tgz",
"integrity": "sha512-rZPtHdWZ3PxtMHreVszo0ehw6gnsabw7fpgDv8ztYcMNj4A22ZTyfTAQrSZ534D7iDkWgHi2my82b31MsnlQzQ==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native": "*"
}
},
"node_modules/@ammarahmed/react-native-background-fetch": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-background-fetch/-/react-native-background-fetch-4.2.2.tgz",
"integrity": "sha512-PLBlb/DD+6Fv1auxdD6VRxtuC7GKPhQqR69bovRm5oGavbdSzRr+ZTr8AyNMCcayxKGdgEX2JysrJg8WIySSFQ==",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-background-fetch/-/react-native-background-fetch-4.2.3.tgz",
"integrity": "sha512-cgO9EEFoU1HYuDNMpdTTke7DYzEDQfLsyd/cWIZ5Lpp4zz76+dgp1+LBqbLYdSenhJU1Omghq3xWPsoWDRaemQ==",
"license": "MIT"
},
"node_modules/@ammarahmed/react-native-eventsource": {
@@ -538,9 +538,9 @@
"license": "ISC"
},
"node_modules/@ammarahmed/react-native-upload": {
"version": "6.31.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-upload/-/react-native-upload-6.31.0.tgz",
"integrity": "sha512-GwMDd2IND3nuS4l13pQPP8zA/GJoxQKz0pxCGUoWj65fbPuZPnuUA2Rpf4jZzLlgY62vdjI/TqmX+rPiwwCceQ==",
"version": "6.33.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-upload/-/react-native-upload-6.33.0.tgz",
"integrity": "sha512-hvr/LN1wgbhZv3WsRhrAPC1D8YW0ClgrE5NArqUgHWreSHBCS2zVQhEferTt5fGESaJvx+c69VqaIPqzzb8Y3A==",
"license": "BSD-3-Clause",
"peerDependencies": {
"react": "*",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.3.13",
"version": "3.3.16",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {
@@ -26,13 +26,13 @@
"react": "19.1.1"
},
"dependencies": {
"@ammarahmed/notifee-react-native": "^7.4.11",
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"@ammarahmed/notifee-react-native": "^7.4.12",
"@ammarahmed/react-native-background-fetch": "^4.2.3",
"@ammarahmed/react-native-eventsource": "1.1.0",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
"@ammarahmed/react-native-share-extension": "^2.9.5",
"@ammarahmed/react-native-sodium": "^1.6.8",
"@ammarahmed/react-native-upload": "^6.31.0",
"@ammarahmed/react-native-upload": "^6.33.0",
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@bam.tech/react-native-image-resizer": "3.0.11",
"@callstack/repack": "~5.2.1",
@@ -208,4 +208,4 @@
"engines": {
"node": ">=20"
}
}
}

View File

@@ -0,0 +1,65 @@
diff --git a/node_modules/react-native-theme-switch-animation/android/src/main/java/com/themeswitchanimation/ThemeSwitchAnimationModule.java b/node_modules/react-native-theme-switch-animation/android/src/main/java/com/themeswitchanimation/ThemeSwitchAnimationModule.java
index f4630ac..a3a837f 100644
--- a/node_modules/react-native-theme-switch-animation/android/src/main/java/com/themeswitchanimation/ThemeSwitchAnimationModule.java
+++ b/node_modules/react-native-theme-switch-animation/android/src/main/java/com/themeswitchanimation/ThemeSwitchAnimationModule.java
@@ -13,7 +13,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.core.DeviceEventManagerModule;
-public class ThemeSwitchAnimationModule extends ThemeSwitchAnimationModuleSpec {
+public class ThemeSwitchAnimationModule extends com.themeswitchanimation.ThemeSwitchAnimationModuleSpec {
public static final String NAME = "ThemeSwitchAnimationModule";
private ReactContext reactContext;
@@ -38,6 +38,12 @@ public class ThemeSwitchAnimationModule extends ThemeSwitchAnimationModuleSpec {
this.isAnimating = true;
this.rootView = (ViewGroup) getCurrentActivity().getWindow().getDecorView();
this.capturedImageBitmap = captureScreenshot(this.rootView, this.reactContext);
+ if (this.capturedImageBitmap == null) {
+ reactContext
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+ .emit("FINISHED_FREEZING_SCREEN", null);
+ return;
+ }
this.capturedImageView = createImageView(this.capturedImageBitmap, this.reactContext);
reactContext.runOnUiQueueThread(() -> {
@@ -57,6 +63,9 @@ public class ThemeSwitchAnimationModule extends ThemeSwitchAnimationModuleSpec {
@Override
public void run() {
if (isAnimating) {
+ if (capturedImageView == null) {
+ cleanUp();
+ }
switch (animationType) {
case "circular":
Animations.performCircleAnimation(capturedImageView, rootView, (int) duration, cxRatio, cyRatio, reactContext, new Runnable() {
@@ -94,16 +103,22 @@ public class ThemeSwitchAnimationModule extends ThemeSwitchAnimationModuleSpec {
capturedImageBitmap.recycle();
capturedImageBitmap = null;
}
- rootView.removeView(capturedImageView);
+ if (capturedImageView != null) {
+ rootView.removeView(capturedImageView);
+ }
isAnimating = false;
}
public static Bitmap captureScreenshot(View rootView, ReactContext reactContext) {
- Bitmap capturedImageBitmap = Bitmap.createBitmap(rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(capturedImageBitmap);
- rootView.draw(canvas);
-
- return capturedImageBitmap;
+ try {
+ Bitmap capturedImageBitmap = Bitmap.createBitmap(rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(capturedImageBitmap);
+ rootView.draw(canvas);
+
+ return capturedImageBitmap;
+ } catch (Exception e) {
+ return null;
+ }
}
public static ImageView createImageView(Bitmap capturedImageBitmap, ReactContext reactContext) {

View File

@@ -89,6 +89,50 @@ index 978045f..59c64f1 100644
- implementation 'com.github.florent37:viewtooltip:1.1.6'
+ implementation 'com.github.florent37:ViewTooltip:f79a8955ef'
}
diff --git a/node_modules/react-native-tooltips/android/src/main/java/px/tooltips/RNTooltipsModule.java b/node_modules/react-native-tooltips/android/src/main/java/px/tooltips/RNTooltipsModule.java
index e987ffb..e898a40 100644
--- a/node_modules/react-native-tooltips/android/src/main/java/px/tooltips/RNTooltipsModule.java
+++ b/node_modules/react-native-tooltips/android/src/main/java/px/tooltips/RNTooltipsModule.java
@@ -11,9 +11,11 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReadableMap;
-import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.bridge.UIManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
+import com.facebook.react.uimanager.UIManagerHelper;
+import com.facebook.react.uimanager.common.UIManagerType;
import com.github.florent37.viewtooltip.ViewTooltip;
public class RNTooltipsModule extends ReactContextBaseJavaModule {
@@ -34,12 +36,12 @@ public class RNTooltipsModule extends ReactContextBaseJavaModule {
@ReactMethod
public void Show(final int targetId, final int parentId, final ReadableMap props, final Callback onHide) {
- this.reactContext.getNativeModule(UIManagerModule.class).prependUIBlock(new UIBlock() {
- @Override
- public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
- final ViewGroup target = (ViewGroup) nativeViewHierarchyManager.resolveView(targetId);
+ UIManager uiManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC);
+ if (uiManager == null) return;
+
+ final ViewGroup target = (ViewGroup) uiManager.resolveView(targetId);
reactContext.runOnUiQueueThread(new Runnable() {
@Override
@@ -121,8 +123,7 @@ public class RNTooltipsModule extends ReactContextBaseJavaModule {
}
});
- }
- });
+
}
@ReactMethod
diff --git a/node_modules/react-native-tooltips/ios/RNTooltips.h b/node_modules/react-native-tooltips/ios/RNTooltips.h
index a150e3e..096ad86 100644
--- a/node_modules/react-native-tooltips/ios/RNTooltips.h
@@ -116,7 +160,7 @@ index fb96466..75a6fdb 100644
@interface TooltipDelegate : NSObject <SexyTooltipDelegate>
@@ -7,9 +9,15 @@
@@ -7,9 +9,15 @@ @interface TooltipDelegate : NSObject <SexyTooltipDelegate>
@end
@implementation TooltipDelegate
@@ -132,7 +176,7 @@ index fb96466..75a6fdb 100644
}
@end
@@ -17,77 +25,120 @@
@@ -17,77 +25,120 @@ - (void)tooltipDidDismiss:(SexyTooltip *)tooltip {
@implementation RNTooltips

View File

@@ -36,27 +36,27 @@ const fontMap = JSON.parse(
);
// Register fonts
const OpenSans = path.join(
const Inter = path.join(
__dirname,
import.meta.env.DEV ? "../assets/fonts/" : "../../assets/fonts/",
"open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf"
"Inter-Regular.ttf"
);
const OpenSansBold = path.join(
const InterBold = path.join(
__dirname,
import.meta.env.DEV ? "../assets/fonts/" : "../../assets/fonts/",
"open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf"
"Inter-SemiBold.ttf"
);
console.log("OpenSans", GlobalFonts.registerFromPath(OpenSans, "OpenSans"));
console.log("Inter", GlobalFonts.registerFromPath(Inter, "Inter"));
console.log(
"registering",
"OpenSansBold",
GlobalFonts.registerFromPath(OpenSansBold, "OpenSansBold")
"InterBold",
GlobalFonts.registerFromPath(InterBold, "InterBold")
);
const fontFamilies = {
regular: ["OpenSans"],
bold: ["OpenSansBold"]
regular: ["Inter"],
bold: ["InterBold"]
};
for (const font of fontMap) {
const id = (font.name + font.weight).replace(/ /g, "");
@@ -115,11 +115,11 @@ export async function makeImage(metadata: OGMetadata, cacheKey: string) {
// Draw bottom text
ctx.fillStyle = theme.primary.heading;
ctx.font = "600 32px OpenSansBold";
ctx.font = "600 32px InterBold";
ctx.fillText("Notesnook Monograph", PADDING + 95, HEIGHT - PADDING - 55);
ctx.fillStyle = theme.secondary.paragraph;
ctx.font = "25px OpenSans";
ctx.font = "25px Inter";
ctx.fillText(
"Anonymous, secure, and encrypted note sharing with password protection.",
PADDING + 95,
@@ -128,7 +128,7 @@ export async function makeImage(metadata: OGMetadata, cacheKey: string) {
// Draw date
ctx.fillStyle = theme.secondary.paragraph;
ctx.font = "25px OpenSans";
ctx.font = "25px Inter";
ctx.fillText(metadata.date, PADDING, PADDING + 25);
// Draw title

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/monograph",
"version": "1.2.7",
"version": "1.2.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/monograph",
"version": "1.2.7",
"version": "1.2.8",
"dependencies": {
"@emotion/cache": "11.11.0",
"@emotion/react": "11.11.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/monograph",
"version": "1.2.7",
"version": "1.2.8",
"private": true,
"sideEffects": false,
"type": "module",

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { APP_LOCK_PASSWORD, USER } from "./utils";
import { test, expect, APP_LOCK_PASSWORD, USER } from "./utils";
test("don't show status bar lock app button to unauthenticated user", async ({
page

View File

@@ -16,9 +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 { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { USER } from "./utils";
import { test, expect, USER } from "./utils";
test("create an unencrypted backup", async ({ page }) => {
const app = new AppModel(page);
@@ -38,6 +37,7 @@ test("restore an unencrypted backup", async ({ page }) => {
const settings = await app.goToSettings();
await settings.restoreData("backup.nnbackup");
await app.toasts.waitForToast("Backup restored");
await settings.close();
const notes = await app.goToNotes();
@@ -67,6 +67,7 @@ test("restore an encrypted backup", async ({ page }) => {
const settings = await app.goToSettings();
await settings.restoreData("encrypted.nnbackup", USER.CURRENT.password);
await app.toasts.waitForToast("Backup restored");
await settings.close();
const notes = await app.goToNotes();

View File

@@ -17,8 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, Page, expect } from "@playwright/test";
import { USER } from "./utils";
import { Page } from "@playwright/test";
import { test, expect, USER } from "./utils";
import { AppModel } from "./models/app.model";
import { PriceItem } from "./models/types";

View File

@@ -16,9 +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 { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { getTestId, NOTE } from "./utils";
import { test, expect, getTestId, NOTE } from "./utils";
test("delete the last note of a color", async ({ page }) => {
const app = new AppModel(page);

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 { expect, test } from "@playwright/test";
import { test, expect } from "./utils";
import { AppModel } from "./models/app.model";
import { NotesViewModel } from "./models/notes-view.model";

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { getTestId, NOTE, TITLE_ONLY_NOTE } from "./utils";
import { test, expect, NOTE, TITLE_ONLY_NOTE } from "./utils";
test("focus mode", async ({ page }) => {
const app = new AppModel(page);
@@ -80,7 +79,7 @@ test("creating a new note should clear the editor contents & title", async ({
await notes.newNote();
expect(await notes.editor.isUnloaded()).toBeTruthy();
expect(await notes.editor.getTitle()).toBe("");
await expect(notes.editor.title).toHaveValue("");
expect(await notes.editor.getContent("text")).toBe("");
});
@@ -114,7 +113,7 @@ test("opening an empty titled note should empty out editor contents", async ({
await onlyTitle?.openNote();
expect(await notes.editor.getContent("text")).toBe("");
expect(await notes.editor.getTitle()).toBe(TITLE_ONLY_NOTE.title);
await expect(notes.editor.title).toHaveValue(TITLE_ONLY_NOTE.title);
});
test("focus should not jump to editor while typing in title input", async ({
@@ -127,7 +126,7 @@ test("focus should not jump to editor while typing in title input", async ({
await notes.editor.typeTitle("Hello", 200);
expect(await notes.editor.getTitle()).toBe("Hello");
await expect(notes.editor.title).toHaveValue("Hello");
expect(await notes.editor.getContent("text")).toBe("");
});
@@ -143,21 +142,21 @@ test("when title format is set to headline, title should be generated from headl
const notes = await app.goToNotes();
await notes.createNote({ content: "my precious" });
expect(await notes.editor.getTitle()).toBe("my precious");
await expect(notes.editor.title).toHaveValue("my precious");
await notes.editor.setContent(", my precious note");
await notes.editor.waitForSaving();
expect(await notes.editor.getTitle()).toBe("my precious, my precious note");
await expect(notes.editor.title).toHaveValue("my precious, my precious note");
await notes.editor.setTitle("not precious");
expect(await notes.editor.getTitle()).toBe("not precious");
await expect(notes.editor.title).toHaveValue("not precious");
await notes.editor.setContent(", but...");
await notes.editor.waitForSaving();
expect(await notes.editor.getTitle()).toBe("not precious");
await expect(notes.editor.title).toHaveValue("not precious");
});
test("select all & backspace should clear all content in editor", async ({
@@ -178,7 +177,7 @@ test("select all & backspace should clear all content in editor", async ({
test("editing a note and switching immediately to another note and making an edit shouldn't overlap both notes", async ({
page
}, test) => {
test.setTimeout(45 * 1000);
test.setTimeout(3 * 60 * 1000);
const app = new AppModel(page);
await app.goto();
@@ -217,7 +216,7 @@ test("editing a note and switching immediately to another note and making an edi
test("editing a note and switching immediately to another note and editing the title shouldn't overlap both notes", async ({
page
}, test) => {
test.setTimeout(45 * 1000);
test.setTimeout(3 * 60 * 1000);
const app = new AppModel(page);
await app.goto();
@@ -333,7 +332,6 @@ test("disable autosave when note crosses MAX_AUTO_SAVEABLE_WORDS", async ({
"Auto-save is disabled for large notes. Press Ctrl + S to save."
)
).toBe(true);
await expect(notes.editor.notSavedIcon).toBeVisible();
});
test("when autosave is disabled, pressing ctrl+s should save the note", async ({
@@ -470,12 +468,12 @@ test("control + alt + right arrow should go to next note", async ({ page }) => {
await note2?.openNote(true);
await page.keyboard.press("ControlOrMeta+Alt+ArrowRight");
expect(await notes.editor.getTitle()).toBe("Note 1");
await expect(notes.editor.title).toHaveValue("Note 1");
expect(await notes.editor.getContent("text")).toBe("Note 1 content");
await page.keyboard.press("ControlOrMeta+Alt+ArrowRight");
expect(await notes.editor.getTitle()).toBe("Note 2");
await expect(notes.editor.title).toHaveValue("Note 2");
expect(await notes.editor.getContent("text")).toBe("Note 2 content");
});
@@ -498,11 +496,11 @@ test("control + alt + left arrow should go to previous note", async ({
await note2?.openNote(true);
await page.keyboard.press("ControlOrMeta+Alt+ArrowLeft");
expect(await notes.editor.getTitle()).toBe("Note 1");
await expect(notes.editor.title).toHaveValue("Note 1");
expect(await notes.editor.getContent("text")).toBe("Note 1 content");
await page.keyboard.press("ControlOrMeta+Alt+ArrowLeft");
expect(await notes.editor.getTitle()).toBe("Note 2");
await expect(notes.editor.title).toHaveValue("Note 2");
expect(await notes.editor.getContent("text")).toBe("Note 2 content");
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect, Page, test } from "@playwright/test";
import { Page } from "@playwright/test";
import { test, expect } from "./utils";
import { AppModel } from "./models/app.model";
import { NoteItemModel } from "./models/note-item.model";

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
import { authenticator } from "otplib";
import { generateSync } from "otplib";
type User = {
email: string;
@@ -62,7 +62,7 @@ export class AuthModel {
}
if (user.totpSecret) {
const token = authenticator.generate(user.totpSecret);
const token = generateSync({ secret: user.totpSecret });
await this.codeInput.fill(token);
await this.submitButton.click();

View File

@@ -24,7 +24,7 @@ import { iterateList } from "./utils";
export class EditorModel {
private readonly page: Page;
private readonly title: Locator;
readonly title: Locator;
readonly content: Locator;
private readonly tags: Locator;
private readonly tagInput: Locator;
@@ -88,8 +88,9 @@ export class EditorModel {
{ expected: title }
);
if (content !== undefined)
await this.content.locator(":scope", { hasText: content }).waitFor();
if (content !== undefined) {
await this.waitForContent(content);
}
}
async waitForUnloading() {
@@ -109,6 +110,22 @@ export class EditorModel {
await this.wordCountText.waitFor();
}
async waitForContent(text: string) {
await this.page.waitForFunction(
({ expected }) => {
const contentElement = document.querySelector(
`.active .ProseMirror`
) as HTMLElement | null;
if (contentElement)
return contentElement.innerText
.replace(/\s+/g, "")
.includes(expected.replace(/\s+/g, ""));
return false;
},
{ expected: text }
);
}
async isUnloaded() {
return (
(await this.tagInput.isDisabled()) &&
@@ -128,7 +145,7 @@ export class EditorModel {
await this.title.focus();
await this.title.selectText();
await this.title.press("ArrowRight");
await this.title.type(text, { delay });
await this.title.pressSequentially(text, { delay });
});
}
@@ -136,6 +153,7 @@ export class EditorModel {
await this.editAndWait(async () => {
await this.content.focus();
await this.content.pressSequentially(text);
await this.waitForContent(text);
});
}
@@ -143,6 +161,7 @@ export class EditorModel {
await this.editAndWait(async () => {
await this.selectAll();
await this.content.press("Backspace");
await this.waitForContent("");
});
}

View File

@@ -27,10 +27,12 @@ import {
fillColorDialog,
fillNotebookDialog,
fillPasswordDialog,
fillReminderDialog,
iterateList
} from "./utils";
import { SessionHistoryItemModel } from "./session-history-item-model";
import dayjs from "dayjs";
import { Dayjs } from "dayjs";
import { Reminder } from "@notesnook/core";
abstract class BaseProperties {
protected readonly page: Page;
@@ -233,7 +235,7 @@ export class NotePropertiesModel extends BaseProperties {
return dateCreated;
}
async editDateCreated(newDateCreated: number) {
async editDateCreated(date: Dayjs) {
await this.open();
const editIcon = this.page.locator(getTestId("edit-date-created"));
@@ -251,9 +253,8 @@ export class NotePropertiesModel extends BaseProperties {
getTestId("time-created-input")
);
const date = new Date(newDateCreated);
await dateInput.fill(dayjs(date).format("DD-MM-YYYY"));
await timeInput.fill(dayjs(date).format("hh:mm A"));
await dateInput.fill(date.format("MM-DD-YYYY"));
await timeInput.fill(date.format("hh:mm A"));
await confirmDialog(editDateCreatedDialog);
@@ -388,6 +389,12 @@ export class NoteContextMenuModel extends BaseProperties {
await confirmDialog(dialog);
}
async addReminder(reminder: Partial<Reminder>) {
await this.open();
await this.menu.clickOnItem("remind-me");
await fillReminderDialog(this.page, reminder);
}
async open() {
await this.menu.open(this.noteLocator);
}

View File

@@ -53,6 +53,7 @@ export class NotesViewModel extends BaseViewModel {
if (note.content) await this.editor.setContent(note.content);
await this.editor.waitForSaving();
if (note.content) await this.editor.waitForContent(note.content);
if (note.title) await this.waitForItem(note.title);
return await this.findNote(note);
}

View File

@@ -62,4 +62,9 @@ export class ReminderItemModel extends BaseItemModel {
await this.contextMenu.open(this.locator);
await this.contextMenu.clickOnItem("toggle");
}
async open() {
await this.contextMenu.open(this.locator);
await this.contextMenu.clickOnItem("edit");
}
}

View File

@@ -141,8 +141,6 @@ export class SettingsViewModel {
await uploadFile(this.page, restoreBackup, filename);
if (password) await fillPasswordDialog(this.page, password);
await waitForDialog(this.page, "Restoring backup");
}
async selectImageCompression(option: { value: string; label: string }) {

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { USER } from "./utils";
import { test, expect, USER } from "./utils";
test("don't show monographs list to unauthenticated user", async ({ page }) => {
const app = new AppModel(page);

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-undef */
import { test, expect } from "@playwright/test";
import { test, expect } from "./utils";
import { AppModel } from "./models/app.model";
function createRoute(key: string, header: string) {

View File

@@ -17,6 +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 { test } from "@playwright/test";
import { test } from "./utils";
test.skip("TODO: pin 3 notebooks & make sure they can be unpinned", () => {});
test.skip("TODO: pin 4 notebooks & make sure 4th pin shows an error", () => {});

View File

@@ -17,12 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { Notebook } from "./models/types";
import {
test,
expect,
getTestId,
groupByOptions,
NOTE,
NOTEBOOK,
orderByOptions,

View File

@@ -17,8 +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 { test, expect } from "@playwright/test";
import { createHistorySession, PASSWORD } from "./utils";
import { test, expect, createHistorySession, PASSWORD } from "./utils";
test.setTimeout(60 * 1000);

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect, test } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { NOTE } from "./utils";
import { expect, test, NOTE } from "./utils";
test.skip("TODO: make sure jump to group works", () => {});
test("#1002 Can't add a tag that's a substring of an existing tag", async ({

View File

@@ -17,9 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import dayjs from "dayjs";
import { AppModel } from "./models/app.model";
import {
test,
expect,
groupByOptions,
NOTE,
orderByOptions,
@@ -453,9 +455,11 @@ test("edit note creation date in properties panel", async ({ page }) => {
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
const date = new Date("2020-01-15T10:30:00Z");
await note?.properties.editDateCreated(date.valueOf());
const date = dayjs().subtract(7, "day");
await note?.properties.editDateCreated(date);
const dateCreated = await note?.properties.getDateCreated();
expect(dateCreated).toBe("15-01-2020 03:30 PM");
expect(date.format("YYYY-MM-DD HH:mm")).toBe(
dayjs(dateCreated).format("YYYY-MM-DD HH:mm")
);
});

View File

@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Reminder } from "@notesnook/core";
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { test, expect, getTestId } from "./utils";
const ONE_TIME_REMINDER: Partial<Reminder> = {
title: "Test reminder 1",
@@ -197,3 +197,26 @@ test("editing a weekly recurring reminder should not revert it to daily", async
expect(await reminder?.getRecurringMode()).toBe("Weekly");
expect(await reminder?.getDescription()).toBe("An edited reminder");
});
test("adding a reminder via note context menu should show reference in edit dialog", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({
title: "Test note",
content: "I am a note"
});
await note?.contextMenu.addReminder(ONE_TIME_REMINDER);
const reminders = await app.goToReminders();
const reminder = await reminders.findReminder({
title: ONE_TIME_REMINDER.title
});
await reminder?.open();
const noteReferences = page.locator(getTestId("reminder-note-references"));
await noteReferences.waitFor({ state: "visible" });
expect(noteReferences.getByText("Test note")).toBeVisible();
});

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { getTestId } from "./utils";
import { test, expect, getTestId } from "./utils";
test("closing search via close button should clear query", async ({ page }) => {
const app = new AppModel(page);

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { NOTE } from "./utils";
import { test, expect, NOTE } from "./utils";
test("ask for image compression during image upload when 'Image Compression' setting is 'Ask every time'", async ({
page

View File

@@ -16,9 +16,9 @@ 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 { test, Browser, expect } from "@playwright/test";
import { Browser } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { USER } from "./utils";
import { test, expect, USER } from "./utils";
// run this test file sequentially
test.describe.configure({ mode: "serial" });

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { createHistorySession, NOTE } from "./utils";
import { test, expect, createHistorySession, NOTE } from "./utils";
test("notes should open in the same tab", async ({ page }) => {
const app = new AppModel(page);

View File

@@ -17,10 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { Item } from "./models/types";
import { getTestId, NOTE, orderByOptions, sortByOptions } from "./utils";
import {
test,
expect,
getTestId,
NOTE,
orderByOptions,
sortByOptions
} from "./utils";
const TAG: Item = { title: "hello-world" };
const EDITED_TAG: Item = { title: "hello-world-2" };

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { getTestId, USER } from "./utils";
import { test, expect, USER } from "./utils";
// async function forceExpireSession() {
// await page.evaluate(() => {

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import fs from "fs";
import dotenv from "dotenv";
import path, { join } from "path";
import { Locator, Page } from "@playwright/test";
import { test as base, Locator, Page } from "@playwright/test";
import {
GroupByOptions,
Notebook,
@@ -30,6 +30,19 @@ import {
import { tmpdir } from "os";
import { AppModel } from "../models/app.model";
export type { Browser, Page } from "@playwright/test";
export { expect } from "@playwright/test";
const test = base.extend<NonNullable<unknown>>({
page: async ({ page }, use) => {
const client = await page.context().newCDPSession(page);
await client.send("Emulation.setCPUThrottlingRate", {
rate: 1
});
await use(page);
}
});
type Note = {
title: string;
content?: string;
@@ -193,6 +206,7 @@ export async function createHistorySession(page: Page, locked = false) {
}
export {
test,
USER,
NOTE,
TITLE_ONLY_NOTE,

View File

@@ -17,9 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { getTestId, NOTE, PASSWORD } from "./utils";
import { test, expect, getTestId, NOTE, PASSWORD } from "./utils";
test("locking a note should show vault unlocked status", async ({ page }) => {
const app = new AppModel(page);

View File

@@ -0,0 +1,182 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
Item,
Group,
Subgroup,
TreeNode,
moveItem
} from "../src/dialogs/settings/components/customize-toolbar";
import { describe, it, expect } from "vitest";
describe("moveItem function", () => {
it("should correctly set depth when moving item from subgroup to main group", () => {
const group: Group = {
type: "group",
id: "group1",
title: "Group 1",
depth: 0
};
const subgroup: Subgroup = {
type: "group",
id: "subgroup1",
title: "Subgroup 1",
depth: 1
};
const item: Item = {
type: "item",
id: "item1",
title: "Item 1",
depth: 2, // currently in subgroup
toolId: "bold",
icon: "bold"
};
const items: TreeNode[] = [group, subgroup, item];
// move item from subgroup to main group
const result = moveItem(items, "item1", "group1");
// find the moved item
const movedItem = result.find((i) => i.id === "item1") as Item;
// the item should now have depth 1 (group depth + 1)
expect(movedItem.depth).toBe(1);
});
it("should correctly set depth when moving item from main group to subgroup", () => {
const group: Group = {
type: "group",
id: "group1",
title: "Group 1",
depth: 0
};
const item: Item = {
type: "item",
id: "item1",
title: "Item 1",
depth: 1, // currently in main group
toolId: "bold",
icon: "bold"
};
const subgroup: Subgroup = {
type: "group",
id: "subgroup1",
title: "Subgroup 1",
depth: 1
};
const items: TreeNode[] = [group, item, subgroup];
// move item from main group to subgroup
const result = moveItem(items, "item1", "subgroup1");
// find the moved item
const movedItem = result.find((i) => i.id === "item1") as Item;
// the item should now have depth 2 (subgroup depth + 1)
expect(movedItem.depth).toBe(2);
});
it("should correctly set depth when moving item to another item at same level", () => {
const group: Group = {
type: "group",
id: "group1",
title: "Group 1",
depth: 0
};
const item1: Item = {
type: "item",
id: "item1",
title: "Item 1",
depth: 1,
toolId: "bold",
icon: "bold"
};
const item2: Item = {
type: "item",
id: "item2",
title: "Item 2",
depth: 1,
toolId: "italic",
icon: "italic"
};
const items: TreeNode[] = [group, item1, item2];
// move item1 to item2's position
const result = moveItem(items, "item1", "item2");
// find the moved item
const movedItem = result.find((i) => i.id === "item1") as Item;
// the item should maintain the same depth as the target item
expect(movedItem.depth).toBe(1);
});
it("should correctly set depth when moving item from subgroup to another subgroup", () => {
const group: Group = {
type: "group",
id: "group1",
title: "Group 1",
depth: 0
};
const subgroup1: Subgroup = {
type: "group",
id: "subgroup1",
title: "Subgroup 1",
depth: 1
};
const item1: Item = {
type: "item",
id: "item1",
title: "Item 1",
depth: 2,
toolId: "bold",
icon: "bold"
};
const subgroup2: Subgroup = {
type: "group",
id: "subgroup2",
title: "Subgroup 2",
depth: 1
};
const items: TreeNode[] = [group, subgroup1, item1, subgroup2];
// move item from subgroup1 to subgroup2
const result = moveItem(items, "item1", "subgroup2");
// find the moved item
const movedItem = result.find((i) => i.id === "item1") as Item;
// the item should now have depth 2 (subgroup depth + 1)
expect(movedItem.depth).toBe(2);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.3.8",
"version": "3.3.10",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -86,7 +86,7 @@
"devDependencies": {
"@babel/core": "7.22.5",
"@cloudflare/workers-types": "^4.20250224.0",
"@playwright/test": "1.57.0",
"@playwright/test": "^1.58.2",
"@swc/plugin-react-remove-properties": "^6.0.2",
"@trpc/server": "10.45.2",
"@types/babel__core": "^7.20.1",
@@ -113,7 +113,7 @@
"happy-dom": "16.0.1",
"ip": "^2.0.1",
"lorem-ipsum": "^2.0.4",
"otplib": "^12.0.1",
"otplib": "^13.3.0",
"rollup-plugin-visualizer": "^5.13.1",
"vite": "5.4.11",
"vite-plugin-env-compatible": "^2.0.1",

View File

@@ -31,7 +31,10 @@ const config: PlaywrightTestConfig = {
// Look for test files in thcleare "tests" directory, relative to this configuration file
testDir: "__e2e__",
timeout: IS_CI ? 60 * 1000 : 30 * 1000,
timeout: IS_CI ? 60 * 1000 : 60 * 1000,
expect: {
timeout: 20 * 1000
},
workers: IS_CI ? 2 : 2,
reporter: "list",
retries: IS_CI ? 1 : 0,

View File

@@ -31,7 +31,7 @@ import {
} from "./common";
import { AppEventManager, AppEvents } from "./common/app-events";
import { db } from "./common/db";
import { EV, EVENTS } from "@notesnook/core";
import { EVENTS } from "@notesnook/core";
import { registerKeyMap } from "./common/key-map";
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
import { hashNavigate } from "./navigation";
@@ -64,12 +64,12 @@ export default function AppEffects() {
await updateLastSynced();
await initUser();
// await resetNotices();
setIsVaultCreated(await db.vault.exists());
await FeatureDialog.show({ featureName: "highlights" });
await scheduleBackups();
await scheduleFullBackups();
await scheduleExpiredNotesDeletion();
if (useSettingStore.getState().isFullOfflineMode)
// NOTE: we deliberately don't await here because we don't want to pause execution.
db.attachments.cacheAttachments().catch(logger.error);
@@ -113,7 +113,7 @@ export default function AppEffects() {
}
}
const fileDownloadEvents = EV.subscribeMulti(
const fileDownloadEvents = db.eventManager.subscribeMulti(
[EVENTS.fileDownloaded, EVENTS.fileDownload],
({ total, current }: { total: number; current: number }) => {
handleDownloadUploadProgress("download", total, current);
@@ -121,7 +121,7 @@ export default function AppEffects() {
null
);
const fileUploadEvents = EV.subscribeMulti(
const fileUploadEvents = db.eventManager.subscribeMulti(
[EVENTS.fileUploaded, EVENTS.fileUpload],
({ total, current }: { total: number; current: number }) => {
handleDownloadUploadProgress("upload", total, current);

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