Compare commits

..

237 Commits

Author SHA1 Message Date
Ammar Ahmed
15c27ed273 mobile: fix sync on iOS 2024-11-20 12:35:16 +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
563 changed files with 45235 additions and 19620 deletions

View File

@@ -35,7 +35,8 @@ const SCOPES = [
"common",
"global",
"docs",
"themebuilder"
"themebuilder",
"intl"
];
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

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

View File

@@ -32,6 +32,12 @@ jobs:
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
@@ -55,8 +61,8 @@ jobs:
run: docker pull streetwriters/monograph:latest
# Setup QEMU
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
@@ -72,7 +78,7 @@ jobs:
file: apps/monograph/Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
tags: ${{ steps.meta.outputs.tags }}
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
cache-from: streetwriters/monograph:latest
- name: Generate artifact attestation

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
@@ -53,7 +53,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download build
uses: actions/download-artifact@v4

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

File diff suppressed because it is too large Load Diff

View File

@@ -2,21 +2,38 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.18",
"version": "3.0.21",
"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": {
"@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.2.1",
"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.7.2",
"zod": "^3.23.8"
@@ -26,14 +43,17 @@
"@types/node": "22.7.0",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.1",
"electron": "^30.5.1",
"electron-builder": "^25.0.5",
"electron": "^31.7.4",
"electron-builder": "^25.1.8",
"esbuild": "^0.24.0",
"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.19.8"
"undici": "^6.19.8",
"vitest": "^2.1.5"
},
"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": {

View File

@@ -63,7 +63,7 @@ if (args.variant === "mas") {
await exec(`yarn run bundle`);
}
await exec(`yarn tsc`);
await exec(`yarn run build`);
if (args.run) {
await exec(`yarn electron-builder --dir --x64`);
@@ -94,7 +94,7 @@ async function patchBetterSQLite3() {
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.2.2";
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";

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("npm rebuild electron --verbose --foreground-scripts");
await exec("yarn electron-builder install-app-deps");
}
await exec(`yarn run bundle`);
execAsync(`yarn`, [`tsc`]);
await exec(`yarn run build`);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");

View File

@@ -46,6 +46,8 @@ export class SQLite {
this.sqlite = require("better-sqlite3-multiple-ciphers")(
filePath
).unsafeMode(true);
const betterTrigram = require("sqlite-better-trigram");
betterTrigram.load(this.sqlite);
}
/**

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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";

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()) {
@@ -87,7 +101,7 @@ async function createWindow() {
...(config.desktopSettings.nativeTitlebar
? {}
: {
titleBarStyle: "hidden",
titleBarStyle: process.platform === "win32" || process.platform === "darwin" ? "hidden" : "default",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,

View File

@@ -19,22 +19,21 @@ 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 { ipcRenderer } from "electron";
import { platform } from "os";
declare global {
var os: () => "mas" | ReturnType<typeof platform>;
var electronTRPC: RendererGlobalElectronTRPC;
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;

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

@@ -39,6 +39,7 @@ DerivedData
*.xcuserstate
*.hprof
**/.xcode.env.local
native/cache
# Android/IntelliJ
#
rn-build-deps/

View File

@@ -16,6 +16,8 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
@@ -63,7 +65,9 @@ const App = () => {
<View
style={{
height: "100%",
width: "100%"
width: "100%",
justifyContent: "center",
alignItems: "center"
}}
>
<View
@@ -138,7 +142,11 @@ export const withTheme = (Element: () => JSX.Element) => {
currTheme = nextTheme;
}
return <Element />;
return (
<I18nProvider i18n={i18n}>
<Element />
</I18nProvider>
);
};
};

View File

@@ -33,11 +33,11 @@ import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { getDatabaseKey } from "./encryption";
import SettingsService from "../../services/settings";
import { strings } from "@notesnook/intl";
export async function setupDatabase(password) {
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);
@@ -54,10 +54,10 @@ export async function setupDatabase(password) {
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
compressor: () => ({
compress: Gzip.deflate,
decompress: Gzip.inflate
},
}),
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({

View File

@@ -24,6 +24,7 @@ import type {
} 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 };
@@ -97,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

@@ -19,6 +19,7 @@ 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";
@@ -89,7 +90,7 @@ export async function downloadAttachments(attachments) {
current: 0,
total: 0,
success: false,
message: "Download cancelled",
message: strings.network.dowloadCancelled(),
canceled: true
});
return true;
@@ -116,7 +117,7 @@ export async function downloadAttachments(attachments) {
if (isCancelled()) return;
if (!uri) throw new Error("Failed to download file");
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);
@@ -139,7 +140,7 @@ export async function downloadAttachments(attachments) {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 1,
message: "Saving zip file... Please wait",
message: strings.savingZipFile(),
groupId
});
// If all goes well, zip the notesnook-attachments folder in cache.
@@ -149,9 +150,7 @@ export async function downloadAttachments(attachments) {
groupId,
current: progress,
total: 1,
message: `Saving zip file (${(progress * 100).toFixed(
1
)}%)... Please wait`
message: strings.savingZipFile((progress * 100).toFixed(1))
});
});
await zip(zipSourceFolder, zipOutputFile);
@@ -245,7 +244,7 @@ export default async function downloadAttachment(
);
}
let filename = getFileNameWithExtension(
let filename = await getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
@@ -273,8 +272,8 @@ export default async function downloadAttachment(
if (!options.silent) {
ToastManager.show({
heading: "Download successful",
message: filename + " downloaded",
heading: strings.network.downloadSuccess(),
message: strings.network.fileDownloaded(filename),
type: "success"
});
}
@@ -284,12 +283,8 @@ export default async function downloadAttachment(
}
if (!options.silent) {
presentSheet({
title: "File downloaded",
paragraph: `${filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
title: strings.network.fileDownloaded(),
paragraph: strings.fileSaved(filename, Platform.OS),
icon: "download",
context: global ? null : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />

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 NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
@@ -48,12 +49,12 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
const size = await getUploadedFileSize(filename);
if (size === -1) {
const error = `Uploaded file verification failed. (File hash: ${filename})`;
const error = `${strings.fileVerificationFailed()} (File hash: ${filename})`;
throw new Error(error);
}
if (size === 0) {
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
const error = `${strings.fileLengthError()} (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
@@ -62,7 +63,10 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
const decryptedLength = size - totalChunks * ABYTES;
if (attachment && attachment.size !== decryptedLength) {
const error = `File length mismatch. Expected ${attachment.size} but got ${decryptedLength} bytes. Please upload this file again from the attachment manager. (File hash: ${filename})`;
const error = `${strings.fileLengthMismatch(
attachment.size,
decryptedLength
)} (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
@@ -76,7 +80,9 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
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`);
throw new Error(
`${res.status}: ${strings.failedToResolvedDownloadUrl()}`
);
}
const downloadUrl = await res.text();
@@ -85,7 +91,7 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: Unable to resolve download url`
);
throw new Error("Unable to resolve download url");
throw new Error(strings.failedToResolvedDownloadUrl());
}
DatabaseLogger.log(`Download starting: ${filename}`);
@@ -138,18 +144,15 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
return status >= 200 && status < 300;
} catch (e) {
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
ToastManager.show({
heading: "Error downloading file",
const toast = {
heading: strings.downloadError(),
message: e.message,
type: "error",
context: "global"
});
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "local"
});
};
ToastManager.show(toast);
toast.context = "local";
ToastManager.show(toast);
}
useAttachmentStore.getState().remove(filename);

View File

@@ -126,9 +126,13 @@ export async function clearFileStorage() {
}
export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
try {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
}
} catch (e) {
DatabaseLogger.error(e);
}
}
@@ -231,22 +235,30 @@ export async function exists(filename) {
}
export async function bulkExists(files) {
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
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.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
missingFiles = missingFiles.filter((file) => !appGroupFiles.includes(file));
if (Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.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 [];
}
return missingFiles;
}
export async function getCacheSize() {
await createCacheDir();
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
let total = 0;
console.log("Total files", stat.length);

View File

@@ -24,7 +24,7 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { createCacheDir } from "./io";
import { cacheDir, getUploadedFileSize } from "./utils";
import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
export async function uploadFile(filename, requestOptions, cancelToken) {
if (!requestOptions) return false;
@@ -33,24 +33,42 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
const uploadedFileSize = await getUploadedFileSize(filename);
if (uploadedFileSize === -1) {
const error = `Uploaded file verification failed. (File hash: ${filename})`;
throw new Error(error);
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.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
filePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(filePath);
}
if (uploadedFileSize !== 0) {
if (!exists) {
throw new Error(
`Trying to upload file at path ${filePath} that doest not exist.`
);
}
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
let remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === -1) return false;
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let res = await fetch(url, {
let uploadUrlResponse = await fetch(url, {
method: "PUT",
headers
});
const uploadUrl = res.ok ? await res.text() : await res.json();
const uploadUrl = uploadUrlResponse.ok
? await uploadUrlResponse.text()
: await uploadUrlResponse.json();
if (typeof uploadUrl !== "string") {
throw new Error(
@@ -58,28 +76,9 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
);
}
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({
let uploadRequest = RNFetchBlob.config({
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
@@ -88,7 +87,7 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
{
"content-type": ""
},
RNFetchBlob.wrap(uploadFilePath)
RNFetchBlob.wrap(filePath)
)
.uploadProgress((sent, total) => {
useAttachmentStore
@@ -101,30 +100,27 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
uploadRequest.cancel();
};
let response = await request;
let status = response.info().status;
let text = await response.text();
let result = status >= 200 && status < 300 && text.length === 0;
let uploadResponse = await uploadRequest;
let status = uploadResponse.info().status;
let uploaded = status >= 200 && status < 300;
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;
} else {
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(response.info())}`
}, info: ${JSON.stringify(uploadResponse.info())}`
);
}
return result;
const attachment = await db.attachments.attachment(filename);
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, "File upload failed");

View File

@@ -123,3 +123,18 @@ export async function getUploadedFileSize(hash) {
return -1;
}
}
export async function checkUpload(filename, chunkSize, expectedSize) {
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);
}

View File

@@ -53,6 +53,7 @@ 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");
@@ -157,7 +158,7 @@ const AppLockedOverlay = () => {
password.current = undefined;
} else {
ToastManager.show({
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
heading: strings.invalid(keyboardType),
type: "error",
context: "local"
});
@@ -250,7 +251,7 @@ const AppLockedOverlay = () => {
textAlign: "center"
}}
>
Unlock your notes
{strings.unlockNotes()}
</Heading>
<Paragraph
@@ -260,7 +261,7 @@ const AppLockedOverlay = () => {
maxWidth: "90%"
}}
>
{"Please verify it's you"}
{strings.verifyItsYou()}
</Paragraph>
<Seperator />
<View
@@ -278,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();
@@ -301,7 +302,7 @@ const AppLockedOverlay = () => {
{user || appLockHasPasswordSecurity ? (
<>
<Button
title="Continue"
title={strings.continue()}
type="accent"
onPress={onSubmit}
width={250}
@@ -317,7 +318,7 @@ const AppLockedOverlay = () => {
{biometricsAuthEnabled ? (
<Button
title="Unlock with Biometrics"
title={strings.unlockWithBiometrics()}
width={250}
onPress={onUnlockAppRequested}
icon={"fingerprint"}

View File

@@ -57,6 +57,7 @@ import { Notice } from "../ui/notice";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
const Actions = ({
attachment,
@@ -81,7 +82,7 @@ const Actions = ({
const actions = [
{
name: "Download",
name: strings.network.download(),
onPress: async () => {
if (currentProgress) {
await db.fs().cancel(attachment.hash);
@@ -93,11 +94,11 @@ const Actions = ({
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,10 +114,10 @@ const Actions = ({
icon: "upload"
},
{
name: "Run file check",
name: strings.fileCheck(),
onPress: async () => {
setLoading({
name: "Run file check"
name: strings.fileCheck()
});
const result = await filesystem.checkAttachment(attachment.hash);
if (!result) return;
@@ -125,7 +126,7 @@ const Actions = ({
db.attachments.markAsFailed(attachment.id, result.failed);
setFailed(result.failed);
ToastManager.show({
heading: "File check failed with error: " + result.failed,
heading: strings.fileCheckFailed(result.failed),
type: "error",
context: "local"
});
@@ -134,7 +135,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"
});
@@ -148,12 +149,11 @@ const Actions = ({
icon: "file-check"
},
{
name: "Rename",
onPress: async () => {
name: strings.rename(),
onPress: () => {
presentDialog({
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) {
@@ -166,13 +166,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);
@@ -256,8 +256,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
@@ -265,7 +264,7 @@ const Actions = ({
Clipboard.setString(attachment.hash);
ToastManager.show({
type: "success",
heading: "Attachment hash copied",
heading: strings.hashCopied(),
context: "local"
});
}}
@@ -295,7 +294,7 @@ const Actions = ({
}}
size={SIZE.sm}
>
List of notes:
{strings.listOf()} {strings.dataTypesPlural.note()}:
</Heading>
{notes.map((item) => (
@@ -354,7 +353,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}

View File

@@ -30,6 +30,7 @@ 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";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
@@ -136,7 +137,7 @@ export const AttachmentItem = ({
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
File size: {formatBytes(attachment.size)}
{strings.fileSize()}: {formatBytes(attachment.size)}
</Paragraph>
) : null}
</View>

View File

@@ -24,6 +24,7 @@ import {
SortOptions,
VirtualizedGrouping
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, View } from "react-native";
@@ -94,31 +95,31 @@ const useRechecker = create<RecheckerState>((set) => ({
const attachmentTypes = [
{
title: "All",
title: strings.mediaTypes.all(),
filterBy: "all"
},
{
title: "Images",
title: strings.mediaTypes.image(),
filterBy: "images"
},
{
title: "Docs",
filterBy: "documents"
},
{
title: "Video",
filterBy: "video"
},
{
title: "Audio",
title: strings.mediaTypes.audio(),
filterBy: "audio"
},
{
title: "Orphaned",
title: strings.mediaTypes.video(),
filterBy: "video"
},
{
title: strings.mediaTypes.document(),
filterBy: "documents"
},
{
title: strings.mediaTypes.orphaned(),
filterBy: "orphaned"
},
{
title: "Errors",
title: strings.mediaTypes.errors(),
filterBy: "errors"
}
];
@@ -234,7 +235,7 @@ export const AttachmentDialog = ({
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
if (!getState().isWorking) {
ToastManager.show({
message: "Attachment recheck cancelled",
message: strings.attachmentRecheckCancelled(),
type: "info",
context: isSheet ? "local" : "global"
});
@@ -321,7 +322,7 @@ export const AttachmentDialog = ({
) : null}
{!isSheet ? (
<Header
title="Manage attachments"
title={strings.manageAttachments()}
renderedInRoute="SettingsGroup"
canGoBack
headerRightButtons={[
@@ -329,23 +330,23 @@ export const AttachmentDialog = ({
onPress() {
onCheck();
},
title: "Recheck all"
title: strings.recheckAll()
},
{
onPress() {
if (!attachments) return;
presentDialog({
title: `Download ${attachments.placeholders.length} attachments`,
paragraph:
"Are you sure you want to download all attachments?",
positiveText: "Download",
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: "Cancel"
negativeText: strings.cancel()
});
},
title: "Download all"
title: strings.downloadAllAttachments()
}
]}
/>
@@ -358,7 +359,7 @@ export const AttachmentDialog = ({
paddingHorizontal: 12
}}
>
<Heading>Attachments</Heading>
<Heading>{strings.dataTypesPluralCamelCase.attachment()}</Heading>
<View
style={{
@@ -387,15 +388,14 @@ export const AttachmentDialog = ({
onPress={() => {
if (!attachments) return;
presentDialog({
title: `Download ${attachments.placeholders.length} attachments`,
paragraph:
"Are you sure you want to download all attachments?",
context: "local",
positiveText: "Download",
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: "Cancel"
negativeText: strings.cancel()
});
}}
size={SIZE.lg}
@@ -414,7 +414,7 @@ export const AttachmentDialog = ({
>
<Seperator />
<Input
placeholder="Filter attachments by filename, type or hash"
placeholder={strings.filterAttachments()}
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
@@ -456,14 +456,16 @@ export const AttachmentDialog = ({
<Paragraph>
{rechecker.isWorking
? note
? `Checking ${rechecker.filter.toLowerCase()} note attachments`
: `Checking ${rechecker.filter.toLowerCase()} attachments`
: "Attachments recheck complete"}
? strings.checkingNoteAttachments()
: strings.checkingAllAttachments()
: strings.attachmentRecheckComplete()}
</Paragraph>
<Paragraph>
{`${rechecker.isWorking ? "Please wait... " : ""}Passed: ${
{`${
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
}${strings.passed()}: ${
rechecker.passed
}, Failed: ${rechecker.failed}`}
}, ${strings.failed()}: ${rechecker.failed}`}
</Paragraph>
</View>
</View>
@@ -554,9 +556,7 @@ export const AttachmentDialog = ({
size={60}
color={colors.secondary.icon}
/>
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
<Paragraph>{strings.noAttachments()}</Paragraph>
</>
)}
</View>

View File

@@ -19,6 +19,9 @@ 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 { Platform, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
eUnSubscribeEvent
@@ -28,9 +31,12 @@ import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import { Toast } from "../toast";
import { initialAuthMode } from "./common";
import { Button } from "../ui/button";
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,
@@ -44,6 +50,7 @@ const AuthModal = () => {
const [visible, setVisible] = useState(false);
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
const actionSheetRef = useRef();
const insets = useGlobalSafeAreaInsets();
useEffect(() => {
eSubscribeEvent(eOpenLoginDialog, open);
@@ -91,18 +98,81 @@ const AuthModal = () => {
centered={false}
enableSheetKeyboardHandler
>
{currentAuthMode !== AuthMode.login ? (
<Signup
changeMode={(mode) => setCurrentAuthMode(mode)}
trial={AuthMode.trialSignup === currentAuthMode}
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
/>
) : (
<Login
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
changeMode={(mode) => setCurrentAuthMode(mode)}
/>
)}
<KeyboardAwareScrollView
style={{
width: "100%"
}}
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
{currentAuthMode !== AuthMode.login ? (
<Signup
changeMode={(mode) => setCurrentAuthMode(mode)}
trial={AuthMode.trialSignup === currentAuthMode}
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
/>
) : (
<Login
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
changeMode={(mode) => setCurrentAuthMode(mode)}
/>
)}
</KeyboardAwareScrollView>
<View
style={{
position: "absolute",
paddingTop: Platform.OS === "android" ? 0 : insets.top,
top: 0,
zIndex: 999,
backgroundColor: colors.secondary.background,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
width: "100%",
height: 50,
justifyContent:
initialAuthMode.current !== AuthMode.welcomeSignup
? "space-between"
: "flex-end"
}}
>
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
<IconButton
name="arrow-left"
onPress={() => {
hideAuth();
}}
color={colors.primary.paragraph}
/>
)}
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title={strings.skip()}
onPress={() => {
hideAuth();
}}
iconSize={16}
type="plain"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingHorizontal: 6
}}
/>
)}
</View>
</View>
<Toast context="local" />
</BaseDialog>

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"
});
@@ -68,13 +69,13 @@ export const ChangePassword = () => {
setLoading(true);
try {
const result = await BackupService.run(false, "change-password-dialog");
if (result.error)
throw new Error(`Failed to create backup: ${result.error}`);
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"
});
@@ -85,7 +86,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"
@@ -102,10 +103,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
@@ -119,7 +117,7 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Old Password"
placeholder={strings.oldPassword()}
/>
<Input
@@ -128,27 +126,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={{
@@ -158,7 +150,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

@@ -1,80 +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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Platform, View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import { hideAuth } from "./common";
export const AuthHeader = (props: { welcome?: boolean }) => {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
return (
<View
style={{
paddingTop: Platform.OS === "android" ? 0 : insets.top,
backgroundColor: colors.secondary.background,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
width: "100%",
height: 50,
justifyContent: !props.welcome ? "space-between" : "flex-end"
}}
>
{props.welcome ? null : (
<IconButton
name="arrow-left"
onPress={() => {
hideAuth();
}}
color={colors.primary.paragraph}
/>
)}
{!props.welcome ? null : (
<Button
title="Skip"
onPress={() => {
hideAuth();
}}
iconSize={16}
type="plain"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingHorizontal: 6
}}
/>
)}
</View>
</View>
);
};

View File

@@ -21,7 +21,6 @@ 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 { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import Sync from "../../services/sync";
@@ -39,6 +38,7 @@ import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { useLogin } from "./use-login";
import { strings } from "@notesnook/intl";
const LoginSteps = {
emailAuth: 1,
@@ -90,233 +90,220 @@ export const Login = ({ changeMode }) => {
<>
<ForgotPassword />
<SheetProvider context="two_factor_verify" />
<KeyboardAwareScrollView
<View
style={{
width: "100%"
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
alignSelf: "center"
}}
contentContainerStyle={{
minHeight: "90%"
}}
nestedScrollEnabled
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
alignSelf: "center"
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.secondary.background,
borderBottomWidth: 0.8,
marginBottom: 12,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%",
minHeight: height * 0.4
}}
>
<View
style={{
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.secondary.background,
borderBottomWidth: 0.8,
marginBottom: 12,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor:
deviceMode !== "mobile" ? colors.primary.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%",
minHeight: height * 0.4
flexDirection: "row"
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<Heading
style={{
marginBottom: 25,
marginTop: 10
}}
extraBold
size={SIZE.xxl}
>
Login to your {"\n"}account
</Heading>
</View>
<View
style={{
width: DDS.isTab
? focused
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: 20
}}
>
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage="Email is invalid"
placeholder="Enter your email"
defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading}
onSubmit={() => {
if (step === LoginSteps.emailAuth) {
login();
} else {
passwordInputRef.current?.focus();
}
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
{step === LoginSteps.passwordAuth && (
<>
<Input
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
testID="input.password"
returnKeyLabel="Done"
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Password"
marginBottom={0}
editable={!loading}
defaultValue={password.current}
onSubmit={() => login()}
/>
<Button
title="Forgot your password?"
style={{
alignSelf: "flex-end",
height: 30,
paddingHorizontal: 0
}}
onPress={() => {
if (loading) return;
SheetManager.show("forgotpassword_sheet", email.current);
}}
textStyle={{
textDecorationLine: "underline"
}}
fontSize={SIZE.xs}
type="plain"
/>
</>
)}
<View
style={{
marginTop: 25
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
>
/>
</View>
<Heading
style={{
marginBottom: 25,
marginTop: 10
}}
extraBold
size={SIZE.xxl}
>
{strings.loginToYourAccount()}
</Heading>
</View>
<View
style={{
width: DDS.isTab
? focused
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: 20
}}
>
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading}
onSubmit={() => {
if (step === LoginSteps.emailAuth) {
login();
} else {
passwordInputRef.current?.focus();
}
}}
/>
{step === LoginSteps.passwordAuth && (
<>
<Input
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
testID="input.password"
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
marginBottom={0}
editable={!loading}
defaultValue={password.current}
onSubmit={() => login()}
/>
<Button
loading={loading}
title={strings.forgotPassword()}
style={{
alignSelf: "flex-end",
height: 30,
paddingHorizontal: 0
}}
onPress={() => {
if (loading) return;
login();
SheetManager.show("forgotpassword_sheet", email.current);
}}
style={{
width: 250,
borderRadius: 100
textStyle={{
textDecorationLine: "underline"
}}
height={50}
fontSize={SIZE.md}
type="accent"
title={!loading ? "Continue" : null}
fontSize={SIZE.xs}
type="plain"
/>
</>
)}
{step === LoginSteps.passwordAuth && (
<Button
title="Cancel logging in"
style={{
alignSelf: "center",
height: 30,
marginTop: 10
}}
onPress={() => {
if (loading) return;
setStep(LoginSteps.emailAuth);
setLoading(false);
}}
textStyle={{
textDecorationLine: "underline"
}}
fontSize={SIZE.xs}
type="errorShade"
/>
)}
<View
style={{
marginTop: 25
}}
>
<Button
loading={loading}
onPress={() => {
if (loading) return;
login();
}}
style={{
width: 250,
borderRadius: 100
}}
height={50}
fontSize={SIZE.md}
type="accent"
title={!loading ? strings.continue() : null}
/>
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
{step === LoginSteps.passwordAuth && (
<Button
title={strings.cancelLogin()}
style={{
alignSelf: "center",
height: 30,
marginTop: 10
}}
onPress={() => {
if (loading) return;
setStep(LoginSteps.emailAuth);
setLoading(false);
}}
textStyle={{
textDecorationLine: "underline"
}}
fontSize={SIZE.xs}
type="errorShade"
/>
)}
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph
size={SIZE.xs + 1}
color={colors.secondary.paragraph}
>
{strings.dontHaveAccount()}{" "}
<Paragraph
size={SIZE.xs + 1}
color={colors.secondary.paragraph}
style={{ color: colors.primary.accent }}
>
Don't have an account?{" "}
<Paragraph
size={SIZE.xs + 1}
style={{ color: colors.primary.accent }}
>
Sign up
</Paragraph>
{strings.signUp()}
</Paragraph>
</TouchableOpacity>
) : null}
</View>
</Paragraph>
</TouchableOpacity>
) : null}
</View>
</View>
</KeyboardAwareScrollView>
</View>
</>
);
};

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 "";
@@ -193,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>
@@ -211,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}
@@ -231,7 +231,7 @@ export const SessionExpired = () => {
loading={loading}
onPress={() => login()}
type="accent"
title={loading ? null : "Login"}
title={loading ? null : strings.login()}
/>
<Button
@@ -242,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

@@ -17,20 +17,18 @@ 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, { useRef, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
import { useSettingStore } from "../../stores/use-setting-store";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../stores/use-user-store";
import { openLinkInBrowser } from "../../utils/functions";
import { SIZE } from "../../utils/size";
import { Loading } from "../loading";
import { PaywallComponent } from "../premium/component";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Input from "../ui/input";
import Heading from "../ui/typography/heading";
@@ -38,17 +36,9 @@ import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { useSettingStore } from "../../stores/use-setting-store";
import SettingsService from "../../services/settings";
import { AuthHeader } from "./header";
import { SignupContext } from "./signup-context";
import { strings } from "@notesnook/intl";
const SignupSteps = {
signup: 0,
selectPlan: 1,
createAccount: 2
};
export const Signup = ({ changeMode, welcome }) => {
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
export const Signup = ({ changeMode, trial }) => {
const { colors } = useThemeColors();
const email = useRef();
const emailInputRef = useRef();
@@ -61,12 +51,12 @@ export const Signup = ({ changeMode, welcome }) => {
const setUser = useUserStore((state) => state.setUser);
const setLastSynced = useUserStore((state) => state.setLastSynced);
const deviceMode = useSettingStore((state) => state.deviceMode);
const { height } = useWindowDimensions();
const { width, height } = useWindowDimensions();
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"
});
@@ -80,287 +70,238 @@ export const Signup = ({ changeMode, welcome }) => {
const signup = async () => {
if (!validateInfo() || error) return;
if (loading) return;
setLoading(true);
try {
setCurrentStep(SignupSteps.createAccount);
await db.user.signup(email.current.toLowerCase(), password.current);
let user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
clearMessage();
setEmailVerifyMessage();
setCurrentStep(SignupSteps.selectPlan);
return true;
hideAuth();
SettingsService.setProperty("encryptedBackup", true);
await sleep(300);
if (trial) {
PremiumService.sheet(null, null, true);
} else {
PremiumService.showVerifyEmailDialog();
}
} catch (e) {
setCurrentStep(SignupSteps.signup);
setLoading(false);
ToastManager.show({
heading: "Signup failed",
heading: strings.signupFailed(),
message: e.message,
type: "error",
context: "local"
});
return false;
}
};
return (
<SignupContext.Provider
value={{
signup: signup
}}
>
{currentStep === SignupSteps.signup ? (
<>
<AuthHeader welcome={welcome} />
<KeyboardAwareScrollView
<>
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
alignSelf: "center"
}}
>
<View
style={{
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.secondary.background,
marginBottom: 20,
borderBottomWidth: 0.8,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%",
minHeight: height * 0.4
}}
>
<View
style={{
width: "100%"
flexDirection: "row"
}}
contentContainerStyle={{
minHeight: "90%"
}}
nestedScrollEnabled
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%"
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<Heading
extraBold
style={{
marginBottom: 25,
marginTop: 10
}}
size={SIZE.xxl}
>
{strings.createYourAccount()}
</Heading>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
paddingHorizontal: 20,
backgroundColor: colors.primary.background,
alignSelf: "center"
}}
>
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.email()}
placeholder={strings.email()}
onSubmit={() => {
passwordInputRef.current?.focus();
}}
/>
<Input
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
testID="input.password"
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
validationType="password"
autoCorrect={false}
placeholder={strings.password()}
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
/>
<Input
fwdRef={confirmPasswordInputRef}
onChangeText={(value) => {
confirmPassword.current = value;
}}
testID="input.confirmPassword"
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.done()}
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
validationType="confirmPassword"
customValidator={() => password.current}
placeholder={strings.confirmPassword()}
marginBottom={12}
onSubmit={signup}
/>
<Paragraph
style={{
marginBottom: 25
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{strings.signupAgreement[0]()}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos", colors);
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
<View
style={{
justifyContent: "flex-end",
paddingHorizontal: 16,
backgroundColor: colors.secondary.background,
marginBottom: 20,
borderBottomWidth: 0.8,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor:
deviceMode !== "mobile" ? colors.primary.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%",
minHeight: height * 0.25
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
{strings.signupAgreement[1]()}
</Paragraph>
{strings.signupAgreement[2]()}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy", colors);
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
{strings.signupAgreement[3]()}
</Paragraph>
{strings.signupAgreement[4]()}
</Paragraph>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<Heading
extraBold
style={{
marginBottom: 25,
marginTop: 10
}}
size={SIZE.xxl}
>
Create your account
</Heading>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
paddingHorizontal: 16,
backgroundColor: colors.primary.background,
flexGrow: 1
}}
>
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage="Email is invalid"
placeholder="Email"
blurOnSubmit={false}
onSubmit={() => {
if (!email.current) return;
passwordInputRef.current?.focus();
}}
/>
<Input
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
testID="input.password"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
blurOnSubmit={false}
validationType="password"
autoCorrect={false}
placeholder="Password"
onSubmit={() => {
if (!password.current) return;
confirmPasswordInputRef.current?.focus();
}}
/>
<Input
fwdRef={confirmPasswordInputRef}
onChangeText={(value) => {
confirmPassword.current = value;
}}
testID="input.confirmPassword"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Signup"
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
blurOnSubmit={false}
validationType="confirmPassword"
customValidator={() => password.current}
placeholder="Confirm password"
marginBottom={12}
onSubmit={signup}
/>
<Button
title={!loading ? "Continue" : null}
type="accent"
loading={loading}
onPress={() => {
signup();
}}
fontSize={SIZE.md}
width="100%"
/>
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(0);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph
size={SIZE.xs + 1}
color={colors.secondary.paragraph}
>
Already have an account?{" "}
<Paragraph
size={SIZE.xs + 1}
style={{ color: colors.primary.accent }}
>
Login
</Paragraph>
</Paragraph>
</TouchableOpacity>
</View>
<Button
title={!loading ? strings.continue() : null}
type="accent"
loading={loading}
onPress={signup}
fontSize={SIZE.md}
style={{
marginRight: 12,
width: 250,
borderRadius: 100
}}
/>
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(0);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.secondary.paragraph}>
{strings.alreadyHaveAccount()}{" "}
<Paragraph
style={{
marginBottom: 25,
paddingHorizontal: 16,
textAlign: "center"
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
size={SIZE.xs + 1}
style={{ color: colors.primary.accent }}
>
By signing up, you agree to our{" "}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos", colors);
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
Terms of Service{" "}
</Paragraph>
and{" "}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy", colors);
}}
style={{
textDecorationLine: "underline"
}}
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.login()}
</Paragraph>
</View>
</KeyboardAwareScrollView>
</>
) : currentStep === SignupSteps.createAccount ? (
<>
<Loading
title="Setting up your account..."
description="Your account is almost ready, please wait..."
/>
</>
) : (
<>
<PaywallComponent
close={() => {
hideAuth();
}}
setupAccount={() => {
setCurrentStep(SignupSteps.createAccount);
}}
isModal={false}
/>
</>
)}
</SignupContext.Provider>
</Paragraph>
</TouchableOpacity>
</View>
</View>
</>
);
};

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,20 +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";
import { ScrollView } from "react-native-actions-sheet";
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
const { colors } = useThemeColors();
const code = useRef();
const [currentMethod, setCurrentMethod] = useState({
@@ -51,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);
@@ -94,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"
}
];
@@ -151,7 +136,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
<View
style={{
alignItems: "center",
paddingHorizontal: currentMethod.method ? 12 : 0
paddingHorizontal: currentMethod.method ? 12 : 0,
gap: 12
}}
>
<IconButton
@@ -168,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={{
@@ -178,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}
@@ -191,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
@@ -238,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}
/>
</>
) : (
<>
@@ -307,15 +299,16 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
);
};
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

@@ -18,15 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useRef, useState } from "react";
import { TextInput } from "react-native";
import { db } from "../../common/database";
import { ToastManager, eSendEvent } from "../../services/event-manager";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { clearMessage } from "../../services/message";
import PremiumService from "../../services/premium";
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,
@@ -34,18 +34,15 @@ export const LoginSteps = {
passwordAuth: 3
};
export const useLogin = (
onFinishLogin?: () => void,
sessionExpired = false
) => {
export const useLogin = (onFinishLogin, sessionExpired = false) => {
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const [step, setStep] = useState(LoginSteps.emailAuth);
const email = useRef<string>();
const password = useRef<string>();
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const email = useRef();
const password = useRef();
const emailInputRef = useRef();
const passwordInputRef = useRef();
const validateInfo = () => {
if (
@@ -53,8 +50,8 @@ export const useLogin = (
(!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"
});
@@ -72,17 +69,11 @@ export const useLogin = (
setLoading(true);
switch (step) {
case LoginSteps.emailAuth: {
if (!email.current) {
setLoading(false);
return;
}
const mfaInfo = await db.user.authenticateEmail(email.current);
console.log("email auth", mfaInfo);
if (mfaInfo) {
TwoFactorVerification.present(
async (
mfa: { code: string; method: string },
callback: (value: boolean) => void
) => {
async (mfa, callback) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
@@ -94,35 +85,36 @@ export const useLogin = (
setLoading(false);
setTimeout(() => {
passwordInputRef.current?.focus();
}, 1);
}, 500);
callback && callback(true);
}
callback && callback(false);
} catch (e) {
callback && callback(false);
if ((e as Error).message === "invalid_grant") {
if (e.message === "invalid_grant") {
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
}
},
mfaInfo
mfaInfo,
() => {
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
);
} else {
finishWithError(new Error("Unable to send 2FA code"));
finishWithError(new Error(strings.unableToSend2faCode()));
}
break;
}
case LoginSteps.passwordAuth: {
if (!email.current || !password.current) {
setLoading(false);
return;
}
await db.user.authenticatePassword(
email.current,
password.current,
undefined,
null,
sessionExpired
);
finishLogin();
@@ -131,15 +123,15 @@ export const useLogin = (
}
setLoading(false);
} catch (e) {
finishWithError(e as Error);
finishWithError(e);
}
};
const finishWithError = async (e: Error) => {
const finishWithError = async (e) => {
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"
@@ -148,13 +140,13 @@ export const useLogin = (
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

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

@@ -38,6 +38,7 @@ 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();
@@ -50,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",

View File

@@ -49,6 +49,7 @@ 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();
@@ -119,22 +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"
? `Enter ${
accountPass ? "account password" : `app lock ${keyboardType}`
} to remove ${keyboardType}`
: `Set up a custom app lock ${keyboardType} to unlock the app`
}
title={strings.changeAppLockCredentials(mode, keyboardType)}
icon="shield"
padding={12}
/>
@@ -157,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}
@@ -179,7 +169,9 @@ export const AppLockPassword = () => {
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={
@@ -206,10 +198,14 @@ export const AppLockPassword = () => {
}
placeholder={
accountPass
? "Account password"
? strings.enterAccountPassword()
: mode === "change"
? `New ${keyboardType}`
: `${keyboardType === "pin" ? "Pin" : "Password"}`
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin" ? strings.pin() : strings.password()
}`
}
/>
@@ -228,10 +224,14 @@ 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}
@@ -253,7 +253,7 @@ export const AppLockPassword = () => {
iconColor={
accountPass ? colors.primary.accent : colors.primary.icon
}
title="Use account password instead"
title={strings.useAccountPassword()}
style={{
width: "100%",
alignSelf: "flex-start",
@@ -272,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"
);
@@ -281,11 +281,7 @@ 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"
);
@@ -301,7 +297,7 @@ export const AppLockPassword = () => {
!values.current.confirmPassword
) {
ToastManager.error(
new Error("All inputs are required"),
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
@@ -310,11 +306,7 @@ 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"
);
@@ -326,9 +318,7 @@ export const AppLockPassword = () => {
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
),
new Error(strings.incorrect(keyboardType)),
undefined,
"local"
);
@@ -342,7 +332,7 @@ export const AppLockPassword = () => {
} else if (mode === "remove") {
if (!values.current.password) {
ToastManager.error(
new Error("All inputs are required"),
new Error(strings.allFieldsRequired()),
undefined,
"local"
);
@@ -357,10 +347,8 @@ export const AppLockPassword = () => {
ToastManager.error(
new Error(
accountPass
? "Account password incorrect"
: `${
keyboardType === "pin" ? "Pin" : "Password"
} incorrect`
? strings.passwordIncorrect()
: strings.incorrect(keyboardType)
),
undefined,
"local"
@@ -377,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"
});
}
@@ -391,9 +379,13 @@ export const AppLockPassword = () => {
close();
}}
positiveTitle={
mode === "remove" ? "Remove" : mode === "change" ? "Change" : "Save"
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
}
negativeTitle="Cancel"
negativeTitle={strings.cancel()}
positiveType="transparent"
loading={false}
doneText=""

View File

@@ -25,13 +25,14 @@ import {
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,
@@ -64,7 +65,7 @@ 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) => (
@@ -123,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%",
@@ -140,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%",
@@ -150,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={() => {

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,31 +149,31 @@ 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,

View File

@@ -45,6 +45,7 @@ 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;
@@ -137,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);
@@ -187,8 +188,7 @@ const PDFPreview = () => {
}}
color={colors.static.white}
>
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
... Please wait
{strings.loadingWithProgress(progress.percent)}
</Paragraph>
</Animated.View>
) : (

View File

@@ -164,7 +164,7 @@ export default function Progress() {
/>
</View>
{data?.canHideProgress ? null : (
{!data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="secondaryAccented"

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,20 +322,21 @@ 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
);
});
}
console.log("VAULT UPDATED EVENT");
eSendEvent("vaultUpdated");
this.setState({
loading: false
@@ -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"
});
@@ -433,7 +411,7 @@ export class VaultDialog extends Component {
this.close();
ToastManager.show({
message: "Note locked successfully",
message: strings.noteLocked(),
type: "error",
context: "local"
});
@@ -446,8 +424,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"
});
@@ -504,8 +481,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 +489,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 +514,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 +535,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"
});
@@ -569,6 +543,7 @@ export class VaultDialog extends Component {
this.close();
})
.catch((e) => {
console.log("Error", e);
this._takeErrorAction(e);
});
}
@@ -588,9 +563,8 @@ export class VaultDialog extends Component {
async _copyNote(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,7 +574,7 @@ export class VaultDialog extends Component {
this.close();
try {
await Share.open({
heading: "Share note",
heading: note.title,
failOnCancel: false,
message: (await convertNoteToText(note)) || ""
});
@@ -610,24 +584,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 +602,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 +674,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 +710,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 +728,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 +755,7 @@ export class VaultDialog extends Component {
marginTop: 10
}}
width="100%"
title={"Delete all notes"}
title={strings.deleteAllNotes()}
type="errorShade"
/>
)}
@@ -809,7 +776,7 @@ export class VaultDialog extends Component {
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={"New password"}
placeholder={strings.newPassword()}
/>
</>
) : null}
@@ -824,13 +791,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 +826,7 @@ export class VaultDialog extends Component {
}
}}
onSubmit={this.onPress}
placeholder="Confirm password"
placeholder={strings.confirmPassword()}
/>
</View>
) : null}
@@ -867,9 +834,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 +853,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 +873,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

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

