Compare commits

...

943 Commits

Author SHA1 Message Date
Ammar Ahmed
fa0282eafc mobile: generate sourcemaps 2025-01-31 13:50:43 +05:00
Ammar Ahmed
abf4612977 core: fix esm imports 2025-01-21 14:32:48 +05:00
Ammar Ahmed
9c6637908d ci: update workflow
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-01-21 13:58:26 +05:00
01zulfi
e1006ed501 core: add nowz and timestampz formats (#7270)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-01-21 13:17:08 +05:00
Ammar Ahmed
7d034f7f16 mobile: fix build errors 2025-01-21 13:01:40 +05:00
Abdullah Atta
76eec5f8bb web: do not show email confirm notice for loggedout users 2025-01-21 12:47:51 +05:00
Ammar Ahmed
698866f53d mobile: release v3.0.27 2025-01-21 12:31:37 +05:00
Abdullah Atta
21e1e73bee desktop: fix ui when native titlebar is enabled 2025-01-21 11:35:47 +05:00
Abdullah Atta
52e89c7842 web: update snapshots 2025-01-21 10:41:05 +05:00
Abdullah Atta
8b357559fa web: fix keyboard-navigation-list tests 2025-01-21 10:30:17 +05:00
Abdullah Atta
2be35fadff editor: fix tests 2025-01-21 10:14:42 +05:00
Abdullah Atta
5fe366f4f3 global: use same versions for dependencies everywhere (#7365)
* global: use same versions for dependencies everywhere

* intl: fix `Cannot find module '@lingui/macro'`

* web: fix `I18n' is not assignable to parameter` type error

* setup: log post install cmd

* setup: more logging

* web: update lockfile
2025-01-21 09:35:38 +05:00
Ammar Ahmed
a13cbadd44 feat: new widgets on android (#7311)
- Added a new widget on android that allows you to put a note on your home screen.
- Added a new widget on android that allows you to see upcoming reminders on home screen
- Fixed new note widget freezing
2025-01-20 17:17:01 +05:00
Abdullah Atta
aa993a2fce web: bump version to 3.0.24 2025-01-20 13:26:50 +05:00
01zulfi
975af5701b web: fix reveal in list (#7297)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-01-20 12:08:13 +05:00
luis-411
3add56cfe4 global: fix search strings (#7302)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-01-20 11:47:48 +05:00
Abdullah Atta
522ea1520d web: fix tests failing on macos 2025-01-20 11:28:26 +05:00
Ammar Ahmed
c680121eb0 mobile: always load note content in editor 2025-01-20 10:38:16 +05:00
Ammar Ahmed
a9de0696c9 mobile: fix image previews not loading on ios 2025-01-20 10:38:00 +05:00
Ammar Ahmed
4a4c52e4c8 mobile: fix material menu 2025-01-20 10:37:39 +05:00
Ammar Ahmed
92f5e04788 mobile: fix lock state detection in sheets and dialogs 2025-01-20 10:37:26 +05:00
Ammar Ahmed
0cad098551 mobile: fix selecting image from gallery when applock is on 2025-01-20 10:37:26 +05:00
Abdullah Atta
46105b2175 web: fix web app on Safari 2025-01-20 10:35:23 +05:00
Abdullah Atta
8ffbda9626 Revert "editor: update tiptap to v2.10.4 (#6872)"
This reverts commit 800f3ca5d3.
2025-01-20 10:20:15 +05:00
Ammar Ahmed
1fc55a69c9 editor: fix clamped image size is larger than editor (#7324) 2025-01-20 10:07:20 +05:00
Ammar Ahmed
c417d4df91 core: fix DATE_TIME_STRIP_REGEX (#7325) 2025-01-20 10:06:26 +05:00
01zulfi
43ccf72773 intl: fix follow casing (#7342)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-01-18 11:38:20 +05:00
01zulfi
a177d36571 web: treat setting item descriptions as md and convert them to html (#7343)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-01-18 11:37:54 +05:00
01zulfi
4428ccd3a0 web: add lock app button in status bar (#7333)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-01-17 11:35:13 +05:00
01zulfi
218ffbd758 intl: fix referencedIn casing (#7334)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-01-17 11:24:11 +05:00
01zulfi
5ace95c9d1 web: fix canceling the edit profile dialog would set name to undefined (#7338)
* escaping or canceling the dialog would set profile's name to undefined

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-01-17 11:21:18 +05:00
Ammar Ahmed
77a2f86119 mobile: fix trash interval picker text showing NaN 2025-01-14 12:02:16 +05:00
01zulfi
71db85458d web: add go to next/previous tab shortcut (#7109)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-01-02 15:13:49 +05:00
01zulfi
0be59c71de web: save image compression upload setting (#7089)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-01-02 15:10:58 +05:00
Abdullah Atta
2357e3de0b web: title bar underlay not showing on mac 2025-01-02 14:15:09 +05:00
Abdullah Atta
53b89a59ed web: fix integrated titlebar on windows 2025-01-02 14:15:09 +05:00
Abdullah Atta
202c9135c2 web: show exclusive titlebar in tablet/mobile mode 2025-01-02 14:15:09 +05:00
Abdullah Atta
69e6263d34 web: better integrated titlebar that looks same across platforms 2025-01-02 14:15:09 +05:00
Abdullah Atta
ebe830bbcc theme: add new borderless input variant 2025-01-02 14:15:09 +05:00
Abdullah Atta
44bb9752d2 web: improve slider ux on mobile & small screens 2025-01-02 14:15:09 +05:00
Abdullah Atta
2f6af50fe2 web: split pane now supports adding/removing panes in place 2025-01-02 14:15:09 +05:00
Ammar Ahmed
1d80ca7d3c mobile: release v3.0.26 2024-12-31 14:03:49 +05:00
Ammar Ahmed
d33baebac8 editor: fix frame around image on selection 2024-12-31 13:21:47 +05:00
Ammar Ahmed
25aaef361a mobile: throw error if db key not found in keychain 2024-12-31 12:50:38 +05:00
Abdullah Atta
f3af39a093 web: fix action bar ui on web 2024-12-30 16:12:19 +05:00
Abdullah Atta
149c2013a2 editor: fix multiple editor views being created 2024-12-30 16:05:28 +05:00
Ammar Ahmed
8fb0b6f94a mobile: fix build errors in editor-mobile 2024-12-30 15:44:57 +05:00
Ammar Ahmed
f09e5cadcf mobile: fix image and file uploads 2024-12-30 15:44:57 +05:00
Ammar Ahmed
724c69655b mobile: fix scroll jumping in realtime sync 2024-12-30 15:44:41 +05:00
Abdullah Atta
800f3ca5d3 editor: update tiptap to v2.10.4 (#6872)
* editor: update tiptap to v2.9.1

* editor: update tiptap & pkgs to 2.10.4

* editor: update test snapshots
2024-12-30 14:51:50 +05:00
Abdullah Atta
193007370a web: add experimental support for responsive pane resizing 2024-12-30 11:56:42 +05:00
Abdullah Atta
c8aff3764b web: lots of improvements 2024-12-30 11:56:42 +05:00
Abdullah Atta
d72b502977 web: fix snapping logic 2024-12-30 11:56:42 +05:00
Abdullah Atta
263c970320 web: add support for macOS like pane resizing logic 2024-12-30 11:56:42 +05:00
Abdullah Atta
aeebc9878d web: only reset size of doubled clicked pane 2024-12-30 11:56:42 +05:00
Abdullah Atta
4f37c92ecd web: add snapping support 2024-12-30 11:56:42 +05:00
Abdullah Atta
3180a88ef0 web: replace react-resizable-panels with custom implementation 2024-12-30 11:56:42 +05:00
01zulfi
6672db6ac2 editor: add markdown shortcut for checklist (#7087)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-30 11:17:18 +05:00
01zulfi
b6da62469e core: fix vault unlock event not sent when a note is unlocked (#7172)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-30 10:43:33 +05:00
Ammar Ahmed
e67e6374c3 mobile: set default widget size 2024-12-30 10:32:15 +05:00
Ammar Ahmed
ff1fc5b391 mobile: fix formatting 2024-12-30 10:32:15 +05:00
Ammar Ahmed
2f8bf191aa mobile: resizable and themeable android widget 2024-12-30 10:32:15 +05:00
Abdullah Atta
c4af2b2159 web: fix build on macos (by updating packages) 2024-12-30 10:13:40 +05:00
Abdullah Atta
690b157387 desktop: b/w when window is unfocused 2024-12-30 10:13:12 +05:00
Abdullah Atta
5a276a43a4 web: improve tab ui 2024-12-30 10:12:02 +05:00
Abdullah Atta
a364ae2e9d core: fix notes search in tags, colors & notebooks not working 2024-12-27 16:20:53 +05:00
Abdullah Atta
1e2055c81b web: fix Cannot start service: Host version "0.20.2" does not match binary version "0.23.1" on macos 2024-12-27 15:30:37 +05:00
01zulfi
31d110252c web: fix status bar size changing (#7143)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-27 11:50:40 +05:00
Ammar Ahmed
702976c6cf mobile: do not show progress when doing full auto backup 2024-12-26 12:44:54 +05:00
Ammar Ahmed
3ec8500515 mobile: add fdroid release notes 2024-12-26 12:32:27 +05:00
Ammar Ahmed
2c03420f33 mobile: release v3.0.25 2024-12-26 12:25:12 +05:00
Ammar Ahmed
868ae597ae mobile: fix ts errors 2024-12-26 12:25:12 +05:00
01zulfi
59d3e06935 intl: fix casing in legal & notebooks strings (#7142)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-26 11:29:57 +05:00
Ammar Ahmed
b296f5dd5c mobile: update export options 2024-12-24 15:59:33 +05:00
Abdullah Atta
2dc5e4fef5 docs(help): add notice on gift card page 2024-12-24 15:54:44 +05:00
Abdullah Atta
f836de3854 docs(help): clarify about expiry date on gift cards 2024-12-24 15:54:44 +05:00
Ammar Ahmed
631a591477 mobile: update ios provisioning 2024-12-24 15:31:06 +05:00
Ammar Ahmed
1bb27145ce mobile: release v3.0.24 2024-12-24 14:14:16 +05:00
Ammar Ahmed
79b89c2e85 mobile: skip migration progress during tests 2024-12-24 14:10:11 +05:00
Ammar Ahmed
ceb2a8c747 intl: fix locales data 2024-12-24 12:06:52 +05:00
Ammar Ahmed
bee06ecf18 mobile: fix multi file sharing 2024-12-24 12:06:52 +05:00
Ammar Ahmed
33c7eb5a74 mobile: fix imports 2024-12-24 12:06:52 +05:00
Abdullah Atta
2cb788712a web: fix editor toolbar scroll ui 2024-12-24 11:13:57 +05:00
Abdullah Atta
90d2338412 Revert "web: fix editor toolbar scroll styling (#7113)"
This reverts commit 5229408156.
2024-12-24 10:31:14 +05:00
01zulfi
4bebf5a7b5 editor: fix task list state & readonly bugs (#7117)
* fix task list complete state not showing on first load
* fix task list allowing paste when readonly

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-24 10:18:23 +05:00
Ammar Ahmed
68eb5023f8 mobile: update deps 2024-12-24 10:10:56 +05:00
Abdullah Atta
8692c87ed7 intl: fix resend code string 2024-12-24 10:10:56 +05:00
Abdullah Atta
ea0623c6bc web: create backup on submit when changing password 2024-12-24 10:10:56 +05:00
Abdullah Atta
b526dc4d39 core: hash old password before deriving new crypto key 2024-12-24 10:10:56 +05:00
Ammar Ahmed
76a1025f59 mobile: do not call clearSessions when changing password 2024-12-24 10:10:56 +05:00
Ammar Ahmed
85e046a379 mobile: fix error in shortcut manager 2024-12-24 10:10:56 +05:00
Ammar Ahmed
58a6bbd262 mobile: fix logout handling 2024-12-24 10:10:56 +05:00
Ammar Ahmed
07aabf9a2a core: return false if no fallback key 2024-12-24 10:10:56 +05:00
Ammar Ahmed
8c7ba6b963 mobile: fix profile not rendered on home 2024-12-24 10:10:56 +05:00
Ammar Ahmed
d125e0f6d9 mobile: fix encryption 2024-12-24 10:10:56 +05:00
Ammar Ahmed
43d017d64a mobile: fix code input when changing email 2024-12-24 10:10:56 +05:00
Abdullah Atta
bc3e7c6a53 web: add noop fallback methods in storage 2024-12-24 10:10:56 +05:00
Abdullah Atta
6e8610c358 core: compute usesFallbackPWHash instead of storing it
this fixes the fallback logic for already logged in users
2024-12-24 10:10:56 +05:00
Abdullah Atta
ab00645c23 web: fix recovery view button text not wrapping 2024-12-23 18:29:10 +05:00
Abdullah Atta
e041976ae0 web: fix gift cards link 2024-12-23 18:06:09 +05:00
Abdullah Atta
9d7e6b40a0 web: bump version to 3.0.23 2024-12-23 18:05:43 +05:00
Abdullah Atta
db40a648ab web: fix Cannot read properties of undefined (reading 'instance') 2024-12-23 17:53:14 +05:00
Abdullah Atta
805e60a808 web: handle NetworkError when attempting to fetch resource. 2024-12-23 17:46:53 +05:00
Abdullah Atta
8c7093189b core: add support for sorting search results
search results are sorted by date created for now until
we add proper support for sorting search results
in the ui
2024-12-23 14:30:45 +05:00
Abdullah Atta
3092696c83 docs: add docs for gift cards 2024-12-23 14:30:25 +05:00
Ammar Ahmed
c2e7f7576e mobile: redeem gift code 2024-12-23 14:30:25 +05:00
Abdullah Atta
6b4f7900ec web: show subscription provider info in subscription settings 2024-12-23 14:30:25 +05:00
Abdullah Atta
523ae5b07b intl: add other subscription providers 2024-12-23 14:30:25 +05:00
Abdullah Atta
a628b3fd92 intl: add strings for gift redeeming 2024-12-23 14:30:25 +05:00
Abdullah Atta
e9f3037129 web: add gift redeem support 2024-12-23 14:30:25 +05:00
Abdullah Atta
46d4b9dece core: add api for redeeming gift codes 2024-12-23 14:30:25 +05:00
01zulfi
c701baaa2c web: better ux when autosave is off (#6610) (#6849)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-12-23 14:30:03 +05:00
Ammar Ahmed
a798f8e280 mobile: fix intro button position 2024-12-23 13:43:57 +05:00
Abdullah Atta
baebf79046 web: fix type errors 2024-12-23 12:29:07 +05:00
Abdullah Atta
5727aa6c4d web: fix checkout discounted snapshots 2024-12-21 16:41:16 +05:00
Abdullah Atta
36cf1feff9 web: fix crash on opening sort menu 2024-12-21 16:41:04 +05:00
Abdullah Atta
91a3b0f5ed web: fix imports with nested notebooks 2024-12-21 16:15:29 +05:00
Ammar Ahmed
293219d781 mobile: fix encryption on ios 2024-12-21 14:15:56 +05:00
Ammar Ahmed
2cd4f44a92 core: fix fallback key encryption 2024-12-21 14:15:56 +05:00
Abdullah Atta
4b9a95ded7 core: add support for fallback hashing
on ios some users using passwords with multi byte characters
generates the wrong hash. This commit adds support for fallback
to allow those users to continue to login once the fix has been applied.
2024-12-21 14:15:56 +05:00
Ammar Ahmed
565b0382f9 mobile: fix encryption on ios 2024-12-21 14:15:56 +05:00
Ammar Ahmed
1e837f2a9c mobile: fix undo/redo buttons in editor 2024-12-21 14:13:07 +05:00
Ammar Ahmed
765ebbe10c mobile: allow saving empty string input 2024-12-21 14:12:24 +05:00
Ammar Ahmed
e95d1601a6 mobile: fix sharing files on android 2024-12-21 14:10:55 +05:00
Ammar Ahmed
c6dd1d1615 mobile: fix snooze time not rendered properly 2024-12-21 14:10:34 +05:00
Ammar Ahmed
bb50850995 Merge pull request #7008 from streetwriters/fix-localization-error 2024-12-21 14:10:05 +05:00
Ammar Ahmed
83083134e9 Merge branch 'master' into fix-localization-error
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-12-21 14:09:22 +05:00
01zulfi
61b8960aa5 web: fix mfa fallback string (#7123)
* also add selected style to mfa type selector button

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-20 15:55:42 +05:00
01zulfi
5229408156 web: fix editor toolbar scroll styling (#7113)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-20 12:30:08 +05:00
01zulfi
0921fef267 web: show account password label in delete vault dialog (#7085)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-11 11:30:42 +05:00
luis-411
60989359e2 theme: make link color align with theme (#7077)
Signed-off-by: Luis Kriner <luis@kriner.info>
2024-12-10 11:01:04 +05:00
luis-411
8fe381afa9 editor: prevent creation of multiple empty notes (#6922)
Signed-off-by: Luis Kriner <luis@kriner.info>
2024-12-10 10:30:19 +05:00
01zulfi
fea9c4e42b web: fix links in report issue dialog (#7071)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-10 10:28:17 +05:00
01zulfi
1d78402d5d web: fix tags not showing in locked note (#7061)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-10 09:58:13 +05:00
01zulfi
aa72c33aea webclipper: handle default case in listener (#7058)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-10 09:57:23 +05:00
01zulfi
69504a150e core: handle error in parseInternalLink (#7065)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-10 09:56:47 +05:00
dyw770
a78a2e4ae2 core: fix object-id is sometimes less than 24 chars in length (#6991) (#6994)
Signed-off-by: dyw770 <dyw770@qq.com>
2024-12-06 10:11:57 +05:00
luis-411
0ffbf83141 editor: hide edit link button when in read only mode (#7018)
Signed-off-by: Luis Kriner <luis@kriner.info>
2024-12-06 09:46:44 +05:00
01zulfi
75c346a460 docs: add README.md for web clipper (#7054)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-05 10:55:28 +05:00
01zulfi
93af79926d editor: disable spellcheck for inline code (#7036)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-05 10:49:40 +05:00
01zulfi
1b354e4bca web: hide spin buttons in mfa code input (#7048)
* they overlap with the resend button

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-05 10:26:54 +05:00
01zulfi
6d53b78f6a web: round off zoom value
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-28 09:59:32 +05:00
Ammar Ahmed
8e90adccab mobile: fix localization issues 2024-11-27 14:02:22 +05:00
Abdullah Atta
c19635b16a web: fix direct index access of strings 2024-11-27 13:14:44 +05:00
Abdullah Atta
94e4b00294 intl: fix direct access of strings in object 2024-11-27 13:14:12 +05:00
Abdullah Atta
76908b1fbd intl: generate action for when data type is unknown 2024-11-27 12:27:09 +05:00
Ammar Ahmed
7409c5382d mobile: fix localization error on searching notebook/tags 2024-11-27 10:41:52 +05:00
01zulfi
2462eb1631 monograph: remove callout headings from toc (#6875) (#6993)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-26 16:27:32 +05:00
Abdullah Atta
986ff32293 monograph: bump version to 1.2.2 2024-11-26 16:23:00 +05:00
Abdullah Atta
8f7fd9d8ee monograph: use fallback fonts when rendering og image 2024-11-26 16:22:48 +05:00
01zulfi
54d6a5c307 monograph: fix PUBLIC_URL on client
* use loader to get PUBLIC_URL

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-26 15:27:43 +05:00
Abdullah Atta
d672a43a83 monograph: bump version to 1.2.1 2024-11-26 11:58:54 +05:00
Abdullah Atta
48343e5d5c web: do not zoom if e.deltaY is 0 2024-11-25 16:26:02 +05:00
Levi Ob
0d3a8c88c8 web: fix inconsistent zoom amount when adjusting with scroll wheel. (#6990)
Zoom steps are always in increments of 10 when using a mouse wheel or trackpad.

Signed-off-by: Levi Ob <leviob.msrni@slmail.me>
2024-11-25 12:36:32 +05:00
Abdullah Atta
31367066f7 web: run prerequisite checks on app start
this gives useful errors to users if their browser doesn't support something
2024-11-25 12:33:05 +05:00
Abdullah Atta
f7c097988a web: fix cannot read property clear of undefined when pressing fix it 2024-11-25 12:31:38 +05:00
Abdullah Atta
b7f1f36090 web: fix async sqlite
async sqlite mode uses indexeddb and has some limitations like:
- running 1 query at a time
This commit uses a mutex to run all queries sequentially
2024-11-25 12:31:11 +05:00
01zulfi
8756299d80 web: always show desktop backup directory path (#6867)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-25 10:26:30 +05:00
dyw770
3525d60f8c monograph: add monograph response charset header
Signed-off-by: dyw770 <dyw770@qq.com>
2024-11-24 23:19:36 +05:00
Abdullah Atta
d08a485111 desktop: extract better-sqlite3 patching to its own module 2024-11-24 22:01:28 +05:00
Abdullah Atta
c203b035c4 monograph: bump version to 1.2.0 2024-11-24 15:38:20 +05:00
Abdullah Atta
0f4b2bdae7 monograph: fix Cannot find module '@remix-run/server-runtime' 2024-11-24 15:24:46 +05:00
Abdullah Atta
cb0bc39cd1 monograph: use bun in prod 2024-11-24 15:18:52 +05:00
Abdullah Atta
84abfa3478 monograph: speed up og image generation
this gets rid of satori and uses custom logic for drawing using @napi-rs/canvas
2024-11-24 15:18:52 +05:00
thecodrr
a40a48e2c1 server(theme): add installs.json to gitignore 2024-11-23 17:48:34 +00:00
Abdullah Atta
124499cabb monograph: use sharp output as buffer instead of stream 2024-11-23 22:23:42 +05:00
Ammar Ahmed
e874d1efd4 mobile: changelog 2024-11-23 22:20:05 +05:00
Ammar Ahmed
fb1a17566f mobile: release v3.0.23 2024-11-23 22:17:42 +05:00
Abdullah Atta
8dd8447212 monograph: cache satori output in lru cache
satori is sloooooow so we need to cache.
find another solution for og generation
2024-11-23 22:16:47 +05:00
Ammar Ahmed
eba7341d45 mobile: restore editor state only if not restored already 2024-11-23 22:15:57 +05:00
Ammar Ahmed
08bd36d518 mobile: fix deps 2024-11-23 22:15:57 +05:00
Ammar Ahmed
7a45522ff3 mobile: make attachment item pressable 2024-11-23 22:15:57 +05:00
Ammar Ahmed
4ee2923181 mobile: fix attachment downloads failing on mobile 2024-11-23 22:15:57 +05:00
Abdullah Atta
48459efdb1 monograph: mark sharp as external in ssr build 2024-11-23 21:45:44 +05:00
Abdullah Atta
c848f3c588 monograph: mark sharp as external dep in vite 2024-11-23 21:44:24 +05:00
Abdullah Atta
52a75cfdaf monograph: install sharp with optional deps during docker 2024-11-23 21:42:28 +05:00
Abdullah Atta
9331b5f919 monograph: bump version to 1.1.1 2024-11-23 21:33:37 +05:00
Abdullah Atta
2ff53d4650 monograph: use sharp for generating og images 2024-11-23 21:33:21 +05:00
Abdullah Atta
7f2c8ca05a web: fix checkout snapshots 2024-11-23 14:56:58 +05:00
Abdullah Atta
9e752e9406 web: bump version to 3.0.22 2024-11-23 14:53:31 +05:00
Abdullah Atta
b8c3e2098f web: fix one-time discount wrongly shown as recurring 2024-11-23 14:39:45 +05:00
Abdullah Atta
4308e3bf49 web: fix editor zoom on trackpads 2024-11-23 14:03:31 +05:00
Abdullah Atta
25956ccadb web: remove auto item reveal when opening items 2024-11-23 13:31:23 +05:00
Abdullah Atta
af3fba5176 core: lazily use db in config & kv stores 2024-11-23 13:28:55 +05:00
Ammar Ahmed
4afaa6a36e mobile: fix reminder visual issues 2024-11-23 13:00:57 +05:00
Ammar Ahmed
e98e1a5f36 mobile: fix pdf-preview crash 2024-11-23 12:24:17 +05:00
01zulfi
abfdad0fec web: fix setting item width (#6909) (#6935)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2024-11-23 12:16:26 +05:00
dyw770
99e50ddad6 monograph: fix monograph version endpoints cors
Signed-off-by: dyw770 <dyw770@qq.com>
2024-11-23 12:05:29 +05:00
Ammar Ahmed
67a61dd3f3 mobile: release v3.0.22 2024-11-20 12:40:37 +05:00
Ammar Ahmed
13df01d55a mobile: fix readonly editor not loading 2024-11-20 12:38:17 +05:00
Ammar Ahmed
af4ea036c8 mobile: fix sync on iOS 2024-11-20 12:37:38 +05:00
Abdullah Atta
97a75d9498 ci: generate & publish flatpak sources during desktop release 2024-11-20 11:51:04 +05:00
Abdullah Atta
950028e3b4 web: bump version to v3.0.21 2024-11-20 11:26:23 +05:00
Abdullah Atta
874726c712 global: update lockfiles 2024-11-20 11:25:28 +05:00
Abdullah Atta
0f380b9962 Revert "editor: fix heading and p gap (#6530)"
This reverts commit c26988134f.
2024-11-20 11:21:48 +05:00
Abdullah Atta
2ce569726c editor: fix image on load animation 2024-11-20 11:17:25 +05:00
Abdullah Atta
a38fe7da43 web: fix editor margins state reset on restart 2024-11-20 11:09:45 +05:00
Abdullah Atta
057b563608 web: fix password app lock stuck on "wrong password" 2024-11-20 11:07:05 +05:00
Ammar Ahmed
3741e26219 mobile: fix error when changing 2fa method 2024-11-20 10:25:11 +05:00
Ammar Ahmed
86bf8f68bf ci: build ios app on latest mac runner 2024-11-19 19:28:45 +05:00
Ammar Ahmed
e7f67d1bb9 mobile: do not show migration on first launch 2024-11-19 18:52:27 +05:00
Ammar Ahmed
9c28e6bb93 mobile: fix build errors 2024-11-19 18:38:29 +05:00
Abdullah Atta
0f84fab810 mobile: only show migration progress for main db 2024-11-19 17:01:44 +05:00
Abdullah Atta
228a8e612f web: only show migration progress for main db 2024-11-19 17:01:33 +05:00
Abdullah Atta
175c6cdba0 core: send db name with migration events 2024-11-19 17:01:24 +05:00
Abdullah Atta
d3e2fcc3ac web: fix app stuck on splash screen after logout 2024-11-19 16:35:25 +05:00
Abdullah Atta
dafc2de2b3 ci: run desktop macos x64 tests on macos-13 runner 2024-11-19 16:00:08 +05:00
Abdullah Atta
f9eb9a1ad9 desktop: add test timeout and force exit 2024-11-19 15:52:27 +05:00
Abdullah Atta
1f5001210b ci: build electron wrapper after npm i 2024-11-19 15:52:17 +05:00
Abdullah Atta
3150eafb89 core: workaround for tokens shorter than 3 chars 2024-11-19 15:45:40 +05:00
Abdullah Atta
ad1e3f2c67 ci: Install sqlite-better-trigram for all arch 2024-11-19 15:16:21 +05:00
Abdullah Atta
d67b78d86e desktop: remove sqlite-better-trigram installation from build script 2024-11-19 12:21:44 +05:00
Abdullah Atta
25f74ff04d desktop: always install both arm64 and x64 packages on all platforms 2024-11-19 12:10:52 +05:00
Abdullah Atta
be7c1453fe desktop: force install both x64 & arm64 packages for sqlite-better-trigram on macos 2024-11-19 12:00:21 +05:00
Abdullah Atta
daa60939c3 desktop: take app screenshot if test fails 2024-11-19 11:55:37 +05:00
Abdullah Atta
ac659b05e4 web: ignore network error 2024-11-19 11:49:31 +05:00
Abdullah Atta
8391d51e02 ci: run desktop tests on macos x64 2024-11-19 11:09:37 +05:00
01zulfi
ad78b3a931 editor: fix backspace deleting formatting in list item (#6712) (#6878)
* fix hitting backspace inside the second (or next) p in list item deleted the formatting of the entire list item

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-19 10:30:13 +05:00
Abdullah Atta
e3a1fdd87d ci: simplify macos desktop test step 2024-11-18 23:43:44 +05:00
Abdullah Atta
12f24db3af desktop: add tests (#6900)
* desktop: add tests

* ci: fix install packages step

* ci: set NOTESNOOK_STAGING env

* ci: fix working dir

* ci: use cross-env on windows

* desktop: fix tests

* ci: use `xvfb-run` on linux

* ci: install playwright deps

* ci: remove DEBUG=pw*

* ci: no need to install playwright deps
2024-11-18 23:38:41 +05:00
Abdullah Atta
2af805017c web: bump version to v3.0.20 2024-11-18 20:01:20 +05:00
Abdullah Atta
97d4ca0afe desktop: install and load sqlite-better-trigram module 2024-11-18 19:03:57 +05:00
Ammar Ahmed
8b5c246531 mobile: release 3.0.21 2024-11-18 18:39:36 +05:00
Ammar Ahmed
8f2113b155 mobile: show migration progress 2024-11-18 13:45:52 +05:00
Abdullah Atta
a378bd397b web: show status when db is migrating on startup 2024-11-18 13:45:52 +05:00
Abdullah Atta
a315473ea4 core: add migration start/finished events 2024-11-18 13:45:52 +05:00
Ammar Ahmed
7dc581b9f3 mobile: copy support email automatically 2024-11-18 12:42:22 +05:00
alihamuh
5ef392ca31 web: add copy button for support email in settings 2024-11-18 12:42:22 +05:00
Abdullah Atta
846c949cd1 desktop: handle Error while decrypting the ciphertext provided to safeStorage.decryptString. error 2024-11-18 12:16:58 +05:00
Abdullah Atta
d88ead4609 web: fix dialogs not closing when app gets locked 2024-11-18 11:51:29 +05:00
Abdullah Atta
055c109563 web: fix crash immediately after unlocking app 2024-11-18 11:51:09 +05:00
dyw770
378a7197c2 monograph: fix code block copy (#6891)
Signed-off-by: dyw770 <dyw770@qq.com>
2024-11-18 11:31:45 +05:00
Abdullah Atta
89ee0f8d27 core: fix sync tests and remove obsolete snapshots 2024-11-16 15:16:18 +05:00
Abdullah Atta
c9ea9b1d4e web: fix app note starting due to outdated sqlite wasm libs 2024-11-16 14:16:01 +05:00
Ammar Ahmed
a91532839f mobile: add better-trigram extension 2024-11-16 14:16:01 +05:00
Abdullah Atta
b32f6b2460 core: remove special handling for queries < 3 in length 2024-11-16 14:16:01 +05:00
Abdullah Atta
e02970b936 web: install better trigram sqlite extension 2024-11-16 14:16:01 +05:00
Abdullah Atta
1f24fd4758 core: use better_trigram for improved content tokenization 2024-11-16 14:16:01 +05:00
Abdullah Atta
5a16403a13 web: fix Could not find a provider port to communicate with. error 2024-11-16 07:42:37 +05:00
01zulfi
48bbaad1b8 editor: fix markdown link pasting (#6639) (#6866)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-16 06:41:22 +05:00
Ammar Ahmed
520711eec9 mobile: match height with mobile action sheet 2024-11-15 16:22:29 +05:00
Abdullah Atta
3f82a4ebd4 core: fix using wrong encryption key shows incorrect password error 2024-11-15 16:02:55 +05:00
Abdullah Atta
4feb22fc12 web: open empty note by default on startup 2024-11-15 16:02:55 +05:00
Abdullah Atta
4e0e8dbb66 web: even faster startup 2024-11-15 16:02:55 +05:00
Ammar Ahmed
431c4eae14 mobile: fix localization issues 2024-11-15 15:54:20 +05:00
Ammar Ahmed
6373cbc9a4 mobile: fix core api usage 2024-11-15 15:54:20 +05:00
Ammar Ahmed
bbf3477512 editor: fix action-sheet not clickable 2024-11-15 15:54:19 +05:00
01zulfi
ca30ac86d7 web: fix close tab opening again
* introduced in #6817

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-15 15:47:39 +05:00
Abdullah Atta
88aecb3af2 setup: use --moduleResolution Bundler when running esm build 2024-11-15 12:31:56 +05:00
Abdullah Atta
c5a78b0293 global: update lockfiles 2024-11-15 12:31:56 +05:00
Abdullah Atta
b0bc1ec4a6 web: fix FOUC on startup 2024-11-15 12:31:56 +05:00
Abdullah Atta
84b3892053 desktop: update electron to v31 2024-11-15 12:31:56 +05:00
Abdullah Atta
d985683c0f desktop: fix Electron failed to install correctly error 2024-11-15 12:31:55 +05:00
Ammar Ahmed
008ce9222e mobile: fix warning 2024-11-15 12:00:41 +05:00
Ammar Ahmed
c360fa8c9b mobile: fix build 2024-11-15 11:56:46 +05:00
Abdullah Atta
c1afd4692d desktop: disable shrinking for titlebar 2024-11-15 10:43:01 +05:00
Abdullah Atta
7748cf453c desktop: fix node version is different error on startup 2024-11-15 10:42:17 +05:00
Abdullah Atta
34d0123351 ci: upgrade all actions to actions/checkout@v4 2024-11-13 18:24:14 +05:00
Abdullah Atta
af1cfc92c2 web: use unique note during sync tests 2024-11-13 18:13:16 +05:00
Abdullah Atta
59e06ef938 core: fix sync not running on app startup 2024-11-13 18:12:54 +05:00
Abdullah Atta
9d12ef82c0 web: do not prefetch wa-sqlite wasm module on desktop 2024-11-13 17:52:40 +05:00
Abdullah Atta
0a9bfd4b69 web: get rid of mitata; run benchmarks directly 2024-11-13 17:29:07 +05:00
Abdullah Atta
329248ebbd web: reduce pbkdf2 db key generator iterations to 100k
this is for performance reasons mainly. 100k should still be super secure.
2024-11-13 17:29:07 +05:00
Ammar Ahmed
cd51f2f2c9 ci: fix tests 2024-11-13 15:53:14 +05:00
Abdullah Atta
a3e7d5bd84 ci: get rid of codspeed 2024-11-13 15:48:33 +05:00
Abdullah Atta
6a261d284f web: update @playwright/test to v1.48.2 2024-11-13 15:48:33 +05:00
Abdullah Atta
eddf9f6d04 web: kill server after running benchmarks 2024-11-13 15:48:33 +05:00
Abdullah Atta
33e0f81988 ci: install playwright chromium before running benchmark 2024-11-13 15:48:33 +05:00
Abdullah Atta
0df8f80b48 ci: add benchmark workflow for @notesnook/web 2024-11-13 15:48:33 +05:00
Abdullah Atta
2f4590fbb6 web: add benchmarks 2024-11-13 15:48:33 +05:00
Abdullah Atta
a011857286 web: more fixes to new notebooks tree 2024-11-13 15:48:33 +05:00
Abdullah Atta
cc9e7e10b6 web: fix tests 2024-11-13 15:48:33 +05:00
Abdullah Atta
78e04bfac5 global: update lockfiles 2024-11-13 15:48:33 +05:00
Abdullah Atta
652231d5fd editor(mobile): fix editor not loading in dev 2024-11-13 15:48:33 +05:00
Abdullah Atta
343920d376 editor(mobile): fix ui for new custom action sheet 2024-11-13 15:48:33 +05:00
Abdullah Atta
1f3e7574f5 web: improve app startup 2024-11-13 15:48:33 +05:00
Abdullah Atta
ea9b10ff23 web: get rid of text-scramble 2024-11-13 15:48:33 +05:00
Abdullah Atta
232a347f4a web: lazy load hash-wasm 2024-11-13 15:48:33 +05:00
Abdullah Atta
00053cb99d web: get rid of web-streams-polyfill 2024-11-13 15:48:33 +05:00
Abdullah Atta
03a2a2f9cc web: lazily load zip/unzip streams 2024-11-13 15:48:33 +05:00
Abdullah Atta
09c8c66841 web: @notesnook/core related changes 2024-11-13 15:48:33 +05:00
Abdullah Atta
53e4bda69d web: stop lazy loading fs
we really should split it up so its easier to partially import
2024-11-13 15:48:33 +05:00
Abdullah Atta
4c77c8e22c web: get rid of react-complex-tree
build a custom implementation using react-virtuouso
2024-11-13 15:48:33 +05:00
Abdullah Atta
9afb4a52c2 web: get rid of tinycolor2 2024-11-13 15:48:33 +05:00
Abdullah Atta
b3d6fd7f5b web: import svg assets as url to exclude them from main bundle 2024-11-13 15:48:33 +05:00
Abdullah Atta
245d022f58 web: prefetch sqlite wasm module 2024-11-13 15:48:33 +05:00
Abdullah Atta
2d226db271 web: replace react-day-picker with custom built day picker
it uses `@rehookify/datepicker`
2024-11-13 15:48:33 +05:00
Abdullah Atta
9281f9a390 web: get rid of framer-motion 2024-11-13 15:48:33 +05:00
Abdullah Atta
d30bb56e75 web: remove unused modules & components 2024-11-13 15:48:33 +05:00
Abdullah Atta
eb92989478 web: remove unused packages 2024-11-13 15:48:33 +05:00
Abdullah Atta
d0dcdcd337 web: replace marked with snarkdown 2024-11-13 15:48:33 +05:00
Abdullah Atta
60aa92ca03 setup: update typescript to 5.6 2024-11-13 15:48:33 +05:00
Abdullah Atta
e2bbc1425b setup: add build script 2024-11-13 15:48:33 +05:00
Abdullah Atta
fc01103353 ui: remove framer-motion 2024-11-13 15:48:33 +05:00
Abdullah Atta
22919561fe theme: remove @types/tinycolor 2024-11-13 15:48:33 +05:00
Abdullah Atta
745cbc082f editor: replace tinycolor2 with colord 2024-11-13 15:48:33 +05:00
Abdullah Atta
6671a97181 editor: get rid of framer-motion 2024-11-13 15:48:33 +05:00
Abdullah Atta
b7aaa382b1 editor: fix postbuild script 2024-11-13 15:48:33 +05:00
Abdullah Atta
ecfa0e2c6b theme: get rid of tinycolor2 2024-11-13 15:48:33 +05:00
Abdullah Atta
58c6cce4e4 common: await getContentFromData 2024-11-13 15:48:33 +05:00
Abdullah Atta
b388a550a5 core: remove unneeded packages 2024-11-13 15:48:33 +05:00
Abdullah Atta
f0c5f6b949 core: lazy init compressor 2024-11-13 15:48:33 +05:00
Abdullah Atta
459cd70c32 core: lazy load signalr 2024-11-13 15:48:33 +05:00
Abdullah Atta
ece142e8d8 core: lazy load content-type 2024-11-13 15:48:33 +05:00
Abdullah Atta
5f1f308540 core: replace mime-db with mime and lazy load it 2024-11-13 15:48:33 +05:00
Abdullah Atta
7437b29518 core: fix type errors in migrations 2024-11-13 15:48:33 +05:00
Abdullah Atta
7acb0a8c0f common: use tsc for builds instead of tsup 2024-11-13 15:48:33 +05:00
Abdullah Atta
a1a24ee865 editor: use tsc for builds instead of tsup 2024-11-13 15:48:33 +05:00
Abdullah Atta
02008ad098 core: use tsc instead of tsup for builds 2024-11-13 15:48:33 +05:00
Abdullah Atta
0a02431f6a web: remove unneeded modules 2024-11-13 15:48:33 +05:00
Ammar Ahmed
9291711036 ci: enable all tests 2024-11-13 11:22:32 +05:00
01zulfi
f3e0fda5fa web: fix button is not defined error
* introduced in #6848

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-13 11:13:17 +05:00
Ammar Ahmed
0e721a4948 mobile: add e2e test workflow 2024-11-13 09:17:11 +05:00
Ammar Ahmed
e0a704d022 mobile: fix e2e tests 2024-11-13 09:17:11 +05:00
Abdullah Atta
e36bc816ea web: fix more failing tests 2024-11-13 09:17:11 +05:00
Abdullah Atta
f8a62ca437 web: fix note & notebook tests 2024-11-13 09:17:11 +05:00
Abdullah Atta
22ecb079b0 intl: update po files 2024-11-13 09:17:11 +05:00
Abdullah Atta
06333cf9aa web: localize more missing strings 2024-11-13 09:17:11 +05:00
Ammar Ahmed
1ad199ce53 mobile: cleanup and fix errors 2024-11-13 09:17:11 +05:00
Abdullah Atta
ceebda5314 web: localize missing strings 2024-11-13 09:17:11 +05:00
Abdullah Atta
e2d33381cc intl: fix type error in vite.config.mts due to duplicate rollup packages 2024-11-13 09:17:11 +05:00
Abdullah Atta
2417f8e233 global: fix many localization related errors and issues 2024-11-13 09:17:11 +05:00
Ammar Ahmed
a240b0b3d7 ci: add e2e workflow 2024-11-11 19:28:08 +05:00
01zulfi
fab085579a web: add text zoom functionality in editor (#6633) (#6846)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-09 12:15:27 +05:00
Abdullah Atta
a5faa4acea web: fix checkout (and make it load faster) 2024-11-08 17:06:17 +05:00
Ammar Ahmed
4014f2a5b5 mobile: support monograph server url 2024-11-08 12:21:07 +05:00
Abdullah Atta
fb62839dad mobile: use mongraph host from core (instead of a static value) 2024-11-08 12:21:07 +05:00
Abdullah Atta
7bf8f1cfc1 web: use mongraph host from core (instead of a static value) 2024-11-08 12:21:07 +05:00
Abdullah Atta
b8a727eee8 monograph: add /api/version endpoint for self hosting support 2024-11-08 12:21:07 +05:00
Abdullah Atta
52e12de296 web: add support for using custom monograph server 2024-11-08 12:21:07 +05:00
Abdullah Atta
0bb2345ccb core: add host for monograph server 2024-11-08 12:21:07 +05:00
Abdullah Atta
78e957eed4 monograph: use public url everywhere instead of monogr.ph 2024-11-08 12:21:07 +05:00
Muhammad Ali
865efaa860 desktop: fix duplicate window controls showing on Linux desktop app (#6844) 2024-11-08 12:16:33 +05:00
01zulfi
2931fd0b3e web: fix copy link button copying link text instead of link (#6642) (#6845)
Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-08 12:13:50 +05:00
Ammar Ahmed
faae68928b mobile: fix localization 2024-11-07 12:23:36 +05:00
01zulfi
21ada0e2d2 web: fix image preview opening as PDF (#6822) (#6843)
Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-07 11:33:57 +05:00
Abdullah Atta
2337b8cd85 web: fix notebook tests 2024-11-06 16:14:39 +05:00
Abdullah Atta
52c8d0b86c desktop: do not use tsc for building desktop app 2024-11-06 16:00:44 +05:00
Abdullah Atta
ce98cea4e7 desktop: update lockfile 2024-11-06 15:42:27 +05:00
Abdullah Atta
b66a093cc2 web: fix missing imports in importer.tsx 2024-11-06 15:38:00 +05:00
Abdullah Atta
a3e5ae4cf5 desktop: use tsup for builds 2024-11-06 15:03:49 +05:00
Abdullah Atta
cddd08c32d desktop: use nodenext module resolution to fix could not resolve errors 2024-11-06 14:51:26 +05:00
Ammar Ahmed
5c3ad96cbe mobile: check item restored successfully 2024-11-06 14:22:30 +05:00
01zulfi
55b441d14e web: fix restore notebooks over limit for free users (#6812)
Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-06 14:22:30 +05:00
Ammar Ahmed
f740a211ce Fix selection scrolls to incorrect position on focus and press enter on mobile (#6838)
* mobile: fix keep-in-view overscrolling

* mobile: fix editor status flickers too much

* mobile: fix setting startingOffset

* mobile: set sticky state if not set already
2024-11-06 13:14:14 +05:00
Ammar Ahmed
3cf00cb0f5 mobile: allow user to cancel 2fa auth 2024-11-06 11:04:08 +05:00
Ammar Ahmed
5659576ce3 mobile: fix vault does not show error toast on incorrect password 2024-11-06 10:33:49 +05:00
Abdullah Atta
55842d44ea chore: set type to Feature for feature requests
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-11-06 08:39:53 +05:00
Abdullah Atta
43238692e4 chore: remove thecodrr as default issue assignee
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-11-06 08:39:02 +05:00
01zulfi
fa1f64f505 web: toc improvements (#4977) (#6818)
1. ToC open/close state is persisted
2. ToC is resizeable
3. ToC no longer covers note contents
4. Increase ToC indentations

Signed-off-by: Zulfiqar Ali <01zulfiqarali@gmail.com>
2024-11-05 16:21:40 +05:00
Abdullah Atta
25e3d56435 intl: restore vaultLocked 2024-11-05 15:43:00 +05:00
01zulfi
8722524c98 web: show vault unlock indication in status bar (#6830)
* web: add vault in status bar
* add vault lock functionality for user
* add new vault EVENTS

Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-05 15:35:02 +05:00
Abdullah Atta
c9b1b42461 desktop: fix Could not resolve "@notesnook/intl/dist/locales/$en.json" error 2024-11-05 13:07:31 +05:00
Ammar Ahmed
fa54f7e7f7 intl: fix localization 2024-11-05 10:14:00 +05:00
Ammar Ahmed
26d1de0620 intl: fix localization 2024-11-04 16:13:02 +05:00
01zulfi
b21548780a web: fix note title not focusing until clicked twice (#6767)
Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-04 11:39:08 +05:00
Ammar Ahmed
c8ba9266a7 intl: fix localization 2024-11-04 10:01:45 +05:00
Abdullah Atta
9e9ef15737 web: add quick checkout (#6811)
* web: add quick checkout

* web: allow changing checkout locale during testing
2024-11-02 12:44:50 +05:00
Muhammad Ali
4d7d2fbf31 web: add confirm password when changing app lock pin/password (#6808) 2024-11-02 12:14:39 +05:00
Abdullah Atta
b7ddae6950 core: fix invalid object-ids generated on desktop 2024-10-28 15:58:45 +05:00
Abdullah Atta
1d60f2132d docs: update analytics script 2024-10-28 08:27:09 +05:00
Abdullah Atta
e9e4a76ac2 ci: publish versioned monograph docker image 2024-10-26 15:47:24 +05:00
Abdullah Atta
4d3a2cc574 monograph: bump version to 1.1.0 2024-10-26 15:44:10 +05:00
Abdullah Atta
c516c8a0ba monograph: fix theme not loading in dev 2024-10-26 15:43:31 +05:00
Abdullah Atta
24d5a0746b monograph: cache storage keys for 5 minutes 2024-10-26 15:43:20 +05:00
Abdullah Atta
dee6c86d15 monograph: add table of contents on mobile 2024-10-26 15:43:03 +05:00
Abdullah Atta
33e64416f3 monograph: fix process is not defined 2024-10-26 15:42:42 +05:00
Abdullah Atta
a74637e9c0 monograph: load i18n for editor 2024-10-26 15:41:55 +05:00
Muhammad Ali
ccd1b6d69b editor: fix editing note link replaces link text with note name (#6768) 2024-10-25 09:27:53 +05:00
Muhammad Ali
3750dd38dc editor: fix some math glyps not rendering correctly (#6762) 2024-10-24 11:47:13 +05:00
Ammar Ahmed
1506cc257d intl: update locale 2024-10-19 14:05:18 +05:00
Ammar Ahmed
fe7c7889e8 mobile: fix file upload checks 2024-10-19 14:05:04 +05:00
Ammar Ahmed
3f54eee11d mobile: fix localization 2024-10-19 14:04:16 +05:00
Ammar Ahmed
11b5922c2c mobile: use pseudo-LOCALE in debug 2024-10-19 11:49:53 +05:00
Ammar Ahmed
71b85d2a4d intl: fix strings 2024-10-19 11:46:50 +05:00
Ammar Ahmed
289832d166 mobile: wrap script manager in try/catch 2024-10-19 11:08:53 +05:00
Ammar Ahmed
a88e612c35 editor: fix table popup ui on mobile 2024-10-17 13:44:16 +05:00
Ammar Ahmed
bda456362b mobile: fix cache dir does not exist error on bulkExist check 2024-10-17 12:02:00 +05:00
Abdullah Atta
f6f644c55d editor: fix queueMicrotask causing hang 2024-10-17 11:13:05 +05:00
Ammar Ahmed
08c92333d6 global: fix discord community url in github issue templates
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-10-17 11:11:01 +05:00
Muhammad Ali
4d72b77f29 web: fix right side panel of settings is not scrollable (#6716) 2024-10-17 11:08:46 +05:00
Ammar Ahmed
4f9d06737f mobile: fix repack not loading async chunks 2024-10-16 16:03:47 +05:00
Ammar Ahmed
bfc494a3d4 mobile: fix copy/share buttons get stuck sometimes with no error thrown (#6708)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-10-16 16:03:08 +05:00
Muhammad Ali
e3b004d35f docs: add help for app lock (#6703) 2024-10-16 11:07:01 +05:00
Ammar Ahmed
bd05ad6548 mobile: fix single file export on ios 2024-10-14 12:59:45 +05:00
Ammar Ahmed
ffe068dbd5 global: fix localization 2024-10-14 12:55:08 +05:00
Abdullah Atta
b1f21c1abe desktop: fix type errors importing @notesnook/intl 2024-10-14 11:35:05 +05:00
Abdullah Atta
b494efb8ce web: fix cannot read property instance of undefined 2024-10-14 11:35:05 +05:00
Ammar Ahmed
99ec45ec2a web: fix google-keep importer link 2024-10-14 09:46:11 +05:00
Abdullah Atta
9cea027506 monograph: use top-level await to initialize svg2png-wasm 2024-10-11 09:54:12 +05:00
Abdullah Atta
8bfc158c51 monograph: fix Already initialized. The initialize function can be used only once. 2024-10-11 09:42:52 +05:00
Abdullah Atta
d6ed99282d monograph: only check for success if an object in kv 2024-10-10 23:27:02 +05:00
Abdullah Atta
ac92f3685f monograph: add /api/og endpoint redirection for old og links 2024-10-10 23:16:43 +05:00
Abdullah Atta
05fea49baf monograph: fix kv read not working 2024-10-10 22:55:30 +05:00
Abdullah Atta
ed16143a2c monograph: fix Use yyyy instead of YYYY`` 2024-10-10 16:00:11 +05:00
Abdullah Atta
6618a21a63 monograph: add more meta tags and social image 2024-10-10 15:54:27 +05:00
Abdullah Atta
5b472748a1 monograph: fix uncolored text on initial load 2024-10-10 14:25:11 +05:00
Abdullah Atta
a117aab558 monograph: add missing links in buttons on landing page 2024-10-10 14:14:33 +05:00
Abdullah Atta
ad2ab03be7 monograph: fix og image url on landing page 2024-10-10 14:08:56 +05:00
Abdullah Atta
514112c077 ci: use latest tag when publishing monograph 2024-10-10 13:10:07 +05:00
Abdullah Atta
6dd939af5b monograph: add health check endpoint 2024-10-10 13:09:53 +05:00
Abdullah Atta
82f9c907be ci: disable qemu for monograph build 2024-10-10 12:50:28 +05:00
Abdullah Atta
3cd6c6a371 intl: use pseudo locale in development 2024-10-10 12:35:58 +05:00
Abdullah Atta
8437394891 intl: add more strings 2024-10-10 12:35:58 +05:00
Abdullah Atta
29cb444746 web: fix ui for longer language strings 2024-10-10 12:35:58 +05:00
Abdullah Atta
41eb22b5ca desktop: use strings from @notesnook/intl 2024-10-10 12:35:58 +05:00
Abdullah Atta
8577c32c1f editor: use strings from @notesnook/intl 2024-10-10 12:35:58 +05:00
Abdullah Atta
3e55492318 web: use strings from @notesnook/intl in more places 2024-10-10 12:35:58 +05:00
Abdullah Atta
bb53b0167b mobile: fix typo 2024-10-10 12:35:58 +05:00
Abdullah Atta
ec07c402c9 intl: add new strings 2024-10-10 12:35:58 +05:00
Abdullah Atta
cb50bccbeb intl: use vite for building 2024-10-10 12:35:58 +05:00
Ammar Ahmed
bf7c7ebc82 mobile: fix build 2024-10-10 12:35:58 +05:00
Ammar Ahmed
1184c9f506 web: localize 2024-10-10 12:35:58 +05:00
Ammar Ahmed
f781aaa6f5 web: localize 2024-10-10 12:35:58 +05:00
Ammar Ahmed
e78b245eea web: localize 2024-10-10 12:35:58 +05:00
Ammar Ahmed
bb1ec22197 intl: update strings 2024-10-10 12:35:58 +05:00
Ammar Ahmed
1fff0f6a1c web: localize 2024-10-10 12:35:58 +05:00
Ammar Ahmed
d28d34fad1 mobile: add missing localizations 2024-10-10 12:35:58 +05:00
Ammar Ahmed
40ae83dcad mobile: localize 2024-10-10 12:35:58 +05:00
Ammar Ahmed
0a18f43d95 mobile: localize new strings 2024-10-10 12:35:58 +05:00
Ammar Ahmed
c639bfb227 mobile: fix localize errors 2024-10-10 12:35:58 +05:00
Ammar Ahmed
014866c330 mobile: localize 2024-10-10 12:35:58 +05:00
Ammar Ahmed
dfe7b2e4a6 mobile: localize 2024-10-10 12:35:58 +05:00
Ammar Ahmed
f371c05446 mobile: localize 2024-10-10 12:35:58 +05:00
Ammar Ahmed
d31f6688f8 mobile: migrate more strings 2024-10-10 12:35:58 +05:00
Ammar Ahmed
3e5b740ef0 mobile: migrate app to intl 2024-10-10 12:35:58 +05:00
Ammar Ahmed
f282265fbb intl: initial setup 2024-10-10 12:35:58 +05:00
Abdullah Atta
05df9b77ed ci: add monograph publishing workflow 2024-10-02 15:27:24 +05:00
Abdullah Atta
69ba20009c monograph: add support for self hosting 2024-10-02 15:27:24 +05:00
Abdullah Atta
9bf490bcb4 monograph: use public url 2024-10-02 15:27:24 +05:00
Abdullah Atta
5a40080347 monograph: publish source code 2024-10-02 15:27:24 +05:00
Abdullah Atta
f42155ca5d core: use math.random instead of crypto random in object-id 2024-10-01 16:17:36 +05:00
Abdullah Atta
3b06450113 editor: fix @emotion/react version 2024-10-01 16:16:43 +05:00
Abdullah Atta
2fb3c3a02b ui: get rid of @mdi/react 2024-10-01 16:15:53 +05:00
Abdullah Atta
c8648659c7 theme: add support for setting default theme via global variable 2024-10-01 16:13:35 +05:00
Abdullah Atta
83a663da2b editor: render react widgets inside queueMicrotask 2024-10-01 16:13:06 +05:00
Abdullah Atta
5f0d14f01c web: fix motion.create is not a function 2024-09-25 14:41:25 +05:00
Ammar Ahmed
069ec39de8 mobile: fix foreground service permission type 2024-09-25 14:20:02 +05:00
Ammar Ahmed
bef06a2a20 mobile: change foreground service type to dataSync 2024-09-25 12:44:17 +05:00
Ammar Ahmed
1a98fb8996 mobile: release v3.0.18 2024-09-25 11:02:09 +05:00
Abdullah Atta
dc15bf75c6 editor: fix framer-motion deprecation warning 2024-09-25 10:55:55 +05:00
Abdullah Atta
7357422053 web: bump version to 3.0.18 2024-09-25 10:53:58 +05:00
Abdullah Atta
7918c089d3 desktop: update packages and electron 2024-09-25 10:53:30 +05:00
Ammar Ahmed
ba50b75c54 mobile: fix download file overwrite error 2024-09-25 10:43:26 +05:00
Ammar Ahmed
0d7f32bdd0 mobile: fix attachment recheck ui 2024-09-25 10:39:51 +05:00
Ammar Ahmed
8251e9437b mobile: scope recheck state to current note 2024-09-25 10:39:51 +05:00
Ammar Ahmed
416d5aef90 mobile: progress for note attachments 2024-09-25 10:39:51 +05:00
Abdullah Atta
edb2e3a0ed web: add a couple more tests for basic plan 2024-09-25 10:00:37 +05:00
Abdullah Atta
64031a7814 web: handle errors during tag/color edit 2024-09-24 14:00:32 +05:00
Abdullah Atta
1bd3c3d0f4 web: change module resolution to Bundler 2024-09-24 13:44:44 +05:00
Abdullah Atta
21b6fc4982 editor: export tool definitions and other utils independently 2024-09-24 13:44:31 +05:00
Abdullah Atta
b881023e7b core: update @streetwriters/showdown 2024-09-24 13:25:37 +05:00
Abdullah Atta
6387156596 common: retain original error stack during export 2024-09-24 13:25:37 +05:00
Abdullah Atta
2b2cfe9d33 web: do not zip note without attachments on export 2024-09-24 13:25:37 +05:00
Ammar Ahmed
e81580024e mobile: fix build failing 2024-09-24 13:14:35 +05:00
Ammar Ahmed
8b207b47dd mobile: fix app state 2024-09-24 13:14:34 +05:00
Ammar Ahmed
15aab4ec29 mobile: fix attachments progress 2024-09-24 13:14:34 +05:00
Ammar Ahmed
bdb0474645 editor: fix paragraph not inserted at the end of some nodes 2024-09-24 13:14:34 +05:00
Abdullah Atta
8d25d03390 web: update note export test snapshots 2024-09-24 12:28:02 +05:00
Abdullah Atta
ea2c7d2ea9 web: make markdown rendered container a div instead of span 2024-09-24 12:26:28 +05:00
theProton
0ff38c7ca3 web: fix text rendering in bug report confirmation dialog (#6573)
Signed-off-by: parthiv-m <parthivmenon.dev@gmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2024-09-24 12:25:23 +05:00
Abdullah Atta
595f2e9ba7 web: do not ask for confirmation when deleting a single tag 2024-09-24 12:19:39 +05:00
Abdullah Atta
b02bf3d4ad web: update failing tests snapshots 2024-09-24 12:19:25 +05:00
Abdullah Atta
1e295d8562 web: fix reminder cron expression validation 2024-09-24 12:19:12 +05:00
Abdullah Atta
27a0aeb170 web: fix found 0 byte sized chunk error on attaching files 2024-09-23 19:30:40 +05:00
Abdullah Atta
7e355c3ecb crypto: @notesnook/sodium no longer requires top-level await 2024-09-23 19:30:18 +05:00
Abdullah Atta
69468487c2 core: note title reset on updating a note 2024-09-23 17:26:50 +05:00
Abdullah Atta
0809b5b701 web: fix rotate direction of update check icon 2024-09-23 16:12:12 +05:00
Muhammad Ali
171f28f7c0 web: fix markdown shortcuts toggle reset on app restart (#6555) 2024-09-23 16:09:32 +05:00
Abdullah Atta
0a2af0ec7d global: publish packages 2024-09-23 15:47:09 +05:00
Abdullah Atta
c53354c368 editor: create languages directory if it doesn't exist 2024-09-23 15:46:22 +05:00
Abdullah Atta
be414f869c common: use local @notesnook/core package 2024-09-23 15:42:22 +05:00
Abdullah Atta
de40285e21 core: create languages directory if it doesn't exist 2024-09-23 15:37:58 +05:00
Abdullah Atta
a7fa0bde00 global: publish packages 2024-09-23 15:32:14 +05:00
Abdullah Atta
19c2d2007f core: fix imports in tests 2024-09-23 15:32:13 +05:00
Abdullah Atta
ecc9736b90 global: use tsup for bundling packages 2024-09-23 15:32:13 +05:00
Abdullah Atta
ffd022cfa6 mobile: fix imports 2024-09-23 15:32:13 +05:00
Abdullah Atta
89aba00aac web: fix imports 2024-09-23 15:32:13 +05:00
Abdullah Atta
bcc739aa57 setup: improve publish script 2024-09-23 15:32:13 +05:00
Abdullah Atta
fc1015b3fb setup: increase cleanup script concurrency 2024-09-23 15:32:13 +05:00
Abdullah Atta
f1fa32c7f5 ui: fix element type is invalid error when using icon 2024-09-23 15:32:13 +05:00
Abdullah Atta
a1e794c583 editor(mobile): fix imports and make build cross platform 2024-09-23 15:32:13 +05:00
Abdullah Atta
c787ef29d6 editor: include esm & cjs variants in build 2024-09-23 15:32:13 +05:00
Abdullah Atta
7c257cfbd2 editor: import & use language grammars from refractor directly 2024-09-23 15:32:13 +05:00
Abdullah Atta
99c77e6c5f editor: fix crash when inserting a code block 2024-09-23 15:32:12 +05:00
Abdullah Atta
f060c65bd5 editor: make imports esm compatible 2024-09-23 15:32:12 +05:00
Abdullah Atta
22559cdf42 common: include esm and cjs variants in build 2024-09-23 15:32:12 +05:00
Abdullah Atta
1fb86cb27b common: make imports esm compatible 2024-09-23 15:32:12 +05:00
Abdullah Atta
7f31066e8f core: include cjs and esm variants in build 2024-09-23 15:32:12 +05:00
Abdullah Atta
50e418dc53 core: use language grammars directly from refractor 2024-09-23 15:32:12 +05:00
Abdullah Atta
59739067b6 core: make imports compatible with esm 2024-09-23 15:32:12 +05:00
Abdullah Atta
f100e157d3 server(themes): upgrade @trpc/server 2024-09-23 15:32:11 +05:00
Abdullah Atta
870ce259f0 theme: include both esm and cjs variants in build 2024-09-23 15:32:11 +05:00
Abdullah Atta
95b50eb5ca theme: make imports esm compatible 2024-09-23 15:32:11 +05:00
Abdullah Atta
f243d13b81 ui: include both esm and cjs variants in build 2024-09-23 15:32:11 +05:00
Abdullah Atta
749a13efbc ui: make imports esm compatible 2024-09-23 15:32:11 +05:00
Abdullah Atta
275d5d7f04 fs: include both esm and cjs variants in build 2024-09-23 15:32:11 +05:00
Abdullah Atta
54de071e85 fs: make imports esm compatible 2024-09-23 15:32:11 +05:00
Abdullah Atta
9647e2513b crypto(sodium): include both esm and cjs variants in build 2024-09-23 15:32:11 +05:00
Abdullah Atta
239781eabd crypto: make imports esm compatible 2024-09-23 15:32:11 +05:00
Abdullah Atta
21573d6cc8 logger: include both esm and cjs variants in build 2024-09-23 15:32:11 +05:00
Abdullah Atta
9b091f0bb0 logger: make imports esm compatible 2024-09-23 15:32:11 +05:00
Abdullah Atta
e0beef745e crypto: include both esm & cjs variants in build 2024-09-23 15:32:11 +05:00
Abdullah Atta
2f2653d223 crypto: make imports esm compatible 2024-09-23 15:32:11 +05:00
Abdullah Atta
7458aa53ae clipper: make imports esm compatible 2024-09-23 15:32:11 +05:00
Ammar Ahmed
f7db306c26 mobile: fix showing toast at bottom always 2024-09-23 13:47:58 +05:00
Ammar Ahmed
7e8d6f2041 mobile: make sure toast moves with keyboard 2024-09-23 13:47:58 +05:00
Ammar Ahmed
d424a6c48f mobile: fix size of text in toast 2024-09-23 13:47:58 +05:00
Ammar Ahmed
7293224514 mobile: fix toast ui in diff cond 2024-09-23 13:47:58 +05:00
Ammar Ahmed
7706cfed9a mobile: new toast 2024-09-23 13:47:58 +05:00
Ammar Ahmed
bbfe0b237d editor: fix duplicate block-id when pasting content from note 2024-09-23 12:49:08 +05:00
Ammar Ahmed
2c7cddd853 mobile: do not zip note without attachments 2024-09-23 12:48:17 +05:00
Ammar Ahmed
37c1d39c41 mobile: fix statusbar flicker on launch app 2024-09-23 12:47:39 +05:00
Ammar Ahmed
5d4960a26f mobile: fix foregroundServiceType 2024-09-23 12:12:09 +05:00
Ammar Ahmed
78f07b23df mobile: fix boot service crash on boot 2024-09-23 12:12:09 +05:00
Ammar Ahmed
7aeb047b50 mobile: improve attachments manager 2024-09-23 12:11:26 +05:00
Abdullah Atta
0e3a661dd0 desktop: delete app data on uninstall (fixes #5209) 2024-09-16 14:25:50 +05:00
Abdullah Atta
fba350ed1e editor: refactor toggleCodeBlock 2024-09-16 12:34:25 +05:00
Sean-Wyn Ng
76bfbc7316 desktop: fix issues with codeblock toggling (#6435)
Signed-off-by: Sean-Wyn Ng <seanwynng@gmail.com>
Co-authored-by: seanwynwins <shidderwinsgit.hcma2@slmail.me>
2024-09-16 12:26:20 +05:00
Abdullah Atta
7683d8fa5b core: fix crash when searching 2024-09-16 11:49:49 +05:00
Abdullah Atta
3fe5352b56 core: resort date formats and add DD.MM.YYYY format (fixes #4744) 2024-09-13 15:39:09 +05:00
Abdullah Atta
a86d9a9c0c web: default notebook always assigned to note (fixes #5861) 2024-09-13 14:35:38 +05:00
Ammar Ahmed
a68f74bc1b core: fix locking notes with empty content (#6545)
* core: fix locking notes with empty content

* core: lock note and return if rawContent already locked

* core: update comment

* core: allow locking an empty note

* core: updates

* core: updates

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-09-13 12:18:55 +05:00
Muhammad Ali
18310698f6 editor: add ctrl+k to create link shortcut (#3501)
* editor: added link pop-up keyboard short-cut

* editor: Ctrl+K to create link

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-09-13 11:10:50 +05:00
Abdullah Atta
020244231f desktop: update electron to v30 (#6498) 2024-09-13 08:39:18 +05:00
Abdullah Atta
59085d7b19 editor: update packages 2024-09-13 08:37:03 +05:00
Muhammad Ali
c15410e168 editor: press enter in link popup to insert link (#4423)
* editor: added shortcut key for link-popup completion.

* editor: remove commented out code

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-09-12 15:46:26 +05:00
Muhammad Ali
2962f13457 editor: exit a link by pressing space (#3122)
* editor: added keyboard shortcut for handling space

* editor: simplify

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-09-12 15:06:13 +05:00
Muhammad Ali
bfc77fc9a4 web: add delete key shortcut for deleting items (#1655)
* web: added delete key short cut for trashing notes

* web: added del key shortcut for tags, topics,trash, reminders, attachments

* web: handle delete key in sub notebooks

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-09-12 14:55:04 +05:00
Abdullah Atta
7e88ec88ee core: search query should be AND by default instead of OR 2024-09-12 12:54:16 +05:00
Abdullah Atta
9ef07f22af core: ignore diacritics when searching (requires rebuilding search index) 2024-09-12 12:54:16 +05:00
Ammar Ahmed
268343be3e mobile: fix android crash when app removed from recents 2024-09-12 11:40:16 +05:00
Abdullah Atta
956e5373d1 web: ignore Error in input stream global error 2024-09-11 20:45:57 +05:00
Abdullah Atta
919fa9bdf1 web: fix invalid filename when saving recovery key (fixes #6436) 2024-09-11 15:04:31 +05:00
Abdullah Atta
e06192c5b4 core: update @streetwriters/showdown (fixes #4470) 2024-09-11 14:59:07 +05:00
Ammar Ahmed
b5be5e0b59 mobile: fix itemExists error 2024-09-11 14:12:56 +05:00
Abdullah Atta
05695cfcc8 web: add menu items to open, view & unpublish a monograph (fixes #4812) 2024-09-11 13:56:18 +05:00
Abdullah Atta
423e1c6816 web: update features list (fixes #6514) 2024-09-11 13:33:35 +05:00
Ammar Ahmed
6db1cf5cc1 clipper: add images only for pro users (#6528) 2024-09-11 13:26:57 +05:00
Ammar Ahmed
2e4a87abd9 core: fix export title has appended Notesnook to end (#6529) 2024-09-11 13:26:20 +05:00
Ammar Ahmed
c26988134f editor: fix heading and p gap (#6530)
* editor: fix heading and p gap

* Update packages/editor/styles/styles.css

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

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-09-11 13:24:58 +05:00
Abdullah Atta
bcffdd0f91 editor: improve toolbar popup controls 2024-09-11 13:22:53 +05:00
Abdullah Atta
de1051d52d editor: remove unnecessary useMemo dependency 2024-09-11 13:22:53 +05:00
Abdullah Atta
b1a360e445 editor: add more neutral default colors for highlight 2024-09-11 13:22:53 +05:00
Abdullah Atta
49e85dfa87 editor: position color picker according to mouse position 2024-09-11 13:22:53 +05:00
Abdullah Atta
452ff12071 editor: use ref instead of document.getElementById 2024-09-11 13:22:53 +05:00
Muhammad Ali
1b6fc40633 editor: fix input is not editable in color picker 2024-09-11 13:22:53 +05:00
Ammar Ahmed
82b28f8263 mobile: fix pricingPhases undefined error 2024-09-10 14:02:41 +05:00
Abdullah Atta
6e47eae30a web: fix error when registering task with invalid time (fixes #4890) 2024-09-10 09:41:16 +05:00
Abdullah Atta
4040f2ecc4 common: fix notes with empty title exporting with empty filename (fixes #6088) 2024-09-10 09:36:55 +05:00
Abdullah Atta
2293866c61 core: fix note title can be set to empty 2024-09-10 09:27:38 +05:00
Ammar Ahmed
2cb4a10076 clipper: add safe querySelector method 2024-09-09 14:57:49 +05:00
Abdullah Atta
2f7b8d3377 web: add clear instructions on how to import from other apps 2024-09-09 13:14:17 +05:00
Abdullah Atta
983220b2f2 web: add support for importing nested notebooks 2024-09-09 13:13:46 +05:00
Ammar Ahmed
ca2848e491 mobile: release v3.0.17 2024-09-06 12:58:36 +05:00
Ammar Ahmed
48e5c4d908 mobile: fix crash in background on boot 2024-09-06 12:33:34 +05:00
Ammar Ahmed
5774dd8c60 mobile: fix crash on opening app from tile 2024-09-06 12:33:00 +05:00
Steve Divskinsy
9d65184f42 docs(help): Fix typo in is-there-an-eta.md
docs(help): Fix typo in is-there-an-eta.md - unnecessary was incorrectly spelled

Signed-off-by: Stephen Argent <stevesbrain@users.noreply.github.com>

Signed-off-by: Steve Divskinsy <stevesbrain@users.noreply.github.com>
2024-09-05 20:10:54 +05:00
Abdullah Atta
395b2750e8 web: increase table viewport by 300px 2024-09-05 09:45:58 +05:00
Abdullah Atta
3bfb1d57a5 web: fix attachment downloading stuck on error 2024-09-05 09:45:44 +05:00
Abdullah Atta
e59b432007 web: ensure file exists & is of the right size after encryption 2024-09-05 09:41:06 +05:00
Abdullah Atta
efbc566386 web: compare uploaded file size with actual file size after upload 2024-09-05 09:40:45 +05:00
Abdullah Atta
2576f1dd00 web: handle errors in attachment manager 2024-09-05 09:39:29 +05:00
Abdullah Atta
7d9388bd27 web: check select all checkbox when all attachments are selected 2024-09-05 09:38:57 +05:00
Abdullah Atta
1979cd2ea4 web: show progress when rechecking attachments in bulk 2024-09-05 09:38:29 +05:00
Abdullah Atta
7bd5dfb772 web: add support for configuring server urls from login screen 2024-09-04 11:07:23 +05:00
Abdullah Atta
ac7c88218b core: fix custom server urls not working in production 2024-09-04 11:06:38 +05:00
Abdullah Atta
fb642233c8 docs(help): add xkcd comic just for kicks and laughs 2024-09-04 09:36:22 +05:00
Abdullah Atta
6e2a19d7e0 docs(help): add "is there an eta for X feature" FAQ 2024-09-04 09:20:02 +05:00
Abdullah Atta
4d4993c570 docs(help): improve merge conflicts documentation 2024-09-04 09:10:35 +05:00
Abdullah Atta
916e097cb3 clipper(web): bump version to v0.3.4 2024-09-03 11:52:53 +05:00
Abdullah Atta
6def76e1bb web: bump version to 3.0.17 2024-09-03 10:39:09 +05:00
Ammar Ahmed
d93ec27c9e mobile: release v3.0.16 2024-09-02 16:29:13 +05:00
Abdullah Atta
15329597a5 core: fix notes search for queries less than 3 letters (#6489) 2024-09-02 15:29:18 +05:00
Abdullah Atta
49caf23436 web: show loader when loading next page in themes 2024-09-02 14:58:13 +05:00
Abdullah Atta
e6d8401c56 desktop: fix multiple updates running simultaneously 2024-09-02 14:21:13 +05:00
Abdullah Atta
11a5e8c180 core: delete all relations with fromId/toId set to null 2024-09-02 14:20:58 +05:00
Abdullah Atta
5bd7a081c1 core: do not allow invalid reference to be added in relations 2024-09-02 14:20:58 +05:00
Abdullah Atta
a740a5db81 clipper(web): fix tags not assigned to clip 2024-09-02 14:20:58 +05:00
Abdullah Atta
a61f2efacf web: fix type errors 2024-09-02 13:43:05 +05:00
Brogio
51a253f3e2 mobile: fix typo in settings (#6485)
Fix typo

Signed-off-by: Brogio <contact@brog.io>
2024-09-02 12:18:49 +05:00
Abdullah Atta
19ed753079 clipper(web): only clip images if user is premium 2024-08-29 12:56:10 +05:00
Abdullah Atta
4a85c49f5d desktop: remove sodium-native package for now 2024-08-28 15:08:52 +05:00
Abdullah Atta
16dd4b3359 web: bump version to 3.0.16 2024-08-28 14:45:31 +05:00
Abdullah Atta
9279c671ac desktop: fix ft.dirname is not a function for "good" 2024-08-28 14:44:40 +05:00
Abdullah Atta
225b21977e crypto: remove libsodium-sumo patch 2024-08-28 13:46:29 +05:00
Abdullah Atta
026b9212c4 web: bump version to v3.0.15 2024-08-28 13:33:56 +05:00
Abdullah Atta
5ce59c6125 desktop: fix ft.dirname is not a function error 2024-08-28 13:32:38 +05:00
Ammar Ahmed
b7c81a956c mobile: release v3.0.15 2024-08-28 12:50:12 +05:00
Abdullah Atta
873f976fd3 crypto: fix ft.dirname is not a function 2024-08-28 12:49:14 +05:00
Ammar Ahmed
7d8a16c463 mobile: make sure settings get reset properly on logout 2024-08-28 10:29:05 +05:00
Ammar Ahmed
f5841985fa mobile: hide select folder button on ios 2024-08-28 10:29:05 +05:00
Ammar Ahmed
da86cbbe32 mobile: fix password input not focused automatically on login 2024-08-28 10:29:05 +05:00
Ammar Ahmed
ed115d3f9e mobile: set encrypted backups on signup 2024-08-28 10:29:05 +05:00
Ammar Ahmed
e52e74523a mobile: fix boot task 2024-08-28 10:29:05 +05:00
Ammar Ahmed
7e43c3b373 mobile: fix background sync 2024-08-28 10:29:05 +05:00
Ammar Ahmed
e6db186b62 mobile: fix automatic backups 2024-08-28 10:29:05 +05:00
Ammar Ahmed
998e62ade7 mobile: fix file picker does not attach file when tabId=0 2024-08-28 10:29:05 +05:00
Abdullah Atta
ac16bc74fc web: fix cache file store 2024-08-27 18:07:42 +05:00
Abdullah Atta
2aee26b082 web: restore attachment files even if they are already uploaded 2024-08-27 18:07:27 +05:00
Abdullah Atta
a188920d96 web: show but disable automatic backup settings for basic users 2024-08-27 18:07:11 +05:00
Abdullah Atta
1f3defc28c web: bump version to 3.0.14 2024-08-27 16:20:12 +05:00
Ammar Ahmed
7aeeabfb3a mobile: run sync on startup 2024-08-27 16:06:11 +05:00
Abdullah Atta
6641d3e625 web: prevent multiple simultaneous syncs 2024-08-27 16:06:11 +05:00
Abdullah Atta
72031617eb web: run sync immediately on init 2024-08-27 16:06:11 +05:00
Abdullah Atta
78090b700b web: remove debouncer for page visibility and connection changes 2024-08-27 16:06:11 +05:00
Abdullah Atta
f2f8d81b98 core: do not run sync before connecting sse 2024-08-27 16:06:11 +05:00
Abdullah Atta
84d68ca67b core: add more tests 2024-08-27 16:04:51 +05:00
Abdullah Atta
6368e47712 clipper(web): bump version to 0.3.3 2024-08-27 13:23:37 +05:00
Abdullah Atta
0f17b01a74 desktop: disable native crypto 2024-08-27 13:07:09 +05:00
Abdullah Atta
025e23356a editor: fix crash on drag dropping an embed node 2024-08-27 13:03:20 +05:00
Muhammad Ali
ac85002dff editor: fix crash when adding tasklist using md in heading or other nodes (#5923)
* editor: fixed adding tasklist using md in heading

* editor: reset nodes before converting them to a task list

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-08-27 12:49:19 +05:00
ChampionBuffalo
3a23e4fbaf web: Fix isNarrow overriding user-preference of navpane
Signed-off-by: ChampionBuffalo <49675578+ChampionBuffalo1@users.noreply.github.com>
2024-08-27 12:36:48 +05:00
Muhammad Ali
3547663af8 docs: add merge-conflicts file to help (#6420)
* Update merge-conflicts.md

edited merge conflict file

Signed-off-by: Muhammad Ali <alihamuh@gmail.com>

* Update merge-conflicts.md

Signed-off-by: Muhammad Ali <alihamuh@gmail.com>

* Update merge-conflicts.md

Signed-off-by: Muhammad Ali <alihamuh@gmail.com>

* Update merge-conflicts.md

Signed-off-by: Muhammad Ali <alihamuh@gmail.com>

* Update merge-conflicts.md

Signed-off-by: Muhammad Ali <alihamuh@gmail.com>

* Update merge-conflicts.md

Signed-off-by: Muhammad Ali <alihamuh@gmail.com>

* Update merge-conflicts.md

Signed-off-by: Muhammad Ali <alihamuh@gmail.com>

* Update merge-conflicts.md

Signed-off-by: Muhammad Ali <alihamuh@gmail.com>

* docs(help): update merge conflicts

* docs: added merge-conflicts file

---------

Signed-off-by: Muhammad Ali <alihamuh@gmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-08-23 16:43:41 +05:00
Abdullah Atta
c5dfc02085 web: minor refactors 2024-08-21 13:14:17 +05:00
Ammar Ahmed
bb9a6431bf mobile: remove telemetry 2024-08-20 15:08:41 +05:00
Abdullah Atta
624d3d7ce8 web: remove unused analytics import 2024-08-20 15:08:41 +05:00
Abdullah Atta
170b0aa49a web: get rid of all telemetry & analytics code 2024-08-20 15:08:41 +05:00
Muhammad Ali
07cfd74506 Delete docs/help/contents/merge-conflict.md
Signed-off-by: Muhammad Ali <alihamuh@gmail.com>
2024-08-20 14:56:56 +05:00
Muhammad Ali
1d40054ac0 Create merge-conflict.md
Signed-off-by: Muhammad Ali <alihamuh@gmail.com>
2024-08-20 14:56:09 +05:00
Muhammad Ali
5cd37f4b64 Create merge-conflicts.md
Signed-off-by: Muhammad Ali <alihamuh@gmail.com>
2024-08-20 14:53:51 +05:00
Tai Lam
25675f71e8 help: Capitalize all instances of "URL" (#6402)
Capitalize all instances of "URL"

Signed-off-by: Tai Lam <47955724+taivlam@users.noreply.github.com>
2024-08-20 12:55:27 +05:00
Ammar Ahmed
202534fca9 mobile: correctly handle error on backup failed 2024-08-19 10:51:14 +05:00
Ammar Ahmed
a57558d8ab mobile: fix restoring backups 2024-08-19 10:50:44 +05:00
Ammar Ahmed
f1c201dca9 core: fix reset attachment failed status 2024-08-19 10:50:44 +05:00
Ammar Ahmed
a73bff83fa mobile: bump version 2024-08-17 17:59:44 +05:00
Abdullah Atta
e4428299bc web: fix trial expired reminder dialog shown again and again 2024-08-16 18:10:35 +05:00
Abdullah Atta
487567ffbc web: upgrade paddle checkout code 2024-08-16 18:06:07 +05:00
Abdullah Atta
efd83e6de3 web: add debug logging to checkout 2024-08-16 11:22:13 +05:00
Ammar Ahmed
a07230f707 mobile: remove old patches 2024-08-16 10:52:16 +05:00
Abdullah Atta
35a2e11afd web: improve toast shown to basic users on using pro features 2024-08-16 10:28:39 +05:00
Abdullah Atta
cf4a7a3d34 core: limit notebooks to 20 on basic plan 2024-08-16 10:28:39 +05:00
Abdullah Atta
86e661ccd6 core: do not allow creating colors in basic plan 2024-08-16 10:28:39 +05:00
Abdullah Atta
e7be9b35ed core: limit tags to 5 on basic plan 2024-08-16 10:28:39 +05:00
Muhammad Ali
53a367b0b2 clipper(web): fix web clipper not capturing images (#6348)
* clipper: added config settings for images

* clipper(web): enable image clipping

* clipper(web): fix clipper not respecting cors proxy setting

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-08-16 10:28:14 +05:00
Ammar Ahmed
b6206e2ca7 mobile: take backup before logout 2024-08-14 12:53:57 +05:00
Abdullah Atta
a910c819f2 web: fix error text ui 2024-08-14 12:53:57 +05:00
Abdullah Atta
6dd7a4960f web: show warning if there are unsynced changes on logout 2024-08-14 12:53:57 +05:00
Abdullah Atta
c4e15b8f90 web: ask to create backup before logout in all cases 2024-08-14 12:53:57 +05:00
Abdullah Atta
f458a4b86a web: check unsynced changes and ask for backup before logout 2024-08-14 12:53:57 +05:00
Abdullah Atta
8a44a7b99a core: add api for checking unsynced changes 2024-08-14 12:53:57 +05:00
Ammar Ahmed
25065c01a0 mobile: Fix apk size increase in v3.0.13 2024-08-14 12:19:42 +05:00
Abdullah Atta
87ae9e86e7 editor: remove 500ms debounce from image observer 2024-08-14 09:18:26 +05:00
Abdullah Atta
782c53b622 web: impl chunkSize method for faster file size calculation 2024-08-14 09:18:26 +05:00
Abdullah Atta
cb52b390ab fs: add chunkSize method for faster file size calculation 2024-08-14 09:18:26 +05:00
Abdullah Atta
5a49941003 core: cache attachment key for faster attachment reading 2024-08-14 09:18:26 +05:00
Ammar Ahmed
6797b9b621 mobile: prompt before restoring a backup 2024-08-13 15:47:10 +05:00
Ammar Ahmed
031a1b31b4 mobile: fix backup success toast 2024-08-13 15:40:10 +05:00
Ammar Ahmed
4e5762be45 mobile: check server compatible 2024-08-13 11:19:47 +05:00
Abdullah Atta
7a6ba5d1d1 web: check server version compatibility when testing connection 2024-08-13 11:19:47 +05:00
Abdullah Atta
cc3ccd041d core: add function for checking compability with server 2024-08-13 11:19:47 +05:00
Ammar Ahmed
31453dc098 mobile: server configuration settings 2024-08-13 11:19:47 +05:00
Abdullah Atta
2e84099c80 web: fix typo 2024-08-13 11:19:47 +05:00
Abdullah Atta
5bbc0cd452 web: add support for changing server urls 2024-08-13 11:19:47 +05:00
Abdullah Atta
22aae401ba web: improve checks before download & upload of attachment 2024-08-13 11:09:03 +05:00
Ammar Ahmed
97f135cd07 mobile: fix attachment downloading 2024-08-13 11:09:03 +05:00
Abdullah Atta
976f933233 web: check for s3 errors on downloading attachment 2024-08-13 11:09:03 +05:00
Ammar Ahmed
95fe1044d6 mobile: clean temp backup folders and files 2024-08-13 11:09:03 +05:00
Ammar Ahmed
677201e979 mobile: fix wording 2024-08-13 11:09:03 +05:00
Ammar Ahmed
6ec9370afb mobile: add support for attachments cache 2024-08-13 11:09:03 +05:00
Abdullah Atta
bea0f1ab0e core: fix conflicts being shown on non-conflicted notes 2024-08-13 11:09:03 +05:00
Abdullah Atta
5357be4741 web: cancel attachments caching on disabling offline mode 2024-08-13 11:09:03 +05:00
Abdullah Atta
02fdec74ce web: fix bulk exists check for files 2024-08-13 11:09:03 +05:00
Abdullah Atta
dbc907ceeb web: write downloading files to temporary location before moving 2024-08-13 11:09:03 +05:00
Abdullah Atta
7bb53a5e0c fs: add support for moving files 2024-08-13 11:09:03 +05:00
Abdullah Atta
69cb24149d core: fix progress when bulk downloading files 2024-08-13 11:09:03 +05:00
Abdullah Atta
438d412ae9 web: fix "too much recursion" error 2024-08-13 11:09:03 +05:00
Abdullah Atta
1b71edef45 web: change offline mode api 2024-08-13 11:09:03 +05:00
Abdullah Atta
a4c0e8412b fs: add support for listing all files 2024-08-13 11:09:03 +05:00
Abdullah Atta
fbcb8675ca core: optimize attachment caching in offline mode 2024-08-13 11:09:03 +05:00
Ammar Ahmed
fbf5c82d04 mobile: update wording 2024-08-13 11:09:03 +05:00
Ammar Ahmed
fa40c539ed mobile: fix restoring backup files 2024-08-13 11:09:03 +05:00
Ammar Ahmed
f3218fdc31 mobile: fix restoring backup 2024-08-13 11:09:03 +05:00
Ammar Ahmed
d12ed967e3 mobile: add support for offline mode, backup with attachments 2024-08-13 11:09:03 +05:00
Abdullah Atta
b6021dc76b web: fix backup tests for good 2024-08-13 11:09:03 +05:00
Abdullah Atta
f95442a828 web: fix backup tests 2024-08-13 11:09:03 +05:00
Abdullah Atta
846aae312e core: fix backup tests 2024-08-13 11:09:03 +05:00
Abdullah Atta
68fdbcb7fc web: better status reports when taking backups in background 2024-08-13 11:09:03 +05:00
Abdullah Atta
b938c58b67 web: take automatic backups in background 2024-08-13 11:09:03 +05:00
Abdullah Atta
759fba06ae web: add options to create backups with attachments 2024-08-13 11:09:03 +05:00
Abdullah Atta
10e24ae632 web: add support for backup/restore of attachments 2024-08-13 11:09:03 +05:00
Abdullah Atta
341032c884 core: add support for including attachments in backups 2024-08-13 11:09:03 +05:00
Abdullah Atta
aa1fa0ca81 crypto: throw error if chunk decryption fails 2024-08-13 11:09:03 +05:00
Abdullah Atta
398432efde web: add setting to toggle full offline mode 2024-08-13 11:09:03 +05:00
Abdullah Atta
57ddaafc16 core: download all attachments when full offline mode is on 2024-08-13 11:09:03 +05:00
Abdullah Atta
e4a755c69f web: optimize attachment uploading 2024-08-13 11:09:03 +05:00
Abdullah Atta
053f70cdb4 fs: get rid of chunks property in file metadata 2024-08-13 11:09:03 +05:00
Abdullah Atta
cdcdc43cad web: fix encryption progress when attaching files 2024-08-13 11:09:03 +05:00
Abdullah Atta
f1111191b1 clipper: ignore stylesheets that throw cors error 2024-08-13 11:07:20 +05:00
Abdullah Atta
529e2a4dfd clipper(web): move to react 18 2024-08-13 11:07:20 +05:00
Abdullah Atta
d88d07a2b0 core: fix Cannot read properties of null (reading 'length') in migration 2024-08-11 11:48:19 +05:00
Abdullah Atta
197ff36c20 web: only show sync status in placeholder on first sync 2024-08-11 11:19:15 +05:00
Ammar Ahmed
d94474de79 mobile: fix android crash api 34 2024-08-10 16:29:05 +05:00
Ammar Ahmed
fcf0e0c531 mobile: update Podfile.lock 2024-08-10 12:35:34 +05:00
Ammar Ahmed
24048ebfbe mobile: remove USE_FULL_SCREEN_INTENT 2024-08-10 11:57:57 +05:00
Ammar Ahmed
5ae8cdd5b9 mobile: fix build errors 2024-08-10 11:31:37 +05:00
Ammar Ahmed
74187b335f ci: set java 17
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-08-09 15:26:12 +05:00
Ammar Ahmed
16fc689299 mobile: release v3.0.13 2024-08-09 15:01:19 +05:00
Ammar Ahmed
578a9de680 mobile: fix ios build 2024-08-09 12:25:59 +05:00
Ammar Ahmed
62d475b2ef mobile: upgrade to react native 0.74.4 2024-08-09 12:25:59 +05:00
Ammar Ahmed
69c902c6c1 mobile: fix title input changes not detected 2024-08-09 12:25:14 +05:00
Abdullah Atta
c8c1028cbc core: fix least relevant results shown on top 2024-08-09 11:52:55 +05:00
Abdullah Atta
313807c384 web: remove Report an issue button from status bar 2024-08-09 11:47:56 +05:00
Abdullah Atta
5dc0e15094 web: fix last change in editor not getting saved 2024-08-09 10:35:53 +05:00
Ammar Ahmed
12c5b6d227 mobile: release v3.0.12 2024-07-29 10:06:21 +05:00
Ammar Ahmed
364ae51e36 mobile: fix backup share button not working 2024-07-29 09:53:20 +05:00
Ammar Ahmed
18947e0c93 mobile: fix missing fonts in share sheet 2024-07-29 09:43:37 +05:00
François Martin
3880db4339 global: clarify explanations for force push and force pull (#6255)
Signed-off-by: François Martin <f.martin@fastmail.com>
2024-07-29 09:38:30 +05:00
Ammar Ahmed
ae5b0b6fcc mobile: fix share sheet not loading on ios 2024-07-29 09:25:46 +05:00
Abdullah Atta
2c729f90cc global: update lockfiles 2024-07-27 13:30:25 +05:00
Ammar Ahmed
800514d769 mobile: fix position of password input 2024-07-27 12:49:57 +05:00
Ammar Ahmed
b03b9482dc mobile: app stuck after unlock 2024-07-27 12:49:57 +05:00
Abdullah Atta
9019490dbd web: bump version to v3.0.13 2024-07-27 11:16:42 +05:00
Abdullah Atta
4a3e23c288 web: fix reminder can't be set in january 2024-07-27 11:16:09 +05:00
Ammar Ahmed
1800ebea2d mobile: fix keyboard blocks continue button on applock 2024-07-27 11:12:09 +05:00
Ammar Ahmed
a708c9e9eb mobile: fix home shortcut not working 2024-07-27 10:56:09 +05:00
Abdullah Atta
2aeb875fec web: fix crash on moving temporary tab 2024-07-27 10:08:38 +05:00
Abdullah Atta
a8164e9f6c web: enter in title should focus editor 2024-07-27 09:46:37 +05:00
Abdullah Atta
0ee8e738e5 web: fix attachments ui in properties 2024-07-27 09:46:23 +05:00
Abdullah Atta
8fdef2ac7d web: fix attachments flicker in editor note properties 2024-07-27 09:46:23 +05:00
Abdullah Atta
920b1d91f3 web: bump version to 3.0.12 2024-07-22 15:42:18 +05:00
Abdullah Atta
0ad382aced web: fix incorrect attachment upload progress 2024-07-22 15:30:38 +05:00
Abdullah Atta
c8aa8e37cf web: add support for clearing attachments cache 2024-07-22 15:30:21 +05:00
Abdullah Atta
9a37f7c506 web: fix image picker dialog shown for non-image attachments 2024-07-22 13:08:49 +05:00
Abdullah Atta
717750d5d4 web: subscribe to user related events before fetching user 2024-07-22 13:08:36 +05:00
Abdullah Atta
1274ecc3b5 core: refresh token on user subscription change 2024-07-22 13:08:12 +05:00
Abdullah Atta
4c2ad4567c setup: use listr2 2024-07-20 10:34:40 +05:00
Abdullah Atta
ebc01af34a global: publish packages 2024-07-20 10:34:28 +05:00
Abdullah Atta
e66d8eee13 ui: add support for ss 2024-07-20 01:16:46 +05:00
Abdullah Atta
55e6a4b65a editor: do not throw during ssr 2024-07-20 00:42:54 +05:00
Abdullah Atta
0aa1db584d editor: do not keep last line in view for readonly editor 2024-07-19 23:50:20 +05:00
Abdullah Atta
a0e6266d28 ui: fix Cannot access 'mouseMoveListenerAttached' before initialization 2024-07-19 23:26:35 +05:00
Abdullah Atta
bd0aaed863 ui: add support for ssr 2024-07-19 23:22:47 +05:00
Abdullah Atta
9410c42f29 web: fix search icon overlapping route header text 2024-07-19 12:42:18 +05:00
Abdullah Atta
80186c1812 web: fix type errors 2024-07-19 11:11:51 +05:00
Abdullah Atta
37dfda5c54 desktop: set updaterCacheDirName in app-update.yml 2024-07-19 10:52:59 +05:00
Abdullah Atta
ec83517dc5 desktop: fix update check gives no feedback in ui 2024-07-19 10:51:10 +05:00
Abdullah Atta
2eb4911592 web: bump version to 3.0.11 2024-07-18 17:41:26 +05:00
Abdullah Atta
cd0a6a8fcb editor: fix editing link adds a new link right beside it 2024-07-18 17:38:18 +05:00
Abdullah Atta
b15f192244 desktop: update esbuild 2024-07-18 16:47:08 +05:00
Abdullah Atta
406cb663c9 editor: pasting list item with single spaced paragraphs shouldn't break formatting 2024-07-18 12:02:39 +05:00
Abdullah Atta
301a30a50a editor: copying a link from a list item shouldn't add spaces at the end 2024-07-18 12:02:39 +05:00
Abdullah Atta
81e3432143 editor: fix copying multiple lists only copied the first one 2024-07-18 12:02:39 +05:00
Abdullah Atta
c43c2936ac web: use resizeobserver to wait for scroll container to resize 2024-07-18 11:56:48 +05:00
Abdullah Atta
fdea1807d2 web: fix editor scroll bar hiding sometimes on startup 2024-07-18 11:56:48 +05:00
Abdullah Atta
5439578fcf server(theme): add health endpoint 2024-07-17 15:25:52 +05:00
Abdullah Atta
e05e52f870 web: fix crash on sync 2024-07-16 17:18:25 +05:00
Abdullah Atta
1c6243d12e web: fix error on resetting database on crash 2024-07-16 17:18:25 +05:00
Abdullah Atta
a51dfb5b6e web: remove all traces of immer 2024-07-16 17:18:25 +05:00
Abdullah Atta
c98bddd35b web: use sqlite for storing editor session data 2024-07-16 17:18:25 +05:00
Abdullah Atta
1eb80ccc88 web: get rid of dialog controller
dialog controller was redundant and created unnecessary overhead.
It also coupled all the dialogs together so you couldn't import a dialog
independently. This new change isolates each dialog to its own module
while keeping the behaviour mostly the same.
2024-07-16 17:18:25 +05:00
Abdullah Atta
5a346aa7de web: fix tags & colors not updating/loading on opening a note 2024-07-16 17:18:25 +05:00
Abdullah Atta
910095a769 web: replace immer with mutative for perf reasons 2024-07-16 17:18:25 +05:00
Abdullah Atta
119059befa core: update tag on rename instead of upserting 2024-07-16 17:18:25 +05:00
Abdullah Atta
a8f5dea315 core: fix type definition 2024-07-16 17:18:25 +05:00
Abdullah Atta
ca0a4cb02c core: add localStorage like ConfigStorage based on SQLite 2024-07-16 17:18:25 +05:00
Abdullah Atta
a04cd6b8dc core: send event when directly unlinking a relation 2024-07-16 17:18:25 +05:00
Abdullah Atta
3e4ab0f6df setup: update typescript to 5.5 2024-07-16 17:18:25 +05:00
Abdullah Atta
a7fe1aca44 global: update lockfiles 2024-07-16 17:18:25 +05:00
Abdullah Atta
ff0188f602 web: fix app lock verification always succeeds 2024-07-12 14:30:37 +05:00
Ammar Ahmed
c846f8abc8 mobile: release v3.0.11 2024-07-11 12:08:15 +05:00
Abdullah Atta
8d8acfb99d web: fix crash on browsers that doesn't have asyncIterator 2024-07-11 11:55:17 +05:00
Abdullah Atta
3a5b5772ad web: bump version to 3.0.10 2024-07-10 10:44:44 +05:00
Abdullah Atta
8046932e4e web: fix tags & other items lists glitching 2024-07-10 10:43:06 +05:00
Abdullah Atta
c45aa84739 editor: fix pro popup shown on every paste 2024-07-10 09:04:40 +05:00
Abdullah Atta
d98be849e3 web: refresh editor on changing editor options 2024-07-10 09:04:40 +05:00
Abdullah Atta
727ac6d907 web: allow multi tab access when using idb vfs 2024-07-10 09:04:23 +05:00
Abdullah Atta
8302f8ba4c web: update wa-sqlite 2024-07-10 09:04:23 +05:00
Abdullah Atta
4db8b0db01 desktop: update electron-builder 2024-07-08 14:22:24 +05:00
Ammar Ahmed
3f1614ab9a mobile: fix stacktrace formatting 2024-07-08 13:58:04 +05:00
Ammar Ahmed
909d8c35f8 mobile: fix issue reporting 2024-07-08 13:52:57 +05:00
Ammar Ahmed
a73780f0b0 mobile: bump version to v3.0.10 2024-07-08 13:32:45 +05:00
Ammar Ahmed
286fdbeec0 mobile: show toast error if upload file url not resolved 2024-07-08 13:32:30 +05:00
Abdullah Atta
59099552f8 web: bump version to 3.0.9 2024-07-08 12:47:45 +05:00
Ammar Ahmed
0b6013be58 mobile: fix color popup on mobile 2024-07-08 12:07:39 +05:00
Abdullah Atta
2d93a17f34 web: fix all failing tests 2024-07-08 11:22:14 +05:00
Abdullah Atta
5ef7e39e17 web: optimize ui for mobile & tablet form factor 2024-07-08 10:24:50 +05:00
Abdullah Atta
f7de4c9084 editor: fix keyboard shortcut for internal link not working 2024-07-08 10:23:30 +05:00
Abdullah Atta
0672c864f7 web: fix drag/drop with multiple items 2024-07-08 10:23:02 +05:00
Abdullah Atta
cb3706c150 editor: fix editor options not applied 2024-07-08 10:22:25 +05:00
Abdullah Atta
08c6a99336 web: fix note toc not loading on app restart 2024-07-08 10:21:46 +05:00
Abdullah Atta
adf961fa08 editor: fix search options not respected 2024-07-08 10:21:10 +05:00
Abdullah Atta
0eb9ae47e9 web: restrict tab drag/drop to horizontal axis 2024-07-08 10:15:04 +05:00
Abdullah Atta
aa706300a4 web: fix tab not closing on single click 2024-07-04 11:14:20 +05:00
Ammar Ahmed
fd183a0e1e mobile: fix error in editor with incorrect tools data 2024-07-01 11:48:52 +05:00
Ammar Ahmed
b27cd4be0f mobile: set initial lastContentChangeTime to 0 2024-07-01 11:16:52 +05:00
Abdullah Atta
9575561832 web: fix real time editor sync not updating content 2024-07-01 11:16:52 +05:00
Ammar Ahmed
02f96f8d14 mobile: fix cannot read property jsx of undefined 2024-07-01 11:14:43 +05:00
Ammar Ahmed
3cf46e7ad1 mobile: bump deps 2024-07-01 10:12:08 +05:00
Abdullah Atta
93551aa0b7 web: improve focus item in list behavior 2024-07-01 09:51:58 +05:00
Abdullah Atta
d00c35bdaf web: fix tab focus requires 2 clicks 2024-07-01 09:51:44 +05:00
Abdullah Atta
6e6b793568 web: fix web app on mobile browsers 2024-07-01 09:51:29 +05:00
Ammar Ahmed
9e064d88c6 mobile: fix privacy screen 2024-07-01 09:48:14 +05:00
Abdullah Atta
1658ad8d18 core: fix SSE not connecting 2024-06-25 08:31:07 +05:00
Abdullah Atta
c94da3bdf0 core: use single listener for receiving pushed items 2024-06-25 08:30:41 +05:00
Abdullah Atta
1f0fc83a7c editor: remove images on paste if user is not pro 2024-06-07 10:31:02 +05:00
Abdullah Atta
a6af92c330 web: handle errors when resolving upload urls 2024-06-07 10:30:36 +05:00
Abdullah Atta
ef93ecc481 web: handle errors during bootstrapping 2024-06-07 10:30:16 +05:00
Abdullah Atta
039709822c desktop: add instructions for creating packages 2024-06-05 10:38:54 +05:00
Abdullah Atta
25c861ce77 global: publish packages to npm 2024-06-05 10:28:28 +05:00
Abdullah Atta
670f1570b5 setup: add script for publishing packages 2024-06-05 10:28:13 +05:00
Abdullah Atta
c93d82d2d6 web: bump version to 3.0.8 2024-06-03 17:34:53 +05:00
Abdullah Atta
bced20031b web: fix Please unlock the key store to get values 2024-06-03 17:31:44 +05:00
Abdullah Atta
f8bd089b51 web: bump version to 3.0.7 2024-06-03 13:32:01 +05:00
Abdullah Atta
2a4d04a26f mobile: use clearer wording on logout confirmation 2024-06-03 13:27:51 +05:00
Abdullah Atta
a0c32191f9 web: use clearer wording on logout confirmation 2024-06-03 13:27:51 +05:00
Abdullah Atta
662d59f60a web: use clearer wording on database corruption errors 2024-06-03 13:27:51 +05:00
Abdullah Atta
13befe1947 web: handle crashes during export 2024-06-03 13:27:21 +05:00
Abdullah Atta
21f8b6d557 web: allow resetting side bar order & hidden items 2024-06-03 13:08:20 +05:00
Abdullah Atta
aef53cace9 web: disable non pro items in context menu 2024-06-03 13:00:36 +05:00
Abdullah Atta
382b5b0240 web: improve web app loading performance by 10x (#5845)
* web: improve web app loading performance by 10x

* web: update test snapshots
2024-06-03 11:07:44 +05:00
Ammar Ahmed
a45b66d449 mobile: release v3.0.9 2024-06-01 14:14:01 +05:00
Ammar Ahmed
dd67b1a803 mobile: fix login 2fa sheet gets blocked by keyboard 2024-06-01 13:45:28 +05:00
Ammar Ahmed
00568ae839 mobile: add back undo/redo buttons 2024-06-01 13:21:27 +05:00
Abdullah Atta
2b2ea5717c web: bring back undo/redo buttons 2024-06-01 13:21:27 +05:00
Abdullah Atta
5672de8565 desktop: allow using native titlebar on desktop app (#5826) 2024-06-01 13:21:05 +05:00
Ammar Ahmed
1c70ad8cec web: fix tags & colors not included in imported notes (#5801)
* web: include tags and colors in imports

* web: refactors

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-06-01 11:40:16 +05:00
Abdullah Atta
0c344a2146 core: fix sqlite hangs on restoring notebook 2024-06-01 11:28:40 +05:00
Yoonjae Choi
38f69b514c web: fix readonly locked notes opening as editable (#5731)
Signed-off-by: Yoonjae Choi <dbswo9795@email.com>
2024-06-01 11:27:59 +05:00
Abdullah Atta
b502f6a08f web: fix CVE-2024-4367 2024-06-01 11:26:43 +05:00
Abdullah Atta
d68c3269d3 editor: fix extra padding around image 2024-06-01 11:26:28 +05:00
Abdullah Atta
6d9af85e2a web: fix background color of count in nav item 2024-05-21 13:14:56 +05:00
Abdullah Atta
1d58cfea3c web: fix navigation bar in settings dialog not themed 2024-05-21 13:14:56 +05:00
Abdullah Atta
c1fa4d3400 web: fix wrong version saved on conflict resolve 2024-05-21 13:14:42 +05:00
Abdullah Atta
df179056d6 logger: fix logger in core not working 2024-05-21 12:19:39 +05:00
Abdullah Atta
26f9e08c23 web: fix sync not running on page focus 2024-05-21 10:47:25 +05:00
Abdullah Atta
f719bebe5a core: create triggers after resetting database 2024-05-21 09:59:20 +05:00
Abdullah Atta
9693c8ea54 desktop: run sqlite on another thread 2024-05-21 09:59:20 +05:00
Abdullah Atta
040a677895 core: add support for closing logger db 2024-05-21 09:59:20 +05:00
Ammar Ahmed
df2708147e mobile: release v3.0.8 2024-05-20 15:35:59 +05:00
Ammar Ahmed
faba5f6385 mobile: always recreate editor on changing note 2024-05-20 15:08:27 +05:00
Ammar Ahmed
eab5bccb99 mobile: use sqlite based logger 2024-05-20 15:05:18 +05:00
Abdullah Atta
ed399c0d0a web: use noop logger before initializing 2024-05-20 15:05:18 +05:00
Abdullah Atta
3c7e962fe0 logger: minor refactor 2024-05-20 15:05:18 +05:00
Abdullah Atta
56614ef19d core: use chunks and set date only once 2024-05-20 15:05:18 +05:00
Abdullah Atta
169318c549 core: remove attachments initialized log 2024-05-20 15:05:18 +05:00
Abdullah Atta
bd6a1b9bff web: add support for opening multiple sqlite databases 2024-05-20 15:05:18 +05:00
Abdullah Atta
443d6625f9 core: add support for deleting logs for a specific date 2024-05-20 15:05:18 +05:00
Abdullah Atta
b6f95ff7cd core: use sqlite for storing logs 2024-05-20 15:05:18 +05:00
Ammar Ahmed
1b13da54a1 mobile: fix image extension 2024-05-20 12:55:34 +05:00
Ammar Ahmed
adf0f132fd mobile: convert heic/heif images to jpeg 2024-05-20 12:55:34 +05:00
Ammar Ahmed
73ec04b42d mobile: fix error when writing user key 2024-05-20 10:45:02 +05:00
Abdullah Atta
54a68e4406 themebuilder: bump version to 1.2.0 2024-05-18 13:09:49 +05:00
Abdullah Atta
bd73ae9df4 theme: add titleBar to theme scopes 2024-05-18 13:09:49 +05:00
Abdullah Atta
fe41f7b687 themebuilder: make theme builder compile 2024-05-18 13:09:49 +05:00
Ammar Ahmed
c9adaa2442 mobile: release v3.0.7 2024-05-17 12:41:13 +05:00
Abdullah Atta
10be5771c6 web: bump version to 3.0.6 2024-05-17 12:31:22 +05:00
Abdullah Atta
43f4669360 core: fix notes with no conflict marked as conflicted 2024-05-17 12:29:58 +05:00
Ammar Ahmed
3b04925e54 mobile: update changelog 2024-05-17 09:58:43 +05:00
Ammar Ahmed
ba3959af9e mobile: release v3.0.6 2024-05-17 09:58:08 +05:00
Ammar Ahmed
552d86d9d2 mobile: fix android compilation on java 17 2024-05-17 09:56:51 +05:00
Ammar Ahmed
096602150e mobile: fix encryption key not found error on login 2024-05-17 09:56:51 +05:00
Abdullah Atta
e5795bcbb2 web: fix app asking password on auto backups with encryption off 2024-05-16 16:25:04 +05:00
Ammar Ahmed
9be52a69ad mobile: add changelog 2024-05-16 15:45:38 +05:00
Ammar Ahmed
5809384f62 mobile: release v3.0.5 2024-05-16 15:44:48 +05:00
Ammar Ahmed
fff70dce16 mobile: fix recovery key dialog ui 2024-05-16 15:14:14 +05:00
Ammar Ahmed
258140b1e0 mobile: fix share widget title issues
1. Do not change title of note when choosing append to note
2. Fix color of title in widget
2024-05-16 12:07:29 +05:00
Ammar Ahmed
0a00844e64 mobile: fix locked note title cannot be edited 2024-05-16 12:07:05 +05:00
Abdullah Atta
03cea85efc web: bump version to 3.0.5 2024-05-16 11:57:12 +05:00
Abdullah Atta
8a16ef9b85 web: fix unlock a note for editing test 2024-05-16 11:55:50 +05:00
Abdullah Atta
c82c30b530 editor: fix search replace 2024-05-16 11:34:10 +05:00
Abdullah Atta
46599f66da web: fix search not working in readonly editor 2024-05-16 11:33:53 +05:00
Abdullah Atta
d1b532a4ae editor: show (empty) toolbar in readonly mode 2024-05-16 11:33:53 +05:00
Abdullah Atta
0baf7afc25 web: allow locked note duplication without unlocking 2024-05-16 09:31:46 +05:00
Abdullah Atta
a0595a7d23 core: copy all note relations when duplicating 2024-05-16 09:31:46 +05:00
Abdullah Atta
1e6f6ab477 core: fix Cannot read properties of undefined (reading 'startsWith') error on export 2024-05-16 09:31:18 +05:00
Abdullah Atta
62d677f9ad web: fix web app not working on safari 2024-05-16 09:30:49 +05:00
Abdullah Atta
8de98d98ab core: fix merger tests 2024-05-16 09:30:28 +05:00
Ammar Ahmed
5b57614bfe mobile: fix app lock issues 2024-05-16 09:27:27 +05:00
Ammar Ahmed
ae7c538ad7 mobile: enable privacy screen with applock 2024-05-16 09:27:27 +05:00
Ammar Ahmed
c6394c9e37 mobile: allow removing app lock with account password 2024-05-16 09:27:27 +05:00
Ammar Ahmed
acf3740a68 mobile: fix pricing plans stuck at loading 2024-05-16 09:24:19 +05:00
Ammar Ahmed
8423543904 mobile: show diff in note history 2024-05-16 09:23:45 +05:00
Abdullah Atta
b90cb28637 web: handle corrupted+migrations: error 2024-05-16 08:40:03 +05:00
Abdullah Atta
780e1e9db0 web: reopen editor session if conflict is resolved/created 2024-05-15 12:37:50 +05:00
Abdullah Atta
ca6f8e5e4b web: fix real time sync ignored when resolved content is synced 2024-05-15 12:37:08 +05:00
Abdullah Atta
4c4cda1c39 core: create conflict if local item is unsynced
previously we were only creating a conflict when the local item was
edited after the remote item. This failed in cases where even though
the local item was edited before the remote item, it still contained
important information.

Now we only check if the local item is synced or not. If it isn't synced,
we most probably want to preserve the local changes, or at least want
to be notified.
2024-05-15 12:36:38 +05:00
Abdullah Atta
6fcbde772c core: fix notes marked as conflicted wrongly 2024-05-15 12:34:23 +05:00
Abdullah Atta
eac1750b1c core: add test for merge conflicts during sync 2024-05-14 15:21:00 +05:00
Abdullah Atta
5eea179a58 core: set conflicted property on notes after fetch is done 2024-05-14 15:21:00 +05:00
Abdullah Atta
fe1ea6dc27 core: do not update dateModified when updating content internally 2024-05-14 15:21:00 +05:00
Abdullah Atta
cf1b774255 core: filter out conflicted notes & content during sync 2024-05-14 15:21:00 +05:00
Abdullah Atta
198fa33fc0 core: do not update dateModified when setting synced=true 2024-05-14 15:21:00 +05:00
Abdullah Atta
e722866d62 web: better handling of database disk image is malformed error 2024-05-14 15:21:00 +05:00
Ammar Ahmed
58845e5508 mobile: release v3.0.4 2024-05-11 15:19:12 +05:00
Abdullah Atta
543c6f17ed web: enable sourcemaps for web app 2024-05-11 13:09:59 +05:00
Abdullah Atta
533f95fcff editor: fix editor crash due to TypeError: t.startsWith is not a function 2024-05-11 12:53:30 +05:00
Ammar Ahmed
c02f1cde16 mobile: disable template when pin note to notifications 2024-05-11 12:49:11 +05:00
Ammar Ahmed
043f55ead0 mobile: fix exporting note to text for share/copy 2024-05-11 12:49:11 +05:00
Ammar Ahmed
fcece06a1c mobile: fix update notification not showing on mobile app 2024-05-11 12:49:11 +05:00
Abdullah Atta
da7d79edc6 core: token without a scope is invalid 2024-05-11 12:36:23 +05:00
Ammar Ahmed
a2e7190a4a mobile: fix colors of error popup in editor 2024-05-11 11:26:15 +05:00
Ammar Ahmed
503f2b5876 mobile: fix cannot read property 'groupBy' of undefined 2024-05-11 11:22:59 +05:00
Abdullah Atta
68d57e2b27 web: bump version to 3.0.4 2024-05-11 10:48:48 +05:00
Abdullah Atta
400aa574cd core: fix 2fa codes not being sent 2024-05-11 10:29:02 +05:00
Abdullah Atta
5028bb1275 web: update snapshots 2024-05-10 15:30:35 +05:00
Abdullah Atta
27ae5bb5a2 desktop: use npx prebuildify instead of yarn 2024-05-10 15:24:39 +05:00
Abdullah Atta
83316fe299 web: update snapshots for linux 2024-05-10 15:20:57 +05:00
Ammar Ahmed
8869224de2 mobile: fix image preview not rendered on iOS 2024-05-10 15:04:28 +05:00
Ammar Ahmed
c4152f0fc0 mobile: set editor reset time to 10 minutes 2024-05-10 14:53:10 +05:00
Ammar Ahmed
d803005637 mobile: release v3.0.3 2024-05-10 14:47:27 +05:00
Ammar Ahmed
53d896be2b mobile: fix opening exported files after export 2024-05-10 14:44:12 +05:00
Abdullah Atta
8c37c6409a ci: use actions/upload-artifact@v4 2024-05-10 12:36:56 +05:00
Abdullah Atta
9195244890 web: fix tests 2024-05-10 12:31:55 +05:00
Ammar Ahmed
152794ccd0 mobile: fix image filename in picker 2024-05-10 12:24:40 +05:00
Ammar Ahmed
2e8b234b63 mobile: fix editor content update on realtime sync 2024-05-10 12:24:40 +05:00
Abdullah Atta
055c647698 web: add Audios tab in attachments dialog 2024-05-10 12:08:38 +05:00
Abdullah Atta
2b7bcfe7c6 web: bump version to 3.0.3 2024-05-10 12:00:06 +05:00
Abdullah Atta
3c8b8a7bc9 web: fix real time sync test 2024-05-10 11:58:56 +05:00
Abdullah Atta
fde45fb6de web: saving timeout notice shown every time 2024-05-10 11:58:44 +05:00
Abdullah Atta
4a616a3e67 web: fix tab not updating when note is locked/unlocked 2024-05-10 11:27:26 +05:00
Abdullah Atta
bd0a338dd6 core: fix dateEdited not updating on saving locked content 2024-05-10 10:46:11 +05:00
Abdullah Atta
5ab721e3e2 core: fix async iterator on sql collection only returning dateCreated & id 2024-05-10 10:22:03 +05:00
Ammar Ahmed
cb70bb9429 mobile: fix errors 2024-05-10 09:22:23 +05:00
Abdullah Atta
82cadd473c web: show error if saving takes more than 30 seconds 2024-05-10 09:22:23 +05:00
Ammar Ahmed
0e8ea3ddeb mobile: fix editor loading 2024-05-10 09:22:23 +05:00
Ammar Ahmed
511acc5fca mobile: handle editor not responding 2024-05-10 09:22:23 +05:00
Ammar Ahmed
001cf8ee92 mobile: add editor lock safety 2024-05-10 09:22:23 +05:00
Muhammad Ali
a965cd694b web: fix color not updating on note (#5573) 2024-05-10 09:16:59 +05:00
Ammar Ahmed
d3c1c1959b mobile: fix wording of push/pull sync 2024-05-09 17:39:35 +05:00
Abdullah Atta
9a866a552a web: replace force sync with force push/pull 2024-05-09 17:39:35 +05:00
Ammar Ahmed
837c296821 mobile: push/pull force sync 2024-05-09 17:39:35 +05:00
Abdullah Atta
9aed6faed8 common: fix locked notes not getting exported 2024-05-09 17:39:16 +05:00
Abdullah Atta
734f491e8d editor: fix crash due to undefined is not an object (evaluating 'a.nodeSize') (#5553) 2024-05-09 17:38:18 +05:00
Ammar Ahmed
0ea13024e9 mobile: fix editor crash due to position (#5519)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-09 15:55:33 +05:00
Abdullah Atta
cb27039245 web: close shared service on window close 2024-05-09 10:30:46 +05:00
Abdullah Atta
714ac2deac core: do not use auth access tokens as normal access tokens 2024-05-09 10:30:25 +05:00
Abdullah Atta
d605bc29bf desktop: fix crash on toggling custom dns (#5538) 2024-05-09 10:15:14 +05:00
Abdullah Atta
080101bc45 core: fix database disk image malformed error (#5525) 2024-05-09 10:08:44 +05:00
Abdullah Atta
4819e17da3 core: rerun sync if there are unsynced changes after sync complete 2024-05-08 16:48:59 +05:00
Ammar Ahmed
69a5d5a1b6 global: update .gitignore 2024-05-08 10:43:35 +05:00
Ammar Ahmed
68a6d6c313 mobile: fix crash when taking camera picture 2024-05-08 09:48:28 +05:00
Ammar Ahmed
c51ef4d5f6 mobile: fix camera permission 2024-05-08 09:48:28 +05:00
Ammar Ahmed
d9c5c1f893 mobile: remove margin in editor 2024-05-08 09:47:56 +05:00
Ammar Ahmed
5a75ca73a9 mobile: fix editor scrolling on update 2024-05-08 09:47:56 +05:00
Abdullah Atta
626d393376 desktop: use yarn instead of npx for build scripts 2024-05-08 09:14:55 +05:00
Abdullah Atta
68d06ff3f8 core: fix legacy backup restoration 2024-05-07 15:57:22 +05:00
Abdullah Atta
79f20d100c core: fix File already exists error on duplicate downloads 2024-05-07 12:57:51 +05:00
Ammar Ahmed
9d5bec3bfb ci: upload sourcemaps 2024-05-07 11:49:58 +05:00
Abdullah Atta
ac96354b4c core: handle case when updating item while a push is ongoing 2024-05-06 22:04:04 +05:00
Abdullah Atta
63f44d6fbc core: set synced to false on removing content by note id 2024-05-06 15:45:45 +05:00
Abdullah Atta
f9bfa88c83 core: set synced to false on unlinking relations 2024-05-06 15:45:45 +05:00
Ammar Ahmed
ef192f3d22 mobile: release v3.0.2 2024-05-05 01:14:20 +05:00
1273 changed files with 106069 additions and 150582 deletions

View File

@@ -10,11 +10,12 @@ const authors = readFileSync("AUTHORS", "utf-8");
const isAuthor = authors.includes(`<${authorEmail}>`);
const SCOPES = [
// for full list of scopes + details see: https://github.com/streetwriters/notesnook-private/blob/master/CONTRIBUTING.md#commit-guidelines
// for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
"mobile",
"web",
"vericrypt",
"monograph",
"desktop",
"crypto",
"editor",
@@ -34,7 +35,9 @@ const SCOPES = [
"common",
"global",
"docs",
"themebuilder"
"themebuilder",
"intl",
"webclipper"
];
module.exports = {

View File

@@ -1,8 +1,6 @@
name: Bug Report
description: Are you facing a bug or a crash in Notesnook?
labels: ["Type: Bug", "Status: Pending"]
assignees:
- thecodrr
type: Bug
body:
- type: markdown
attributes:

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Notesnook Discord Community
url: https://discord.gg/6mHHyncHJE
url: https://go.notesnook.com/discord
about: Reach out to us directly & discuss your issues, suggestions & other feedback!

View File

@@ -1,6 +1,6 @@
name: Feature request
description: Suggest a feature you are missing in Notesnook
labels: ["Type: Feature Request", "Status: Pending"]
type: Feature
body:
- type: markdown
attributes:

100
.github/workflows/android.e2e.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: e2e-android
on: workflow_dispatch
jobs:
e2e-android:
runs-on: ubuntu-latest
env:
API_LEVEL: 34
ARCH: x86_64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-version: wrapper
cache-read-only: false
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
architecture: "x64"
- name: Install Detox CLI
run: npm install detox-cli --global
- name: Detox build
run: |
yarn build:android
ls apps/mobile/native/android/app/build/outputs/apk
ls apps/mobile/native/android/app/build/outputs/apk/release
- name: Get device name
id: device
run: |
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: ${{ steps.device.outputs.AVD_NAME }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Detox test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
disable-animations: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
force-avd-creation: false
script: yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: detox-artifacts
path: artifacts
retention-days: 14

View File

@@ -147,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
@@ -22,7 +22,7 @@ jobs:
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "11"
java-version: "17"
architecture: "x64"
- name: Install node modules
@@ -133,7 +133,6 @@ jobs:
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
@@ -148,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -84,6 +84,26 @@ jobs:
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
run: |
sudo apt-get install -y flatpak flatpak-builder pipx
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
cd /tmp/flatpak-builder-tools/node
pipx install .
cd -
node scripts/generate-sources.mjs
- name: Upload flatpak sources
uses: softprops/action-gh-release@v1
if: inputs.publish-github && inputs.build-linux
with:
draft: true
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./generated-sources.json
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
build-macos:
name: Build for macOS
needs: build
@@ -92,7 +112,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -136,6 +156,12 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
@@ -188,7 +214,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -212,6 +238,12 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
@@ -247,7 +279,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
@@ -272,6 +304,12 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build
run: npx nx run release --project @notesnook/desktop

233
.github/workflows/desktop.tests.yml vendored Normal file
View File

@@ -0,0 +1,233 @@
name: Test @notesnook/desktop
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "app/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
pull_request:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Generate desktop build
run: npx nx build:desktop @notesnook/web
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
test-macos-x64:
name: Test macOS x64
needs: build
runs-on: macos-13
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --mac --dir --x64
working-directory: ./apps/desktop
- name: Run tests x64
run: |
EXECUTABLE_PATH=output/mac/Notesnook.app/Contents/MacOS/Notesnook npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos-x64
path: apps/desktop/test-results
retention-days: 5
test-macos:
name: Test macOS
needs: build
runs-on: macos-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --mac --dir --arm64
working-directory: ./apps/desktop
- name: Run tests arm64
run: |
EXECUTABLE_PATH=output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos
path: apps/desktop/test-results
retention-days: 5
test-linux:
name: Test for Linux
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --linux --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: |
EXECUTABLE_PATH=output/linux-unpacked/notesnook xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-linux
path: apps/desktop/test-results
retention-days: 5
test-windows:
name: Test for Windows
needs: build
runs-on: windows-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --win --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: |
npx cross-env EXECUTABLE_PATH=output/win-unpacked/Notesnook.exe npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-win
path: apps/desktop/test-results
retention-days: 5

View File

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

View File

@@ -1,6 +1,6 @@
name: Publish Notesnook Help
on:
on:
workflow_dispatch:
push:
branches:
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable

View File

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: macos-13
runs-on: macos-latest
timeout-minutes: 60
steps:
@@ -89,16 +89,19 @@ jobs:
- name: CCache Stats After Build
run: ccache -sv
- name: "Upload app to TestFlight"
uses: apple-actions/upload-testflight-build@v1
with:
app-path: Notesnook.ipa
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
api-key-id: ${{ secrets.API_KEY_ID }}
api-private-key: ${{ secrets.API_KEY }}
# - name: "Upload app to TestFlight"
# uses: apple-actions/upload-testflight-build@v1
# with:
# app-path: Notesnook.ipa
# issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
# api-key-id: ${{ secrets.API_KEY_ID }}
# api-private-key: ${{ secrets.API_KEY }}
- name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Notesnook.ipa
path: Notesnook.ipa
name: Notesnook.zip
path: |
Notesnook.ipa
apps/mobile/native/ios/**/*.map
packages/editor-mobile/sourcemaps/*.map

89
.github/workflows/monograph.publish.yml vendored Normal file
View File

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

View File

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

View File

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

44
.github/workflows/web.benchmarks.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Benchmark @notesnook/web
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.benchmarks.yml"
pull_request:
jobs:
bench:
name: Bench
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Build
run: npm run build:web
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: apps/web
- name: Run benchmarks
id: benchmark
run: node __bench__/app.bench.mjs
working-directory: apps/web

View File

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

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -40,7 +40,7 @@ jobs:
run: npm run build:test:web
- name: Archive build artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
@@ -53,10 +53,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download build
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
@@ -88,7 +88,7 @@ jobs:
working-directory: apps/web
- name: Upload test results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.shard }}

View File

@@ -40,12 +40,35 @@ Once you are inside the `./notesnook` directory, run the preparation step:
npm install
```
Now you can finally start the desktop app:
Now you can finally start the desktop app for development:
```bash
npm run start:desktop
```
To run the app in release mode:
```bash
npm run staging -- --rebuild
```
This will compile and run the app in production mode but it won't generate any packages. To create the final packages, you'll have to run the following commands:
```bash
npm run release -- --rebuild
# For macOS
npx electron-builder --mac dmg --arm64 --x64 -publish never
# For Linux (AppImage)
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
# For Windows
npx electron-builder --win --publish never
```
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
## Developer guide
### The tech stack

View File

@@ -0,0 +1,53 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import test from "node:test";
import { launchApp } from "./utils.mjs";
import assert from "assert";
import slugify from "slugify";
import path from "path";
import { mkdir } from "fs/promises";
test("make sure app loads", async (t) => {
const { app, page } = await launchApp();
try {
await page.waitForSelector("#authForm");
assert.ok(
await page.getByRole("button", { name: "Create account" }).isVisible()
);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
} catch (e) {
await mkdir("test-results", { recursive: true });
await page.screenshot({
path: path.join(
"test-results",
`${slugify(t.name)}-${process.platform}-${process.arch}-error.png`
)
});
throw e;
} finally {
await app.close();
}
});

View File

@@ -0,0 +1,38 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { _electron as electron } from "playwright";
const executablePath = process.env.EXECUTABLE_PATH;
// process.platform === "linux"
// ? "output/linux-unpacked/Notesnook"
// : process.platform === "darwin"
// ? "output/mac/Notesnook.app/Contents/MacOS/Notesnook"
// : "output/win-unpacked/Notesnook.exe";
export async function launchApp() {
const app = await electron.launch({
executablePath
});
const page = await app.firstWindow();
return { app, page };
}

View File

@@ -1,3 +1,4 @@
owner: streetwriters
repo: notesnook
provider: github
updaterCacheDirName: Notesnook

File diff suppressed because it is too large Load Diff

View File

@@ -2,38 +2,58 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.2",
"version": "3.0.24",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"require": {
"types": "./dist/types/index.d.ts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/esm/index.js"
}
}
},
"homepage": "https://notesnook.com/",
"repository": "https://github.com/streetwriters/notesnook",
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/crypto": "file:../../packages/crypto",
"@trpc/client": "10.38.3",
"@trpc/server": "10.38.3",
"better-sqlite3-multiple-ciphers": "^9.5.0",
"electron-trpc": "0.5.2",
"electron-updater": "6.1.4",
"icojs": "^0.17.1",
"sodium-native": "^4.1.1",
"@lingui/core": "4.11.4",
"@notesnook/intl": "file:../../packages/intl",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"better-sqlite3-multiple-ciphers": "11.5.0",
"electron-trpc": "0.6.1",
"electron-updater": "^6.3.4",
"icojs": "^0.19.4",
"sqlite-better-trigram": "0.0.2",
"typed-emitter": "^2.1.0",
"yargs": "^17.6.2",
"zod": "^3.21.4"
"yargs": "^17.7.2",
"zod": "3.23.8"
},
"devDependencies": {
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "^29.3.1",
"electron-builder": "^24.13.3",
"esbuild": "^0.20.0",
"node-gyp-build": "^4.8.0",
"@streetwriters/kysely": "^0.27.4",
"@types/node": "22.7.0",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.1",
"electron": "^31.7.4",
"electron-builder": "^25.1.8",
"esbuild": "0.21.5",
"vitest": "2.1.8",
"node-abi": "^3.68.0",
"node-gyp-build": "^4.8.2",
"playwright": "^1.48.2",
"prebuildify": "^6.0.1",
"slugify": "1.6.6",
"tree-kill": "^1.2.2",
"undici": "^6.14.1"
"undici": "^6.19.8"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
@@ -42,10 +62,11 @@
"start": "node scripts/dev.mjs",
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "tsc",
"build": "node ../../scripts/build.mjs",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
"postinstall": "patch-package"
"postinstall": "patch-package",
"test": "node --test --test-timeout 30000 --test-force-exit"
},
"author": {
"name": "Streetwriters (Private) Limited",
@@ -74,7 +95,20 @@
"node_modules/file-uri-to-path",
"node_modules/bindings",
"node_modules/node-gyp-build",
"node_modules/sqlite-better-trigram",
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
{
"from": "node_modules/sqlite-better-trigram-linux-${arch}",
"to": "node_modules/sqlite-better-trigram-linux-${arch}"
},
{
"from": "node_modules/sqlite-better-trigram-darwin-${arch}",
"to": "node_modules/sqlite-better-trigram-darwin-${arch}"
},
{
"from": "node_modules/sqlite-better-trigram-windows-${arch}",
"to": "node_modules/sqlite-better-trigram-windows-${arch}"
},
"node_modules/sodium-native/index.js",
"node_modules/sodium-native/package.json"
],
@@ -147,10 +181,12 @@
]
}
],
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js",
"signtoolOptions": {
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js"
},
"icon": "assets/icons/app.ico"
},
"portable": {
@@ -158,7 +194,8 @@
},
"nsis": {
"oneClick": true,
"createDesktopShortcut": "always"
"createDesktopShortcut": "always",
"deleteAppDataOnUninstall": true
},
"linux": {
"target": [
@@ -225,4 +262,4 @@
}
]
}
}
}

View File

@@ -1,8 +1,8 @@
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
index fcb7f54..cb1c7f7 100644
index 739a48c..7a7b4af 100644
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
@@ -117,6 +117,7 @@ class LinuxTargetHelper {
StartupWMClass: appInfo.productName,
...extra,
...targetSpecificOptions.desktop,
@@ -10,7 +10,7 @@ index fcb7f54..cb1c7f7 100644
};
const description = this.getDescription(targetSpecificOptions);
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
@@ -160,6 +161,23 @@ class LinuxTargetHelper {
data += `\n${name}=${desktopMeta[name]}`;
}
data += "\n";

View File

@@ -1,23 +0,0 @@
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
index 379cf3b..2644e5d 100644
--- a/node_modules/electron-trpc/dist/main.mjs
+++ b/node_modules/electron-trpc/dist/main.mjs
@@ -221,9 +221,16 @@ class G {
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
}
detachWindow(r) {
+
y(this, c, i(this, c).filter((n) => n !== r));
- for (const [n, t] of i(this, u).entries())
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
+ for (const [n, t] of i(this, u).entries()) {
+ try {
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
+ } catch(e) {
+ console.error(e);
+ // ignore
+ }
+ }
}
}
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {

View File

@@ -0,0 +1,39 @@
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
index 07971db..b67a3cf 100644
--- a/node_modules/electron-trpc/dist/main.mjs
+++ b/node_modules/electron-trpc/dist/main.mjs
@@ -537,8 +537,15 @@ class be {
attachWindow(r) {
F(this, w).includes(r) || (y("Attaching window", r.id), F(this, w).push(r), v(this, N, Z).call(this, r));
}
- detachWindow(r) {
- y("Detaching window", r.id), L(this, w, F(this, w).filter((e) => e !== r)), v(this, R, M).call(this, { webContentsId: r.webContents.id });
+ detachWindow(r, webContentsId) {
+ y("Detaching window", r.id);
+
+ if (r.isDestroyed() && webContentsId === undefined) {
+ throw new Error('webContentsId is required when calling detachWindow on a destroyed window');
+ }
+
+ L(this, w, F(this, w).filter((e) => e !== r));
+ v(this, R, M).call(this, { webContentsId: webContentsId ?? r.webContents.id });
}
}
w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
@@ -548,6 +555,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
for (const [i, a] of F(this, E).entries())
i.startsWith(`${r}-${e ?? ""}`) && (y("Closing subscription", i), a.unsubscribe(), F(this, E).delete(i));
}, N = new WeakSet(), Z = function(r) {
+ const webContentsId = r.webContents.id;
r.webContents.on("did-start-navigation", ({ frame: e }) => {
y(
"Handling webContents `did-start-navigation` event",
@@ -558,7 +566,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
frameRoutingId: e.routingId
});
}), r.webContents.on("destroyed", () => {
- y("Handling webContents `destroyed` event"), this.detachWindow(r);
+ y("Handling webContents `destroyed` event"), this.detachWindow(r, webContentsId);
});
};
const me = ({

View File

@@ -1,46 +0,0 @@
diff --git a/node_modules/node-gyp-build/bin.js b/node_modules/node-gyp-build/bin.js
index 3fbcdf0..7ca3ab5 100644
--- a/node_modules/node-gyp-build/bin.js
+++ b/node_modules/node-gyp-build/bin.js
@@ -16,7 +16,8 @@ if (!buildFromSource()) {
}
function build () {
- var args = [os.platform() === 'win32' ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
+ var win32 = os.platform() === 'win32'
+ var args = [win32 ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
try {
var pkg = require('node-gyp/package.json')
@@ -27,7 +28,7 @@ function build () {
]
} catch (_) {}
- proc.spawn(args[0], args.slice(1), { stdio: 'inherit' }).on('exit', function (code) {
+ proc.spawn(args[0], args.slice(1), { stdio: 'inherit', shell: win32, windowsHide: true }).on('exit', function (code) {
if (code || !process.argv[3]) process.exit(code)
exec(process.argv[3]).on('exit', function (code) {
process.exit(code)
@@ -45,15 +46,18 @@ function preinstall () {
function exec (cmd) {
if (process.platform !== 'win32') {
- var shell = os.platform() === 'android' ? 'sh' : '/bin/sh'
- return proc.spawn(shell, ['-c', '--', cmd], {
+ var shell = os.platform() === 'android' ? 'sh' : true
+ return proc.spawn(cmd, [], {
+ shell,
stdio: 'inherit'
})
}
- return proc.spawn(process.env.comspec || 'cmd.exe', ['/s', '/c', '"' + cmd + '"'], {
+ return proc.spawn(cmd, [], {
windowsVerbatimArguments: true,
- stdio: 'inherit'
+ stdio: 'inherit',
+ shell: true,
+ windowsHide: true
})
}

View File

@@ -18,19 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "path";
import fs, { readFile, writeFile } from "fs/promises";
import { existsSync, readFileSync } from "fs";
import fs from "fs/promises";
import { existsSync } from "fs";
import yargs from "yargs-parser";
import os from "os";
import * as childProcess from "child_process";
import { fileURLToPath } from "url";
import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
);
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
@@ -46,31 +44,27 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
// temporary until there's support for prebuilt binaries for linux ARM
if (os.platform() === "linux") await patchBetterSQLite3();
if (os.platform() === "win32")
await exec(
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
path.join(__dirname, "..", "node_modules", "sodium-native")
);
await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true,
force: true
});
if (args.variant === "mas") {
await exec(`npm run bundle:mas`);
await exec(`yarn run bundle:mas`);
} else {
await exec(`npm run bundle`);
await exec(`yarn run bundle`);
}
await exec(`npx tsc`);
await exec(`yarn run build`);
if (args.run) {
await exec(`npx electron-builder --dir --x64`);
await exec(`yarn electron-builder --dir --${process.arch}`);
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
if (process.arch === "arm64")
await exec(`./output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook`);
else await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
} else {
await exec(`./output/linux-unpacked/Notesnook`);
}
@@ -83,21 +77,3 @@ async function exec(cmd, cwd) {
cwd: cwd || process.cwd()
});
}
async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "9.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}

View File

@@ -36,6 +36,8 @@ const ENV = {
FORCE_COLOR: "false",
COLOR: "0"
};
process.chdir(path.join(__dirname, ".."));
await onChange(true);
console.log("Watching...");
@@ -53,11 +55,13 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npx electron-builder install-app-deps");
await exec("npm rebuild electron --verbose --foreground-scripts");
await exec("yarn electron-builder install-app-deps");
}
await exec(`npm run bundle`);
execAsync(`npx`, [`tsc`]);
await exec(`yarn run bundle`);
await exec(`yarn run build`);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
@@ -66,8 +70,8 @@ async function onChange(first) {
if (first) {
await spawnAndWaitUntil(
["npx", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."),
["npm", "run", "start:desktop"],
path.join(__dirname, "..", "..", "web"),
(data) => data.includes("Network: use --host to expose")
);
}
@@ -78,7 +82,7 @@ async function onChange(first) {
}
execAsync(
"npx",
"yarn",
["electron", path.join("build", "electron.js")],
true,
cleanup

View File

@@ -0,0 +1,48 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { readFile, writeFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}
if (process.argv[1] === __filename) {
console.log("Patching better-sqlite3");
patchBetterSQLite3();
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { initTRPC } from "@trpc/server";
import { z } from "zod";
import { dialog, nativeTheme, Notification, shell } from "electron";
import { app, dialog, nativeTheme, Notification, shell } from "electron";
import { AutoLaunch } from "../utils/autolaunch";
import { config, DesktopIntegration } from "../utils/config";
import { bringToFront } from "../utils/bring-to-front";
@@ -58,11 +58,11 @@ export const osIntegrationRouter = t.router({
customDns: t.procedure.query(() => config.customDns),
setCustomDns: t.procedure
.input(z.boolean())
.input(z.boolean().optional())
.mutation(({ input: customDns }) => {
if (customDns) enableCustomDns();
else disableCustomDns();
config.customDns = customDns;
config.customDns = !!customDns;
}),
proxyRules: t.procedure.query(() => config.proxyRules),
@@ -152,6 +152,10 @@ export const osIntegrationRouter = t.router({
await rm(input);
}),
restart: t.procedure.query(() => {
app.relaunch();
app.exit();
}),
showNotification: t.procedure
.input(NotificationOptions)
.query(({ input }) => {
@@ -191,7 +195,10 @@ export const osIntegrationRouter = t.router({
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
if (windowControlsIconColor) {
config.windowControlsIconColor = windowControlsIconColor;
if (process.platform === "win32")
if (
process.platform === "win32" &&
!config.desktopSettings.nativeTitlebar
)
globalThis.window?.setTitleBarOverlay({
symbolColor: windowControlsIconColor
});

View File

@@ -0,0 +1,145 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type { Database, Statement } from "better-sqlite3-multiple-ciphers";
import type { QueryResult } from "@streetwriters/kysely";
type SQLiteCompatibleType =
| number
| string
| Uint8Array
| Array<number>
| bigint
| null;
export class SQLite {
sqlite?: Database;
initialized = false;
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
retryCounter: Record<string, number> = {};
constructor() {
console.log("new sqlite worker");
}
async open(filePath: string) {
if (this.sqlite) {
console.error("Database is already initialized");
return;
}
this.sqlite = require("better-sqlite3-multiple-ciphers")(
filePath
).unsafeMode(true);
const betterTrigram = require("sqlite-better-trigram");
betterTrigram.load(this.sqlite);
}
/**
* Wrapper function for preparing SQL statements with caching
* to avoid unnecessary computations.
*/
async prepare(sql: string): Promise<Statement | undefined> {
if (!this.sqlite) throw new Error("Database is not initialized.");
try {
const cached = this.preparedStatements.get(sql);
if (cached !== undefined) return cached;
const prepared = this.sqlite.prepare(sql);
if (!prepared) return;
this.preparedStatements.set(sql, prepared);
// reset retry count on success
this.retryCounter[sql] = 0;
return prepared;
} catch (ex) {
console.error(ex);
// statement prepare process can be flaky so retry at least 5 times
// before giving up.
if (this.retryCounter[sql] < 5) {
this.retryCounter[sql] = (this.retryCounter[sql] || 0) + 1;
console.warn("Failed to prepare statement. Retrying:", sql);
return this.prepare(sql);
} else this.retryCounter[sql] = 0;
if (ex instanceof Error) ex.message += ` (query: ${sql})`;
throw ex;
}
}
async exec<R>(
sql: string,
parameters: SQLiteCompatibleType[] = []
): Promise<QueryResult<R>> {
const prepared = await this.prepare(sql);
if (!prepared) return { rows: [] };
try {
if (prepared.reader) {
return {
rows: prepared.all(parameters) as R[]
};
} else {
const { changes, lastInsertRowid } = prepared.run(parameters);
const numAffectedRows =
changes !== undefined && changes !== null && !isNaN(changes)
? BigInt(changes)
: undefined;
return {
numAffectedRows,
insertId:
lastInsertRowid !== undefined && lastInsertRowid !== null
? typeof lastInsertRowid === "bigint"
? lastInsertRowid
: BigInt(lastInsertRowid)
: undefined,
rows: [] as R[]
};
}
} catch (e) {
if (e instanceof Error) e.message += ` (query: ${sql})`;
throw e;
}
}
async run<R>(
sql: string,
parameters?: SQLiteCompatibleType[]
): Promise<QueryResult<R>> {
if (!this.sqlite) throw new Error("No database is not opened.");
return await this.exec(sql, parameters);
}
async close() {
if (!this.sqlite) return;
this.preparedStatements.clear();
this.sqlite.close();
this.sqlite = undefined;
}
async delete(filePath: string) {
await this.close();
await require("fs/promises").rm(filePath, {
force: true,
maxRetries: 5,
retryDelay: 500
});
}
}

View File

@@ -29,19 +29,37 @@ type Progress = { percent: number };
const t = initTRPC.create();
let cancellationToken: CancellationToken | undefined = undefined;
let downloadTimeout: NodeJS.Timeout | undefined = undefined;
export const updaterRouter = t.router({
autoUpdates: t.procedure.query(() => config.automaticUpdates),
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
download: t.procedure.query(async () => {
if (cancellationToken) return;
cancellationToken = new CancellationToken();
await autoUpdater
.downloadUpdate(cancellationToken)
.finally(() => (cancellationToken = undefined));
clearTimeout(downloadTimeout);
await new Promise<string[]>((resolve, reject) => {
downloadTimeout = setTimeout(async () => {
cancellationToken = new CancellationToken();
autoUpdater.isUpdaterActive();
await autoUpdater
.downloadUpdate(cancellationToken)
.then(resolve)
.catch(reject)
.finally(() => (cancellationToken = undefined));
}, 1000);
});
}),
check: t.procedure.query(async () => {
await autoUpdater.checkForUpdates().catch(console.error);
if (cancellationToken) return;
clearTimeout(downloadTimeout);
await new Promise<void>((resolve) => {
downloadTimeout = setTimeout(async () => {
await autoUpdater
.checkForUpdates()
.catch(console.error)
.finally(resolve);
}, 1000);
});
}),
toggleAutoUpdates: t.procedure
@@ -61,7 +79,18 @@ export const updaterRouter = t.router({
"update-not-available"
),
onAvailable: createSubscription<"update-available", UpdateInfo>(
"update-available"
"update-available",
() => {
if (!config.automaticUpdates) return false;
autoUpdater.emit("download-progress", {
bytesPerSecond: 0,
delta: 0,
percent: 0,
total: 100,
transferred: 0
});
return true;
}
),
onError: createSubscription("error")
});
@@ -69,10 +98,11 @@ export const updaterRouter = t.router({
function createSubscription<
TName extends keyof AppUpdaterEvents,
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
>(eventName: TName) {
>(eventName: TName, handler?: (args: TReturnType) => boolean) {
return t.procedure.subscription(() => {
return observable<TReturnType>((emit) => {
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
if (handler?.(args[0])) return;
emit.next(args[0]);
};
autoUpdater.removeAllListeners(eventName);

View File

@@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export * from "./constants";
export { PATHS } from "./constants";
export type { AppRouter } from "./api";
export { type UpdateInfo } from "builder-util-runtime";
export { type DesktopIntegration } from "./utils/config";
export { SQLite } from "./api/sqlite-kysely.js";

View File

@@ -36,6 +36,20 @@ import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
import { Messages, setI18nGlobal } from "@notesnook/intl";
import { i18n } from "@lingui/core";
const locale =
process.env.NODE_ENV === "development"
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
: import("@notesnook/intl/locales/$en.json");
locale.then(({ default: locale }) => {
i18n.load({
en: locale.messages as unknown as Messages
});
i18n.activate("en");
});
setI18nGlobal(i18n);
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -84,22 +98,27 @@ async function createWindow() {
format: process.platform === "win32" ? "ico" : "png"
}),
titleBarStyle: "hidden",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
trafficLightPosition: {
x: 16,
y: 12
},
...(config.desktopSettings.nativeTitlebar
? {}
: {
titleBarStyle: process.platform === "win32" || process.platform === "darwin" ? "hidden" : "default",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
trafficLightPosition: {
x: 16,
y: 12
}
}),
webPreferences: {
zoomFactor: config.zoomFactor,
nodeIntegration: true,
contextIsolation: false,
nodeIntegrationInWorker: true,
spellcheck: config.isSpellCheckerEnabled,
preload: __dirname + "/preload.js"
}

View File

@@ -19,29 +19,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable no-var */
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
import type { NNCrypto } from "@notesnook/crypto";
// import type { NNCrypto } from "@notesnook/crypto";
import { ipcRenderer } from "electron";
import { platform } from "os";
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
declare global {
var os: () => "mas" | ReturnType<typeof platform>;
var electronTRPC: RendererGlobalElectronTRPC;
var NativeNNCrypto: (new () => NNCrypto) | undefined;
var createSQLite3Database: (filename: string) => Database;
var electronTRPC: any;
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
}
process.once("loaded", async () => {
const electronTRPC: RendererGlobalElectronTRPC = {
sendMessage: (operation) =>
const electronTRPC = {
sendMessage: (operation: any) =>
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
onMessage: (callback) =>
onMessage: (callback: any) =>
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
};
globalThis.electronTRPC = electronTRPC;
});
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { NativeImage, nativeImage } from "electron";
import path from "path";
import { isDevelopment } from "./index";
import { parse, ParsedImage } from "icojs";
import { ParsedImage, parseICO } from "icojs";
import { getSystemTheme } from "./theme";
import { readFile } from "fs/promises";
@@ -71,7 +71,7 @@ export class AssetManager {
`${icon}${prefix}.ico`
);
const icoBuffer = await readFile(icoPath);
const images = await parse(icoBuffer, "image/png");
const images = await parseICO(icoBuffer, "image/png");
ALL_ICONS.push({ id: icon, images, prefix });
}
}

View File

@@ -35,6 +35,7 @@ async function configureAutoUpdater() {
autoUpdater.allowDowngrade = false;
autoUpdater.allowPrerelease = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.disableWebInstaller = true;
}
export { configureAutoUpdater };

View File

@@ -25,7 +25,8 @@ export const DesktopIntegration = z.object({
autoStart: z.boolean().optional(),
startMinimized: z.boolean().optional(),
minimizeToSystemTray: z.boolean().optional(),
closeToSystemTray: z.boolean().optional()
closeToSystemTray: z.boolean().optional(),
nativeTitlebar: z.boolean().optional()
});
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
@@ -35,7 +36,8 @@ export const config = {
autoStart: false,
startMinimized: false,
minimizeToSystemTray: false,
closeToSystemTray: false
closeToSystemTray: false,
nativeTitlebar: false
},
privacyMode: false,
isSpellCheckerEnabled: true,

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { Menu, MenuItem, clipboard, shell } from "electron";
function setupMenu() {
@@ -40,7 +41,7 @@ function setupMenu() {
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: "Add to dictionary",
label: strings.addToDictionary(),
click: () =>
globalThis.window?.webContents.session.addWordToSpellCheckerDictionary(
params.misspelledWord
@@ -59,7 +60,7 @@ function setupMenu() {
if (params.linkURL.length) {
menu.append(
new MenuItem({
label: "Open in browser",
label: strings.openInBrowser(),
click: () => shell.openExternal(params.linkURL)
})
);
@@ -68,7 +69,7 @@ function setupMenu() {
if (params.isEditable) {
menu.append(
new MenuItem({
label: "Undo",
label: strings.undo(),
role: "undo",
enabled: params.isEditable,
accelerator: "CommandOrControl+Z"
@@ -77,7 +78,7 @@ function setupMenu() {
menu.append(
new MenuItem({
label: "Redo",
label: strings.redo(),
role: "redo",
enabled: params.isEditable,
accelerator: "CommandOrControl+Y"
@@ -94,7 +95,7 @@ function setupMenu() {
if (params.isEditable)
menu.append(
new MenuItem({
label: "Cut",
label: strings.cut(),
role: "cut",
enabled: params.selectionText.length > 0,
accelerator: "CommandOrControl+X"
@@ -104,7 +105,7 @@ function setupMenu() {
if (params.linkURL?.length) {
menu.append(
new MenuItem({
label: "Copy link",
label: strings.copyLink(),
click() {
clipboard.writeText(params.linkURL);
}
@@ -113,7 +114,7 @@ function setupMenu() {
menu.append(
new MenuItem({
label: "Copy link text",
label: strings.copyLinkText(),
click() {
clipboard.writeText(params.linkText);
}
@@ -124,7 +125,7 @@ function setupMenu() {
if (params.selectionText.length) {
menu.append(
new MenuItem({
label: "Copy",
label: strings.copy(),
role: "copy",
accelerator: "CommandOrControl+C"
})
@@ -135,7 +136,7 @@ function setupMenu() {
menu.append(
new MenuItem({
id: "copy-image",
label: "Copy Image",
label: strings.copyImage(),
click() {
globalThis.window?.webContents.copyImageAt(params.x, params.y);
}
@@ -145,7 +146,7 @@ function setupMenu() {
if (params.isEditable)
menu.append(
new MenuItem({
label: "Paste",
label: strings.paste(),
role: "pasteAndMatchStyle",
enabled: clipboard.readText("clipboard").length > 0,
accelerator: "CommandOrControl+V"

View File

@@ -57,7 +57,8 @@ function getSystemTheme() {
nativeTheme.themeSource = oldThemeSource;
setTimeout(
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
1000
);
return currentTheme;
@@ -72,7 +73,8 @@ function changeTheme(theme: Theme) {
nativeTheme.themeSource = theme;
setTimeout(
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
1000
);
}

View File

@@ -2,7 +2,8 @@
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist/",
"lib": ["ESNext"]
"lib": ["ESNext"],
"moduleResolution": "Bundler"
},
"include": ["src", "global.d.ts"]
}

View File

@@ -38,7 +38,8 @@ DerivedData
*.ipa
*.xcuserstate
*.hprof
ios/.xcode.env.local
**/.xcode.env.local
native/cache
# Android/IntelliJ
#
rn-build-deps/

View File

@@ -16,13 +16,15 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "react-native-gesture-handler";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { I18nManager, View } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import AppLockedOverlay from "./components/app-lock-overlay";
@@ -30,45 +32,46 @@ import { withErrorBoundry } from "./components/exception-handler";
import GlobalSafeAreaProvider from "./components/globalsafearea";
import { useAppEvents } from "./hooks/use-app-events";
import { ApplicationHolder } from "./navigation";
import { NotePreviewConfigure } from "./screens/note-preview-configure";
import { themeTrpcClient } from "./screens/settings/theme-selector";
import Notifications from "./services/notifications";
import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import { IntentService } from "./services/intent";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
const App = () => {
const init = useAppEvents();
IntentService.onLaunch();
const App = (props: { configureMode: "note-preview" }) => {
useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
useEffect(() => {
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
SettingsService.onFirstLaunch();
setTimeout(async () => {
SettingsService.onFirstLaunch();
await Notifications.get();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(true);
}
TipManager.init();
}, 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<View
style={{
height: "100%",
width: "100%"
width: "100%",
justifyContent: "center",
alignItems: "center"
}}
>
<View
@@ -91,7 +94,11 @@ const App = () => {
width: "100%"
}}
>
<ApplicationHolder />
{props.configureMode === "note-preview" ? (
<NotePreviewConfigure />
) : (
<ApplicationHolder />
)}
</GestureHandlerRootView>
<AppLockedOverlay />
</View>
@@ -104,8 +111,8 @@ let currTheme =
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: () => JSX.Element) => {
return function AppWithThemeProvider() {
export const withTheme = (Element: (props: any) => JSX.Element) => {
return function AppWithThemeProvider(props: any) {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,
state.darkTheme,
@@ -124,13 +131,14 @@ export const withTheme = (Element: () => JSX.Element) => {
})
.then((theme) => {
if (theme) {
console.log(theme.version, "theme updated");
theme.colorScheme === "dark"
? useThemeStore.getState().setDarkTheme(theme)
: useThemeStore.getState().setLightTheme(theme);
}
})
.catch(console.log);
.catch(() => {
/* empty */
});
}, 1000);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -143,7 +151,11 @@ export const withTheme = (Element: () => JSX.Element) => {
currTheme = nextTheme;
}
return <Element />;
return (
<I18nProvider i18n={i18n}>
<Element {...props} />
</I18nProvider>
);
};
};

View File

@@ -1,156 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import { MMKV } from "../database/mmkv";
import { useSettingStore } from "../../stores/use-setting-store";
const WEBSITE_ID = "3c6890ce-8410-49d5-8831-15fb2eb28a21";
const baseUrl = "https://analytics.streetwriters.co/api/collect";
const UA =
Platform.OS === "ios"
? "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
: `
Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36`;
/**
*
* @param {Routes} route
* @param {Routes[]} conditions
* @returns
*/
async function canUpdateAnalytics(route, conditions = []) {
if (!useSettingStore?.getState()?.settings?.telemetry) return false;
let eventsList = MMKV.getString("notesnookUserEvents");
if (eventsList) {
eventsList = JSON.parse(eventsList);
}
if (eventsList && eventsList[route]) {
console.log("analytics: event already sent", route);
return false;
}
if (route !== "/welcome") {
for (let cond of conditions) {
if (!eventsList || !eventsList[cond]) {
console.log("analytics: conditions not met for event", route, cond);
return false;
}
}
}
console.log("analytics: will send event", route);
return true;
}
async function saveAnalytics(route, value = true) {
let eventsList = MMKV.getString("notesnookUserEvents");
if (eventsList) {
eventsList = JSON.parse(eventsList);
} else {
eventsList = {};
}
eventsList[route] = value;
MMKV.setString("notesnookUserEvents", JSON.stringify(eventsList));
}
/**
*@typedef {"/welcome" | "/home" | "/signup" | "/first-note" | "/account-created" | "/pro-sheet" | "/pro-plans" | "/iap-native" | "/pro-screen" | "/editor" | "/editor-toolbar" | "/properties" | "/sidemenu"} Routes
* @param {Routes} prevRoute
* @param {Routes} route
* @param {Routes[]} conditions
* @param {boolean} once
* @returns
*/
async function pageView(
route,
prevRoute = "",
conditions = ["/welcome"],
once = true
) {
if (__DEV__) return;
if (!(await canUpdateAnalytics(route, conditions)) && once) return;
let body = {
payload: {
website: WEBSITE_ID,
url: `notesnook-${Platform.OS}${prevRoute}${route}`,
referrer: `https://notesnook.com/notesnook-${Platform.OS}${prevRoute}`,
hostname: `notesnook-${Platform.OS}`,
language: "en-US",
screen: "1920x1080"
},
type: "pageview"
};
try {
let response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA
},
body: JSON.stringify(body)
});
console.log("analytics: event sent", route);
await saveAnalytics(route);
return await response.text();
} catch (e) {
console.log(e);
}
}
async function sendEvent(type, value, once = true) {
if (__DEV__) return;
if (!(await canUpdateAnalytics(type)) && once) return;
let body = {
payload: {
website: WEBSITE_ID,
url: "/",
event_type: type,
event_value: value,
hostname: "notesnook-android-app",
language: "en-US",
screen: "1920x1080"
},
type: "event"
};
try {
let response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA
},
body: JSON.stringify(body)
});
console.log(response.status);
return await response.text();
} catch (e) {
console.log(e);
}
}
export default {
sendEvent,
pageView,
saveAnalytics
};

View File

@@ -1,389 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { MMKV } from "./mmkv";
// Database key cipher is persisted across different user sessions hence it has
// it's independent storage which we will never clear. This is only used when application has
// app lock with password enabled.
export const CipherStorage = new MMKVLoader()
.withInstanceID("cipher_storage")
.setProcessingMode(
Platform.OS === "ios"
? ProcessingModes.MULTI_PROCESS
: ProcessingModes.SINGLE_PROCESS
)
.disableIndexing()
.initialize();
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
const APPLOCK_CIPHER = "applockCipher";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
},
android: {}
});
function generatePassword() {
const length = 80;
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
"Crypto API is not supported. Please upgrade your web browser"
);
}
const charset =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
const indexes = crypto.getRandomValues(new Uint32Array(length));
let secret = "";
for (const index of indexes) {
secret += charset[index % charset.length];
}
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
const key = getDatabaseKey();
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = await encrypt(appLockCredentials, key);
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
databaseKey,
KEYSTORE_CONFIG
);
MMKV.removeItem(DB_KEY_CIPHER);
return true;
}
export async function setAppLockVerificationCipher(appLockPassword) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = await encrypt(appLockCredentials, generatePassword());
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
DatabaseLogger.error(e);
console.log(e);
}
}
export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword) {
try {
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const decrypted = await decrypt(
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
appLockCipher
);
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
);
return typeof decrypted === "string";
} catch (e) {
DatabaseLogger.error(e);
return false;
}
}
let DB_KEY;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword) {
if (DB_KEY) return DB_KEY;
try {
if (appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
let credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
KEYSTORE_CONFIG
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = credentials.password;
}
}
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook",
KEYSTORE_CONFIG
);
if (userKeyCredentials) {
const userKeyCipher = await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_USER_KEY_SALT
},
userKeyCredentials.password
);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
return DB_KEY;
} catch (e) {
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKey(data) {
try {
let credentials = await Sodium.deriveKey(data.password, data.salt);
const userKeyCipher = await encrypt(
{
key: await getDatabaseKey(),
salt: NOTESNOOK_USER_KEY_SALT
},
credentials.key
);
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey(_name) {
try {
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return null;
}
const key = await decrypt(
{
key: await getDatabaseKey()
},
keyCipher
);
DatabaseLogger.info("User key decrypted: ", !!key);
return key;
} catch (e) {
console.log("getCryptoKey", e);
DatabaseLogger.error(e);
}
}
export async function removeCryptoKey(_name) {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
return true;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getRandomBytes(length) {
return await generateSecureRandom(length);
}
export async function hash(password, email) {
let result = await Sodium.hashPassword(password, email);
return result;
}
export async function generateCryptoKey(password, salt) {
try {
let credentials = await Sodium.deriveKey(password, salt || null);
return credentials;
} catch (e) {
DatabaseLogger.error(e);
}
}
export function getAlgorithm(base64Variant) {
return `xcha-argon2i13-${base64Variant}`;
}
export async function decrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
return await Sodium.decrypt(password, _data);
}
export async function decryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
data = data.map((d) => {
d.output = "plain";
return d;
});
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
return await Sodium.decryptMulti(password, data);
}
export function parseAlgorithm(alg) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
return {
encryptionAlgorithm: enc,
kdfAlgorithm: kdf,
compressionAlgorithm: compressionAlg,
isCompress: compressed === "1",
base64_variant: base64variant
};
}
export async function encrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let message = {
type: "plain",
data: data
};
let result = await Sodium.encrypt(password, message);
return {
...result,
alg: getAlgorithm(7)
};
}
export async function encryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let results = await Sodium.encryptMulti(
password,
data.map((item) => ({
type: "plain",
data: item
}))
);
return !results
? []
: results.map((result) => ({
...result,
alg: getAlgorithm(7)
}));
}

View File

@@ -0,0 +1,473 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
import { SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
// Database key cipher is persisted across different user sessions hence it has
// it's independent storage which we will never clear. This is only used when application has
// app lock with password enabled.
export const CipherStorage = new MMKVLoader()
.withInstanceID("cipher_storage")
.setProcessingMode(
Platform.OS === "ios"
? ProcessingModes.MULTI_PROCESS
: ProcessingModes.SINGLE_PROCESS
)
.disableIndexing()
.initialize();
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
const DB_KEY_CIPHER = "databaseKeyCipher";
const USER_KEY_CIPHER = "userKeyCipher";
const APPLOCK_CIPHER = "applockCipher";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
},
android: {}
});
function generatePassword() {
const length = 80;
//@ts-ignore
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
"Crypto API is not supported. Please upgrade your web browser"
);
}
const charset =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
const indexes = crypto.getRandomValues(new Uint32Array(length));
let secret = "";
for (const index of indexes) {
secret += charset[index % charset.length];
}
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
const key = (await getDatabaseKey()) as string;
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = (await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
)) as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
databaseKey,
KEYSTORE_CONFIG
);
MMKV.removeItem(DB_KEY_CIPHER);
return true;
}
export async function setAppLockVerificationCipher(appLockPassword: string) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = (await encrypt(
appLockCredentials,
generatePassword()
)) as Cipher;
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword: string) {
try {
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
DatabaseLogger.info(
`validateAppLockPassword: ${typeof decrypted === "string"}`
);
return typeof decrypted === "string";
} catch (e) {
DatabaseLogger.error(e);
return false;
}
}
let DB_KEY: string | undefined;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword?: string) {
if (DB_KEY) return DB_KEY;
if (appLockPassword) {
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
DatabaseLogger.info("Getting database key from cipher");
DB_KEY = databaseKey;
}
if (!DB_KEY) {
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
}
}
if (!DB_KEY) {
DatabaseLogger.info("Generating new database key");
const password = generatePassword();
const derivedDatabaseKey = await Sodium.deriveKey(
password,
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
"notesnook",
DB_KEY,
KEYSTORE_CONFIG
);
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
}
DatabaseLogger.info("Migrated user credentials to cipher storage");
}
if (!DB_KEY) {
throw new Error(
`Failed to get database key, ${await Keychain.hasInternetCredentials(
KEYCHAIN_SERVER_DBKEY
)}`
);
}
return DB_KEY;
}
export async function deriveCryptoKeyFallback(data: SerializedKey) {
if (Platform.OS !== "ios") return;
try {
if (!data.password || !data.salt)
throw new Error(
"Invalid password and salt provided to deriveCryptoKeyFallback"
);
const credentials = await Sodium.deriveKeyFallback?.(
data.password,
data.salt
);
if (!credentials) return;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key fallback stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function deriveCryptoKey(data: SerializedKey) {
try {
if (!data.password || !data.salt)
throw new Error("Invalid password and salt provided to deriveCryptoKey");
const credentials = (await Sodium.deriveKey(
data.password,
data.salt
)) as Password;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey() {
try {
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return undefined;
}
const key = await decrypt(
{
key: (await getDatabaseKey()) as string,
salt: keyCipher.salt
},
keyCipher
);
return key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function removeCryptoKey() {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
return true;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getRandomBytes(length: number) {
return await generateSecureRandom(length);
}
export async function hash(
password: string,
email: string,
options?: { usesFallback?: boolean }
) {
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
if (options?.usesFallback && Platform.OS !== "ios") {
return "";
}
return (
options?.usesFallback
? await Sodium.hashPasswordFallback?.(password, email)
: await Sodium.hashPassword(password, email)
) as string;
}
export async function generateCryptoKey(password: string, salt?: string) {
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
}
export async function generateCryptoKeyFallback(
password: string,
salt?: string
): Promise<SerializedKey> {
return Sodium.deriveKeyFallback?.(
password,
salt as string
) as Promise<SerializedKey>;
}
export function getAlgorithm(base64Variant: number) {
return `xcha-argon2i13-${base64Variant}`;
}
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
const _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decrypt(key, _data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decrypt(fallbackKey, _data);
} else {
throw e;
}
}
}
return await Sodium.decrypt(password, _data);
}
export async function decryptMulti(
password: Password,
data: Cipher<"base64">[]
) {
data = data.map((d) => {
d.output = "plain";
return d;
});
if (data.length && !password.salt) {
password.salt = data[0].salt;
}
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decryptMulti(key, data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt as string
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decryptMulti(fallbackKey, data);
} else {
throw e;
}
}
}
return await Sodium.decryptMulti(password, data);
}
export function parseAlgorithm(alg: string) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
return {
encryptionAlgorithm: enc,
kdfAlgorithm: kdf,
compressionAlgorithm: compressionAlg,
isCompress: compressed === "1",
base64_variant: base64variant
};
}
export async function encrypt(password: SerializedKey, plainText: string) {
const result = await Sodium.encrypt<"base64">(password, {
type: "plain",
data: plainText
});
return {
...result,
alg: getAlgorithm(7)
};
}
export async function encryptMulti(
password: SerializedKey,
plainText: string[]
) {
const results = await Sodium.encryptMulti<"base64">(
password,
plainText.map((item) => ({
type: "plain",
data: item
}))
);
return !results
? []
: results.map((result) => ({
...result,
alg: getAlgorithm(7)
}));
}

View File

@@ -16,57 +16,50 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./logger";
import { database } from "@notesnook/common";
import { logger as dbLogger } from "@notesnook/core/dist/logger";
import { logger as dbLogger, ICompressor } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import SettingsService from "../../services/settings";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { FileStorage } from "../filesystem";
import { getDatabaseKey } from "./encryption";
import "./logger";
import { RNSqliteDriver } from "./sqlite.kysely";
import { Storage } from "./storage";
database.host(
__DEV__
? {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
// API_HOST: "http://192.168.43.5:5264",
// AUTH_HOST: "http://192.168.43.5:8264",
// SSE_HOST: "http://192.168.43.5:7264",
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
// ISSUES_HOST: "http://192.168.43.5:2624"
}
: {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
}
);
export async function setupDatabase(password) {
export async function setupDatabase(password?: string) {
const key = await getDatabaseKey(password);
if (!key)
throw new Error("Database setup failed, could not get database key");
if (!key) throw new Error(strings.databaseSetupFailed());
console.log("Opening database with key:", !!key);
database.host({
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
MONOGRAPH_HOST: "https://monogr.ph",
...(SettingsService.getProperty("serverUrls") || {})
});
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
},
eventsource: (Platform.OS === "ios"
? EventSource
: AndroidEventSource) as any,
fs: FileStorage,
compressor: async () =>
({
compress: Gzip.deflate,
decompress: Gzip.inflate
} as ICompressor),
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({
@@ -85,4 +78,10 @@ export async function setupDatabase(password) {
}
export const db = database;
export const DatabaseLogger = dbLogger;
let DatabaseLogger = dbLogger.scope(Platform.OS);
const setLogger = () => {
DatabaseLogger = dbLogger.scope(Platform.OS);
};
export { DatabaseLogger, setLogger };

View File

@@ -0,0 +1,49 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { initialize } from "@notesnook/core";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import { Platform } from "react-native";
import { setLogger } from ".";
import { RNSqliteDriver } from "./sqlite.kysely";
let loggerLoaded = false;
const initializeLogger = async () => {
if (loggerLoaded) return;
await initialize({
dialect: (name) => ({
createDriver: () => {
return new RNSqliteDriver({ async: true, dbName: name });
},
createAdapter: () => new SqliteAdapter(),
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler()
}),
tempStore: "memory",
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL"
});
setLogger();
loggerLoaded = true;
};
export { initializeLogger };

View File

@@ -17,11 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type { DatabaseConnection, Driver, QueryResult } from "kysely";
import { CompiledQuery } from "kysely";
import type {
DatabaseConnection,
Driver,
QueryResult
} from "@streetwriters/kysely";
import { CompiledQuery } from "@streetwriters/kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
import { strings } from "@notesnook/intl";
type Config = { dbName: string; async: boolean; location: string };
type Config = { dbName: string; async: boolean; location?: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
@@ -93,7 +98,7 @@ class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error("wasqlite driver doesn't support streaming");
throw new Error(strings.streamingNotSupported());
}
async executeQuery<R>(

View File

@@ -16,58 +16,52 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { IStorage } from "@notesnook/core";
import { MMKVInstance } from "react-native-mmkv-storage";
import {
decrypt,
decryptMulti,
deriveCryptoKey,
deriveCryptoKeyFallback,
encrypt,
encryptMulti,
generateCryptoKey,
getCryptoKey,
getRandomBytes,
hash,
removeCryptoKey,
decryptMulti,
encryptMulti
generateCryptoKeyFallback
} from "./encryption";
import { MMKV } from "./mmkv";
export class KV {
/**
* @type {typeof MMKV}
*/
storage = null;
constructor(storage) {
storage: MMKVInstance;
constructor(storage: MMKVInstance) {
this.storage = storage;
}
async read(key) {
if (!key) return null;
let data = this.storage.getString(key);
if (!data) return null;
async read<T>(key: string, isArray?: boolean) {
if (!key) return undefined;
const data = this.storage.getString(key);
if (!data) return undefined;
try {
let parse = JSON.parse(data);
return parse;
return JSON.parse(data) as T;
} catch (e) {
return data;
return data as T;
}
}
async write(key, data) {
async write<T>(key: string, data: T) {
this.storage.setString(
key,
typeof data === "string" ? data : JSON.stringify(data)
);
return true;
}
async readMulti(keys) {
async readMulti<T>(keys: string[]) {
if (keys.length <= 0) {
return [];
} else {
try {
let data = await this.storage.getMultipleItemsAsync(
const data = await this.storage.getMultipleItemsAsync<any>(
keys.slice(),
"string"
);
@@ -79,24 +73,24 @@ export class KV {
obj = value;
}
return [key, obj];
});
}) as [string, T][];
} catch (e) {
console.log(e);
return [];
}
}
}
async remove(key) {
return this.storage.removeItem(key);
async remove(key: string) {
this.storage.removeItem(key);
}
async removeMulti(keys) {
if (!keys) return true;
return this.storage.removeItems(keys);
async removeMulti(keys: string[]) {
if (!keys) return;
this.storage.removeItems(keys);
}
async clear() {
return this.storage.clearStore();
this.storage.clearStore();
}
async getAllKeys() {
@@ -113,54 +107,46 @@ export class KV {
return keys;
}
async writeMulti(items) {
return this.storage.setMultipleItemsAsync(items, "object");
async writeMulti(items: [string, any][]) {
await this.storage.setMultipleItemsAsync(items, "object");
}
}
const DefaultStorage = new KV(MMKV);
async function requestPermission() {
if (Platform.OS === "ios") return true;
return true;
}
async function checkAndCreateDir(path) {
let dir =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.DocumentDir + path
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
try {
let exists = await RNFetchBlob.fs.exists(dir);
let isDir = await RNFetchBlob.fs.isDir(dir);
if (!exists || !isDir) {
await RNFetchBlob.fs.mkdir(dir);
}
} catch (e) {
await RNFetchBlob.fs.mkdir(dir);
}
return dir;
}
export default {
read: (key) => DefaultStorage.read(key),
write: (key, value) => DefaultStorage.write(key, value),
readMulti: (keys) => DefaultStorage.readMulti(keys),
remove: (key) => DefaultStorage.remove(key),
clear: () => DefaultStorage.clear(),
getAllKeys: () => DefaultStorage.getAllKeys(),
writeMulti: (items) => DefaultStorage.writeMulti(items),
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
export const Storage: IStorage = {
write<T>(key: string, data: T): Promise<void> {
return DefaultStorage.write(key, data);
},
writeMulti<T>(entries: [string, T][]): Promise<void> {
return DefaultStorage.writeMulti(entries);
},
readMulti<T>(keys: string[]): Promise<[string, T][]> {
return DefaultStorage.readMulti(keys);
},
read<T>(key: string, isArray?: boolean): Promise<T | undefined> {
return DefaultStorage.read(key, isArray);
},
remove(key: string): Promise<void> {
return DefaultStorage.remove(key);
},
removeMulti(keys: string[]): Promise<void> {
return DefaultStorage.removeMulti(keys);
},
clear(): Promise<void> {
return DefaultStorage.clear();
},
getAllKeys(): Promise<string[]> {
return DefaultStorage.getAllKeys();
},
hash,
getCryptoKey,
encrypt,
encryptMulti,
decrypt,
decryptMulti,
getRandomBytes,
checkAndCreateDir,
requestPermission,
deriveCryptoKey,
getCryptoKey,
removeCryptoKey,
hash,
generateCryptoKey,
encryptMulti
generateCryptoKeyFallback,
deriveCryptoKeyFallback
};

View File

@@ -20,7 +20,6 @@ import { Dimensions, Image, Platform } from "react-native";
import ImageResizer from "@bam.tech/react-native-image-resizer";
import RNFetchBlob from "react-native-blob-util";
console.log("hello world");
/**
* Scale down & compress images to screen width
* for loading in editor.
@@ -28,13 +27,9 @@ console.log("hello world");
*/
export async function compressToBase64(path, type) {
const { width: screenWidth, scale } = Dimensions.get("window");
console.log("COMPRESSING TO BASE64...");
return new Promise((resolve) => {
console.log(path, "image path...");
Image.getSize(path, async (width) => {
console.log("image width", width);
const response = await ImageResizer.createResizedImage(
path,
screenWidth * scale,
@@ -56,10 +51,12 @@ export async function compressToBase64(path, type) {
: response.uri,
"base64"
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
RNFetchBlob.fs
.unlink(response.uri.replace("file://", ""))
.catch(console.log);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(() => {
/* empty */
});
resolve(base64);
});
@@ -81,6 +78,8 @@ export async function compressToFile(path, type) {
onlyScaleDown: true
}
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
/* empty */
});
return response.uri;
}

View File

@@ -1,304 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import {
getFileNameWithExtension,
isImage,
isDocument
} from "@notesnook/core/dist/utils/filename";
import React from "react";
import { Platform } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import Storage from "../database/storage";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
import { createCacheDir, exists } from "./io";
import { IOS_APPGROUPID } from "../../utils/constants";
export const FileDownloadStatus = {
Success: 1,
Fail: 0
};
/**
* Download all user's attachments
* @returns
*/
export async function downloadAllAttachments() {
const attachments = await db.attachments.all.ids();
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachments
* @param onProgress
* @returns
*/
export async function downloadAttachments(
attachments,
onProgress,
canceled,
groupId
) {
await createCacheDir();
if (!attachments || !attachments.length) return;
const result = new Map();
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
let file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await Storage.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
const zipOutputFile =
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
await RNFetchBlob.fs.unlink(zipSourceFolder);
}
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = await db.attachments.attachment(attachments[i]);
const hash = attachment.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
onProgress?.(
i + 1 / attachments.length,
`Downloading attachments (${i + 1}/${
attachments.length
})... Please wait`
);
// Download to cache
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Success,
attachment: attachment
});
} catch (e) {
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Fail,
reason: e
});
ToastManager.error(e, "Error downloading attachment");
}
}
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (result?.size) {
let sub;
try {
onProgress?.(0, `Zipping... Please wait`);
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
onProgress(
progress,
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
);
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
onProgress(1, `Saving zip file... Please wait`);
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
onProgress?.(1, `Done`);
releasePermissions(outputFolder);
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
}
}
return result;
}
export default async function downloadAttachment(
hash,
global = true,
options = {
silent: false,
cache: false,
throwError: false,
groupId: undefined,
base64: false,
text: false
}
) {
await createCacheDir();
let attachment = await db.attachments.attachment(hash);
if (!attachment) {
DatabaseLogger.log("Attachment not found");
return;
}
let folder = {};
if (!options.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder.uri = await Storage.checkAndCreateDir("/downloads/");
}
}
try {
await db
.fs()
.downloadFile(options.groupId || attachment.hash, attachment.hash);
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
return;
}
if (options.base64 || options.text) {
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
options.base64 ? "base64" : "text"
);
}
let filename = getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
let key = await db.attachments.decryptKey(attachment.key);
let info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
alg: attachment.alg,
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
};
let fileUri = await Sodium.decryptFile(
key,
info,
options.cache ? "cache" : "file"
);
if (!options.silent) {
ToastManager.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"
});
}
if (
attachment.dateUploaded &&
!isImage(attachment.mimeType) &&
!isDocument(attachment.mimeType)
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios" && !options.cache) {
fileUri = folder.uri + `/${filename}`;
}
if (!options.silent) {
presentSheet({
title: "File downloaded",
paragraph: `${filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
return fileUri;
} catch (e) {
DatabaseLogger.error(e);
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(console.log);
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
.catch(console.log);
}
useAttachmentStore.getState().remove(attachment.hash);
if (options.throwError) {
throw e;
}
}
}

View File

@@ -0,0 +1,353 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import { getFileNameWithExtension } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import React from "react";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import filesystem from ".";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { createCacheDir, exists } from "./io";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
export const FileDownloadStatus = {
Success: 1,
Fail: 0
};
/**
* Download all user's attachments
* @returns
*/
export async function downloadAllAttachments() {
const attachments = await db.attachments.all.ids();
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachmentIds
* @param onProgress
* @returns
*/
export async function downloadAttachments(attachmentIds: string[]) {
await createCacheDir();
if (!attachmentIds || !attachmentIds.length) return;
const groupId = `download-all-${Date.now()}`;
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
const file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
const zipOutputFile =
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
await RNFetchBlob.fs.unlink(zipSourceFolder);
}
await RNFetchBlob.fs.mkdir(zipSourceFolder);
const isCancelled = () => {
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
useAttachmentStore.getState().setDownloading({
groupId,
current: 0,
total: 0,
success: false,
message: strings.network.dowloadCancelled(),
canceled: true
});
return true;
}
};
for (let i = 0; i < attachmentIds.length; i++) {
if (isCancelled()) return;
const attachment = await db.attachments.attachment(attachmentIds[i]);
if (!attachment) continue;
const hash = attachment.hash;
try {
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: i + 1,
total: attachmentIds.length,
filename: attachment.hash
});
// Download to cache
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (isCancelled()) return;
if (!uri) throw new Error(strings.failedToDownloadFile());
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
} catch (e) {
DatabaseLogger.error(e);
}
}
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: 0,
total: 0,
success: true
});
if (isCancelled()) return;
let sub;
try {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 1,
message: strings.savingZipFile(),
groupId
});
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
useAttachmentStore.getState().setDownloading({
groupId,
current: progress,
total: 1,
message: strings.savingZipFile((progress * 100).toFixed(1))
});
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
useAttachmentStore.getState().setDownloading({
current: 0,
total: 0,
message: undefined,
success: true,
groupId
});
releasePermissions(outputFolder);
} catch (e) {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 0,
message: undefined,
success: false,
groupId
});
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e as Error, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
/* empty */
});
}
}
export default async function downloadAttachment(
hashOrId: string,
global = true,
options?: {
silent?: boolean;
cache?: boolean;
throwError?: boolean;
groupId?: string;
base64?: boolean;
text?: boolean;
}
) {
await createCacheDir();
const attachment = await db.attachments.attachment(hashOrId);
if (!attachment) {
DatabaseLogger.log("Attachment not found", {
hash: hashOrId
});
return;
}
let folder: {
uri: string;
} | null = null;
if (!options?.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder = {
uri: await filesystem.checkAndCreateDir("/downloads/")
};
}
}
try {
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 0,
total: 1,
filename: attachment.filename
});
await db
.fs()
.downloadFile(
options?.groupId || attachment.hash,
attachment.hash,
attachment.chunkSize
);
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 1,
total: 1,
filename: attachment.filename,
success: true
});
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
return;
}
if (options?.base64 || options?.text) {
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
options.base64 ? "base64" : "text"
);
}
const filename = await getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
const key = await db.attachments.decryptKey(attachment.key);
if (!key) return;
const info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
alg: attachment.alg,
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
fileName: options?.cache ? undefined : filename,
uri: options?.cache ? undefined : folder?.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
};
let fileUri = await Sodium.decryptFile(
key,
info,
options?.cache ? "cache" : "file"
);
if (!options?.silent) {
ToastManager.show({
heading: strings.network.downloadSuccess(),
message: strings.network.fileDownloaded(filename),
type: "success"
});
}
if (Platform.OS === "ios" && !options?.cache) {
fileUri = folder?.uri + `/${filename}`;
}
if (!options?.silent) {
presentSheet({
title: strings.network.fileDownloaded(),
paragraph: strings.fileSaved(filename, Platform.OS),
icon: "download",
context: global ? "global" : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
return fileUri;
} catch (e) {
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(() => {
/* empty */
});
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
.catch(() => {
/* empty */
});
}
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 0,
total: 0,
filename: attachment.filename,
success: false
});
DatabaseLogger.error(e);
useAttachmentStore.getState().remove(attachment.hash);
if (options?.throwError) {
throw e;
}
}
}

View File

@@ -1,153 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import hosts from "@notesnook/core/dist/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { cacheDir, fileCheck } from "./utils";
import { createCacheDir, exists } from "./io";
export async function downloadFile(filename, data, cancelToken) {
if (!data) {
DatabaseLogger.log(`Error downloading file: ${filename}, reason: No data`);
return false;
}
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
let { url, headers } = data;
let path = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
DatabaseLogger.log(`File Exists already: ${filename}`);
return true;
}
let res = await fetch(url, {
method: "GET",
headers
});
if (!res.ok) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
);
throw new Error(`${res.status}: Unable to resolve download url`);
}
const downloadUrl = await res.text();
if (!downloadUrl) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: Unable to resolve download url`
);
throw new Error("Unable to resolve download url");
}
let totalSize = 0;
DatabaseLogger.log(`Download starting: ${filename}`);
let request = RNFetchBlob.config({
path: path,
IOSBackgroundTask: true
})
.fetch("GET", downloadUrl, null)
.progress((recieved, total) => {
useAttachmentStore
.getState()
.setProgress(0, total, filename, recieved, "download");
totalSize = total;
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
DatabaseLogger.log(`Download cancelled: ${filename}`);
};
let response = await request;
await fileCheck(response, totalSize);
let status = response.info().status;
useAttachmentStore.getState().remove(filename);
return status >= 200 && status < 300;
} catch (e) {
if (e.message !== "canceled") {
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
});
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "local"
});
}
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e, {
url,
headers
});
return false;
}
}
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
DatabaseLogger.error(e);
return -1;
}
}
export async function checkAttachment(hash) {
const internetState = await NetInfo.fetch();
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
if (!isInternetReachable) return { success: true };
const attachment = await db.attachments.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {
const size = await getUploadedFileSize(hash);
if (size === -1) return { success: true };
if (size === 0) return { failed: "File length is 0." };
} catch (e) {
return { failed: e?.message };
}
return { success: true };
}

View File

@@ -0,0 +1,210 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { RequestOptions } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { createCacheDir, exists } from "./io";
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
export async function downloadFile(
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) {
if (!requestOptions) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: No requestOptions`
);
return false;
}
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
const { url, headers, chunkSize } = requestOptions;
const tempFilePath = `${cacheDir}/${filename}_temp`;
const originalFilePath = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
DatabaseLogger.log(`File Exists already: ${filename}`);
return true;
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
const size = await getUploadedFileSize(filename);
if (size === -1) {
const error = `${strings.fileVerificationFailed()} (File hash: ${filename})`;
throw new Error(error);
}
if (size === 0) {
const error = `${strings.fileLengthError()} (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
if (attachment && attachment.size !== decryptedLength) {
const error = `${strings.fileLengthMismatch(
attachment.size,
decryptedLength
)} (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
const resolveUrlResponse = await fetch(url, {
method: "GET",
headers
});
if (!resolveUrlResponse.ok) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
);
throw new Error(
`${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
);
}
const downloadUrl = await resolveUrlResponse.text();
if (!downloadUrl) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: Unable to resolve download url`
);
throw new Error(strings.failedToResolvedDownloadUrl());
}
DatabaseLogger.log(`Download starting: ${filename}`);
const request = RNFetchBlob.config({
path: tempFilePath,
IOSBackgroundTask: true,
overwrite: true
})
.fetch("GET", downloadUrl)
.progress(async (recieved, total) => {
useAttachmentStore
.getState()
.setProgress(0, total, filename, recieved, "download");
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});
cancelToken.cancel = async (reason) => {
useAttachmentStore.getState().remove(filename);
request.cancel();
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
/* empty */
});
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
};
const response = await request;
const contentType =
response.info().headers?.["content-type"] ||
response.info().headers?.["Content-Type"];
if (contentType === "application/xml") {
const error = parseS3Error(await response.text());
throw new Error(`[${error.Code}] ${error.Message}`);
}
const status = response.info().status;
useAttachmentStore.getState().remove(filename);
if (await exists(originalFilePath)) {
await RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
/* empty */
});
}
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
if (!(await exists(filename))) {
throw new Error("File size mismatch");
}
return status >= 200 && status < 300;
} catch (e) {
if (
(e as Error).message !== "canceled" &&
!(e as Error).message.includes("NoSuchKey")
) {
const toast = {
heading: strings.downloadError((e as Error).message),
message: (e as Error).message,
type: "error" as const,
context: "global"
};
ToastManager.show(toast);
toast.context = "local";
ToastManager.show(toast);
}
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
/* empty */
});
RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
/* empty */
});
DatabaseLogger.error(e, "Download failed: ", {
url
});
return false;
}
}
export async function checkAttachment(hash: string) {
const internetState = await NetInfo.fetch();
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
if (!isInternetReachable) return;
const attachment = await db.attachments.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {
const size = await getUploadedFileSize(hash);
if (size === -1) return { success: true };
if (size === 0)
return {
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
};
} catch (e) {
return { failed: (e as Error)?.message };
}
return { success: true };
}

View File

@@ -17,19 +17,41 @@ 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 { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
import { IFileStorage } from "@notesnook/core";
import { checkAttachment, downloadFile } from "./download";
import {
bulkExists,
clearCache,
clearFileStorage,
deleteCacheFileByName,
deleteCacheFileByPath,
deleteFile,
exists,
getCacheSize,
hashBase64,
readEncrypted,
writeEncryptedBase64,
hashBase64
writeEncryptedBase64
} from "./io";
import { uploadFile } from "./upload";
import { cancelable } from "./utils";
import {
cancelable,
checkAndCreateDir,
getUploadedFileSize,
requestPermission
} from "./utils";
export default {
checkAttachment,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
getCacheSize,
requestPermission,
checkAndCreateDir,
getUploadedFileSize
};
export const FileStorage: IFileStorage = {
readEncrypted,
writeEncryptedBase64,
hashBase64,
@@ -39,5 +61,5 @@ export default {
exists,
clearFileStorage,
getUploadedFileSize,
checkAttachment
bulkExists
};

View File

@@ -1,202 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
DatabaseLogger.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
return false;
}
let output = await Sodium.decryptFile(
key,
{
...cipherData,
hash: filename,
appGroupId: IOS_APPGROUPID
},
cipherData.outputType === "base64" ? "base64" : "text"
);
DatabaseLogger.log("File decrypted...");
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e);
return false;
}
}
export async function hashBase64(data) {
const hash = await Sodium.hashFile({
type: "base64",
data,
uri: ""
});
return {
hash: hash,
type: "xxh64"
};
}
export async function writeEncryptedBase64(data, key) {
await createCacheDir();
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
let output = await Sodium.encryptFile(key, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
output.size = output.length;
delete output.length;
return {
...output,
alg: "xcha-stream"
};
}
export async function deleteFile(filename, data) {
await createCacheDir();
let delFilePath = cacheDir + `/${filename}`;
if (!data) {
if (!filename) return;
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
return true;
}
let { url, headers } = data;
try {
let response = await RNFetchBlob.fetch("DELETE", url, headers);
let status = response.info().status;
let ok = status >= 200 && status < 300;
if (ok) {
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
}
return ok;
} catch (e) {
console.log("delete file: ", e, url, headers);
return false;
}
}
export async function clearFileStorage() {
try {
let files = await RNFetchBlob.fs.ls(cacheDir);
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
}
for (let file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
}
} catch (e) {
console.log("clearFileStorage", e);
}
}
export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
}
}
export async function migrateFilesFromCache() {
try {
await createCacheDir();
const migratedFilesPath = cacheDir + "/.migrated_1";
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
if (migrated) {
console.log("Files migrated already");
return;
}
let files = await RNFetchBlob.fs.ls(cacheDir);
console.log("Files to migrate:", files.join(","));
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
.catch(console.log);
console.log("Moved", file);
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
console.log("migrateFilesFromCache", e);
}
}
const ABYTES = 17;
export async function exists(filename) {
let path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
// Check if file is present in app group path.
let existsInAppGroup = false;
if (!exists && Platform.OS === "ios") {
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
}
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
const stat = await RNFetchBlob.fs.stat(
existsInAppGroup ? appGroupPath : path
);
if (stat.size !== expectedFileSize) {
DatabaseLogger.log(
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
);
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(console.log);
return false;
}
exists = true;
}
return exists;
}

View File

@@ -0,0 +1,311 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import {
FileEncryptionMetadataWithHash,
FileEncryptionMetadataWithOutputType,
Output,
RequestOptions
} from "@notesnook/core";
import { DataFormat, SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
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";
export async function readEncrypted<TOutputFormat extends DataFormat>(
filename: string,
key: SerializedKey,
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
) {
await migrateFilesFromCache();
DatabaseLogger.log("Read encrypted file...");
const path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
return;
}
const output = await Sodium.decryptFile(
key,
{
...cipherData,
hash: filename,
appGroupId: IOS_APPGROUPID
},
cipherData.outputType === "base64" ? "base64" : "text"
);
DatabaseLogger.log("File decrypted...");
return output as Output<TOutputFormat>;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(() => {
/* empty */
});
DatabaseLogger.error(e);
}
}
export async function hashBase64(data: string) {
const hash = await Sodium.hashFile({
type: "base64",
data,
uri: ""
});
return {
hash: hash,
type: "xxh64"
};
}
export async function writeEncryptedBase64(
data: string,
encryptionKey: SerializedKey,
mimeType: string
): Promise<FileEncryptionMetadataWithHash> {
await createCacheDir();
const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
const output = await Sodium.encryptFile(encryptionKey, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
RNFetchBlob.fs.unlink(filepath).catch(() => {
/* empty */
});
return {
...output,
alg: "xcha-stream"
};
}
export async function deleteFile(
filename: string,
requestOptions?: RequestOptions
): Promise<boolean> {
await createCacheDir();
const localFilePath = cacheDir + `/${filename}`;
if (!requestOptions) {
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
/* empty */
});
return true;
}
const { url, headers } = requestOptions;
try {
const response = await RNFetchBlob.fetch("DELETE", url, headers);
const status = response.info().status;
const ok = status >= 200 && status < 300;
if (ok) {
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
/* empty */
});
}
return ok;
} catch (e) {
DatabaseLogger.error(e, "Delete file", {
url: url
});
return false;
}
}
export async function clearFileStorage() {
try {
const files = await RNFetchBlob.fs.ls(cacheDir);
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (const file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(() => {
/* empty */
});
}
for (const file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(() => {
/* empty */
});
}
} catch (e) {
DatabaseLogger.error(e, "clearFileStorage");
}
}
export async function createCacheDir() {
try {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
}
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function migrateFilesFromCache() {
try {
await createCacheDir();
const migratedFilesPath = cacheDir + "/.migrated_1";
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
if (migrated) {
return;
}
const files = await RNFetchBlob.fs.ls(cacheDir);
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (const file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
.catch(() => {
/* empty */
});
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
DatabaseLogger.error(e, "migrateFilesFromCache");
}
}
export async function clearCache() {
await RNFetchBlob.fs.unlink(cacheDir).catch(() => {
/* empty */
});
await createCacheDir();
eSendEvent("cache-cleared");
}
export async function deleteCacheFileByPath(path: string) {
await RNFetchBlob.fs.unlink(path).catch(() => {
/* empty */
});
}
export async function deleteCacheFileByName(name: string) {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${name}`;
await RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
/* empty */
});
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(() => {
/* empty */
});
}
export async function deleteDCacheFiles() {
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (file.includes("_dcache") || file.startsWith("NN_")) {
await RNFetchBlob.fs.unlink(file).catch(() => {
/* empty */
});
}
}
}
export async function exists(filename: string) {
const path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
// Check if file is present in app group path.
let existsInAppGroup = false;
if (!exists && Platform.OS === "ios") {
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
}
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
const stat = await RNFetchBlob.fs.stat(
existsInAppGroup ? appGroupPath : path
);
if (stat.size !== expectedFileSize) {
DatabaseLogger.log(
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
);
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(() => {
/* empty */
});
return false;
}
exists = true;
}
return exists;
}
export async function bulkExists(files: string[]) {
try {
await createCacheDir();
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
if (Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
missingFiles = missingFiles.filter(
(file) => !appGroupFiles.includes(file)
);
}
return missingFiles;
} catch (e) {
DatabaseLogger.error(e);
return [];
}
}
export async function getCacheSize() {
await createCacheDir();
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
let total = 0;
stat.forEach((file) => {
total += parseInt(file.size as unknown as string);
});
return total;
}

View File

@@ -1,132 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import RNFetchBlob from "react-native-blob-util";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { cacheDir } from "./utils";
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
import { createCacheDir } from "./io";
import { getUploadedFileSize } from "./download";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
let { url, headers } = data;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
const uploadedFileSize = await getUploadedFileSize(filename);
if (uploadedFileSize === -1) {
DatabaseLogger.log("Upload verification failed.");
return false;
}
const isUploaded = uploadedFileSize !== 0;
if (isUploaded) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let res = await fetch(url, {
method: "PUT",
headers
});
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
const uploadUrl = await res.text();
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
let uploadFilePath = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(uploadFilePath);
}
if (!exists) {
throw new Error(
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
let request = RNFetchBlob.config({
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
"PUT",
uploadUrl,
{
"content-type": ""
},
RNFetchBlob.wrap(uploadFilePath)
)
.uploadProgress((sent, total) => {
useAttachmentStore
.getState()
.setProgress(sent, total, filename, 0, "upload");
DatabaseLogger.info(
`File upload progress: ${filename}, ${sent}/${total}`
);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
};
let response = await request;
let status = response.info().status;
let text = await response.text();
let result = status >= 200 && status < 300 && text.length === 0;
useAttachmentStore.getState().remove(filename);
if (result) {
DatabaseLogger.info(
`File upload status: ${filename}, ${status}, ${text}`
);
let attachment = await db.attachments.attachment(filename);
if (!attachment) return result;
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
} else {
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
throw new Error(
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(response.info())}`
);
}
return result;
} catch (e) {
useAttachmentStore.getState().remove(filename);
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
DatabaseLogger.error(e);
return false;
}
}

View File

@@ -0,0 +1,147 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { createCacheDir } from "./io";
import {
cacheDir,
checkUpload,
FileSizeResult,
getUploadedFileSize
} from "./utils";
export async function uploadFile(
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) {
if (!requestOptions) return false;
const { url, headers } = requestOptions;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
let filePath = `${cacheDir}/${filename}`;
let exists = await RNFetchBlob.fs.exists(filePath);
// Check for file in appGroupPath if it doesn't exist in cacheDir
if (!exists && Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
filePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(filePath);
}
if (!exists) {
throw new Error(
`Trying to upload file at path ${filePath} that doest not exist.`
);
}
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
const remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === FileSizeResult.Error) return false;
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
const uploadUrlResponse = await fetch(url, {
method: "PUT",
headers
});
const uploadUrl = uploadUrlResponse.ok
? await uploadUrlResponse.text()
: await uploadUrlResponse.json();
if (typeof uploadUrl !== "string") {
throw new Error(
uploadUrl.error || "Unable to resolve attachment upload url."
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
const uploadRequest = RNFetchBlob.config({
//@ts-ignore
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
"PUT",
uploadUrl,
{
"content-type": ""
},
RNFetchBlob.wrap(filePath)
)
.uploadProgress((sent, total) => {
useAttachmentStore
.getState()
.setProgress(sent, total, filename, 0, "upload");
DatabaseLogger.info(
`File upload progress: ${filename}, ${sent}/${total}`
);
});
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
uploadRequest.cancel();
};
const uploadResponse = await uploadRequest;
const status = uploadResponse.info().status;
const uploaded = status >= 200 && status < 300;
useAttachmentStore.getState().remove(filename);
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(uploadResponse.info())}`
);
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
return uploaded;
} catch (e) {
useAttachmentStore.getState().remove(filename);
ToastManager.error(e as Error, "File upload failed");
DatabaseLogger.error(e, "File upload failed", {
filename
});
return false;
}
}

View File

@@ -1,93 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import * as ScopedStorage from "react-native-scoped-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
export const cacheDir =
Platform.OS == "ios"
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
export function getRandomId(prefix) {
return Math.random()
.toString(36)
.replace("0.", prefix || "");
}
export function extractValueFromXmlTag(code, xml) {
if (!xml.includes(code)) return `Unknown ${code}`;
return xml.slice(
xml.indexOf(`<${code}>`) + code.length + 2,
xml.indexOf(`</${code}>`)
);
}
export async function fileCheck(response, totalSize) {
if (totalSize < 1000) {
let text = await response.text();
if (text.startsWith("<?xml")) {
let errorJson = {
Code: extractValueFromXmlTag("Code", text),
Message: extractValueFromXmlTag("Message", text)
};
throw new Error(`${errorJson.Code}: ${errorJson.Message}`);
}
}
}
export function cancelable(operation) {
const cancelToken = {
cancel: () => {}
};
return (filename, { url, headers }) => {
return {
execute: () => operation(filename, { url, headers }, cancelToken),
cancel: async () => {
await cancelToken.cancel();
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
};
};
}
export function copyFileAsync(source, dest) {
return new Promise((resolve, reject) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
if (e) {
reject(e);
return;
}
resolve();
});
});
}
export async function releasePermissions(path) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (let uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}

View File

@@ -0,0 +1,175 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { hosts, RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { DatabaseLogger, db } from "../database";
export const ABYTES = 17;
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
export const cacheDir =
Platform.OS == "ios"
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
export function getRandomId(prefix: string) {
return Math.random()
.toString(36)
.replace("0.", prefix || "");
}
export function parseS3Error(data?: string) {
const xml = typeof data === "string" ? data : null;
const error = {
Code: "UNKNOWN",
Message: xml
};
try {
if (!xml) return error;
const doc = new DOMParser().parseFromString(xml, "text/xml");
const ErrorElement = doc.getElementsByTagName("Error")[0];
if (!ErrorElement) return error;
for (const child of ErrorElement.children) {
if (
child.textContent &&
(child.tagName === "Code" || child.tagName === "Message")
)
error[child.tagName] = child.textContent;
}
return error;
} catch (e) {
return error;
}
}
export function cancelable(
operation: (
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) => Promise<boolean>
) {
const cancelToken = {
cancel: async (reason?: string) => {}
};
return (filename: string, requestOptions: RequestOptions) => {
return {
execute: () => operation(filename, requestOptions, cancelToken),
cancel: async () => {
await cancelToken.cancel();
}
};
};
}
export function copyFileAsync(source: string, dest: string) {
return new Promise((resolve, reject) => {
//@ts-ignore
ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
if (e) {
reject(e);
return;
}
resolve(true);
});
});
}
export async function releasePermissions(path: string) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (const uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}
export const FileSizeResult = {
Empty: 0,
Error: -1
};
export async function getUploadedFileSize(hash: string) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length") || "0"
);
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
} catch (e) {
DatabaseLogger.error(e);
return FileSizeResult.Error;
}
}
export async function checkUpload(
filename: string,
chunkSize: number,
expectedSize: number
) {
const size = await getUploadedFileSize(filename);
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
const error =
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;
if (error) throw new Error(error);
}
export async function requestPermission() {
if (Platform.OS === "ios") return true;
return true;
}
export async function checkAndCreateDir(path: string) {
const dir =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.DocumentDir + path
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
try {
const exists = await RNFetchBlob.fs.exists(dir);
const isDir = await RNFetchBlob.fs.isDir(dir);
if (!exists || !isDir) {
await RNFetchBlob.fs.mkdir(dir);
}
} catch (e) {
await RNFetchBlob.fs.mkdir(dir);
}
return dir;
}

View File

@@ -17,9 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function info(context: string, ...logs: unknown[]) {
console.log(`${new Date().toLocaleDateString()}::info::${context}:`, ...logs);
}
function info(context: string, ...logs: unknown[]) {}
function error(context: string, ...logs: unknown[]) {
console.log(

View File

@@ -20,15 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Linking, View } from "react-native";
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Sync from "../../services/sync";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eCloseAnnouncementDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { PricingPlans } from "../premium/pricing-plans";
import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import { allowedOnPlatform, getStyle } from "./functions";
@@ -43,7 +41,9 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
await sleep(500);
}
if (item.type === "link") {
Linking.openURL(item.data).catch(console.log);
Linking.openURL(item.data).catch(() => {
/* empty */
});
} else if (item.type === "promo") {
presentSheet({
component: (
@@ -56,13 +56,6 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
/>
)
});
} else if (item.type === "force-sync") {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, true, () => {
eSendEvent(eCloseSheet);
});
}
};
return (

View File

@@ -18,10 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useRef } from "react";
import { AppStateStatus, Platform, TextInput, View } from "react-native";
import {
AppStateStatus,
Platform,
TextInput,
useWindowDimensions,
View
} from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import { enabled } from "react-native-privacy-snapshot";
import { DatabaseLogger } from "../../common/database";
import {
decrypt,
@@ -34,17 +39,21 @@ import {
import { MMKV } from "../../common/database/mmkv";
import { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import { NotesnookModule } from "../../utils/notesnook-module";
import { SIZE } from "../../utils/size";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { strings } from "@notesnook/intl";
const getUser = () => {
const user = MMKV.getString("user");
@@ -85,6 +94,7 @@ const AppLockedOverlay = () => {
const appState = useAppState();
const lastAppState = useRef<AppStateStatus>(appState);
const biometricUnlockAwaitingUserInput = useRef(false);
const { height } = useWindowDimensions();
const keyboardType = useSettingStore(
(state) => state.settings.applockKeyboardType
);
@@ -119,7 +129,6 @@ const AppLockedOverlay = () => {
);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = undefined;
}
biometricUnlockAwaitingUserInput.current = false;
@@ -146,8 +155,13 @@ const AppLockedOverlay = () => {
}
lockApp(false);
enabled(false);
password.current = undefined;
} else {
ToastManager.show({
heading: strings.invalid(keyboardType),
type: "error",
context: "local"
});
}
} catch (e) {
console.error(e);
@@ -155,10 +169,6 @@ const AppLockedOverlay = () => {
};
useEffect(() => {
if (appState === "active") {
enabled(false);
}
const prevState = lastAppState.current;
lastAppState.current = appState;
@@ -184,31 +194,28 @@ const AppLockedOverlay = () => {
DatabaseLogger.info("Biometric unlock request");
onUnlockAppRequested();
}
enabled(false);
} else {
SettingsService.appEnteredBackground();
if (
SettingsService.get().privacyScreen ||
SettingsService.getProperty("appLockEnabled")
) {
enabled(true);
}
}
}, [appState, onUnlockAppRequested, appLocked]);
return appLocked ? (
<View
<KeyboardAwareScrollView
style={{
backgroundColor: colors.primary.background,
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999,
justifyContent: "center"
zIndex: 999
}}
contentContainerStyle={{
justifyContent: "center",
minHeight: height
}}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
>
<Toast context="local" />
<View
style={{
flex: 1,
@@ -244,7 +251,7 @@ const AppLockedOverlay = () => {
textAlign: "center"
}}
>
Unlock your notes
{strings.unlockNotes()}
</Heading>
<Paragraph
@@ -254,7 +261,7 @@ const AppLockedOverlay = () => {
maxWidth: "90%"
}}
>
{"Please verify it's you"}
{strings.verifyItsYou()}
</Paragraph>
<Seperator />
<View
@@ -272,13 +279,13 @@ const AppLockedOverlay = () => {
keyboardType={
appLockHasPasswordSecurity ? keyboardType : "default"
}
placeholder={`Enter ${
placeholder={
appLockHasPasswordSecurity
? `app lock ${
keyboardType === "numeric" ? "pin" : "password"
}`
: "account password"
}`}
? keyboardType === "numeric"
? strings.enterApplockPassword()
: strings.enterApplockPin()
: strings.enterAccountPassword()
}
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();
@@ -295,7 +302,7 @@ const AppLockedOverlay = () => {
{user || appLockHasPasswordSecurity ? (
<>
<Button
title="Continue"
title={strings.continue()}
type="accent"
onPress={onSubmit}
width={250}
@@ -311,7 +318,7 @@ const AppLockedOverlay = () => {
{biometricsAuthEnabled ? (
<Button
title="Unlock with Biometrics"
title={strings.unlockWithBiometrics()}
width={250}
onPress={onUnlockAppRequested}
icon={"fingerprint"}
@@ -321,7 +328,7 @@ const AppLockedOverlay = () => {
</View>
</View>
</View>
</View>
</KeyboardAwareScrollView>
) : null;
};

View File

@@ -30,6 +30,8 @@ import filesystem from "../../common/filesystem";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import picker from "../../screens/editor/tiptap/picker";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import {
ToastManager,
eSendEvent,
@@ -49,22 +51,23 @@ import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { openNote } from "../list-items/note/wrapper";
import { DateMeta } from "../properties/date-meta";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { Notice } from "../ui/notice";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import SheetProvider from "../sheet-provider";
import { strings } from "@notesnook/intl";
const Actions = ({
attachment,
close,
setAttachments,
fwdRef
fwdRef,
context
}: {
attachment: Attachment;
context: string;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close?: () => void;
fwdRef: RefObject<ActionSheetRef>;
@@ -78,26 +81,25 @@ const Actions = ({
const [loading, setLoading] = useState<{
name?: string;
}>({});
const actions = [
{
name: "Download",
name: strings.network.download(),
onPress: async () => {
if (currentProgress) {
await db.fs().cancel(attachment.hash);
useAttachmentStore.getState().remove(attachment.hash);
}
downloadAttachment(attachment.hash, false);
eSendEvent(eCloseSheet, contextId);
downloadAttachment(attachment.hash, context === "global");
fwdRef.current?.hide();
},
icon: "download"
},
{
name: "Reupload",
name: strings.network.reupload(),
onPress: async () => {
if (!PremiumService.get()) {
ToastManager.show({
heading: "Upgrade to pro",
heading: strings.upgradeToPro(),
type: "error",
context: "local"
});
@@ -113,17 +115,19 @@ const Actions = ({
icon: "upload"
},
{
name: "Run file check",
name: strings.fileCheck(),
onPress: async () => {
setLoading({
name: "Run file check"
name: strings.fileCheck()
});
const res = await filesystem.checkAttachment(attachment.hash);
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
const result = await filesystem.checkAttachment(attachment.hash);
if (!result) return;
if (result.failed) {
db.attachments.markAsFailed(attachment.id, result.failed);
setFailed(result.failed);
ToastManager.show({
heading: "File check failed with error: " + res.failed,
heading: strings.fileCheckFailed(result.failed),
type: "error",
context: "local"
});
@@ -132,7 +136,7 @@ const Actions = ({
db.attachments.markAsFailed(attachment.id);
eSendEvent(eDBItemUpdate, attachment.id);
ToastManager.show({
heading: "File check passed",
heading: strings.fileCheckPassed(),
type: "success",
context: "local"
});
@@ -146,13 +150,11 @@ const Actions = ({
icon: "file-check"
},
{
name: "Rename",
name: strings.rename(),
onPress: () => {
presentDialog({
context: contextId as any,
input: true,
title: "Rename file",
paragraph: "Enter a new name for the file",
title: strings.renameFile(),
defaultValue: attachment.filename,
positivePress: async (value) => {
if (value && value.length > 0) {
@@ -165,13 +167,13 @@ const Actions = ({
eSendEvent(eDBItemUpdate, attachment.id);
}
},
positiveText: "Rename"
positiveText: strings.rename()
});
},
icon: "form-textbox"
},
{
name: "Delete",
name: strings.delete(),
onPress: async () => {
const relations = await db.relations.to(attachment, "note").get();
await db.attachments.remove(attachment.hash, false);
@@ -217,6 +219,7 @@ const Actions = ({
style={{
maxHeight: "100%"
}}
keyboardShouldPersistTaps="never"
>
<Dialog context={contextId} />
<SheetProvider context={contextId} />
@@ -254,8 +257,7 @@ const Actions = ({
{notes.length ? (
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{notes.length} note
{notes.length > 1 ? "s" : ""}
{strings.notes(notes.length)}
</Paragraph>
) : null}
<Paragraph
@@ -263,7 +265,7 @@ const Actions = ({
Clipboard.setString(attachment.hash);
ToastManager.show({
type: "success",
heading: "Attachment hash copied",
heading: strings.hashCopied(),
context: "local"
});
}}
@@ -293,7 +295,7 @@ const Actions = ({
}}
size={SIZE.sm}
>
List of notes:
{strings.listOf()} {strings.dataTypesPlural.note()}:
</Heading>
{notes.map((item) => (
@@ -352,7 +354,7 @@ const Actions = ({
{failed ? (
<Notice
type="alert"
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
text={strings.fileCheckFailed(failed)}
size="small"
/>
) : null}
@@ -374,6 +376,7 @@ Actions.present = (
setAttachments={set}
close={close}
attachment={attachment}
context={context || "global"}
/>
)
});

View File

@@ -22,7 +22,6 @@ import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useDBItem } from "../../hooks/use-db-item";
@@ -31,6 +30,8 @@ import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
import { strings } from "@notesnook/intl";
import { Pressable } from "../ui/pressable";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
@@ -44,7 +45,8 @@ export const AttachmentItem = ({
setAttachments,
pressable = true,
hideWhenNotDownloading,
context
context,
errorOnly
}: {
id: string | number;
attachments?: VirtualizedGrouping<Attachment>;
@@ -53,9 +55,9 @@ export const AttachmentItem = ({
pressable?: boolean;
hideWhenNotDownloading?: boolean;
context?: string;
errorOnly?: boolean;
}) => {
const [attachment] = useDBItem(id, "attachment", attachments);
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
@@ -67,10 +69,8 @@ export const AttachmentItem = ({
Actions.present(attachment, setAttachments, context);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
return errorOnly && attachment && !attachment?.failed ? null : (
<Pressable
onPress={onPress}
style={{
flexDirection: "row",
@@ -78,8 +78,6 @@ export const AttachmentItem = ({
justifyContent: "space-between",
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.secondary.background,
minHeight: 45
}}
>
@@ -89,41 +87,46 @@ export const AttachmentItem = ({
style={{
flexShrink: 1,
flexDirection: "row",
alignItems: "center"
alignItems: "center",
gap: 10
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginLeft: -5
marginLeft: -5,
borderWidth: 1,
borderColor: colors.secondary.border,
paddingHorizontal: 2,
width: 30,
height: 30,
borderRadius: 5
}}
>
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
<Paragraph
adjustsFontSizeToFit
size={6}
color={colors.static.white}
size={8}
color={colors.secondary.paragraph}
style={{
position: "absolute"
maxWidth: 30
}}
numberOfLines={1}
>
{getFileExtension(attachment.filename).toUpperCase()}
{getFileExtension(attachment.filename).toUpperCase() ||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
</Paragraph>
</View>
<View
style={{
flexShrink: 1,
marginLeft: 10
flexShrink: 1
}}
>
<Paragraph
size={SIZE.sm - 1}
size={SIZE.sm}
style={{
flexWrap: "wrap",
marginBottom: 2.5
flexWrap: "wrap"
}}
numberOfLines={1}
lineBreakMode="middle"
@@ -133,11 +136,8 @@ export const AttachmentItem = ({
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.size)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
{strings.fileSize()}: {formatBytes(attachment.size)}
</Paragraph>
) : null}
</View>
@@ -186,6 +186,6 @@ export const AttachmentItem = ({
)}
</>
)}
</TouchableOpacity>
</Pressable>
);
};

View File

@@ -1,297 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { Platform, View } from "react-native";
import { db } from "../../common/database";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import {
PresentSheetOptions,
presentSheet
} from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { ProgressBarComponent } from "../ui/svg/lazy";
import { useThemeColors } from "@notesnook/theme";
import { FlatList } from "react-native-actions-sheet";
import { AttachmentItem } from "./attachment-item";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
const DownloadAttachments = ({
close,
attachments,
isNote,
update
}: {
attachments: VirtualizedGrouping<Attachment>;
close?: ((ctx?: string | undefined) => void) | undefined;
isNote?: boolean;
update?: (props: PresentSheetOptions) => void;
}) => {
const { colors } = useThemeColors();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
value: 0,
statusText: "Download started... Please wait"
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef<string>();
const onDownload = async () => {
update?.({
disableClosing: true
} as PresentSheetOptions);
setDownloading(true);
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
await attachments.ids(),
(progress: number, statusText: string) =>
setProgress({ value: progress, statusText }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update?.({
disableClosing: false
} as PresentSheetOptions);
};
const cancel = async () => {
update?.({
disableClosing: false
} as PresentSheetOptions);
canceled.current = true;
if (!groupId.current) return;
console.log(groupId.current, "canceling groupId downloads");
await db.fs().cancel(groupId.current);
setDownloading(false);
setResult(new Map());
groupId.current = undefined;
};
const failedResults = () => {
const results = [];
for (const value of result.values()) {
if (value.status === 0) results.push(value.attachment);
}
return results;
};
function getResultText() {
const downloadedAttachmentsCount =
attachments?.placeholders?.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments?.placeholders.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
}
return (
<View
style={{
alignItems: "center",
width: "100%",
paddingVertical: 12,
paddingHorizontal: 12
}}
>
<Heading>
{downloading
? "Downloading attachments"
: result?.size
? "Downloaded attachments"
: "Download attachments"}
</Heading>
{downloading ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{progress.statusText}
</Paragraph>
) : result?.size ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{getResultText()}
</Paragraph>
) : (
<Paragraph
style={{
textAlign: "center"
}}
>
Are you sure you want to download all attachments
{isNote ? " of this note?" : "?"}
</Paragraph>
)}
{downloading ? (
<View
style={{
width: 200,
marginTop: 10
}}
>
<ProgressBarComponent
height={5}
width={null}
animated={true}
useNativeDriver
progress={
progress.value
? progress.value / attachments.placeholders?.length
: 0
}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
) : null}
<FlatList
style={{
maxHeight: 300,
width: "100%",
minHeight: 60,
backgroundColor: colors.secondary.background,
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments.placeholders : undefined}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 60
}}
>
<Paragraph color={colors.secondary.paragraph}>
No downloads in progress.
</Paragraph>
</View>
}
keyExtractor={(index) => "attachment_download" + index}
renderItem={({ index }) => {
return (
<AttachmentItem
id={index}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
attachments={attachments}
/>
);
}}
/>
{result?.size ? (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={() => {
close?.();
}}
type="accent"
title="Done"
/>
) : !downloading ? (
<View
style={{
flexDirection: "row",
width: "100%",
marginTop: 20
}}
>
<Button
style={{
flex: 1,
borderRadius: 100,
marginRight: 5
}}
onPress={() => {
close?.();
}}
type="secondary"
title="No"
/>
<Button
style={{
flex: 1,
borderRadius: 100,
marginLeft: 5
}}
onPress={onDownload}
type="accent"
title="Yes"
/>
</View>
) : (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={cancel}
type="error"
title="Cancel"
/>
)}
</View>
);
};
DownloadAttachments.present = (
context: string,
attachments: VirtualizedGrouping<Attachment>,
isNote?: boolean
) => {
presentSheet({
context: context,
component: (ref, close, update) => (
<DownloadAttachments
close={close}
attachments={attachments}
isNote={isNote}
update={update}
/>
)
});
};
export default DownloadAttachments;

View File

@@ -19,21 +19,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
Attachment,
FilteredSelector,
Note,
SortOptions,
VirtualizedGrouping
} from "@notesnook/core";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { ActivityIndicator, View } from "react-native";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import { ScrollView } from "react-native-gesture-handler";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create, { State } from "zustand";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { presentSheet } from "../../services/event-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { SIZE } from "../../utils/size";
import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { Header } from "../header";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
@@ -42,22 +50,105 @@ import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
sortDirection: "desc"
};
export const AttachmentDialog = ({ note }: { note?: Note }) => {
type RecheckerProgress = {
failed: number;
passed: number;
isWorking: boolean;
shown: boolean;
filter: string;
};
interface RecheckerState extends State {
progress: {
[key: string]: RecheckerProgress;
};
setProgress: (key: string, progress: RecheckerProgress) => void;
currentFilter: string;
}
const useRechecker = create<RecheckerState>((set) => ({
progress: {
all: {
failed: 0,
passed: 0,
isWorking: false,
shown: false,
filter: "all"
}
},
currentFilter: "all",
setProgress: (key: string, progress: RecheckerProgress) => {
set((state) => ({
progress: {
...state.progress,
[key]: progress
}
}));
}
}));
const attachmentTypes = [
{
title: strings.mediaTypes.all(),
filterBy: "all"
},
{
title: strings.mediaTypes.image(),
filterBy: "images"
},
{
title: strings.mediaTypes.audio(),
filterBy: "audio"
},
{
title: strings.mediaTypes.video(),
filterBy: "video"
},
{
title: strings.mediaTypes.document(),
filterBy: "documents"
},
{
title: strings.mediaTypes.orphaned(),
filterBy: "orphaned"
},
{
title: strings.mediaTypes.errors(),
filterBy: "errors"
}
];
export const AttachmentDialog = ({
note,
isSheet
}: {
note?: Note;
isSheet: boolean;
}) => {
const { colors } = useThemeColors();
const { height } = useSettingStore((state) => state.dimensions);
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>();
const [loading, setLoading] = useState(true);
const searchTimer = useRef<NodeJS.Timeout>();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
const rechecker = useRechecker((state) =>
note ? state.progress[note.id] || {} : state.progress.all
);
const currentFilterRef = useRef(currentFilter);
currentFilterRef.current = currentFilter;
useEffect(() => {
useRechecker.setState({
currentFilter: currentFilter
});
}, [currentFilter]);
const refresh = React.useCallback(() => {
if (note) {
@@ -68,6 +159,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
sortBy: "dateModified"
})
.then((attachments) => {
setLoading(false);
setAttachments(attachments);
});
} else {
@@ -76,7 +168,10 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
...DEFAULT_SORTING,
sortBy: "dateModified"
})
.then((attachments) => setAttachments(attachments));
.then((attachments) => {
setAttachments(attachments);
setLoading(false);
});
}
}, [note]);
@@ -108,52 +203,70 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
setAttachments={async () => {
setAttachments(await filterAttachments(currentFilter));
}}
errorOnly={currentFilter === "errors"}
attachments={attachments}
id={index}
context="attachments-list"
context={!isSheet ? "global" : "attachments-list"}
/>
);
const onCheck = async () => {
if (!attachments) return;
setLoading(true);
const getState = () =>
useRechecker.getState().progress[note?.id || "all"] || {};
const setState = (state: Partial<RecheckerProgress>) =>
useRechecker.getState().setProgress(note?.id || "all", {
...getState(),
...state
});
for (let i = 0; i < attachments.placeholders.length; i++) {
if (!attachments || getState().isWorking) return;
setState({
isWorking: true,
failed: 0,
passed: 0,
shown: true,
filter: currentFilterRef.current
});
const filter = currentFilterRef.current;
const filteredAttachments = await filterAttachments(
currentFilterRef.current
);
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
if (!getState().isWorking) {
ToastManager.show({
message: strings.attachmentRecheckCancelled(),
type: "info",
context: isSheet ? "local" : "global"
});
return;
}
const attachment = (await attachments.item(i))?.item;
if (currentFilter == "errors" && !attachment?.failed) continue;
if (!attachment) continue;
const result = await filesystem.checkAttachment(attachment.hash);
if (!result) return;
if (result.failed) {
setState({
failed: getState().failed + 1
});
await db.attachments.markAsFailed(attachment.hash, result.failed);
} else {
await db.attachments.markAsFailed(attachment.id, undefined);
setState({
passed: getState().passed + 1
});
await db.attachments.markAsFailed(attachment.id);
}
}
refresh();
setLoading(false);
};
const attachmentTypes = [
{
title: "All",
filterBy: "all"
},
{
title: "Images",
filterBy: "images"
},
{
title: "Documents",
filterBy: "documents"
},
{
title: "Video",
filterBy: "video"
},
{
title: "Audio",
filterBy: "audio"
if (filter === useRechecker.getState().currentFilter) {
setAttachments(await filterAttachments(currentFilter));
}
];
setState({
isWorking: false
});
};
const filterAttachments = async (type: string) => {
let items: FilteredSelector<Attachment> = db.attachments.all;
@@ -183,6 +296,14 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
items = note
? db.attachments.ofNote(note.id, "documents")
: db.attachments.documents;
break;
case "orphaned":
items = db.attachments.orphaned;
break;
case "errors":
items = items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.all;
}
return await items.sorted({
@@ -192,39 +313,59 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
};
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
height: height * 0.85
}}
>
<SheetProvider context="attachments-list" />
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading>Attachments</Heading>
<>
{isSheet ? (
<>
<SheetProvider context="attachments-list" />
<Dialog context="local" />
</>
) : null}
{!isSheet ? (
<Header
title={strings.manageAttachments()}
renderedInRoute="SettingsGroup"
canGoBack
headerRightButtons={[
{
onPress() {
onCheck();
},
title: strings.recheckAll()
},
{
onPress() {
if (!attachments) return;
presentDialog({
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: strings.cancel()
});
},
title: strings.downloadAllAttachments()
}
]}
/>
) : (
<View
style={{
flexDirection: "row"
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 12
}}
>
{loading ? (
<ActivityIndicator
style={{
height: 40,
width: 40,
marginRight: 10
}}
size={SIZE.lg}
/>
) : (
<Heading>{strings.dataTypesPluralCamelCase.attachment()}</Heading>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
name="check-all"
style={{
@@ -236,125 +377,244 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
size={SIZE.lg}
onPress={onCheck}
/>
)}
<IconButton
name="download"
style={{
height: 40,
width: 40
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
!!note
);
}}
size={SIZE.lg}
/>
</View>
</View>
<Seperator />
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
}}
/>
<View>
<ScrollView
style={{
width: "100%",
height: 50,
flexDirection: "row",
backgroundColor: colors.primary.background
}}
contentContainerStyle={{
minWidth: "100%",
height: 50
}}
horizontal
>
{attachmentTypes.map((item) => (
<Button
type={
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
}
key={item.title}
title={item.title}
<IconButton
name="download"
style={{
borderRadius: 0,
borderBottomWidth: 1,
flexGrow: 1,
borderBottomColor:
currentFilter !== item.filterBy
? "transparent"
: colors.primary.accent
height: 40,
width: 40
}}
onPress={async () => {
setCurrentFilter(item.filterBy);
setAttachments(await filterAttachments(item.filterBy));
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
presentDialog({
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: strings.cancel()
});
}}
size={SIZE.lg}
/>
))}
</ScrollView>
</View>
<FlashList
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
ListEmptyComponent={
<View
style={{
height: 150,
justifyContent: "center",
alignItems: "center"
}}
>
<Icon name="attachment" size={60} color={colors.secondary.icon} />
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
</View>
}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
estimatedItemSize={50}
data={attachments?.placeholders}
renderItem={renderItem}
/>
</View>
)}
<Paragraph
color={colors.secondary.paragraph}
size={SIZE.xs}
<View
style={{
textAlign: "center",
marginTop: 10
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
height: "100%"
}}
>
<Icon
name="shield-key-outline"
size={SIZE.xs}
color={colors.primary.icon}
<Seperator />
<Input
placeholder={strings.filterAttachments()}
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
}}
/>
{" "}All attachments are end-to-end encrypted.
</Paragraph>
{rechecker.shown ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
borderRadius: 10,
padding: 10,
borderWidth: 1,
borderColor: colors.primary.border,
gap: 12,
justifyContent: "space-between"
}}
>
<View
style={{
flexDirection: "row",
gap: 12,
alignItems: "center"
}}
>
{rechecker.isWorking ? (
<ActivityIndicator color={colors.primary.accent} />
) : (
<Icon
name="file-check"
size={30}
color={colors.primary.accent}
/>
)}
<View>
<Paragraph>
{rechecker.isWorking
? note
? strings.checkingNoteAttachments()
: strings.checkingAllAttachments()
: strings.attachmentRecheckComplete()}
</Paragraph>
<Paragraph>
{`${
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
}${strings.passed()}: ${
rechecker.passed
}, ${strings.failed()}: ${rechecker.failed}`}
</Paragraph>
</View>
</View>
<IconButton
type={rechecker.isWorking ? "errorShade" : "plain"}
name={rechecker.isWorking ? "close" : "check"}
size={SIZE.lg}
color={
rechecker.isWorking ? colors.error.icon : colors.primary.accent
}
onPress={() => {
useRechecker.getState().setProgress(note?.id || "all", {
...useRechecker.getState().progress[note?.id || "all"],
shown: false,
isWorking: false
});
}}
/>
</View>
) : null}
<View>
<ScrollView
style={{
backgroundColor: colors.primary.background,
flexWrap: "wrap",
flexDirection: "row",
paddingVertical: 12
}}
contentContainerStyle={{
alignItems: "center"
}}
horizontal
>
{attachmentTypes.map((item) =>
item.filterBy === "orphaned" && note ? null : (
<Button
type={
currentFilter === item.filterBy
? "secondaryAccented"
: "plain"
}
key={item.title}
title={item.title}
fontSize={SIZE.sm}
style={{
borderRadius: 100,
paddingHorizontal: 12,
height: 40,
minWidth: 80
}}
onPress={async () => {
const filterBy = item.filterBy;
setCurrentFilter(filterBy);
setLoading(true);
filterAttachments(filterBy)
.then((results) => {
if (filterBy !== currentFilterRef.current) return;
setAttachments(results);
setLoading(false);
})
.catch(() => {
/* empty */
});
}}
/>
)
)}
</ScrollView>
</View>
<FlashList
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
ListEmptyComponent={
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center"
}}
>
{loading ? (
<ActivityIndicator size={40} color={colors.primary.accent} />
) : (
<>
<Icon
name="attachment"
size={60}
color={colors.secondary.icon}
/>
<Paragraph>{strings.noAttachments()}</Paragraph>
</>
)}
</View>
}
ListHeaderComponent={<AllProgress note={note} />}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
estimatedItemSize={50}
data={loading ? [] : attachments?.placeholders}
renderItem={renderItem}
/>
</View>
</>
);
};
const AllProgress = ({ note }: { note?: Note }) => {
const progress = useAttachmentStore((state) =>
note
? {
downloading: {
[note.id]: state.downloading?.[note.id]
},
uploading: {
[note.id]: state.uploading?.[note.id]
}
}
: state
);
return (
<View
style={{
gap: 10,
width: "100%"
}}
>
{Object.keys(progress.downloading || {}).map((groupId) => (
<AttachmentGroupProgress key={groupId} groupId={groupId} />
))}
{Object.keys(progress.uploading || {}).map((groupId) => (
<AttachmentGroupProgress key={groupId} groupId={groupId} />
))}
</View>
);
};
AttachmentDialog.present = (note?: Note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
component: () => <AttachmentDialog note={note} isSheet={true} />,
keyboardHandlerDisabled: true
});
};

View File

@@ -36,6 +36,7 @@ import { IconButton } from "../ui/icon-button";
import { hideAuth, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { strings } from "@notesnook/intl";
export const AuthMode = {
login: 0,
@@ -153,7 +154,7 @@ const AuthModal = () => {
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title="Skip"
title={strings.skip()}
onPress={() => {
hideAuth();
}}

View File

@@ -35,6 +35,7 @@ import Seperator from "../ui/seperator";
import { Dialog } from "../dialog";
import BackupService from "../../services/backup";
import { sleep } from "../../utils/time";
import { strings } from "@notesnook/intl";
export const ChangePassword = () => {
const passwordInputRef = useRef();
@@ -49,8 +50,8 @@ export const ChangePassword = () => {
const changePassword = async () => {
if (!user?.isEmailConfirmed) {
ToastManager.show({
heading: "Email not confirmed",
message: "Please confirm your email to change account password",
heading: strings.emailNotConfirmed(),
message: strings.emailNotConfirmedDesc(),
type: "error",
context: "local"
});
@@ -58,8 +59,8 @@ export const ChangePassword = () => {
}
if (error || !oldPassword.current || !password.current) {
ToastManager.show({
heading: "All fields required",
message: "Fill all the fields and try again.",
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
type: "error",
context: "local"
});
@@ -67,13 +68,18 @@ export const ChangePassword = () => {
}
setLoading(true);
try {
const result = await BackupService.run(false, "change-password-dialog");
if (!result) throw new Error("Failed to create backup");
const result = await BackupService.run(
false,
"change-password-dialog",
"partial"
);
if (result.error) {
throw new Error(strings.backupFailed() + `: ${result.error}`);
}
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastManager.show({
heading: "Account password updated",
heading: strings.passwordChangedSuccessfully(),
type: "success",
context: "global"
});
@@ -84,7 +90,7 @@ export const ChangePassword = () => {
} catch (e) {
setLoading(false);
ToastManager.show({
heading: "Failed to change password",
heading: strings.passwordChangeFailed(),
message: e.message,
type: "error",
context: "local"
@@ -101,10 +107,7 @@ export const ChangePassword = () => {
}}
>
<Dialog context="change-password-dialog" />
<DialogHeader
title="Change password"
paragraph="Enter your old and new passwords"
/>
<DialogHeader title={strings.changePassword()} />
<Seperator />
<Input
@@ -118,7 +121,7 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Old Password"
placeholder={strings.oldPassword()}
/>
<Input
@@ -127,27 +130,21 @@ export const ChangePassword = () => {
password.current = value;
}}
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
validationType="password"
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="New password"
placeholder={strings.newPassword()}
/>
<Notice
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
type="alert"
/>
<Notice text={strings.changePasswordNotice()} type="alert" />
<View style={{ height: 10 }} />
<Notice
text={`Once your password is changed, please make sure to save the new account recovery key.`}
type="alert"
/>
<Notice text={strings.changePasswordNotice2()} type="alert" />
<Button
style={{
@@ -157,7 +154,7 @@ export const ChangePassword = () => {
loading={loading}
onPress={changePassword}
type="accent"
title={loading ? null : "I understand, change my password"}
title={loading ? null : strings.changePasswordConfirm()}
/>
</View>
);

View File

@@ -32,6 +32,7 @@ import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export const ForgotPassword = () => {
const { colors } = useThemeColors("sheet");
@@ -44,7 +45,7 @@ export const ForgotPassword = () => {
const sendRecoveryEmail = async () => {
if (!email.current || error) {
ToastManager.show({
heading: "Account email is required.",
heading: strings.emailRequired(),
type: "error",
context: "local"
});
@@ -57,15 +58,15 @@ export const ForgotPassword = () => {
lastRecoveryEmailTime &&
Date.now() - JSON.parse(lastRecoveryEmailTime) < 60000 * 3
) {
throw new Error("Please wait before requesting another email");
throw new Error(strings.pleaseWaitBeforeSendEmail());
}
await db.user.recoverAccount(email.current.toLowerCase());
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
ToastManager.show({
heading: "Check your email to reset password",
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
heading: strings.recoveryEmailSent(),
message: strings.recoveryEmailSentDesc(),
type: "success",
context: "local",
duration: 7000
@@ -75,7 +76,7 @@ export const ForgotPassword = () => {
} catch (e) {
setLoading(false);
ToastManager.show({
heading: "Recovery email not sent",
heading: strings.recoveryEmailFailed(),
message: e.message,
type: "error",
context: "local"
@@ -121,13 +122,13 @@ export const ForgotPassword = () => {
name="email"
size={50}
/>
<Heading>Recovery email sent!</Heading>
<Heading>{strings.recoveryEmailSent()}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
Please follow the link in the email to recover your account.
{strings.recoveryEmailSentDesc()}
</Paragraph>
</View>
) : (
@@ -140,10 +141,7 @@ export const ForgotPassword = () => {
padding: 12
}}
>
<DialogHeader
title="Account recovery"
paragraph="We will send you an email with steps on how to reset your password."
/>
<DialogHeader title={strings.accountRecovery()} />
<Seperator />
<Input
@@ -153,14 +151,14 @@ export const ForgotPassword = () => {
}}
defaultValue={email.current}
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyLabel={strings.next()}
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage="Email is invalid"
placeholder="Email"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
onSubmit={() => {}}
/>
@@ -172,7 +170,7 @@ export const ForgotPassword = () => {
loading={loading}
onPress={sendRecoveryEmail}
type="accent"
title={loading ? null : "Next"}
title={loading ? null : strings.next()}
/>
</View>
)}

View File

@@ -17,12 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import Sync from "../../services/sync";
import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import { eUserLoggedIn } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import SheetProvider from "../sheet-provider";
@@ -34,11 +38,7 @@ import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { useLogin } from "./use-login";
import { useSettingStore } from "../../stores/use-setting-store";
import { eUserLoggedIn } from "../../utils/events";
import { useUserStore } from "../../stores/use-user-store";
import Sync from "../../services/sync";
import { Notice } from "../ui/notice";
import { strings } from "@notesnook/intl";
const LoginSteps = {
emailAuth: 1,
@@ -67,7 +67,7 @@ export const Login = ({ changeMode }) => {
Progress.present();
setTimeout(() => {
if (!useUserStore.getState().syncing) {
Sync.run("global", false, true);
Sync.run("global", false, "full");
}
}, 5000);
});
@@ -148,7 +148,7 @@ export const Login = ({ changeMode }) => {
extraBold
size={SIZE.xxl}
>
Login to your {"\n"}account
{strings.loginToYourAccount()}
</Heading>
</View>
@@ -179,12 +179,16 @@ export const Login = ({ changeMode }) => {
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage="Email is invalid"
placeholder="Enter your email"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading}
onSubmit={() => {
passwordInputRef.current?.focus();
if (step === LoginSteps.emailAuth) {
login();
} else {
passwordInputRef.current?.focus();
}
}}
/>
@@ -196,20 +200,20 @@ export const Login = ({ changeMode }) => {
password.current = value;
}}
testID="input.password"
returnKeyLabel="Done"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Password"
placeholder={strings.password()}
marginBottom={0}
editable={!loading}
defaultValue={password.current}
onSubmit={() => login()}
/>
<Button
title="Forgot your password?"
title={strings.forgotPassword()}
style={{
alignSelf: "flex-end",
height: 30,
@@ -243,14 +247,15 @@ export const Login = ({ changeMode }) => {
width: 250,
borderRadius: 100
}}
height={50}
fontSize={SIZE.md}
type="accent"
title={!loading ? "Continue" : null}
title={!loading ? strings.continue() : null}
/>
{step === LoginSteps.passwordAuth && (
<Button
title="Cancel logging in"
title={strings.cancelLogin()}
style={{
alignSelf: "center",
height: 30,
@@ -286,12 +291,12 @@ export const Login = ({ changeMode }) => {
size={SIZE.xs + 1}
color={colors.secondary.paragraph}
>
Don't have an account?{" "}
{strings.dontHaveAccount()}{" "}
<Paragraph
size={SIZE.xs + 1}
style={{ color: colors.primary.accent }}
>
Sign up
{strings.signUp()}
</Paragraph>
</Paragraph>
</TouchableOpacity>

View File

@@ -47,6 +47,7 @@ import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
import { strings } from "@notesnook/intl";
function getObfuscatedEmail(email) {
if (!email) return "";
@@ -112,7 +113,11 @@ export const SessionExpired = () => {
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
Sync.run("global", false, true, async (complete) => {
const key = await db.user.getEncryptionKey();
if (!key) throw new Error("No encryption key found.");
Sync.run("global", false, "full", async (complete) => {
if (!complete) {
let user = await db.user.getUser();
if (!user) return;
@@ -127,7 +132,6 @@ export const SessionExpired = () => {
setVisible(false);
});
} catch (e) {
console.log(e);
let user = await db.user.getUser();
if (!user) return;
email.current = user.email;
@@ -190,15 +194,14 @@ export const SessionExpired = () => {
size={50}
/>
<Heading size={SIZE.xxxl} color={colors.primary.heading}>
Session expired
{strings.sessionExpired()}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
Your session on this device has expired. Please enter password for{" "}
{getObfuscatedEmail(email.current)} to continue.
{strings.sessionExpiredDesc(getObfuscatedEmail(email.current))}
</Paragraph>
</View>
@@ -208,13 +211,13 @@ export const SessionExpired = () => {
onChangeText={(value) => {
password.current = value;
}}
returnKeyLabel="Next"
returnKeyLabel={strings.done()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Password"
placeholder={strings.password()}
onSubmit={() => login()}
/>
) : null}
@@ -228,7 +231,7 @@ export const SessionExpired = () => {
loading={loading}
onPress={() => login()}
type="accent"
title={loading ? null : "Login"}
title={loading ? null : strings.login()}
/>
<Button
@@ -239,16 +242,15 @@ export const SessionExpired = () => {
onPress={() => {
presentDialog({
context: "session_expiry",
title: "Logout",
paragraph:
"Are you sure you want to logout from this device? Any unsynced changes will be lost.",
positiveText: "Logout",
title: strings.logoutFromDevice(),
paragraph: strings.logoutDesc(),
positiveText: strings.logout(),
positiveType: "errorShade",
positivePress: logout
});
}}
type="errorShade"
title={loading ? null : "Logout from this device"}
title={loading ? null : strings.logoutFromDevice()}
/>
</View>
<Toast context="local" />

View File

@@ -35,6 +35,8 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { useSettingStore } from "../../stores/use-setting-store";
import SettingsService from "../../services/settings";
import { strings } from "@notesnook/intl";
export const Signup = ({ changeMode, trial }) => {
const { colors } = useThemeColors();
@@ -53,8 +55,8 @@ export const Signup = ({ changeMode, trial }) => {
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastManager.show({
heading: "All fields required",
message: "Fill all the fields and try again",
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
type: "error",
context: "local"
});
@@ -77,6 +79,7 @@ export const Signup = ({ changeMode, trial }) => {
clearMessage();
setEmailVerifyMessage();
hideAuth();
SettingsService.setProperty("encryptedBackup", true);
await sleep(300);
if (trial) {
PremiumService.sheet(null, null, true);
@@ -86,7 +89,7 @@ export const Signup = ({ changeMode, trial }) => {
} catch (e) {
setLoading(false);
ToastManager.show({
heading: "Signup failed",
heading: strings.signupFailed(),
message: e.message,
type: "error",
context: "local"
@@ -154,7 +157,7 @@ export const Signup = ({ changeMode, trial }) => {
}}
size={SIZE.xxl}
>
Create your {"\n"}account
{strings.createYourAccount()}
</Heading>
</View>
@@ -173,14 +176,14 @@ export const Signup = ({ changeMode, trial }) => {
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyLabel={strings.next()}
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage="Email is invalid"
placeholder="Email"
errorMessage={strings.email()}
placeholder={strings.email()}
onSubmit={() => {
passwordInputRef.current?.focus();
}}
@@ -193,14 +196,14 @@ export const Signup = ({ changeMode, trial }) => {
}}
testID="input.password"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
validationType="password"
autoCorrect={false}
placeholder="Password"
placeholder={strings.password()}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
@@ -213,7 +216,7 @@ export const Signup = ({ changeMode, trial }) => {
}}
testID="input.confirmPassword"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Signup"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
autoComplete="password"
@@ -221,7 +224,7 @@ export const Signup = ({ changeMode, trial }) => {
autoCorrect={false}
validationType="confirmPassword"
customValidator={() => password.current}
placeholder="Confirm password"
placeholder={strings.confirmPassword()}
marginBottom={12}
onSubmit={signup}
/>
@@ -233,7 +236,7 @@ export const Signup = ({ changeMode, trial }) => {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
By signing up, you agree to our{" "}
{strings.signupAgreement[0]()}
<Paragraph
size={SIZE.xs}
onPress={() => {
@@ -244,9 +247,9 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.primary.accent}
>
Terms of Service{" "}
{strings.signupAgreement[1]()}
</Paragraph>
and{" "}
{strings.signupAgreement[2]()}
<Paragraph
size={SIZE.xs}
onPress={() => {
@@ -257,14 +260,13 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.primary.accent}
>
Privacy Policy.
</Paragraph>{" "}
You also agree to recieve marketing emails from us which you can
opt-out of from app settings.
{strings.signupAgreement[3]()}
</Paragraph>
{strings.signupAgreement[4]()}
</Paragraph>
<Button
title={!loading ? "Continue" : null}
title={!loading ? strings.continue() : null}
type="accent"
loading={loading}
onPress={signup}
@@ -289,12 +291,12 @@ export const Signup = ({ changeMode, trial }) => {
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.secondary.paragraph}>
Already have an account?{" "}
{strings.alreadyHaveAccount()}{" "}
<Paragraph
size={SIZE.xs + 1}
style={{ color: colors.primary.accent }}
>
Login
{strings.login()}
</Paragraph>
</Paragraph>
</TouchableOpacity>

View File

@@ -17,8 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { db } from "../../common/database/index";
import useTimer from "../../hooks/use-timer";
import {
@@ -26,19 +29,16 @@ import {
presentSheet,
ToastManager
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
const { colors } = useThemeColors();
const code = useRef();
const [currentMethod, setCurrentMethod] = useState({
@@ -50,20 +50,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
const inputRef = useRef();
const [sending, setSending] = useState(false);
const codeHelpText = {
app: "Enter the 6 digit code from your authenticator app to continue logging in",
sms: "Enter the 6 digit code sent to your phone number to continue logging in",
email: "Enter the 6 digit code sent to your email to continue logging in",
recoveryCode: "Enter the recovery code to continue logging in"
};
const secondaryMethodsText = {
app: "I don't have access to authenticator app",
sms: "I don't have access to my phone",
email: "I don't have access to email",
recoveryCode: "I don't have recovery codes"
};
const onNext = async () => {
if (!code.current || code.current.length < 6) return;
setLoading(true);
@@ -93,22 +79,22 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
const methods = [
{
id: "sms",
title: "Send code via SMS",
title: strings.sendCodeSms(),
icon: "message-plus-outline"
},
{
id: "email",
title: "Send code via email",
title: strings.sendCodeEmail(),
icon: "email-outline"
},
{
id: "app",
title: "Enter code from authenticator app",
title: strings.authAppCode(),
icon: "cellphone-key"
},
{
id: "recoveryCode",
title: "I have a recovery code",
title: strings.recoveryCode(),
icon: "key"
}
];
@@ -143,11 +129,15 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
return (
<View>
<ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
>
<View
style={{
alignItems: "center",
paddingHorizontal: currentMethod.method ? 12 : 0
paddingHorizontal: currentMethod.method ? 12 : 0,
gap: 12
}}
>
<IconButton
@@ -164,9 +154,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
textAlign: "center"
}}
>
{currentMethod.method
? "Two factor authentication"
: "Select methods for two-factor authentication"}
{currentMethod.method ? strings["2fa"]() : strings.select2faMethod()}
</Heading>
<Paragraph
style={{
@@ -174,12 +162,10 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
textAlign: "center"
}}
>
{codeHelpText[currentMethod.method] ||
"Select how you would like to recieve the code"}
{strings["2faCodeHelpText"][currentMethod.method]?.() ||
strings.select2faCodeHelpText()}
</Paragraph>
<Seperator />
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
<Button
onPress={onSendCode}
@@ -187,15 +173,17 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
title={
sending
? ""
: `${seconds ? `Resend code in (${seconds})` : "Send code"}`
: `${
seconds
? strings.resend2faCode(seconds)
: strings.sendCode()
}`
}
loading={sending}
height={30}
/>
) : null}
<Seperator />
{currentMethod.method ? (
<>
<Input
@@ -214,6 +202,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
code.current = value;
//onNext();
}}
onSubmitEditing={onNext}
caretHidden
inputStyle={{
fontSize: SIZE.lg,
@@ -225,6 +214,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
keyboardType={
currentMethod.method === "recoveryCode" ? "default" : "numeric"
}
enablesReturnKeyAutomatically
containerStyle={{
height: 60,
borderWidth: 0,
@@ -232,25 +222,33 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
minWidth: "50%"
}}
/>
<Seperator />
<Button
title={loading ? null : "Next"}
title={loading ? null : strings.next()}
type="accent"
width={250}
loading={loading}
onPress={onNext}
style={{
borderRadius: 100,
marginBottom: 10
borderRadius: 100
}}
/>
<Button
title={secondaryMethodsText[currentMethod.method]}
title={strings["2faCodeSecondaryMethodText"][
currentMethod.method
]()}
type="plain"
onPress={onRequestSecondaryMethod}
height={30}
/>
<Button
title={strings.cancel()}
type="plain"
onPress={onCancel}
height={30}
/>
</>
) : (
<>
@@ -297,19 +295,20 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
</>
)}
</View>
</View>
</ScrollView>
);
};
TwoFactorVerification.present = (onMfaLogin, data, context) => {
TwoFactorVerification.present = (onMfaLogin, data, onCancel, context) => {
presentSheet({
component: () => (
<TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />
<TwoFactorVerification
onMfaLogin={onMfaLogin}
mfaInfo={data}
onCancel={onCancel}
/>
),
context: context || "two_factor_verify",
onClose: () => {
onMfaLogin();
},
disableClosing: true
});
};

View File

@@ -26,6 +26,7 @@ import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet } from "../../utils/events";
import TwoFactorVerification from "./two-factor";
import { strings } from "@notesnook/intl";
export const LoginSteps = {
emailAuth: 1,
@@ -49,8 +50,8 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
(!email.current && step === LoginSteps.emailAuth)
) {
ToastManager.show({
heading: "All fields required",
message: "Fill all the fields and try again",
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
type: "error",
context: "local"
});
@@ -69,35 +70,43 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
switch (step) {
case LoginSteps.emailAuth: {
const mfaInfo = await db.user.authenticateEmail(email.current);
console.log("email auth", mfaInfo);
if (mfaInfo) {
TwoFactorVerification.present(async (mfa, callback) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
mfa.method
);
if (success) {
setStep(LoginSteps.passwordAuth);
setLoading(false);
setTimeout(() => {
passwordInputRef.current?.focus();
}, 1);
callback && callback(true);
}
callback && callback(false);
} catch (e) {
callback && callback(false);
if (e.message === "invalid_grant") {
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
if (mfaInfo) {
TwoFactorVerification.present(
async (mfa, callback) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
mfa.method
);
if (success) {
setStep(LoginSteps.passwordAuth);
setLoading(false);
setTimeout(() => {
passwordInputRef.current?.focus();
}, 500);
callback && callback(true);
}
callback && callback(false);
} catch (e) {
callback && callback(false);
if (e.message === "invalid_grant") {
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
}
},
mfaInfo,
() => {
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
}, mfaInfo);
);
} else {
finishWithError(new Error("Unable to send 2FA code"));
finishWithError(new Error(strings.unableToSend2faCode()));
}
break;
}
@@ -122,7 +131,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
ToastManager.show({
heading: "Login failed",
heading: strings.loginFailed(),
message: e.message,
type: "error",
context: "local"
@@ -131,13 +140,13 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
const finishLogin = async () => {
const user = await db.user.getUser();
if (!user) throw new Error("Email or password incorrect!");
if (!user) throw new Error(strings.emailOrPasswordIncorrect());
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastManager.show({
heading: "Login successful",
message: `Logged in as ${user.email}`,
heading: strings.loginSuccess(),
message: strings.loginSuccessDesc(),
type: "success",
context: "global"
});

View File

@@ -34,11 +34,9 @@ import { useSelectionStore } from "../../stores/use-selection-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { getElevationStyle } from "../../utils/elevation";
import { SIZE, normalize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import { Pressable } from "../ui/pressable";
interface FloatingButtonProps {
title: string;
onPress: () => void;
color?: string;
shouldShow?: boolean;
@@ -46,7 +44,6 @@ interface FloatingButtonProps {
}
const FloatingButton = ({
title,
onPress,
color,
alwaysVisible = false
@@ -126,11 +123,6 @@ const FloatingButton = ({
...getElevationStyle(5),
borderRadius: 100
}}
onLongPress={(event) => {
if (title) {
NativeTooltip.show(event, title, NativeTooltip.POSITIONS.LEFT);
}
}}
onPress={onPress}
>
<View

View File

@@ -23,6 +23,7 @@ import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
import { AppLockPassword } from "../dialogs/applock-password";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { LoadingDialog } from "../dialogs/loading";
import PDFPreview from "../dialogs/pdf-preview";
@@ -35,8 +36,7 @@ import { Expiring } from "../premium/expiring";
import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data";
import { AppLockPassword } from "../dialogs/applock-password";
import Progress from "../dialogs/progress";
const DialogProvider = () => {
const { colors } = useThemeColors();
@@ -52,7 +52,6 @@ const DialogProvider = () => {
<RecoveryKeySheet colors={colors} />
<SheetProvider />
<SheetProvider context="sync_progress" />
<RestoreDataSheet />
<ResultDialog />
<VaultDialog colors={colors} />
<RateAppSheet />
@@ -62,6 +61,7 @@ const DialogProvider = () => {
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
<Progress />
</>
);
};

View File

@@ -38,7 +38,7 @@ import { useAppState } from "../../hooks/use-app-state";
export interface BaseDialogProps extends PropsWithChildren {
animation?: "fade" | "none" | "slide" | undefined;
visible: boolean;
visible?: boolean;
onRequestClose?: () => void;
onShow?: () => void;
premium?: boolean;
@@ -75,9 +75,9 @@ const BaseDialog = ({
background
}: BaseDialogProps) => {
const floating = useIsFloatingKeyboard();
const appState = useAppState();
const lockEvents = useRef(false);
const [internalVisible, setIntervalVisible] = useState(true);
const locked = useUserStore((state) => state.appLocked);
useEffect(() => {
return () => {
@@ -86,26 +86,20 @@ const BaseDialog = ({
}, []);
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.canLockAppInBackground()) {
if (appState === "background") {
setIntervalVisible(false);
if (useUserStore.getState().appLocked) {
lockEvents.current = true;
const unsub = useUserStore.subscribe((state) => {
if (!state.appLocked) {
setIntervalVisible(true);
unsub();
setTimeout(() => {
lockEvents.current = false;
});
}
if (locked) {
setIntervalVisible(false);
lockEvents.current = true;
const unsub = useUserStore.subscribe((state) => {
if (!state.appLocked) {
setIntervalVisible(true);
unsub();
setTimeout(() => {
lockEvents.current = false;
});
}
}
});
}
}, [appState]);
}, [locked]);
const Wrapper = useSafeArea ? SafeAreaView : View;

View File

@@ -26,12 +26,13 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { getColorLinearShade } from "../../utils/colors";
import { strings } from "@notesnook/intl";
const DialogButtons = ({
onPressPositive,
onPressNegative,
positiveTitle,
negativeTitle = "Cancel",
negativeTitle = strings.cancel(),
loading,
doneText,
positiveType

View File

@@ -32,21 +32,29 @@ const DialogContainer = ({
}: ViewProps & {
width?: any;
height?: any;
noBorder?: boolean;
}) => {
const { colors } = useThemeColors();
return (
<View
{...restProps}
style={{
...getElevationStyle(5),
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12,
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
}}
style={[
style,
{
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
},
restProps?.noBorder
? {}
: {
...getElevationStyle(5),
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
}
]}
/>
);
};

View File

@@ -25,7 +25,7 @@ type DialogInfo = {
paragraph?: string;
positiveText?: string;
negativeText?: string;
positivePress?: (value: any) => void;
positivePress?: (...args: any[]) => void;
onClose?: () => void;
positiveType?:
| "transparent"
@@ -44,6 +44,16 @@ type DialogInfo = {
// eslint-disable-next-line @typescript-eslint/ban-types
context: "global" | "local" | (string & {});
secureTextEntry?: boolean;
keyboardType?: string;
check?: {
info: string;
type?: string;
defaultValue?: boolean;
};
notice: {
text: string;
type: "alert" | "information";
};
};
export function presentDialog(data: Partial<DialogInfo>): void {

View File

@@ -37,6 +37,8 @@ import DialogHeader from "./dialog-header";
import { useCallback } from "react";
import { Button } from "../ui/button";
import { getContainerBorder } from "../../utils/colors";
import { Notice } from "../ui/notice";
import { strings } from "@notesnook/intl";
export const Dialog = ({ context = "global" }) => {
const { colors } = useThemeColors();
@@ -49,8 +51,8 @@ export const Dialog = ({ context = "global" }) => {
const [dialogInfo, setDialogInfo] = useState({
title: "",
paragraph: "",
positiveText: "Done",
negativeText: "Cancel",
positiveText: strings.done(),
negativeText: strings.cancel(),
positivePress: () => {},
onClose: () => {},
positiveType: "transparent",
@@ -62,7 +64,8 @@ export const Dialog = ({ context = "global" }) => {
disableBackdropClosing: false,
check: {
info: "Check",
type: "transparent"
type: "transparent",
defaultValue: false
}
});
@@ -86,7 +89,7 @@ export const Dialog = ({ context = "global" }) => {
if (!data.context) data.context = "global";
if (data.context !== context) return;
setDialogInfo(data);
setChecked(false);
setChecked(data.check?.defaultValue);
values.current.inputValue = data.defaultValue;
setVisible(true);
},
@@ -174,11 +177,25 @@ export const Dialog = ({ context = "global" }) => {
onSubmit={onPressPositive}
returnKeyLabel="Done"
returnKeyType="done"
keyboardType={dialogInfo.keyboardType || "default"}
placeholder={dialogInfo.inputPlaceholder}
/>
</View>
) : null}
{dialogInfo?.notice ? (
<View
style={{
paddingHorizontal: 12
}}
>
<Notice
type={dialogInfo.notice.type || "information"}
text={dialogInfo.notice.text}
/>
</View>
) : null}
{dialogInfo.check ? (
<>
<Button
@@ -197,7 +214,7 @@ export const Dialog = ({ context = "global" }) => {
iconSize={20}
width="100%"
title={dialogInfo.check.info}
type={checked ? dialogInfo.check.type || "selected" : "plain"}
type={checked ? dialogInfo.check.type || "plain" : "plain"}
/>
</>
) : null}

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { db } from "../../../common/database";
import {
clearAppLockVerificationCipher,
setAppLockVerificationCipher,
@@ -44,9 +45,11 @@ import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { strings } from "@notesnook/intl";
export const AppLockPassword = () => {
const { colors } = useThemeColors();
@@ -66,6 +69,7 @@ export const AppLockPassword = () => {
confirmPassword?: string;
}>({});
const [secureTextEntry, setSecureTextEntry] = useState(true);
const [accountPass, setAccountPass] = useState(false);
useEffect(() => {
const subs = [
@@ -73,6 +77,7 @@ export const AppLockPassword = () => {
eOpenAppLockPasswordDialog,
(mode: "create" | "change" | "remove") => {
setMode(mode);
setAccountPass(false);
setVisible(true);
}
),
@@ -115,20 +120,7 @@ export const AppLockPassword = () => {
}}
>
<DialogHeader
title={
mode === "change"
? `Change app lock ${keyboardType}`
: mode === "remove"
? `Remove app lock ${keyboardType}`
: `Set up app lock ${keyboardType}`
}
paragraph={
mode === "change"
? `Change app lock ${keyboardType}`
: mode === "remove"
? `Remove app lock ${keyboardType}`
: `Set up a custom app lock ${keyboardType} to unlock the app`
}
title={strings.changeAppLockCredentials(mode, keyboardType)}
icon="shield"
padding={12}
/>
@@ -151,11 +143,15 @@ export const AppLockPassword = () => {
}}
defaultValue={values.current.currentPassword}
autoComplete="password"
returnKeyLabel="Next"
returnKeyLabel={strings.next()}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
returnKeyType="next"
secureTextEntry={secureTextEntry}
placeholder={`Current ${keyboardType}`}
placeholder={
keyboardType === "pin"
? strings.currentPin()
: strings.currentPassword()
}
/>
) : null}
@@ -169,35 +165,47 @@ export const AppLockPassword = () => {
confirmPasswordInputRef.current?.focus();
}}
defaultValue={values.current.password}
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
keyboardType={
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
}
autoComplete="password"
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
returnKeyLabel={
mode !== "remove" ? strings.next() : strings.remove()
}
returnKeyType={mode !== "remove" ? "next" : "done"}
secureTextEntry={secureTextEntry}
buttonLeft={
<IconButton
name={keyboardType === "password" ? "numeric" : "keyboard"}
onPress={() => {
setKeyboardType(
keyboardType === "password" ? "pin" : "password"
);
setSecureTextEntry(false);
setImmediate(() => {
setSecureTextEntry(true);
});
}}
style={{
width: 25,
height: 25,
marginRight: 5
}}
size={SIZE.lg}
/>
accountPass ? null : (
<IconButton
name={keyboardType === "password" ? "numeric" : "keyboard"}
onPress={() => {
setKeyboardType(
keyboardType === "password" ? "pin" : "password"
);
setSecureTextEntry(false);
setImmediate(() => {
setSecureTextEntry(true);
});
}}
style={{
width: 25,
height: 25,
marginRight: 5
}}
size={SIZE.lg}
/>
)
}
placeholder={
mode === "change"
? `New ${keyboardType}`
: `${keyboardType === "pin" ? "Pin" : "Password"}`
accountPass
? strings.enterAccountPassword()
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin" ? strings.pin() : strings.password()
}`
}
/>
@@ -216,12 +224,46 @@ export const AppLockPassword = () => {
customValidator={() => values.current.password || ""}
validationType="confirmPassword"
autoComplete="password"
returnKeyLabel="Done"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry={secureTextEntry}
placeholder={`Confirm ${keyboardType}`}
placeholder={
keyboardType === "pin"
? strings.confirmPin()
: strings.confirmPassword()
}
/>
) : null}
{mode === "remove" ? (
<>
<Button
icon={
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
}
onPress={() => {
setSecureTextEntry(false);
setAccountPass(!accountPass);
setTimeout(() => {
setSecureTextEntry(true);
});
}}
iconSize={SIZE.lg}
type="plain"
iconColor={
accountPass ? colors.primary.accent : colors.primary.icon
}
title={strings.useAccountPassword()}
style={{
width: "100%",
alignSelf: "flex-start",
justifyContent: "flex-start",
paddingHorizontal: 0,
height: 30
}}
/>
</>
) : null}
</View>
<DialogButtons
@@ -230,7 +272,7 @@ export const AppLockPassword = () => {
if (mode === "create") {
if (!values.current.password || !values.current.confirmPassword) {
ToastManager.error(
new Error("All inputs are required"),
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
@@ -239,17 +281,14 @@ export const AppLockPassword = () => {
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(
`${
keyboardType === "pin" ? "Pin" : "Password"
} does not match`
),
new Error(strings.mismatch(keyboardType)),
undefined,
"local"
);
return;
}
await setAppLockVerificationCipher(values.current.password);
const password = values.current.password;
setAppLockVerificationCipher(password);
SettingsService.setProperty("appLockHasPasswordSecurity", true);
} else if (mode === "change") {
if (
@@ -258,7 +297,7 @@ export const AppLockPassword = () => {
!values.current.confirmPassword
) {
ToastManager.error(
new Error("All inputs are required"),
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
@@ -267,52 +306,49 @@ export const AppLockPassword = () => {
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(
`${
keyboardType === "pin" ? "Pin" : "Password"
} does not match`
),
new Error(strings.mismatch(keyboardType)),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.currentPassword
);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
),
new Error(strings.incorrect(keyboardType)),
undefined,
"local"
);
return;
}
const password = values.current.password;
await clearAppLockVerificationCipher();
SettingsService.setProperty("appLockHasPasswordSecurity", true);
await setAppLockVerificationCipher(values.current.password);
await setAppLockVerificationCipher(password);
} else if (mode === "remove") {
if (!values.current.password) {
ToastManager.error(
new Error("All inputs are required"),
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
return;
}
const isCurrentPasswordCorrect = await validateAppLockPassword(
values.current.password
);
const isCurrentPasswordCorrect = accountPass
? await db.user.verifyPassword(values.current.password)
: await validateAppLockPassword(values.current.password);
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
accountPass
? strings.passwordIncorrect()
: strings.incorrect(keyboardType)
),
undefined,
"local"
@@ -329,7 +365,7 @@ export const AppLockPassword = () => {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(SettingsService.get());
ToastManager.show({
message: "App lock disabled",
message: strings.applockDisabled(),
type: "success"
});
}
@@ -342,8 +378,14 @@ export const AppLockPassword = () => {
close();
}}
positiveTitle="Save"
negativeTitle="Cancel"
positiveTitle={
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
}
negativeTitle={strings.cancel()}
positiveType="transparent"
loading={false}
doneText=""

View File

@@ -17,15 +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 React, { useState } from "react";
import { Image, ScrollView, TouchableOpacity, View } from "react-native";
import {
Image,
Platform,
ScrollView,
TouchableOpacity,
View
} from "react-native";
import { Image as ImageType } from "react-native-image-crop-picker";
import { useThemeColors } from "../../../../../../packages/theme/dist";
import { useThemeColors } from "@notesnook/theme";
import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Notice } from "../../ui/notice";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export default function AttachImage({
response,
@@ -58,14 +65,17 @@ export default function AttachImage({
}}
>
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
Attaching {response?.length} image(s):
{strings.attachImageHeading(response?.length || 1)}
</Paragraph>
<ScrollView horizontal>
{response?.map((item) => (
{response?.map?.((item) => (
<TouchableOpacity key={item.filename} activeOpacity={0.9}>
<Image
source={{
uri: item.sourceURL || item.path
uri:
Platform.OS === "ios"
? item.sourceURL || item.path
: item.path
}}
style={{
width: 100,
@@ -114,14 +124,14 @@ export default function AttachImage({
}}
size={SIZE.sm}
>
Compress (recommended)
{strings.compress()} ({strings.recommended().toLowerCase()})
</Paragraph>
</TouchableOpacity>
{!compress ? (
<Notice
type="alert"
text="Images uploaded without compression are slow to load and take more bandwidth. We recommend compressing images unless you need image in original quality."
text={strings.compressionOffNotice()}
size="small"
style={{
width: "100%",
@@ -131,7 +141,7 @@ export default function AttachImage({
) : (
<Notice
type="information"
text="Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases."
text={strings.compressionOnNotice()}
size="small"
style={{
width: "100%",
@@ -141,9 +151,7 @@ export default function AttachImage({
)}
<Button
title={`${
(response?.length || 0) > 1 ? "Attach Images" : "Attach Image"
}`}
title={strings.attachImageHeading(response?.length || 1)}
type="accent"
width="100%"
onPress={() => {
@@ -172,7 +180,7 @@ AttachImage.present = (response: ImageType[], context?: string) => {
close={close}
onAttach={(result) => {
resolved = true;
console.log("closing");
resolve(result);
close?.();
}}

View File

@@ -33,6 +33,7 @@ import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
import { strings } from "@notesnook/intl";
const HEX_COLOR_REGEX_ALPHA =
/^#(?:(?:[\da-fA-F]{3}){1,2}|(?:[\da-fA-F]{4}){1,2})$/;
@@ -148,36 +149,37 @@ const ColorPicker = ({
}}
testID="color-title-input"
defaultValue={title.current}
placeholder={title.current || "Color title"}
placeholder={title.current || strings.colorTitle()}
/>
<Button
title="Add color"
title={strings.addColor()}
style={{
marginBottom: 10
}}
onPress={async () => {
if (!selectedColor)
return ToastManager.error(
new Error("Select a color"),
new Error(strings.noColorSelected()),
undefined,
"color-picker"
);
if (!title.current)
return ToastManager.error(
new Error("Enter a title for the color")
new Error(strings.allFieldsRequired())
);
const exists = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", selectedColor)])
);
if (exists)
return ToastManager.error(
new Error(`Color #${selectedColor} already exists`)
new Error(strings.colorExists(selectedColor))
);
const id = await db.colors.add({
title: title.current,
colorCode: selectedColor
});
if (!id) return;
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
setVisible(false);

View File

@@ -44,6 +44,8 @@ import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import { sleep } from "../../../utils/time";
import { MMKV } from "../../../common/database/mmkv";
import { deleteCacheFileByPath } from "../../../common/filesystem/io";
import { strings } from "@notesnook/intl";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
@@ -124,6 +126,7 @@ const PDFPreview = () => {
);
const close = () => {
deleteCacheFileByPath(pdfSource);
setPDFSource(null);
setVisible(false);
setPassword("");
@@ -135,10 +138,10 @@ const PDFPreview = () => {
presentDialog({
context: attachment?.hash,
input: true,
inputPlaceholder: "Enter password",
positiveText: "Unlock",
title: "Decrypt",
paragraph: "Please input password to view pdf.",
inputPlaceholder: strings.enterPassword(),
positiveText: strings.open(),
title: strings.pdfLocked(),
paragraph: strings.pdfLockedDesc(),
positivePress: (value) => {
setTimeout(() => {
setPassword(value);
@@ -185,8 +188,7 @@ const PDFPreview = () => {
}}
color={colors.static.white}
>
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
... Please wait
{strings.loadingWithProgress(progress?.percent)}
</Paragraph>
</Animated.View>
) : (
@@ -305,9 +307,7 @@ const PDFPreview = () => {
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {
console.log(`Link pressed: ${uri}`);
}}
onPressLink={(uri) => {}}
style={{
flex: 1,
width: width,

View File

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

View File

@@ -39,10 +39,9 @@ const ResultDialog = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [dialogData, setDialogData] = useState({
title: "Thank you for signing up!",
paragraph:
"Try out all features of Notesnook free for 7 days. No limitations. No commitments.",
button: "Start taking notes"
title: "",
paragraph: "",
button: ""
});
useEffect(() => {
eSubscribeEvent(eOpenResultDialog, open);

View File

@@ -52,6 +52,7 @@ import { Button } from "../../ui/button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export class VaultDialog extends Component {
constructor(props) {
@@ -76,7 +77,7 @@ export class VaultDialog extends Component {
changePassword: false,
copyNote: false,
revokeFingerprintAccess: false,
title: "Unlock Note",
title: strings.goToEditor(),
description: null,
clearVault: false,
deleteVault: false,
@@ -91,46 +92,25 @@ export class VaultDialog extends Component {
this.password = null;
this.confirmPassword = null;
this.newPassword = null;
(this.title = !this.state.novault
? "Create Vault"
this.title = !this.state.novault
? strings.createVault()
: this.state.fingerprintAccess
? "Vault Fingerprint Unlock"
? strings.vaultFingerprintUnlock()
: this.state.revokeFingerprintAccess
? "Revoke Vault Fingerprint Unlock"
? strings.revokeVaultFingerprintUnlock()
: this.state.changePassword
? "Change Vault Password"
? strings.changeVaultPassword()
: this.state.noteLocked
? this.state.deleteNote
? "Delete note"
? strings.deleteNote()
: this.state.share
? "Share note"
? strings.shareNote()
: this.state.copyNote
? "Copy note"
? strings.copyNote()
: this.state.goToEditor
? "Unlock note"
: "Unlock note"
: "Lock note"),
(this.description = !this.state.novault
? "Set a password to create vault"
: this.state.fingerprintAccess
? "Enter vault password to enable fingerprint unlocking."
: this.state.revokeFingerprintAccess
? "Disable vault fingerprint unlock "
: this.state.changePassword
? "Setup a new password for the vault."
: this.state.permanant
? "Enter password to remove note from vault."
: this.state.noteLocked
? this.state.deleteNote
? "Unlock note to delete it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.share
? "Unlock note to share it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.copyNote
? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.goToEditor
? "Unlock note to open it in editor."
: "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault."
: "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note.");
? strings.goToEditor()
: strings.goToEditor()
: strings.lockNote();
}
componentDidMount() {
@@ -199,7 +179,7 @@ export class VaultDialog extends Component {
if (this.state.loading) {
ToastManager.show({
heading: this.state.title,
message: "Please wait and do not close the app.",
message: strings.pleaseWait() + "...",
type: "success",
context: "local"
});
@@ -234,8 +214,7 @@ export class VaultDialog extends Component {
if (!this.password) {
ToastManager.show({
heading: "Password not entered",
message: "Enter a password for the vault and try again.",
heading: strings.passwordNotEntered(),
type: "error",
context: "local"
});
@@ -245,7 +224,7 @@ export class VaultDialog extends Component {
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
ToastManager.show({
heading: "Passwords do not match",
heading: strings.passwordNotMatched(),
type: "error",
context: "local"
});
@@ -271,7 +250,7 @@ export class VaultDialog extends Component {
this._enrollFingerprint(this.newPassword);
}
ToastManager.show({
heading: "Vault password updated successfully",
heading: strings.passwordUpdated(),
type: "success",
context: "global"
});
@@ -283,8 +262,7 @@ export class VaultDialog extends Component {
});
if (e.message === db.vault.ERRORS.wrongPassword) {
ToastManager.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
@@ -295,8 +273,7 @@ export class VaultDialog extends Component {
} else if (this.state.locked) {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
@@ -334,8 +311,10 @@ export class VaultDialog extends Component {
loading: true
});
try {
let verified = await db.user.verifyPassword(this.password);
if (!(await db.user.getUser())) verified = true;
let verified = true;
if (await db.user.getUser()) {
verified = await db.user.verifyPassword(this.password);
}
if (verified) {
let noteIds = [];
if (this.state.deleteAll) {
@@ -343,19 +322,20 @@ export class VaultDialog extends Component {
const relations = await db.relations.from(vault, "note").get();
noteIds = relations.map((item) => item.toId);
}
await db.vault.delete(this.state.deleteAll);
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
if (this.state.deleteAll) {
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
}
eSendEvent("vaultUpdated");
this.setState({
@@ -364,8 +344,7 @@ export class VaultDialog extends Component {
this.close();
} else {
ToastManager.show({
heading: "Account password incorrect",
message: "Please enter correct password for your account.",
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
@@ -406,8 +385,7 @@ export class VaultDialog extends Component {
eSendEvent("vaultUpdated");
} catch (e) {
ToastManager.show({
heading: "Vault password incorrect",
message: "Please enter correct password to clear vault.",
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
@@ -420,7 +398,7 @@ export class VaultDialog extends Component {
async _lockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: "Incorrect password",
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
@@ -428,12 +406,11 @@ export class VaultDialog extends Component {
} else {
await db.vault.add(this.state.note.id);
console.log("update note event...");
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.close();
ToastManager.show({
message: "Note locked successfully",
message: strings.noteLocked(),
type: "error",
context: "local"
});
@@ -446,8 +423,7 @@ export class VaultDialog extends Component {
async _unlockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
@@ -483,7 +459,7 @@ export class VaultDialog extends Component {
async _deleteNote() {
try {
await db.vault.remove(this.state.note.id, this.password);
await deleteItems([this.state.note.id], "note");
await deleteItems("note", [this.state.note.id]);
this.close();
} catch (e) {
this._takeErrorAction(e);
@@ -504,8 +480,7 @@ export class VaultDialog extends Component {
});
eSendEvent("vaultUpdated");
ToastManager.show({
heading: "Biometric unlocking enabled!",
message: "Now you can unlock notes in vault with biometrics.",
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
@@ -513,9 +488,7 @@ export class VaultDialog extends Component {
} catch (e) {
this.close();
ToastManager.show({
heading: "Incorrect password",
message:
"Please enter the correct vault password to enable biometrics.",
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
@@ -540,20 +513,20 @@ export class VaultDialog extends Component {
loading: false
});
ToastManager.show({
heading: "Note added to vault",
heading: strings.noteLocked(),
type: "success",
context: "global"
});
this.close();
} else {
eSendEvent("vaultUpdated");
ToastManager.show({
heading: "Vault created successfully",
heading: strings.vaultCreated(),
type: "success",
context: "global"
});
this.close();
}
eSendEvent("vaultUpdated");
}
_permanantUnlock() {
@@ -561,7 +534,7 @@ export class VaultDialog extends Component {
.remove(this.state.note.id, this.password)
.then(() => {
ToastManager.show({
heading: "Note permanently unlocked.",
heading: strings.noteUnlocked(),
type: "success",
context: "global"
});
@@ -586,11 +559,10 @@ export class VaultDialog extends Component {
}
async _copyNote(note) {
Clipboard.setString(await convertNoteToText(note));
Clipboard.setString((await convertNoteToText(note, true)) || "");
ToastManager.show({
heading: "Note copied",
heading: strings.noteCopied(),
type: "success",
message: "Note has been copied to clipboard!",
context: "global"
});
this.close();
@@ -600,9 +572,9 @@ export class VaultDialog extends Component {
this.close();
try {
await Share.open({
heading: "Share note",
heading: note.title,
failOnCancel: false,
message: await convertNoteToText(note)
message: (await convertNoteToText(note)) || ""
});
} catch (e) {
console.error(e);
@@ -610,24 +582,17 @@ export class VaultDialog extends Component {
}
_takeErrorAction(e) {
if (
e.message === db.vault.ERRORS.wrongPassword ||
e.message === "FAILURE"
) {
this.setState({
wrongPassword: true,
visible: true
this.setState({
wrongPassword: true,
visible: true
});
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setTimeout(() => {
ToastManager.show({
heading: "Incorrect password",
type: "error",
context: "local"
});
}, 500);
return;
}
}, 500);
}
_revokeFingerprintAccess = async () => {
@@ -635,14 +600,13 @@ export class VaultDialog extends Component {
await BiometricService.resetCredentials();
eSendEvent("vaultUpdated");
ToastManager.show({
heading: "Biometric unlocking disabled!",
heading: strings.biometricUnlockDisabled(),
type: "success",
context: "global"
});
} catch (e) {
ToastManager.show({
heading: "Failed to disable Biometric unlocking.",
message: e.message,
heading: e.message,
type: "success",
context: "global"
});
@@ -708,12 +672,7 @@ export class VaultDialog extends Component {
paddingTop: 12
}}
>
<DialogHeader
title={this.state.title}
paragraph={this.state.description}
icon="shield"
padding={12}
/>
<DialogHeader title={this.state.title} icon="shield" padding={12} />
<Seperator half />
<View
@@ -749,10 +708,16 @@ export class VaultDialog extends Component {
: this.onPress;
}}
autoComplete="password"
returnKeyLabel={changePassword ? "Next" : this.state.title}
returnKeyLabel={
changePassword ? strings.next() : this.state.title
}
returnKeyType={changePassword ? "next" : "done"}
secureTextEntry
placeholder={changePassword ? "Current password" : "Password"}
placeholder={
changePassword
? strings.currentPassword()
: strings.password()
}
/>
{!this.state.biometricUnlock ||
@@ -761,11 +726,11 @@ export class VaultDialog extends Component {
changePassword ? null : (
<Button
onPress={() =>
this._onPressFingerprintAuth("Unlock note", "")
this._onPressFingerprintAuth(strings.unlockNote(), "")
}
icon="fingerprint"
width="100%"
title={"Biometric unlock"}
title={strings.unlockWithBiometrics()}
type="transparent"
/>
)}
@@ -788,7 +753,7 @@ export class VaultDialog extends Component {
marginTop: 10
}}
width="100%"
title={"Delete all notes"}
title={strings.deleteAllNotes()}
type="errorShade"
/>
)}
@@ -809,7 +774,7 @@ export class VaultDialog extends Component {
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={"New password"}
placeholder={strings.newPassword()}
/>
</>
) : null}
@@ -824,13 +789,13 @@ export class VaultDialog extends Component {
this.password = value;
}}
autoComplete="password"
returnKeyLabel="Next"
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
onSubmit={() => {
this.confirmPassRef.current?.focus();
}}
placeholder="Password"
placeholder={strings.password()}
/>
<Input
@@ -859,7 +824,7 @@ export class VaultDialog extends Component {
}
}}
onSubmit={this.onPress}
placeholder="Confirm password"
placeholder={strings.confirmPassword()}
/>
</View>
) : null}
@@ -867,9 +832,7 @@ export class VaultDialog extends Component {
{this.state.biometricUnlock &&
!this.state.isBiometryEnrolled &&
novault ? (
<Paragraph>
Unlock with password once to enable biometric access.
</Paragraph>
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
) : null}
{this.state.isBiometryAvailable &&
@@ -888,7 +851,7 @@ export class VaultDialog extends Component {
}}
icon="fingerprint"
width="100%"
title="Biometric unlocking"
title={strings.unlockWithBiometrics()}
iconColor={
this.state.biometricUnlock
? colors.selected.accent
@@ -908,26 +871,26 @@ export class VaultDialog extends Component {
}
positiveTitle={
deleteVault
? "Delete"
? strings.delete()
: clearVault
? "Clear"
? strings.clear()
: fingerprintAccess
? "Enable"
? strings.enable()
: this.state.revokeFingerprintAccess
? "Revoke"
? strings.revoke()
: changePassword
? "Change"
? strings.change()
: this.state.noteLocked
? deleteNote
? "Delete"
? strings.delete()
: share
? "Share "
? strings.share()
: goToEditor
? "Open"
: "Unlock"
? strings.open()
: strings.unlock()
: !note.id
? "Create"
: "Lock"
? strings.create()
: strings.lock()
}
/>
</View>

View File

@@ -23,10 +23,7 @@ import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { Dialog } from "../dialog";
import { Issue } from "../sheets/github/issue";
const error = (
stack: string,
component: string
) => `Please let us know what happened. What steps we can take to reproduce the issue here.
const error = (stack: string, component: string) => `
_______________________________
Stacktrace: In ${component}::${stack}`;
@@ -39,9 +36,9 @@ class ExceptionHandler extends React.Component<{
error: Error | null;
hasError: boolean;
} = {
hasError: false,
error: null
};
hasError: false,
error: null
};
static getDerivedStateFromError(error: Error) {
return { hasError: true, error: error };
}
@@ -81,10 +78,10 @@ class ExceptionHandler extends React.Component<{
}
export const withErrorBoundry = (Element: React.ElementType, name: string) => {
return function ErrorBoundary() {
return function ErrorBoundary(props: any) {
return (
<ExceptionHandler component={name}>
<Element />
<Element {...props} />
</ExceptionHandler>
);
};

View File

@@ -33,7 +33,6 @@ import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
import { useNavigation } from "@react-navigation/native";
type HeaderRightButton = {
title: string;
@@ -55,7 +54,7 @@ export const Header = ({
onSearch
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute: RouteName;
renderedInRoute?: RouteName;
id?: string;
title: string;
headerRightButtons?: HeaderRightButton[];
@@ -67,8 +66,6 @@ export const Header = ({
hasSearch?: boolean;
onSearch?: () => void;
}) => {
const navigation = useNavigation();
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [borderHidden, setBorderHidden] = useState(true);

View File

@@ -19,9 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useRef } from "react";
import { Platform, StyleSheet, View } from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import Menu from "react-native-reanimated-material-menu";
import { Menu } from "react-native-material-menu";
import { notesnook } from "../../../e2e/test.ids";
import {
HeaderRightButton,
@@ -42,7 +41,7 @@ export const RightMenus = ({
onSearch
}: {
headerRightButtons?: HeaderRightButton[];
renderedInRoute: RouteName;
renderedInRoute?: RouteName;
id?: string;
onPressDefaultRightButton?: () => void;
search?: boolean;
@@ -96,14 +95,17 @@ export const RightMenus = ({
animationDuration={200}
style={{
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background
backgroundColor: contextMenuColors.primary.background,
marginTop: 35
}}
onRequestClose={() => {
//@ts-ignore
menuRef.current?.hide();
}}
anchor={
<IconButton
onPress={() => {
//@ts-ignore
menuRef.current?.show();
}}
name="dots-vertical"
@@ -115,9 +117,10 @@ export const RightMenus = ({
{headerRightButtons.map((item) => (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0
borderRadius: 0,
alignSelf: "flex-start",
width: "100%"
}}
type="plain"
buttonType={{
@@ -126,6 +129,7 @@ export const RightMenus = ({
key={item.title}
title={item.title}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();

View File

@@ -41,7 +41,7 @@ export const Title = ({
isHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
renderedInRoute: string;
renderedInRoute?: string;
id?: string;
}) => {
const { colors } = useThemeColors();

View File

@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import dataurl from "@notesnook/core/dist/utils/dataurl";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
import { DataURL, getFileNameWithExtension } from "@notesnook/core";
import type { ImageAttributes } from "@notesnook/editor";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native";
@@ -34,6 +34,11 @@ import {
import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
import RNFetchBlob from "react-native-blob-util";
import Share from "react-native-share";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useSettingStore } from "../../stores/use-setting-store";
const ImagePreview = () => {
const { colors } = useThemeColors("dialog");
@@ -41,6 +46,8 @@ const ImagePreview = () => {
const [image, setImage] = useState<string>();
const [loading, setLoading] = useState(false);
const imageRef = useRef<ImageAttributes>();
const insets = useGlobalSafeAreaInsets();
const [showHeader, setShowHeader] = useState(true);
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
@@ -55,8 +62,8 @@ const ImagePreview = () => {
setLoading(true);
setTimeout(async () => {
let hash = image.hash;
if (!hash && image.src && dataurl.toObject(image.src)) {
const data = dataurl.toObject(image.src);
if (!hash && image.src && DataURL.toObject(image.src)) {
const data = DataURL.toObject(image.src);
if (!data) return;
hash = await Sodium.hashFile({
data: data.data,
@@ -73,16 +80,86 @@ const ImagePreview = () => {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
if (!uri) {
setLoading(false);
return;
}
const attachment = await db.attachments.attachment(hash);
const path = `${cacheDir}/${
"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, path).catch(() => {
/* empty */
});
setImage("file://" + path);
setLoading(false);
}, 100);
};
const close = () => {
const close = React.useCallback(() => {
image &&
RNFetchBlob.fs.unlink(image.replace("file://", "")).catch(() => {
/* empty */
});
setImage(undefined);
setVisible(false);
};
}, [image]);
const renderHeader = React.useCallback(
() => (
<View
style={{
paddingTop: insets.top,
backgroundColor: "rgba(0,0,0,0.3)",
position: "absolute",
zIndex: 999,
display: showHeader ? "flex" : "none"
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 50,
paddingHorizontal: 12,
gap: 10
}}
>
<IconButton
name="share"
color="white"
style={{
borderWidth: 0
}}
onPress={async () => {
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(true);
await Share.open({
url: image
}).catch(() => {
/* empty */
});
useSettingStore
.getState()
.setAppDidEnterBackgroundForAction(false);
}}
/>
<IconButton
name="close"
color="white"
onPress={() => {
close();
}}
/>
</View>
</View>
),
[close, image, insets.top, showHeader]
);
return (
visible && (
@@ -90,6 +167,7 @@ const ImagePreview = () => {
background="black"
animation="slide"
visible={true}
useSafeArea={false}
onRequestClose={close}
transparent
>
@@ -130,43 +208,25 @@ const ImagePreview = () => {
/>
</View>
) : (
<ImageViewer
enableImageZoom={true}
renderIndicator={() => <></>}
enableSwipeDown
useNativeDriver
onSwipeDown={close}
saveToLocalByLongPress={false}
renderHeader={() => (
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 50,
paddingHorizontal: 24,
position: "absolute",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)",
marginTop: Platform.OS === "android" ? 30 : 0
}}
>
<IconButton
name="close"
color="white"
onPress={() => {
close();
}}
/>
</View>
)}
imageUrls={[
{
url: image as string
}
]}
/>
<>
<ImageViewer
enableImageZoom={true}
renderIndicator={() => <></>}
enableSwipeDown
useNativeDriver
onSwipeDown={close}
saveToLocalByLongPress={false}
onClick={() => {
setShowHeader(!showHeader);
}}
renderHeader={renderHeader}
imageUrls={[
{
url: image as string
}
]}
/>
</>
)}
</View>
</BaseDialog>

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