Compare commits

...

205 Commits

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

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

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

---------

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

* web: handle native menu case

---------

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

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

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

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

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

* core: remove redundant `textContainsTokens` check

* core: fix wrong edge case handling

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

---------

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

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

* editor: render webclips via bloburl with strict sandboxing

---------

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

* web: use entities package for escaping note title in diff viewer
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-03-16 13:07:56 +05:00
Ammar Ahmed
71d9738980 mobile: fix glitch when opening note, flickery behaviour 2026-03-16 09:07:18 +05:00
Ammar Ahmed
80328af888 mobile: fix merge conflicts for locked notes 2026-03-13 12:48:29 +05:00
Ammar Ahmed
5e83da5575 mobile: escape title when creating web-clip for shared metadata
fixes https://github.com/streetwriters/notesnook/security/advisories/GHSA-f27j-fqc6-v7pm
2026-03-13 10:54:07 +05:00
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
b20ad48b31 mobile: release 3.3.15-beta.0 2026-02-20 08:04:53 +05:00
Ammar Ahmed
8b17495f0e mobile: fix vault with biometrics enabled does not lock note 2026-02-19 13:16:09 +05:00
Abdullah Atta
e113e1b59c web: bump version to 3.3.9-beta.1 2026-02-19 11:38:30 +05:00
Abdullah Atta
6bd4924c0b web: add logging about persistence type 2026-02-19 11:37:59 +05:00
Abdullah Atta
a2939aa183 web: use in memory db only for account recovery routes 2026-02-19 11:34:17 +05:00
Abdullah Atta
effa726b65 web: bump version to 3.3.9-beta.0 2026-02-19 10:43:37 +05:00
Abdullah Atta
20d9bf1f23 core: ensure user is fetched after sign up 2026-02-19 10:42:55 +05:00
Abdullah Atta
09e5d014df web: fix page state persistence on account recovery pages 2026-02-19 10:42:55 +05:00
01zulfi
add1b8a68a editor: support audio attachment playback
Co-authored-by: Hamish <133548095+Hamster45105@users.noreply.github.com>
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-18 11:24:45 +05:00
Ammar Ahmed
fe143f66b7 core: remove log 2026-02-16 15:04:05 +05:00
Ammar Ahmed
75c8ca29ef web: publish vaultLocked event from db eventManager 2026-02-16 14:42:43 +05:00
Ammar Ahmed
c52779df23 Merge branch 'master' into beta 2026-02-16 14:40:45 +05:00
Ammar Ahmed
b7b77646e6 Merge pull request #9357 from streetwriters/mobile/release-3.3.14
mobile: release 3.3.14
2026-02-16 14:36:42 +05:00
01zulfi
1157c6b7cd Merge pull request #9351 from streetwriters/web/server-url-from-environment
web: prioritize environment variables for server urls configuration
2026-02-16 14:28:08 +05:00
Abdullah Atta
ab824d7f66 web: fix server urls 2026-02-16 14:27:50 +05:00
Abdullah Atta
702e28a6e6 Merge pull request #9323 from streetwriters/fix/password-changing
Fix account password changing
2026-02-16 14:15:39 +05:00
Ammar Ahmed
0e0ab58959 mobile: release 3.3.14 2026-02-16 13:39:41 +05:00
Ammar Ahmed
e5cdc9ea3d Merge pull request #9355 from streetwriters/fix/use-db-ev-manager
core: use db event manager
2026-02-16 13:34:31 +05:00
Ammar Ahmed
d3c9744e65 core: use db event manager 2026-02-16 13:11:45 +05:00
Ammar Ahmed
6b02144b30 Merge pull request #9354 from streetwriters/fix-loading-note-bug
mobile: loading note gets stuck
2026-02-16 13:07:27 +05:00
Abdullah Atta
91cbc5ddf0 core: simplify user signup 2026-02-16 13:06:58 +05:00
Ammar Ahmed
93aa1ca5aa Merge pull request #9352 from streetwriters/fix-attachment-upload-error
mobile: fix attachment upload error Failed to delete file
2026-02-16 12:59:15 +05:00
Ammar Ahmed
6ae13ebb2f mobile: fix attachment upload error Failed to delete file 2026-02-16 11:43:55 +05:00
Ammar Ahmed
73cdbf64c7 mobile: fix note loading is slow in editor after login/signup 2026-02-13 14:31:13 +05:00
01zulfi
0aeea965c0 core: add support for bulk delete files
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-13 13:01:56 +05:00
Abdullah Atta
a63b15637f core: fix types 2026-02-13 11:33:09 +05:00
Ammar Ahmed
5153b43bc6 mobile: enable password change and forgot password 2026-02-13 11:27:58 +05:00
Ammar Ahmed
9ea7d09722 core: fix imports 2026-02-13 09:45:08 +05:00
01zulfi
b7b474b245 core: fix archived notes not showing in attachment's linked notes (#9001)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-12 14:19:59 +05:00
ash
e2184a8bdf web: fix text not selectable from entire height of editor (#8972)
Signed-off-by: Ash Holland <x-hv2j5m@keysmash.solutions>
2026-02-12 14:05:49 +05:00
01zulfi
9ea84ca9ae editor: reduce horizontal rule height for large screens (#9040)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-12 14:02:01 +05:00
01zulfi
50136e3689 editor: use last set code language as default (#7086)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-02-12 13:49:52 +05:00
01zulfi
e87f5e5f89 editor: escape html in twitter embed src (#9326)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-10 12:33:56 +05:00
01zulfi
d44792d132 web: align check icon inside note's color in properties panel (#9308)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-02-09 12:33:28 +05:00
01zulfi
50b01e0b1b web: fix attachment not marked as uploaded when app locked & unlocked (#9306)
When RouteWrapper was unmounted, it would unsub all events including the ones subbed during db initialization (db.init()). Since db isn't initialized again when app is locked & unlocked, those events weren't subbed again. One of these event subscription (fileUploaded event) is responsible for marking an attachment as uploaded. There is no need to unsubscribe all events on RouteWrapper unmount.

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

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

* web: add tests for customize-toolbar.tsx

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

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

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

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

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

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

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

---------

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

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

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

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

* web: fix image previews for regular attachments

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

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

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-02-02 10:26:03 +05:00
Abdullah Atta
75103dc706 Merge pull request #9237 from streetwriters/web/dont-render-properties-on-locked-note
web: don't render properties panel for locked sessions
2026-02-02 09:56:20 +05:00
Abdullah Atta
cf63c2fdc5 Merge branch 'master' into beta 2026-01-28 12:44:31 +05:00
Ammar Ahmed
f15b4cf634 mobile: show note references in reminder page 2026-01-20 10:17:57 +05:00
01zulfi
6430611e3e web: reminder dialogs improvements
* add note references (if any) in reminder edit dialog
* wrap snooze option buttons in reminder preview dialog

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-20 09:29:52 +05:00
01zulfi
afe57e41ce web: add confirmation dialog for delete data option
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-19 16:18:29 +05:00
Ammar Ahmed
86f3f90143 mobile: delete all data option in settings 2026-01-19 14:24:21 +05:00
01zulfi
d3ea51cc01 web: add delete data option for not logged in user
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-19 12:58:48 +05:00
01zulfi
94dbec4486 web: don't render properties panel for locked sessions
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-01-17 14:04:38 +05:00
282 changed files with 9172 additions and 4829 deletions

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

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

View File

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

View File

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

View File

@@ -252,6 +252,7 @@ export const osIntegrationRouter = t.router({
if (menuItem) menu.append(menuItem);
}
if (menu.items.length > 0) menu.popup();
menu.on("menu-will-close", () => emit.next([]));
return () => {
menu.removeAllListeners();
menu.closePopup();

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 3091
versionCode 3098
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

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

View File

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

View File

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

@@ -205,8 +205,9 @@ export async function getDatabaseKey(appLockPassword?: string) {
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials =
await Keychain.getInternetCredentials("notesnook");
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(

View File

@@ -72,7 +72,7 @@ export async function setupDatabase(password?: string) {
({
compress: Gzip.deflate,
decompress: Gzip.inflate
}) as ICompressor,
} as ICompressor),
batchSize: 50,
sqliteOptions: {
dialect: (name) => ({

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

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

@@ -239,8 +239,8 @@ const AppLocked = () => {
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,

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

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

View File

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

View File

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

View File

@@ -25,7 +25,11 @@ import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { DDS } from "../../services/device-detection";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -110,7 +114,6 @@ export const Login = ({
return (
<>
<AuthHeader />
<ForgotPassword />
<Dialog context="two_factor_verify" />
<KeyboardAwareScrollView
style={{
@@ -192,8 +195,8 @@ export const Login = ({
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
@@ -257,13 +260,10 @@ export const Login = ({
paddingHorizontal: 0
}}
onPress={() => {
ToastManager.show({
type: "info",
message:
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
if (loading || !email.current) return;
presentSheet({
component: <ForgotPassword userEmail={email.current} />
});
// if (loading || !email.current) return;
// SheetManager.show("forgotpassword_sheet");
}}
textStyle={{
textDecorationLine: "underline"

View File

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

View File

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

View File

@@ -136,8 +136,8 @@ const BaseDialog = ({
backgroundColor: background
? background
: transparent
? "transparent"
: "rgba(0,0,0,0.3)"
? "transparent"
: "rgba(0,0,0,0.3)"
}}
>
<KeyboardAvoidingView
@@ -155,8 +155,8 @@ const BaseDialog = ({
justifyContent: centered
? "center"
: bottom
? "flex-end"
: "flex-start"
? "flex-end"
: "flex-start"
}
]}
>

View File

@@ -201,14 +201,12 @@ export const AppLockPassword = () => {
accountPass
? strings.enterAccountPassword()
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin"
? strings.pin()
: strings.password()
}`
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin" ? strings.pin() : strings.password()
}`
}
/>
@@ -385,8 +383,8 @@ export const AppLockPassword = () => {
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
? strings.change()
: strings.save()
}
negativeTitle={strings.cancel()}
positiveType="transparent"

View File

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

View File

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

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

View File

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

@@ -77,10 +77,10 @@ export default function List(props: ListProps) {
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;
? "favorites"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;
const groupOptions = useGroupOptions(groupType);

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

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

View File

@@ -33,7 +33,6 @@ import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { Dialog } from "../dialog";
const TOP_BAR_ITEMS: ActionId[] = [
"pin",
@@ -174,15 +173,15 @@ export const Items = ({
DDS.isTab
? AppFontSize.xxl
: shouldShrink
? AppFontSize.xxl
: AppFontSize.lg
? AppFontSize.xxl
: AppFontSize.lg
}
color={
item.checked
? item.activeColor || colors.primary.accent
: item.id.match(/(delete|trash)/g)
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
</Pressable>
@@ -213,8 +212,8 @@ export const Items = ({
text: item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.paragraph
: colors.primary.paragraph
? colors.error.paragraph
: colors.primary.paragraph
}}
testID={"icon-" + item.id}
onPress={item.onPress}
@@ -278,8 +277,8 @@ export const Items = ({
item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
@@ -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

@@ -356,7 +356,7 @@ const ProductItem = (props: {
(product as RNIap.Subscription)?.productId
) ||
props.pricingPlans.user?.subscription?.productId ===
(product as Plan).id);
(product as Plan)?.id);
useEffect(() => {
props.pricingPlans

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

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

View File

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

View File

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

View File

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

View File

@@ -136,8 +136,8 @@ const SimpleTabView = ({
{loadedKeysRef.current.has(route.key)
? getSceneForRoute(route)
: navigationState.index === routeIndex
? getSceneForRoute(route)
: null}
? getSceneForRoute(route)
: null}
</View>
))}
</View>
@@ -435,8 +435,9 @@ const TabBar = (props: SimpleTabBarProps) => {
color={colors.primary.icon}
onPress={async () => {
if (props.navigationState.index === 1) {
const notebooksFeature =
await isFeatureAvailable("notebooks");
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
if (!notebooksFeature.isAllowed) {
PaywallSheet.present(notebooksFeature);
return;

View File

@@ -151,8 +151,8 @@ export function MenuItem({
item.icon === "crown"
? colors.static.yellow
: isFocused
? colors.selected.icon
: colors.secondary.icon
? colors.selected.icon
: colors.secondary.icon
}
size={AppFontSize.md}
/>

View File

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

@@ -158,22 +158,22 @@ export const Toast = ({ context = "global" }) => {
toastOptions.icon
? toastOptions.icon
: toastOptions.type === "success"
? "check"
: toastOptions.type === "info"
? "information"
: "close"
? "check"
: toastOptions.type === "info"
? "information"
: "close"
}
size={isFullToastMessage ? AppFontSize.xxxl : AppFontSize.xl}
color={
toastOptions?.icon
? toastOptions?.icon
: toastOptions.type === "error"
? colors.error.icon
: toastOptions.type === "info"
? isDark
? colors.static.white
: colors.static.black
: colors.success.icon
? colors.error.icon
: toastOptions.type === "info"
? isDark
? colors.static.white
: colors.static.black
: colors.success.icon
}
/>

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

@@ -121,8 +121,8 @@ const Input = ({
const color = error
? colors.error.border
: focus
? customColor || colors.selected.border
: colors.primary.border;
? customColor || colors.selected.border
: colors.primary.border;
const validate = async (value: string) => {
if (!validationType) return;

View File

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

View File

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

View File

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

View File

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

@@ -94,6 +94,7 @@ import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors";
import { BETA } from "../utils/constants";
import {
eAfterSync,
eCloseSheet,
eEditorReset,
eLoginSessionExpired,
@@ -332,6 +333,7 @@ const onLogout = async (reason: string) => {
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
eSendEvent(eAfterSync);
};
async function checkForShareExtensionLaunchedInBackground() {
@@ -561,6 +563,7 @@ export const useAppEvents = () => {
initialUrl: string;
backupDidWait: boolean;
isConnectingSSE: boolean;
attachmentsCachedOfflineMode: boolean;
}>
>({});
@@ -655,7 +658,11 @@ export const useAppEvents = () => {
}
}
if (SettingsService.getProperty("offlineMode")) {
if (
SettingsService.getProperty("offlineMode") &&
!refValues.current.attachmentsCachedOfflineMode
) {
refValues.current.attachmentsCachedOfflineMode = true;
db.attachments.cacheAttachments().catch(() => {
/* empty */
});
@@ -722,24 +729,39 @@ export const useAppEvents = () => {
useEffect(() => {
const subscriptions = [
EV.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
EV.subscribe(EVENTS.syncAborted, onSyncAborted),
EV.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
db.eventManager.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
db.eventManager.subscribe(EVENTS.syncAborted, onSyncAborted),
db.eventManager.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
db.eventManager.subscribe(EVENTS.userLoggedOut, onLogout),
db.eventManager.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
EV.subscribe(EVENTS.userSessionExpired, onUserSessionExpired),
db.eventManager.subscribe(
EVENTS.userSessionExpired,
onUserSessionExpired
),
db.eventManager.subscribe(
EVENTS.userSubscriptionUpdated,
onUserSubscriptionStatusChanged
),
EV.subscribe(EVENTS.fileDownload, onDownloadingAttachmentProgress),
EV.subscribe(EVENTS.fileUpload, onUploadingAttachmentProgress),
EV.subscribe(EVENTS.fileDownloaded, onDownloadedAttachmentProgress),
EV.subscribe(EVENTS.fileUploaded, onUploadedAttachmentProgress),
EV.subscribe(EVENTS.downloadCanceled, (data) => {
db.eventManager.subscribe(
EVENTS.fileDownload,
onDownloadingAttachmentProgress
),
db.eventManager.subscribe(
EVENTS.fileUpload,
onUploadingAttachmentProgress
),
db.eventManager.subscribe(
EVENTS.fileDownloaded,
onDownloadedAttachmentProgress
),
db.eventManager.subscribe(
EVENTS.fileUploaded,
onUploadedAttachmentProgress
),
db.eventManager.subscribe(EVENTS.downloadCanceled, (data) => {
useAttachmentStore.getState().setDownloading(data);
}),
EV.subscribe(EVENTS.uploadCanceled, (data) => {
db.eventManager.subscribe(EVENTS.uploadCanceled, (data) => {
useAttachmentStore.getState().setUploading(data);
}),
EV.subscribe(EVENTS.migrationStarted, (name) => {
@@ -765,7 +787,7 @@ export const useAppEvents = () => {
return;
endProgress();
}),
EV.subscribe(EVENTS.vaultLocked, async () => {
db.eventManager.subscribe(EVENTS.vaultLocked, async () => {
// Lock all notes in all tabs...
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
@@ -797,7 +819,6 @@ export const useAppEvents = () => {
return () => {
emitterSubscriptions.forEach((sub) => sub?.remove?.());
subscriptions.forEach((sub) => sub?.unsubscribe?.());
EV.unsubscribeAll();
};
}, [onSyncComplete, onUserUpdated]);

View File

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

View File

@@ -276,8 +276,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(product as Plan).period === "yearly"
? (product as Plan).price.gross
: (product as Plan).period === "5-year"
? (product as Plan).price.gross
: (product as Plan).price.gross
? (product as Plan).price.gross
: (product as Plan).price.gross
}`;
}
@@ -432,8 +432,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
return period.endsWith("W")
? "week"
: period.endsWith("M")
? "month"
: "year";
? "month"
: "year";
} else {
const unit = (product as RNIap.SubscriptionIOS)
?.subscriptionPeriodUnitIOS;
@@ -475,8 +475,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
type: phase.billingPeriod.endsWith("W")
? "week"
: phase.billingPeriod.endsWith("M")
? "month"
: "year"
? "month"
: "year"
};
} else {
const productIos = product as RNIap.SubscriptionIOS;
@@ -607,8 +607,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(product as Plan).period === "yearly"
? ((product as Plan).price.gross / 12).toFixed(2)
: (product as Plan).period === "5-year"
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
: (product as Plan).price.gross
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
: (product as Plan).price.gross
}`;
}

View File

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

View File

@@ -243,8 +243,8 @@ export const FluidPanelsView = React.memo(
const nextDeviceMode = DDS.isLargeTablet()
? "tablet"
: DDS.isSmallTab
? "smallTablet"
: "mobile";
? "smallTablet"
: "mobile";
setDeviceMode(nextDeviceMode, size);
},
[orientation, setDeviceMode, setDimensions]

View File

@@ -39,7 +39,7 @@ import Input from "../../components/ui/input";
import { ReminderTime } from "../../components/ui/reminder-time";
import Paragraph from "../../components/ui/typography/paragraph";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import Notifications from "../../services/notifications";
import SettingsService from "../../services/settings";
@@ -47,9 +47,18 @@ import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { getFormattedDate, useIsFeatureAvailable } from "@notesnook/common";
import {
getFormattedDate,
useIsFeatureAvailable,
usePromise
} from "@notesnook/common";
import PaywallSheet from "../../components/sheets/paywall";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { Pressable } from "../../components/ui/pressable";
import { TimeSince } from "../../components/ui/time-since";
import Heading from "../../components/ui/typography/heading";
import { eOnLoadNote } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
const ReminderModes =
Platform.OS === "ios"
@@ -114,6 +123,15 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const titleRef = useRef<TextInput>(null);
const descriptionRef = useRef<TextInput>(null);
const timer = useRef<NodeJS.Timeout>(undefined);
const referencedNotes = usePromise(
() =>
reminder?.id
? db.relations
.to({ id: reminder.id, type: "reminder" }, "note")
.resolve()
: null,
[reminder?.id]
);
const showDatePicker = () => {
setDatePickerVisibility(true);
@@ -237,6 +255,9 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
marginBottom: DDS.isTab ? 25 : undefined,
paddingHorizontal: DefaultAppStyles.GAP
}}
contentContainerStyle={{
gap: DefaultAppStyles.GAP_VERTICAL
}}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
>
@@ -271,15 +292,11 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
height={80}
wrapperStyle={{
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
/>
<ScrollView
style={{
flexDirection: "row",
marginBottom: DefaultAppStyles.GAP_VERTICAL
flexDirection: "row"
}}
horizontal
>
@@ -335,8 +352,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
style={{
backgroundColor: colors.secondary.background,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius,
marginBottom: DefaultAppStyles.GAP_VERTICAL
borderRadius: defaultBorderRadius
}}
>
<View
@@ -384,45 +400,17 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
recurringMode === RecurringModes.Year
? null
: recurringMode === RecurringModes.Week
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
<Button
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
type={
selectedDays.indexOf(item) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
return [...days];
}
days.push(item);
return [...days];
});
}}
/>
)
)
: MonthDays.map((item, index) => (
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
<Button
key={index + "monthday"}
title={index + 1 + ""}
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
type={
selectedDays.indexOf(index + 1) > -1
selectedDays.indexOf(item) > -1
? "selected"
: "plain"
}
@@ -434,16 +422,44 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(index + 1) > -1) {
days.splice(days.indexOf(index + 1), 1);
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
return [...days];
}
days.push(index + 1);
days.push(item);
return [...days];
});
}}
/>
))}
)
)
: MonthDays.map((item, index) => (
<Button
key={index + "monthday"}
title={index + 1 + ""}
type={
selectedDays.indexOf(index + 1) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(index + 1) > -1) {
days.splice(days.indexOf(index + 1), 1);
return [...days];
}
days.push(index + 1);
return [...days];
});
}}
/>
))}
</ScrollView>
</View>
) : null}
@@ -454,7 +470,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
width: "100%",
flexDirection: "column",
justifyContent: "center",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
alignItems: "center"
}}
>
@@ -513,10 +528,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
style={{
borderRadius: defaultBorderRadius,
flexDirection: "row",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
alignItems: "center",
justifyContent: "flex-start",
marginBottom: DefaultAppStyles.GAP_VERTICAL
justifyContent: "flex-start"
}}
>
<>
@@ -529,45 +542,33 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
dayjs(date).format("hh:mm A")
)
: recurringMode === RecurringModes.Year
? strings.reminderRepeatStrings.year(
dayjs(date).format("dddd, MMMM D, h:mm A")
)
: selectedDays.length === 7 &&
recurringMode === RecurringModes.Week
? strings.reminderRepeatStrings.week.daily(
dayjs(date).format("hh:mm A")
)
: selectedDays.length === 0
? strings.reminderRepeatStrings[
recurringMode as "week" | "month"
].selectDays()
: strings.reminderRepeatStrings.repeats(
repeatFrequency,
recurringMode as string,
getSelectedDaysText(selectedDays),
dayjs(date).format("hh:mm A")
)}
? strings.reminderRepeatStrings.year(
dayjs(date).format("dddd, MMMM D, h:mm A")
)
: selectedDays.length === 7 &&
recurringMode === RecurringModes.Week
? strings.reminderRepeatStrings.week.daily(
dayjs(date).format("hh:mm A")
)
: selectedDays.length === 0
? strings.reminderRepeatStrings[
recurringMode as "week" | "month"
].selectDays()
: strings.reminderRepeatStrings.repeats(
repeatFrequency,
recurringMode as string,
getSelectedDaysText(selectedDays),
dayjs(date).format("hh:mm A")
)}
</Paragraph>
</>
</View>
)}
<ReminderTime
reminder={reminder}
style={{
width: "100%",
justifyContent: "flex-start",
borderWidth: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
alignSelf: "flex-start"
}}
/>
{reminderMode === ReminderModes.Permanent ? null : (
<ScrollView
style={{
flexDirection: "row",
marginTop: DefaultAppStyles.GAP_VERTICAL,
height: 50
}}
horizontal
@@ -586,8 +587,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
mode === "Silent"
? "minus-circle"
: mode === "Vibrate"
? "vibrate"
: "volume-high"
? "vibrate"
: "volume-high"
}
fontSize={AppFontSize.xs}
height={35}
@@ -612,6 +613,60 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
))}
</ScrollView>
)}
<ReminderTime
reminder={reminder}
style={{
width: "100%",
justifyContent: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
alignSelf: "flex-start"
}}
/>
{referencedNotes &&
referencedNotes.status === "fulfilled" &&
referencedNotes.value !== null &&
referencedNotes.value?.length > 0 ? (
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Heading size={AppFontSize.md}>{strings.referencedIn()}</Heading>
{referencedNotes.value.map((item) => (
<Pressable
style={{
justifyContent: "space-between",
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
onPress={() => {
Navigation.navigate("FluidPanelsView");
fluidTabsRef.current?.goToPage("editor");
eSendEvent(eOnLoadNote, {
item: item
});
}}
type="secondary"
>
<Paragraph>{item.title}</Paragraph>
<TimeSince
style={{
fontSize: AppFontSize.xxs,
color: colors.secondary.paragraph,
marginRight: 6
}}
time={item.dateEdited}
updateFrequency={
Date.now() - item.dateEdited < 60000 ? 2000 : 60000
}
/>
</Pressable>
))}
</View>
) : null}
</ScrollView>
</KeyboardViewIOS>
</SafeAreaView>

View File

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

View File

@@ -27,7 +27,7 @@ import {
KeepLocalCopyResponse,
pick as pickFile
} from "@react-native-documents/picker";
import { basename } from "pathe";
import { basename, dirname } from "pathe";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
@@ -119,41 +119,30 @@ const file = async (fileOptions: PickerOptions) => {
throw new Error("Failed to attach file");
}
await RNFetchBlob.fs.unlink(uri);
RNFetchBlob.fs.unlink(dirname(fileCopyUri.localUri)).catch((e) => {
console.log(e, "error");
});
if (
fileOptions.tabId !== undefined &&
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
fileOptions.noteId
) {
if (isImage(file.type || "application/octet-stream")) {
editorController.current?.commands.insertImage(
{
hash: hash,
filename: fileName,
mime: file.type || "application/octet-stream",
size: file.size || 0,
dataurl: (await db.attachments.read(hash, "base64")) as string,
type: "image"
},
fileOptions.tabId
);
} else {
editorController.current?.commands.insertAttachment(
{
hash: hash,
filename: fileName,
mime: file.type || "application/octet-stream",
size: file.size || 0,
type: "file"
},
fileOptions.tabId
);
}
editorController.current?.commands.insertAttachment(
{
hash: hash,
filename: fileName,
mime: file.type || "application/octet-stream",
size: file.size || 0,
type: "file"
},
fileOptions.tabId
);
} else {
throw new Error("Failed to attach file, no tabId is set");
}
} catch (e) {
console.log("ERROR OCCURED HERE.", e);
ToastManager.show({
heading: (e as Error).message,
type: "error",
@@ -264,7 +253,6 @@ const handleImageResponse = async (
for (const image of response) {
const isPng = /(png)/g.test(image.mime);
const isJpeg = /(jpeg|jpg)/g.test(image.mime);
if (compress && (isPng || isJpeg)) {
image.path = await compressToFile(
Platform.OS === "ios" ? "file://" + image.path : image.path,
@@ -304,7 +292,7 @@ const handleImageResponse = async (
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
RNFetchBlob.fs.unlink(uri).catch((e) => {});
if (
options.tabId !== undefined &&
@@ -389,9 +377,7 @@ export async function attachFile(
return true;
} catch (e) {
DatabaseLogger.error(e);
if (Platform.OS === "ios") {
await RNFetchBlob.fs.unlink(uri);
}
RNFetchBlob.fs.unlink(uri).catch((e) => {});
return false;
}
}

View File

@@ -471,8 +471,9 @@ export const useEditorEvents = (
relationType: "from",
title: strings.dataTypesPluralCamelCase.reminder(),
onAdd: async () => {
const reminderFeature =
await isFeatureAvailable("activeReminders");
const reminderFeature = await isFeatureAvailable(
"activeReminders"
);
if (!reminderFeature.isAllowed) {
ToastManager.show({
type: "info",
@@ -534,15 +535,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 +560,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,
@@ -684,8 +695,6 @@ export const useEditor = (
lastContentChangeTime.current[item.id] = item.dateEdited;
currentNotes.current[item.id] = item;
if (!currentNotes.current[item.id]) return;
editorSessionHistory.newSession(item.id);
await commands.setStatus(
@@ -1034,13 +1043,6 @@ export const useEditor = (
state.current.isRestoringState = false;
}, []);
useEffect(() => {
eSubscribeEvent(eOnLoadNote + editorId, loadNote);
return () => {
eUnSubscribeEvent(eOnLoadNote + editorId, loadNote);
};
}, [editorId, loadNote, restoreEditorState, isDefaultEditor]);
const onContentChanged = (noteId?: string) => {
if (noteId) {
lastContentChangeTime.current[noteId] = Date.now();
@@ -1104,6 +1106,8 @@ export const useEditor = (
if (noteId) {
const note = await db.notes?.note(noteId);
fluidTabsRef.current?.goToPage("editor");
loadNoteMutex.cancel();
loadNoteMutex.release();
if (note) {
loadNote({
item: note
@@ -1111,6 +1115,8 @@ export const useEditor = (
}
} else {
noteId = useTabStore.getState().getCurrentNoteId() || null;
loadNoteMutex.cancel();
loadNoteMutex.release();
if (!noteId) {
loadNote({ newNote: true });
if (fluidTabsRef.current?.page() === "editor") {

View File

@@ -237,8 +237,9 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
button={{
icon: "plus",
onPress: async () => {
const notebooksFeature =
await isFeatureAvailable("notebooks");
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
if (!notebooksFeature.isAllowed) {
ToastManager.show({
message: notebooksFeature.error,
@@ -387,8 +388,8 @@ const NotebookItemWrapper = React.memo(
!selected
? "selected"
: !state.initialState[item.notebook.id]
? undefined
: "deselected"
? undefined
: "deselected"
);
}}
canDisableSelectionMode={false}

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
);
@@ -414,15 +417,15 @@ const TagItem = ({
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
)}

View File

@@ -242,8 +242,9 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
button={{
icon: "plus",
onPress: async () => {
const notebooksFeature =
await isFeatureAvailable("notebooks");
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
if (!notebooksFeature.isAllowed) {
ToastManager.show({
message: notebooksFeature.error,

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

@@ -322,8 +322,8 @@ export const MFASetup = ({
method?.id === "email"
? user?.email
: method?.id === "app"
? authenticatorDetails?.sharedKey || ""
: undefined
? authenticatorDetails?.sharedKey || ""
: undefined
}
multiline={method.id === "app"}
onChangeText={(value) => {
@@ -356,12 +356,12 @@ export const MFASetup = ({
sending
? null
: method.id === "app"
? strings.copy()
: `${
seconds
? strings.resendCode(seconds as number)
: strings.sendCode()
}`
? strings.copy()
: `${
seconds
? strings.resendCode(seconds as number)
: strings.sendCode()
}`
}
/>
)

View File

@@ -75,15 +75,15 @@ export default function DebugLogs() {
item.level === LogLevel.Error || item.level === LogLevel.Fatal
? hexToRGBA(colors.error.paragraph, 0.2)
: item.level === LogLevel.Warn
? hexToRGBA(colors.static.orange, 0.2)
: "transparent";
? hexToRGBA(colors.static.orange, 0.2)
: "transparent";
const color =
item.level === LogLevel.Error || item.level === LogLevel.Fatal
? colors.error.paragraph
: item.level === LogLevel.Warn
? colors.static.black
: colors.primary.paragraph;
? colors.static.black
: colors.primary.paragraph;
return !item ? null : (
<TouchableOpacity

View File

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

View File

@@ -123,8 +123,8 @@ export const TrashIntervalPicker = createSettingsPicker({
return item === -1
? strings.never()
: item === 1
? strings.reminderRecurringMode.day()
: strings.days(item);
? strings.reminderRecurringMode.day()
: strings.days(item);
},
getItemKey: (item) => item.toString(),
options: [-1, 1, 7, 30, 365],
@@ -276,10 +276,10 @@ export const ApplockTimerPicker = createSettingsPicker({
return item === -1
? strings.never()
: item === 0 || item === undefined
? strings.immediately()
: item === 1
? strings.minutes(1)
: strings.minutes(item);
? strings.immediately()
: item === 1
? strings.minutes(1)
: strings.minutes(item);
},
getItemKey: (item) => item.toString(),
options: [-1, 0, 1, 5, 15, 30],

View File

@@ -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";
@@ -62,7 +63,11 @@ import SettingsService from "../../services/settings";
import Sync from "../../services/sync";
import { useThemeStore } from "../../stores/use-theme-store";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
import {
eAfterSync,
eCloseSheet,
eOpenRecoveryKeyDialog
} from "../../utils/events";
import { NotesnookModule } from "../../utils/notesnook-module";
import { sleep } from "../../utils/time";
import { MFARecoveryCodes, MFASheet } from "./2fa";
@@ -72,8 +77,50 @@ import { logoutUser } from "./logout";
import { SettingSection } from "./types";
import { getTimeLeft } from "./user-section";
import { EDITOR_LINE_HEIGHT } from "../../utils/constants";
import { MMKV } from "../../common/database/mmkv";
import { resetTabStore } from "../editor/tiptap/use-tab-store";
import { clearAllStores } from "../../stores";
import { refreshAllStores } from "../../stores/create-db-collection-store";
export const settingsGroups: SettingSection[] = [
{
id: "account-local",
name: strings.account(),
useHook: () => useUserStore((state) => state.user),
hidden: (current) => !!current,
sections: [
{
id: "delete-data",
name: strings.deleteData(),
icon: "delete",
description: strings.deleteAccountDesc(),
modifer: () => {
presentDialog({
title: strings.deleteData(),
paragraph: strings.irreverisibleAction(),
positiveType: "errorShade",
positiveText: "Delete data",
positivePress: async () => {
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
resetTabStore();
clearAllStores();
Navigation.queueRoutesForUpdate();
SettingsService.resetSettings();
db.reset();
setImmediate(() => {
refreshAllStores();
eSendEvent(eAfterSync);
});
return true;
}
});
}
}
]
},
{
id: "account",
name: strings.account(),
@@ -159,15 +206,15 @@ export const settingsGroups: SettingSection[] = [
return status === SubscriptionStatus.TRIAL
? strings.trialOnGoing(trialEndDate)
: status === SubscriptionStatus.ACTIVE
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
status === SubscriptionStatus.PAUSED
? strings.subEndsOn(expiryDate)
: status === SubscriptionStatus.EXPIRED
? subscriptionDaysLeft.time < -3
? strings.subEnded()
: strings.accountDowngradedIn(3)
: strings.neverHesitate();
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
status === SubscriptionStatus.PAUSED
? strings.subEndsOn(expiryDate)
: status === SubscriptionStatus.EXPIRED
? subscriptionDaysLeft.time < -3
? strings.subEnded()
: strings.accountDowngradedIn(3)
: strings.neverHesitate();
}
return strings.neverHesitate();
@@ -286,17 +333,10 @@ export const settingsGroups: SettingSection[] = [
{
id: "change-password",
name: strings.changePassword(),
// type: "screen",
type: "screen",
description: strings.changePasswordDesc(),
// component: "change-password",
icon: "form-textbox-password",
modifer: () => {
ToastManager.show({
type: "info",
message:
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
});
}
component: "change-password",
icon: "form-textbox-password"
},
{
id: "change-email",
@@ -929,8 +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()
});
}
},
@@ -942,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()
})
},
{
@@ -955,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"
});
}
},
@@ -969,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"
});
}
},
@@ -989,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()
});
}
}
@@ -1199,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,
@@ -1501,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

@@ -146,7 +146,7 @@ const updateAvailableMessage = (
icon: "update",
type: "normal",
id: "app-update"
}) as Message;
} as Message);
export function setUpdateAvailableMessage(
version: GithubVersionInfo | CheckVersionResponse

View File

@@ -195,8 +195,8 @@ const NotebookItem = ({
!nestedNotebooks?.placeholders.length
? "book-outline"
: isExpanded
? "chevron-down"
: "chevron-right"
? "chevron-down"
: "chevron-right"
}
/>
</TouchableOpacity>
@@ -538,10 +538,10 @@ export const Search = ({
{searchKeyword
? `No results found for "${searchKeyword}"`
: mode === "appendNote"
? "No notes"
: mode === "selectTags"
? "No tags"
: "No notebooks"}
? "No notes"
: mode === "selectTags"
? "No tags"
: "No notebooks"}
</Text>
</View>
}

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>`;
}
@@ -419,8 +420,8 @@ const ShareView = () => {
let html = !rawData.value
? ""
: isURL(rawData?.value)
? makeHtmlFromUrl(rawData?.value)
: makeHtmlFromPlainText(rawData?.value);
? makeHtmlFromUrl(rawData?.value)
: makeHtmlFromPlainText(rawData?.value);
setNote((note) => {
note.content.data = html;
noteContent.current = html;

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

@@ -46,9 +46,10 @@ describe("Tags", () => {
.waitAndTapByText('Add "#testtag"')
.isVisibleByText("#testtag")
.pressBack(2)
.wait(1000)
.openSideMenu()
.waitAndTapById("tab-tags")
.waitAndTapByText("#testtag")
.waitAndTapByText("testtag")
.processResult(async (note) => {
await TestBuilder.create().isVisibleByText(note.body).run();
})

View File

@@ -32,10 +32,7 @@ const testvars = {
class Element {
element: Detox.NativeElement;
constructor(
public type: "id" | "text",
public value: string
) {
constructor(public type: "id" | "text", public value: string) {
if (type == "id") {
this.element = element(by.id(value)).atIndex(0);
} else {

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 = 2171;
CURRENT_PROJECT_VERSION = 2178;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1104,7 +1110,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.18;
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 = 2171;
CURRENT_PROJECT_VERSION = 2178;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1210,7 +1216,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.13;
MARKETING_VERSION = 3.3.18;
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 = 2171;
CURRENT_PROJECT_VERSION = 2178;
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.13;
MARKETING_VERSION = 3.3.18;
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 = 2171;
CURRENT_PROJECT_VERSION = 2178;
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.13;
MARKETING_VERSION = 3.3.18;
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 = 2171;
CURRENT_PROJECT_VERSION = 2178;
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.13;
MARKETING_VERSION = 3.3.18;
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 = 2171;
CURRENT_PROJECT_VERSION = 2178;
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.13;
MARKETING_VERSION = 3.3.18;
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-beta.1",
"version": "3.3.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.13-beta.1",
"version": "3.3.17",
"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",
@@ -181,7 +181,6 @@
"otplib": "12.0.1",
"patch-package": "7.0.0",
"pixelmatch": "^6.0.0",
"prettier": "3.5.3",
"react-native-bundle-visualizer": "^3.1.1",
"react-native-cli-bump-version": "1.5.0",
"react-refresh": "0.14.0",
@@ -300,7 +299,7 @@
"@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme",
"@notesnook/ui": "file:../ui",
"@social-embed/lib": "^0.1.0-next.7",
"@social-embed/lib": "^0.1.0-next.11",
"@tiptap/core": "2.6.6",
"@tiptap/extension-blockquote": "^2.6.6",
"@tiptap/extension-bullet-list": "^2.6.6",
@@ -490,18 +489,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 +537,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,11 +1,11 @@
{
"name": "@notesnook/mobile",
"version": "3.3.13",
"version": "3.3.18",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {
"postinstall": "patch-package && node ./scripts/optimize-fonts.mjs",
"run-android": "react-native run-android --active-arch-only",
"run-android": "react-native run-android --active-arch-only --mode=release",
"build": "echo 'No build script defined'",
"run-ios": "react-native run-ios",
"start": "react-native start",
@@ -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",
@@ -197,7 +197,6 @@
"otplib": "12.0.1",
"patch-package": "7.0.0",
"pixelmatch": "^6.0.0",
"prettier": "3.5.3",
"react-native-bundle-visualizer": "^3.1.1",
"react-native-cli-bump-version": "1.5.0",
"react-refresh": "0.14.0",
@@ -208,4 +207,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

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