View File

@@ -17,10 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
import { SwiperFlatList } from "react-native-swiper-flatlist";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import SettingsService from "../../services/settings";
@@ -33,37 +35,13 @@ import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
type IntroItem = {
headings?: string[];
body?: string;
testimonial?: string;
link?: string;
user?: string;
};
const introItems: IntroItem[] = [
{
headings: ["Open source.", "End to end encrypted.", "Private."],
body: "Write notes with freedom, no spying, no tracking."
},
{
headings: ["Privacy for everyone", "— not just the", "privileged few"],
body: "Your privacy matters to us, no matter who you are. In a world where everyone is trying to spy on you, Notesnook encrypts all your data before it leaves your device. With Notesnook no one can ever sell your data again."
},
{
testimonial:
"You simply cannot get any better of a note taking app than @notesnook. The UI is clean and slick, it is feature rich, encrypted, reasonably priced (esp. for students & educators) & open source",
link: "https://twitter.com/andrewsayer/status/1637817220113002503",
user: "@andrewsayer on Twitter"
}
];
const Intro = () => {
const Intro = ({ navigation }) => {
const { colors } = useThemeColors();
const { width } = useWindowDimensions();
const { width, height } = useWindowDimensions();
const deviceMode = useSettingStore((state) => state.deviceMode);
const insets = useGlobalSafeAreaInsets();
const renderItem = React.useCallback(
({ item }: { item: IntroItem }) => (
({ item }) => (
<View
style={{
justifyContent: "center",
@@ -105,31 +83,32 @@ const Intro = () => {
>
{item.headings?.map((heading) => (
<Heading
key={heading}
key={heading()}
style={{
marginBottom: 5
}}
extraBold
size={SIZE.xxl}
>
{heading}
{heading()}
</Heading>
))}
{item.body ? <Paragraph size={SIZE.sm}>{item.body}</Paragraph> : null}
{item.body ? (
<Paragraph size={SIZE.sm}>{item.body()}</Paragraph>
) : null}
{item.testimonial ? (
{item.tesimonial ? (
<Paragraph
style={{
fontStyle: "italic",
fontSize: SIZE.lg
}}
onPress={() => {
if (!item.link) return;
Linking.openURL(item.link);
}}
>
{item.testimonial} {item.user}
{item.tesimonial()} {item.user}
</Paragraph>
) : null}
</View>
@@ -144,23 +123,22 @@ const Intro = () => {
style={{
width: "100%"
}}
contentContainerStyle={{
minHeight: "100%"
}}
>
<View
style={{
width: deviceMode !== "mobile" ? width / 2 : "100%",
backgroundColor: colors.secondary.background,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : undefined,
borderColor:
deviceMode !== "mobile" ? colors.primary.border : undefined,
borderRadius: deviceMode !== "mobile" ? 20 : undefined,
marginTop: deviceMode !== "mobile" ? 50 : undefined,
flexGrow: 2
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
minHeight: height * 0.7
}}
>
<SwiperFlatList
@@ -170,7 +148,7 @@ const Intro = () => {
index={0}
useReactNativeGestureHandler={true}
showPagination
data={introItems}
data={strings.introData}
paginationActiveColor={colors.primary.accent}
paginationStyleItem={{
width: 10,
@@ -186,14 +164,13 @@ const Intro = () => {
<View
style={{
width: "100%",
paddingHorizontal: 16,
gap: 12,
paddingVertical: 16
justifyContent: "center",
minHeight: height * 0.3
}}
>
<Button
width="100%"
onPress={() => {
width={250}
onPress={async () => {
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
setTimeout(() => {
SettingsService.set({
@@ -207,28 +184,12 @@ const Intro = () => {
style={{
paddingHorizontal: 24,
alignSelf: "center",
...getElevationStyle(5)
...getElevationStyle(5),
borderRadius: 100
}}
fontSize={SIZE.md}
type="accent"
title="Get started"
/>
<Button
width="100%"
title="I already have an account"
type="secondary"
onPress={() => {
eSendEvent(eOpenLoginDialog, AuthMode.login);
setTimeout(() => {
SettingsService.set({
introCompleted: true
});
Navigation.replace("Notes", {
canGoBack: false
});
}, 1000);
}}
title={strings.getStarted()}
/>
</View>
</ScrollView>

File diff suppressed because one or more lines are too long

View File

@@ -29,6 +29,7 @@ import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
export const NotebookHeader = ({
notebook,
@@ -57,7 +58,7 @@ export const NotebookHeader = ({
}
});
ToastManager.show({
heading: "Shortcut created",
heading: strings.shortcutCreated(),
type: "success"
});
}
@@ -148,11 +149,7 @@ export const NotebookHeader = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{notebook && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
{strings.notes(totalNotes || 0)}
</Paragraph>
</View>
);

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
@@ -26,7 +27,6 @@ import { presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { RouteName } from "../../../stores/use-navigation-store";
import { getContainerBorder } from "../../../utils/colors";
import { GROUP } from "../../../utils/constants";
import { SIZE } from "../../../utils/size";
import Sort from "../../sheets/sort";
import { Button } from "../../ui/button";
@@ -57,17 +57,14 @@ export const SectionHeader = React.memo<
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
let groupBy = Object.keys(GROUP).find(
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
);
const groupBy =
strings.groupByStrings[
groupOptions.groupBy as keyof typeof strings.groupByStrings
]?.();
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
);
groupBy = !groupBy
? "Default"
: groupBy.slice(0, 1).toUpperCase() + groupBy.slice(1, groupBy.length);
return (
<View
style={{
@@ -103,7 +100,7 @@ export const SectionHeader = React.memo<
textAlignVertical: "center"
}}
>
{!item.title || item.title === "" ? "Pinned" : item.title}
{!item.title || item.title === "" ? strings.pinned() : item.title}
</Heading>
</TouchableOpacity>
@@ -117,12 +114,10 @@ export const SectionHeader = React.memo<
<>
<Button
onPress={() => {
console.log("Opening Sort sheet", screen, dataType);
presentSheet({
component: <Sort screen={screen} type={dataType} />
});
}}
tooltipText="Change sorting of items in list"
title={groupBy}
icon={
groupOptions.sortDirection === "asc"
@@ -156,11 +151,6 @@ export const SectionHeader = React.memo<
screen !== "Notes"
}
testID="icon-compact-mode"
tooltipText={
isCompactModeEnabled
? "Switch to normal mode"
: "Switch to compact mode"
}
color={colors.secondary.icon}
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
onPress={() => {

View File

@@ -46,6 +46,7 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
@@ -341,9 +342,10 @@ const NoteItem = ({
marginRight: 6
}}
>
Deleted on{" "}
{item && item.dateDeleted
? new Date(item.dateDeleted).toISOString().slice(0, 10)
? strings.deletedOn(
new Date(item.dateDeleted).toISOString().slice(0, 10)
)
: null}
</Paragraph>
@@ -354,8 +356,7 @@ const NoteItem = ({
marginRight: 6
}}
>
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
{strings.note()}
</Paragraph>
</>
)}

View File

@@ -30,6 +30,7 @@ import { Properties } from "../../properties";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
type NotebookItemProps = {
item: Notebook | BaseTrashItem<Notebook>;
@@ -112,10 +113,11 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{"Deleted on " +
{strings.deletedOn(
new Date((item as TrashItem).dateDeleted)
.toISOString()
.slice(0, 10)}
.slice(0, 10)
)}
</Paragraph>
<Paragraph
color={colors.primary.accent}
@@ -147,11 +149,7 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
{strings.notes(totalNotes)}
</Paragraph>
{item.pinned ? (
@@ -183,11 +181,7 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
{strings.notes(totalNotes)}
</Paragraph>
</>
) : null}

View File

@@ -28,6 +28,7 @@ import { useSelectionStore } from "../../../stores/use-selection-store";
import { useTrashStore } from "../../../stores/use-trash-store";
import { presentDialog } from "../../dialog/functions";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import { strings } from "@notesnook/intl";
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
const isTrash = item.type === "trash";
@@ -36,16 +37,15 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
if (isTrash) {
presentDialog({
title: `Restore notebook`,
paragraph: `Restore or delete notebook forever`,
positiveText: "Restore",
negativeText: "Delete",
title: strings.restoreNotebook(),
positiveText: strings.restore(),
negativeText: strings.delete(),
positivePress: async () => {
await db.trash.restore(item.id);
if ((await db.trash.restore(item.id)) === false) return;
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: "Restore successful",
heading: strings.notebookRestored(),
type: "success"
});
},
@@ -54,7 +54,7 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: "Permanently deleted items",
heading: strings.permanentlyDeletedNotebook(),
type: "success",
context: "local"
});

View File

@@ -30,6 +30,7 @@ import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import { strings } from "@notesnook/intl";
const ReminderItem = React.memo(
({
@@ -108,7 +109,7 @@ const ReminderItem = React.memo(
color={colors.secondary.paragraph}
style={{ marginLeft: 5 }}
>
Disabled
{strings.disabled()}
</Paragraph>
</View>
) : null}
@@ -137,9 +138,7 @@ const ReminderItem = React.memo(
color={colors.secondary.paragraph}
style={{ marginLeft: 5 }}
>
{item.recurringMode.slice(0, 1).toUpperCase() +
item.recurringMode.replace("y", "i").slice(1) +
"ly"}
{strings.reminderRecurringMode[item.recurringMode]()}
</Paragraph>
</View>
) : null}

View File

@@ -29,6 +29,7 @@ import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import { strings } from "@notesnook/intl";
const TagItem = React.memo(
({
@@ -77,11 +78,7 @@ const TagItem = React.memo(
marginTop: 5
}}
>
{totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: null}
{strings.notes(totalNotes)}
</Paragraph>
</View>

View File

@@ -87,8 +87,8 @@ export const Empty = React.memo(function Empty({
color={color}
tip={
screen !== "Search"
? tip || ({ text: placeholder?.paragraph } as TTip)
: ({ text: placeholder?.paragraph } as TTip)
? tip || ({ text: () => placeholder?.paragraph } as TTip)
: ({ text: () => placeholder?.paragraph } as TTip)
}
style={{
backgroundColor: "transparent",

View File

@@ -34,12 +34,13 @@ import {
Reminder,
Tag,
TrashItem,
VirtualizedGrouping
VirtualizedGrouping,
getSortValue
} from "@notesnook/core";
import { getSortValue } from "@notesnook/core";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
import { eSendEvent } from "../../services/event-manager";
import { RouteName } from "../../stores/use-navigation-store";
import { eOpenJumpToDialog } from "../../utils/events";
@@ -48,7 +49,6 @@ import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;

View File

@@ -1,88 +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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { ProgressBarComponent } from "../ui/svg/lazy";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export const Loading = (props: {
title?: string;
description?: string;
icon?: string;
}) => {
const { colors } = useThemeColors();
return (
<View
style={{
width: "100%",
height: "100%",
backgroundColor: colors.primary.background,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 16
}}
>
{props.icon ? (
<Icon name={props.icon} size={80} color={colors.primary.accent} />
) : null}
{props.title ? (
<Heading
style={{
textAlign: "center"
}}
>
{props.title}
</Heading>
) : null}
{props.description ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{props.description}
</Paragraph>
) : null}
<View
style={{
flexDirection: "row",
width: 100,
marginTop: 15
}}
>
<ProgressBarComponent
height={5}
width={100}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
</View>
);
};

View File

@@ -48,6 +48,7 @@ import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
const MergeConflicts = () => {
const { colors } = useThemeColors();
@@ -171,7 +172,11 @@ const MergeConflicts = () => {
fontWeight: "bold"
}}
>
{isCurrent ? "(This Device)" : "(Incoming)"}
(
{isCurrent
? strings.mergeConflict.thisDevice()
: strings.mergeConflict.otherDevice()}
)
</Text>
{"\n"}
{getFormattedDate(contentToKeep?.dateEdited)}
@@ -191,7 +196,7 @@ const MergeConflicts = () => {
setCopy(contentToKeep);
setDialogVisible(true);
}}
title="Save a copy"
title={strings.saveACopy()}
type="secondary"
height={30}
style={{
@@ -204,7 +209,7 @@ const MergeConflicts = () => {
<View style={{ width: 10 }} />
{isDiscarded ? (
<Button
title="Discard"
title={strings.discard()}
type="accent"
buttonType={{
color: colors.static.red,
@@ -234,7 +239,9 @@ const MergeConflicts = () => {
}}
type="accent"
fontSize={SIZE.xs}
title={keeping && !isDiscarded ? "Undo" : "Keep"}
title={
keeping && !isDiscarded ? strings.undo() : strings.keep()
}
onPress={() => {
setKeep(keeping && !isDiscarded ? null : contentToKeep);
}}
@@ -277,11 +284,7 @@ const MergeConflicts = () => {
{dialogVisible && (
<BaseDialog visible={true}>
<DialogContainer>
<DialogHeader
title="Apply Changes"
paragraph="Apply selected changes to note?"
padding={12}
/>
<DialogHeader title={strings.applyChanges()} padding={12} />
<Seperator />
<DialogButtons
positiveTitle="Apply"

View File

@@ -37,6 +37,7 @@ import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import NotePreview from "./preview";
import { strings } from "@notesnook/intl";
const HistoryItem = ({
index,
@@ -141,11 +142,7 @@ export default function NoteHistory({
return (
<View>
<SheetProvider context="note_history" />
<DialogHeader
title="Note history"
paragraph="Revert back to an older version of this note"
padding={12}
/>
<DialogHeader title={strings.noteHistory()} padding={12} />
<Seperator />
@@ -180,7 +177,7 @@ export default function NoteHistory({
<>
<Icon name="history" size={50} color={colors.primary.icon} />
<Paragraph color={colors.secondary.paragraph}>
No note history found on this device.
{strings.noteHistoryPlaceholder()}
</Paragraph>
</>
)}
@@ -196,7 +193,7 @@ export default function NoteHistory({
alignSelf: "center"
}}
>
Note version history is local only.{" "}
{strings.noteHistoryNotice[0]()}{" "}
<Text
onPress={() => {
openLinkInBrowser(
@@ -208,7 +205,7 @@ export default function NoteHistory({
textDecorationLine: "underline"
}}
>
Learn how this works.
{strings.noteHistoryNotice[1]()}
</Text>
</Paragraph>
</View>

View File

@@ -35,6 +35,7 @@ import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
/**
*
@@ -47,11 +48,11 @@ export default function NotePreview({ session, content, note }) {
async function restore() {
if (note && note.type === "trash") {
await db.trash.restore(note.id);
if ((await db.trash.restore(note.id)) === false) return;
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
heading: "Restore successful",
heading: strings.noteRestored(),
type: "success"
});
eSendEvent(eCloseSheet);
@@ -72,7 +73,7 @@ export default function NotePreview({ session, content, note }) {
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: "Note restored successfully",
heading: strings.noteRestoredFromHistory(),
type: "success"
});
}
@@ -83,17 +84,17 @@ export default function NotePreview({ session, content, note }) {
const deleteNote = async () => {
presentDialog({
title: `Delete note permanently`,
paragraph: `Are you sure you want to delete this note from trash permanentaly`,
positiveText: "Delete",
negativeText: "Cancel",
title: strings.deleteNote(),
paragraph: strings.deleteNoteConfirmation(),
positiveText: strings.delete(),
negativeText: strings.cancel(),
context: "local",
positivePress: async () => {
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
heading: "Permanently deleted items",
heading: strings.noteDeleted(),
type: "success",
context: "local"
});
@@ -150,7 +151,7 @@ export default function NotePreview({ session, content, note }) {
}}
>
<Paragraph color={colors.secondary.paragraph}>
Preview not available, content is encrypted.
{strings.encryptedNoteHistoryNotice()}
</Paragraph>
</View>
)}
@@ -160,10 +161,15 @@ export default function NotePreview({ session, content, note }) {
paddingHorizontal: 12
}}
>
<Button onPress={restore} title="Restore" type="accent" width="100%" />
<Button
onPress={restore}
title={strings.restore()}
type="accent"
width="100%"
/>
<Button
onPress={deleteNote}
title="Delete permanently"
title={strings.deletePermanently()}
type="error"
width="100%"
style={{

View File

@@ -0,0 +1,292 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useState } from "react";
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
import { LAUNCH_ROCKET } from "../../assets/images/assets";
import { db } from "../../common/database";
import { usePricing } from "../../hooks/use-pricing";
import { DDS } from "../../services/device-detection";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../stores/use-user-store";
import { getElevationStyle } from "../../utils/elevation";
import {
eClosePremiumDialog,
eCloseSheet,
eOpenLoginDialog
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { AuthMode } from "../auth";
import SheetProvider from "../sheet-provider";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import { features } from "./features";
import { Group } from "./group";
import { PricingPlans } from "./pricing-plans";
export const Component = ({ close, promo }) => {
const { colors } = useThemeColors();
const user = useUserStore((state) => state.user);
const userCanRequestTrial =
user && (!user.subscription || !user.subscription.expiry) ? true : false;
const [floatingButton, setFloatingButton] = useState(false);
const pricing = usePricing("monthly");
const onPress = async () => {
if (user) {
presentSheet({
context: "pricing_plans",
component: (
<PricingPlans showTrialOption={false} marginTop={1} promo={promo} />
)
});
} else {
close();
setTimeout(() => {
eSendEvent(eOpenLoginDialog, AuthMode.trialSignup);
}, 400);
}
};
const onScroll = (event) => {
let contentSize = event.nativeEvent.contentSize.height;
contentSize = contentSize - event.nativeEvent.layoutMeasurement.height;
let yOffset = event.nativeEvent.contentOffset.y;
if (yOffset > 600 && yOffset < contentSize - 400) {
setFloatingButton(true);
} else {
setFloatingButton(false);
}
};
return (
<View
style={{
width: "100%",
backgroundColor: colors.primary.background,
justifyContent: "space-between",
borderRadius: 10,
maxHeight: "100%"
}}
>
<SheetProvider context="pricing_plans" />
<IconButton
onPress={() => {
close();
}}
style={{
position: "absolute",
right: DDS.isTab ? 30 : 15,
top: Platform.OS === "ios" ? 0 : 30,
zIndex: 10,
width: 50,
height: 50
}}
color={colors.primary.paragraph}
name="close"
/>
<ScrollView
style={{
paddingHorizontal: DDS.isTab ? DDS.width / 5 : 0
}}
scrollEventThrottle={0}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
onScroll={onScroll}
>
<View
key="top-banner"
style={{
width: "100%",
alignItems: "center",
height: 400,
justifyContent: "center"
}}
>
<SvgView
width={350}
height={350}
src={LAUNCH_ROCKET(colors.primary.accent)}
/>
</View>
<Heading
key="heading"
size={SIZE.lg}
style={{
alignSelf: "center",
paddingTop: 20
}}
>
Notesnook{" "}
<Heading size={SIZE.lg} color={colors.primary.accent}>
Pro
</Heading>
</Heading>
{!pricing ? (
<ActivityIndicator
style={{
marginBottom: 20
}}
size={SIZE.md}
color={colors.primary.accent}
/>
) : (
<Paragraph
style={{
alignSelf: "center",
marginBottom: 20
}}
size={SIZE.md}
>
(
{Platform.OS === "android"
? pricing.product?.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0].formattedPrice
: pricing.product?.localizedPrice}{" "}
/ mo)
</Paragraph>
)}
<Paragraph
key="description"
size={SIZE.md}
style={{
paddingHorizontal: 12,
textAlign: "center",
alignSelf: "center",
paddingBottom: 20,
width: "90%"
}}
>
Ready to take the next step on your private note taking journey?
</Paragraph>
{userCanRequestTrial ? (
<Button
key="calltoaction"
onPress={async () => {
try {
await db.user.activateTrial();
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseSheet);
await sleep(300);
Walkthrough.present("trialstarted", false, true);
} catch (e) {
console.error(e);
}
}}
title="Try free for 14 days"
type="accent"
width={250}
style={{
paddingHorizontal: 12,
marginBottom: 15,
borderRadius: 100
}}
/>
) : null}
<Button
key="calltoaction"
onPress={onPress}
title={
promo ? promo.text : user ? "See all plans" : "Sign up for free"
}
type={userCanRequestTrial ? "secondaryAccented" : "accent"}
width={250}
style={{
paddingHorizontal: 12,
marginBottom: 15,
borderRadius: 100
}}
/>
{!user || userCanRequestTrial ? (
<Paragraph
color={colors.secondary.paragraph}
size={SIZE.xs}
style={{
alignSelf: "center",
textAlign: "center",
marginTop: 10,
maxWidth: "80%"
}}
>
{user
? 'On clicking "Try free for 14 days", your free trial will be activated.'
: "After sign up you will be asked to activate your free trial."}{" "}
<Paragraph size={SIZE.xs} style={{ fontWeight: "bold" }}>
No credit card is required.
</Paragraph>
</Paragraph>
) : null}
<Seperator key="seperator_1" />
{features.map((item, index) => (
<Group key={item.title} item={item} index={index} />
))}
<View
key="plans"
style={{
paddingHorizontal: 12
}}
>
<PricingPlans showTrialOption={false} promo={promo} />
</View>
</ScrollView>
{floatingButton ? (
<Button
onPress={onPress}
title={
promo ? promo.text : user ? "See all plans" : "Sign up for free"
}
type="accent"
style={{
paddingHorizontal: 24,
position: "absolute",
borderRadius: 100,
bottom: 30,
...getElevationStyle(10)
}}
/>
) : null}
<Toast context="local" />
<View
style={{
paddingBottom: 10
}}
/>
</View>
);
};

File diff suppressed because one or more lines are too long

View File

@@ -1,60 +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/>.
*/
export const FeaturesList = {
// Monthly: ["$0/mo", "$1.99/mo", "$5.99/mo", "$9.99/mo"],
// Yearly: ["$0/yr", "$14.99/yr", "$59.99/yr", "$109.99/yr"],
// "5 year": [null, null, "$249.99/5yr", "$449.99/5yr"],
Plans: ["Free", "Essential", "Pro", "Believer"],
Devices: [2, 5, "Unlimited", "Unlimited"],
Storage: ["100MB/mo", "1GB/mo", "10GB/mo", "25GB/mo"],
"File size": ["10MB", "100MB", "1GB", "5GB"],
"File compression": [true, true, "Optional", "Optional"],
"Check list": [true, true, true, true],
"Bullet list": [true, true, true, true],
Quote: [true, true, true, true],
"Note link": [true, true, true, true],
"Task list": [false, true, true, true],
"Outline list": [false, true, true, true],
Callouts: [false, true, true, true],
Colors: [7, 20, "∞", "∞"],
Tags: [50, 500, "∞", "∞"],
Notebooks: [100, 1000, "∞", "∞"],
"Active Reminders": [10, 50, "∞", "∞"],
Shortcuts: [10, "∞", "∞", "∞"],
"Note history": ["Local", "Local", "Synced", "Synced"],
"Security key app lock": [false, false, true, true],
"Pin note in notification": [false, false, true, true],
"Create note from notification drawer": [false, false, true, true],
"Default startup screen": [false, false, true, true],
"Markdown shortcuts": [false, true, true, true],
"Custom toolbar preset": [false, false, true, true],
"Customizable navigation menu": [false, true, true, true],
"Files in monograph": [
"Images",
"Images & files",
"Images & files",
"Images & files"
],
"Note history retention": ["1 month", "∞", "∞", "∞"],
"App lock": ["immediate", "immediate", "any", "any"],
"Password on monograph": ["Default", "Custom", "Custom", "Custom"],
"Voice memos": ["5 minute", "1 hour", "∞", true],
"Notebook publishing": [1, 2, 5, true],
Vaults: [1, 3, "∞", "∞"]
};

View File

@@ -24,8 +24,7 @@ import {
} from "../../services/event-manager";
import { eClosePremiumDialog, eOpenPremiumDialog } from "../../utils/events";
import BaseDialog from "../dialog/base-dialog";
import { PaywallComponent } from "./component";
import { IconButton } from "../ui/icon-button";
import { Component } from "./component";
class PremiumDialog extends React.Component {
constructor(props) {
@@ -75,7 +74,7 @@ class PremiumDialog extends React.Component {
background={this.props.colors.primary.background}
onRequestClose={this.onClose}
>
<PaywallComponent
<Component
getRef={() => this.actionSheetRef}
promo={this.state.promo}
close={this.close}

View File

@@ -48,7 +48,7 @@ import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import { PricingItem } from "./pricing-item";
import { useSettingStore } from "../../stores/use-setting-store";
import usePricingPlans from "../../hooks/use-pricing-plans";
import { strings } from "@notesnook/intl";
const UUID_PREFIX = "0bdaea";
const UUID_VERSION = "4";
@@ -93,21 +93,173 @@ export const PricingPlans = ({
compact?: boolean;
}) => {
const { colors } = useThemeColors();
const {
buySubscription,
buying,
getStandardPrice,
product,
setBuying,
loading,
user,
setProduct,
monthlyPlan,
yearlyPlan,
getPromo
} = usePricingPlans({
promoOffer: promo
});
const user = useUserStore((state) => state.user);
const [product, setProduct] = useState<{
type: string;
offerType: "monthly" | "yearly";
data: RNIap.Subscription;
cycleText: string;
info: string;
}>();
const [buying, setBuying] = useState(false);
const [loading, setLoading] = useState(false);
const userCanRequestTrial =
user && (!user.subscription || !user.subscription.expiry) ? true : false;
const [upgrade, setUpgrade] = useState(!userCanRequestTrial);
const yearlyPlan = usePricing("yearly");
const monthlyPlan = usePricing("monthly");
const getSkus = useCallback(async () => {
try {
setLoading(true);
if (promo?.promoCode) {
getPromo(promo?.promoCode);
}
setLoading(false);
} catch (e) {
setLoading(false);
console.log("error getting sku", e);
}
}, [promo?.promoCode]);
const getPromo = async (code: string) => {
try {
let skuId: string;
if (code.startsWith("com.streetwriters.notesnook")) {
skuId = code;
} else {
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
}
const products = await PremiumService.getProducts();
const product = products.find((p) => p.productId === skuId);
if (!product) return false;
const isMonthly = product.productId.indexOf(".mo") > -1;
const cycleText = isMonthly
? promoCyclesMonthly[
(Platform.OS === "android"
? (product as RNIap.SubscriptionAndroid)
.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0].billingCycleCount
: parseInt(
(product as RNIap.SubscriptionIOS)
.introductoryPriceNumberOfPeriodsIOS as string
)) as keyof typeof promoCyclesMonthly
]
: promoCyclesYearly[
(Platform.OS === "android"
? (product as RNIap.SubscriptionAndroid)
.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0].billingCycleCount
: parseInt(
(product as RNIap.SubscriptionIOS)
.introductoryPriceNumberOfPeriodsIOS as string
)) as keyof typeof promoCyclesYearly
];
setProduct({
type: "promo",
offerType: isMonthly ? "monthly" : "yearly",
data: product,
cycleText: cycleText,
info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
});
return true;
} catch (e) {
console.log("PROMOCODE ERROR:", code, e);
return false;
}
};
useEffect(() => {
getSkus();
}, [getSkus]);
const buySubscription = async (product: RNIap.Subscription) => {
if (buying || !product) return;
setBuying(true);
try {
if (!user) {
setBuying(false);
return;
}
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
const androidOfferToken =
Platform.OS === "android"
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
.offerToken
: null;
DatabaseLogger.info(
`Subscription Requested initiated for user ${toUUID(user.id)}`
);
await RNIap.requestSubscription({
sku: product?.productId,
obfuscatedAccountIdAndroid: user.id,
obfuscatedProfileIdAndroid: user.id,
appAccountToken: toUUID(user.id),
andDangerouslyFinishTransactionAutomaticallyIOS: false,
subscriptionOffers: androidOfferToken
? [
{
offerToken: androidOfferToken,
sku: product?.productId
}
]
: undefined
});
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
setBuying(false);
eSendEvent(eCloseSheet);
eSendEvent(eClosePremiumDialog);
await sleep(500);
presentSheet({
title: "Thank you for subscribing!",
paragraph:
"Your Notesnook Pro subscription will be activated soon. If your account is not upgraded to Notesnook Pro, your money will be refunded to you. In case of any issues, please reach out to us at support@streetwriters.co",
action: async () => {
eSendEvent(eCloseSheet);
},
icon: "check",
actionText: "Continue"
});
} catch (e) {
setBuying(false);
console.log(e);
}
};
function getStandardPrice() {
if (!product) return;
const productType = product.offerType;
if (Platform.OS === "android") {
const pricingPhaseListItem = (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[1];
if (!pricingPhaseListItem) {
const product =
productType === "monthly"
? monthlyPlan?.product
: yearlyPlan?.product;
return (product as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[0]
?.formattedPrice;
}
return pricingPhaseListItem?.formattedPrice;
} else {
const productDefault =
productType === "monthly" ? monthlyPlan?.product : yearlyPlan?.product;
return (
(product.data as RNIap.SubscriptionIOS)?.localizedPrice ||
(productDefault as RNIap.SubscriptionIOS)?.localizedPrice
);
}
}
return loading ? (
<View
@@ -127,215 +279,354 @@ export const PricingPlans = ({
}}
>
{buying ? (
<BaseDialog visible statusBarTranslucent centered>
<BaseDialog statusBarTranslucent centered>
<ActivityIndicator size={50} color="white" />
</BaseDialog>
) : null}
{!user && !product ? (
{!upgrade ? (
<>
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
<>
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
<View
style={{
alignSelf: "center",
marginTop: marginTop || 20,
marginBottom: 20
<Paragraph
style={{
alignSelf: "center"
}}
size={SIZE.lg}
>
{(Platform.OS === "android"
? (monthlyPlan?.product as RNIap.SubscriptionAndroid | undefined)
?.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (monthlyPlan?.product as RNIap.SubscriptionIOS | undefined)
?.localizedPrice) ||
(PremiumService.getMontlySub() as any)?.localizedPrice}
/ mo
</Paragraph>
<Button
onPress={() => {
setUpgrade(true);
}}
title={"Upgrade now"}
type="accent"
width={250}
style={{
paddingHorizontal: 12,
marginBottom: 15,
marginTop: 15,
borderRadius: 100
}}
/>
<Button
onPress={async () => {
try {
await db.user?.activateTrial();
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseSheet);
await sleep(300);
Walkthrough.present("trialstarted", false, true);
} catch (e) {
console.error(e);
}
}}
title={"Try free for 14 days"}
type="secondaryAccented"
width={250}
style={{
paddingHorizontal: 12,
marginBottom: 15
}}
/>
</>
) : (
<>
{product?.type === "promo" ? (
<View
style={{
paddingVertical: 15,
alignItems: "center"
}}
>
{product?.offerType === "monthly" ? (
<PricingItem
product={{
type: "monthly",
data: monthlyPlan?.product,
info: "Pay once a month, cancel anytime."
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.accent}
>
Get {monthlyPlan?.info?.discount}% off in{" "}
{monthlyPlan?.info?.country}
</Heading>
</View>
strikethrough={true}
/>
) : (
<PricingItem
onPress={() => {
if (!monthlyPlan?.product) return;
buySubscription(monthlyPlan?.product);
}}
product={{
type: "yearly",
data: yearlyPlan?.product,
info: "Pay once a year, cancel anytime."
}}
strikethrough={true}
/>
)}
<Heading
style={{
paddingTop: 15,
fontSize: SIZE.lg
}}
>
Special offer for you
</Heading>
<View
style={{
paddingVertical: 20,
paddingBottom: 10
}}
>
<Heading
style={{
alignSelf: "center",
marginTop: marginTop || 20,
marginBottom: 20
}}
>
Choose a plan
</Heading>
)}
</>
) : null}
<View
style={{
flexDirection: !compact ? "column" : "row",
flexWrap: "wrap",
justifyContent: "space-around"
}}
>
<PricingItem
onPress={() => {
if (!monthlyPlan?.product) return;
buySubscription(monthlyPlan?.product);
}}
compact={compact}
product={{
type: "monthly",
data: monthlyPlan?.product,
info: "Pay once a month, cancel anytime."
}}
/>
{!compact && (
<View
style={{
height: 1,
marginVertical: 5
}}
/>
)}
<PricingItem
onPress={() => {
if (!yearlyPlan?.product) return;
buySubscription(yearlyPlan?.product);
}}
compact={compact}
product={{
type: "yearly",
data: yearlyPlan?.product,
info: "Pay once a year, cancel anytime."
}}
/>
</View>
{Platform.OS !== "ios" ? (
<Button
height={35}
style={{
marginTop: 10
}}
onPress={() => {
presentDialog({
context: "local",
input: true,
inputPlaceholder: "Enter code",
positiveText: "Apply",
positivePress: async (value) => {
if (!value) return;
eSendEvent(eCloseSimpleDialog);
setBuying(true);
try {
if (!(await getPromo(value as string)))
throw new Error("Error applying promo code");
ToastManager.show({
heading: "Discount applied!",
type: "success",
context: "local"
});
setBuying(false);
} catch (e) {
setBuying(false);
ToastManager.show({
heading: "Promo code invalid or expired",
message: (e as Error).message,
type: "error",
context: "local"
});
}
},
title: "Have a promo code?",
paragraph: "Enter your promo code to get a special discount."
});
}}
title="I have a promo code"
/>
) : (
<View
style={{
height: 15
}}
/>
)}
</>
) : (
<View>
{!user ? (
<>
<Button
onPress={() => {
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseSheet);
setTimeout(() => {
eSendEvent(eOpenLoginDialog, 1);
}, 400);
}}
title={"Sign up for free"}
type="accent"
width={250}
style={{
paddingHorizontal: 12,
marginTop: product?.type === "promo" ? 0 : 30,
marginBottom: 10
}}
/>
{Platform.OS !== "ios" &&
promo &&
!promo.promoCode.startsWith("com.streetwriters.notesnook") ? (
<Paragraph
size={SIZE.md}
textBreakStrategy="balanced"
style={{
alignSelf: "center",
justifyContent: "center",
textAlign: "center"
}}
size={SIZE.xxl}
>
Use promo code{" "}
<Text
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (product.data as RNIap.SubscriptionIOS)
?.introductoryPrice ||
(product.data as RNIap.SubscriptionIOS)
?.localizedPrice}{" "}
{product?.cycleText
? `for ${product.cycleText}`
: product?.offerType}
</Heading>
{product?.cycleText ? (
<Paragraph
style={{
fontFamily: "OpenSans-SemiBold"
color: colors.secondary.paragraph,
alignSelf: "center",
textAlign: "center"
}}
size={SIZE.md}
>
{promo.promoCode}
</Text>{" "}
at checkout
</Paragraph>
then {getStandardPrice()} {product?.offerType}.
</Paragraph>
) : null}
</View>
</View>
) : null}
{user && !product ? (
<>
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
<>
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
<View
style={{
alignSelf: "center",
marginTop: marginTop || 20,
marginBottom: 20
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.accent}
>
Get {monthlyPlan?.info?.discount}% off in{" "}
{monthlyPlan?.info?.country}
</Heading>
</View>
) : (
<Heading
style={{
alignSelf: "center",
marginTop: marginTop || 20,
marginBottom: 20
}}
>
Choose a plan
</Heading>
)}
</>
) : null}
<View
style={{
flexDirection: !compact ? "column" : "row",
flexWrap: "wrap",
justifyContent: "space-around"
}}
>
<PricingItem
onPress={() => {
if (!monthlyPlan?.product) return;
buySubscription(monthlyPlan?.product);
}}
compact={compact}
product={{
type: "monthly",
data: monthlyPlan?.product,
info: "Pay once a month, cancel anytime."
}}
/>
{!compact && (
<View
style={{
height: 1,
marginVertical: 5
}}
/>
)}
<PricingItem
onPress={() => {
if (!yearlyPlan?.product) return;
buySubscription(yearlyPlan?.product);
}}
compact={compact}
product={{
type: "yearly",
data: yearlyPlan?.product,
info: "Pay once a year, cancel anytime."
}}
/>
</View>
{Platform.OS !== "ios" ? (
<Button
height={35}
style={{
marginTop: 10
}}
onPress={() => {
presentDialog({
context: "local",
input: true,
inputPlaceholder: "Enter code",
positiveText: "Apply",
positivePress: async (value) => {
if (!value) return;
eSendEvent(eCloseSimpleDialog);
setBuying(true);
try {
if (!(await getPromo(value as string)))
throw new Error(strings.errorApplyingPromoCode());
ToastManager.show({
heading: "Discount applied!",
type: "success",
context: "local"
});
setBuying(false);
} catch (e) {
setBuying(false);
ToastManager.show({
heading: "Promo code invalid or expired",
message: (e as Error).message,
type: "error",
context: "local"
});
}
},
title: "Have a promo code?",
paragraph:
"Enter your promo code to get a special discount."
});
}}
title="I have a promo code"
/>
) : (
<View
style={{
height: 15
}}
/>
)}
</>
) : (
<>
<Button
onPress={() => {
if (!product?.data) return;
buySubscription(product.data);
}}
height={40}
width="50%"
type="accent"
title="Subscribe now"
/>
<View>
{!user ? (
<>
<Button
onPress={() => {
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseSheet);
setTimeout(() => {
eSendEvent(eOpenLoginDialog, 1);
}, 400);
}}
title={"Sign up for free"}
type="accent"
width={250}
style={{
paddingHorizontal: 12,
marginTop: product?.type === "promo" ? 0 : 30,
marginBottom: 10
}}
/>
{Platform.OS !== "ios" &&
promo &&
!promo.promoCode.startsWith("com.streetwriters.notesnook") ? (
<Paragraph
size={SIZE.md}
textBreakStrategy="balanced"
style={{
alignSelf: "center",
justifyContent: "center",
textAlign: "center"
}}
>
Use promo code{" "}
<Text
style={{
fontFamily: "OpenSans-SemiBold"
}}
>
{promo.promoCode}
</Text>{" "}
at checkout
</Paragraph>
) : null}
</>
) : (
<>
<Button
onPress={() => {
if (!product?.data) return;
buySubscription(product.data);
}}
height={40}
width="50%"
type="accent"
title="Subscribe now"
/>
<Button
onPress={() => {
setProduct(undefined);
}}
style={{
marginTop: 5
}}
height={30}
fontSize={13}
type="errorShade"
title="Cancel promo code"
/>
</>
<Button
onPress={() => {
setProduct(undefined);
}}
style={{
marginTop: 5
}}
height={30}
fontSize={13}
type="errorShade"
title="Cancel promo code"
/>
</>
)}
</View>
)}
</View>
</>
)}
{!user ? (
{!user || !upgrade ? (
<Paragraph
color={colors.secondary.paragraph}
size={SIZE.xs}
@@ -355,7 +646,7 @@ export const PricingPlans = ({
</Paragraph>
) : null}
{user ? (
{user && upgrade ? (
<>
{Platform.OS === "ios" ? (
<Paragraph

View File

@@ -48,7 +48,7 @@ export const ProTag = ({ width, size, background }) => {
marginRight: 3
}}
size={size}
color={colors.static.orange}
color={colors.primary.accent}
name="crown"
/>
<Paragraph size={size - 1.5} color={colors.primary.accent}>

View File

@@ -36,6 +36,7 @@ import ColorPicker from "../dialogs/color-picker";
import { Button } from "../ui/button";
import NativeTooltip from "../../utils/tooltip";
import { Pressable } from "../ui/pressable";
import { strings } from "@notesnook/intl";
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
const { colors } = useThemeColors();
@@ -145,7 +146,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
buttonType={{
text: colors.primary.accent
}}
title="Add color"
title={strings.addColor()}
type="secondary"
icon="plus"
iconPosition="right"

View File

@@ -23,26 +23,10 @@ import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
export const DateMeta = ({ item }) => {
const { colors } = useThemeColors();
const getNameFromKey = (key) => {
switch (key) {
case "dateCreated":
return "Created at:";
case "dateEdited":
return "Last edited at:";
case "dateModified":
return "Last modified at:";
case "dateDeleted":
return "Deleted at:";
case "dateUploaded":
return "Uploaded at:";
default:
return key;
}
};
function getDateMeta() {
let keys = Object.keys(item);
if (keys.includes("dateEdited"))
@@ -64,7 +48,7 @@ export const DateMeta = ({ item }) => {
}}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getNameFromKey(key)}
{strings.dateDescFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getFormattedDate(item[key], "date-time")}

View File

@@ -39,6 +39,7 @@ import Notebooks from "./notebooks";
import { Synced } from "./synced";
import { TagStrip, Tags } from "./tags";
import { tabBarRef } from "../../utils/global-refs";
import { strings } from "@notesnook/intl";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
@@ -61,7 +62,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
Start writing to save your note.
{strings.noNotePropertiesNotice()}
</Paragraph>
);
}

View File

@@ -137,14 +137,13 @@ export const Items = ({ item, buttons, close }) => {
style={{
alignSelf: "flex-start",
paddingHorizontal: 0,
width: topBarItemWidth
flex: 1
}}
>
<View
onPress={item.func}
style={{
height: topBarItemWidth,
width: topBarItemWidth,
justifyContent: "center",
alignItems: "center",
textAlign: "center",
@@ -169,7 +168,7 @@ export const Items = ({ item, buttons, close }) => {
<Paragraph
textBreakStrategy="simple"
size={SIZE.xxs + 1}
size={SIZE.xxs}
style={{ textAlign: "center" }}
>
{item.title}
@@ -242,7 +241,7 @@ export const Items = ({ item, buttons, close }) => {
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
paddingRight: 25,
gap: 5
gap: 15
}}
>
{topBarItems.map(renderTopBarItem)}

View File

@@ -29,6 +29,7 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { Pressable } from "../ui/pressable";
import { strings } from "@notesnook/intl";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
@@ -102,7 +103,7 @@ export default function Notebooks({ note, close, full }) {
{noteNotebooks.length > 1 && !full ? (
<Button
title={`View all linked notebooks`}
title={strings.viewAllLinkedNotebooks()}
fontSize={SIZE.xs}
style={{
alignSelf: "flex-end",

View File

@@ -28,6 +28,7 @@ import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export const Synced = ({ item, close }) => {
const { colors } = useThemeColors();
const user = useUserStore((state) => state.user);
@@ -71,7 +72,7 @@ export const Synced = ({ item, close }) => {
flexWrap: "wrap"
}}
>
Encrypted and synced
{strings.noteSyncedNoticeHeading()}
</Heading>
{shouldShrink ? null : (
<Paragraph
@@ -81,7 +82,7 @@ export const Synced = ({ item, close }) => {
size={SIZE.xs}
color={colors.primary.paragraph}
>
No one can view this {item.itemType || item.type} except you.
{strings.noteSyncedNoticeDesc(item.itemType || item.type)}
</Paragraph>
)}
</View>
@@ -99,7 +100,7 @@ export const Synced = ({ item, close }) => {
console.error(e);
}
}}
title="Learn more"
title={strings.learnMore()}
fontSize={SIZE.xs}
height={30}
type="secondaryAccented"

View File

@@ -27,6 +27,7 @@ import { sleep } from "../../utils/time";
import ManageTagsSheet from "../sheets/manage-tags";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
import { strings } from "@notesnook/intl";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();
@@ -51,7 +52,7 @@ export const Tags = ({ item, close }) => {
buttonType={{
text: colors.primary.accent
}}
title="Add tag"
title={strings.addTag()}
type="secondary"
icon="plus"
iconPosition="right"

View File

@@ -46,6 +46,7 @@ import { MoveNotebookSheet } from "../sheets/move-notebook";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import { strings } from "@notesnook/intl";
export const SelectionHeader = React.memo(
({
@@ -89,26 +90,24 @@ export const SelectionHeader = React.memo(
const restoreItem = async () => {
if (!selectedItemsList.length) return;
await db.trash.restore(...selectedItemsList);
if ((await db.trash.restore(...selectedItemsList)) === false) return;
Navigation.queueRoutesForUpdate();
clearSelection();
ToastManager.show({
heading: "Restore successful",
heading: strings.restored(),
type: "success"
});
};
const deleteItem = async () => {
presentDialog({
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
paragraph: `Are you sure you want to delete ${
selectedItemsList.length > 1
? "these items permanently?"
: "this item permanently?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
title: strings.doActions.delete[type](selectedItemsList.length),
paragraph: strings.actionConfirmations.delete[type](
selectedItemsList.length
),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: async () => {
if (!selectedItemsList.length) return;
await db.trash.delete(...selectedItemsList);
@@ -236,10 +235,7 @@ export const SelectionHeader = React.memo(
>
{[
{
title:
selectedItemsList.length > 1
? "Move notebooks"
: "Move notebook",
title: strings.move(),
onPress: async () => {
const ids = selectedItemsList;
const notebooks = await db.notebooks.all.items(ids);
@@ -249,7 +245,7 @@ export const SelectionHeader = React.memo(
icon: "arrow-right-bold-box-outline"
},
{
title: "Manage tags",
title: strings.manageTags(),
onPress: async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
@@ -258,7 +254,7 @@ export const SelectionHeader = React.memo(
icon: "pound"
},
{
title: "Export",
title: strings.export(),
onPress: async () => {
await sleep(100);
ExportNotesSheet.present(selectedItemsList);
@@ -267,7 +263,7 @@ export const SelectionHeader = React.memo(
icon: "export"
},
{
title: "Link notebook",
title: strings.linkNotebook(),
onPress: async () => {
await sleep(100);
MoveNoteSheet.present();
@@ -276,7 +272,7 @@ export const SelectionHeader = React.memo(
icon: "plus"
},
{
title: "Unlink notebook",
title: strings.unlinkNotebook(),
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(id, ...selectedItemsList);
@@ -288,13 +284,13 @@ export const SelectionHeader = React.memo(
icon: "minus"
},
{
title: "Unfavorite",
title: strings.unfavorite(),
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: `Move to trash`,
title: strings.moveToTrash(),
onPress: async () => {
deleteItems(
undefined,
@@ -308,13 +304,13 @@ export const SelectionHeader = React.memo(
icon: "delete"
},
{
title: `Restore`,
title: strings.restore(),
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: `Delete`,
title: strings.delete(),
onPress: deleteItem,
visible: type === "trash",
icon: "delete"

View File

@@ -40,6 +40,7 @@ import { MoveNotes } from "../move-notes/movenote";
import { eOnNotebookUpdated } from "../../../utils/events";
import { getParentNotebookId } from "../../../utils/notebooks";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { strings } from "@notesnook/intl";
export const AddNotebookSheet = ({
notebook,
@@ -62,25 +63,25 @@ export const AddNotebookSheet = ({
const onSaveChanges = async () => {
if (loading) return;
setLoading(true);
if (!title.current || title?.current.trim().length === 0) {
ToastManager.show({
heading: "Notebook title is required",
heading: strings.enterNotebookTitle(),
type: "error",
context: "local"
});
setLoading(false);
return;
}
setLoading(true);
const id = await db.notebooks.add({
title: title.current,
description: description.current,
id: notebook?.id
});
if (parentNotebook) {
if (parentNotebook && id) {
await db.relations.add(parentNotebook, {
type: "notebook",
id: id
@@ -93,7 +94,8 @@ export const AddNotebookSheet = ({
useNotebookStore.getState().refresh();
const parent =
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
parentNotebook?.id ||
(await getParentNotebookId(notebook?.id || (id as string)));
eSendEvent(eOnNotebookUpdated, parent);
if (notebook) {
@@ -103,7 +105,7 @@ export const AddNotebookSheet = ({
});
}
if (!notebook && showMoveNotesOnComplete) {
if (!notebook && showMoveNotesOnComplete && id) {
MoveNotes.present(await db.notebooks.notebook(id));
} else {
close?.(true);
@@ -119,7 +121,7 @@ export const AddNotebookSheet = ({
}}
>
<Heading size={SIZE.lg}>
{notebook ? "Edit Notebook" : "New Notebook"}
{notebook ? strings.editNotebook() : strings.newNotebook()}
</Heading>
<Seperator />
@@ -135,7 +137,7 @@ export const AddNotebookSheet = ({
titleInput?.current?.focus();
});
}}
placeholder="Enter notebook title"
placeholder={strings.enterNotebookTitle()}
onSubmit={() => {
descriptionInput.current?.focus();
}}
@@ -150,14 +152,14 @@ export const AddNotebookSheet = ({
onChangeText={(value) => {
description.current = value;
}}
placeholder="Enter notebook description"
returnKeyLabel="Next"
placeholder={strings.enterNotebookDescription()}
returnKeyLabel={strings.next()}
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
<Button
title={notebook ? "Save" : "Add"}
title={notebook ? strings.save() : strings.add()}
type="accent"
height={45}
fontSize={SIZE.md}

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
@@ -49,12 +50,11 @@ import DialogHeader from "../../dialog/dialog-header";
import SheetProvider from "../../sheet-provider";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store";
import { AddNotebookSheet } from "../add-notebook";
import Input from "../../ui/input";
import { presentDialog } from "../../dialog/functions";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
@@ -200,11 +200,11 @@ const MoveNoteSheet = ({
minHeight: 10,
flexShrink: 1
}}
title="Select notebooks"
title={strings.selectNotebooks()}
paragraph={
!multiSelect
? "Long press to enable multi-select."
: "Select notebooks you want to add note(s) to."
? strings.enableMultiSelect()
: strings.selectNotebooksDesc()
}
/>
@@ -236,7 +236,7 @@ const MoveNoteSheet = ({
paddingHorizontal: 24,
alignSelf: "flex-start"
}}
title="Save"
title={strings.save()}
type={"accent"}
onPress={onSave}
/>
@@ -266,7 +266,7 @@ const MoveNoteSheet = ({
}}
>
<Input
placeholder="Search notebooks...."
placeholder={strings.searchNotebooks()}
button={{
icon: "plus",
onPress: () => {
@@ -319,7 +319,7 @@ const MoveNoteSheet = ({
/>
) : (
<Paragraph color={colors.primary.icon}>
No notebooks
{strings.emptyPlaceholders("notebook")}
</Paragraph>
)}
</View>

View File

@@ -1,371 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import {
Platform,
ScrollView,
Text,
TouchableOpacity,
View
} from "react-native";
import usePricingPlans from "../../../hooks/use-pricing-plans";
import { presentSheet } from "../../../services/event-manager";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { SIZE } from "../../../utils/size";
import Paragraph from "../../ui/typography/paragraph";
import Heading from "../../ui/typography/heading";
import { useThemeColors } from "@notesnook/theme";
import * as RNIap from "react-native-iap";
import { Button } from "../../ui/button";
import dayjs from "dayjs";
import { openLinkInBrowser } from "../../../utils/functions";
import { IconButton } from "../../ui/icon-button";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
export const BuyPlan = (props: {
planId: string;
productId: string;
canActivateTrial?: boolean;
goBack: () => void;
goNext: () => void;
}) => {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const pricingPlans = usePricingPlans({
planId: props.planId,
productId: props.productId,
onBuy: () => {
props.goNext();
}
});
const billingDuration = pricingPlans.getBillingDuration(
pricingPlans.selectedProduct as RNIap.Subscription,
0,
0,
true
);
const is5YearPlanSelected =
pricingPlans.selectedProduct?.productId.includes("5year");
return (
<ScrollView
contentContainerStyle={{
gap: 16,
paddingBottom: 80,
paddingTop: Platform.OS === "android" ? insets.top : 0
}}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
stickyHeaderIndices={[0]}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
backgroundColor: colors.primary.background
}}
>
<IconButton
name="chevron-left"
onPress={() => {
props.goBack();
}}
style={{
alignSelf: "flex-start"
}}
/>
</View>
<View
style={{
paddingHorizontal: 16,
gap: 16
}}
>
<Heading
style={{
alignSelf: "center",
marginBottom: 10
}}
>
{props.canActivateTrial
? `Try ${pricingPlans.currentPlan?.name} plan for free`
: `${pricingPlans.currentPlan?.name} plan`}{" "}
</Heading>
{props.canActivateTrial ? (
<View
style={{
gap: 10,
marginBottom: 10
}}
>
{(is5YearPlanSelected
? [
"One time purchase, no auto-renewal",
"Pay once and use for 5 years"
]
: [
`Free ${billingDuration.duration} day trial, cancel any time`,
"Google will remind you before your trial ends"
]
).map((item) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 10
}}
key={item}
>
<Icon
color={colors.primary.accent}
size={SIZE.lg}
name="check"
/>
<Paragraph>{item}</Paragraph>
</View>
))}
</View>
) : null}
{[
`notesnook.${props.planId}.yearly`,
`notesnook.${props.planId}.monthly`,
...(props.planId === "essential"
? []
: [`notesnook.${props.planId}.5year`])
].map((item) => (
<ProductItem
key={item}
pricingPlans={pricingPlans}
productId={item}
/>
))}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
marginTop: 20
}}
>
<Heading color={colors.primary.paragraph} size={SIZE.sm}>
Due today{" "}
{pricingPlans.userCanRequestTrial ? (
<Text
style={{
color: colors.primary.accent
}}
>
({billingDuration.duration} days free)
</Text>
) : null}
</Heading>
<Paragraph color={colors.primary.paragraph}>
{pricingPlans.userCanRequestTrial
? "0.00"
: pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Paragraph>
</View>
{pricingPlans.userCanRequestTrial ? (
<View
style={{
flexDirection: "row",
justifyContent: "space-between"
}}
>
<Paragraph color={colors.secondary.paragraph}>
Due{" "}
{dayjs().add(billingDuration.duration, "day").format("DD MMMM")}
</Paragraph>
<Paragraph color={colors.secondary.paragraph}>
{pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Paragraph>
</View>
) : null}
<Button
width="100%"
type="accent"
loading={pricingPlans.loading}
title={
pricingPlans.selectedProduct?.productId.includes("5year")
? "Purchase"
: pricingPlans?.userCanRequestTrial
? "Subscribe and start free trial"
: "Subscribe"
}
onPress={() => {
const offerToken = pricingPlans.getOfferTokenAndroid(
pricingPlans.selectedProduct as RNIap.SubscriptionAndroid,
0
);
pricingPlans.subscribe(
pricingPlans.selectedProduct as RNIap.Subscription,
offerToken
);
}}
/>
<View>
<Heading
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={SIZE.xs}
>
{is5YearPlanSelected
? `This is a one time purchase, no subscription.`
: `Cancel anytime, subscription auto-renews.`}
</Heading>
<Heading
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={SIZE.xs}
>
By joining you agree to our{" "}
<Text
style={{
textDecorationLine: "underline"
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy");
}}
>
privacy policy
</Text>{" "}
and{" "}
<Text
style={{
textDecorationLine: "underline"
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos");
}}
>
terms of use
</Text>
.
</Heading>
</View>
</View>
</ScrollView>
);
};
const ProductItem = (props: {
pricingPlans: ReturnType<typeof usePricingPlans>;
productId: string;
}) => {
const { colors } = useThemeColors();
const product =
props.pricingPlans?.currentPlan?.subscriptions?.[props.productId] ||
props.pricingPlans?.currentPlan?.products?.[props.productId];
const isAnnual = product?.productId.includes("yearly");
const isSelected =
product?.productId === props.pricingPlans.selectedProduct?.productId;
return (
<TouchableOpacity
style={{
flexDirection: "row",
gap: 10
}}
activeOpacity={0.9}
onPress={() => {
props.pricingPlans.selectProduct(product?.productId);
}}
>
<Icon
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
color={isSelected ? colors.primary.accent : colors.secondary.icon}
size={SIZE.lg}
/>
<View
style={{
gap: 10
}}
>
<View
style={{
flexDirection: "row",
gap: 10
}}
>
<Heading size={SIZE.md}>
{isAnnual
? "Yearly"
: product?.productId.includes("5year")
? "5 year plan (One time purchase)"
: "Monthly"}
</Heading>
{isAnnual ? (
<View
style={{
backgroundColor: colors.static.red,
borderRadius: 100,
paddingHorizontal: 6,
alignItems: "center",
justifyContent: "center"
}}
>
<Heading color={colors.static.white} size={SIZE.xs}>
Best value -{" "}
{props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
)}
% Off
</Heading>
</View>
) : null}
</View>
<Paragraph>
{product?.productId.includes("5year")
? (product as RNIap.Product).localizedPrice
: props.pricingPlans.getStandardPrice(
product as RNIap.Subscription
)}{" "}
{isAnnual || product?.productId.includes("5year")
? `(${props.pricingPlans.getPrice(
product as RNIap.Subscription,
props.pricingPlans.hasTrialOffer() ? 1 : 0,
isAnnual
)}/month)`
: null}
</Paragraph>
</View>
</TouchableOpacity>
);
};

View File

@@ -30,6 +30,7 @@ import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { eUserLoggedIn } from "../../../utils/events";
import { strings } from "@notesnook/intl";
type ChangeEmailProps = {
actionSheetRef: RefObject<ActionSheetRef>;
@@ -68,7 +69,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
const verified = await db.user?.verifyPassword(
emailChangeData.current.password
);
if (!verified) throw new Error("Password is incorrect");
if (!verified) throw new Error(strings.passwordIncorrect());
await db.user?.sendVerificationEmail(emailChangeData.current.email);
setStep(EmailChangeSteps.changeEmail);
setLoading(false);
@@ -88,8 +89,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
eSendEvent(eUserLoggedIn);
close?.();
ToastManager.show({
heading: `Email changed`,
message: `Your account email has been updated to ${emailChangeData.current.email}`,
heading: strings.emailUpdated(emailChangeData.current.email),
type: "success",
context: "global"
});
@@ -103,8 +103,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
return (
<View style={{ paddingHorizontal: 12 }}>
<DialogHeader
title="Change email"
paragraph="Your account email will be changed without affecting your subscription or any other settings."
title={strings.changeEmail()}
paragraph={strings.changeEmailDesc()}
/>
<View
style={{
@@ -115,7 +115,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
<>
<Input
fwdRef={emailInputRef}
placeholder="Enter your new email"
placeholder={strings.enterNewEmail()}
validationType="email"
onErrorCheck={(e) => setError(e)}
onChangeText={(email) => {
@@ -124,7 +124,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
/>
<Input
fwdRef={passInputRef}
placeholder="Enter your account password"
placeholder={strings.enterAccountPassword()}
secureTextEntry
onChangeText={(pass) => {
emailChangeData.current.password = pass;
@@ -135,7 +135,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
<>
<Input
fwdRef={codeInputRef}
placeholder="Enter verification code sent to your new email"
placeholder={strings.verifyNewEmail()}
onChangeText={(code) => {
emailChangeData.current.code = code;
}}
@@ -149,8 +149,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
loading
? undefined
: step === EmailChangeSteps.verify
? "Verify"
: "Change email"
? strings.verify()
: strings.changeEmail()
}
type="accent"
width={250}

View File

@@ -35,6 +35,7 @@ import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
type TabItem = {
id: number;
@@ -137,7 +138,9 @@ const TabItemComponent = (props: {
numberOfLines={1}
size={SIZE.md}
>
{props.tab.noteId ? item?.title || "Untitled note" : "New note"}
{props.tab.noteId
? item?.title || strings.untitledNote()
: strings.newNote()}
</Paragraph>
</View>
@@ -229,7 +232,7 @@ export default function EditorTabs({
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>Tabs</Heading>
<Heading size={SIZE.lg}>{strings.tabs()}</Heading>
<Button
onPress={() => {
useTabStore.getState().newTab();
@@ -240,7 +243,7 @@ export default function EditorTabs({
}, 500);
close?.();
}}
title="New tab"
title={strings.newTab()}
icon="plus"
style={{
flexDirection: "row",

View File

@@ -52,6 +52,7 @@ import { Pressable } from "../../ui/pressable";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
const ExportNotesSheet = ({
ids,
@@ -118,7 +119,6 @@ const ExportNotesSheet = ({
await exportNoteAs("pdf");
},
icon: "file-pdf-box",
desc: "View in any pdf reader app",
id: notesnook.ids.dialogs.export.pdf,
pro: premium
},
@@ -128,7 +128,6 @@ const ExportNotesSheet = ({
await exportNoteAs("md");
},
icon: "language-markdown",
desc: "View in any text or markdown editor",
id: notesnook.ids.dialogs.export.md,
pro: premium
},
@@ -138,7 +137,6 @@ const ExportNotesSheet = ({
await exportNoteAs("md-frontmatter");
},
icon: "language-markdown",
desc: "View in any text or markdown editor",
id: notesnook.ids.dialogs.export.md,
pro: premium
},
@@ -148,7 +146,6 @@ const ExportNotesSheet = ({
await exportNoteAs("txt");
},
icon: "card-text",
desc: "View in any text editor",
id: notesnook.ids.dialogs.export.text,
pro: true
},
@@ -158,7 +155,6 @@ const ExportNotesSheet = ({
await exportNoteAs("html");
},
icon: "language-html5",
desc: "View in any web browser & html reader",
id: notesnook.ids.dialogs.export.html,
pro: premium
}
@@ -175,14 +171,7 @@ const ExportNotesSheet = ({
>
<DialogHeader
icon="export"
title={
ids.length > 1 ? `Export ${ids.length} Notes` : "Export Note"
}
paragraph={`All exports are saved in ${
Platform.OS === "android"
? "the selected"
: "Notesnook/exported"
} folder in phone storage`}
title={strings.exportNotes(ids.length)}
/>
</View>
@@ -238,13 +227,13 @@ const ExportNotesSheet = ({
<Heading style={{ marginLeft: 10 }} size={SIZE.md}>
{item.title}
</Heading>
<Paragraph
{/* <Paragraph
style={{ marginLeft: 10 }}
size={SIZE.sm}
color={colors.secondary.paragraph}
>
{item.desc}
</Paragraph>
</Paragraph> */}
</View>
</Pressable>
</Fragment>
@@ -264,9 +253,10 @@ const ExportNotesSheet = ({
<>
<ActivityIndicator />
<Paragraph>
{`${
status ? `${status})` : `Exporting notes`
}... Please wait`}
{strings.exportingNotes(status) +
"..." +
" " +
strings.pleaseWait()}
</Paragraph>
</>
) : (
@@ -287,21 +277,20 @@ const ExportNotesSheet = ({
}}
color={colors.secondary.heading}
>
{ids.length > 1
? `${ids.length} Notes exported`
: "Note exported"}
{strings.exportSuccessHeading(ids.length)}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
Your {ids.length > 1 ? "notes are" : "note is"} exported
successfully as {result?.fileName}
{strings.exportSuccessDesc(result?.fileName as string)}
</Paragraph>
<Button
title={
Platform.OS === "android" ? "Open file location" : "Open"
Platform.OS === "android"
? strings.openFileLocation()
: strings.open()
}
type="accent"
width={250}
@@ -322,8 +311,7 @@ const ExportNotesSheet = ({
showAppsSuggestions: true
}).catch((e) => {
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
heading: strings.noApplicationFound(result.name),
type: "success",
context: "local"
});
@@ -332,7 +320,7 @@ const ExportNotesSheet = ({
}}
/>
<Button
title="Share"
title={strings.share()}
type="secondaryAccented"
width={250}
fontSize={SIZE.md}
@@ -356,7 +344,7 @@ const ExportNotesSheet = ({
}}
/>
<Button
title="Export in another format"
title={strings.exportAgain()}
type="inverted"
width={250}
fontSize={SIZE.md}

View File

@@ -23,6 +23,7 @@ import FileViewer from "react-native-file-viewer";
import { ToastManager } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import { strings } from "@notesnook/intl";
export const ShareComponent = ({ uri, name, padding }) => {
return (
<View
@@ -31,7 +32,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
}}
>
<Button
title="Open"
title={strings.open()}
type="accent"
width="100%"
fontSize={SIZE.md}
@@ -41,8 +42,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
showAppsSuggestions: true
}).catch(() => {
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${name} file.`,
heading: strings.noApplicationFound(name),
type: "success",
context: "local"
});
@@ -50,7 +50,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
}}
/>
<Button
title="Share"
title={strings.share()}
type="shade"
width="100%"
fontSize={SIZE.md}

View File

@@ -34,6 +34,7 @@ import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
const { colors } = useThemeColors();
@@ -75,8 +76,7 @@ Logged in: ${user ? "yes" : "no"}`,
} catch (e) {
setLoading(false);
ToastManager.show({
heading: "An error occured",
message: e.message,
heading: e.message,
type: "error"
});
}
@@ -99,14 +99,14 @@ Logged in: ${user ? "yes" : "no"}`,
gap: 10
}}
>
<Heading>Issue submitted</Heading>
<Heading>{strings.issueCreatedHeading()}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
selectable={true}
>
You can track your issue at{" "}
{strings.issueCreatedDesc[0]()}
<Paragraph
style={{
textDecorationLine: "underline",
@@ -118,12 +118,11 @@ Logged in: ${user ? "yes" : "no"}`,
>
{issueUrl.current}
</Paragraph>
. Please note that we will respond to your issue on the given
link. We recommend that you save it.
. {strings.issueCreatedDesc[1]()}
</Paragraph>
<Button
title="Open issue"
title={strings.openIssue()}
onPress={() => {
Linking.openURL(issueUrl.current);
}}
@@ -135,18 +134,14 @@ Logged in: ${user ? "yes" : "no"}`,
) : (
<>
<DialogHeader
title={issueTitle || "Report issue"}
paragraph={
issueTitle
? "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
: "Let us know if you have faced any issue/bug while using Notesnook."
}
title={issueTitle || strings.issueTitle()}
paragraph={issueTitle ? strings.issueDesc() : strings.issueDesc2()}
/>
<Seperator half />
<TextInput
placeholder="Title"
placeholder={strings.title()}
onChangeText={(v) => (title.current = v)}
defaultValue={title.current}
style={{
@@ -165,13 +160,7 @@ Logged in: ${user ? "yes" : "no"}`,
<TextInput
ref={bodyRef}
multiline
placeholder={`Tell us more about the issue you are facing.
For example:
- What were you trying to do in the app?
- What did you expect to happen?
- Steps to reproduce the issue
- Things you have tried etc.`}
placeholder={strings.issuePlaceholder()}
numberOfLines={5}
textAlignVertical="top"
onChangeText={(v) => (body.current = v)}
@@ -211,7 +200,7 @@ For example:
<Seperator />
<Button
onPress={onPress}
title={loading ? null : "Submit"}
title={loading ? null : strings.submit()}
loading={loading}
width="100%"
type="accent"
@@ -225,10 +214,10 @@ For example:
textAlign: "center"
}}
>
The information above will be publically available at{" "}
{strings.issueNotice[0]()}{" "}
<Text
onPress={() => {
Linking.openURL("https://github.com/streetwriters/notesnook");
Linking.openURL("https://github.com/streetwriters/notesnook/issues");
}}
style={{
textDecorationLine: "underline",
@@ -237,8 +226,7 @@ For example:
>
github.com/streetwriters/notesnook.
</Text>{" "}
If you want to ask something in general or need some assistance, we
would suggest that you{" "}
{strings.issueNotice[1]()}{" "}
<Text
style={{
textDecorationLine: "underline",
@@ -255,7 +243,7 @@ For example:
}
}}
>
join our community on Discord.
{strings.issueNotice[2]()}
</Text>
</Paragraph>
</>

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