Compare commits

...

131 Commits

Author SHA1 Message Date
Ammar Ahmed
503c1a5c8d mobile: fix app gets locked when requesting notification permission for file upload progress 2026-03-24 13:17:01 +05:00
Ammar Ahmed
6c6ad9a766 Merge pull request #9516 from streetwriters/fix/9496
Fix iOS app remains stuck when restoring backup from list
2026-03-24 09:59:02 +05:00
Ammar Ahmed
fc8807f74d Merge pull request #9519 from streetwriters/fix/GHSA-f27j-fqc6-v7pm
mobile: escape title when creating web-clip for shared metadata
2026-03-24 09:57:41 +05:00
Ammar Ahmed
edec48f9d4 Merge pull request #9523 from streetwriters/fix/mobile-merge-conflicts
mobile: fix merge conflicts for locked notes
2026-03-24 09:56:58 +05:00
Ammar Ahmed
ce98551057 Merge pull request #9532 from streetwriters/fix/note-open-glitch
mobile: fix glitch when opening note, flickery behaviour
2026-03-24 09:56:10 +05:00
Ammar Ahmed
74a2cc5b19 Merge pull request #9550 from streetwriters/fix/attachment-nav-flow
mobile: fix linked note not opening from attachment properties
2026-03-24 09:54:43 +05:00
01zulfi
9628a940e9 core: normalize special separators in fuzzy search (#9541)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-24 09:50:27 +05:00
01zulfi
d48643cbb9 web: fix editor title temporary flicker on tab switch (#9548)
* also remove debounce on editor's resize observer

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

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

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

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

* core: remove redundant `textContainsTokens` check

* core: fix wrong edge case handling

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

---------

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

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

* editor: render webclips via bloburl with strict sandboxing

---------

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

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

* web: use setting store listener instead of directly using autoUpdates value
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-13 09:16:57 +05:00
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
Ammar Ahmed
f1fc166713 mobile: fix top bar item page width and count 2026-03-11 12:00:36 +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
8b17495f0e mobile: fix vault with biometrics enabled does not lock note 2026-02-19 13:16:09 +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
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
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
192 changed files with 5367 additions and 2158 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.9-beta.1",
"version": "3.3.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.3.9-beta.1",
"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.9-beta.1",
"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

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

View File

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

View File

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

@@ -33,6 +33,8 @@ import {
} from "./utils";
import Upload from "@ammarahmed/react-native-upload";
import { CloudUploader } from "react-native-nitro-cloud-uploader";
import { useUserStore } from "../../stores/use-user-store";
import { sleep } from "../../utils/time";
// Upload constants
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
@@ -212,6 +214,9 @@ export async function uploadFile(
);
if (Platform.OS === "android") {
useUserStore.setState({
disableAppLockRequests: true
});
const status = await PermissionsAndroid.request(
"android.permission.POST_NOTIFICATIONS"
);
@@ -221,6 +226,10 @@ export async function uploadFile(
type: "info"
});
}
await sleep(500);
useUserStore.setState({
disableAppLockRequests: false
});
}
let uploaded = false;

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

