mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
542 Commits
fix-open-n
...
feat/prici
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
169db9ba59 | ||
|
|
9c044d86d5 | ||
|
|
2e926015e3 | ||
|
|
2d43e734ca | ||
|
|
05df9b77ed | ||
|
|
69ba20009c | ||
|
|
9bf490bcb4 | ||
|
|
5a40080347 | ||
|
|
f42155ca5d | ||
|
|
3b06450113 | ||
|
|
2fb3c3a02b | ||
|
|
c8648659c7 | ||
|
|
83a663da2b | ||
|
|
5f0d14f01c | ||
|
|
069ec39de8 | ||
|
|
bef06a2a20 | ||
|
|
1a98fb8996 | ||
|
|
dc15bf75c6 | ||
|
|
7357422053 | ||
|
|
7918c089d3 | ||
|
|
ba50b75c54 | ||
|
|
0d7f32bdd0 | ||
|
|
8251e9437b | ||
|
|
416d5aef90 | ||
|
|
edb2e3a0ed | ||
|
|
64031a7814 | ||
|
|
1bd3c3d0f4 | ||
|
|
21b6fc4982 | ||
|
|
b881023e7b | ||
|
|
6387156596 | ||
|
|
2b2cfe9d33 | ||
|
|
e81580024e | ||
|
|
8b207b47dd | ||
|
|
15aab4ec29 | ||
|
|
bdb0474645 | ||
|
|
8d25d03390 | ||
|
|
ea2c7d2ea9 | ||
|
|
0ff38c7ca3 | ||
|
|
595f2e9ba7 | ||
|
|
b02bf3d4ad | ||
|
|
1e295d8562 | ||
|
|
27a0aeb170 | ||
|
|
7e355c3ecb | ||
|
|
69468487c2 | ||
|
|
0809b5b701 | ||
|
|
171f28f7c0 | ||
|
|
0a2af0ec7d | ||
|
|
c53354c368 | ||
|
|
be414f869c | ||
|
|
de40285e21 | ||
|
|
a7fa0bde00 | ||
|
|
19c2d2007f | ||
|
|
ecc9736b90 | ||
|
|
ffd022cfa6 | ||
|
|
89aba00aac | ||
|
|
bcc739aa57 | ||
|
|
fc1015b3fb | ||
|
|
f1fa32c7f5 | ||
|
|
a1e794c583 | ||
|
|
c787ef29d6 | ||
|
|
7c257cfbd2 | ||
|
|
99c77e6c5f | ||
|
|
f060c65bd5 | ||
|
|
22559cdf42 | ||
|
|
1fb86cb27b | ||
|
|
7f31066e8f | ||
|
|
50e418dc53 | ||
|
|
59739067b6 | ||
|
|
f100e157d3 | ||
|
|
870ce259f0 | ||
|
|
95b50eb5ca | ||
|
|
f243d13b81 | ||
|
|
749a13efbc | ||
|
|
275d5d7f04 | ||
|
|
54de071e85 | ||
|
|
9647e2513b | ||
|
|
239781eabd | ||
|
|
21573d6cc8 | ||
|
|
9b091f0bb0 | ||
|
|
e0beef745e | ||
|
|
2f2653d223 | ||
|
|
7458aa53ae | ||
|
|
f7db306c26 | ||
|
|
7e8d6f2041 | ||
|
|
d424a6c48f | ||
|
|
7293224514 | ||
|
|
7706cfed9a | ||
|
|
bbfe0b237d | ||
|
|
2c7cddd853 | ||
|
|
37c1d39c41 | ||
|
|
5d4960a26f | ||
|
|
78f07b23df | ||
|
|
7aeb047b50 | ||
|
|
0e3a661dd0 | ||
|
|
fba350ed1e | ||
|
|
76bfbc7316 | ||
|
|
7683d8fa5b | ||
|
|
3fe5352b56 | ||
|
|
a86d9a9c0c | ||
|
|
a68f74bc1b | ||
|
|
18310698f6 | ||
|
|
020244231f | ||
|
|
59085d7b19 | ||
|
|
c15410e168 | ||
|
|
2962f13457 | ||
|
|
bfc77fc9a4 | ||
|
|
7e88ec88ee | ||
|
|
9ef07f22af | ||
|
|
268343be3e | ||
|
|
956e5373d1 | ||
|
|
919fa9bdf1 | ||
|
|
e06192c5b4 | ||
|
|
b5be5e0b59 | ||
|
|
05695cfcc8 | ||
|
|
423e1c6816 | ||
|
|
6db1cf5cc1 | ||
|
|
2e4a87abd9 | ||
|
|
c26988134f | ||
|
|
bcffdd0f91 | ||
|
|
de1051d52d | ||
|
|
b1a360e445 | ||
|
|
49e85dfa87 | ||
|
|
452ff12071 | ||
|
|
1b6fc40633 | ||
|
|
82b28f8263 | ||
|
|
6e47eae30a | ||
|
|
4040f2ecc4 | ||
|
|
2293866c61 | ||
|
|
2cb4a10076 | ||
|
|
2f7b8d3377 | ||
|
|
983220b2f2 | ||
|
|
ca2848e491 | ||
|
|
48e5c4d908 | ||
|
|
5774dd8c60 | ||
|
|
9d65184f42 | ||
|
|
395b2750e8 | ||
|
|
3bfb1d57a5 | ||
|
|
e59b432007 | ||
|
|
efbc566386 | ||
|
|
2576f1dd00 | ||
|
|
7d9388bd27 | ||
|
|
1979cd2ea4 | ||
|
|
7bd5dfb772 | ||
|
|
ac7c88218b | ||
|
|
fb642233c8 | ||
|
|
6e2a19d7e0 | ||
|
|
4d4993c570 | ||
|
|
916e097cb3 | ||
|
|
6def76e1bb | ||
|
|
d93ec27c9e | ||
|
|
15329597a5 | ||
|
|
49caf23436 | ||
|
|
e6d8401c56 | ||
|
|
11a5e8c180 | ||
|
|
5bd7a081c1 | ||
|
|
a740a5db81 | ||
|
|
a61f2efacf | ||
|
|
51a253f3e2 | ||
|
|
19ed753079 | ||
|
|
4a85c49f5d | ||
|
|
16dd4b3359 | ||
|
|
9279c671ac | ||
|
|
225b21977e | ||
|
|
026b9212c4 | ||
|
|
5ce59c6125 | ||
|
|
b7c81a956c | ||
|
|
873f976fd3 | ||
|
|
7d8a16c463 | ||
|
|
f5841985fa | ||
|
|
da86cbbe32 | ||
|
|
ed115d3f9e | ||
|
|
e52e74523a | ||
|
|
7e43c3b373 | ||
|
|
e6db186b62 | ||
|
|
998e62ade7 | ||
|
|
ac16bc74fc | ||
|
|
2aee26b082 | ||
|
|
a188920d96 | ||
|
|
1f3defc28c | ||
|
|
7aeeabfb3a | ||
|
|
6641d3e625 | ||
|
|
72031617eb | ||
|
|
78090b700b | ||
|
|
f2f8d81b98 | ||
|
|
84d68ca67b | ||
|
|
6368e47712 | ||
|
|
0f17b01a74 | ||
|
|
025e23356a | ||
|
|
ac85002dff | ||
|
|
3a23e4fbaf | ||
|
|
3547663af8 | ||
|
|
c5dfc02085 | ||
|
|
bb9a6431bf | ||
|
|
624d3d7ce8 | ||
|
|
170b0aa49a | ||
|
|
07cfd74506 | ||
|
|
1d40054ac0 | ||
|
|
5cd37f4b64 | ||
|
|
25675f71e8 | ||
|
|
202534fca9 | ||
|
|
a57558d8ab | ||
|
|
f1c201dca9 | ||
|
|
a73bff83fa | ||
|
|
e4428299bc | ||
|
|
487567ffbc | ||
|
|
efd83e6de3 | ||
|
|
a07230f707 | ||
|
|
35a2e11afd | ||
|
|
cf4a7a3d34 | ||
|
|
86e661ccd6 | ||
|
|
e7be9b35ed | ||
|
|
53a367b0b2 | ||
|
|
b6206e2ca7 | ||
|
|
a910c819f2 | ||
|
|
6dd7a4960f | ||
|
|
c4e15b8f90 | ||
|
|
f458a4b86a | ||
|
|
8a44a7b99a | ||
|
|
25065c01a0 | ||
|
|
87ae9e86e7 | ||
|
|
782c53b622 | ||
|
|
cb52b390ab | ||
|
|
5a49941003 | ||
|
|
6797b9b621 | ||
|
|
031a1b31b4 | ||
|
|
4e5762be45 | ||
|
|
7a6ba5d1d1 | ||
|
|
cc3ccd041d | ||
|
|
31453dc098 | ||
|
|
2e84099c80 | ||
|
|
5bbc0cd452 | ||
|
|
22aae401ba | ||
|
|
97f135cd07 | ||
|
|
976f933233 | ||
|
|
95fe1044d6 | ||
|
|
677201e979 | ||
|
|
6ec9370afb | ||
|
|
bea0f1ab0e | ||
|
|
5357be4741 | ||
|
|
02fdec74ce | ||
|
|
dbc907ceeb | ||
|
|
7bb53a5e0c | ||
|
|
69cb24149d | ||
|
|
438d412ae9 | ||
|
|
1b71edef45 | ||
|
|
a4c0e8412b | ||
|
|
fbcb8675ca | ||
|
|
fbf5c82d04 | ||
|
|
fa40c539ed | ||
|
|
f3218fdc31 | ||
|
|
d12ed967e3 | ||
|
|
b6021dc76b | ||
|
|
f95442a828 | ||
|
|
846aae312e | ||
|
|
68fdbcb7fc | ||
|
|
b938c58b67 | ||
|
|
759fba06ae | ||
|
|
10e24ae632 | ||
|
|
341032c884 | ||
|
|
aa1fa0ca81 | ||
|
|
398432efde | ||
|
|
57ddaafc16 | ||
|
|
e4a755c69f | ||
|
|
053f70cdb4 | ||
|
|
cdcdc43cad | ||
|
|
f1111191b1 | ||
|
|
529e2a4dfd | ||
|
|
d88d07a2b0 | ||
|
|
197ff36c20 | ||
|
|
d94474de79 | ||
|
|
fcf0e0c531 | ||
|
|
24048ebfbe | ||
|
|
5ae8cdd5b9 | ||
|
|
74187b335f | ||
|
|
16fc689299 | ||
|
|
578a9de680 | ||
|
|
62d475b2ef | ||
|
|
69c902c6c1 | ||
|
|
c8c1028cbc | ||
|
|
313807c384 | ||
|
|
5dc0e15094 | ||
|
|
12c5b6d227 | ||
|
|
364ae51e36 | ||
|
|
18947e0c93 | ||
|
|
3880db4339 | ||
|
|
ae5b0b6fcc | ||
|
|
2c729f90cc | ||
|
|
800514d769 | ||
|
|
b03b9482dc | ||
|
|
9019490dbd | ||
|
|
4a3e23c288 | ||
|
|
1800ebea2d | ||
|
|
a708c9e9eb | ||
|
|
2aeb875fec | ||
|
|
a8164e9f6c | ||
|
|
0ee8e738e5 | ||
|
|
8fdef2ac7d | ||
|
|
920b1d91f3 | ||
|
|
0ad382aced | ||
|
|
c8aa8e37cf | ||
|
|
9a37f7c506 | ||
|
|
717750d5d4 | ||
|
|
1274ecc3b5 | ||
|
|
4c2ad4567c | ||
|
|
ebc01af34a | ||
|
|
e66d8eee13 | ||
|
|
55e6a4b65a | ||
|
|
0aa1db584d | ||
|
|
a0e6266d28 | ||
|
|
bd0aaed863 | ||
|
|
9410c42f29 | ||
|
|
80186c1812 | ||
|
|
37dfda5c54 | ||
|
|
ec83517dc5 | ||
|
|
2eb4911592 | ||
|
|
cd0a6a8fcb | ||
|
|
b15f192244 | ||
|
|
406cb663c9 | ||
|
|
301a30a50a | ||
|
|
81e3432143 | ||
|
|
c43c2936ac | ||
|
|
fdea1807d2 | ||
|
|
5439578fcf | ||
|
|
e05e52f870 | ||
|
|
1c6243d12e | ||
|
|
a51dfb5b6e | ||
|
|
c98bddd35b | ||
|
|
1eb80ccc88 | ||
|
|
5a346aa7de | ||
|
|
910095a769 | ||
|
|
119059befa | ||
|
|
a8f5dea315 | ||
|
|
ca0a4cb02c | ||
|
|
a04cd6b8dc | ||
|
|
3e4ab0f6df | ||
|
|
a7fe1aca44 | ||
|
|
ff0188f602 | ||
|
|
c846f8abc8 | ||
|
|
8d8acfb99d | ||
|
|
3a5b5772ad | ||
|
|
8046932e4e | ||
|
|
c45aa84739 | ||
|
|
d98be849e3 | ||
|
|
727ac6d907 | ||
|
|
8302f8ba4c | ||
|
|
4db8b0db01 | ||
|
|
3f1614ab9a | ||
|
|
909d8c35f8 | ||
|
|
a73780f0b0 | ||
|
|
286fdbeec0 | ||
|
|
59099552f8 | ||
|
|
0b6013be58 | ||
|
|
2d93a17f34 | ||
|
|
5ef7e39e17 | ||
|
|
f7de4c9084 | ||
|
|
0672c864f7 | ||
|
|
cb3706c150 | ||
|
|
08c6a99336 | ||
|
|
adf961fa08 | ||
|
|
0eb9ae47e9 | ||
|
|
aa706300a4 | ||
|
|
fd183a0e1e | ||
|
|
b27cd4be0f | ||
|
|
9575561832 | ||
|
|
02f96f8d14 | ||
|
|
3cf46e7ad1 | ||
|
|
93551aa0b7 | ||
|
|
d00c35bdaf | ||
|
|
6e6b793568 | ||
|
|
9e064d88c6 | ||
|
|
1658ad8d18 | ||
|
|
c94da3bdf0 | ||
|
|
1f0fc83a7c | ||
|
|
a6af92c330 | ||
|
|
ef93ecc481 | ||
|
|
039709822c | ||
|
|
25c861ce77 | ||
|
|
670f1570b5 | ||
|
|
c93d82d2d6 | ||
|
|
bced20031b | ||
|
|
f8bd089b51 | ||
|
|
2a4d04a26f | ||
|
|
a0c32191f9 | ||
|
|
662d59f60a | ||
|
|
13befe1947 | ||
|
|
21f8b6d557 | ||
|
|
aef53cace9 | ||
|
|
382b5b0240 | ||
|
|
a45b66d449 | ||
|
|
dd67b1a803 | ||
|
|
00568ae839 | ||
|
|
2b2ea5717c | ||
|
|
5672de8565 | ||
|
|
1c70ad8cec | ||
|
|
0c344a2146 | ||
|
|
38f69b514c | ||
|
|
b502f6a08f | ||
|
|
d68c3269d3 | ||
|
|
6d9af85e2a | ||
|
|
1d58cfea3c | ||
|
|
c1fa4d3400 | ||
|
|
df179056d6 | ||
|
|
26f9e08c23 | ||
|
|
f719bebe5a | ||
|
|
9693c8ea54 | ||
|
|
040a677895 | ||
|
|
df2708147e | ||
|
|
faba5f6385 | ||
|
|
eab5bccb99 | ||
|
|
ed399c0d0a | ||
|
|
3c7e962fe0 | ||
|
|
56614ef19d | ||
|
|
169318c549 | ||
|
|
bd6a1b9bff | ||
|
|
443d6625f9 | ||
|
|
b6f95ff7cd | ||
|
|
1b13da54a1 | ||
|
|
adf0f132fd | ||
|
|
73ec04b42d | ||
|
|
54a68e4406 | ||
|
|
bd73ae9df4 | ||
|
|
fe41f7b687 | ||
|
|
c9adaa2442 | ||
|
|
10be5771c6 | ||
|
|
43f4669360 | ||
|
|
3b04925e54 | ||
|
|
ba3959af9e | ||
|
|
552d86d9d2 | ||
|
|
096602150e | ||
|
|
e5795bcbb2 | ||
|
|
9be52a69ad | ||
|
|
5809384f62 | ||
|
|
fff70dce16 | ||
|
|
258140b1e0 | ||
|
|
0a00844e64 | ||
|
|
03cea85efc | ||
|
|
8a16ef9b85 | ||
|
|
c82c30b530 | ||
|
|
46599f66da | ||
|
|
d1b532a4ae | ||
|
|
0baf7afc25 | ||
|
|
a0595a7d23 | ||
|
|
1e6f6ab477 | ||
|
|
62d677f9ad | ||
|
|
8de98d98ab | ||
|
|
5b57614bfe | ||
|
|
ae7c538ad7 | ||
|
|
c6394c9e37 | ||
|
|
acf3740a68 | ||
|
|
8423543904 | ||
|
|
b90cb28637 | ||
|
|
780e1e9db0 | ||
|
|
ca6f8e5e4b | ||
|
|
4c4cda1c39 | ||
|
|
6fcbde772c | ||
|
|
eac1750b1c | ||
|
|
5eea179a58 | ||
|
|
fe1ea6dc27 | ||
|
|
cf1b774255 | ||
|
|
198fa33fc0 | ||
|
|
e722866d62 | ||
|
|
58845e5508 | ||
|
|
543c6f17ed | ||
|
|
533f95fcff | ||
|
|
c02f1cde16 | ||
|
|
043f55ead0 | ||
|
|
fcece06a1c | ||
|
|
da7d79edc6 | ||
|
|
a2e7190a4a | ||
|
|
503f2b5876 | ||
|
|
68d57e2b27 | ||
|
|
400aa574cd | ||
|
|
5028bb1275 | ||
|
|
27ae5bb5a2 | ||
|
|
83316fe299 | ||
|
|
8869224de2 | ||
|
|
c4152f0fc0 | ||
|
|
d803005637 | ||
|
|
53d896be2b | ||
|
|
8c37c6409a | ||
|
|
9195244890 | ||
|
|
152794ccd0 | ||
|
|
2e8b234b63 | ||
|
|
055c647698 | ||
|
|
2b7bcfe7c6 | ||
|
|
3c8b8a7bc9 | ||
|
|
fde45fb6de | ||
|
|
4a616a3e67 | ||
|
|
bd0a338dd6 | ||
|
|
5ab721e3e2 | ||
|
|
cb70bb9429 | ||
|
|
82cadd473c | ||
|
|
0e8ea3ddeb | ||
|
|
511acc5fca | ||
|
|
001cf8ee92 | ||
|
|
a965cd694b | ||
|
|
d3c1c1959b | ||
|
|
9a866a552a | ||
|
|
837c296821 | ||
|
|
9aed6faed8 | ||
|
|
734f491e8d | ||
|
|
0ea13024e9 | ||
|
|
cb27039245 | ||
|
|
714ac2deac | ||
|
|
d605bc29bf | ||
|
|
080101bc45 | ||
|
|
4819e17da3 | ||
|
|
69a5d5a1b6 | ||
|
|
68a6d6c313 | ||
|
|
c51ef4d5f6 | ||
|
|
d9c5c1f893 | ||
|
|
5a75ca73a9 | ||
|
|
626d393376 | ||
|
|
68d06ff3f8 | ||
|
|
79f20d100c | ||
|
|
9d5bec3bfb | ||
|
|
ac96354b4c | ||
|
|
63f44d6fbc | ||
|
|
f9bfa88c83 | ||
|
|
ef192f3d22 | ||
|
|
c9a8134265 | ||
|
|
cd7a5d2ec0 | ||
|
|
b92bc03fa9 | ||
|
|
5f5e472800 | ||
|
|
0951906cac | ||
|
|
6f0fe03760 | ||
|
|
159ce97e42 | ||
|
|
8a9848634c | ||
|
|
282705a683 | ||
|
|
c91b706ed0 | ||
|
|
16afc07b2f | ||
|
|
04f8be5a62 | ||
|
|
0e8a5ca1fa | ||
|
|
1491bf771e | ||
|
|
0f5ce465c3 | ||
|
|
647ff7ae0e | ||
|
|
45a552adef | ||
|
|
8e207d787d | ||
|
|
00754593ac | ||
|
|
b22770f1ae | ||
|
|
523da4e91f | ||
|
|
de6c8154a3 |
@@ -15,6 +15,7 @@ const SCOPES = [
|
||||
"mobile",
|
||||
"web",
|
||||
"vericrypt",
|
||||
"monograph",
|
||||
"desktop",
|
||||
"crypto",
|
||||
"editor",
|
||||
|
||||
10
.github/workflows/android.publish.internal.yml
vendored
10
.github/workflows/android.publish.internal.yml
vendored
@@ -147,8 +147,10 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload signed aab to Github
|
||||
uses: actions/upload-artifact@v2
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Notesnook.aab
|
||||
path: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/native/android/app/build/**/*.map
|
||||
packages/editor-mobile/sourcemaps/*.map
|
||||
|
||||
13
.github/workflows/android.publish.yml
vendored
13
.github/workflows/android.publish.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "11"
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install node modules
|
||||
@@ -133,7 +133,6 @@ jobs:
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
|
||||
|
||||
- name: Create release draft on Github
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
@@ -148,8 +147,10 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload signed aab to Github
|
||||
uses: actions/upload-artifact@v2
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Notesnook.aab
|
||||
path: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
name: sourcemaps
|
||||
path: |
|
||||
apps/mobile/native/android/app/build/**/*.map
|
||||
packages/editor-mobile/sourcemaps/*.map
|
||||
|
||||
9
.github/workflows/ios.publish.yml
vendored
9
.github/workflows/ios.publish.yml
vendored
@@ -98,7 +98,10 @@ jobs:
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload Notesnook.ipa to Github
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Notesnook.ipa
|
||||
path: Notesnook.ipa
|
||||
name: Notesnook.zip
|
||||
path: |
|
||||
Notesnook.ipa
|
||||
apps/mobile/native/ios/**/*.map
|
||||
packages/editor-mobile/sourcemaps/*.map
|
||||
|
||||
83
.github/workflows/monograph.publish.yml
vendored
Normal file
83
.github/workflows/monograph.publish.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# GitHub recommends pinning actions to a commit SHA.
|
||||
# To get a newer version, you will need to update the SHA.
|
||||
# You can also reference a tag or branch, but the action may change without warning.
|
||||
|
||||
name: Publish @notesnook/monograph
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=monograph
|
||||
|
||||
- name: Generate build
|
||||
run: npx nx build @notesnook/monograph
|
||||
|
||||
# Setup Buildx
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
ecr: auto
|
||||
logout: true
|
||||
|
||||
# Pull previous image from docker hub to use it as cache to improve the image build time.
|
||||
- name: docker pull cache image
|
||||
continue-on-error: true
|
||||
run: docker pull streetwriters/monograph:latest
|
||||
|
||||
# Setup QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: streetwriters/monograph
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: apps/monograph
|
||||
file: apps/monograph/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: streetwriters/monograph:latest
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: index.docker.io/streetwriters/monograph
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
6
.github/workflows/web.tests.yml
vendored
6
.github/workflows/web.tests.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
run: npm run build:test:web
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
working-directory: apps/web
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-${{ matrix.shard }}
|
||||
|
||||
@@ -40,12 +40,35 @@ Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you can finally start the desktop app:
|
||||
Now you can finally start the desktop app for development:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
To run the app in release mode:
|
||||
|
||||
```bash
|
||||
npm run staging -- --rebuild
|
||||
```
|
||||
|
||||
This will compile and run the app in production mode but it won't generate any packages. To create the final packages, you'll have to run the following commands:
|
||||
|
||||
```bash
|
||||
npm run release -- --rebuild
|
||||
|
||||
# For macOS
|
||||
npx electron-builder --mac dmg --arm64 --x64 -publish never
|
||||
|
||||
# For Linux (AppImage)
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
|
||||
|
||||
# For Windows
|
||||
npx electron-builder --win --publish never
|
||||
```
|
||||
|
||||
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
|
||||
|
||||
## Developer guide
|
||||
|
||||
### The tech stack
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
owner: streetwriters
|
||||
repo: notesnook
|
||||
provider: github
|
||||
updaterCacheDirName: Notesnook
|
||||
2578
apps/desktop/package-lock.json
generated
2578
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.18",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -11,29 +11,29 @@
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/server": "10.38.3",
|
||||
"better-sqlite3-multiple-ciphers": "^9.5.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.4",
|
||||
"icojs": "^0.17.1",
|
||||
"sodium-native": "^4.1.1",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"better-sqlite3-multiple-ciphers": "11.2.1",
|
||||
"electron-trpc": "0.6.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"icojs": "^0.19.4",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.6.2",
|
||||
"zod": "^3.21.4"
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^29.3.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"esbuild": "^0.20.0",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.7.0",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.1",
|
||||
"electron": "^30.5.1",
|
||||
"electron-builder": "^25.0.5",
|
||||
"esbuild": "^0.24.0",
|
||||
"node-abi": "^3.68.0",
|
||||
"node-gyp-build": "^4.8.2",
|
||||
"prebuildify": "^6.0.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^6.14.1"
|
||||
"undici": "^6.19.8"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
@@ -158,7 +158,8 @@
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": true,
|
||||
"createDesktopShortcut": "always"
|
||||
"createDesktopShortcut": "always",
|
||||
"deleteAppDataOnUninstall": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
index fcb7f54..cb1c7f7 100644
|
||||
index 739a48c..7a7b4af 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
|
||||
@@ -117,6 +117,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
@@ -10,7 +10,7 @@ index fcb7f54..cb1c7f7 100644
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
|
||||
@@ -160,6 +161,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
@@ -1,23 +0,0 @@
|
||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||
index 379cf3b..2644e5d 100644
|
||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||
@@ -221,9 +221,16 @@ class G {
|
||||
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
|
||||
}
|
||||
detachWindow(r) {
|
||||
+
|
||||
y(this, c, i(this, c).filter((n) => n !== r));
|
||||
- for (const [n, t] of i(this, u).entries())
|
||||
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ for (const [n, t] of i(this, u).entries()) {
|
||||
+ try {
|
||||
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ } catch(e) {
|
||||
+ console.error(e);
|
||||
+ // ignore
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {
|
||||
39
apps/desktop/patches/electron-trpc+0.6.1.patch
Normal file
39
apps/desktop/patches/electron-trpc+0.6.1.patch
Normal file
@@ -0,0 +1,39 @@
|
||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||
index 07971db..b67a3cf 100644
|
||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||
@@ -537,8 +537,15 @@ class be {
|
||||
attachWindow(r) {
|
||||
F(this, w).includes(r) || (y("Attaching window", r.id), F(this, w).push(r), v(this, N, Z).call(this, r));
|
||||
}
|
||||
- detachWindow(r) {
|
||||
- y("Detaching window", r.id), L(this, w, F(this, w).filter((e) => e !== r)), v(this, R, M).call(this, { webContentsId: r.webContents.id });
|
||||
+ detachWindow(r, webContentsId) {
|
||||
+ y("Detaching window", r.id);
|
||||
+
|
||||
+ if (r.isDestroyed() && webContentsId === undefined) {
|
||||
+ throw new Error('webContentsId is required when calling detachWindow on a destroyed window');
|
||||
+ }
|
||||
+
|
||||
+ L(this, w, F(this, w).filter((e) => e !== r));
|
||||
+ v(this, R, M).call(this, { webContentsId: webContentsId ?? r.webContents.id });
|
||||
}
|
||||
}
|
||||
w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
@@ -548,6 +555,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
for (const [i, a] of F(this, E).entries())
|
||||
i.startsWith(`${r}-${e ?? ""}`) && (y("Closing subscription", i), a.unsubscribe(), F(this, E).delete(i));
|
||||
}, N = new WeakSet(), Z = function(r) {
|
||||
+ const webContentsId = r.webContents.id;
|
||||
r.webContents.on("did-start-navigation", ({ frame: e }) => {
|
||||
y(
|
||||
"Handling webContents `did-start-navigation` event",
|
||||
@@ -558,7 +566,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
frameRoutingId: e.routingId
|
||||
});
|
||||
}), r.webContents.on("destroyed", () => {
|
||||
- y("Handling webContents `destroyed` event"), this.detachWindow(r);
|
||||
+ y("Handling webContents `destroyed` event"), this.detachWindow(r, webContentsId);
|
||||
});
|
||||
};
|
||||
const me = ({
|
||||
@@ -1,46 +0,0 @@
|
||||
diff --git a/node_modules/node-gyp-build/bin.js b/node_modules/node-gyp-build/bin.js
|
||||
index 3fbcdf0..7ca3ab5 100644
|
||||
--- a/node_modules/node-gyp-build/bin.js
|
||||
+++ b/node_modules/node-gyp-build/bin.js
|
||||
@@ -16,7 +16,8 @@ if (!buildFromSource()) {
|
||||
}
|
||||
|
||||
function build () {
|
||||
- var args = [os.platform() === 'win32' ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
|
||||
+ var win32 = os.platform() === 'win32'
|
||||
+ var args = [win32 ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
|
||||
|
||||
try {
|
||||
var pkg = require('node-gyp/package.json')
|
||||
@@ -27,7 +28,7 @@ function build () {
|
||||
]
|
||||
} catch (_) {}
|
||||
|
||||
- proc.spawn(args[0], args.slice(1), { stdio: 'inherit' }).on('exit', function (code) {
|
||||
+ proc.spawn(args[0], args.slice(1), { stdio: 'inherit', shell: win32, windowsHide: true }).on('exit', function (code) {
|
||||
if (code || !process.argv[3]) process.exit(code)
|
||||
exec(process.argv[3]).on('exit', function (code) {
|
||||
process.exit(code)
|
||||
@@ -45,15 +46,18 @@ function preinstall () {
|
||||
|
||||
function exec (cmd) {
|
||||
if (process.platform !== 'win32') {
|
||||
- var shell = os.platform() === 'android' ? 'sh' : '/bin/sh'
|
||||
- return proc.spawn(shell, ['-c', '--', cmd], {
|
||||
+ var shell = os.platform() === 'android' ? 'sh' : true
|
||||
+ return proc.spawn(cmd, [], {
|
||||
+ shell,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
}
|
||||
|
||||
- return proc.spawn(process.env.comspec || 'cmd.exe', ['/s', '/c', '"' + cmd + '"'], {
|
||||
+ return proc.spawn(cmd, [], {
|
||||
windowsVerbatimArguments: true,
|
||||
- stdio: 'inherit'
|
||||
+ stdio: 'inherit',
|
||||
+ shell: true,
|
||||
+ windowsHide: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
await exec(
|
||||
"npx nx build:desktop @notesnook/web",
|
||||
"yarn nx build:desktop @notesnook/web",
|
||||
path.join(__dirname, "..", "..", "..")
|
||||
);
|
||||
}
|
||||
@@ -46,11 +46,11 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
// temporary until there's support for prebuilt binaries for linux ARM
|
||||
if (os.platform() === "linux") await patchBetterSQLite3();
|
||||
|
||||
if (os.platform() === "win32")
|
||||
await exec(
|
||||
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
);
|
||||
// if (os.platform() === "win32")
|
||||
// await exec(
|
||||
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
// path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
// );
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
recursive: true,
|
||||
@@ -58,15 +58,15 @@ await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
});
|
||||
|
||||
if (args.variant === "mas") {
|
||||
await exec(`npm run bundle:mas`);
|
||||
await exec(`yarn run bundle:mas`);
|
||||
} else {
|
||||
await exec(`npm run bundle`);
|
||||
await exec(`yarn run bundle`);
|
||||
}
|
||||
|
||||
await exec(`npx tsc`);
|
||||
await exec(`yarn tsc`);
|
||||
|
||||
if (args.run) {
|
||||
await exec(`npx electron-builder --dir --x64`);
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
@@ -94,7 +94,7 @@ async function patchBetterSQLite3() {
|
||||
);
|
||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||
|
||||
json.version = "9.5.1";
|
||||
json.version = "11.2.2";
|
||||
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
|
||||
json.repository.url =
|
||||
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
|
||||
|
||||
@@ -53,11 +53,11 @@ async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await exec("npx electron-builder install-app-deps");
|
||||
await exec("yarn electron-builder install-app-deps");
|
||||
}
|
||||
|
||||
await exec(`npm run bundle`);
|
||||
execAsync(`npx`, [`tsc`]);
|
||||
await exec(`yarn run bundle`);
|
||||
execAsync(`yarn`, [`tsc`]);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
@@ -66,7 +66,7 @@ async function onChange(first) {
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
@@ -78,7 +78,7 @@ async function onChange(first) {
|
||||
}
|
||||
|
||||
execAsync(
|
||||
"npx",
|
||||
"yarn",
|
||||
["electron", path.join("build", "electron.js")],
|
||||
true,
|
||||
cleanup
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { app, dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
@@ -58,11 +58,11 @@ export const osIntegrationRouter = t.router({
|
||||
|
||||
customDns: t.procedure.query(() => config.customDns),
|
||||
setCustomDns: t.procedure
|
||||
.input(z.boolean())
|
||||
.input(z.boolean().optional())
|
||||
.mutation(({ input: customDns }) => {
|
||||
if (customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
config.customDns = customDns;
|
||||
config.customDns = !!customDns;
|
||||
}),
|
||||
|
||||
proxyRules: t.procedure.query(() => config.proxyRules),
|
||||
@@ -152,6 +152,10 @@ export const osIntegrationRouter = t.router({
|
||||
await rm(input);
|
||||
}),
|
||||
|
||||
restart: t.procedure.query(() => {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}),
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
@@ -191,7 +195,10 @@ export const osIntegrationRouter = t.router({
|
||||
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
|
||||
if (windowControlsIconColor) {
|
||||
config.windowControlsIconColor = windowControlsIconColor;
|
||||
if (process.platform === "win32")
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
!config.desktopSettings.nativeTitlebar
|
||||
)
|
||||
globalThis.window?.setTitleBarOverlay({
|
||||
symbolColor: windowControlsIconColor
|
||||
});
|
||||
|
||||
143
apps/desktop/src/api/sqlite-kysely.ts
Normal file
143
apps/desktop/src/api/sqlite-kysely.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { Database, Statement } from "better-sqlite3-multiple-ciphers";
|
||||
import type { QueryResult } from "@streetwriters/kysely";
|
||||
|
||||
type SQLiteCompatibleType =
|
||||
| number
|
||||
| string
|
||||
| Uint8Array
|
||||
| Array<number>
|
||||
| bigint
|
||||
| null;
|
||||
|
||||
export class SQLite {
|
||||
sqlite?: Database;
|
||||
initialized = false;
|
||||
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
|
||||
retryCounter: Record<string, number> = {};
|
||||
constructor() {
|
||||
console.log("new sqlite worker");
|
||||
}
|
||||
|
||||
async open(filePath: string) {
|
||||
if (this.sqlite) {
|
||||
console.error("Database is already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sqlite = require("better-sqlite3-multiple-ciphers")(
|
||||
filePath
|
||||
).unsafeMode(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function for preparing SQL statements with caching
|
||||
* to avoid unnecessary computations.
|
||||
*/
|
||||
async prepare(sql: string): Promise<Statement | undefined> {
|
||||
if (!this.sqlite) throw new Error("Database is not initialized.");
|
||||
try {
|
||||
const cached = this.preparedStatements.get(sql);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const prepared = this.sqlite.prepare(sql);
|
||||
if (!prepared) return;
|
||||
|
||||
this.preparedStatements.set(sql, prepared);
|
||||
|
||||
// reset retry count on success
|
||||
this.retryCounter[sql] = 0;
|
||||
return prepared;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
// statement prepare process can be flaky so retry at least 5 times
|
||||
// before giving up.
|
||||
if (this.retryCounter[sql] < 5) {
|
||||
this.retryCounter[sql] = (this.retryCounter[sql] || 0) + 1;
|
||||
console.warn("Failed to prepare statement. Retrying:", sql);
|
||||
return this.prepare(sql);
|
||||
} else this.retryCounter[sql] = 0;
|
||||
|
||||
if (ex instanceof Error) ex.message += ` (query: ${sql})`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
async exec<R>(
|
||||
sql: string,
|
||||
parameters: SQLiteCompatibleType[] = []
|
||||
): Promise<QueryResult<R>> {
|
||||
const prepared = await this.prepare(sql);
|
||||
if (!prepared) return { rows: [] };
|
||||
try {
|
||||
if (prepared.reader) {
|
||||
return {
|
||||
rows: prepared.all(parameters) as R[]
|
||||
};
|
||||
} else {
|
||||
const { changes, lastInsertRowid } = prepared.run(parameters);
|
||||
const numAffectedRows =
|
||||
changes !== undefined && changes !== null && !isNaN(changes)
|
||||
? BigInt(changes)
|
||||
: undefined;
|
||||
return {
|
||||
numAffectedRows,
|
||||
insertId:
|
||||
lastInsertRowid !== undefined && lastInsertRowid !== null
|
||||
? typeof lastInsertRowid === "bigint"
|
||||
? lastInsertRowid
|
||||
: BigInt(lastInsertRowid)
|
||||
: undefined,
|
||||
rows: [] as R[]
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async run<R>(
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
): Promise<QueryResult<R>> {
|
||||
if (!this.sqlite) throw new Error("No database is not opened.");
|
||||
return await this.exec(sql, parameters);
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.sqlite) return;
|
||||
|
||||
this.preparedStatements.clear();
|
||||
this.sqlite.close();
|
||||
this.sqlite = undefined;
|
||||
}
|
||||
|
||||
async delete(filePath: string) {
|
||||
await this.close();
|
||||
await require("fs/promises").rm(filePath, {
|
||||
force: true,
|
||||
maxRetries: 5,
|
||||
retryDelay: 500
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -29,19 +29,37 @@ type Progress = { percent: number };
|
||||
|
||||
const t = initTRPC.create();
|
||||
let cancellationToken: CancellationToken | undefined = undefined;
|
||||
let downloadTimeout: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
export const updaterRouter = t.router({
|
||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||
download: t.procedure.query(async () => {
|
||||
if (cancellationToken) return;
|
||||
cancellationToken = new CancellationToken();
|
||||
await autoUpdater
|
||||
.downloadUpdate(cancellationToken)
|
||||
.finally(() => (cancellationToken = undefined));
|
||||
clearTimeout(downloadTimeout);
|
||||
await new Promise<string[]>((resolve, reject) => {
|
||||
downloadTimeout = setTimeout(async () => {
|
||||
cancellationToken = new CancellationToken();
|
||||
autoUpdater.isUpdaterActive();
|
||||
await autoUpdater
|
||||
.downloadUpdate(cancellationToken)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.finally(() => (cancellationToken = undefined));
|
||||
}, 1000);
|
||||
});
|
||||
}),
|
||||
check: t.procedure.query(async () => {
|
||||
await autoUpdater.checkForUpdates().catch(console.error);
|
||||
if (cancellationToken) return;
|
||||
clearTimeout(downloadTimeout);
|
||||
await new Promise<void>((resolve) => {
|
||||
downloadTimeout = setTimeout(async () => {
|
||||
await autoUpdater
|
||||
.checkForUpdates()
|
||||
.catch(console.error)
|
||||
.finally(resolve);
|
||||
}, 1000);
|
||||
});
|
||||
}),
|
||||
|
||||
toggleAutoUpdates: t.procedure
|
||||
@@ -61,7 +79,18 @@ export const updaterRouter = t.router({
|
||||
"update-not-available"
|
||||
),
|
||||
onAvailable: createSubscription<"update-available", UpdateInfo>(
|
||||
"update-available"
|
||||
"update-available",
|
||||
() => {
|
||||
if (!config.automaticUpdates) return false;
|
||||
autoUpdater.emit("download-progress", {
|
||||
bytesPerSecond: 0,
|
||||
delta: 0,
|
||||
percent: 0,
|
||||
total: 100,
|
||||
transferred: 0
|
||||
});
|
||||
return true;
|
||||
}
|
||||
),
|
||||
onError: createSubscription("error")
|
||||
});
|
||||
@@ -69,10 +98,11 @@ export const updaterRouter = t.router({
|
||||
function createSubscription<
|
||||
TName extends keyof AppUpdaterEvents,
|
||||
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
|
||||
>(eventName: TName) {
|
||||
>(eventName: TName, handler?: (args: TReturnType) => boolean) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<TReturnType>((emit) => {
|
||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||
if (handler?.(args[0])) return;
|
||||
emit.next(args[0]);
|
||||
};
|
||||
autoUpdater.removeAllListeners(eventName);
|
||||
|
||||
@@ -21,3 +21,4 @@ export * from "./constants";
|
||||
export type { AppRouter } from "./api";
|
||||
export { type UpdateInfo } from "builder-util-runtime";
|
||||
export { type DesktopIntegration } from "./utils/config";
|
||||
export { SQLite } from "./api/sqlite-kysely.js";
|
||||
|
||||
@@ -84,22 +84,27 @@ async function createWindow() {
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
}),
|
||||
|
||||
titleBarStyle: "hidden",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
color: "#00000000",
|
||||
symbolColor: config.windowControlsIconColor
|
||||
},
|
||||
trafficLightPosition: {
|
||||
x: 16,
|
||||
y: 12
|
||||
},
|
||||
...(config.desktopSettings.nativeTitlebar
|
||||
? {}
|
||||
: {
|
||||
titleBarStyle: "hidden",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
color: "#00000000",
|
||||
symbolColor: config.windowControlsIconColor
|
||||
},
|
||||
trafficLightPosition: {
|
||||
x: 16,
|
||||
y: 12
|
||||
}
|
||||
}),
|
||||
|
||||
webPreferences: {
|
||||
zoomFactor: config.zoomFactor,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
nodeIntegrationInWorker: true,
|
||||
spellcheck: config.isSpellCheckerEnabled,
|
||||
preload: __dirname + "/preload.js"
|
||||
}
|
||||
|
||||
@@ -20,16 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
||||
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
import type { NNCrypto } from "@notesnook/crypto";
|
||||
// import type { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { platform } from "os";
|
||||
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
var createSQLite3Database: (filename: string) => Database;
|
||||
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
@@ -42,6 +40,5 @@ process.once("loaded", async () => {
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
});
|
||||
|
||||
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
|
||||
// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { NativeImage, nativeImage } from "electron";
|
||||
import path from "path";
|
||||
import { isDevelopment } from "./index";
|
||||
import { parse, ParsedImage } from "icojs";
|
||||
import { ParsedImage, parseICO } from "icojs";
|
||||
import { getSystemTheme } from "./theme";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
@@ -71,7 +71,7 @@ export class AssetManager {
|
||||
`${icon}${prefix}.ico`
|
||||
);
|
||||
const icoBuffer = await readFile(icoPath);
|
||||
const images = await parse(icoBuffer, "image/png");
|
||||
const images = await parseICO(icoBuffer, "image/png");
|
||||
ALL_ICONS.push({ id: icon, images, prefix });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ async function configureAutoUpdater() {
|
||||
autoUpdater.allowDowngrade = false;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
}
|
||||
|
||||
export { configureAutoUpdater };
|
||||
|
||||
@@ -25,7 +25,8 @@ export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
startMinimized: z.boolean().optional(),
|
||||
minimizeToSystemTray: z.boolean().optional(),
|
||||
closeToSystemTray: z.boolean().optional()
|
||||
closeToSystemTray: z.boolean().optional(),
|
||||
nativeTitlebar: z.boolean().optional()
|
||||
});
|
||||
|
||||
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
||||
@@ -35,7 +36,8 @@ export const config = {
|
||||
autoStart: false,
|
||||
startMinimized: false,
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: false
|
||||
closeToSystemTray: false,
|
||||
nativeTitlebar: false
|
||||
},
|
||||
privacyMode: false,
|
||||
isSpellCheckerEnabled: true,
|
||||
|
||||
2
apps/mobile/.gitignore
vendored
2
apps/mobile/.gitignore
vendored
@@ -38,7 +38,7 @@ DerivedData
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
*.hprof
|
||||
ios/.xcode.env.local
|
||||
**/.xcode.env.local
|
||||
# Android/IntelliJ
|
||||
#
|
||||
rn-build-deps/
|
||||
|
||||
@@ -16,13 +16,13 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "react-native-gesture-handler";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
@@ -40,30 +40,25 @@ import { useUserStore } from "./stores/use-user-store";
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
useEffect(() => {
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
SettingsService.onFirstLaunch();
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import { MMKV } from "../database/mmkv";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const WEBSITE_ID = "3c6890ce-8410-49d5-8831-15fb2eb28a21";
|
||||
const baseUrl = "https://analytics.streetwriters.co/api/collect";
|
||||
|
||||
const UA =
|
||||
Platform.OS === "ios"
|
||||
? "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
|
||||
: `
|
||||
Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36`;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Routes} route
|
||||
* @param {Routes[]} conditions
|
||||
* @returns
|
||||
*/
|
||||
async function canUpdateAnalytics(route, conditions = []) {
|
||||
if (!useSettingStore?.getState()?.settings?.telemetry) return false;
|
||||
let eventsList = MMKV.getString("notesnookUserEvents");
|
||||
|
||||
if (eventsList) {
|
||||
eventsList = JSON.parse(eventsList);
|
||||
}
|
||||
|
||||
if (eventsList && eventsList[route]) {
|
||||
console.log("analytics: event already sent", route);
|
||||
return false;
|
||||
}
|
||||
if (route !== "/welcome") {
|
||||
for (let cond of conditions) {
|
||||
if (!eventsList || !eventsList[cond]) {
|
||||
console.log("analytics: conditions not met for event", route, cond);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("analytics: will send event", route);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveAnalytics(route, value = true) {
|
||||
let eventsList = MMKV.getString("notesnookUserEvents");
|
||||
if (eventsList) {
|
||||
eventsList = JSON.parse(eventsList);
|
||||
} else {
|
||||
eventsList = {};
|
||||
}
|
||||
eventsList[route] = value;
|
||||
MMKV.setString("notesnookUserEvents", JSON.stringify(eventsList));
|
||||
}
|
||||
|
||||
/**
|
||||
*@typedef {"/welcome" | "/home" | "/signup" | "/first-note" | "/account-created" | "/pro-sheet" | "/pro-plans" | "/iap-native" | "/pro-screen" | "/editor" | "/editor-toolbar" | "/properties" | "/sidemenu"} Routes
|
||||
* @param {Routes} prevRoute
|
||||
* @param {Routes} route
|
||||
* @param {Routes[]} conditions
|
||||
* @param {boolean} once
|
||||
* @returns
|
||||
*/
|
||||
|
||||
async function pageView(
|
||||
route,
|
||||
prevRoute = "",
|
||||
conditions = ["/welcome"],
|
||||
once = true
|
||||
) {
|
||||
if (__DEV__) return;
|
||||
if (!(await canUpdateAnalytics(route, conditions)) && once) return;
|
||||
let body = {
|
||||
payload: {
|
||||
website: WEBSITE_ID,
|
||||
url: `notesnook-${Platform.OS}${prevRoute}${route}`,
|
||||
referrer: `https://notesnook.com/notesnook-${Platform.OS}${prevRoute}`,
|
||||
hostname: `notesnook-${Platform.OS}`,
|
||||
language: "en-US",
|
||||
screen: "1920x1080"
|
||||
},
|
||||
type: "pageview"
|
||||
};
|
||||
|
||||
try {
|
||||
let response = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": UA
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
console.log("analytics: event sent", route);
|
||||
await saveAnalytics(route);
|
||||
return await response.text();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEvent(type, value, once = true) {
|
||||
if (__DEV__) return;
|
||||
if (!(await canUpdateAnalytics(type)) && once) return;
|
||||
let body = {
|
||||
payload: {
|
||||
website: WEBSITE_ID,
|
||||
url: "/",
|
||||
event_type: type,
|
||||
event_value: value,
|
||||
hostname: "notesnook-android-app",
|
||||
language: "en-US",
|
||||
screen: "1920x1080"
|
||||
},
|
||||
type: "event"
|
||||
};
|
||||
|
||||
try {
|
||||
let response = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": UA
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
console.log(response.status);
|
||||
return await response.text();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
sendEvent,
|
||||
pageView,
|
||||
saveAnalytics
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
@@ -43,9 +44,8 @@ const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
|
||||
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
|
||||
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
@@ -117,9 +117,7 @@ export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
@@ -135,10 +133,8 @@ export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const decrypted = await decrypt(
|
||||
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
|
||||
appLockCipher
|
||||
);
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
@@ -214,7 +210,7 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
@@ -227,6 +223,7 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
|
||||
return DB_KEY;
|
||||
} catch (e) {
|
||||
ToastManager.error(e, "Error getting database key");
|
||||
console.log(e, "error");
|
||||
DatabaseLogger.error(e);
|
||||
return null;
|
||||
@@ -239,7 +236,7 @@ export async function deriveCryptoKey(data) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
@@ -264,13 +261,12 @@ export async function getCryptoKey(_name) {
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey()
|
||||
key: await getDatabaseKey(),
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
DatabaseLogger.info("User key decrypted: ", !!key);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
console.log("getCryptoKey", e);
|
||||
|
||||
@@ -18,39 +18,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "./logger";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
import { logger as dbLogger } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.43.5:5264",
|
||||
// AUTH_HOST: "http://192.168.43.5:8264",
|
||||
// SSE_HOST: "http://192.168.43.5:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
|
||||
// ISSUES_HOST: "http://192.168.43.5:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
@@ -59,6 +41,15 @@ export async function setupDatabase(password) {
|
||||
|
||||
console.log("Opening database with key:", !!key);
|
||||
|
||||
database.host({
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
...(SettingsService.getProperty("serverUrls") || {})
|
||||
});
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
@@ -85,4 +76,10 @@ export async function setupDatabase(password) {
|
||||
}
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
let DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
|
||||
const setLogger = () => {
|
||||
DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
};
|
||||
|
||||
export { DatabaseLogger, setLogger };
|
||||
|
||||
@@ -16,14 +16,34 @@ 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 { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import { initialize } from "@notesnook/core/dist/logger";
|
||||
import { KV } from "./storage";
|
||||
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
import { initialize } from "@notesnook/core";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import { Platform } from "react-native";
|
||||
import { setLogger } from ".";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
|
||||
initialize(new KV(LoggerStorage));
|
||||
let loggerLoaded = false;
|
||||
const initializeLogger = async () => {
|
||||
if (loggerLoaded) return;
|
||||
await initialize({
|
||||
dialect: (name) => ({
|
||||
createDriver: () => {
|
||||
return new RNSqliteDriver({ async: true, dbName: name });
|
||||
},
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
}),
|
||||
tempStore: "memory",
|
||||
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL"
|
||||
});
|
||||
setLogger();
|
||||
loggerLoaded = true;
|
||||
};
|
||||
|
||||
export { LoggerStorage };
|
||||
export { initializeLogger };
|
||||
|
||||
@@ -17,8 +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 type { DatabaseConnection, Driver, QueryResult } from "kysely";
|
||||
import { CompiledQuery } from "kysely";
|
||||
import type {
|
||||
DatabaseConnection,
|
||||
Driver,
|
||||
QueryResult
|
||||
} from "@streetwriters/kysely";
|
||||
import { CompiledQuery } from "@streetwriters/kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
|
||||
@@ -21,15 +21,15 @@ import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
deriveCryptoKey,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
generateCryptoKey,
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
removeCryptoKey,
|
||||
decryptMulti,
|
||||
encryptMulti
|
||||
removeCryptoKey
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import { Dimensions, Image, Platform } from "react-native";
|
||||
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
console.log("hello world");
|
||||
/**
|
||||
* Scale down & compress images to screen width
|
||||
* for loading in editor.
|
||||
|
||||
@@ -18,24 +18,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
import { getFileNameWithExtension } from "@notesnook/core";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -58,15 +54,10 @@ export async function downloadAllAttachments() {
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(
|
||||
attachments,
|
||||
onProgress,
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
export async function downloadAttachments(attachments) {
|
||||
await createCacheDir();
|
||||
if (!attachments || !attachments.length) return;
|
||||
const result = new Map();
|
||||
const groupId = `download-all-${Date.now()}`;
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
@@ -90,92 +81,117 @@ export async function downloadAttachments(
|
||||
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
const isCancelled = () => {
|
||||
if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
current: 0,
|
||||
total: 0,
|
||||
success: false,
|
||||
message: "Download cancelled",
|
||||
canceled: true
|
||||
});
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
if (isCancelled()) return;
|
||||
let attachment = await db.attachments.attachment(attachments[i]);
|
||||
const hash = attachment.hash;
|
||||
try {
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
onProgress?.(
|
||||
i + 1 / attachments.length,
|
||||
`Downloading attachments (${i + 1}/${
|
||||
attachments.length
|
||||
})... Please wait`
|
||||
);
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: i + 1,
|
||||
total: attachments.length,
|
||||
filename: attachment.hash
|
||||
});
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
});
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCancelled()) return;
|
||||
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Success,
|
||||
attachment: attachment
|
||||
});
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
ToastManager.error(e, "Error downloading attachment");
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (result?.size) {
|
||||
let sub;
|
||||
try {
|
||||
onProgress?.(0, `Zipping... Please wait`);
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
onProgress(
|
||||
progress,
|
||||
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
|
||||
);
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: 0,
|
||||
total: 0,
|
||||
success: true
|
||||
});
|
||||
|
||||
if (isCancelled()) return;
|
||||
|
||||
let sub;
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 1,
|
||||
message: "Saving zip file... Please wait",
|
||||
groupId
|
||||
});
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
current: progress,
|
||||
total: 1,
|
||||
message: `Saving zip file (${(progress * 100).toFixed(
|
||||
1
|
||||
)}%)... Please wait`
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
onProgress(1, `Saving zip file... Please wait`);
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
|
||||
onProgress?.(1, `Done`);
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 0,
|
||||
message: undefined,
|
||||
success: true,
|
||||
groupId
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 0,
|
||||
message: undefined,
|
||||
success: false,
|
||||
groupId
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
@@ -211,7 +227,11 @@ export default async function downloadAttachment(
|
||||
try {
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(options.groupId || attachment.hash, attachment.hash);
|
||||
.downloadFile(
|
||||
options.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
return;
|
||||
@@ -259,15 +279,6 @@ export default async function downloadAttachment(
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
attachment.dateUploaded &&
|
||||
!isImage(attachment.mimeType) &&
|
||||
!isDocument(attachment.mimeType)
|
||||
) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(console.log);
|
||||
}
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
@@ -287,7 +298,6 @@ export default async function downloadAttachment(
|
||||
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
@@ -296,6 +306,7 @@ export default async function downloadAttachment(
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||
.catch(console.log);
|
||||
}
|
||||
DatabaseLogger.error(e);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
|
||||
@@ -17,64 +17,127 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import hosts from "@notesnook/core/dist/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { cacheDir, fileCheck } from "./utils";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
||||
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Downloading", filename);
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
let { url, headers } = data;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
let { url, headers, chunkSize } = requestOptions;
|
||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
let originalFilePath = `${cacheDir}/${filename}`;
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
console.log("Exists already", filename);
|
||||
DatabaseLogger.log(`File Exists already: ${filename}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
const error = `Uploaded file verification failed. (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})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
|
||||
if (attachment && attachment.size !== decryptedLength) {
|
||||
const error = `File length mismatch. Expected ${attachment.size} but got ${decryptedLength} bytes. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers
|
||||
});
|
||||
if (!res.ok)
|
||||
|
||||
if (!res.ok) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error(`${res.status}: Unable to resolve download url`);
|
||||
}
|
||||
|
||||
const downloadUrl = await res.text();
|
||||
|
||||
if (!downloadUrl) throw new Error("Unable to resolve download url");
|
||||
let totalSize = 0;
|
||||
console.log("Download starting");
|
||||
if (!downloadUrl) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error("Unable to resolve download url");
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
let request = RNFetchBlob.config({
|
||||
path: path,
|
||||
IOSBackgroundTask: true
|
||||
path: tempFilePath,
|
||||
IOSBackgroundTask: true,
|
||||
overwrite: true
|
||||
})
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.progress((recieved, total) => {
|
||||
.progress(async (recieved, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(0, total, filename, recieved, "download");
|
||||
totalSize = total;
|
||||
console.log("downloading: ", recieved, total);
|
||||
|
||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
||||
};
|
||||
|
||||
let response = await request;
|
||||
await fileCheck(response, totalSize);
|
||||
console.log(response.info().headers);
|
||||
|
||||
const contentType =
|
||||
response.info().headers?.["content-type"] ||
|
||||
response.info().headers?.["Content-Type"];
|
||||
|
||||
if (contentType === "application/xml") {
|
||||
const error = parseS3Error(await response.text());
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
let status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
if (exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
||||
|
||||
if (!(await exists(filename))) {
|
||||
throw new Error("File size mismatch");
|
||||
}
|
||||
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled") {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
ToastManager.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
@@ -90,28 +153,13 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
console.log("Download file error:", e, url, headers);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
});
|
||||
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,13 +167,19 @@ export async function checkAttachment(hash) {
|
||||
const internetState = await NetInfo.fetch();
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
if (!isInternetReachable) return { success: true };
|
||||
if (!isInternetReachable) return;
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) return { failed: "Attachment not found." };
|
||||
|
||||
try {
|
||||
const size = await getUploadedFileSize(hash);
|
||||
if (size <= 0) return { failed: "File length is 0." };
|
||||
console.log("File Size", size);
|
||||
if (size === -1) return { success: true };
|
||||
|
||||
if (size === 0)
|
||||
return {
|
||||
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
} catch (e) {
|
||||
return { failed: e?.message };
|
||||
}
|
||||
|
||||
@@ -17,17 +17,22 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
|
||||
import { checkAttachment, downloadFile } from "./download";
|
||||
import {
|
||||
clearFileStorage,
|
||||
deleteFile,
|
||||
exists,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64
|
||||
hashBase64,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable } from "./utils";
|
||||
import { cancelable, getUploadedFileSize } from "./utils";
|
||||
|
||||
export default {
|
||||
readEncrypted,
|
||||
@@ -39,5 +44,10 @@ export default {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
checkAttachment
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
};
|
||||
|
||||
@@ -20,13 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { db } from "../database";
|
||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
console.log("Read encrypted file...");
|
||||
DatabaseLogger.log("Read encrypted file...");
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
@@ -34,8 +35,6 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
console.log("decrypting....");
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
@@ -45,12 +44,13 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
},
|
||||
cipherData.outputType === "base64" ? "base64" : "text"
|
||||
);
|
||||
console.log("file decrypted...", attachment?.mimeType);
|
||||
|
||||
DatabaseLogger.log("File decrypted...");
|
||||
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
console.log("readEncrypted", e);
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export async function clearFileStorage() {
|
||||
export async function createCacheDir() {
|
||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||
console.log("Cache directory created");
|
||||
DatabaseLogger.log("Cache directory created");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,35 @@ export async function migrateFilesFromCache() {
|
||||
}
|
||||
}
|
||||
|
||||
const ABYTES = 17;
|
||||
export async function clearCache() {
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
|
||||
await createCacheDir();
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
@@ -188,6 +216,9 @@ export async function exists(filename) {
|
||||
);
|
||||
|
||||
if (stat.size !== expectedFileSize) {
|
||||
DatabaseLogger.log(
|
||||
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
|
||||
);
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(console.log);
|
||||
@@ -198,3 +229,29 @@ export async function exists(filename) {
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files) {
|
||||
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));
|
||||
}
|
||||
|
||||
return missingFiles;
|
||||
}
|
||||
|
||||
export async function getCacheSize() {
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
console.log("Total files", stat.length);
|
||||
stat.forEach((s) => {
|
||||
total += parseInt(s.size);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -17,29 +17,47 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir } from "./io";
|
||||
import { cacheDir, getUploadedFileSize } from "./utils";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) return false;
|
||||
let { url, headers } = requestOptions;
|
||||
await createCacheDir();
|
||||
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);
|
||||
}
|
||||
|
||||
if (uploadedFileSize !== 0) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
|
||||
const uploadUrl = await res.text();
|
||||
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
|
||||
|
||||
const uploadUrl = res.ok ? await res.text() : await res.json();
|
||||
|
||||
if (typeof uploadUrl !== "string") {
|
||||
throw new Error(
|
||||
uploadUrl.error || "Unable to resolve attachment upload url."
|
||||
);
|
||||
}
|
||||
|
||||
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
@@ -50,7 +68,15 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
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({
|
||||
@@ -89,9 +115,6 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
);
|
||||
let attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
} else {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
|
||||
throw new Error(
|
||||
@@ -104,8 +127,10 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
return result;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
|
||||
DatabaseLogger.error(e);
|
||||
ToastManager.error(e, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,13 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { hosts } from "@notesnook/core";
|
||||
|
||||
export const ABYTES = 17;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
export const cacheDir =
|
||||
@@ -34,24 +37,36 @@ export function getRandomId(prefix) {
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
export function extractValueFromXmlTag(code, xml) {
|
||||
if (!xml.includes(code)) return `Unknown ${code}`;
|
||||
return xml.slice(
|
||||
xml.indexOf(`<${code}>`) + code.length + 2,
|
||||
xml.indexOf(`</${code}>`)
|
||||
);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
|
||||
export async function fileCheck(response, totalSize) {
|
||||
if (totalSize < 1000) {
|
||||
let text = await response.text();
|
||||
if (text.startsWith("<?xml")) {
|
||||
let errorJson = {
|
||||
Code: extractValueFromXmlTag("Code", text),
|
||||
Message: extractValueFromXmlTag("Message", text)
|
||||
};
|
||||
throw new Error(`${errorJson.Code}: ${errorJson.Message}`);
|
||||
const error = {
|
||||
Code: "UNKNOWN",
|
||||
Message: xml
|
||||
};
|
||||
|
||||
try {
|
||||
if (!xml) return error;
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
|
||||
const ErrorElement = doc.getElementsByTagName("Error")[0];
|
||||
if (!ErrorElement) return error;
|
||||
|
||||
for (const child of ErrorElement.children) {
|
||||
if (
|
||||
child.textContent &&
|
||||
(child.tagName === "Code" || child.tagName === "Message")
|
||||
)
|
||||
error[child.tagName] = child.textContent;
|
||||
}
|
||||
return error;
|
||||
} catch (e) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,12 +74,11 @@ export function cancelable(operation) {
|
||||
const cancelToken = {
|
||||
cancel: () => {}
|
||||
};
|
||||
return (filename, { url, headers }) => {
|
||||
return (filename, requestOptions) => {
|
||||
return {
|
||||
execute: () => operation(filename, { url, headers }, cancelToken),
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
cancel: async () => {
|
||||
await cancelToken.cancel();
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -91,3 +105,21 @@ export async function releasePermissions(path) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { Linking, View } from "react-native";
|
||||
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import Sync from "../../services/sync";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eCloseAnnouncementDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { PricingPlans } from "../premium/pricing-plans";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import { Button } from "../ui/button";
|
||||
import { allowedOnPlatform, getStyle } from "./functions";
|
||||
|
||||
@@ -56,13 +54,6 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else if (item.type === "force-sync") {
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Progress.present();
|
||||
Sync.run("global", true, true, () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -18,10 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { AppStateStatus, Platform, TextInput, View } from "react-native";
|
||||
import {
|
||||
AppStateStatus,
|
||||
Platform,
|
||||
TextInput,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
//@ts-ignore
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { DatabaseLogger } from "../../common/database";
|
||||
import {
|
||||
decrypt,
|
||||
@@ -34,17 +39,20 @@ import {
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
|
||||
const getUser = () => {
|
||||
const user = MMKV.getString("user");
|
||||
@@ -85,6 +93,7 @@ const AppLockedOverlay = () => {
|
||||
const appState = useAppState();
|
||||
const lastAppState = useRef<AppStateStatus>(appState);
|
||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||
const { height } = useWindowDimensions();
|
||||
const keyboardType = useSettingStore(
|
||||
(state) => state.settings.applockKeyboardType
|
||||
);
|
||||
@@ -119,7 +128,6 @@ const AppLockedOverlay = () => {
|
||||
);
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
}
|
||||
biometricUnlockAwaitingUserInput.current = false;
|
||||
@@ -146,8 +154,13 @@ const AppLockedOverlay = () => {
|
||||
}
|
||||
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -155,10 +168,6 @@ const AppLockedOverlay = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (appState === "active") {
|
||||
enabled(false);
|
||||
}
|
||||
|
||||
const prevState = lastAppState.current;
|
||||
lastAppState.current = appState;
|
||||
|
||||
@@ -184,31 +193,28 @@ const AppLockedOverlay = () => {
|
||||
DatabaseLogger.info("Biometric unlock request");
|
||||
onUnlockAppRequested();
|
||||
}
|
||||
|
||||
enabled(false);
|
||||
} else {
|
||||
SettingsService.appEnteredBackground();
|
||||
|
||||
if (
|
||||
SettingsService.get().privacyScreen ||
|
||||
SettingsService.getProperty("appLockEnabled")
|
||||
) {
|
||||
enabled(true);
|
||||
}
|
||||
}
|
||||
}, [appState, onUnlockAppRequested, appLocked]);
|
||||
|
||||
return appLocked ? (
|
||||
<View
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
zIndex: 999
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
justifyContent: "center",
|
||||
minHeight: height
|
||||
}}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -321,7 +327,7 @@ const AppLockedOverlay = () => {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ import filesystem from "../../common/filesystem";
|
||||
import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import picker from "../../screens/editor/tiptap/picker";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
@@ -49,13 +51,12 @@ import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { openNote } from "../list-items/note/wrapper";
|
||||
import { DateMeta } from "../properties/date-meta";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { Notice } from "../ui/notice";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
@@ -87,7 +88,7 @@ const Actions = ({
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
}
|
||||
downloadAttachment(attachment.hash, false);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
fwdRef.current?.hide();
|
||||
},
|
||||
icon: "download"
|
||||
},
|
||||
@@ -117,12 +118,14 @@ const Actions = ({
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
});
|
||||
const res = await filesystem.checkAttachment(attachment.hash);
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, result.failed);
|
||||
setFailed(result.failed);
|
||||
ToastManager.show({
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
heading: "File check failed with error: " + result.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -146,9 +149,8 @@ const Actions = ({
|
||||
},
|
||||
{
|
||||
name: "Rename",
|
||||
onPress: () => {
|
||||
onPress: async () => {
|
||||
presentDialog({
|
||||
context: contextId as any,
|
||||
input: true,
|
||||
title: "Rename file",
|
||||
paragraph: "Enter a new name for the file",
|
||||
@@ -216,8 +218,10 @@ const Actions = ({
|
||||
style={{
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
keyboardShouldPersistTaps="never"
|
||||
>
|
||||
<Dialog context={contextId} />
|
||||
<SheetProvider context={contextId} />
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
@@ -238,36 +242,20 @@ const Actions = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 12
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 6,
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.type}
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{attachment.mimeType}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
|
||||
{notes.length ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{notes.length} note
|
||||
{notes.length > 1 ? "s" : ""}
|
||||
</Paragraph>
|
||||
@@ -366,7 +354,7 @@ const Actions = ({
|
||||
{failed ? (
|
||||
<Notice
|
||||
type="alert"
|
||||
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`}
|
||||
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
|
||||
size="small"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
@@ -44,7 +43,8 @@ export const AttachmentItem = ({
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading,
|
||||
context
|
||||
context,
|
||||
errorOnly
|
||||
}: {
|
||||
id: string | number;
|
||||
attachments?: VirtualizedGrouping<Attachment>;
|
||||
@@ -53,9 +53,9 @@ export const AttachmentItem = ({
|
||||
pressable?: boolean;
|
||||
hideWhenNotDownloading?: boolean;
|
||||
context?: string;
|
||||
errorOnly?: boolean;
|
||||
}) => {
|
||||
const [attachment] = useDBItem(id, "attachment", attachments);
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
attachment,
|
||||
@@ -67,8 +67,7 @@ export const AttachmentItem = ({
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
return errorOnly && attachment && !attachment?.failed ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
@@ -78,8 +77,6 @@ export const AttachmentItem = ({
|
||||
justifyContent: "space-between",
|
||||
padding: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
minHeight: 45
|
||||
}}
|
||||
>
|
||||
@@ -89,41 +86,46 @@ export const AttachmentItem = ({
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: -5
|
||||
marginLeft: -5,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.border,
|
||||
paddingHorizontal: 2,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 5
|
||||
}}
|
||||
>
|
||||
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
|
||||
|
||||
<Paragraph
|
||||
adjustsFontSizeToFit
|
||||
size={6}
|
||||
color={colors.static.white}
|
||||
size={8}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
position: "absolute"
|
||||
maxWidth: 30
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{getFileExtension(attachment.filename).toUpperCase()}
|
||||
{getFileExtension(attachment.filename).toUpperCase() ||
|
||||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 10
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.sm - 1}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginBottom: 2.5
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
numberOfLines={1}
|
||||
lineBreakMode="middle"
|
||||
@@ -133,11 +135,8 @@ export const AttachmentItem = ({
|
||||
</Paragraph>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{formatBytes(attachment.size)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
|
||||
File size: {formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import {
|
||||
PresentSheetOptions,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
|
||||
const DownloadAttachments = ({
|
||||
close,
|
||||
attachments,
|
||||
isNote,
|
||||
update
|
||||
}: {
|
||||
attachments: VirtualizedGrouping<Attachment>;
|
||||
close?: ((ctx?: string | undefined) => void) | undefined;
|
||||
isNote?: boolean;
|
||||
update?: (props: PresentSheetOptions) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
value: 0,
|
||||
statusText: "Download started... Please wait"
|
||||
});
|
||||
const [result, setResult] = useState(new Map());
|
||||
const canceled = useRef(false);
|
||||
const groupId = useRef<string>();
|
||||
|
||||
const onDownload = async () => {
|
||||
update?.({
|
||||
disableClosing: true
|
||||
} as PresentSheetOptions);
|
||||
setDownloading(true);
|
||||
canceled.current = false;
|
||||
groupId.current = Date.now().toString();
|
||||
const result = await downloadAttachments(
|
||||
await attachments.ids(),
|
||||
(progress: number, statusText: string) =>
|
||||
setProgress({ value: progress, statusText }),
|
||||
canceled,
|
||||
groupId.current
|
||||
);
|
||||
if (canceled.current) return;
|
||||
setResult(result || new Map());
|
||||
setDownloading(false);
|
||||
update?.({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
update?.({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
canceled.current = true;
|
||||
if (!groupId.current) return;
|
||||
console.log(groupId.current, "canceling groupId downloads");
|
||||
await db.fs().cancel(groupId.current);
|
||||
setDownloading(false);
|
||||
setResult(new Map());
|
||||
groupId.current = undefined;
|
||||
};
|
||||
|
||||
const failedResults = () => {
|
||||
const results = [];
|
||||
for (const value of result.values()) {
|
||||
if (value.status === 0) results.push(value.attachment);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
function getResultText() {
|
||||
const downloadedAttachmentsCount =
|
||||
attachments?.placeholders?.length - failedResults().length;
|
||||
if (downloadedAttachmentsCount === 0)
|
||||
return "Failed to download all attachments";
|
||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||
attachments?.placeholders.length
|
||||
} attachments as a zip file at ${
|
||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||
}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading>
|
||||
{downloading
|
||||
? "Downloading attachments"
|
||||
: result?.size
|
||||
? "Downloaded attachments"
|
||||
: "Download attachments"}
|
||||
</Heading>
|
||||
|
||||
{downloading ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{progress.statusText}
|
||||
</Paragraph>
|
||||
) : result?.size ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{getResultText()}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Are you sure you want to download all attachments
|
||||
{isNote ? " of this note?" : "?"}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{downloading ? (
|
||||
<View
|
||||
style={{
|
||||
width: 200,
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={
|
||||
progress.value
|
||||
? progress.value / attachments.placeholders?.length
|
||||
: 0
|
||||
}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
width: "100%",
|
||||
minHeight: 60,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginVertical: 12
|
||||
}}
|
||||
data={downloading ? attachments.placeholders : undefined}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 60
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
No downloads in progress.
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(index) => "attachment_download" + index}
|
||||
renderItem={({ index }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
id={index}
|
||||
setAttachments={() => {}}
|
||||
pressable={false}
|
||||
hideWhenNotDownloading={true}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{result?.size ? (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="accent"
|
||||
title="Done"
|
||||
/>
|
||||
) : !downloading ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
marginTop: 20
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="secondary"
|
||||
title="No"
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginLeft: 5
|
||||
}}
|
||||
onPress={onDownload}
|
||||
type="accent"
|
||||
title="Yes"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={cancel}
|
||||
type="error"
|
||||
title="Cancel"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
DownloadAttachments.present = (
|
||||
context: string,
|
||||
attachments: VirtualizedGrouping<Attachment>,
|
||||
isNote?: boolean
|
||||
) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close, update) => (
|
||||
<DownloadAttachments
|
||||
close={close}
|
||||
attachments={attachments}
|
||||
isNote={isNote}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default DownloadAttachments;
|
||||
@@ -19,21 +19,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {
|
||||
Attachment,
|
||||
FilteredSelector,
|
||||
Note,
|
||||
SortOptions,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import create, { State } from "zustand";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Header } from "../header";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
@@ -42,22 +49,105 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import DownloadAttachments from "./download-attachments";
|
||||
|
||||
const DEFAULT_SORTING: SortOptions = {
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
};
|
||||
|
||||
export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
type RecheckerProgress = {
|
||||
failed: number;
|
||||
passed: number;
|
||||
isWorking: boolean;
|
||||
shown: boolean;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
interface RecheckerState extends State {
|
||||
progress: {
|
||||
[key: string]: RecheckerProgress;
|
||||
};
|
||||
setProgress: (key: string, progress: RecheckerProgress) => void;
|
||||
currentFilter: string;
|
||||
}
|
||||
|
||||
const useRechecker = create<RecheckerState>((set) => ({
|
||||
progress: {
|
||||
all: {
|
||||
failed: 0,
|
||||
passed: 0,
|
||||
isWorking: false,
|
||||
shown: false,
|
||||
filter: "all"
|
||||
}
|
||||
},
|
||||
currentFilter: "all",
|
||||
setProgress: (key: string, progress: RecheckerProgress) => {
|
||||
set((state) => ({
|
||||
progress: {
|
||||
...state.progress,
|
||||
[key]: progress
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
const attachmentTypes = [
|
||||
{
|
||||
title: "All",
|
||||
filterBy: "all"
|
||||
},
|
||||
{
|
||||
title: "Images",
|
||||
filterBy: "images"
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: "Video",
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
filterBy: "audio"
|
||||
},
|
||||
{
|
||||
title: "Orphaned",
|
||||
filterBy: "orphaned"
|
||||
},
|
||||
{
|
||||
title: "Errors",
|
||||
filterBy: "errors"
|
||||
}
|
||||
];
|
||||
|
||||
export const AttachmentDialog = ({
|
||||
note,
|
||||
isSheet
|
||||
}: {
|
||||
note?: Note;
|
||||
isSheet: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const [attachments, setAttachments] =
|
||||
useState<VirtualizedGrouping<Attachment>>();
|
||||
const attachmentSearchValue = useRef<string>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const searchTimer = useRef<NodeJS.Timeout>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
const rechecker = useRechecker((state) =>
|
||||
note ? state.progress[note.id] || {} : state.progress.all
|
||||
);
|
||||
const currentFilterRef = useRef(currentFilter);
|
||||
currentFilterRef.current = currentFilter;
|
||||
|
||||
useEffect(() => {
|
||||
useRechecker.setState({
|
||||
currentFilter: currentFilter
|
||||
});
|
||||
}, [currentFilter]);
|
||||
|
||||
const refresh = React.useCallback(() => {
|
||||
if (note) {
|
||||
@@ -68,6 +158,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => {
|
||||
setLoading(false);
|
||||
setAttachments(attachments);
|
||||
});
|
||||
} else {
|
||||
@@ -76,7 +167,10 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => setAttachments(attachments));
|
||||
.then((attachments) => {
|
||||
setAttachments(attachments);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [note]);
|
||||
|
||||
@@ -108,52 +202,70 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
setAttachments={async () => {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}}
|
||||
errorOnly={currentFilter === "errors"}
|
||||
attachments={attachments}
|
||||
id={index}
|
||||
context="attachments-list"
|
||||
context="global"
|
||||
/>
|
||||
);
|
||||
|
||||
const onCheck = async () => {
|
||||
if (!attachments) return;
|
||||
setLoading(true);
|
||||
const getState = () =>
|
||||
useRechecker.getState().progress[note?.id || "all"] || {};
|
||||
const setState = (state: Partial<RecheckerProgress>) =>
|
||||
useRechecker.getState().setProgress(note?.id || "all", {
|
||||
...getState(),
|
||||
...state
|
||||
});
|
||||
|
||||
for (let i = 0; i < attachments.placeholders.length; i++) {
|
||||
if (!attachments || getState().isWorking) return;
|
||||
setState({
|
||||
isWorking: true,
|
||||
failed: 0,
|
||||
passed: 0,
|
||||
shown: true,
|
||||
filter: currentFilterRef.current
|
||||
});
|
||||
const filter = currentFilterRef.current;
|
||||
const filteredAttachments = await filterAttachments(
|
||||
currentFilterRef.current
|
||||
);
|
||||
|
||||
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
|
||||
if (!getState().isWorking) {
|
||||
ToastManager.show({
|
||||
message: "Attachment recheck cancelled",
|
||||
type: "info",
|
||||
context: isSheet ? "local" : "global"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const attachment = (await attachments.item(i))?.item;
|
||||
if (currentFilter == "errors" && !attachment?.failed) continue;
|
||||
|
||||
if (!attachment) continue;
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
if (result.failed) {
|
||||
setState({
|
||||
failed: getState().failed + 1
|
||||
});
|
||||
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
||||
} else {
|
||||
await db.attachments.markAsFailed(attachment.id, undefined);
|
||||
setState({
|
||||
passed: getState().passed + 1
|
||||
});
|
||||
await db.attachments.markAsFailed(attachment.id);
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const attachmentTypes = [
|
||||
{
|
||||
title: "All",
|
||||
filterBy: "all"
|
||||
},
|
||||
{
|
||||
title: "Images",
|
||||
filterBy: "images"
|
||||
},
|
||||
{
|
||||
title: "Documents",
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: "Video",
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
filterBy: "audio"
|
||||
if (filter === useRechecker.getState().currentFilter) {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}
|
||||
];
|
||||
|
||||
setState({
|
||||
isWorking: false
|
||||
});
|
||||
};
|
||||
|
||||
const filterAttachments = async (type: string) => {
|
||||
let items: FilteredSelector<Attachment> = db.attachments.all;
|
||||
@@ -177,12 +289,20 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
case "audio":
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "audio")
|
||||
: db.attachments.all;
|
||||
: db.attachments.audios;
|
||||
break;
|
||||
case "documents":
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "documents")
|
||||
: db.attachments.documents;
|
||||
break;
|
||||
case "orphaned":
|
||||
items = db.attachments.orphaned;
|
||||
break;
|
||||
case "errors":
|
||||
items = items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: db.attachments.all;
|
||||
}
|
||||
|
||||
return await items.sorted({
|
||||
@@ -192,39 +312,59 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
height: height * 0.85
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="attachments-list" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading>Attachments</Heading>
|
||||
|
||||
<>
|
||||
{isSheet ? (
|
||||
<>
|
||||
<SheetProvider context="attachments-list" />
|
||||
<Dialog context="local" />
|
||||
</>
|
||||
) : null}
|
||||
{!isSheet ? (
|
||||
<Header
|
||||
title="Manage attachments"
|
||||
renderedInRoute="SettingsGroup"
|
||||
canGoBack
|
||||
headerRightButtons={[
|
||||
{
|
||||
onPress() {
|
||||
onCheck();
|
||||
},
|
||||
title: "Recheck all"
|
||||
},
|
||||
{
|
||||
onPress() {
|
||||
if (!attachments) return;
|
||||
presentDialog({
|
||||
title: `Download ${attachments.placeholders.length} attachments`,
|
||||
paragraph:
|
||||
"Are you sure you want to download all attachments?",
|
||||
positiveText: "Download",
|
||||
positivePress: async () => {
|
||||
downloadAttachments(await attachments.ids());
|
||||
},
|
||||
negativeText: "Cancel"
|
||||
});
|
||||
},
|
||||
title: "Download all"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
) : (
|
||||
<Heading>Attachments</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="check-all"
|
||||
style={{
|
||||
@@ -236,125 +376,243 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
size={SIZE.lg}
|
||||
onPress={onCheck}
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
name="download"
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
DownloadAttachments.present(
|
||||
"attachments-list",
|
||||
attachments,
|
||||
!!note
|
||||
);
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
}}
|
||||
/>
|
||||
|
||||
<View>
|
||||
<ScrollView
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minWidth: "100%",
|
||||
height: 50
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{attachmentTypes.map((item) => (
|
||||
<Button
|
||||
type={
|
||||
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
|
||||
}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
<IconButton
|
||||
name="download"
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
flexGrow: 1,
|
||||
borderBottomColor:
|
||||
currentFilter !== item.filterBy
|
||||
? "transparent"
|
||||
: colors.primary.accent
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
onPress={async () => {
|
||||
setCurrentFilter(item.filterBy);
|
||||
setAttachments(await filterAttachments(item.filterBy));
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
presentDialog({
|
||||
title: `Download ${attachments.placeholders.length} attachments`,
|
||||
paragraph:
|
||||
"Are you sure you want to download all attachments?",
|
||||
context: "local",
|
||||
positiveText: "Download",
|
||||
positivePress: async () => {
|
||||
downloadAttachments(await attachments.ids());
|
||||
},
|
||||
negativeText: "Cancel"
|
||||
});
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<FlashList
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon name="attachment" size={60} color={colors.secondary.icon} />
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={attachments?.placeholders}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
<View
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="shield-key-outline"
|
||||
size={SIZE.xs}
|
||||
color={colors.primary.icon}
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
}}
|
||||
/>
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
|
||||
{rechecker.shown ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
gap: 12,
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{rechecker.isWorking ? (
|
||||
<ActivityIndicator color={colors.primary.accent} />
|
||||
) : (
|
||||
<Icon
|
||||
name="file-check"
|
||||
size={30}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View>
|
||||
<Paragraph>
|
||||
{rechecker.isWorking
|
||||
? note
|
||||
? `Checking ${rechecker.filter.toLowerCase()} note attachments`
|
||||
: `Checking ${rechecker.filter.toLowerCase()} attachments`
|
||||
: "Attachments recheck complete"}
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
{`${rechecker.isWorking ? "Please wait... " : ""}Passed: ${
|
||||
rechecker.passed
|
||||
}, Failed: ${rechecker.failed}`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
type={rechecker.isWorking ? "errorShade" : "plain"}
|
||||
name={rechecker.isWorking ? "close" : "check"}
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
rechecker.isWorking ? colors.error.icon : colors.primary.accent
|
||||
}
|
||||
onPress={() => {
|
||||
useRechecker.getState().setProgress(note?.id || "all", {
|
||||
...useRechecker.getState().progress[note?.id || "all"],
|
||||
shown: false,
|
||||
isWorking: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View>
|
||||
<ScrollView
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
paddingVertical: 12
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignItems: "center"
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{attachmentTypes.map((item) =>
|
||||
item.filterBy === "orphaned" && note ? null : (
|
||||
<Button
|
||||
type={
|
||||
currentFilter === item.filterBy
|
||||
? "secondaryAccented"
|
||||
: "plain"
|
||||
}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 12,
|
||||
height: 40,
|
||||
minWidth: 80
|
||||
}}
|
||||
onPress={async () => {
|
||||
const filterBy = item.filterBy;
|
||||
setCurrentFilter(filterBy);
|
||||
setLoading(true);
|
||||
filterAttachments(filterBy)
|
||||
.then((results) => {
|
||||
if (filterBy !== currentFilterRef.current) return;
|
||||
setAttachments(results);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(console.log);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<FlashList
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator size={40} color={colors.primary.accent} />
|
||||
) : (
|
||||
<>
|
||||
<Icon
|
||||
name="attachment"
|
||||
size={60}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
ListHeaderComponent={<AllProgress note={note} />}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={loading ? [] : attachments?.placeholders}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AllProgress = ({ note }: { note?: Note }) => {
|
||||
const progress = useAttachmentStore((state) =>
|
||||
note
|
||||
? {
|
||||
downloading: {
|
||||
[note.id]: state.downloading?.[note.id]
|
||||
},
|
||||
uploading: {
|
||||
[note.id]: state.uploading?.[note.id]
|
||||
}
|
||||
}
|
||||
: state
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
gap: 10,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{Object.keys(progress.downloading || {}).map((groupId) => (
|
||||
<AttachmentGroupProgress key={groupId} groupId={groupId} />
|
||||
))}
|
||||
|
||||
{Object.keys(progress.uploading || {}).map((groupId) => (
|
||||
<AttachmentGroupProgress key={groupId} groupId={groupId} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note?: Note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} />
|
||||
component: () => <AttachmentDialog note={note} isSheet={true} />,
|
||||
keyboardHandlerDisabled: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,9 +19,6 @@ 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
|
||||
@@ -31,9 +28,7 @@ import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth, initialAuthMode } from "./common";
|
||||
import { initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
|
||||
@@ -49,7 +44,6 @@ const AuthModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
|
||||
const actionSheetRef = useRef();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenLoginDialog, open);
|
||||
@@ -97,81 +91,18 @@ const AuthModal = () => {
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
<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="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
|
||||
@@ -68,7 +68,8 @@ export const ChangePassword = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (!result) throw new Error("Failed to create backup");
|
||||
if (result.error)
|
||||
throw new Error(`Failed to create backup: ${result.error}`);
|
||||
|
||||
await db.user.clearSessions();
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
|
||||
80
apps/mobile/app/components/auth/header.tsx
Normal file
80
apps/mobile/app/components/auth/header.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -17,12 +17,17 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
@@ -34,11 +39,6 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import Sync from "../../services/sync";
|
||||
import { Notice } from "../ui/notice";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -67,7 +67,7 @@ export const Login = ({ changeMode }) => {
|
||||
Progress.present();
|
||||
setTimeout(() => {
|
||||
if (!useUserStore.getState().syncing) {
|
||||
Sync.run("global", false, true);
|
||||
Sync.run("global", false, "full");
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
@@ -90,215 +90,233 @@ export const Login = ({ changeMode }) => {
|
||||
<>
|
||||
<ForgotPassword />
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<View
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<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
|
||||
}}
|
||||
>
|
||||
<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%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
<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
|
||||
}}
|
||||
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={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
{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?"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
height: 30,
|
||||
paddingHorizontal: 0
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<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={{
|
||||
marginTop: 25
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
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();
|
||||
}
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
/>
|
||||
|
||||
{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"
|
||||
/>
|
||||
<>
|
||||
<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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
login();
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
/>
|
||||
|
||||
{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"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Sign up
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Sign up
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -112,7 +112,11 @@ export const SessionExpired = () => {
|
||||
if (!res) throw new Error("no token found");
|
||||
if (db.tokenManager._isTokenExpired(res))
|
||||
throw new Error("token expired");
|
||||
Sync.run("global", false, true, async (complete) => {
|
||||
|
||||
const key = await db.user.getEncryptionKey();
|
||||
if (!key) throw new Error("No encryption key found.");
|
||||
|
||||
Sync.run("global", false, "full", async (complete) => {
|
||||
if (!complete) {
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
@@ -127,7 +131,6 @@ export const SessionExpired = () => {
|
||||
setVisible(false);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
|
||||
@@ -16,16 +16,12 @@ 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, { useContext } from "react";
|
||||
|
||||
import Config from "./config";
|
||||
export const SignupContext = React.createContext<{
|
||||
signup?: () => Promise<boolean>;
|
||||
}>({
|
||||
signup: undefined
|
||||
});
|
||||
|
||||
export function isTelemetryEnabled() {
|
||||
// telemetry is always disabled in DEBUG/TEST mode
|
||||
if (import.meta.env.DEV || IS_TESTING) return false;
|
||||
|
||||
return Config.get("telemetry", false);
|
||||
}
|
||||
|
||||
export function setTelemetry(state: boolean) {
|
||||
Config.set("telemetry", state);
|
||||
}
|
||||
export const useSignupContext = () => useContext(SignupContext);
|
||||
@@ -17,26 +17,38 @@ 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 PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Loading } from "../loading";
|
||||
import { PaywallComponent } from "../premium/component";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { AuthHeader } from "./header";
|
||||
import { SignupContext } from "./signup-context";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
selectPlan: 1,
|
||||
createAccount: 2
|
||||
};
|
||||
|
||||
export const Signup = ({ changeMode, welcome }) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
const { colors } = useThemeColors();
|
||||
const email = useRef();
|
||||
const emailInputRef = useRef();
|
||||
@@ -49,7 +61,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const { height } = useWindowDimensions();
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
@@ -68,22 +80,20 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
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();
|
||||
hideAuth();
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
} else {
|
||||
PremiumService.showVerifyEmailDialog();
|
||||
}
|
||||
setCurrentStep(SignupSteps.selectPlan);
|
||||
return true;
|
||||
} catch (e) {
|
||||
setCurrentStep(SignupSteps.signup);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Signup failed",
|
||||
@@ -91,215 +101,266 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
<SignupContext.Provider
|
||||
value={{
|
||||
signup: signup
|
||||
}}
|
||||
>
|
||||
{currentStep === SignupSteps.signup ? (
|
||||
<>
|
||||
<AuthHeader welcome={welcome} />
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
}}
|
||||
/>
|
||||
|
||||
<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 {"\n"}account
|
||||
</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="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Email"
|
||||
onSubmit={() => {
|
||||
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"
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
onSubmit={() => {
|
||||
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}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder="Confirm password"
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
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.
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={!loading ? "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}>
|
||||
Already have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ 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
|
||||
}}
|
||||
>
|
||||
Login
|
||||
<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
|
||||
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>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
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.
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ 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 { colors } = useThemeColors();
|
||||
@@ -143,7 +144,10 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
@@ -214,6 +218,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
}}
|
||||
onSubmitEditing={onNext}
|
||||
caretHidden
|
||||
inputStyle={{
|
||||
fontSize: SIZE.lg,
|
||||
@@ -225,6 +230,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
height: 60,
|
||||
borderWidth: 0,
|
||||
@@ -297,7 +303,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ 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 { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -33,15 +34,18 @@ export const LoginSteps = {
|
||||
passwordAuth: 3
|
||||
};
|
||||
|
||||
export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
export const useLogin = (
|
||||
onFinishLogin?: () => void,
|
||||
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();
|
||||
const password = useRef();
|
||||
const emailInputRef = useRef();
|
||||
const passwordInputRef = useRef();
|
||||
const email = useRef<string>();
|
||||
const password = useRef<string>();
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
|
||||
const validateInfo = () => {
|
||||
if (
|
||||
@@ -68,44 +72,57 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
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, callback) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
TwoFactorVerification.present(
|
||||
async (
|
||||
mfa: { code: string; method: string },
|
||||
callback: (value: boolean) => void
|
||||
) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 1);
|
||||
callback && callback(true);
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 1);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if (e.message === "invalid_grant") {
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
}
|
||||
}, mfaInfo);
|
||||
},
|
||||
mfaInfo
|
||||
);
|
||||
} else {
|
||||
finishWithError(new Error("Unable to send 2FA code"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
if (!email.current || !password.current) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
null,
|
||||
undefined,
|
||||
sessionExpired
|
||||
);
|
||||
finishLogin();
|
||||
@@ -114,11 +131,11 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
finishWithError(e);
|
||||
finishWithError(e as Error);
|
||||
}
|
||||
};
|
||||
|
||||
const finishWithError = async (e) => {
|
||||
const finishWithError = async (e: Error) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
@@ -23,6 +23,7 @@ import { AnnouncementDialog } from "../announcements";
|
||||
import AuthModal from "../auth/auth-modal";
|
||||
import { SessionExpired } from "../auth/session-expired";
|
||||
import { Dialog } from "../dialog";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
import JumpToSectionDialog from "../dialogs/jump-to-section";
|
||||
import { LoadingDialog } from "../dialogs/loading";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
@@ -35,8 +36,7 @@ import { Expiring } from "../premium/expiring";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
import Progress from "../dialogs/progress";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -52,7 +52,6 @@ const DialogProvider = () => {
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<SheetProvider />
|
||||
<SheetProvider context="sync_progress" />
|
||||
<RestoreDataSheet />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
@@ -62,6 +61,7 @@ const DialogProvider = () => {
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
<Progress />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,21 +32,29 @@ const DialogContainer = ({
|
||||
}: ViewProps & {
|
||||
width?: any;
|
||||
height?: any;
|
||||
noBorder?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...restProps}
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
width: width || DDS.isTab ? 500 : "85%",
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12,
|
||||
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
|
||||
}}
|
||||
style={[
|
||||
style,
|
||||
{
|
||||
width: width || DDS.isTab ? 500 : "85%",
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
},
|
||||
restProps?.noBorder
|
||||
? {}
|
||||
: {
|
||||
...getElevationStyle(5),
|
||||
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ type DialogInfo = {
|
||||
paragraph?: string;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positivePress?: (value: any) => void;
|
||||
positivePress?: (...args: any[]) => void;
|
||||
onClose?: () => void;
|
||||
positiveType?:
|
||||
| "transparent"
|
||||
@@ -44,6 +44,16 @@ type DialogInfo = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
context: "global" | "local" | (string & {});
|
||||
secureTextEntry?: boolean;
|
||||
keyboardType?: string;
|
||||
check?: {
|
||||
info: string;
|
||||
type?: string;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
notice: {
|
||||
text: string;
|
||||
type: "alert" | "information";
|
||||
};
|
||||
};
|
||||
|
||||
export function presentDialog(data: Partial<DialogInfo>): void {
|
||||
|
||||
@@ -37,6 +37,7 @@ import DialogHeader from "./dialog-header";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { Notice } from "../ui/notice";
|
||||
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -62,7 +63,8 @@ export const Dialog = ({ context = "global" }) => {
|
||||
disableBackdropClosing: false,
|
||||
check: {
|
||||
info: "Check",
|
||||
type: "transparent"
|
||||
type: "transparent",
|
||||
defaultValue: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,7 +88,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(false);
|
||||
setChecked(data.check?.defaultValue);
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
},
|
||||
@@ -174,11 +176,25 @@ export const Dialog = ({ context = "global" }) => {
|
||||
onSubmit={onPressPositive}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
keyboardType={dialogInfo.keyboardType || "default"}
|
||||
placeholder={dialogInfo.inputPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo?.notice ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Notice
|
||||
type={dialogInfo.notice.type || "information"}
|
||||
text={dialogInfo.notice.text}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -197,7 +213,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type || "selected" : "plain"}
|
||||
type={checked ? dialogInfo.check.type || "plain" : "plain"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
clearAppLockVerificationCipher,
|
||||
setAppLockVerificationCipher,
|
||||
@@ -44,6 +45,7 @@ import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
@@ -66,6 +68,7 @@ export const AppLockPassword = () => {
|
||||
confirmPassword?: string;
|
||||
}>({});
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
const [accountPass, setAccountPass] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const subs = [
|
||||
@@ -73,6 +76,7 @@ export const AppLockPassword = () => {
|
||||
eOpenAppLockPasswordDialog,
|
||||
(mode: "create" | "change" | "remove") => {
|
||||
setMode(mode);
|
||||
setAccountPass(false);
|
||||
setVisible(true);
|
||||
}
|
||||
),
|
||||
@@ -126,7 +130,9 @@ export const AppLockPassword = () => {
|
||||
mode === "change"
|
||||
? `Change app lock ${keyboardType}`
|
||||
: mode === "remove"
|
||||
? `Remove app lock ${keyboardType}`
|
||||
? `Enter ${
|
||||
accountPass ? "account password" : `app lock ${keyboardType}`
|
||||
} to remove ${keyboardType}`
|
||||
: `Set up a custom app lock ${keyboardType} to unlock the app`
|
||||
}
|
||||
icon="shield"
|
||||
@@ -169,33 +175,39 @@ export const AppLockPassword = () => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
defaultValue={values.current.password}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
keyboardType={
|
||||
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
|
||||
}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
|
||||
returnKeyType={mode !== "remove" ? "next" : "done"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
buttonLeft={
|
||||
<IconButton
|
||||
name={keyboardType === "password" ? "numeric" : "keyboard"}
|
||||
onPress={() => {
|
||||
setKeyboardType(
|
||||
keyboardType === "password" ? "pin" : "password"
|
||||
);
|
||||
setSecureTextEntry(false);
|
||||
setImmediate(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
accountPass ? null : (
|
||||
<IconButton
|
||||
name={keyboardType === "password" ? "numeric" : "keyboard"}
|
||||
onPress={() => {
|
||||
setKeyboardType(
|
||||
keyboardType === "password" ? "pin" : "password"
|
||||
);
|
||||
setSecureTextEntry(false);
|
||||
setImmediate(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
mode === "change"
|
||||
accountPass
|
||||
? "Account password"
|
||||
: mode === "change"
|
||||
? `New ${keyboardType}`
|
||||
: `${keyboardType === "pin" ? "Pin" : "Password"}`
|
||||
}
|
||||
@@ -222,6 +234,36 @@ export const AppLockPassword = () => {
|
||||
placeholder={`Confirm ${keyboardType}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{mode === "remove" ? (
|
||||
<>
|
||||
<Button
|
||||
icon={
|
||||
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
|
||||
}
|
||||
onPress={() => {
|
||||
setSecureTextEntry(false);
|
||||
setAccountPass(!accountPass);
|
||||
setTimeout(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
iconSize={SIZE.lg}
|
||||
type="plain"
|
||||
iconColor={
|
||||
accountPass ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
title="Use account password instead"
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
height: 30
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
@@ -249,7 +291,8 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
const password = values.current.password;
|
||||
setAppLockVerificationCipher(password);
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
} else if (mode === "change") {
|
||||
if (
|
||||
@@ -277,7 +320,6 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.currentPassword
|
||||
);
|
||||
@@ -293,8 +335,10 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = values.current.password;
|
||||
await clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
await setAppLockVerificationCipher(password);
|
||||
} else if (mode === "remove") {
|
||||
if (!values.current.password) {
|
||||
ToastManager.error(
|
||||
@@ -305,14 +349,18 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.password
|
||||
);
|
||||
const isCurrentPasswordCorrect = accountPass
|
||||
? await db.user.verifyPassword(values.current.password)
|
||||
: await validateAppLockPassword(values.current.password);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
|
||||
accountPass
|
||||
? "Account password incorrect"
|
||||
: `${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} incorrect`
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
@@ -342,7 +390,9 @@ export const AppLockPassword = () => {
|
||||
|
||||
close();
|
||||
}}
|
||||
positiveTitle="Save"
|
||||
positiveTitle={
|
||||
mode === "remove" ? "Remove" : mode === "change" ? "Change" : "Save"
|
||||
}
|
||||
negativeTitle="Cancel"
|
||||
positiveType="transparent"
|
||||
loading={false}
|
||||
|
||||
@@ -17,8 +17,14 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { Image, ScrollView, TouchableOpacity, View } from "react-native";
|
||||
import { ImagePickerResponse } from "react-native-image-picker";
|
||||
import {
|
||||
Image,
|
||||
Platform,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { Image as ImageType } from "react-native-image-crop-picker";
|
||||
import { useThemeColors } from "../../../../../../packages/theme/dist";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
@@ -32,7 +38,7 @@ export default function AttachImage({
|
||||
onAttach,
|
||||
close
|
||||
}: {
|
||||
response: ImagePickerResponse;
|
||||
response: ImageType[];
|
||||
onAttach: ({ compress }: { compress: boolean }) => void;
|
||||
close: ((ctx?: string | undefined) => void) | undefined;
|
||||
}) {
|
||||
@@ -58,14 +64,17 @@ export default function AttachImage({
|
||||
}}
|
||||
>
|
||||
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
|
||||
Attaching {response.assets?.length} image(s):
|
||||
Attaching {response?.length} image(s):
|
||||
</Paragraph>
|
||||
<ScrollView horizontal>
|
||||
{response.assets?.map((item) => (
|
||||
<TouchableOpacity key={item.fileName} activeOpacity={0.9}>
|
||||
{response?.map?.((item) => (
|
||||
<TouchableOpacity key={item.filename} activeOpacity={0.9}>
|
||||
<Image
|
||||
source={{
|
||||
uri: item.uri
|
||||
uri:
|
||||
Platform.OS === "ios"
|
||||
? item.sourceURL || item.path
|
||||
: item.path
|
||||
}}
|
||||
style={{
|
||||
width: 100,
|
||||
@@ -142,7 +151,7 @@ export default function AttachImage({
|
||||
|
||||
<Button
|
||||
title={`${
|
||||
(response.assets?.length || 0) > 1 ? "Attach Images" : "Attach Image"
|
||||
(response?.length || 0) > 1 ? "Attach Images" : "Attach Image"
|
||||
}`}
|
||||
type="accent"
|
||||
width="100%"
|
||||
@@ -156,10 +165,16 @@ export default function AttachImage({
|
||||
);
|
||||
}
|
||||
|
||||
AttachImage.present = (response: ImagePickerResponse) => {
|
||||
return new Promise((resolve) => {
|
||||
AttachImage.present = (response: ImageType[], context?: string) => {
|
||||
return new Promise<
|
||||
| {
|
||||
compress: boolean;
|
||||
}
|
||||
| undefined
|
||||
>((resolve) => {
|
||||
let resolved = false;
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close, update) => (
|
||||
<AttachImage
|
||||
response={response}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { deleteCacheFileByPath } from "../../../common/filesystem/io";
|
||||
|
||||
const WIN_WIDTH = Dimensions.get("window").width;
|
||||
const WIN_HEIGHT = Dimensions.get("window").height;
|
||||
@@ -124,6 +125,7 @@ const PDFPreview = () => {
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
deleteCacheFileByPath(pdfSource);
|
||||
setPDFSource(null);
|
||||
setVisible(false);
|
||||
setPassword("");
|
||||
|
||||
198
apps/mobile/app/components/dialogs/progress/index.tsx
Normal file
198
apps/mobile/app/components/dialogs/progress/index.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { eSendEvent, eSubscribeEvent } from "../../../services/event-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogContainer from "../../dialog/dialog-container";
|
||||
import { Button } from "../../ui/button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
export type ProgressOptions = {
|
||||
progress?: string;
|
||||
cancelCallback?: () => void;
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
};
|
||||
|
||||
export const PROGRESS_EVENTS = {
|
||||
start: "startProgress",
|
||||
end: "endProgress",
|
||||
update: "updateProgress"
|
||||
};
|
||||
|
||||
export default function Progress() {
|
||||
const { colors } = useThemeColors();
|
||||
const [progress, setProgress] = React.useState<string | undefined>();
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const cancelCallback = React.useRef<() => void>();
|
||||
const [data, setData] = React.useState<{
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
console.log("options", options.fillBackground);
|
||||
setData({
|
||||
title: options.title,
|
||||
paragraph: options.paragraph,
|
||||
fillBackground: options.fillBackground,
|
||||
canHideProgress: options.canHideProgress
|
||||
});
|
||||
setVisible(true);
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
|
||||
setProgress(undefined);
|
||||
setVisible(false);
|
||||
setData(undefined);
|
||||
cancelCallback.current?.();
|
||||
cancelCallback.current = undefined;
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
if (options.cancelCallback) {
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
}
|
||||
|
||||
const data: ProgressOptions = {};
|
||||
if (options.title) data.title = options.title;
|
||||
if (options.paragraph) data.paragraph = options.paragraph;
|
||||
if (options.fillBackground)
|
||||
data.fillBackground = options.fillBackground;
|
||||
|
||||
setData((current) => {
|
||||
return {
|
||||
...current,
|
||||
...data
|
||||
};
|
||||
});
|
||||
})
|
||||
];
|
||||
return () => {
|
||||
events.forEach((event) => event?.unsubscribe());
|
||||
cancelCallback.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
background={data?.fillBackground ? colors.primary.background : undefined}
|
||||
visible
|
||||
>
|
||||
<DialogContainer
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
noBorder={data?.fillBackground ? true : false}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
gap: 10,
|
||||
paddingBottom: 20
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{data?.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{progress ? progress : data?.paragraph}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{data?.canHideProgress ? null : (
|
||||
<Button
|
||||
title={cancelCallback.current ? "Cancel" : "Hide"}
|
||||
type="secondaryAccented"
|
||||
onPress={() => {
|
||||
if (cancelCallback.current) {
|
||||
cancelCallback.current?.();
|
||||
}
|
||||
setVisible(false);
|
||||
setProgress(undefined);
|
||||
setData(undefined);
|
||||
}}
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</DialogContainer>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function startProgress(options: ProgressOptions) {
|
||||
eSendEvent(PROGRESS_EVENTS.start, options);
|
||||
}
|
||||
|
||||
export function endProgress() {
|
||||
eSendEvent(PROGRESS_EVENTS.end);
|
||||
}
|
||||
|
||||
export function updateProgress(options: ProgressOptions) {
|
||||
eSendEvent(PROGRESS_EVENTS.update, options);
|
||||
}
|
||||
@@ -586,7 +586,7 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
|
||||
async _copyNote(note) {
|
||||
Clipboard.setString(await convertNoteToText(note));
|
||||
Clipboard.setString((await convertNoteToText(note, true)) || "");
|
||||
ToastManager.show({
|
||||
heading: "Note copied",
|
||||
type: "success",
|
||||
@@ -602,7 +602,7 @@ export class VaultDialog extends Component {
|
||||
await Share.open({
|
||||
heading: "Share note",
|
||||
failOnCancel: false,
|
||||
message: await convertNoteToText(note)
|
||||
message: (await convertNoteToText(note)) || ""
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -23,10 +23,7 @@ import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Issue } from "../sheets/github/issue";
|
||||
|
||||
const error = (
|
||||
stack: string,
|
||||
component: string
|
||||
) => `Please let us know what happened. What steps we can take to reproduce the issue here.
|
||||
const error = (stack: string, component: string) => `
|
||||
|
||||
_______________________________
|
||||
Stacktrace: In ${component}::${stack}`;
|
||||
@@ -39,9 +36,9 @@ class ExceptionHandler extends React.Component<{
|
||||
error: Error | null;
|
||||
hasError: boolean;
|
||||
} = {
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error: error };
|
||||
}
|
||||
|
||||
@@ -67,8 +67,6 @@ export const Header = ({
|
||||
hasSearch?: boolean;
|
||||
onSearch?: () => void;
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [borderHidden, setBorderHidden] = useState(true);
|
||||
|
||||
@@ -96,7 +96,8 @@ export const RightMenus = ({
|
||||
animationDuration={200}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
backgroundColor: contextMenuColors.primary.background
|
||||
backgroundColor: contextMenuColors.primary.background,
|
||||
marginTop: -40
|
||||
}}
|
||||
onRequestClose={() => {
|
||||
menuRef.current?.hide();
|
||||
|
||||
@@ -18,11 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import dataurl from "@notesnook/core/dist/utils/dataurl";
|
||||
import { DataURL } from "@notesnook/core";
|
||||
import type { ImageAttributes } from "@notesnook/editor";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import ImageViewer from "react-native-image-zoom-viewer";
|
||||
import { db } from "../../common/database";
|
||||
import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { cacheDir } from "../../common/filesystem/utils";
|
||||
import {
|
||||
@@ -32,13 +34,13 @@ import {
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
|
||||
const ImagePreview = () => {
|
||||
const { colors } = useThemeColors("dialog");
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [image, setImage] = useState<string>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const imageRef = useRef<ImageAttributes>();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("ImagePreview", open);
|
||||
@@ -48,18 +50,22 @@ const ImagePreview = () => {
|
||||
}, []);
|
||||
|
||||
const open = async (image: ImageAttributes) => {
|
||||
imageRef.current = image;
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
let hash = image.hash;
|
||||
if (!hash && image.src && dataurl.toObject(image.src)) {
|
||||
const data = dataurl.toObject(image.src);
|
||||
if (!hash && image.src && DataURL.toObject(image.src)) {
|
||||
const data = DataURL.toObject(image.src);
|
||||
if (!data) return;
|
||||
hash = await Sodium.hashFile({
|
||||
data: data.data,
|
||||
type: "base64",
|
||||
uri: ""
|
||||
});
|
||||
if (imageRef.current) {
|
||||
imageRef.current.hash = hash;
|
||||
}
|
||||
}
|
||||
if (!hash) return;
|
||||
//@ts-ignore // FIX ME
|
||||
@@ -80,7 +86,13 @@ const ImagePreview = () => {
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<BaseDialog animation="slide" visible={true} onRequestClose={close}>
|
||||
<BaseDialog
|
||||
background="black"
|
||||
animation="slide"
|
||||
visible={true}
|
||||
onRequestClose={close}
|
||||
transparent
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -101,6 +113,21 @@ const ImagePreview = () => {
|
||||
color={colors.primary.accent}
|
||||
borderColor="transparent"
|
||||
/>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
if (imageRef.current?.hash) {
|
||||
db.fs().cancel(imageRef.current?.hash);
|
||||
}
|
||||
close();
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: Platform.OS === "android" ? 35 : 0,
|
||||
right: 12
|
||||
}}
|
||||
color={colors.static.white}
|
||||
name="close"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<ImageViewer
|
||||
@@ -117,13 +144,12 @@ const ImagePreview = () => {
|
||||
width: "100%",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
height: 80,
|
||||
marginTop: 0,
|
||||
paddingHorizontal: 12,
|
||||
height: 50,
|
||||
paddingHorizontal: 24,
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
paddingTop: Platform.OS === "android" ? 30 : 0
|
||||
marginTop: Platform.OS === "android" ? 30 : 0
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
@@ -137,7 +163,7 @@ const ImagePreview = () => {
|
||||
)}
|
||||
imageUrls={[
|
||||
{
|
||||
url: image
|
||||
url: image as string
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -19,38 +19,51 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import {
|
||||
Linking,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
|
||||
import { SwiperFlatList } from "react-native-swiper-flatlist";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
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";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eOpenLoginDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AuthMode } from "../auth";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { eOpenLoginDialog } from "../../utils/events";
|
||||
import { AuthMode } from "../auth";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
|
||||
const Intro = ({ navigation }) => {
|
||||
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 { colors } = useThemeColors();
|
||||
const isTelemetryEnabled = useSettingStore(
|
||||
(state) => state.settings.telemetry
|
||||
);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const { width } = useWindowDimensions();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const renderItem = React.useCallback(
|
||||
({ item }) => (
|
||||
({ item }: { item: IntroItem }) => (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
@@ -105,17 +118,18 @@ const Intro = ({ navigation }) => {
|
||||
|
||||
{item.body ? <Paragraph size={SIZE.sm}>{item.body}</Paragraph> : null}
|
||||
|
||||
{item.tesimonial ? (
|
||||
{item.testimonial ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
fontSize: SIZE.lg
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!item.link) return;
|
||||
Linking.openURL(item.link);
|
||||
}}
|
||||
>
|
||||
{item.tesimonial} — {item.user}
|
||||
{item.testimonial} — {item.user}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -130,22 +144,23 @@ const Intro = ({ navigation }) => {
|
||||
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 : 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
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : undefined,
|
||||
borderColor:
|
||||
deviceMode !== "mobile" ? colors.primary.border : undefined,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : undefined,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : undefined,
|
||||
flexGrow: 2
|
||||
}}
|
||||
>
|
||||
<SwiperFlatList
|
||||
@@ -155,26 +170,7 @@ const Intro = ({ navigation }) => {
|
||||
index={0}
|
||||
useReactNativeGestureHandler={true}
|
||||
showPagination
|
||||
data={[
|
||||
{
|
||||
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."
|
||||
},
|
||||
{
|
||||
tesimonial:
|
||||
"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"
|
||||
}
|
||||
]}
|
||||
data={introItems}
|
||||
paginationActiveColor={colors.primary.accent}
|
||||
paginationStyleItem={{
|
||||
width: 10,
|
||||
@@ -190,13 +186,14 @@ const Intro = ({ navigation }) => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
minHeight: height * 0.3
|
||||
paddingHorizontal: 16,
|
||||
gap: 12,
|
||||
paddingVertical: 16
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
width={250}
|
||||
onPress={async () => {
|
||||
width="100%"
|
||||
onPress={() => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
@@ -210,51 +207,29 @@ const Intro = ({ navigation }) => {
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "center",
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
...getElevationStyle(5)
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title="Get started"
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
width: "90%",
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "center",
|
||||
padding: 12,
|
||||
maxWidth: 500
|
||||
}}
|
||||
<Button
|
||||
width="100%"
|
||||
title="I already have an account"
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
SettingsService.set({ telemetry: !isTelemetryEnabled });
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.login);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
}, 1000);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={SIZE.md}
|
||||
name={
|
||||
isTelemetryEnabled ? "checkbox-marked" : "checkbox-blank-outline"
|
||||
}
|
||||
color={
|
||||
isTelemetryEnabled ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 6
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
Help improve Notesnook by sending completely anonymized{" "}
|
||||
<Heading size={SIZE.xs}>private analytics and bug reports.</Heading>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
File diff suppressed because one or more lines are too long
@@ -49,7 +49,8 @@ export const Card = ({ color }: { color?: string }) => {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 0
|
||||
paddingHorizontal: 0,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -91,7 +92,6 @@ export const Card = ({ color }: { color?: string }) => {
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
flexShrink: 1,
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function List(props: ListProps) {
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
Sync.run("global", false, true, () => {
|
||||
Sync.run("global", false, "full", () => {
|
||||
props.onRefresh?.();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
TrashItem,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
||||
import { getSortValue } from "@notesnook/core";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
|
||||
88
apps/mobile/app/components/loading/index.tsx
Normal file
88
apps/mobile/app/components/loading/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -21,10 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Editor from "../../screens/editor";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../stores/use-trash-store";
|
||||
@@ -34,7 +34,7 @@ import DialogHeader from "../dialog/dialog-header";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
import { diff } from "diffblazer";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -121,12 +121,21 @@ export default function NotePreview({ session, content, note }) {
|
||||
<ReadonlyEditor
|
||||
editorId="historyPreview"
|
||||
onLoad={async (loadContent) => {
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
loadContent({
|
||||
data: content.data,
|
||||
id: _note.id
|
||||
});
|
||||
try {
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
const currentContent = await db.content.get(_note.contentId);
|
||||
loadContent({
|
||||
data: diff(currentContent.data, content.data),
|
||||
id: _note.id
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
ToastManager.error(
|
||||
e,
|
||||
"Failed to load history preview",
|
||||
"local"
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,292 +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, { 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>
|
||||
);
|
||||
};
|
||||
768
apps/mobile/app/components/premium/component.tsx
Normal file
768
apps/mobile/app/components/premium/component.tsx
Normal file
File diff suppressed because one or more lines are too long
60
apps/mobile/app/components/premium/features-list.tsx
Normal file
60
apps/mobile/app/components/premium/features-list.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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, "∞", "∞"]
|
||||
};
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
} from "../../services/event-manager";
|
||||
import { eClosePremiumDialog, eOpenPremiumDialog } from "../../utils/events";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Component } from "./component";
|
||||
import { PaywallComponent } from "./component";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
class PremiumDialog extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -74,7 +75,7 @@ class PremiumDialog extends React.Component {
|
||||
background={this.props.colors.primary.background}
|
||||
onRequestClose={this.onClose}
|
||||
>
|
||||
<Component
|
||||
<PaywallComponent
|
||||
getRef={() => this.actionSheetRef}
|
||||
promo={this.state.promo}
|
||||
close={this.close}
|
||||
|
||||
@@ -85,7 +85,7 @@ export const PricingItem = ({
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails[0].pricingPhases
|
||||
?.subscriptionOfferDetails?.[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice}
|
||||
|
||||
@@ -48,6 +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";
|
||||
|
||||
const UUID_PREFIX = "0bdaea";
|
||||
const UUID_VERSION = "4";
|
||||
@@ -92,173 +93,21 @@ export const PricingPlans = ({
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
const {
|
||||
buySubscription,
|
||||
buying,
|
||||
getStandardPrice,
|
||||
product,
|
||||
setBuying,
|
||||
loading,
|
||||
user,
|
||||
setProduct,
|
||||
monthlyPlan,
|
||||
yearlyPlan,
|
||||
getPromo
|
||||
} = usePricingPlans({
|
||||
promoOffer: promo
|
||||
});
|
||||
|
||||
return loading ? (
|
||||
<View
|
||||
@@ -278,354 +127,215 @@ export const PricingPlans = ({
|
||||
}}
|
||||
>
|
||||
{buying ? (
|
||||
<BaseDialog statusBarTranslucent centered>
|
||||
<BaseDialog visible statusBarTranslucent centered>
|
||||
<ActivityIndicator size={50} color="white" />
|
||||
</BaseDialog>
|
||||
) : null}
|
||||
|
||||
{!upgrade ? (
|
||||
{!user && !product ? (
|
||||
<>
|
||||
<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 || (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<>
|
||||
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Get {monthlyPlan?.info?.discount}% off in{" "}
|
||||
{monthlyPlan?.info?.country}
|
||||
</Heading>
|
||||
</View>
|
||||
) : (
|
||||
<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",
|
||||
textAlign: "center"
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{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}
|
||||
Choose a plan
|
||||
</Heading>
|
||||
{product?.cycleText ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
color: colors.secondary.paragraph,
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
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("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"
|
||||
}}
|
||||
>
|
||||
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"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
setProduct(undefined);
|
||||
}}
|
||||
style={{
|
||||
marginTop: 5
|
||||
}}
|
||||
height={30}
|
||||
fontSize={13}
|
||||
type="errorShade"
|
||||
title="Cancel promo code"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<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"
|
||||
}}
|
||||
>
|
||||
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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!user || !upgrade ? (
|
||||
{!user ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
@@ -645,7 +355,7 @@ export const PricingPlans = ({
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{user && upgrade ? (
|
||||
{user ? (
|
||||
<>
|
||||
{Platform.OS === "ios" ? (
|
||||
<Paragraph
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ProTag = ({ width, size, background }) => {
|
||||
marginRight: 3
|
||||
}}
|
||||
size={size}
|
||||
color={colors.primary.accent}
|
||||
color={colors.static.orange}
|
||||
name="crown"
|
||||
/>
|
||||
<Paragraph size={size - 1.5} color={colors.primary.accent}>
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Color, Note } from "@notesnook/core/dist/types";
|
||||
import { Color, Note } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
371
apps/mobile/app/components/sheets/buy-plan/index.tsx
Normal file
371
apps/mobile/app/components/sheets/buy-plan/index.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Note } from "@notesnook/core";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { EVENTS } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from "react-native";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
import Share from "react-native-share";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
@@ -29,14 +35,12 @@ import { requestInAppReview } from "../../../services/app-review";
|
||||
import {
|
||||
PresentSheetOptions,
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import Exporter from "../../../services/exporter";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { SIZE, ph, pv } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { Dialog } from "../../dialog";
|
||||
@@ -65,6 +69,7 @@ const ExportNotesSheet = ({
|
||||
filePath: string;
|
||||
name: string;
|
||||
type: string;
|
||||
fileDir: string;
|
||||
}
|
||||
| undefined
|
||||
>();
|
||||
@@ -295,7 +300,9 @@ const ExportNotesSheet = ({
|
||||
successfully as {result?.fileName}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title="Open"
|
||||
title={
|
||||
Platform.OS === "android" ? "Open file location" : "Open"
|
||||
}
|
||||
type="accent"
|
||||
width={250}
|
||||
fontSize={SIZE.md}
|
||||
@@ -305,20 +312,23 @@ const ExportNotesSheet = ({
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result?.filePath) return;
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(500);
|
||||
FileViewer.open(result?.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
}).catch((e) => {
|
||||
console.log(e);
|
||||
ToastManager.show({
|
||||
heading: "Cannot open",
|
||||
message: `No application found to open ${result.name} file.`,
|
||||
type: "success",
|
||||
context: "local"
|
||||
if (Platform.OS === "android") {
|
||||
Linking.openURL(result.fileDir).catch((e) => {
|
||||
ToastManager.error(e as Error);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
FileViewer.open(result?.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
}).catch((e) => {
|
||||
ToastManager.show({
|
||||
heading: "Cannot open",
|
||||
message: `No application found to open ${result.name} file.`,
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@@ -331,10 +341,10 @@ const ExportNotesSheet = ({
|
||||
borderRadius: 100
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result?.filePath) return;
|
||||
if (!result) return;
|
||||
if (Platform.OS === "ios") {
|
||||
Share.open({
|
||||
url: result.filePath
|
||||
url: result?.fileDir + result.fileName
|
||||
}).catch(console.log);
|
||||
} else {
|
||||
FileViewer.open(result.filePath, {
|
||||
@@ -347,7 +357,7 @@ const ExportNotesSheet = ({
|
||||
/>
|
||||
<Button
|
||||
title="Export in another format"
|
||||
type="secondaryAccented"
|
||||
type="inverted"
|
||||
width={250}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
|
||||
@@ -17,36 +17,34 @@ 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 { Debug } from "@notesnook/core/dist/api/debug";
|
||||
import { Debug } from "@notesnook/core";
|
||||
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Linking, Platform, Text, TextInput, View } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import { useStoredRef } from "../../../hooks/use-stored-ref";
|
||||
import { ToastManager, eSendEvent } from "../../../services/event-manager";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const body = useStoredRef("issueBody", defaultBody);
|
||||
const body = useStoredRef("issueBody");
|
||||
const title = useStoredRef("issueTitle", defaultTitle);
|
||||
|
||||
const [done, setDone] = useState(false);
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const bodyRef = useRef();
|
||||
const initialLayout = useRef(false);
|
||||
const issueUrl = useRef();
|
||||
|
||||
const onPress = async () => {
|
||||
if (loading) return;
|
||||
@@ -56,57 +54,24 @@ export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
let issue_url = await Debug.report({
|
||||
issueUrl.current = await Debug.report({
|
||||
title: title.current,
|
||||
body:
|
||||
body.current +
|
||||
`\n${defaultBody || ""}` +
|
||||
`\n_______________
|
||||
**Device information:**
|
||||
App version: ${getVersion()}
|
||||
Platform: ${Platform.OS}
|
||||
Model: ${Platform.constants.Brand || ""}-${Platform.constants.Model || ""}-${
|
||||
Platform.constants.Version || ""
|
||||
}
|
||||
Device: ${getBrand() || ""}-${getModel() || ""}-${getSystemVersion() || ""}
|
||||
Pro: ${PremiumService.get()}
|
||||
Logged in: ${user ? "yes" : "no"}`,
|
||||
userId: user?.id
|
||||
});
|
||||
setLoading(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
body.reset();
|
||||
title.reset();
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
title: "Issue reported",
|
||||
paragraph: (
|
||||
<Text>
|
||||
You can track your issue at{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(issue_url);
|
||||
}}
|
||||
>
|
||||
{issue_url}.
|
||||
</Text>{" "}
|
||||
Please note that we will respond to your issue on the given link. We
|
||||
recommend that you save it.
|
||||
</Text>
|
||||
),
|
||||
positiveText: "Copy link",
|
||||
positivePress: () => {
|
||||
Clipboard.setString(issue_url);
|
||||
ToastManager.show({
|
||||
heading: "Issue url copied!",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
},
|
||||
negativeText: "Close"
|
||||
});
|
||||
setDone(true);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
@@ -124,125 +89,177 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<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."
|
||||
}
|
||||
/>
|
||||
{done ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
height: 250,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Heading>Issue submitted</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
selectable={true}
|
||||
>
|
||||
You can track your issue at{" "}
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(issueUrl.current);
|
||||
}}
|
||||
>
|
||||
{issueUrl.current}
|
||||
</Paragraph>
|
||||
. Please note that we will respond to your issue on the given
|
||||
link. We recommend that you save it.
|
||||
</Paragraph>
|
||||
|
||||
<Seperator half />
|
||||
|
||||
<TextInput
|
||||
placeholder="Title"
|
||||
onChangeText={(v) => (title.current = v)}
|
||||
defaultValue={title.current}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
marginBottom: 10,
|
||||
fontSize: SIZE.md,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={bodyRef}
|
||||
placeholder={`Tell us more about the issue you are facing.
|
||||
|
||||
For example:
|
||||
- Steps to reproduce the issue
|
||||
- Things you have tried etc.`}
|
||||
multiline
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
onChangeText={(v) => (body.current = v)}
|
||||
onLayout={() => {
|
||||
if (initialLayout.current) return;
|
||||
initialLayout.current = true;
|
||||
if (body.current) {
|
||||
bodyRef.current?.setNativeProps({
|
||||
text: body.current,
|
||||
selection: {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
maxHeight: 200,
|
||||
fontSize: SIZE.sm,
|
||||
marginBottom: 2.5,
|
||||
color: colors.primary.paragraph
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>{`App version: ${getVersion()} Platform: ${Platform.OS} Model: ${
|
||||
Platform.constants.Brand
|
||||
}-${Platform.constants.Model}-${Platform.constants.Version}`}</Paragraph>
|
||||
|
||||
<Seperator />
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={loading ? null : "Submit"}
|
||||
loading={loading}
|
||||
width="100%"
|
||||
type="accent"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
The information above will be publically available at{" "}
|
||||
<Text
|
||||
onPress={() => {
|
||||
Linking.openURL("https://github.com/streetwriters/notesnook");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
github.com/streetwriters/notesnook.
|
||||
</Text>{" "}
|
||||
If you want to ask something in general or need some assistance, we
|
||||
would suggest that you{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser("https://discord.gg/zQBK97EE22", colors);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
<Button
|
||||
title="Open issue"
|
||||
onPress={() => {
|
||||
Linking.openURL(issueUrl.current);
|
||||
}}
|
||||
type="accent"
|
||||
width="100%"
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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."
|
||||
}
|
||||
}}
|
||||
>
|
||||
join our community on Discord.
|
||||
</Text>
|
||||
</Paragraph>
|
||||
/>
|
||||
|
||||
<Seperator half />
|
||||
|
||||
<TextInput
|
||||
placeholder="Title"
|
||||
onChangeText={(v) => (title.current = v)}
|
||||
defaultValue={title.current}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
marginBottom: 10,
|
||||
fontSize: SIZE.md,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<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.`}
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
onChangeText={(v) => (body.current = v)}
|
||||
onLayout={() => {
|
||||
if (initialLayout.current) return;
|
||||
initialLayout.current = true;
|
||||
if (body.current) {
|
||||
bodyRef.current?.setNativeProps({
|
||||
text: body.current,
|
||||
selection: {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
maxHeight: 200,
|
||||
fontSize: SIZE.sm,
|
||||
marginBottom: 2.5,
|
||||
color: colors.primary.paragraph
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>{`App version: ${getVersion()} Platform: ${
|
||||
Platform.OS
|
||||
} Model: ${getBrand()}-${getModel()}-${getSystemVersion()}`}</Paragraph>
|
||||
|
||||
<Seperator />
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={loading ? null : "Submit"}
|
||||
loading={loading}
|
||||
width="100%"
|
||||
type="accent"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
The information above will be publically available at{" "}
|
||||
<Text
|
||||
onPress={() => {
|
||||
Linking.openURL("https://github.com/streetwriters/notesnook");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
github.com/streetwriters/notesnook.
|
||||
</Text>{" "}
|
||||
If you want to ask something in general or need some assistance, we
|
||||
would suggest that you{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(
|
||||
"https://discord.gg/zQBK97EE22",
|
||||
colors
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
join our community on Discord.
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,8 +34,11 @@ import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import type { LinkAttributes } from "@notesnook/editor";
|
||||
import {
|
||||
EditorEvents,
|
||||
editorController
|
||||
} from "../../../screens/editor/tiptap/utils";
|
||||
|
||||
const ListNoteItem = ({
|
||||
id,
|
||||
@@ -190,13 +193,13 @@ export default function LinkNote(props: {
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
editorController.current.commands.createInternalLink(
|
||||
{
|
||||
editorController.current?.postMessage(EditorEvents.resolve, {
|
||||
data: {
|
||||
href: link,
|
||||
title: selectedNote.title
|
||||
},
|
||||
props.resolverId
|
||||
);
|
||||
resolverId: props.resolverId
|
||||
});
|
||||
};
|
||||
|
||||
const onSelectNote = async (note: Note) => {
|
||||
|
||||
@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Tags } from "@notesnook/core/dist/collections/tags";
|
||||
import { Tag } from "@notesnook/core/dist/types";
|
||||
import { sanitizeTag } from "@notesnook/core";
|
||||
import { Tag } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
RefObject,
|
||||
@@ -268,8 +268,8 @@ const ManageTagsSheet = (props: {
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(Tags.sanitize(v));
|
||||
checkQueryExists(Tags.sanitize(v));
|
||||
setQuery(sanitizeTag(v));
|
||||
checkQueryExists(sanitizeTag(v));
|
||||
}}
|
||||
onFocusInput={() => {
|
||||
setFocus(true);
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { EVENTS } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
@@ -42,10 +42,7 @@ import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { Issue } from "../github/issue";
|
||||
|
||||
export const makeError = (
|
||||
stack: string,
|
||||
component: string
|
||||
) => `Please let us know what happened. What steps we can take to reproduce the issue here.
|
||||
export const makeError = (stack: string, component: string) => `
|
||||
|
||||
_______________________________
|
||||
Stacktrace: In ${component}::${stack}`;
|
||||
@@ -96,10 +93,12 @@ export default function Migrate() {
|
||||
});
|
||||
setLoading(true);
|
||||
await sleep(1);
|
||||
const { error } = await BackupService.run(false, "local");
|
||||
const { error, report } = await BackupService.run(false, "local");
|
||||
if (error) {
|
||||
ToastManager.error(error, "Backup failed");
|
||||
reportError(error);
|
||||
ToastManager.error(error as Error, "Backup failed");
|
||||
if (report) {
|
||||
reportError(error as Error);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Note, Notebook } from "@notesnook/core/dist/types";
|
||||
import { Note, Notebook } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, TextInput, View } from "react-native";
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
createInternalLink,
|
||||
highlightInternalLinks
|
||||
} from "@notesnook/core";
|
||||
import { ContentBlock, ItemReference, Note } from "@notesnook/core/dist/types";
|
||||
import { ContentBlock, ItemReference, Note } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Item, ItemReference } from "@notesnook/core/dist/types";
|
||||
import { Item, ItemReference } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
@@ -1,600 +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 { getFormattedDate } from "@notesnook/common";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { unzip } from "react-native-zip-archive";
|
||||
import { db } from "../../../common/database";
|
||||
import storage from "../../../common/database/storage";
|
||||
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
|
||||
import {
|
||||
ToastManager,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { refreshAllStores } from "../../../stores/create-db-collection-store";
|
||||
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
|
||||
const RestoreDataSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
const sheet = useRef();
|
||||
useEffect(() => {
|
||||
const open = async () => {
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
sheet.current?.show();
|
||||
}, 1);
|
||||
};
|
||||
eSubscribeEvent(eOpenRestoreDialog, open);
|
||||
eSubscribeEvent(eCloseRestoreDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenRestoreDialog, open);
|
||||
eUnSubscribeEvent(eCloseRestoreDialog, close);
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (restoring) {
|
||||
showIsWorking();
|
||||
return;
|
||||
}
|
||||
sheet.current?.hide();
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, 150);
|
||||
}, [restoring]);
|
||||
|
||||
const showIsWorking = () => {
|
||||
ToastManager.show({
|
||||
heading: "Restoring Backup",
|
||||
message: "Your backup data is being restored. please wait.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
fwdRef={sheet}
|
||||
gestureEnabled={!restoring}
|
||||
closeOnTouchBackdrop={!restoring}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<RestoreDataComponent
|
||||
close={close}
|
||||
restoring={restoring}
|
||||
setRestoring={setRestoring}
|
||||
actionSheetRef={sheet}
|
||||
/>
|
||||
<Toast context="local" />
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestoreDataSheet;
|
||||
|
||||
const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [backupDirectoryAndroid, setBackupDirectoryAndroid] = useState(false);
|
||||
const [progress, setProgress] = useState();
|
||||
useEffect(() => {
|
||||
const subscription = db.eventManager.subscribe(
|
||||
EVENTS.migrationProgress,
|
||||
(progress) => {
|
||||
setProgress(progress);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
checkBackups();
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const restore = async (item) => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = Platform.OS === "ios" ? item.path : item.uri;
|
||||
console.log(file);
|
||||
if (file.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const cacheFile = `file://${RNFetchBlob.fs.dirs.CacheDir}/backup.zip`;
|
||||
if (await RNFetchBlob.fs.exists(cacheFile)) {
|
||||
await RNFetchBlob.fs.unlink(cacheFile);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(cacheFile, "", "utf8");
|
||||
console.log("copying");
|
||||
await copyFileAsync(file, cacheFile);
|
||||
console.log("copied");
|
||||
await restoreFromZip(cacheFile);
|
||||
} else {
|
||||
await restoreFromZip(file, false);
|
||||
}
|
||||
} else if (file.endsWith(".nnbackup")) {
|
||||
let backup;
|
||||
if (Platform.OS === "android") {
|
||||
backup = await ScopedStorage.readFile(file, "utf8");
|
||||
} else {
|
||||
backup = await RNFetchBlob.fs.readFile(file, "utf8");
|
||||
}
|
||||
await restoreFromNNBackup(JSON.parse(backup));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const withPassword = () => {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: () => {
|
||||
if (resolved) return;
|
||||
resolve(undefined);
|
||||
},
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password, isEncryptionKey) => {
|
||||
resolve({
|
||||
value: password,
|
||||
isEncryptionKey
|
||||
});
|
||||
resolved = true;
|
||||
return true;
|
||||
},
|
||||
check: {
|
||||
info: "Use encryption key",
|
||||
type: "transparent"
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const checkBackups = async () => {
|
||||
try {
|
||||
let files = [];
|
||||
if (Platform.OS === "android") {
|
||||
let backupDirectory = SettingsService.get().backupDirectoryAndroid;
|
||||
if (backupDirectory) {
|
||||
setBackupDirectoryAndroid(backupDirectory);
|
||||
files = await ScopedStorage.listFiles(backupDirectory.uri);
|
||||
} else {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let path = await storage.checkAndCreateDir("/backups/");
|
||||
files = await RNFetchBlob.fs.lstat(path);
|
||||
}
|
||||
files = files
|
||||
.filter((file) => {
|
||||
const name = Platform.OS === "android" ? file.name : file.filename;
|
||||
return name.endsWith(".nnbackup") || name.endsWith(".nnbackupz");
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
let timeA = a.lastModified;
|
||||
let timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
setFiles(files);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restoreBackup = async (backup, password, key) => {
|
||||
await db.transaction(async () => {
|
||||
await db.backup.import(backup, password, key);
|
||||
});
|
||||
await db.initCollections();
|
||||
refreshAllStores();
|
||||
ToastManager.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
return true;
|
||||
};
|
||||
|
||||
const backupError = (e) => {
|
||||
console.log(e.stack);
|
||||
ToastManager.show({
|
||||
heading: "Restore failed",
|
||||
message:
|
||||
e.message ||
|
||||
"The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromZip(file, remove) {
|
||||
try {
|
||||
const zipOutputFolder = `${cacheDir}/backup_extracted`;
|
||||
if (await RNFetchBlob.fs.exists(zipOutputFolder)) {
|
||||
await RNFetchBlob.fs.unlink(zipOutputFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipOutputFolder);
|
||||
}
|
||||
await unzip(file, zipOutputFolder);
|
||||
console.log("Unzipped files successfully to", zipOutputFolder);
|
||||
|
||||
const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
|
||||
|
||||
if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
|
||||
throw new Error("Backup file is invalid");
|
||||
}
|
||||
|
||||
await db.transaction(async () => {
|
||||
let password;
|
||||
let key;
|
||||
console.log(
|
||||
`Found ${backupFiles?.length} files to restore from backup`
|
||||
);
|
||||
for (const path of backupFiles) {
|
||||
if (path === ".nnbackup") continue;
|
||||
const filePath = `${zipOutputFolder}/${path}`;
|
||||
const data = await RNFetchBlob.fs.readFile(filePath, "utf8");
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (parsed.encrypted && !password) {
|
||||
console.log("Backup is encrypted...", "requesting password");
|
||||
const { value, isEncryptionKey } = await withPassword();
|
||||
|
||||
if (isEncryptionKey) {
|
||||
key = value;
|
||||
} else {
|
||||
password = value;
|
||||
}
|
||||
if (!password && !key) throw new Error("Failed to decrypt backup");
|
||||
}
|
||||
await db.backup.import(parsed, password, key);
|
||||
console.log("Imported", path);
|
||||
}
|
||||
});
|
||||
// Remove files from cache
|
||||
RNFetchBlob.fs.unlink(zipOutputFolder).catch(console.log);
|
||||
if (remove) {
|
||||
RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
|
||||
refreshAllStores();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setRestoring(false);
|
||||
close();
|
||||
ToastManager.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
setRestoring(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromNNBackup(backup) {
|
||||
try {
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
const { value, isEncryptionKey } = await withPassword();
|
||||
|
||||
let key;
|
||||
let password;
|
||||
if (isEncryptionKey) {
|
||||
key = value;
|
||||
} else {
|
||||
password = value;
|
||||
}
|
||||
|
||||
if (key || password) {
|
||||
try {
|
||||
await restoreBackup(backup, password, key);
|
||||
close();
|
||||
setRestoring(false);
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
} else {
|
||||
setRestoring(false);
|
||||
}
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
setRestoring(false);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
|
||||
const button = {
|
||||
title: "Restore from files",
|
||||
onPress: async () => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
console.log("disabled...");
|
||||
const file = await DocumentPicker.pickSingle({
|
||||
copyTo: "cachesDirectory"
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
if (file.name.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
await restoreFromZip(file.fileCopyUri, true);
|
||||
} else if (file.name.endsWith(".nnbackup")) {
|
||||
RNFetchBlob.fs.unlink(file.fileCopyUri).catch(console.log);
|
||||
setRestoring(true);
|
||||
const data = await fetch(file.uri);
|
||||
await restoreFromNNBackup(await data.json());
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e.stack);
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, 1000);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }) => (
|
||||
<View
|
||||
style={{
|
||||
minHeight: 50,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
borderRadius: 0,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
maxWidth: "75%"
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.sm} style={{ width: "100%", maxWidth: "100%" }}>
|
||||
{getFormattedDate(item?.lastModified * 1)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs}>
|
||||
{(item.filename || item.name).replace(".nnbackup", "")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Button
|
||||
title="Restore"
|
||||
height={30}
|
||||
type="accent"
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
fontSize={SIZE.sm - 1}
|
||||
onPress={() => restore(item, index)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Dialog context="local" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 8,
|
||||
paddingRight: 8,
|
||||
alignItems: "center",
|
||||
paddingTop: restoring ? 8 : 0
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title="Backups"
|
||||
paragraph={`All the backups are stored in ${
|
||||
Platform.OS === "ios"
|
||||
? "File Manager/Notesnook/Backups"
|
||||
: "selected backups folder."
|
||||
}`}
|
||||
button={button}
|
||||
/>
|
||||
</View>
|
||||
<Seperator half />
|
||||
<FlatList
|
||||
ListEmptyComponent={
|
||||
!restoring ? (
|
||||
loading ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 100
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 100
|
||||
}}
|
||||
>
|
||||
{Platform.OS === "android" && !backupDirectoryAndroid ? (
|
||||
<>
|
||||
<Button
|
||||
title="Select backups folder"
|
||||
icon="folder"
|
||||
onPress={async () => {
|
||||
let folder = await ScopedStorage.openDocumentTree(
|
||||
true
|
||||
);
|
||||
let subfolder;
|
||||
if (folder.name !== "Notesnook backups") {
|
||||
subfolder = await ScopedStorage.createDirectory(
|
||||
folder.uri,
|
||||
"Notesnook backups"
|
||||
);
|
||||
} else {
|
||||
subfolder = folder;
|
||||
}
|
||||
SettingsService.set({
|
||||
backupDirectoryAndroid: subfolder
|
||||
});
|
||||
setBackupDirectoryAndroid(subfolder);
|
||||
setLoading(true);
|
||||
checkBackups();
|
||||
}}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
height={30}
|
||||
width={null}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 5
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
textBreakStrategy="balanced"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Select the folder that includes your backup files to
|
||||
list them here.
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
No backups found
|
||||
</Paragraph>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 200
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator color={colors.primary.accent} />
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
Restoring {progress ? progress?.collection : null}
|
||||
{progress ? `(${progress.current}/${progress.total}) ` : null}
|
||||
...Please wait.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
keyExtractor={(item) => item.name || item.filename}
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
data={restoring || loading ? [] : files}
|
||||
renderItem={renderItem}
|
||||
ListFooterComponent={
|
||||
restoring || loading || files.length === 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -86,16 +86,16 @@ const Sort = ({ type, screen }) => {
|
||||
<Button
|
||||
title={
|
||||
groupOptions?.sortDirection === "asc"
|
||||
? groupOptions.groupBy === "abc" ||
|
||||
groupOptions.sortBy === "title"
|
||||
? groupOptions?.groupBy === "abc" ||
|
||||
groupOptions?.sortBy === "title"
|
||||
? "A - Z"
|
||||
: groupOptions.sortBy === "dueDate"
|
||||
: groupOptions?.sortBy === "dueDate"
|
||||
? "Earliest first"
|
||||
: "Old - New"
|
||||
: groupOptions.groupBy === "abc" ||
|
||||
groupOptions.sortBy === "title"
|
||||
: groupOptions?.groupBy === "abc" ||
|
||||
groupOptions?.sortBy === "title"
|
||||
? "Z - A"
|
||||
: groupOptions.sortBy === "dueDate"
|
||||
: groupOptions?.sortBy === "dueDate"
|
||||
? "Latest first"
|
||||
: "New - Old"
|
||||
}
|
||||
@@ -134,7 +134,7 @@ const Sort = ({ type, screen }) => {
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{groupOptions.groupBy === "abc" ? (
|
||||
{groupOptions?.groupBy === "abc" ? (
|
||||
<Button
|
||||
type="secondary"
|
||||
title="Title"
|
||||
@@ -154,18 +154,18 @@ const Sort = ({ type, screen }) => {
|
||||
item === "dateEdited") ? null : (
|
||||
<Button
|
||||
key={item}
|
||||
type={groupOptions.sortBy === item ? "selected" : "plain"}
|
||||
type={groupOptions?.sortBy === item ? "selected" : "plain"}
|
||||
title={SORT[item]}
|
||||
height={40}
|
||||
iconPosition="left"
|
||||
icon={groupOptions.sortBy === item ? "check" : null}
|
||||
icon={groupOptions?.sortBy === item ? "check" : null}
|
||||
style={{
|
||||
marginRight: 10,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
buttonType={{
|
||||
text:
|
||||
groupOptions.sortBy === item
|
||||
groupOptions?.sortBy === item
|
||||
? colors.primary.accent
|
||||
: colors.secondary.paragraph
|
||||
}}
|
||||
@@ -213,11 +213,11 @@ const Sort = ({ type, screen }) => {
|
||||
key={item}
|
||||
testID={"btn-" + item}
|
||||
type={
|
||||
groupOptions.groupBy === GROUP[item] ? "selected" : "plain"
|
||||
groupOptions?.groupBy === GROUP[item] ? "selected" : "plain"
|
||||
}
|
||||
buttonType={{
|
||||
text:
|
||||
groupOptions.groupBy === GROUP[item]
|
||||
groupOptions?.groupBy === GROUP[item]
|
||||
? colors.primary.accent
|
||||
: colors.secondary.paragraph
|
||||
}}
|
||||
@@ -235,7 +235,7 @@ const Sort = ({ type, screen }) => {
|
||||
updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
height={40}
|
||||
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
|
||||
icon={groupOptions?.groupBy === GROUP[item] ? "check" : null}
|
||||
title={
|
||||
item.slice(0, 1).toUpperCase() + item.slice(1, item.length)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import { eSendEvent } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
interface TabProps extends ViewProps {
|
||||
dimensions: { width: number; height: number };
|
||||
@@ -85,7 +86,9 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
);
|
||||
const translateX = useSharedValue(
|
||||
widths
|
||||
? appState && !appState?.movedAway
|
||||
? appState &&
|
||||
appState?.movedAway === false &&
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
? widths.sidebar + widths.list
|
||||
: widths.sidebar
|
||||
: 0
|
||||
@@ -121,9 +124,9 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
} else {
|
||||
if (prevWidths.current?.sidebar !== widths.sidebar) {
|
||||
translateX.value =
|
||||
!appState || (appState && appState?.movedAway)
|
||||
? widths.sidebar
|
||||
: editorPosition;
|
||||
appState && appState?.movedAway === false
|
||||
? editorPosition
|
||||
: widths.sidebar;
|
||||
if (translateX.value === editorPosition) {
|
||||
onChangeTab?.({ i: 2, from: 1 });
|
||||
}
|
||||
|
||||
@@ -1,235 +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, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eHideToast, eShowToast } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
export const Toast = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [data, setData] = useState({});
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const hideTimeout = useRef();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const toastMessages = useRef([]);
|
||||
|
||||
const show = useCallback(
|
||||
async (data) => {
|
||||
if (!data) return;
|
||||
if (data.context !== context) return;
|
||||
if (
|
||||
toastMessages.current.findIndex((m) => m.message === data.message) >= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
toastMessages.current.push(data);
|
||||
if (toastMessages.current?.length > 1) return;
|
||||
setData(data);
|
||||
|
||||
setVisible(true);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hide();
|
||||
}, data.duration);
|
||||
},
|
||||
[context, hide]
|
||||
);
|
||||
|
||||
const next = useCallback(
|
||||
(data) => {
|
||||
if (!data) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
setData(data);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hide();
|
||||
}, data?.duration);
|
||||
},
|
||||
[hide]
|
||||
);
|
||||
|
||||
const hide = useCallback(() => {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
let msg =
|
||||
toastMessages.current.length > 1 ? toastMessages.current.shift() : null;
|
||||
|
||||
if (msg) {
|
||||
setVisible(false);
|
||||
next(msg);
|
||||
setTimeout(() => {
|
||||
setVisible(true);
|
||||
}, 300);
|
||||
} else {
|
||||
setVisible(false);
|
||||
toastMessages.current.shift();
|
||||
setTimeout(() => {
|
||||
setData({});
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [next]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowToast, show);
|
||||
eSubscribeEvent(eHideToast, hide);
|
||||
return () => {
|
||||
toastMessages.current = [];
|
||||
eUnSubscribeEvent(eShowToast, show);
|
||||
eUnSubscribeEvent(eHideToast, hide);
|
||||
};
|
||||
}, [hide, show]);
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<TouchableOpacity
|
||||
onPress={hide}
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
width: DDS.isTab ? 400 : "100%",
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
minHeight: 30,
|
||||
top: insets.top + 10,
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
elevation: 15
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
maxWidth: "95%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
minWidth: data?.func ? "95%" : "50%",
|
||||
alignSelf: "center",
|
||||
borderRadius: 5,
|
||||
minHeight: 30,
|
||||
paddingVertical: 10,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 5,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "95%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexGrow: 1,
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 30,
|
||||
borderRadius: 100,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={data?.type === "success" ? "check" : "close"}
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
data?.type === "error"
|
||||
? colors.error.icon
|
||||
: colors.primary.accent
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
paddingRight: 25
|
||||
}}
|
||||
>
|
||||
{data?.heading ? (
|
||||
<Heading
|
||||
color={colors.primary.paragraph}
|
||||
size={SIZE.md}
|
||||
onPress={() => {
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{data.heading}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{data?.message ? (
|
||||
<Paragraph
|
||||
color={colors.primary.paragraph}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
paddingRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{data.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{data.func ? (
|
||||
<Button
|
||||
testID={notesnook.toast.button}
|
||||
fontSize={SIZE.md}
|
||||
type={data.type === "error" ? "errorShade" : "transparent"}
|
||||
onPress={data.func}
|
||||
title={data.actionText}
|
||||
height={30}
|
||||
style={{
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
);
|
||||
};
|
||||
219
apps/mobile/app/components/toast/index.tsx
Normal file
219
apps/mobile/app/components/toast/index.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
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, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastOptions
|
||||
} from "../../services/event-manager";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eHideToast, eShowToast } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Toast = ({ context = "global" }) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [toastOptions, setToastOptions] = useState<ToastOptions | undefined>();
|
||||
const hideTimeout = useRef<NodeJS.Timeout | undefined>();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const toastMessages = useRef<ToastOptions[]>([]);
|
||||
const dimensions = useWindowDimensions();
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
const hideToast = useCallback(() => {
|
||||
const nextToastMessage = toastMessages.current.shift();
|
||||
if (nextToastMessage) {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
setVisible(true);
|
||||
setToastOptions(nextToastMessage);
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hideToast();
|
||||
}, nextToastMessage?.duration);
|
||||
} else {
|
||||
setVisible(false);
|
||||
setToastOptions(undefined);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showToast = useCallback(
|
||||
(data?: ToastOptions) => {
|
||||
if (
|
||||
!data ||
|
||||
data.context !== context ||
|
||||
toastMessages.current.findIndex((m) => m.message === data.message) != -1
|
||||
)
|
||||
return;
|
||||
|
||||
toastMessages.current.push(data);
|
||||
if (toastMessages.current?.length > 1) return;
|
||||
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
setVisible(true);
|
||||
const nextToastMessage = toastMessages.current.shift();
|
||||
setToastOptions(nextToastMessage);
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hideToast();
|
||||
}, nextToastMessage?.duration);
|
||||
},
|
||||
[context, hideToast]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowToast, showToast);
|
||||
eSubscribeEvent(eHideToast, hideToast);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eShowToast, showToast);
|
||||
eUnSubscribeEvent(eHideToast, hideToast);
|
||||
};
|
||||
}, [hideToast, showToast]);
|
||||
|
||||
const isFullToastMessage = toastOptions?.heading && toastOptions?.message;
|
||||
|
||||
return visible && toastOptions ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
hideToast();
|
||||
}}
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
width: DDS.isTab ? dimensions.width / 2 : "100%",
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
bottom:
|
||||
Platform.OS === "android"
|
||||
? Math.max(insets.bottom, 40)
|
||||
: Math.max(insets.bottom, 40) +
|
||||
(keyboard.keyboardShown ? keyboard.keyboardHeight : 0),
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
elevation: 15
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
backgroundColor: isDark ? colors.static.black : colors.static.white,
|
||||
alignSelf: "center",
|
||||
borderRadius: 100,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
maxWidth: "95%",
|
||||
gap: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
toastOptions.type === "success"
|
||||
? "check"
|
||||
: toastOptions.type === "info"
|
||||
? "information"
|
||||
: "close"
|
||||
}
|
||||
size={isFullToastMessage ? SIZE.xxxl : SIZE.xl}
|
||||
color={
|
||||
toastOptions?.icon
|
||||
? toastOptions?.icon
|
||||
: toastOptions.type === "error"
|
||||
? colors.error.icon
|
||||
: toastOptions.type === "info"
|
||||
? isDark
|
||||
? colors.static.white
|
||||
: colors.static.black
|
||||
: colors.success.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingRight: toastOptions?.func ? 0 : 12,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{isFullToastMessage ? (
|
||||
<Heading
|
||||
color={!isDark ? colors.static.black : colors.static.white}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{toastOptions.heading}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{toastOptions.message || toastOptions.heading ? (
|
||||
<Paragraph
|
||||
color={!isDark ? colors.static.black : colors.static.white}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
{toastOptions.message || toastOptions.heading}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{toastOptions.func ? (
|
||||
<Button
|
||||
testID={notesnook.toast.button}
|
||||
fontSize={SIZE.md}
|
||||
type={toastOptions.type === "error" ? "errorShade" : "transparent"}
|
||||
onPress={toastOptions.func}
|
||||
title={toastOptions.actionText}
|
||||
height={35}
|
||||
style={{
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : null;
|
||||
};
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
DimensionValue,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
@@ -92,6 +93,9 @@ export const Button = ({
|
||||
});
|
||||
const textColor = buttonType?.text ? buttonType.text : text;
|
||||
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const growFactor = 1 + (fontScale - 1) / 10;
|
||||
|
||||
const Component = bold ? Heading : Paragraph;
|
||||
|
||||
return (
|
||||
@@ -108,7 +112,7 @@ export const Button = ({
|
||||
NativeTooltip.show(event, tooltipText, NativeTooltip.POSITIONS.TOP);
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
disabled={loading || restProps.disabled}
|
||||
type={type}
|
||||
accentColor={accentColor}
|
||||
accentText={accentText}
|
||||
@@ -117,14 +121,18 @@ export const Button = ({
|
||||
customOpacity={buttonType?.opacity}
|
||||
customAlpha={buttonType?.alpha}
|
||||
style={{
|
||||
height: height,
|
||||
width: (width as DimensionValue) || undefined,
|
||||
height: typeof height === "number" ? height * growFactor : height,
|
||||
width:
|
||||
typeof width === "number"
|
||||
? width * growFactor
|
||||
: (width as DimensionValue) || undefined,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 5,
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
opacity: restProps?.disabled ? 0.5 : 1,
|
||||
...(style as ViewStyle)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -41,6 +41,7 @@ import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../icon-button";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import phone from "phone";
|
||||
import isURL from "validator/lib/isURL";
|
||||
|
||||
interface InputProps extends TextInputProps {
|
||||
fwdRef?: RefObject<TextInput>;
|
||||
@@ -49,7 +50,8 @@ interface InputProps extends TextInputProps {
|
||||
| "email"
|
||||
| "confirmPassword"
|
||||
| "username"
|
||||
| "phonenumber";
|
||||
| "phonenumber"
|
||||
| "url";
|
||||
loading?: boolean;
|
||||
onSubmit?: (
|
||||
event: NativeSyntheticEvent<TextInputSubmitEditingEventData>
|
||||
@@ -151,6 +153,9 @@ const Input = ({
|
||||
case "confirmPassword":
|
||||
isError = customValidator && value === customValidator();
|
||||
break;
|
||||
case "url":
|
||||
isError = isURL(value);
|
||||
break;
|
||||
case "phonenumber": {
|
||||
const result = phone(value, {
|
||||
strictDetection: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user