@@ -59,6 +59,7 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import Navigation from "../../services/navigation";
const Actions = ({
attachment,
@@ -304,9 +305,9 @@ const Actions = ({
<Pressable
onPress={async () => {
eSendEvent(eCloseSheet, contextId);
await sleep(150);
close?.();
eSendEvent(eCloseAttachmentDialog);
await sleep(300);
Navigation.navigate("FluidPanelsView");
openNote(item, (item as any).type === "trash");
}}
style={{

View File

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

@@ -28,6 +28,7 @@ import { DDS } from "../../../services/device-detection";
import {
ToastManager,
Vault,
VaultRequestType,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
@@ -55,151 +56,119 @@ import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import { Note, NoteContent, VAULT_ERRORS } from "@notesnook/core";
import {
isEncryptedContent,
Note,
NoteContent,
VAULT_ERRORS
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
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;
if (data.item) {
const locked = await db.vaults.itemExists(data.item);
noteLockedRef.current = locked;
if (!locked) {
const content = await db.content.findByNoteId(data.item!.id);
if (content && isEncryptedContent(content)) {
noteLockedRef.current = true;
}
}
}
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;
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 +178,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 +216,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 +229,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 +244,7 @@ export const VaultDialog: React.FC = () => {
});
}
eSendEvent("vaultUpdated");
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
setTimeout(() => {
close();
}, 100);
@@ -298,10 +258,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 +279,7 @@ export const VaultDialog: React.FC = () => {
true
);
});
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
close();
eSendEvent("vaultUpdated");
} catch (e) {
@@ -329,7 +289,7 @@ export const VaultDialog: React.FC = () => {
context: "local"
});
}
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
}, [close]);
const lockNote = useCallback(async () => {
@@ -341,9 +301,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 +311,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 +381,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 +405,41 @@ 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;
const unlock = onUnlockRef.current;
close();
await sleep(300);
state.onUnlock(note, password);
await sleep(500);
unlock(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 +457,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 +485,7 @@ export const VaultDialog: React.FC = () => {
type: "error",
context: "local"
});
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
}
},
[close]
@@ -532,13 +494,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 +516,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 +540,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 +552,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 +581,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 +611,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 +622,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 +699,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 +735,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 +749,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 +765,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 +812,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 +829,7 @@ export const VaultDialog: React.FC = () => {
/>
)}
{changePassword ? (
{isChangePassword ? (
<>
<Seperator half />
<Input
@@ -877,7 +852,7 @@ export const VaultDialog: React.FC = () => {
</>
) : null}
{!novault ? (
{isCreateVault ? (
<View>
<Input
fwdRef={passInputRef}
@@ -912,12 +887,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 +900,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 +925,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 +936,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

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

View File

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

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",
@@ -327,8 +326,9 @@ export const Items = ({
const getTopBarItemChunksOfFour = () => {
const chunks = [];
for (let i = 0; i < topBarItems.length; i += 5) {
chunks.push(topBarItems.slice(i, i + 5));
const itemCount = shouldShrink ? 4 : 5;
for (let i = 0; i < topBarItems.length; i += itemCount) {
chunks.push(topBarItems.slice(i, i + itemCount));
}
return chunks;
};
@@ -375,7 +375,8 @@ export const Items = ({
style={{
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP,
gap: 5
gap: 5,
width: width
}}
>
{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

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

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

View File

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

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

@@ -21,6 +21,7 @@ import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
import {
Color,
createInternalLink,
isEncryptedContent,
Item,
ItemReference,
Note,
@@ -54,7 +55,8 @@ import {
eSubscribeEvent,
openVault,
presentSheet,
ToastManager
ToastManager,
VaultRequestType
} from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
@@ -220,7 +222,15 @@ export const useActions = ({
useEffect(() => {
if (item.type === "note") {
db.vaults.itemExists(item).then((locked) => setLocked(locked));
db.vaults.itemExists(item).then(async (locked) => {
setLocked(locked);
if (!locked) {
const content = await db.content.findByNoteId(item.id);
if (content && isEncryptedContent(content)) {
setLocked(true);
}
}
});
}
}, [item]);
@@ -408,12 +418,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 +877,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 +905,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 +926,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 +958,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

@@ -563,6 +563,7 @@ export const useAppEvents = () => {
initialUrl: string;
backupDidWait: boolean;
isConnectingSSE: boolean;
attachmentsCachedOfflineMode: boolean;
}>
>({});
@@ -657,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 */
});

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

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

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

@@ -487,6 +487,17 @@ export const useEditor = (
};
} else if (note.contentId) {
const rawContent = await db.content?.get(note.contentId);
if (rawContent && isEncryptedContent(rawContent)) {
useTabStore.getState().updateTab(useTabStore.getState().currentTab, {
session: {
noteLocked: true,
locked: true
}
});
return;
}
if (rawContent && !isDeleted(rawContent) && !rawContent.locked) {
currentContents.current[note.id] = {
data: rawContent.data,

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

@@ -570,13 +570,17 @@ const BackupItem = ({
paragraph: strings.restoreBackupConfirm(),
positiveText: strings.restore(),
negativeText: strings.cancel(),
context: "global",
positivePress: async () => {
restoreBackup({
uri:
Platform.OS === "android"
? (item as ScopedStorage.FileType).uri
: (item as ReactNativeBlobUtilStat).path
});
setTimeout(() => {
restoreBackup({
uri:
Platform.OS === "android"
? (item as ScopedStorage.FileType).uri
: (item as ReactNativeBlobUtilStat).path
});
}, 500);
return true;
}
});
}}

View File

@@ -25,7 +25,6 @@ import {
} from "@react-navigation/native";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, TextInput, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { FeatureResult, useIsFeatureAvailable } from "@notesnook/common";
//@ts-ignore
import ToggleSwitch from "toggle-switch-react-native";
@@ -218,14 +217,15 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
}}
>
{!!item.icon && (
<Icon
<AppIcon
color={
item.type === "danger"
? colors.error.icon
: colors.secondary.icon
}
iconFamily={item.iconFamily}
name={item.icon}
size={30}
size={item.iconSize || 30}
/>
)}
</View>

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";
@@ -91,6 +92,7 @@ export const settingsGroups: SettingSection[] = [
{
id: "delete-data",
name: strings.deleteData(),
icon: "delete",
description: strings.deleteAccountDesc(),
modifer: () => {
presentDialog({
@@ -967,8 +969,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()
});
}
},
@@ -980,9 +983,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()
})
},
{
@@ -993,9 +996,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"
});
}
},
@@ -1007,9 +1011,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"
});
}
},
@@ -1027,13 +1032,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()
});
}
}
@@ -1237,7 +1244,7 @@ export const settingsGroups: SettingSection[] = [
}
},
{
id: "backup-now",
id: "backup-now-with-attachments",
name: strings.backupNowWithAttachments(),
description: strings.backupNowWithAttachmentsDesc(),
hidden: () => !useUserStore.getState().user,
@@ -1539,6 +1546,9 @@ export const settingsGroups: SettingSection[] = [
id: "join-telegram",
name: strings.joinTelegram(),
description: strings.joinTelegramDesc(),
icon: "sc-telegram",
iconFamily: "evilicons",
iconSize: 35,
modifer: () => {
Linking.openURL("https://t.me/notesnook").catch(() => {
/* empty */

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { TextInput } from "react-native";
import { Settings } from "../../stores/use-setting-store";
import { FeatureId } from "@notesnook/common";
import { IconProps } from "../../components/ui/AppIcon";
export type SettingSection = {
id: string;
@@ -34,6 +35,8 @@ export type SettingSection = {
name?: string | ((current?: unknown) => string);
description?: string | ((current: unknown) => string);
icon?: string;
iconFamily?: IconProps["iconFamily"];
iconSize?: number;
property?: keyof Settings;
sections?: SettingSection[];
component?: string;

View File

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

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

@@ -71,6 +71,7 @@ import { initDatabase, useShareStore } from "./store";
import { isTablet } from "react-native-device-info";
import { NotesnookModule } from "../utils/notesnook-module";
import { DefaultAppStyles } from "../utils/styles";
import { encodeHTML5 } from "entities";
const getLinkPreview = (url: string) => {
return getPreviewData(url, 5000);
@@ -81,7 +82,7 @@ async function sanitizeHtml(site: string, title?: string) {
return (
html +
"<hr/>" +
`<p>Clipped from <a href="${site}">${title || site}</a></p>
`<p>Clipped from <a href="${site}">${encodeHTML5(title || site)}</a></p>
<p>Date clipped ${getFormattedDate(Date.now())}</p>`
);
} catch (e) {
@@ -96,7 +97,7 @@ function makeHtmlFromUrl(url: string) {
function makeHtmlFromPlainText(text: string) {
if (!text) return "";
return `<p>${text
return `<p>${encodeHTML5(text)
.replace(/[\n]+/g, "\n")
.replace(/(?:\r\n|\r|\n)/g, "</p><p>")}</p>`;
}

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

@@ -46,7 +46,7 @@ export interface UserStore {
appLocked: boolean;
lockApp: (verified: boolean) => void;
disableAppLockRequests: boolean;
setDisableAppLockRequests: (shouldBlockVerifyUser: boolean) => void;
setDisableAppLockRequests: (disableAppLockRequests: boolean) => void;
profile?: Partial<Profile>;
}

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

Binary file not shown.

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

@@ -34,6 +34,8 @@
65AA857E25E6DDEC00772A01 /* NotesWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AA857D25E6DDEC00772A01 /* NotesWidget.swift */; };
65AA858025E6DDEE00772A01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65AA857F25E6DDEE00772A01 /* Assets.xcassets */; };
65AA858425E6DDEE00772A01 /* NotesWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 65AA857725E6DDEC00772A01 /* NotesWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
65AD1F4A2F6289110033176F /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 65AD1F492F6289110033176F /* EvilIcons.ttf */; };
65AD1F4B2F6289110033176F /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 65AD1F492F6289110033176F /* EvilIcons.ttf */; };
65B5014425A672B200E2D264 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B5014325A672B200E2D264 /* ShareViewController.m */; };
65B5014725A672B200E2D264 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65B5014525A672B200E2D264 /* MainInterface.storyboard */; };
65B5014B25A672B200E2D264 /* Make Note.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 65B5014025A672B200E2D264 /* Make Note.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -129,6 +131,7 @@
65AA857D25E6DDEC00772A01 /* NotesWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesWidget.swift; sourceTree = "<group>"; };
65AA857F25E6DDEE00772A01 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
65AA858125E6DDEE00772A01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
65AD1F492F6289110033176F /* EvilIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = EvilIcons.ttf; path = ../fonts/EvilIcons.ttf; sourceTree = SOURCE_ROOT; };
65B5014025A672B200E2D264 /* Make Note.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Make Note.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
65B5014325A672B200E2D264 /* ShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = "<group>"; };
65B5014625A672B200E2D264 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
@@ -291,6 +294,7 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
65AD1F492F6289110033176F /* EvilIcons.ttf */,
653F4E042EA3887C009A42A2 /* Inter-Bold.ttf */,
653F4E052EA3887C009A42A2 /* Inter-Italic.ttf */,
653F4E062EA3887C009A42A2 /* Inter-Regular.ttf */,
@@ -536,6 +540,7 @@
buildActionMask = 2147483647;
files = (
659201D82EA78265006B16DB /* Inter-Bold.ttf in Resources */,
65AD1F4B2F6289110033176F /* EvilIcons.ttf in Resources */,
659201D92EA78265006B16DB /* Inter-Italic.ttf in Resources */,
659201DA2EA78265006B16DB /* Inter-Regular.ttf in Resources */,
659201DB2EA78265006B16DB /* Inter-SemiBold.ttf in Resources */,
@@ -581,6 +586,7 @@
659201DE2EA7826D006B16DB /* Inter-Regular.ttf in Resources */,
659201DF2EA7826D006B16DB /* Inter-SemiBold.ttf in Resources */,
65C400DF2A80B6B600AA3DF5 /* MaterialCommunityIcons.ttf in Resources */,
65AD1F4A2F6289110033176F /* EvilIcons.ttf in Resources */,
65C149872A61151B005C40F1 /* extension.bundle in Resources */,
65B5014725A672B200E2D264 /* MainInterface.storyboard in Resources */,
);
@@ -1029,7 +1035,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2173;
CURRENT_PROJECT_VERSION = 2176;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1104,7 +1110,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.15;
MARKETING_VERSION = 3.3.16;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1135,7 +1141,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2173;
CURRENT_PROJECT_VERSION = 2176;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1210,7 +1216,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.15;
MARKETING_VERSION = 3.3.16;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1367,7 +1373,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2173;
CURRENT_PROJECT_VERSION = 2176;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1379,7 +1385,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.15;
MARKETING_VERSION = 3.3.16;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1410,7 +1416,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2173;
CURRENT_PROJECT_VERSION = 2176;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1423,7 +1429,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.15;
MARKETING_VERSION = 3.3.16;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1453,7 +1459,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2173;
CURRENT_PROJECT_VERSION = 2176;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1534,7 +1540,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.15;
MARKETING_VERSION = 3.3.16;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1565,7 +1571,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2173;
CURRENT_PROJECT_VERSION = 2176;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1647,7 +1653,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.15;
MARKETING_VERSION = 3.3.16;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1724,10 +1730,7 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -1792,10 +1795,7 @@
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;

View File

@@ -97,6 +97,7 @@
<string>Inter-Regular.ttf</string>
<string>Inter-Bold.ttf</string>
<string>MaterialCommunityIcons.ttf</string>
<string>EvilIcons.ttf</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>

View File

@@ -1,22 +1,22 @@
{
"name": "@notesnook/mobile",
"version": "3.3.13",
"version": "3.3.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.13",
"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.15-beta.0",
"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

@@ -31,7 +31,7 @@ import {
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 {
@@ -235,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"));
@@ -253,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);

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

@@ -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,9 +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 { getTestId } from "./utils";
import { test, expect, getTestId } from "./utils";
const ONE_TIME_REMINDER: Partial<Reminder> = {
title: "Test reminder 1",

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

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.9-beta.1",
"version": "3.3.10",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -52,6 +52,7 @@
"dayjs": "1.11.13",
"diffblazer": "^1.0.1",
"electron-trpc": "0.7.1",
"entities": "^7.0.1",
"event-source-polyfill": "1.0.31",
"fflate": "^0.8.0",
"file-saver": "^2.0.5",
@@ -86,7 +87,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 +114,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

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

View File

@@ -24,16 +24,12 @@ import { createDialect } from "./sqlite";
import { isFeatureSupported } from "../utils/feature-check";
import { generatePassword } from "../utils/password-generator";
import { deriveKey, useKeyStore } from "../interfaces/key-store";
import {
hosts,
logManager,
SubscriptionPlan,
SubscriptionStatus
} from "@notesnook/core";
import { hosts, SubscriptionPlan, SubscriptionStatus } from "@notesnook/core";
import Config from "../utils/config";
import { FileStorage } from "../interfaces/fs";
function getHostUrl(hostUrl: keyof typeof hosts, defaultUrl: string) {
if (IS_TESTING) return defaultUrl;
const envValue = import.meta.env[`NN_${hostUrl}`];
return envValue || defaultUrl;
}
@@ -130,13 +126,6 @@ async function initializeDatabase(persistence: DatabasePersistence) {
await db.init();
performance.mark("end:initdb");
window.addEventListener("beforeunload", async () => {
if (IS_DESKTOP_APP) {
await db.sql().destroy();
await logManager?.close();
}
});
if (db.migrations?.required()) {
await import("../dialogs/migration-dialog").then(({ MigrationDialog }) =>
MigrationDialog.show({})

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