Compare commits
1084 Commits
fix-github
...
fix/invali
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
318c43ede9 | ||
|
|
9ef32e8081 | ||
|
|
5f53593a26 | ||
|
|
1f404944d9 | ||
|
|
23dca36475 | ||
|
|
3dd8e20e9a | ||
|
|
8f412f9487 | ||
|
|
bf790219db | ||
|
|
c6af310b2d | ||
|
|
8d89b5dca9 | ||
|
|
fa35244607 | ||
|
|
d96815fa36 | ||
|
|
4a9f5b1d8b | ||
|
|
43fac79011 | ||
|
|
f2c62c8d8b | ||
|
|
70c9dd0083 | ||
|
|
ae6d8c5f2b | ||
|
|
38e2250fd1 | ||
|
|
17e2c5a8a2 | ||
|
|
2e226f0f72 | ||
|
|
167c54578b | ||
|
|
fb656af5ca | ||
|
|
ddfab272d2 | ||
|
|
28958311c1 | ||
|
|
f1b8275b4b | ||
|
|
c8c75d40dd | ||
|
|
bace90808d | ||
|
|
fe7c7e8db4 | ||
|
|
519a569366 | ||
|
|
89d602a40f | ||
|
|
a8db65b5af | ||
|
|
716239abfd | ||
|
|
62dd5a5416 | ||
|
|
96558c5df5 | ||
|
|
45997dd2e4 | ||
|
|
bde190f01c | ||
|
|
042c97688f | ||
|
|
95b5114070 | ||
|
|
20742e2a1c | ||
|
|
1e3149284e | ||
|
|
d77c09f1fd | ||
|
|
aa602ea8e9 | ||
|
|
8ab6b6dc31 | ||
|
|
404d136a73 | ||
|
|
cf9aeecd7e | ||
|
|
1adda96ce2 | ||
|
|
6dd6d818c9 | ||
|
|
c7e83961d4 | ||
|
|
03428e5848 | ||
|
|
d0fceed56d | ||
|
|
b8c68b5163 | ||
|
|
2496004896 | ||
|
|
16ec35de25 | ||
|
|
d248b8e2b8 | ||
|
|
641e7484e3 | ||
|
|
a46409ff44 | ||
|
|
5dbabc2706 | ||
|
|
2d4d656dd9 | ||
|
|
ab51b22e4a | ||
|
|
781d09daca | ||
|
|
f21533e5e4 | ||
|
|
2ea9b04863 | ||
|
|
675d517cbc | ||
|
|
c336c57bda | ||
|
|
7b5fa09c71 | ||
|
|
d85974ed7a | ||
|
|
9598ea7542 | ||
|
|
42048ad53a | ||
|
|
1fbd2efb92 | ||
|
|
fe91ef79a0 | ||
|
|
583d585b2f | ||
|
|
94b28f86ca | ||
|
|
f927f05f86 | ||
|
|
bde8dea048 | ||
|
|
511bea9b23 | ||
|
|
eda95530de | ||
|
|
5173decaa5 | ||
|
|
00dcb55f6e | ||
|
|
6dbe1073cd | ||
|
|
9be3e4fcfa | ||
|
|
f9f0a866bd | ||
|
|
daafeb2fa8 | ||
|
|
a92b34b81c | ||
|
|
e29b6838b3 | ||
|
|
16ff7c92e4 | ||
|
|
07e1ea6edf | ||
|
|
b26ede95fb | ||
|
|
971793a3ad | ||
|
|
6a2b5e1499 | ||
|
|
d18b775d06 | ||
|
|
d6cc899d95 | ||
|
|
ea10058276 | ||
|
|
568c88be3e | ||
|
|
f047552f18 | ||
|
|
9a58430b5d | ||
|
|
205885edf5 | ||
|
|
8ffb49b700 | ||
|
|
25b18a2edd | ||
|
|
cfea85c8fb | ||
|
|
82fc6d31c3 | ||
|
|
fb2de1753e | ||
|
|
ceb5ce5cdd | ||
|
|
98d443001c | ||
|
|
5115dee069 | ||
|
|
0dc29c7026 | ||
|
|
a5eb5e9df3 | ||
|
|
b680575328 | ||
|
|
c94ef616a5 | ||
|
|
3ae40c09fc | ||
|
|
5de4e00ce2 | ||
|
|
99e4e61a55 | ||
|
|
1ad7bfb82f | ||
|
|
30ef679432 | ||
|
|
bb97f5e757 | ||
|
|
6cd5a04470 | ||
|
|
0c32ebf163 | ||
|
|
e150e08f29 | ||
|
|
bbd603f10c | ||
|
|
677944cba9 | ||
|
|
dd2cee53d3 | ||
|
|
60c8d5ac89 | ||
|
|
59276de406 | ||
|
|
67981be516 | ||
|
|
77dfcec08c | ||
|
|
c4360ab95f | ||
|
|
17c4b72381 | ||
|
|
e91cc2bdca | ||
|
|
811f2305ec | ||
|
|
98a707b5f5 | ||
|
|
aa9b24fa10 | ||
|
|
0b3060e4c0 | ||
|
|
d523db84cc | ||
|
|
6a6a9ae6b3 | ||
|
|
b1bc28e069 | ||
|
|
f95c068bc1 | ||
|
|
0e4cd087d4 | ||
|
|
f217b8cfec | ||
|
|
d3787efbf0 | ||
|
|
e279878b12 | ||
|
|
154a227be0 | ||
|
|
756259c907 | ||
|
|
453ed4820a | ||
|
|
10ab612366 | ||
|
|
8dc4f7ed3d | ||
|
|
a73538bafc | ||
|
|
014887c7dc | ||
|
|
246e49bd8c | ||
|
|
146ac12935 | ||
|
|
4f5245adcf | ||
|
|
0bc0cc5729 | ||
|
|
1136072eff | ||
|
|
4cc8a538a4 | ||
|
|
271913ba20 | ||
|
|
3efe541869 | ||
|
|
b3bcdd4f9c | ||
|
|
13f739763c | ||
|
|
bde2b26133 | ||
|
|
6eef65952d | ||
|
|
3d85795216 | ||
|
|
f1a1989990 | ||
|
|
76ff5eddd0 | ||
|
|
068ee8dc68 | ||
|
|
bbf0fb641b | ||
|
|
3cae22faf1 | ||
|
|
34a4a27ba0 | ||
|
|
d96a78c741 | ||
|
|
c9e5ee9ddb | ||
|
|
f43c2a40f5 | ||
|
|
b12a0e979c | ||
|
|
d0372b82db | ||
|
|
dac1441add | ||
|
|
a65ebec7ae | ||
|
|
d5cf65e568 | ||
|
|
424073298e | ||
|
|
580c83687e | ||
|
|
de226a8ab6 | ||
|
|
8b94b08066 | ||
|
|
18c460a8b2 | ||
|
|
fbb3a56c0c | ||
|
|
48f602d4fe | ||
|
|
ad2b68c8ed | ||
|
|
c2eafccf2a | ||
|
|
5ca74b6127 | ||
|
|
143a38ade6 | ||
|
|
40cff39713 | ||
|
|
3efa1140b2 | ||
|
|
64f28aedbf | ||
|
|
74e3a28e2a | ||
|
|
58c324b2ac | ||
|
|
25fa2dc406 | ||
|
|
3e76f3b8c6 | ||
|
|
a12a3fbdc8 | ||
|
|
46c492c92a | ||
|
|
dbace71754 | ||
|
|
ece5b56bdd | ||
|
|
c35dd826dc | ||
|
|
675324f6af | ||
|
|
8788a14229 | ||
|
|
c8f697c5b1 | ||
|
|
48a4d67c13 | ||
|
|
e4289bb69e | ||
|
|
7000c67535 | ||
|
|
0eb61c80fa | ||
|
|
4bbd43e54b | ||
|
|
ce949d94f2 | ||
|
|
bdbf205745 | ||
|
|
899ab2ddb1 | ||
|
|
a302276af9 | ||
|
|
ee14a292ab | ||
|
|
646949c96c | ||
|
|
ebfb5c29dd | ||
|
|
3932ae62b0 | ||
|
|
f671d2dec0 | ||
|
|
6261f3f2c6 | ||
|
|
1b9802b8e0 | ||
|
|
ca95af7043 | ||
|
|
0aac2eb1e0 | ||
|
|
c10d083dab | ||
|
|
27c6cdec47 | ||
|
|
8fd0f97dfe | ||
|
|
59d65a2cbb | ||
|
|
9bd978b8c6 | ||
|
|
d6e22f2e3e | ||
|
|
a9a1290530 | ||
|
|
b8eab5bddc | ||
|
|
6836d6551f | ||
|
|
8e07ae8b26 | ||
|
|
b013a1cfa7 | ||
|
|
0650d00ca1 | ||
|
|
c048f789e7 | ||
|
|
9c158dfa53 | ||
|
|
168be74277 | ||
|
|
016532ef42 | ||
|
|
7368796a72 | ||
|
|
b3e10de0a1 | ||
|
|
dd674d38df | ||
|
|
a427e32796 | ||
|
|
b44d2e5c9b | ||
|
|
c42d046188 | ||
|
|
ae5f9f0f4a | ||
|
|
1681a587b9 | ||
|
|
b43d51aa72 | ||
|
|
f99d4bc013 | ||
|
|
430c2f51f6 | ||
|
|
b55b521def | ||
|
|
c26e2d800d | ||
|
|
310de9f950 | ||
|
|
9109c5180a | ||
|
|
db57c125d6 | ||
|
|
3ec177c631 | ||
|
|
fa561df14f | ||
|
|
b0abf56f20 | ||
|
|
a4ac051af0 | ||
|
|
7a2db59516 | ||
|
|
76c33e3cbc | ||
|
|
fd295b226a | ||
|
|
9bb82aebfe | ||
|
|
38c6dd3953 | ||
|
|
0517f61164 | ||
|
|
17289f2df6 | ||
|
|
44a7f057ac | ||
|
|
be5d0820d5 | ||
|
|
23bd4a564f | ||
|
|
be41cf1252 | ||
|
|
cf643fe0b0 | ||
|
|
988efa4577 | ||
|
|
631160c2c9 | ||
|
|
ebe3f2b771 | ||
|
|
3e300d98c4 | ||
|
|
1a1e673a6c | ||
|
|
6ce08b5aa3 | ||
|
|
2ef0d5b5cd | ||
|
|
a15978bf6a | ||
|
|
8fb8c54157 | ||
|
|
9f4647be47 | ||
|
|
1a38da30f4 | ||
|
|
c3d244b3b6 | ||
|
|
751af3c2d1 | ||
|
|
ec00779062 | ||
|
|
165ba064a9 | ||
|
|
2763068e21 | ||
|
|
210bdeaed0 | ||
|
|
3037a153dd | ||
|
|
474fb597c6 | ||
|
|
6c19968ef6 | ||
|
|
38dedcbc3c | ||
|
|
0e066d5d63 | ||
|
|
d45a65edea | ||
|
|
98e3c2955b | ||
|
|
9c0eca0891 | ||
|
|
32c377a0ea | ||
|
|
02145ec17e | ||
|
|
9e1a864f54 | ||
|
|
4989dc4301 | ||
|
|
4f43e4eab5 | ||
|
|
d59219e62b | ||
|
|
5f5209af8c | ||
|
|
6a9e6065d1 | ||
|
|
00119dbefe | ||
|
|
c466af60a6 | ||
|
|
e5512974d5 | ||
|
|
7861fa0b11 | ||
|
|
c20dbf1158 | ||
|
|
9e7c302693 | ||
|
|
e007f2592c | ||
|
|
3a5ea3e8ef | ||
|
|
0a0edd4814 | ||
|
|
62bbea01e6 | ||
|
|
3730596030 | ||
|
|
7f176942ba | ||
|
|
72a09f48e0 | ||
|
|
632291e901 | ||
|
|
17352e783b | ||
|
|
d8729f8171 | ||
|
|
8ac8f8f56a | ||
|
|
a2cfa8873f | ||
|
|
13a7f39d27 | ||
|
|
4e22a5605b | ||
|
|
0afcf8cbab | ||
|
|
c54759e766 | ||
|
|
b1d13c125f | ||
|
|
85aab9506c | ||
|
|
728b63b001 | ||
|
|
b91567e2cf | ||
|
|
c3179542a6 | ||
|
|
7ea575a1b8 | ||
|
|
d2eeb93339 | ||
|
|
ca59670b68 | ||
|
|
a1117d77f3 | ||
|
|
e7ee455ee3 | ||
|
|
9ad9c0c633 | ||
|
|
ec2a6f26e6 | ||
|
|
ead4f59ba7 | ||
|
|
c4f0cd18ad | ||
|
|
77d5bd71f9 | ||
|
|
eb0aff2fc3 | ||
|
|
e4e1eb0ced | ||
|
|
116a017938 | ||
|
|
6dfbb27c33 | ||
|
|
097c6915fb | ||
|
|
97490a5e37 | ||
|
|
799678efca | ||
|
|
e3afb1d068 | ||
|
|
d32db74ccc | ||
|
|
e24452c386 | ||
|
|
68e97f17d0 | ||
|
|
9040a4561d | ||
|
|
1ed8205f45 | ||
|
|
feaa207f68 | ||
|
|
76e766583c | ||
|
|
f15446b1d6 | ||
|
|
18be9fda41 | ||
|
|
9b3430b5d9 | ||
|
|
5b79fbd7b5 | ||
|
|
cc0be4565d | ||
|
|
41865fb286 | ||
|
|
37507d4f0a | ||
|
|
755a8d45e9 | ||
|
|
357d872362 | ||
|
|
63be4088a5 | ||
|
|
7e4d123a51 | ||
|
|
1a2ce6ba72 | ||
|
|
adb3805ec7 | ||
|
|
328f324d9e | ||
|
|
108c9e2944 | ||
|
|
f2a5ed6642 | ||
|
|
e1339617b5 | ||
|
|
4f22010c56 | ||
|
|
831b8497b2 | ||
|
|
37dd691270 | ||
|
|
4349445910 | ||
|
|
669849018a | ||
|
|
71ad15b4b6 | ||
|
|
732185a69d | ||
|
|
479dec6998 | ||
|
|
ff6b65c55c | ||
|
|
6e5493f149 | ||
|
|
b185338be2 | ||
|
|
b4161644d0 | ||
|
|
61e0fdea50 | ||
|
|
cb124c7445 | ||
|
|
fce0d50061 | ||
|
|
dac2a22824 | ||
|
|
655bdf3798 | ||
|
|
907bfd04ed | ||
|
|
38ca451d7e | ||
|
|
52800b39e4 | ||
|
|
9d700f6fd1 | ||
|
|
0a5f0d407e | ||
|
|
a84f3a4434 | ||
|
|
1dbd2d8f5f | ||
|
|
ec1001067d | ||
|
|
0374aa17c4 | ||
|
|
78cbce5ca0 | ||
|
|
65c988cf36 | ||
|
|
1487bf027a | ||
|
|
c5ec5f127d | ||
|
|
2627c83e79 | ||
|
|
d54f2351f6 | ||
|
|
460c2b4662 | ||
|
|
cf5fac7d41 | ||
|
|
8d4ae8e970 | ||
|
|
f9db4d49b1 | ||
|
|
728b22f337 | ||
|
|
de42a895dd | ||
|
|
0b813b3fa1 | ||
|
|
d13ff079a4 | ||
|
|
eca9f221ac | ||
|
|
48a3f11215 | ||
|
|
dc7fcb67f0 | ||
|
|
78e8f932c2 | ||
|
|
65678a0995 | ||
|
|
2b9381d40e | ||
|
|
0f795ff913 | ||
|
|
43cb5d278c | ||
|
|
c7142e409b | ||
|
|
3876cb5ec3 | ||
|
|
47862c3182 | ||
|
|
533bef014d | ||
|
|
f5ac0b93f6 | ||
|
|
d5a27b052c | ||
|
|
4a36606121 | ||
|
|
b69933444c | ||
|
|
48866efa57 | ||
|
|
c56445f817 | ||
|
|
18afc0a49c | ||
|
|
f38c4bbbac | ||
|
|
de125a33d1 | ||
|
|
81b7e34b0d | ||
|
|
5d2853d002 | ||
|
|
ad41544403 | ||
|
|
c8bf1fc2cd | ||
|
|
3d25a1d5c2 | ||
|
|
a8896b847f | ||
|
|
a1b3327aa8 | ||
|
|
e9431af138 | ||
|
|
8b07508204 | ||
|
|
3e0d9f92af | ||
|
|
0e4a2c7593 | ||
|
|
a7a2dcf791 | ||
|
|
47221dc81b | ||
|
|
6fb8f11d2f | ||
|
|
0cfd607806 | ||
|
|
a86565a8ec | ||
|
|
3ee231e459 | ||
|
|
335021090d | ||
|
|
b001107135 | ||
|
|
dba1d8645d | ||
|
|
9dff23bdd5 | ||
|
|
90e48cbc16 | ||
|
|
4a1a912d0f | ||
|
|
8a6328ef07 | ||
|
|
a66c72e099 | ||
|
|
652cbcda52 | ||
|
|
199c5d3e64 | ||
|
|
8b1b569fee | ||
|
|
d869a69477 | ||
|
|
00995431f4 | ||
|
|
cec7c5f3b1 | ||
|
|
47ae03ba8a | ||
|
|
3aa3858981 | ||
|
|
67fa1d6bb4 | ||
|
|
00ecb149df | ||
|
|
d931d20006 | ||
|
|
b4a0e97e3f | ||
|
|
8edcfcf2f1 | ||
|
|
6a8ccf9401 | ||
|
|
e2eab24574 | ||
|
|
6a4e31e972 | ||
|
|
04f54f79fa | ||
|
|
c148c6b9cf | ||
|
|
030cbefe3f | ||
|
|
85342e5f74 | ||
|
|
46565aa45c | ||
|
|
8496423b50 | ||
|
|
e39be83f70 | ||
|
|
3a6d1e7a76 | ||
|
|
15fc1d0cca | ||
|
|
685322f02a | ||
|
|
b849861146 | ||
|
|
91e041108c | ||
|
|
76f2ac2c1d | ||
|
|
bc11999201 | ||
|
|
103b1b88f5 | ||
|
|
5b703fe53a | ||
|
|
ab33aa7ebd | ||
|
|
4db043b111 | ||
|
|
3efe977c49 | ||
|
|
08c03bc3a4 | ||
|
|
5cb373f730 | ||
|
|
123f1a2658 | ||
|
|
72645ce0ad | ||
|
|
3f375065fc | ||
|
|
3d816751f6 | ||
|
|
f44c553749 | ||
|
|
9c0ee7e63c | ||
|
|
ae3a73c334 | ||
|
|
fe0129e1a4 | ||
|
|
274395d01b | ||
|
|
5deb5253c1 | ||
|
|
55e3213e5c | ||
|
|
bc9b5c39ee | ||
|
|
7a48c7c1e3 | ||
|
|
324256a499 | ||
|
|
eaea7bdd2a | ||
|
|
5985e22777 | ||
|
|
5de7fc5bd7 | ||
|
|
b54457f522 | ||
|
|
f94341facc | ||
|
|
90a19e7152 | ||
|
|
32998b9c8c | ||
|
|
88532ae13b | ||
|
|
64a138fa80 | ||
|
|
8fe2774d8f | ||
|
|
8ef42dd605 | ||
|
|
5b44206931 | ||
|
|
29f92a0806 | ||
|
|
03f8332c67 | ||
|
|
144d362527 | ||
|
|
7a6ccb932d | ||
|
|
94c1556a5c | ||
|
|
5974ffc0d4 | ||
|
|
6184281694 | ||
|
|
c51406ca04 | ||
|
|
07db957f67 | ||
|
|
d31194bb39 | ||
|
|
44827d13fe | ||
|
|
c847ca0817 | ||
|
|
bd102ea346 | ||
|
|
688dde9890 | ||
|
|
adfef8faef | ||
|
|
7785bcd5a8 | ||
|
|
569f4537e7 | ||
|
|
43a03079a1 | ||
|
|
852802da4b | ||
|
|
ce4e0a1916 | ||
|
|
ce53495b76 | ||
|
|
42571b0794 | ||
|
|
3a2eba3242 | ||
|
|
2d9f41e114 | ||
|
|
8dada6d936 | ||
|
|
2c4170bacc | ||
|
|
86ff1890f1 | ||
|
|
2ef17b800c | ||
|
|
0c0d91f8fd | ||
|
|
ba7b88f5d8 | ||
|
|
47a396ffaf | ||
|
|
f60fcefd1c | ||
|
|
57e1629b0c | ||
|
|
5988640d3f | ||
|
|
6512ace33b | ||
|
|
0a36b5ba5b | ||
|
|
832652730a | ||
|
|
ae7908e000 | ||
|
|
b40fe805e7 | ||
|
|
09f37ae872 | ||
|
|
ef6c4bb6fb | ||
|
|
4fc5ec00ae | ||
|
|
9267433d74 | ||
|
|
dbda1eff82 | ||
|
|
c8f9925973 | ||
|
|
ff87bebe8f | ||
|
|
54f1945a58 | ||
|
|
2a547c879b | ||
|
|
cfa98578ea | ||
|
|
270cb16d30 | ||
|
|
06113db2ff | ||
|
|
3efb79308a | ||
|
|
b0b5bd235f | ||
|
|
4a2fcf950e | ||
|
|
e166a634f2 | ||
|
|
b97ec3948b | ||
|
|
e14bb065dc | ||
|
|
80a77871a0 | ||
|
|
51963f799c | ||
|
|
71d94853ba | ||
|
|
c33c365b62 | ||
|
|
2ce31fc81c | ||
|
|
d9521b5058 | ||
|
|
366948cd28 | ||
|
|
ad338aeefc | ||
|
|
6b6ee76c39 | ||
|
|
3f23507a74 | ||
|
|
815c9eb7de | ||
|
|
aab2b0307e | ||
|
|
b9e7cbd608 | ||
|
|
0cf0b00c58 | ||
|
|
29e07036b1 | ||
|
|
61b1aa919a | ||
|
|
90b7b5f87b | ||
|
|
7103167693 | ||
|
|
77e0f5af17 | ||
|
|
0618341cf8 | ||
|
|
42a4acb688 | ||
|
|
131b1a0aa6 | ||
|
|
e327c92e1a | ||
|
|
d22301ccf9 | ||
|
|
5d2c615a30 | ||
|
|
4a2088ec1e | ||
|
|
238510132f | ||
|
|
b7a8277749 | ||
|
|
0251363d65 | ||
|
|
a784f47430 | ||
|
|
cf28b27da6 | ||
|
|
1fa020c569 | ||
|
|
ead87f7068 | ||
|
|
e35f3c5a96 | ||
|
|
c5282c9ddd | ||
|
|
fc21a8be97 | ||
|
|
c3e315a2ae | ||
|
|
ca30433fd5 | ||
|
|
03240345eb | ||
|
|
156b844fd9 | ||
|
|
f1a6662f01 | ||
|
|
4e5b5e6e3f | ||
|
|
b80dd8c81e | ||
|
|
2cee5020dd | ||
|
|
1a00fc6e30 | ||
|
|
64697a7771 | ||
|
|
d97be2d4fb | ||
|
|
6504beda78 | ||
|
|
3795c69a7b | ||
|
|
77b6ad5408 | ||
|
|
480b4398da | ||
|
|
7ed7d26d6f | ||
|
|
2463ad2820 | ||
|
|
85c50bf0fa | ||
|
|
54ba654b97 | ||
|
|
e58fc38a6d | ||
|
|
f0ef9e072e | ||
|
|
cd62069b9b | ||
|
|
8089f7e181 | ||
|
|
c1ec580fdc | ||
|
|
ae43d58a38 | ||
|
|
2713f26f90 | ||
|
|
783a97e340 | ||
|
|
dcea1544b9 | ||
|
|
9e0286052f | ||
|
|
a41b7d8fd8 | ||
|
|
9473c30426 | ||
|
|
8bf7609d2f | ||
|
|
c63572abd5 | ||
|
|
a003e396b8 | ||
|
|
c4323b7814 | ||
|
|
f27c7e9cf4 | ||
|
|
613c1ca30f | ||
|
|
b4542a532b | ||
|
|
6d8e5a1d1d | ||
|
|
951f628351 | ||
|
|
3ff6b5afd1 | ||
|
|
5aef851eb0 | ||
|
|
f14a72ec1c | ||
|
|
484fc4f5a4 | ||
|
|
6b9567a0be | ||
|
|
0f2a88124b | ||
|
|
740a7b4918 | ||
|
|
6fd3c77348 | ||
|
|
70818c24cd | ||
|
|
69c20045c2 | ||
|
|
c65a6e5e62 | ||
|
|
4a2da72fa3 | ||
|
|
2d83fde2c5 | ||
|
|
3898cc7752 | ||
|
|
c633864073 | ||
|
|
77ee56dd14 | ||
|
|
6c47a485f0 | ||
|
|
9f241959cf | ||
|
|
da20e7977a | ||
|
|
0931c0d5f2 | ||
|
|
cdebfdd7c0 | ||
|
|
ef6ff34ec6 | ||
|
|
9a42273a50 | ||
|
|
75a358a4ce | ||
|
|
6c122c6498 | ||
|
|
963f331290 | ||
|
|
9798ada335 | ||
|
|
2f17ad69a1 | ||
|
|
505d71f1c7 | ||
|
|
4ed6bdd668 | ||
|
|
b87dce6a51 | ||
|
|
295eb8ffec | ||
|
|
c7b2046e46 | ||
|
|
254c2acede | ||
|
|
4ea4bbd782 | ||
|
|
e3f08668ed | ||
|
|
d6113f75ca | ||
|
|
1dcd227014 | ||
|
|
8af190e83d | ||
|
|
dd06e5fd52 | ||
|
|
cd55f443d9 | ||
|
|
974221d938 | ||
|
|
02c084ed6d | ||
|
|
456927548a | ||
|
|
53490cc28b | ||
|
|
15014d2d49 | ||
|
|
3ee14b9463 | ||
|
|
9e46f1ec06 | ||
|
|
47c433b2c9 | ||
|
|
72d5072cac | ||
|
|
6b14b29103 | ||
|
|
e3ef25714a | ||
|
|
ac1bf9e84e | ||
|
|
54abea488f | ||
|
|
9d1ff3231a | ||
|
|
eef8b98b62 | ||
|
|
a4e0fecfe9 | ||
|
|
370837278b | ||
|
|
c03d25ece6 | ||
|
|
34be765ac5 | ||
|
|
bf208921ad | ||
|
|
14b469d8b6 | ||
|
|
cc239d927c | ||
|
|
1c7e619b55 | ||
|
|
3205dae30c | ||
|
|
57da24821f | ||
|
|
16f4184e25 | ||
|
|
4c51fbf816 | ||
|
|
f7835f43f6 | ||
|
|
aeabd9929c | ||
|
|
94a0d841ac | ||
|
|
3172d5a552 | ||
|
|
a042f6d418 | ||
|
|
d9ff5e0053 | ||
|
|
7ce9121f8e | ||
|
|
e0c1d6344d | ||
|
|
6c378a0122 | ||
|
|
1371b14005 | ||
|
|
bf7f3eb86f | ||
|
|
c8716df6a6 | ||
|
|
fd12d4bd00 | ||
|
|
29d0f40c1a | ||
|
|
63fd526db8 | ||
|
|
0fbfc709da | ||
|
|
19f0488745 | ||
|
|
f81c30222a | ||
|
|
26ae76d644 | ||
|
|
f370118498 | ||
|
|
d3a32421ee | ||
|
|
af9a83117b | ||
|
|
ce418487ad | ||
|
|
9bd7dbc940 | ||
|
|
bc91a41bbe | ||
|
|
7efbc666a0 | ||
|
|
5ecd70e121 | ||
|
|
e4d165edb7 | ||
|
|
d24f29e98f | ||
|
|
5559738654 | ||
|
|
54a321cb4c | ||
|
|
b2f0e74ade | ||
|
|
a105c09841 | ||
|
|
6ad8d7ba1b | ||
|
|
c58bb57de0 | ||
|
|
aec183bd78 | ||
|
|
75ed6de07f | ||
|
|
8f56953915 | ||
|
|
7d98f93dd6 | ||
|
|
e74123bc9c | ||
|
|
d21b4c021d | ||
|
|
ca05865021 | ||
|
|
b30ed83f78 | ||
|
|
a4d0b2d0dc | ||
|
|
fa1c48be41 | ||
|
|
ec29cc0801 | ||
|
|
44e9a69faa | ||
|
|
920598f943 | ||
|
|
5afdd88eb7 | ||
|
|
81ebc3906f | ||
|
|
ec7d581c3d | ||
|
|
60e1da412c | ||
|
|
27cf0a3c6b | ||
|
|
e24beebd43 | ||
|
|
9bea47e358 | ||
|
|
779f000f3e | ||
|
|
37eeeb1d64 | ||
|
|
c231c07633 | ||
|
|
4ee82b4fd6 | ||
|
|
d0f0c32ffa | ||
|
|
2c931d72f9 | ||
|
|
1cf832f0fe | ||
|
|
1e49fd8145 | ||
|
|
bd93048aaa | ||
|
|
1b5dc9eb14 | ||
|
|
e93490acfc | ||
|
|
487306b943 | ||
|
|
573a37f671 | ||
|
|
8bfee794c8 | ||
|
|
aea31102f6 | ||
|
|
bfe9aa5330 | ||
|
|
a45c5db34e | ||
|
|
3e588e243c | ||
|
|
03dba9f624 | ||
|
|
b707690817 | ||
|
|
3b7660834d | ||
|
|
f4b14e37ae | ||
|
|
146223e2b6 | ||
|
|
d6b037c178 | ||
|
|
3df8cc53e3 | ||
|
|
63ab54cc42 | ||
|
|
569954b767 | ||
|
|
9722fab8f3 | ||
|
|
bad138d2fa | ||
|
|
a7e57354dd | ||
|
|
45fa486a6d | ||
|
|
a65d271cbd | ||
|
|
feba11626d | ||
|
|
a5afc47d82 | ||
|
|
9edf6d165d | ||
|
|
12b8a9e487 | ||
|
|
9e8b5487b2 | ||
|
|
305a522a65 | ||
|
|
2f77390a8e | ||
|
|
0ab704b115 | ||
|
|
c353f7a872 | ||
|
|
9a242f63eb | ||
|
|
cc07cedd4a | ||
|
|
f1f4cd67f7 | ||
|
|
ceb948bd1f | ||
|
|
90a0878600 | ||
|
|
26a63591bd | ||
|
|
87ee599da1 | ||
|
|
7843a153a7 | ||
|
|
72331faee7 | ||
|
|
28470d60c6 | ||
|
|
ab47c89ef7 | ||
|
|
b086e652c3 | ||
|
|
ef2d8bf5af | ||
|
|
d3221abd7e | ||
|
|
448d19e9fd | ||
|
|
0a973cd83a | ||
|
|
4bff11bf12 | ||
|
|
f127203a5e | ||
|
|
dc3caa16d0 | ||
|
|
1f33ea9db3 | ||
|
|
4e157455c8 | ||
|
|
2bb3652be2 | ||
|
|
becc00d2ee | ||
|
|
e6186c0bbd | ||
|
|
6538540f3b | ||
|
|
a437125730 | ||
|
|
339e5965bb | ||
|
|
e3eb3c8000 | ||
|
|
2ef0f41612 | ||
|
|
0a38a734f5 | ||
|
|
c2d11cdc79 | ||
|
|
0fcab638a3 | ||
|
|
b37853866f | ||
|
|
e6642ac9ab | ||
|
|
c294e80230 | ||
|
|
996f4d29be | ||
|
|
0c5a5c7438 | ||
|
|
9926c0b6da | ||
|
|
8e691769ae | ||
|
|
add2d8af9b | ||
|
|
bc72c8b87c | ||
|
|
b73964afb9 | ||
|
|
96b6115e7b | ||
|
|
ea46488d94 | ||
|
|
df0e8ed567 | ||
|
|
1e86b4d772 | ||
|
|
b1e1ed982a | ||
|
|
500388a634 | ||
|
|
7a60ac17ce | ||
|
|
a6a2f848d7 | ||
|
|
25e6350c04 | ||
|
|
e0329c273f | ||
|
|
fd7b33fe30 | ||
|
|
6d70f8d2f9 | ||
|
|
c6bb28c139 | ||
|
|
6796206099 | ||
|
|
ca40342978 | ||
|
|
36150fb55b | ||
|
|
7b3f671159 | ||
|
|
0cd6191190 | ||
|
|
cbccb97d42 | ||
|
|
2ce9386759 | ||
|
|
f5b85a82d0 | ||
|
|
0123da10f7 | ||
|
|
1228f1b6fa | ||
|
|
3126c2d325 | ||
|
|
a9c9ca29c8 | ||
|
|
665f44bcee | ||
|
|
c444f7ddb8 | ||
|
|
787c36c754 | ||
|
|
3502469c1d | ||
|
|
c58b16cba6 | ||
|
|
15ea57b692 | ||
|
|
655cf0617e | ||
|
|
57311397e0 | ||
|
|
94db2387c8 | ||
|
|
bd1cf0fadc | ||
|
|
cfd53a8b2c | ||
|
|
baa2d1a389 | ||
|
|
8cd963a893 | ||
|
|
6e3ee9171b | ||
|
|
088ef2285f | ||
|
|
e127b8f4ab | ||
|
|
e218962c55 | ||
|
|
fa080b5ce2 | ||
|
|
d7d019f870 | ||
|
|
74b5c4d894 | ||
|
|
4ebc3af1e4 | ||
|
|
3188b33d19 | ||
|
|
c2c69c282e | ||
|
|
f57c7b5998 | ||
|
|
56faf3ed48 | ||
|
|
2fe8c87caa | ||
|
|
b7ecb7b6fb | ||
|
|
569b6c7939 | ||
|
|
e2f9b9c58d | ||
|
|
94150a92aa | ||
|
|
f81814a426 | ||
|
|
443b6ad7e9 | ||
|
|
2ceb68ec87 | ||
|
|
29f66d457d | ||
|
|
304d69f8b5 | ||
|
|
900a5b69a5 | ||
|
|
ebe1b641fd | ||
|
|
f26ea526e8 | ||
|
|
b36e6faac5 | ||
|
|
528fb3092b | ||
|
|
d5ee925c52 | ||
|
|
208e32d7ea | ||
|
|
5429161f91 | ||
|
|
81cdfcf61a | ||
|
|
6186e22006 | ||
|
|
4ff051130b | ||
|
|
97ae36dcda | ||
|
|
800b5f9d9a | ||
|
|
d4b1f32516 | ||
|
|
6626b856fb | ||
|
|
44df437c4e | ||
|
|
189a9da5b5 | ||
|
|
73def8a682 | ||
|
|
7c97db74dc | ||
|
|
ceb6e94d0c | ||
|
|
1a61c4ee8f | ||
|
|
d4aa4fdb90 | ||
|
|
3e7db7051b | ||
|
|
6b6e903322 | ||
|
|
2f62c72cfe | ||
|
|
8951352f63 | ||
|
|
6c9e265a11 | ||
|
|
1a14d37c72 | ||
|
|
5bfad0149b | ||
|
|
2168609577 | ||
|
|
7e8e981145 | ||
|
|
300e65f5c4 | ||
|
|
69590f9547 | ||
|
|
ae023bdf5b | ||
|
|
4556e4d230 | ||
|
|
a77c88bddd | ||
|
|
8672f92942 | ||
|
|
a8e81a32bf | ||
|
|
95b638a135 | ||
|
|
4302cdf0ea | ||
|
|
36d097f86c | ||
|
|
b364178fc0 | ||
|
|
2c7927bb08 | ||
|
|
af6041128b | ||
|
|
6ccee9f6eb | ||
|
|
404f2072a1 | ||
|
|
01e774bff5 | ||
|
|
df9fb59f40 | ||
|
|
9ac9e21645 | ||
|
|
44846cc7b5 | ||
|
|
96e36a6f73 | ||
|
|
37ad3ea31a | ||
|
|
07d2a701c7 | ||
|
|
94f7e8cd08 | ||
|
|
51819e9d4a | ||
|
|
860ec7fea8 | ||
|
|
21b3866df4 | ||
|
|
a91717aca3 | ||
|
|
91a272ee3d | ||
|
|
d51079fc88 | ||
|
|
37ae106713 | ||
|
|
b93b5cc8d6 | ||
|
|
9d6733deff | ||
|
|
86652230bc | ||
|
|
588e37aa20 | ||
|
|
79b75c5ba7 | ||
|
|
cc60558839 | ||
|
|
c7a6cd8964 | ||
|
|
5b537ab470 | ||
|
|
48af6c460f | ||
|
|
89c9c58631 | ||
|
|
c10f518e3a | ||
|
|
1f4cd6504a | ||
|
|
9ef40c01ce | ||
|
|
843be2a62c | ||
|
|
a881a6d51e | ||
|
|
22221afb7a | ||
|
|
21a7c64b0b | ||
|
|
41ea254301 | ||
|
|
3b15717150 | ||
|
|
94f02cdb07 | ||
|
|
061433d8cd | ||
|
|
c8ba88bb99 | ||
|
|
3b181cd9b2 | ||
|
|
5384bed382 | ||
|
|
ac0d831f22 | ||
|
|
8776058655 | ||
|
|
5c8c806228 | ||
|
|
aa2110b736 | ||
|
|
cbfaf6ae18 | ||
|
|
d815608701 | ||
|
|
696c280eaf | ||
|
|
b8c785fa3f | ||
|
|
d80fdfbfc1 | ||
|
|
f783dad211 | ||
|
|
c335d10167 | ||
|
|
bf7a02df5e | ||
|
|
bf12385099 | ||
|
|
f98b53e9b8 | ||
|
|
8f379d253e | ||
|
|
e1f07618d3 | ||
|
|
9a0c129844 | ||
|
|
aa2a8ab6f4 | ||
|
|
8e59ff1318 | ||
|
|
ad99c87f72 | ||
|
|
ba776b5854 | ||
|
|
e526145e48 | ||
|
|
45c5e65bf6 | ||
|
|
87bec4e2ab | ||
|
|
433e67a6dc | ||
|
|
6acba51b88 | ||
|
|
cf1322952d | ||
|
|
b5ac9dd430 | ||
|
|
a3b590bcb7 | ||
|
|
d4f7e3ef07 | ||
|
|
16119f6958 | ||
|
|
e39c2583a5 | ||
|
|
eecff009dd | ||
|
|
d9ed61a7da | ||
|
|
58db265702 | ||
|
|
bd39f0cbf8 | ||
|
|
255015108a | ||
|
|
b80f366f8d | ||
|
|
c845c7fce6 | ||
|
|
6e7b7ce0e1 | ||
|
|
9b728902d4 | ||
|
|
9a1f54c41a | ||
|
|
b3251eb016 | ||
|
|
f4064b7939 | ||
|
|
f014f08ff4 | ||
|
|
dee1cf4b5a | ||
|
|
f35a3d8fa3 | ||
|
|
ca9a8d14fa | ||
|
|
35c35fa9d7 | ||
|
|
09fff95d58 | ||
|
|
287307a1ba | ||
|
|
d628e62302 | ||
|
|
a070bc46ad | ||
|
|
9027c44511 | ||
|
|
722aa7b9c6 | ||
|
|
a4e777b24b | ||
|
|
af7120e953 | ||
|
|
d5adc3bbac | ||
|
|
aa3d778ad1 | ||
|
|
3465a816c6 | ||
|
|
3d1726c5aa | ||
|
|
488de2d583 | ||
|
|
c415b114a6 | ||
|
|
da36784e9c | ||
|
|
78fafdc272 | ||
|
|
7d57055f73 | ||
|
|
b84cc747cb | ||
|
|
9a4f3ccbad | ||
|
|
68de3cc928 | ||
|
|
6e135cdd36 | ||
|
|
696116ac85 | ||
|
|
08a13ff35a | ||
|
|
af1cea81dc | ||
|
|
2c22a7ca90 | ||
|
|
6fd6bd0375 | ||
|
|
1a96dc965a | ||
|
|
9550350f25 | ||
|
|
0ed605a5b9 | ||
|
|
18910d931c | ||
|
|
87648b7d8b | ||
|
|
7e7a1aeecb | ||
|
|
cb353358b7 | ||
|
|
ba1619711b | ||
|
|
b3cc9475ad |
@@ -15,6 +15,8 @@ apps/mobile/e2e/
|
|||||||
|
|
||||||
# web
|
# web
|
||||||
apps/web/public/an.js
|
apps/web/public/an.js
|
||||||
|
apps/web/src/common/sqlite/wa-sqlite-async.js
|
||||||
|
apps/web/src/common/sqlite/wa-sqlite.js
|
||||||
|
|
||||||
# editor
|
# editor
|
||||||
packages/editor/styles/
|
packages/editor/styles/
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ module.exports = {
|
|||||||
semi: ["error", "always"],
|
semi: ["error", "always"],
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"header/header": ["error", "block", LICENSE, 1]
|
"header/header": ["error", "block", LICENSE, 1],
|
||||||
|
"@typescript-eslint/no-empty-interface": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
allowSingleExtends: true
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 20.x
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|||||||
111
.github/workflows/android.publish.internal.yml
vendored
@@ -6,6 +6,11 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
|
env:
|
||||||
|
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||||
|
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||||
|
NDK_CCACHE: ccache
|
||||||
|
USE_CCACHE: 1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -24,30 +29,126 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||||
npm run bootstrap -- --scope=mobile
|
npm run bootstrap -- --scope=mobile
|
||||||
|
|
||||||
- name: Make Gradlew Executable
|
- name: Make Gradlew Executable
|
||||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||||
|
|
||||||
|
- name: Install CCache
|
||||||
|
uses: hendrikmuhs/ccache-action@v1.2.11
|
||||||
|
with:
|
||||||
|
key: ${{ runner.os }}-ccache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
max-size: 1500M
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-ccache-
|
||||||
|
|
||||||
|
- name: Setup CCache
|
||||||
|
run: |
|
||||||
|
ln -s $(which ccache) /usr/local/bin/gcc
|
||||||
|
ln -s $(which ccache) /usr/local/bin/g++
|
||||||
|
ln -s $(which ccache) /usr/local/bin/cc
|
||||||
|
ln -s $(which ccache) /usr/local/bin/c++
|
||||||
|
ln -s $(which ccache) /usr/local/bin/clang
|
||||||
|
ln -s $(which ccache) /usr/local/bin/clang++
|
||||||
|
ccache --set-config=base_dir=$PWD
|
||||||
|
ccache --set-config=inode_cache=true
|
||||||
|
ccache --set-config=compiler_check=content
|
||||||
|
ccache --set-config=depend_mode=true
|
||||||
|
ccache --set-config=file_clone=true
|
||||||
|
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
|
||||||
|
ccache -p
|
||||||
|
|
||||||
|
- name: Cache gradle
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
|
||||||
|
- name: Patch CPP Projects to use CCache
|
||||||
|
run: |
|
||||||
|
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||||
|
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
|
||||||
|
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
|
||||||
|
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||||
|
|
||||||
|
- name: CCache Stats Before Build
|
||||||
|
run: ccache -sv
|
||||||
|
|
||||||
- name: Build unsigned app bundle
|
- name: Build unsigned app bundle
|
||||||
run: yarn release:android:bundle
|
run: yarn release:android:bundle
|
||||||
|
|
||||||
|
- name: CCache Stats after build
|
||||||
|
run: ccache -sv
|
||||||
|
|
||||||
- name: Sign app bundle for Playstore release
|
- name: Sign app bundle for Playstore release
|
||||||
id: sign_app
|
id: sign_app
|
||||||
uses: r0adkll/sign-android-release@v1
|
uses: r0adkll/sign-android-release@master
|
||||||
with:
|
with:
|
||||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
alias: ${{ secrets.ALIAS }}
|
alias: ${{ secrets.ALIAS }}
|
||||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: "33.0.0"
|
||||||
|
|
||||||
|
- name: Build apks for Github release
|
||||||
|
run: yarn release:android
|
||||||
|
|
||||||
|
- name: Sign apk files
|
||||||
|
id: sign_apk
|
||||||
|
uses: r0adkll/sign-android-release@master
|
||||||
|
with:
|
||||||
|
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
|
||||||
|
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.PUBLIC_ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: "33.0.0"
|
||||||
|
|
||||||
|
- name: Rename apk files
|
||||||
|
run: |
|
||||||
|
cd apps/mobile/native/android/app/build/outputs/apk/release/
|
||||||
|
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
|
||||||
|
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
|
||||||
|
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
|
||||||
|
mv app-x86_64-release-unsigned-signed.apk notesnook-x86_64.apk
|
||||||
|
|
||||||
|
- name: Get app version
|
||||||
|
id: package-version
|
||||||
|
uses: martinbeentjes/npm-get-version-action@master
|
||||||
|
with:
|
||||||
|
path: apps/mobile
|
||||||
|
|
||||||
- name: Publish to Playstore
|
- name: Publish to Playstore
|
||||||
id: deploy
|
id: deploy
|
||||||
uses: r0adkll/upload-google-play@v1
|
uses: r0adkll/upload-google-play@v1.1.1
|
||||||
with:
|
with:
|
||||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||||
packageName: com.streetwriters.notesnook
|
packageName: com.streetwriters.notesnook
|
||||||
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||||
track: internal
|
track: alpha
|
||||||
status: completed
|
status: completed
|
||||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||||
|
|
||||||
|
- name: Create release draft on Github
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
|
||||||
|
name: Notesnook Android v${{ steps.package-version.outputs.current-version}} Beta
|
||||||
|
repository: streetwriters/notesnook
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: |
|
||||||
|
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
|
||||||
|
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
name: Notesnook.aab
|
||||||
|
path: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||||
|
|||||||
79
.github/workflows/android.publish.yml
vendored
@@ -6,6 +6,11 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
|
env:
|
||||||
|
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||||
|
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||||
|
NDK_CCACHE: ccache
|
||||||
|
USE_CCACHE: 1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -28,9 +33,54 @@ jobs:
|
|||||||
- name: Make Gradlew Executable
|
- name: Make Gradlew Executable
|
||||||
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
run: cd apps/mobile/native/android && chmod +x ./gradlew
|
||||||
|
|
||||||
|
- name: Install CCache
|
||||||
|
uses: hendrikmuhs/ccache-action@v1.2.11
|
||||||
|
with:
|
||||||
|
key: ${{ runner.os }}-ccache-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
max-size: 1500M
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-ccache-
|
||||||
|
|
||||||
|
- name: Setup CCache
|
||||||
|
run: |
|
||||||
|
ln -s $(which ccache) /usr/local/bin/gcc
|
||||||
|
ln -s $(which ccache) /usr/local/bin/g++
|
||||||
|
ln -s $(which ccache) /usr/local/bin/cc
|
||||||
|
ln -s $(which ccache) /usr/local/bin/c++
|
||||||
|
ln -s $(which ccache) /usr/local/bin/clang
|
||||||
|
ln -s $(which ccache) /usr/local/bin/clang++
|
||||||
|
ccache --set-config=base_dir=$PWD
|
||||||
|
ccache --set-config=inode_cache=true
|
||||||
|
ccache --set-config=compiler_check=content
|
||||||
|
ccache --set-config=depend_mode=true
|
||||||
|
ccache --set-config=file_clone=true
|
||||||
|
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
|
||||||
|
ccache -p
|
||||||
|
|
||||||
|
- name: Cache gradle
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
|
||||||
|
- name: Patch CPP Projects to use CCache
|
||||||
|
run: |
|
||||||
|
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||||
|
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
|
||||||
|
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
|
||||||
|
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
|
||||||
|
|
||||||
|
- name: CCache Stats Before Build
|
||||||
|
run: ccache -sv
|
||||||
|
|
||||||
- name: Build unsigned app bundle
|
- name: Build unsigned app bundle
|
||||||
run: yarn release:android:bundle
|
run: yarn release:android:bundle
|
||||||
|
|
||||||
|
- name: CCache Stats after build
|
||||||
|
run: ccache -sv
|
||||||
|
|
||||||
- name: Sign app bundle for Playstore release
|
- name: Sign app bundle for Playstore release
|
||||||
id: sign_app
|
id: sign_app
|
||||||
uses: r0adkll/sign-android-release@master
|
uses: r0adkll/sign-android-release@master
|
||||||
@@ -43,17 +93,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BUILD_TOOLS_VERSION: "33.0.0"
|
BUILD_TOOLS_VERSION: "33.0.0"
|
||||||
|
|
||||||
- name: Publish to Playstore
|
|
||||||
id: deploy
|
|
||||||
uses: r0adkll/upload-google-play@v1.1.1
|
|
||||||
with:
|
|
||||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
|
||||||
packageName: com.streetwriters.notesnook
|
|
||||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
|
||||||
track: production
|
|
||||||
status: completed
|
|
||||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
|
||||||
|
|
||||||
- name: Build apks for Github release
|
- name: Build apks for Github release
|
||||||
run: yarn release:android
|
run: yarn release:android
|
||||||
|
|
||||||
@@ -83,6 +122,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: apps/mobile
|
path: apps/mobile
|
||||||
|
|
||||||
|
- name: Publish to Playstore
|
||||||
|
id: deploy
|
||||||
|
uses: r0adkll/upload-google-play@v1.1.1
|
||||||
|
with:
|
||||||
|
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||||
|
packageName: com.streetwriters.notesnook
|
||||||
|
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||||
|
track: production
|
||||||
|
status: completed
|
||||||
|
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||||
|
|
||||||
|
|
||||||
- name: Create release draft on Github
|
- name: Create release draft on Github
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
@@ -96,3 +147,9 @@ jobs:
|
|||||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
|
||||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
|
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
|
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||||
|
|
||||||
|
- name: Upload signed aab to Github
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: Notesnook.aab
|
||||||
|
path: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||||
|
|||||||
32
.github/workflows/desktop.publish.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
run: npx nx build:desktop @notesnook/web
|
run: npx nx build:desktop @notesnook/web
|
||||||
|
|
||||||
- name: Archive build artifact
|
- name: Archive build artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: apps/web/build/**/*
|
path: apps/web/build/**/*
|
||||||
@@ -82,12 +82,13 @@ jobs:
|
|||||||
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
|
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
|
||||||
tag_name: v${{ steps.app_metadata.outputs.app_version }}
|
tag_name: v${{ steps.app_metadata.outputs.app_version }}
|
||||||
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
|
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
|
||||||
|
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
name: Build for macOS
|
name: Build for macOS
|
||||||
needs: build
|
needs: build
|
||||||
if: inputs.build-mac
|
if: inputs.build-mac
|
||||||
runs-on: macos-12
|
runs-on: macos-14
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
@@ -96,6 +97,9 @@ jobs:
|
|||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
|
|
||||||
|
- name: Install setuptools
|
||||||
|
run: brew install python-setuptools
|
||||||
|
|
||||||
- name: Setup notarization
|
- name: Setup notarization
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/private_keys/
|
mkdir -p ~/private_keys/
|
||||||
@@ -122,7 +126,7 @@ jobs:
|
|||||||
platform: "MAC_OS"
|
platform: "MAC_OS"
|
||||||
|
|
||||||
- name: Download build
|
- name: Download build
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: ./apps/web/build
|
path: ./apps/web/build
|
||||||
@@ -143,7 +147,7 @@ jobs:
|
|||||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||||
run: |
|
run: |
|
||||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
npx nx run release --project @notesnook/desktop -- --variant=mas
|
||||||
npx electron-builder --mac mas --universal -p never
|
yarn electron-builder --mac mas --universal -p never
|
||||||
working-directory: ./apps/desktop
|
working-directory: ./apps/desktop
|
||||||
|
|
||||||
- name: Build zip and dmg
|
- name: Build zip and dmg
|
||||||
@@ -153,13 +157,13 @@ jobs:
|
|||||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||||
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
|
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
|
||||||
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
||||||
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
|
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
|
||||||
run: |
|
run: |
|
||||||
npx nx run release --project @notesnook/desktop
|
npx nx run release --project @notesnook/desktop
|
||||||
if [ ${{ inputs.publish-github }} == true ]; then
|
if [ ${{ inputs.publish-github }} == true ]; then
|
||||||
npx electron-builder --mac zip dmg --arm64 --x64 -p always
|
yarn electron-builder --mac zip dmg --arm64 --x64 -p always
|
||||||
else
|
else
|
||||||
npx electron-builder --mac zip dmg --arm64 --x64 -p never
|
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||||
fi
|
fi
|
||||||
working-directory: ./apps/desktop
|
working-directory: ./apps/desktop
|
||||||
|
|
||||||
@@ -198,7 +202,7 @@ jobs:
|
|||||||
if: inputs.publish-snap
|
if: inputs.publish-snap
|
||||||
|
|
||||||
- name: Download build
|
- name: Download build
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: ./apps/web/build
|
path: ./apps/web/build
|
||||||
@@ -215,7 +219,7 @@ jobs:
|
|||||||
- name: Build snap
|
- name: Build snap
|
||||||
if: inputs.publish-snap
|
if: inputs.publish-snap
|
||||||
run: |
|
run: |
|
||||||
npx electron-builder --linux snap:x64 -p never
|
yarn electron-builder --linux snap:x64 -p never
|
||||||
working-directory: ./apps/desktop
|
working-directory: ./apps/desktop
|
||||||
|
|
||||||
- name: Build AppImage
|
- name: Build AppImage
|
||||||
@@ -223,9 +227,9 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
if [ ${{ inputs.publish-github }} == true ]; then
|
if [ ${{ inputs.publish-github }} == true ]; then
|
||||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
||||||
else
|
else
|
||||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||||
fi
|
fi
|
||||||
working-directory: ./apps/desktop
|
working-directory: ./apps/desktop
|
||||||
|
|
||||||
@@ -250,15 +254,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
dotnet-version: 6.0.x
|
dotnet-version: 6.0.x
|
||||||
|
|
||||||
- name: Install AzureCodeSigning
|
- name: Install TrustedSigning
|
||||||
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -Force -Repository PSGallery
|
run: Install-Module -Name TrustedSigning -RequiredVersion 0.3.15 -Force -Repository PSGallery
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
uses: ./.github/actions/setup-node-with-cache
|
||||||
|
|
||||||
- name: Download build
|
- name: Download build
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: ./apps/web/build
|
path: ./apps/web/build
|
||||||
|
|||||||
45
.github/workflows/ios.publish.yml
vendored
@@ -22,11 +22,45 @@ jobs:
|
|||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: npx nx run @notesnook/mobile:build
|
run: npx nx run @notesnook/mobile:build
|
||||||
|
|
||||||
|
- name: ccache
|
||||||
|
uses: hendrikmuhs/ccache-action@v1.2.11
|
||||||
|
with:
|
||||||
|
key: ${{ runner.os }}-ccache-${{ hashFiles(format('apps/mobile/native/ios/Podfile.lock')) }}
|
||||||
|
max-size: 1500M
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-ccache-
|
||||||
|
|
||||||
|
- name: Setup ccache
|
||||||
|
run: |
|
||||||
|
ccache --set-config=base_dir=$PWD
|
||||||
|
ccache --set-config=inode_cache=true
|
||||||
|
ccache --set-config=compiler_check=content
|
||||||
|
ccache --set-config=depend_mode=true
|
||||||
|
ccache --set-config=file_clone=true
|
||||||
|
ccache --set-config=sloppiness=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros
|
||||||
|
ln -s $(which ccache) /usr/local/bin/gcc
|
||||||
|
ln -s $(which ccache) /usr/local/bin/g++
|
||||||
|
ln -s $(which ccache) /usr/local/bin/cc
|
||||||
|
ln -s $(which ccache) /usr/local/bin/c++
|
||||||
|
ln -s $(which ccache) /usr/local/bin/clang
|
||||||
|
ln -s $(which ccache) /usr/local/bin/clang++
|
||||||
|
ccache -p
|
||||||
|
|
||||||
|
- name: Cache Pods
|
||||||
|
uses: actions/cache@v2
|
||||||
|
id: pods-cache
|
||||||
|
with:
|
||||||
|
path: apps/mobile/native/ios/Pods
|
||||||
|
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/native/ios/Podfile.lock') }}
|
||||||
|
|
||||||
- name: Install Pods
|
- name: Install Pods
|
||||||
run: npm run prepare:ios
|
run: npm run prepare:ios
|
||||||
|
|
||||||
|
- name: CCache Stats Before Build
|
||||||
|
run: ccache -sv
|
||||||
|
|
||||||
- name: Build iOS App
|
- name: Build iOS App
|
||||||
uses: yukiarrr/ios-build-action@v1.11.2
|
uses: ammarahm-ed/ios-build-action@master
|
||||||
with:
|
with:
|
||||||
bundle-identifier: org.streetwriters.notesnook
|
bundle-identifier: org.streetwriters.notesnook
|
||||||
scheme: Notesnook
|
scheme: Notesnook
|
||||||
@@ -52,15 +86,18 @@ jobs:
|
|||||||
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
||||||
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
||||||
|
|
||||||
- name: 'Upload app to TestFlight'
|
- name: CCache Stats After Build
|
||||||
|
run: ccache -sv
|
||||||
|
|
||||||
|
- name: "Upload app to TestFlight"
|
||||||
uses: apple-actions/upload-testflight-build@v1
|
uses: apple-actions/upload-testflight-build@v1
|
||||||
with:
|
with:
|
||||||
app-path: Notesnook.ipa
|
app-path: Notesnook.ipa
|
||||||
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||||
api-key-id: ${{ secrets.API_KEY_ID }}
|
api-key-id: ${{ secrets.API_KEY_ID }}
|
||||||
api-private-key: ${{ secrets.API_KEY }}
|
api-private-key: ${{ secrets.API_KEY }}
|
||||||
|
|
||||||
- name: Upload to Github
|
- name: Upload Notesnook.ipa to Github
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: Notesnook.ipa
|
name: Notesnook.ipa
|
||||||
|
|||||||
69
.github/workflows/web.beta.publish.yml
vendored
@@ -1,69 +0,0 @@
|
|||||||
name: Publish @notesnook/web Beta
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- '!v3-beta'
|
|
||||||
paths-ignore:
|
|
||||||
- "apps/mobile/**"
|
|
||||||
- "packages/editor-mobile/**"
|
|
||||||
- "apps/desktop/**"
|
|
||||||
- "apps/vericrypt/**"
|
|
||||||
- "servers/**"
|
|
||||||
- "packages/clipper/**"
|
|
||||||
- "extensions/web-clipper/**"
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- closed
|
|
||||||
- ready_for_review
|
|
||||||
- reopened
|
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- '!v3-beta'
|
|
||||||
paths-ignore:
|
|
||||||
- "apps/mobile/**"
|
|
||||||
- "packages/editor-mobile/**"
|
|
||||||
- "apps/desktop/**"
|
|
||||||
- "apps/vericrypt/**"
|
|
||||||
- "servers/**"
|
|
||||||
- "packages/clipper/**"
|
|
||||||
- "extensions/web-clipper/**"
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
|
||||||
|
|
||||||
- name: Install packages
|
|
||||||
run: |
|
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
|
||||||
npm run bootstrap -- --scope=web
|
|
||||||
|
|
||||||
- name: Setup environment
|
|
||||||
run: |
|
|
||||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
|
||||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
|
||||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Generate beta build
|
|
||||||
run: npm run build:beta:web
|
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages
|
|
||||||
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
|
|
||||||
id: pages
|
|
||||||
with:
|
|
||||||
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
cloudflare-project-name: notesnook-beta-app
|
|
||||||
directory: ./apps/web/build
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}
|
|
||||||
2
.github/workflows/web.tests.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
|||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: |
|
run: |
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||||
npm install -D @playwright/test otplib
|
npm install -D @playwright/test otplib dayjs @zip.js/zip.js
|
||||||
|
|
||||||
- name: Validate build extraction
|
- name: Validate build extraction
|
||||||
run: ls -ld ./apps/web/build/index.html
|
run: ls -ld ./apps/web/build/index.html
|
||||||
|
|||||||
40
.github/workflows/web.v3-beta.publish.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: Publish @notesnook/web v3 Beta
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: ./.github/actions/setup-node-with-cache
|
|
||||||
|
|
||||||
- name: Install packages
|
|
||||||
run: |
|
|
||||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
|
||||||
npm run bootstrap -- --scope=web
|
|
||||||
|
|
||||||
- name: Setup environment
|
|
||||||
run: |
|
|
||||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
|
||||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
|
||||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Generate beta build
|
|
||||||
run: npm run build:beta:web
|
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages
|
|
||||||
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
|
|
||||||
id: pages
|
|
||||||
with:
|
|
||||||
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
cloudflare-project-name: notesnook-v3-app
|
|
||||||
directory: ./apps/web/build
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}
|
|
||||||
3
.gitignore
vendored
@@ -9,4 +9,5 @@ nx-cloud.env
|
|||||||
.env.local
|
.env.local
|
||||||
.nyc_output
|
.nyc_output
|
||||||
site
|
site
|
||||||
node_modules.backup
|
node_modules.backup
|
||||||
|
.nx
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ apps/mobile/native/
|
|||||||
apps/web/public/an.js
|
apps/web/public/an.js
|
||||||
|
|
||||||
# editor
|
# editor
|
||||||
packages/editor/styles/
|
packages/editor/styles/
|
||||||
|
/.nx/cache
|
||||||
3
apps/desktop/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
output
|
output
|
||||||
dist
|
dist
|
||||||
|
_catalog
|
||||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 401 B |
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 543 B |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 763 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 7.3 KiB |
1497
apps/desktop/package-lock.json
generated
@@ -2,7 +2,7 @@
|
|||||||
"name": "@notesnook/desktop",
|
"name": "@notesnook/desktop",
|
||||||
"productName": "Notesnook",
|
"productName": "Notesnook",
|
||||||
"description": "Your private note taking space",
|
"description": "Your private note taking space",
|
||||||
"version": "2.6.15",
|
"version": "3.0.1",
|
||||||
"appAppleId": "1544027013",
|
"appAppleId": "1544027013",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -14,9 +14,11 @@
|
|||||||
"@notesnook/crypto": "file:../../packages/crypto",
|
"@notesnook/crypto": "file:../../packages/crypto",
|
||||||
"@trpc/client": "10.38.3",
|
"@trpc/client": "10.38.3",
|
||||||
"@trpc/server": "10.38.3",
|
"@trpc/server": "10.38.3",
|
||||||
|
"better-sqlite3-multiple-ciphers": "^9.5.0",
|
||||||
"electron-trpc": "0.5.2",
|
"electron-trpc": "0.5.2",
|
||||||
"electron-updater": "6.1.4",
|
"electron-updater": "6.1.4",
|
||||||
"icojs": "^0.17.1",
|
"icojs": "^0.17.1",
|
||||||
|
"sodium-native": "^4.1.1",
|
||||||
"typed-emitter": "^2.1.0",
|
"typed-emitter": "^2.1.0",
|
||||||
"yargs": "^17.6.2",
|
"yargs": "^17.6.2",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
@@ -25,12 +27,13 @@
|
|||||||
"@types/node": "18.16.1",
|
"@types/node": "18.16.1",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.24",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"electron": "25.9.8",
|
"electron": "^29.3.1",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-builder-notarize": "^1.5.1",
|
"esbuild": "^0.20.0",
|
||||||
"esbuild": "^0.17.19",
|
"node-gyp-build": "^4.8.0",
|
||||||
|
"prebuildify": "^6.0.1",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"undici": "^5.23.0"
|
"undici": "^6.14.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"dmg-license": "^1.0.11"
|
"dmg-license": "^1.0.11"
|
||||||
@@ -40,7 +43,7 @@
|
|||||||
"staging": "node scripts/build.mjs --run",
|
"staging": "node scripts/build.mjs --run",
|
||||||
"release": "node scripts/build.mjs",
|
"release": "node scripts/build.mjs",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
@@ -54,7 +57,7 @@
|
|||||||
"productName": "Notesnook",
|
"productName": "Notesnook",
|
||||||
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
||||||
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
||||||
"npmRebuild": false,
|
"generateUpdatesFilesForAllChannels": true,
|
||||||
"asar": false,
|
"asar": false,
|
||||||
"files": [
|
"files": [
|
||||||
"!*.chunk.js.map",
|
"!*.chunk.js.map",
|
||||||
@@ -64,9 +67,17 @@
|
|||||||
"!build/banner.jpg",
|
"!build/banner.jpg",
|
||||||
"!build/*.ico",
|
"!build/*.ico",
|
||||||
"!build/*.png",
|
"!build/*.png",
|
||||||
"!node_modules${/*}"
|
"!node_modules${/*}",
|
||||||
|
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
|
||||||
|
"node_modules/better-sqlite3-multiple-ciphers/lib",
|
||||||
|
"node_modules/better-sqlite3-multiple-ciphers/package.json",
|
||||||
|
"node_modules/file-uri-to-path",
|
||||||
|
"node_modules/bindings",
|
||||||
|
"node_modules/node-gyp-build",
|
||||||
|
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
|
||||||
|
"node_modules/sodium-native/index.js",
|
||||||
|
"node_modules/sodium-native/package.json"
|
||||||
],
|
],
|
||||||
"afterSign": "electron-builder-notarize",
|
|
||||||
"afterPack": "./scripts/removeLocales.js",
|
"afterPack": "./scripts/removeLocales.js",
|
||||||
"mac": {
|
"mac": {
|
||||||
"bundleVersion": "240",
|
"bundleVersion": "240",
|
||||||
@@ -94,7 +105,8 @@
|
|||||||
"entitlements": "assets/entitlements.mac.plist",
|
"entitlements": "assets/entitlements.mac.plist",
|
||||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||||
"gatekeeperAssess": false,
|
"gatekeeperAssess": false,
|
||||||
"icon": "assets/icons/app.icns"
|
"icon": "assets/icons/app.icns",
|
||||||
|
"notarize": true
|
||||||
},
|
},
|
||||||
"dmg": {
|
"dmg": {
|
||||||
"contents": [
|
"contents": [
|
||||||
@@ -138,7 +150,7 @@
|
|||||||
"signingHashAlgorithms": [
|
"signingHashAlgorithms": [
|
||||||
"sha256"
|
"sha256"
|
||||||
],
|
],
|
||||||
"sign": "./sign.js",
|
"sign": "./scripts/sign.js",
|
||||||
"icon": "assets/icons/app.ico"
|
"icon": "assets/icons/app.ico"
|
||||||
},
|
},
|
||||||
"portable": {
|
"portable": {
|
||||||
|
|||||||
46
apps/desktop/patches/node-gyp-build+4.8.0.patch
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs, { readFile, writeFile } from "fs/promises";
|
||||||
import { existsSync } from "fs";
|
import { existsSync, readFileSync } from "fs";
|
||||||
import yargs from "yargs-parser";
|
import yargs from "yargs-parser";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import * as childProcess from "child_process";
|
import * as childProcess from "child_process";
|
||||||
@@ -28,27 +28,30 @@ import { fileURLToPath } from "url";
|
|||||||
const args = yargs(process.argv);
|
const args = yargs(process.argv);
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const sodiumNativePrebuildPath = (arch) =>
|
const packageJson = JSON.parse(
|
||||||
path.join(
|
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
|
||||||
`node_modules`,
|
);
|
||||||
`@notesnook`,
|
|
||||||
`crypto`,
|
|
||||||
`node_modules`,
|
|
||||||
`@notesnook`,
|
|
||||||
`sodium`,
|
|
||||||
`node_modules`,
|
|
||||||
`sodium-native`,
|
|
||||||
`prebuilds`,
|
|
||||||
`${os.platform()}-${arch}`
|
|
||||||
);
|
|
||||||
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||||
|
|
||||||
await fs.rm("./build/", { force: true, recursive: true });
|
await fs.rm("./build/", { force: true, recursive: true });
|
||||||
|
|
||||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||||
await exec(`cd ${webAppPath} && npm run build:desktop`);
|
await exec(
|
||||||
|
"npx nx build:desktop @notesnook/web",
|
||||||
|
path.join(__dirname, "..", "..", "..")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// temporary until there's support for prebuilt binaries for linux ARM
|
||||||
|
if (os.platform() === "linux") await patchBetterSQLite3();
|
||||||
|
|
||||||
|
if (os.platform() === "win32")
|
||||||
|
await exec(
|
||||||
|
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||||
|
path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||||
|
);
|
||||||
|
|
||||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true
|
force: true
|
||||||
@@ -62,56 +65,39 @@ if (args.variant === "mas") {
|
|||||||
|
|
||||||
await exec(`npx tsc`);
|
await exec(`npx tsc`);
|
||||||
|
|
||||||
if (existsSync(sodiumNativePrebuildPath("x64"))) {
|
|
||||||
console.log("copying sodium-native-x64");
|
|
||||||
await fs.cp(
|
|
||||||
sodiumNativePrebuildPath("x64"),
|
|
||||||
path.join("build", "prebuilds", `${process.platform}-x64`),
|
|
||||||
{
|
|
||||||
recursive: true,
|
|
||||||
force: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsSync(sodiumNativePrebuildPath("ia32"))) {
|
|
||||||
console.log("copying sodium-native-ia32");
|
|
||||||
await fs.cp(
|
|
||||||
sodiumNativePrebuildPath("ia32"),
|
|
||||||
path.join("build", "prebuilds", `${process.platform}-ia32`),
|
|
||||||
{
|
|
||||||
recursive: true,
|
|
||||||
force: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsSync(sodiumNativePrebuildPath("arm64"))) {
|
|
||||||
console.log("copying sodium-native-arm64");
|
|
||||||
await fs.cp(
|
|
||||||
sodiumNativePrebuildPath("arm64"),
|
|
||||||
path.join("build", "prebuilds", `${process.platform}-arm64`),
|
|
||||||
{
|
|
||||||
recursive: true,
|
|
||||||
force: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.run) {
|
if (args.run) {
|
||||||
await exec(`npx electron-builder --dir --x64`);
|
await exec(`npx electron-builder --dir --x64`);
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||||
} else if (process.platform === "darwin") {
|
} else if (process.platform === "darwin") {
|
||||||
await exec(`./output/darwin-unpacked/Notesnook`);
|
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||||
} else {
|
} else {
|
||||||
await exec(`./output/linux-unpacked/Notesnook`);
|
await exec(`./output/linux-unpacked/Notesnook`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exec(cmd) {
|
async function exec(cmd, cwd) {
|
||||||
return childProcess.execSync(cmd, {
|
return childProcess.execSync(cmd, {
|
||||||
env: process.env,
|
env: { ...process.env, NOTESNOOK_STAGING: true },
|
||||||
stdio: "inherit"
|
stdio: "inherit",
|
||||||
|
cwd: cwd || process.cwd()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function patchBetterSQLite3() {
|
||||||
|
const jsonPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"node_modules",
|
||||||
|
"better-sqlite3-multiple-ciphers",
|
||||||
|
"package.json"
|
||||||
|
);
|
||||||
|
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||||
|
|
||||||
|
json.version = "9.5.1";
|
||||||
|
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
|
||||||
|
json.repository.url =
|
||||||
|
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
|
||||||
|
|
||||||
|
await writeFile(jsonPath, JSON.stringify(json));
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,21 +27,15 @@ import crypto from "crypto";
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const sodiumNativePrebuildPath = path.join(
|
|
||||||
`node_modules`,
|
|
||||||
`@notesnook`,
|
|
||||||
`crypto`,
|
|
||||||
`node_modules`,
|
|
||||||
`@notesnook`,
|
|
||||||
`sodium`,
|
|
||||||
`node_modules`,
|
|
||||||
`sodium-native`,
|
|
||||||
`prebuilds`
|
|
||||||
);
|
|
||||||
const RUNNING_PROCESSES = [];
|
const RUNNING_PROCESSES = [];
|
||||||
const RESTARTABLE_PROCESSES = [];
|
const RESTARTABLE_PROCESSES = [];
|
||||||
let lastBundleHash = null;
|
let lastBundleHash = null;
|
||||||
|
const ENV = {
|
||||||
|
...process.env,
|
||||||
|
NO_COLOR: "true",
|
||||||
|
FORCE_COLOR: "false",
|
||||||
|
COLOR: "0"
|
||||||
|
};
|
||||||
await onChange(true);
|
await onChange(true);
|
||||||
|
|
||||||
console.log("Watching...");
|
console.log("Watching...");
|
||||||
@@ -58,6 +52,8 @@ process.on("SIGINT", async (s) => {
|
|||||||
async function onChange(first) {
|
async function onChange(first) {
|
||||||
if (first) {
|
if (first) {
|
||||||
await fs.rm("./build/", { force: true, recursive: true });
|
await fs.rm("./build/", { force: true, recursive: true });
|
||||||
|
|
||||||
|
await exec("npx electron-builder install-app-deps");
|
||||||
}
|
}
|
||||||
|
|
||||||
await exec(`npm run bundle`);
|
await exec(`npm run bundle`);
|
||||||
@@ -68,13 +64,6 @@ async function onChange(first) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (first) {
|
|
||||||
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
|
|
||||||
recursive: true,
|
|
||||||
force: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first) {
|
if (first) {
|
||||||
await spawnAndWaitUntil(
|
await spawnAndWaitUntil(
|
||||||
["npx", "nx", "start:desktop", "@notesnook/web"],
|
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||||
@@ -102,7 +91,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
|
|||||||
|
|
||||||
const s = spawn(cmd[0], cmd.slice(1), {
|
const s = spawn(cmd[0], cmd.slice(1), {
|
||||||
cwd,
|
cwd,
|
||||||
env: { ...process.env, NO_COLOR: "true" },
|
env: ENV,
|
||||||
shell: false
|
shell: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,14 +105,15 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exec(cmd) {
|
async function exec(cmd, cwd) {
|
||||||
try {
|
try {
|
||||||
console.log(">", cmd);
|
console.log(">", cmd);
|
||||||
|
|
||||||
return execSync(cmd, {
|
return execSync(cmd, {
|
||||||
env: process.env,
|
env: ENV,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
shell: false
|
shell: false,
|
||||||
|
cwd: cwd || process.cwd()
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
//ignore
|
//ignore
|
||||||
@@ -136,7 +126,7 @@ function execAsync(cmd, args, restartable, onExit) {
|
|||||||
|
|
||||||
const proc = spawn(cmd, args, {
|
const proc = spawn(cmd, args, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
env: process.env,
|
env: ENV,
|
||||||
shell: false
|
shell: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,9 +169,10 @@ async function cleanup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function isBundleSame() {
|
async function isBundleSame() {
|
||||||
const bundle = await fs.readFile(
|
const bundle = Buffer.concat([
|
||||||
path.join(__dirname, "..", "build", "electron.js")
|
await fs.readFile(path.join(__dirname, "..", "build", "electron.js")),
|
||||||
);
|
await fs.readFile(path.join(__dirname, "..", "build", "preload.js"))
|
||||||
|
]);
|
||||||
|
|
||||||
const hashSum = crypto.createHash("sha256");
|
const hashSum = crypto.createHash("sha256");
|
||||||
hashSum.update(bundle);
|
hashSum.update(bundle);
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
const { writeFileSync, rmSync, readFileSync } = require("fs");
|
const { writeFileSync, rmSync } = require("fs");
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
const { relative, join } = require("path");
|
const { relative, join } = require("path");
|
||||||
|
|
||||||
module.exports = async function (configuration) {
|
module.exports = async function (configuration) {
|
||||||
|
if (process.env.NOTESNOOK_STAGING) return;
|
||||||
|
|
||||||
const Endpoint = "https://weu.codesigning.azure.net";
|
const Endpoint = "https://weu.codesigning.azure.net";
|
||||||
const CodeSigningAccountName = "Notesnook";
|
const CodeSigningAccountName = "Notesnook";
|
||||||
const CertificateProfileName = "Notesnook";
|
const CertificateProfileName = "Notesnook";
|
||||||
@@ -31,7 +33,7 @@ module.exports = async function (configuration) {
|
|||||||
const DescriptionUrl = "https://notesnook.com/";
|
const DescriptionUrl = "https://notesnook.com/";
|
||||||
const FilesCatalog = createCatalog(configuration.path);
|
const FilesCatalog = createCatalog(configuration.path);
|
||||||
|
|
||||||
const command = `Invoke-AzureCodeSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
|
const command = `Invoke-TrustedSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
|
||||||
|
|
||||||
console.debug("Signing", configuration.path, "using command", command);
|
console.debug("Signing", configuration.path, "using command", command);
|
||||||
|
|
||||||
@@ -39,16 +41,13 @@ module.exports = async function (configuration) {
|
|||||||
|
|
||||||
console.debug("Signed", configuration.path);
|
console.debug("Signed", configuration.path);
|
||||||
|
|
||||||
deleteCatalog();
|
rmSync(FilesCatalog);
|
||||||
};
|
};
|
||||||
|
|
||||||
function createCatalog(path) {
|
function createCatalog(path) {
|
||||||
writeFileSync("_catalog", relative(__dirname, path));
|
const catalogPath = join(__dirname, "_catalog");
|
||||||
return join(__dirname, "_catalog");
|
writeFileSync(catalogPath, relative(__dirname, path));
|
||||||
}
|
return catalogPath;
|
||||||
|
|
||||||
function deleteCatalog() {
|
|
||||||
rmSync("_catalog");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function psexec(cmd) {
|
function psexec(cmd) {
|
||||||
@@ -23,6 +23,8 @@ import { osIntegrationRouter } from "./os-integration";
|
|||||||
import { spellCheckerRouter } from "./spell-checker";
|
import { spellCheckerRouter } from "./spell-checker";
|
||||||
import { updaterRouter } from "./updater";
|
import { updaterRouter } from "./updater";
|
||||||
import { bridgeRouter } from "./bridge";
|
import { bridgeRouter } from "./bridge";
|
||||||
|
import { safeStorageRouter } from "./safe-storage";
|
||||||
|
import { windowRouter } from "./window";
|
||||||
|
|
||||||
const t = initTRPC.create();
|
const t = initTRPC.create();
|
||||||
|
|
||||||
@@ -31,7 +33,9 @@ export const router = t.router({
|
|||||||
integration: osIntegrationRouter,
|
integration: osIntegrationRouter,
|
||||||
spellChecker: spellCheckerRouter,
|
spellChecker: spellCheckerRouter,
|
||||||
updater: updaterRouter,
|
updater: updaterRouter,
|
||||||
bridge: bridgeRouter
|
bridge: bridgeRouter,
|
||||||
|
safeStorage: safeStorageRouter,
|
||||||
|
window: windowRouter
|
||||||
});
|
});
|
||||||
|
|
||||||
export const api = router.createCaller({});
|
export const api = router.createCaller({});
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import { observable } from "@trpc/server/observable";
|
|||||||
import { AssetManager } from "../utils/asset-manager";
|
import { AssetManager } from "../utils/asset-manager";
|
||||||
import { isFlatpak } from "../utils";
|
import { isFlatpak } from "../utils";
|
||||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||||
|
import { rm } from "fs/promises";
|
||||||
|
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
|
||||||
|
|
||||||
const t = initTRPC.create();
|
const t = initTRPC.create();
|
||||||
|
|
||||||
@@ -54,6 +56,23 @@ export const osIntegrationRouter = t.router({
|
|||||||
config.zoomFactor = factor;
|
config.zoomFactor = factor;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
customDns: t.procedure.query(() => config.customDns),
|
||||||
|
setCustomDns: t.procedure
|
||||||
|
.input(z.boolean())
|
||||||
|
.mutation(({ input: customDns }) => {
|
||||||
|
if (customDns) enableCustomDns();
|
||||||
|
else disableCustomDns();
|
||||||
|
config.customDns = customDns;
|
||||||
|
}),
|
||||||
|
|
||||||
|
proxyRules: t.procedure.query(() => config.proxyRules),
|
||||||
|
setProxyRules: t.procedure
|
||||||
|
.input(z.string().optional())
|
||||||
|
.mutation(({ input: proxyRules }) => {
|
||||||
|
globalThis.window?.webContents.session.setProxy({ proxyRules });
|
||||||
|
config.proxyRules = proxyRules || "";
|
||||||
|
}),
|
||||||
|
|
||||||
privacyMode: t.procedure.query(() => config.privacyMode),
|
privacyMode: t.procedure.query(() => config.privacyMode),
|
||||||
setPrivacyMode: t.procedure
|
setPrivacyMode: t.procedure
|
||||||
.input(z.object({ enabled: z.boolean() }))
|
.input(z.object({ enabled: z.boolean() }))
|
||||||
@@ -126,10 +145,13 @@ export const osIntegrationRouter = t.router({
|
|||||||
.input(z.object({ filePath: z.string() }))
|
.input(z.object({ filePath: z.string() }))
|
||||||
.query(({ input }) => {
|
.query(({ input }) => {
|
||||||
const { filePath } = input;
|
const { filePath } = input;
|
||||||
if (!filePath) return;
|
|
||||||
return resolvePath(filePath);
|
return resolvePath(filePath);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
deleteFile: t.procedure.input(z.string()).query(async ({ input }) => {
|
||||||
|
await rm(input);
|
||||||
|
}),
|
||||||
|
|
||||||
showNotification: t.procedure
|
showNotification: t.procedure
|
||||||
.input(NotificationOptions)
|
.input(NotificationOptions)
|
||||||
.query(({ input }) => {
|
.query(({ input }) => {
|
||||||
@@ -158,16 +180,42 @@ export const osIntegrationRouter = t.router({
|
|||||||
}),
|
}),
|
||||||
bringToFront: t.procedure.query(() => bringToFront()),
|
bringToFront: t.procedure.query(() => bringToFront()),
|
||||||
changeTheme: t.procedure
|
changeTheme: t.procedure
|
||||||
.input(Theme)
|
.input(
|
||||||
.mutation(({ input }) => setTheme(input)),
|
z.object({
|
||||||
|
theme: Theme,
|
||||||
|
windowControlsIconColor: z.string().optional(),
|
||||||
|
backgroundColor: z.string().optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(
|
||||||
|
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
|
||||||
|
if (windowControlsIconColor) {
|
||||||
|
config.windowControlsIconColor = windowControlsIconColor;
|
||||||
|
if (process.platform === "win32")
|
||||||
|
globalThis.window?.setTitleBarOverlay({
|
||||||
|
symbolColor: windowControlsIconColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backgroundColor) {
|
||||||
|
config.backgroundColor = backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(theme);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
onThemeChanged: t.procedure.subscription(() =>
|
onThemeChanged: t.procedure.subscription(() =>
|
||||||
observable<"dark" | "light">((emit) => {
|
observable<"dark" | "light">((emit) => {
|
||||||
nativeTheme.on("updated", () => {
|
const updated = () => {
|
||||||
if (getTheme() === "system") {
|
if (getTheme() === "system") {
|
||||||
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
|
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
nativeTheme.on("updated", updated);
|
||||||
|
return () => {
|
||||||
|
nativeTheme.off("updated", updated);
|
||||||
|
};
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
44
apps/desktop/src/api/safe-storage.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
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 { safeStorage } from "electron";
|
||||||
|
import { initTRPC } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const t = initTRPC.create();
|
||||||
|
|
||||||
|
export const safeStorageRouter = t.router({
|
||||||
|
isEncryptionAvailable: t.procedure.query(() => {
|
||||||
|
return (
|
||||||
|
!process.env.PORTABLE_EXECUTABLE_DIR &&
|
||||||
|
safeStorage.isEncryptionAvailable()
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* Takes a string and returns an encrypted base64 string
|
||||||
|
*/
|
||||||
|
encryptString: t.procedure.input(z.string()).query(({ input }) => {
|
||||||
|
return safeStorage.encryptString(input).toString("base64");
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* Takes an encrypted base64 string and returns a string
|
||||||
|
*/
|
||||||
|
decryptString: t.procedure.input(z.string()).query(({ input }) => {
|
||||||
|
return safeStorage.decryptString(Buffer.from(input, "base64"));
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -118,5 +118,13 @@ export const spellCheckerRouter = t.router({
|
|||||||
.mutation(({ input: { enabled } }) => {
|
.mutation(({ input: { enabled } }) => {
|
||||||
globalThis.window?.webContents.session.setSpellCheckerEnabled(enabled);
|
globalThis.window?.webContents.session.setSpellCheckerEnabled(enabled);
|
||||||
config.isSpellCheckerEnabled = enabled;
|
config.isSpellCheckerEnabled = enabled;
|
||||||
})
|
}),
|
||||||
|
words: t.procedure.query(() =>
|
||||||
|
globalThis.window?.webContents.session.listWordsInSpellCheckerDictionary()
|
||||||
|
),
|
||||||
|
deleteWord: t.procedure.input(z.string()).mutation(({ input: word }) => {
|
||||||
|
globalThis.window?.webContents.session.removeWordFromSpellCheckerDictionary(
|
||||||
|
word
|
||||||
|
);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,16 +28,20 @@ type UpdateInfo = { version: string };
|
|||||||
type Progress = { percent: number };
|
type Progress = { percent: number };
|
||||||
|
|
||||||
const t = initTRPC.create();
|
const t = initTRPC.create();
|
||||||
|
let cancellationToken: CancellationToken | undefined = undefined;
|
||||||
|
|
||||||
export const updaterRouter = t.router({
|
export const updaterRouter = t.router({
|
||||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||||
download: t.procedure.query(async () => {
|
download: t.procedure.query(async () => {
|
||||||
const cancellationToken = new CancellationToken();
|
if (cancellationToken) return;
|
||||||
await autoUpdater.downloadUpdate(cancellationToken);
|
cancellationToken = new CancellationToken();
|
||||||
|
await autoUpdater
|
||||||
|
.downloadUpdate(cancellationToken)
|
||||||
|
.finally(() => (cancellationToken = undefined));
|
||||||
}),
|
}),
|
||||||
check: t.procedure.query(async () => {
|
check: t.procedure.query(async () => {
|
||||||
await autoUpdater.checkForUpdates();
|
await autoUpdater.checkForUpdates().catch(console.error);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
toggleAutoUpdates: t.procedure
|
toggleAutoUpdates: t.procedure
|
||||||
@@ -71,6 +75,7 @@ function createSubscription<
|
|||||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||||
emit.next(args[0]);
|
emit.next(args[0]);
|
||||||
};
|
};
|
||||||
|
autoUpdater.removeAllListeners(eventName);
|
||||||
autoUpdater.addListener(eventName, listener);
|
autoUpdater.addListener(eventName, listener);
|
||||||
return () => {
|
return () => {
|
||||||
autoUpdater.removeListener(eventName, listener);
|
autoUpdater.removeListener(eventName, listener);
|
||||||
|
|||||||
83
apps/desktop/src/api/window.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
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 { initTRPC } from "@trpc/server";
|
||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
|
const t = initTRPC.create();
|
||||||
|
|
||||||
|
export const windowRouter = t.router({
|
||||||
|
maximize: t.procedure.mutation(() => {
|
||||||
|
globalThis.window?.maximize();
|
||||||
|
}),
|
||||||
|
restore: t.procedure.mutation(() => {
|
||||||
|
globalThis.window?.restore();
|
||||||
|
}),
|
||||||
|
minimze: t.procedure.mutation(() => {
|
||||||
|
globalThis.window?.minimize();
|
||||||
|
}),
|
||||||
|
maximized: t.procedure.query(() => globalThis.window?.isMaximized()),
|
||||||
|
fullscreen: t.procedure.query(() => globalThis.window?.isFullScreen()),
|
||||||
|
onWindowStateChanged: t.procedure.subscription(() => {
|
||||||
|
return observable<{ maximized: boolean; fullscreen: boolean }>((emit) => {
|
||||||
|
function listener() {
|
||||||
|
emit.next({
|
||||||
|
maximized: !!globalThis.window?.isMaximized(),
|
||||||
|
fullscreen: !!globalThis.window?.isFullScreen()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function enterFullscreen() {
|
||||||
|
emit.next({
|
||||||
|
maximized: !!globalThis.window?.isMaximized(),
|
||||||
|
fullscreen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function leaveFullscreen() {
|
||||||
|
emit.next({
|
||||||
|
maximized: !!globalThis.window?.isMaximized(),
|
||||||
|
fullscreen: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
globalThis.window?.addListener("maximize", listener);
|
||||||
|
globalThis.window?.addListener("minimize", listener);
|
||||||
|
globalThis.window?.addListener("unmaximize", listener);
|
||||||
|
globalThis.window?.addListener("restore", listener);
|
||||||
|
globalThis.window?.addListener("enter-full-screen", enterFullscreen);
|
||||||
|
globalThis.window?.addListener("leave-full-screen", leaveFullscreen);
|
||||||
|
globalThis.window?.addListener("leave-html-full-screen", leaveFullscreen);
|
||||||
|
globalThis.window?.addListener("enter-html-full-screen", enterFullscreen);
|
||||||
|
return () => {
|
||||||
|
globalThis.window?.removeListener("maximize", listener);
|
||||||
|
globalThis.window?.removeListener("minimize", listener);
|
||||||
|
globalThis.window?.removeListener("unmaximize", listener);
|
||||||
|
globalThis.window?.removeListener("restore", listener);
|
||||||
|
globalThis.window?.removeListener("enter-full-screen", enterFullscreen);
|
||||||
|
globalThis.window?.removeListener("leave-full-screen", leaveFullscreen);
|
||||||
|
globalThis.window?.removeListener(
|
||||||
|
"leave-html-full-screen",
|
||||||
|
leaveFullscreen
|
||||||
|
);
|
||||||
|
globalThis.window?.removeListener(
|
||||||
|
"enter-html-full-screen",
|
||||||
|
enterFullscreen
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -20,3 +20,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
export * from "./constants";
|
export * from "./constants";
|
||||||
export type { AppRouter } from "./api";
|
export type { AppRouter } from "./api";
|
||||||
export { type UpdateInfo } from "builder-util-runtime";
|
export { type UpdateInfo } from "builder-util-runtime";
|
||||||
|
export { type DesktopIntegration } from "./utils/config";
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import path from "path";
|
|||||||
import { bringToFront } from "./utils/bring-to-front";
|
import { bringToFront } from "./utils/bring-to-front";
|
||||||
import { bridge } from "./api/bridge";
|
import { bridge } from "./api/bridge";
|
||||||
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
||||||
|
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
|
||||||
|
|
||||||
// only run a single instance
|
// only run a single instance
|
||||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||||
@@ -82,6 +83,19 @@ async function createWindow() {
|
|||||||
size: 512,
|
size: 512,
|
||||||
format: process.platform === "win32" ? "ico" : "png"
|
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
|
||||||
|
},
|
||||||
|
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
zoomFactor: config.zoomFactor,
|
zoomFactor: config.zoomFactor,
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
@@ -112,6 +126,7 @@ async function createWindow() {
|
|||||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||||
"http://dictionaries.notesnook.com/"
|
"http://dictionaries.notesnook.com/"
|
||||||
);
|
);
|
||||||
|
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
|
||||||
|
|
||||||
mainWindow.once("closed", () => {
|
mainWindow.once("closed", () => {
|
||||||
globalThis.window = null;
|
globalThis.window = null;
|
||||||
@@ -121,7 +136,7 @@ async function createWindow() {
|
|||||||
setupJumplist();
|
setupJumplist();
|
||||||
|
|
||||||
if (isDevelopment())
|
if (isDevelopment())
|
||||||
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
|
mainWindow.webContents.openDevTools({ mode: "bottom", activate: true });
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url);
|
shell.openExternal(details.url);
|
||||||
@@ -137,6 +152,9 @@ async function createWindow() {
|
|||||||
app.once("ready", async () => {
|
app.once("ready", async () => {
|
||||||
console.info("App ready. Opening window.");
|
console.info("App ready. Opening window.");
|
||||||
|
|
||||||
|
if (config.customDns) enableCustomDns();
|
||||||
|
else disableCustomDns();
|
||||||
|
|
||||||
if (!isDevelopment()) registerProtocol();
|
if (!isDevelopment()) registerProtocol();
|
||||||
await createWindow();
|
await createWindow();
|
||||||
configureAutoUpdater();
|
configureAutoUpdater();
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ 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 { ipcRenderer } from "electron";
|
||||||
import { platform } from "os";
|
import { platform } from "os";
|
||||||
|
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var os: () => "mas" | ReturnType<typeof platform>;
|
var os: () => "mas" | ReturnType<typeof platform>;
|
||||||
var electronTRPC: RendererGlobalElectronTRPC;
|
var electronTRPC: RendererGlobalElectronTRPC;
|
||||||
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||||
|
var createSQLite3Database: (filename: string) => Database;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.once("loaded", async () => {
|
process.once("loaded", async () => {
|
||||||
@@ -40,11 +42,6 @@ process.once("loaded", async () => {
|
|||||||
globalThis.electronTRPC = electronTRPC;
|
globalThis.electronTRPC = electronTRPC;
|
||||||
});
|
});
|
||||||
|
|
||||||
globalThis.NativeNNCrypto =
|
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||||
process.platform === "win32" &&
|
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
|
||||||
process.arch !== "x64" &&
|
|
||||||
process.arch !== "ia32"
|
|
||||||
? undefined
|
|
||||||
: require("@notesnook/crypto").NNCrypto;
|
|
||||||
|
|
||||||
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ Type=Application
|
|||||||
Version=${app.getVersion()}
|
Version=${app.getVersion()}
|
||||||
Name=${app.getName()}
|
Name=${app.getName()}
|
||||||
Comment=${app.getName()} startup script
|
Comment=${app.getName()} startup script
|
||||||
Exec=${process.execPath}${hidden ? " --hidden" : ""}
|
Exec=${
|
||||||
|
process.env.APPIMAGE
|
||||||
|
? `${process.env.APPIMAGE}${hidden ? " --hidden" : ""}`
|
||||||
|
: `${process.execPath}${hidden ? " --hidden" : ""}`
|
||||||
|
}
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
Terminal=false`;
|
Terminal=false`;
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
import { autoUpdater } from "electron-updater";
|
import { autoUpdater } from "electron-updater";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
|
|
||||||
|
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
|
||||||
|
? "beta"
|
||||||
|
: "latest";
|
||||||
async function configureAutoUpdater() {
|
async function configureAutoUpdater() {
|
||||||
autoUpdater.setFeedURL({
|
autoUpdater.setFeedURL({
|
||||||
provider: "generic",
|
provider: "generic",
|
||||||
url: `https://notesnook.com/releases/${process.platform}/`,
|
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
|
||||||
useMultipleRangeRequest: false
|
useMultipleRangeRequest: false,
|
||||||
|
channel: CHANNEL
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.autoDownload = config.automaticUpdates;
|
autoUpdater.autoDownload = config.automaticUpdates;
|
||||||
|
|||||||
@@ -41,7 +41,13 @@ export const config = {
|
|||||||
isSpellCheckerEnabled: true,
|
isSpellCheckerEnabled: true,
|
||||||
zoomFactor: 1,
|
zoomFactor: 1,
|
||||||
theme: nativeTheme.themeSource,
|
theme: nativeTheme.themeSource,
|
||||||
automaticUpdates: true
|
automaticUpdates: true,
|
||||||
|
proxyRules: "",
|
||||||
|
customDns: true,
|
||||||
|
|
||||||
|
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
|
||||||
|
windowControlsIconColor:
|
||||||
|
nativeTheme.themeSource === "dark" ? "#ffffff" : "#000000"
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConfigKey = keyof typeof config;
|
type ConfigKey = keyof typeof config;
|
||||||
|
|||||||
37
apps/desktop/src/utils/custom-dns.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
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 { app } from "electron";
|
||||||
|
|
||||||
|
export function enableCustomDns() {
|
||||||
|
app.configureHostResolver({
|
||||||
|
secureDnsServers: [
|
||||||
|
"https://mozilla.cloudflare-dns.com/dns-query",
|
||||||
|
"https://dns.quad9.net/dns-query"
|
||||||
|
],
|
||||||
|
enableBuiltInResolver: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disableCustomDns() {
|
||||||
|
app.configureHostResolver({
|
||||||
|
secureDnsServers: [],
|
||||||
|
enableBuiltInResolver: true
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -131,6 +131,17 @@ function setupMenu() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.mediaType === "image")
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
id: "copy-image",
|
||||||
|
label: "Copy Image",
|
||||||
|
click() {
|
||||||
|
globalThis.window?.webContents.copyImageAt(params.x, params.y);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (params.isEditable)
|
if (params.isEditable)
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
|
|||||||
@@ -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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { protocol } from "electron";
|
import { net, protocol } from "electron";
|
||||||
import { isDevelopment } from "./index";
|
import { isDevelopment } from "./index";
|
||||||
import { createReadStream } from "fs";
|
import { createReadStream } from "fs";
|
||||||
import { extname, normalize } from "path";
|
import { extname, normalize } from "path";
|
||||||
@@ -62,26 +62,7 @@ function registerProtocol() {
|
|||||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (request.headers.has("X-Content-Length")) {
|
return net.fetch(request, { bypassCustomProtocolHandlers: true });
|
||||||
request.headers.set(
|
|
||||||
"Content-Length",
|
|
||||||
request.headers.get("X-Content-Length") || "0"
|
|
||||||
);
|
|
||||||
request.headers.delete("X-Content-Length");
|
|
||||||
}
|
|
||||||
const headers = Object.fromEntries(request.headers.entries());
|
|
||||||
|
|
||||||
return await fetch(request.url, {
|
|
||||||
signal: request.signal,
|
|
||||||
mode: request.mode,
|
|
||||||
headers,
|
|
||||||
method: request.method,
|
|
||||||
body: request.body,
|
|
||||||
credentials: request.credentials,
|
|
||||||
referrer: (request as any).referrer,
|
|
||||||
duplex: "half",
|
|
||||||
redirect: "manual"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.info(`${SCHEME} protocol inteception "successful"`);
|
console.info(`${SCHEME} protocol inteception "successful"`);
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ function setTheme(theme: Theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBackgroundColor() {
|
function getBackgroundColor() {
|
||||||
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
|
return (
|
||||||
|
config.backgroundColor ||
|
||||||
|
(nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSystemTheme() {
|
function getSystemTheme() {
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import "@azure/core-asynciterator-polyfill";
|
import "react-native-gesture-handler";
|
||||||
import {
|
import {
|
||||||
THEME_COMPATIBILITY_VERSION,
|
THEME_COMPATIBILITY_VERSION,
|
||||||
useThemeEngineStore
|
useThemeEngineStore
|
||||||
} from "@notesnook/theme";
|
} from "@notesnook/theme";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { I18nManager, View } from "react-native";
|
import { I18nManager, View } from "react-native";
|
||||||
import "react-native-gesture-handler";
|
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||||
@@ -42,14 +41,15 @@ I18nManager.allowRTL(false);
|
|||||||
I18nManager.forceRTL(false);
|
I18nManager.forceRTL(false);
|
||||||
I18nManager.swapLeftAndRightInRTL(false);
|
I18nManager.swapLeftAndRightInRTL(false);
|
||||||
|
|
||||||
SettingsService.checkOrientation();
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const init = useAppEvents();
|
const init = useAppEvents();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let { appLockMode } = SettingsService.get();
|
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||||
if (appLockMode && appLockMode !== "none") {
|
if (appLockEnabled || appLockMode !== "none") {
|
||||||
useUserStore.getState().lockApp(true);
|
useUserStore.getState().lockApp(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||||
init();
|
init();
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@@ -101,10 +101,10 @@ const App = () => {
|
|||||||
let currTheme =
|
let currTheme =
|
||||||
useThemeStore.getState().colorScheme === "dark"
|
useThemeStore.getState().colorScheme === "dark"
|
||||||
? SettingsService.getProperty("darkTheme")
|
? SettingsService.getProperty("darkTheme")
|
||||||
: SettingsService.getProperty("lightTheme");
|
: SettingsService.getProperty("lighTheme");
|
||||||
useThemeEngineStore.getState().setTheme(currTheme);
|
useThemeEngineStore.getState().setTheme(currTheme);
|
||||||
|
|
||||||
export const withTheme = (Element) => {
|
export const withTheme = (Element: () => JSX.Element) => {
|
||||||
return function AppWithThemeProvider() {
|
return function AppWithThemeProvider() {
|
||||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||||
state.colorScheme,
|
state.colorScheme,
|
||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 108 KiB |
@@ -17,16 +17,39 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
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 { Platform } from "react-native";
|
||||||
import "react-native-get-random-values";
|
import "react-native-get-random-values";
|
||||||
import * as Keychain from "react-native-keychain";
|
import * as Keychain from "react-native-keychain";
|
||||||
|
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||||
import { generateSecureRandom } from "react-native-securerandom";
|
import { generateSecureRandom } from "react-native-securerandom";
|
||||||
import Sodium from "@ammarahmed/react-native-sodium";
|
import { DatabaseLogger } from ".";
|
||||||
import { MMKV } from "./mmkv";
|
import { MMKV } from "./mmkv";
|
||||||
|
|
||||||
|
// Database key cipher is persisted across different user sessions hence it has
|
||||||
|
// it's independent storage which we will never clear. This is only used when application has
|
||||||
|
// app lock with password enabled.
|
||||||
|
export const CipherStorage = new MMKVLoader()
|
||||||
|
.withInstanceID("cipher_storage")
|
||||||
|
.setProcessingMode(
|
||||||
|
Platform.OS === "ios"
|
||||||
|
? ProcessingModes.MULTI_PROCESS
|
||||||
|
: ProcessingModes.SINGLE_PROCESS
|
||||||
|
)
|
||||||
|
.disableIndexing()
|
||||||
|
.initialize();
|
||||||
|
|
||||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||||
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
|
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||||
|
|
||||||
|
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
|
||||||
|
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
|
||||||
|
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
|
||||||
|
|
||||||
|
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||||
|
const USER_KEY_CIPHER = "userKeyCipher";
|
||||||
|
const APPLOCK_CIPHER = "applockCipher";
|
||||||
|
|
||||||
const KEYSTORE_CONFIG = Platform.select({
|
const KEYSTORE_CONFIG = Platform.select({
|
||||||
ios: {
|
ios: {
|
||||||
@@ -37,61 +60,231 @@ const KEYSTORE_CONFIG = Platform.select({
|
|||||||
android: {}
|
android: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function deriveCryptoKey(name, data) {
|
function generatePassword() {
|
||||||
try {
|
const length = 80;
|
||||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
const crypto = window.crypto || window.msCrypto;
|
||||||
await Keychain.setInternetCredentials(
|
if (typeof crypto === "undefined") {
|
||||||
"notesnook",
|
throw new Error(
|
||||||
name,
|
"Crypto API is not supported. Please upgrade your web browser"
|
||||||
credentials.key,
|
|
||||||
KEYSTORE_CONFIG
|
|
||||||
);
|
);
|
||||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
}
|
||||||
return credentials.key;
|
const charset =
|
||||||
|
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||||
|
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||||
|
let secret = "";
|
||||||
|
for (const index of indexes) {
|
||||||
|
secret += charset[index % charset.length];
|
||||||
|
}
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
|
||||||
|
const key = getDatabaseKey();
|
||||||
|
const appLockCredentials = await Sodium.deriveKey(
|
||||||
|
appLockPassword,
|
||||||
|
NOTESNOOK_APPLOCK_KEY_SALT
|
||||||
|
);
|
||||||
|
const databaseKeyCipher = await encrypt(appLockCredentials, key);
|
||||||
|
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||||
|
// We reset the database key from keychain once app lock password is set.
|
||||||
|
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||||
|
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||||
|
const databaseKey = await decrypt(
|
||||||
|
{
|
||||||
|
password: appLockPassword
|
||||||
|
},
|
||||||
|
databaseKeyCipher
|
||||||
|
);
|
||||||
|
|
||||||
|
await Keychain.setInternetCredentials(
|
||||||
|
KEYCHAIN_SERVER_DBKEY,
|
||||||
|
"notesnook",
|
||||||
|
databaseKey,
|
||||||
|
KEYSTORE_CONFIG
|
||||||
|
);
|
||||||
|
MMKV.removeItem(DB_KEY_CIPHER);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAppLockVerificationCipher(appLockPassword) {
|
||||||
|
try {
|
||||||
|
const appLockCredentials = await Sodium.deriveKey(
|
||||||
|
appLockPassword,
|
||||||
|
NOTESNOOK_APPLOCK_KEY_SALT
|
||||||
|
);
|
||||||
|
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||||
|
|
||||||
|
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||||
|
|
||||||
|
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
DatabaseLogger.error(e);
|
||||||
|
console.log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upgradeIOSKeychain(username, password) {
|
export async function clearAppLockVerificationCipher() {
|
||||||
if (Platform.OS !== "ios") return;
|
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||||
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
|
}
|
||||||
await Keychain.setInternetCredentials(
|
|
||||||
"notesnook",
|
export async function validateAppLockPassword(appLockPassword) {
|
||||||
username,
|
try {
|
||||||
password,
|
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||||
KEYSTORE_CONFIG
|
if (!appLockCipher) return true;
|
||||||
|
const decrypted = await decrypt(
|
||||||
|
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
|
||||||
|
appLockCipher
|
||||||
);
|
);
|
||||||
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
|
|
||||||
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
|
DatabaseLogger.info(
|
||||||
|
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||||
|
);
|
||||||
|
return typeof decrypted === "string";
|
||||||
|
} catch (e) {
|
||||||
|
DatabaseLogger.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let DB_KEY;
|
||||||
|
export function clearDatabaseKey() {
|
||||||
|
DB_KEY = undefined;
|
||||||
|
DatabaseLogger.info("Cleared database key");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDatabaseKey(appLockPassword) {
|
||||||
|
if (DB_KEY) return DB_KEY;
|
||||||
|
try {
|
||||||
|
if (appLockPassword) {
|
||||||
|
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
|
||||||
|
const databaseKey = await decrypt(
|
||||||
|
{
|
||||||
|
password: appLockPassword
|
||||||
|
},
|
||||||
|
databaseKeyCipher
|
||||||
|
);
|
||||||
|
DatabaseLogger.info("Getting database key from cipher");
|
||||||
|
DB_KEY = databaseKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DB_KEY) {
|
||||||
|
const hasKey = await Keychain.hasInternetCredentials(
|
||||||
|
KEYCHAIN_SERVER_DBKEY
|
||||||
|
);
|
||||||
|
if (hasKey) {
|
||||||
|
let credentials = await Keychain.getInternetCredentials(
|
||||||
|
KEYCHAIN_SERVER_DBKEY,
|
||||||
|
KEYSTORE_CONFIG
|
||||||
|
);
|
||||||
|
|
||||||
|
DatabaseLogger.info("Getting database key from Keychain");
|
||||||
|
DB_KEY = credentials.password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DB_KEY) {
|
||||||
|
DatabaseLogger.info("Generating new database key");
|
||||||
|
const password = generatePassword();
|
||||||
|
const derivedDatabaseKey = await Sodium.deriveKey(
|
||||||
|
password,
|
||||||
|
NOTESNOOK_DB_KEY_SALT
|
||||||
|
);
|
||||||
|
|
||||||
|
DB_KEY = derivedDatabaseKey.key;
|
||||||
|
|
||||||
|
await Keychain.setInternetCredentials(
|
||||||
|
KEYCHAIN_SERVER_DBKEY,
|
||||||
|
"notesnook",
|
||||||
|
DB_KEY,
|
||||||
|
KEYSTORE_CONFIG
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||||
|
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||||
|
"notesnook",
|
||||||
|
KEYSTORE_CONFIG
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userKeyCredentials) {
|
||||||
|
const userKeyCipher = await encrypt(
|
||||||
|
{
|
||||||
|
key: DB_KEY,
|
||||||
|
salt: NOTESNOOK_USER_KEY_SALT
|
||||||
|
},
|
||||||
|
userKeyCredentials.password
|
||||||
|
);
|
||||||
|
// Store encrypted user key in MMKV
|
||||||
|
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||||
|
await Keychain.resetInternetCredentials("notesnook");
|
||||||
|
}
|
||||||
|
DatabaseLogger.info("Migrated user credentials to cipher storage");
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB_KEY;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e, "error");
|
||||||
|
DatabaseLogger.error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deriveCryptoKey(data) {
|
||||||
|
try {
|
||||||
|
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||||
|
const userKeyCipher = await encrypt(
|
||||||
|
{
|
||||||
|
key: await getDatabaseKey(),
|
||||||
|
salt: NOTESNOOK_USER_KEY_SALT
|
||||||
|
},
|
||||||
|
credentials.key
|
||||||
|
);
|
||||||
|
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
|
||||||
|
|
||||||
|
// Store encrypted user key in MMKV
|
||||||
|
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||||
|
return credentials.key;
|
||||||
|
} catch (e) {
|
||||||
|
DatabaseLogger.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCryptoKey(_name) {
|
export async function getCryptoKey(_name) {
|
||||||
try {
|
try {
|
||||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||||
let credentials = await Keychain.getInternetCredentials(
|
|
||||||
"notesnook",
|
if (!keyCipher) {
|
||||||
KEYSTORE_CONFIG
|
DatabaseLogger.info("User key cipher is null");
|
||||||
);
|
|
||||||
// upgrades ios keychain to use accessGroups
|
|
||||||
// so we have access to keychain in share extension.
|
|
||||||
await upgradeIOSKeychain(credentials.username, credentials.password);
|
|
||||||
return credentials.password;
|
|
||||||
} else {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const key = await decrypt(
|
||||||
|
{
|
||||||
|
key: await getDatabaseKey()
|
||||||
|
},
|
||||||
|
keyCipher
|
||||||
|
);
|
||||||
|
|
||||||
|
DatabaseLogger.info("User key decrypted: ", !!key);
|
||||||
|
|
||||||
|
return key;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.log("getCryptoKey", e);
|
||||||
|
DatabaseLogger.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeCryptoKey(_name) {
|
export async function removeCryptoKey(_name) {
|
||||||
try {
|
try {
|
||||||
let result = await Keychain.resetInternetCredentials("notesnook");
|
MMKV.removeItem(USER_KEY_CIPHER);
|
||||||
return result;
|
await Keychain.resetInternetCredentials("notesnook");
|
||||||
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
DatabaseLogger.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +302,7 @@ export async function generateCryptoKey(password, salt) {
|
|||||||
let credentials = await Sodium.deriveKey(password, salt || null);
|
let credentials = await Sodium.deriveKey(password, salt || null);
|
||||||
return credentials;
|
return credentials;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("generateCryptoKey: ", e);
|
DatabaseLogger.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +316,8 @@ export async function decrypt(password, data) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
let _data = { ...data };
|
let _data = { ...data };
|
||||||
_data.output = "plain";
|
_data.output = "plain";
|
||||||
|
|
||||||
|
if (!password.salt) password.salt = data.salt;
|
||||||
return await Sodium.decrypt(password, _data);
|
return await Sodium.decrypt(password, _data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +330,11 @@ export async function decryptMulti(password, data) {
|
|||||||
d.output = "plain";
|
d.output = "plain";
|
||||||
return d;
|
return d;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (data.length && !password.salt) {
|
||||||
|
password.salt = data[0].salt;
|
||||||
|
}
|
||||||
|
|
||||||
return await Sodium.decryptMulti(password, data);
|
return await Sodium.decryptMulti(password, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,15 +16,18 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import "./logger";
|
||||||
import { database } from "@notesnook/common";
|
import { database } from "@notesnook/common";
|
||||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import * as Gzip from "react-native-gzip";
|
import * as Gzip from "react-native-gzip";
|
||||||
import EventSource from "../../utils/sse/even-source-ios";
|
import EventSource from "../../utils/sse/even-source-ios";
|
||||||
import AndroidEventSource from "../../utils/sse/event-source";
|
import AndroidEventSource from "../../utils/sse/event-source";
|
||||||
|
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||||
import filesystem from "../filesystem";
|
import filesystem from "../filesystem";
|
||||||
import "./logger";
|
|
||||||
import Storage from "./storage";
|
import Storage from "./storage";
|
||||||
|
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||||
|
import { getDatabaseKey } from "./encryption";
|
||||||
|
|
||||||
database.host(
|
database.host(
|
||||||
__DEV__
|
__DEV__
|
||||||
@@ -34,11 +37,11 @@ database.host(
|
|||||||
SSE_HOST: "https://events.streetwriters.co",
|
SSE_HOST: "https://events.streetwriters.co",
|
||||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||||
// API_HOST: "http://192.168.43.108:5264",
|
// API_HOST: "http://192.168.43.5:5264",
|
||||||
// AUTH_HOST: "http://192.168.43.108:8264",
|
// AUTH_HOST: "http://192.168.43.5:8264",
|
||||||
// SSE_HOST: "http://192.168.43.108:7264",
|
// SSE_HOST: "http://192.168.43.5:7264",
|
||||||
// SUBSCRIPTIONS_HOST: "http://192.168.43.108:9264",
|
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
|
||||||
// ISSUES_HOST: "http://192.168.43.108:2624"
|
// ISSUES_HOST: "http://192.168.43.5:2624"
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
API_HOST: "https://api.notesnook.com",
|
API_HOST: "https://api.notesnook.com",
|
||||||
@@ -49,15 +52,37 @@ database.host(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
database.setup(
|
export async function setupDatabase(password) {
|
||||||
Storage,
|
const key = await getDatabaseKey(password);
|
||||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
if (!key)
|
||||||
filesystem,
|
throw new Error("Database setup failed, could not get database key");
|
||||||
{
|
|
||||||
compress: Gzip.deflate,
|
console.log("Opening database with key:", !!key);
|
||||||
decompress: Gzip.inflate
|
|
||||||
}
|
database.setup({
|
||||||
);
|
storage: Storage,
|
||||||
|
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||||
|
fs: filesystem,
|
||||||
|
compressor: {
|
||||||
|
compress: Gzip.deflate,
|
||||||
|
decompress: Gzip.inflate
|
||||||
|
},
|
||||||
|
batchSize: 100,
|
||||||
|
sqliteOptions: {
|
||||||
|
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",
|
||||||
|
password: key
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const db = database;
|
export const db = database;
|
||||||
export const DatabaseLogger = dbLogger;
|
export const DatabaseLogger = dbLogger;
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { initalize } from "@notesnook/core/dist/logger";
|
|
||||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||||
|
import { initialize } from "@notesnook/core/dist/logger";
|
||||||
import { KV } from "./storage";
|
import { KV } from "./storage";
|
||||||
|
|
||||||
const LoggerStorage = new MMKVLoader()
|
const LoggerStorage = new MMKVLoader()
|
||||||
.withInstanceID("notesnook_logs")
|
.withInstanceID("notesnook_logs")
|
||||||
.initialize();
|
.initialize();
|
||||||
|
|
||||||
initalize(new KV(LoggerStorage));
|
initialize(new KV(LoggerStorage));
|
||||||
|
|
||||||
export { LoggerStorage };
|
export { LoggerStorage };
|
||||||
|
|||||||
123
apps/mobile/app/common/database/sqlite.kysely.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
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 { DatabaseConnection, Driver, QueryResult } from "kysely";
|
||||||
|
import { CompiledQuery } from "kysely";
|
||||||
|
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||||
|
|
||||||
|
type Config = { dbName: string; async: boolean; location: string };
|
||||||
|
|
||||||
|
export class RNSqliteDriver implements Driver {
|
||||||
|
private connection?: DatabaseConnection;
|
||||||
|
private connectionMutex = new ConnectionMutex();
|
||||||
|
private db: QuickSQLiteConnection;
|
||||||
|
constructor(private readonly config: Config) {
|
||||||
|
this.db = open({
|
||||||
|
name: config.dbName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
this.connection = new RNSqliteConnection(this.db);
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquireConnection(): Promise<DatabaseConnection> {
|
||||||
|
// SQLite only has one single connection. We use a mutex here to wait
|
||||||
|
// until the single connection has been released.
|
||||||
|
await this.connectionMutex.lock();
|
||||||
|
return this.connection!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||||
|
await connection.executeQuery(CompiledQuery.raw("BEGIN TRANSACTION"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||||
|
await connection.executeQuery(CompiledQuery.raw("COMMIT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||||
|
await connection.executeQuery(CompiledQuery.raw("ROLLBACK"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseConnection(): Promise<void> {
|
||||||
|
this.connectionMutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
this.db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionMutex {
|
||||||
|
private promise?: Promise<void>;
|
||||||
|
private resolve?: () => void;
|
||||||
|
|
||||||
|
async lock(): Promise<void> {
|
||||||
|
while (this.promise) {
|
||||||
|
await this.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.promise = new Promise((resolve) => {
|
||||||
|
this.resolve = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock(): void {
|
||||||
|
const resolve = this.resolve;
|
||||||
|
|
||||||
|
this.promise = undefined;
|
||||||
|
this.resolve = undefined;
|
||||||
|
|
||||||
|
resolve?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RNSqliteConnection implements DatabaseConnection {
|
||||||
|
constructor(private readonly db: QuickSQLiteConnection) {}
|
||||||
|
|
||||||
|
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||||
|
throw new Error("wasqlite driver doesn't support streaming");
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeQuery<R>(
|
||||||
|
compiledQuery: CompiledQuery<unknown>
|
||||||
|
): Promise<QueryResult<R>> {
|
||||||
|
const { parameters, sql, query } = compiledQuery;
|
||||||
|
const mode =
|
||||||
|
query.kind === "SelectQueryNode"
|
||||||
|
? "query"
|
||||||
|
: query.kind === "RawNode"
|
||||||
|
? "raw"
|
||||||
|
: "exec";
|
||||||
|
|
||||||
|
const result = await this.db.executeAsync(sql, parameters as any[]);
|
||||||
|
|
||||||
|
if (mode === "query" || !result.insertId)
|
||||||
|
return {
|
||||||
|
rows: result.rows?._array || []
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
insertId: BigInt(result.insertId),
|
||||||
|
numAffectedRows: BigInt(result.rowsAffected),
|
||||||
|
rows: mode === "raw" ? result.rows?._array || [] : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,7 +91,8 @@ export class KV {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeMulti(keys) {
|
async removeMulti(keys) {
|
||||||
return this.storage.removeItems(...keys);
|
if (!keys) return true;
|
||||||
|
return this.storage.removeItems(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear() {
|
async clear() {
|
||||||
|
|||||||
@@ -16,20 +16,61 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Dimensions, Platform } from "react-native";
|
import { Dimensions, Image, Platform } from "react-native";
|
||||||
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
import RNFetchBlob from "react-native-blob-util";
|
||||||
|
|
||||||
|
console.log("hello world");
|
||||||
/**
|
/**
|
||||||
* Scale down & compress images to screen width
|
* Scale down & compress images to screen width
|
||||||
* for loading in editor.
|
* for loading in editor.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function compressToBase64(path, type) {
|
export async function compressToBase64(path, type) {
|
||||||
const { width, scale } = Dimensions.get("window");
|
const { width: screenWidth, scale } = Dimensions.get("window");
|
||||||
|
console.log("COMPRESSING TO BASE64...");
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
console.log(path, "image path...");
|
||||||
|
|
||||||
|
Image.getSize(path, async (width) => {
|
||||||
|
console.log("image width", width);
|
||||||
|
|
||||||
|
const response = await ImageResizer.createResizedImage(
|
||||||
|
path,
|
||||||
|
screenWidth * scale,
|
||||||
|
99999,
|
||||||
|
type,
|
||||||
|
width <= 1920 ? 100 : 90,
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
mode: "contain",
|
||||||
|
onlyScaleDown: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const base64 = await RNFetchBlob.fs.readFile(
|
||||||
|
Platform.OS === "ios"
|
||||||
|
? response.uri?.replace("file://", "")
|
||||||
|
: response.uri,
|
||||||
|
"base64"
|
||||||
|
);
|
||||||
|
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||||
|
RNFetchBlob.fs
|
||||||
|
.unlink(response.uri.replace("file://", ""))
|
||||||
|
.catch(console.log);
|
||||||
|
|
||||||
|
resolve(base64);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compressToFile(path, type) {
|
||||||
const response = await ImageResizer.createResizedImage(
|
const response = await ImageResizer.createResizedImage(
|
||||||
path,
|
path,
|
||||||
width * scale,
|
1920,
|
||||||
9999,
|
99999,
|
||||||
type,
|
type,
|
||||||
80,
|
80,
|
||||||
0,
|
0,
|
||||||
@@ -40,11 +81,6 @@ export async function compressToBase64(path, type) {
|
|||||||
onlyScaleDown: true
|
onlyScaleDown: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const base64 = await RNFetchBlob.fs.readFile(
|
|
||||||
Platform.OS === "ios" ? response.uri?.replace("file://", "") : response.uri,
|
|
||||||
"base64"
|
|
||||||
);
|
|
||||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||||
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(console.log);
|
return response.uri;
|
||||||
return base64;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ import * as ScopedStorage from "react-native-scoped-storage";
|
|||||||
import { subscribe, zip } from "react-native-zip-archive";
|
import { subscribe, zip } from "react-native-zip-archive";
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
import RNFetchBlob from "react-native-blob-util";
|
||||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||||
import { ToastEvent, presentSheet } from "../../services/event-manager";
|
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||||
import { db } from "../database";
|
import { DatabaseLogger, db } from "../database";
|
||||||
import Storage from "../database/storage";
|
import Storage from "../database/storage";
|
||||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||||
import { createCacheDir, exists } from "./io";
|
import { createCacheDir, exists } from "./io";
|
||||||
@@ -46,15 +46,15 @@ export const FileDownloadStatus = {
|
|||||||
* Download all user's attachments
|
* Download all user's attachments
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function downloadAllAttachments() {
|
export async function downloadAllAttachments() {
|
||||||
const attachments = db.attachments.all;
|
const attachments = await db.attachments.all.ids();
|
||||||
return downloadAttachments(attachments);
|
return downloadAttachments(attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads provided attachments to a .zip file
|
* Downloads provided attachments to a .zip file
|
||||||
* on user's device.
|
* on user's device.
|
||||||
* @param attachments
|
* @param {string[]} attachments
|
||||||
* @param onProgress
|
* @param onProgress
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@@ -84,13 +84,15 @@ export async function downloadAttachments(
|
|||||||
Platform.OS === "ios"
|
Platform.OS === "ios"
|
||||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
||||||
: `${cacheDir}/notesnook-attachments.zip`;
|
: `${cacheDir}/notesnook-attachments.zip`;
|
||||||
if (await RNFetchBlob.fs.exists(zipSourceFolder))
|
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
|
||||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||||
|
}
|
||||||
|
|
||||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||||
|
|
||||||
for (let i = 0; i < attachments.length; i++) {
|
for (let i = 0; i < attachments.length; i++) {
|
||||||
let attachment = attachments[i];
|
let attachment = await db.attachments.attachment(attachments[i]);
|
||||||
const hash = attachment.metadata.hash;
|
const hash = attachment.hash;
|
||||||
try {
|
try {
|
||||||
if (canceled.current) {
|
if (canceled.current) {
|
||||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||||
@@ -114,19 +116,20 @@ export async function downloadAttachments(
|
|||||||
}
|
}
|
||||||
if (!uri) throw new Error("Failed to download file");
|
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.
|
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||||
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
|
const filePath = `${zipSourceFolder}/${attachment.filename}`;
|
||||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||||
result.set(hash, {
|
result.set(hash, {
|
||||||
filename: attachment.metadata.filename,
|
filename: attachment.filename,
|
||||||
status: FileDownloadStatus.Success
|
status: FileDownloadStatus.Success,
|
||||||
|
attachment: attachment
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.set(hash, {
|
result.set(hash, {
|
||||||
filename: attachment.metadata.filename,
|
filename: attachment.filename,
|
||||||
status: FileDownloadStatus.Fail,
|
status: FileDownloadStatus.Fail,
|
||||||
reason: e
|
reason: e
|
||||||
});
|
});
|
||||||
console.log("Error downloading attachment", e);
|
ToastManager.error(e, "Error downloading attachment");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (canceled.current) {
|
if (canceled.current) {
|
||||||
@@ -163,7 +166,7 @@ export async function downloadAttachments(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
releasePermissions(outputFolder);
|
releasePermissions(outputFolder);
|
||||||
sub?.remove();
|
sub?.remove();
|
||||||
console.log("Error zipping attachments", e);
|
ToastManager.error(e, "Error zipping attachments");
|
||||||
}
|
}
|
||||||
// Remove source & zip file from cache.
|
// Remove source & zip file from cache.
|
||||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||||
@@ -189,9 +192,9 @@ export default async function downloadAttachment(
|
|||||||
) {
|
) {
|
||||||
await createCacheDir();
|
await createCacheDir();
|
||||||
|
|
||||||
let attachment = db.attachments.attachment(hash);
|
let attachment = await db.attachments.attachment(hash);
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
console.log("attachment not found");
|
DatabaseLogger.log("Attachment not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,47 +209,42 @@ export default async function downloadAttachment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(
|
await db
|
||||||
"starting download attachment",
|
.fs()
|
||||||
attachment.metadata.hash,
|
.downloadFile(options.groupId || attachment.hash, attachment.hash);
|
||||||
options.groupId
|
if (!(await exists(attachment.hash))) {
|
||||||
);
|
DatabaseLogger.log("Attachment does not exist after download.");
|
||||||
await db.fs.downloadFile(
|
|
||||||
options.groupId || attachment.metadata.hash,
|
|
||||||
attachment.metadata.hash
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!(await exists(attachment.metadata.hash))) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.base64 || options.text) {
|
if (options.base64 || options.text) {
|
||||||
|
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
|
||||||
return await db.attachments.read(
|
return await db.attachments.read(
|
||||||
attachment.metadata.hash,
|
attachment.hash,
|
||||||
options.base64 ? "base64" : "text"
|
options.base64 ? "base64" : "text"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filename = getFileNameWithExtension(
|
let filename = getFileNameWithExtension(
|
||||||
attachment.metadata.filename,
|
attachment.filename,
|
||||||
attachment.metadata.type
|
attachment.mimeType
|
||||||
);
|
);
|
||||||
|
|
||||||
let key = await db.attachments.decryptKey(attachment.key);
|
let key = await db.attachments.decryptKey(attachment.key);
|
||||||
|
|
||||||
let info = {
|
let info = {
|
||||||
iv: attachment.iv,
|
iv: attachment.iv,
|
||||||
salt: attachment.salt,
|
salt: attachment.salt,
|
||||||
length: attachment.length,
|
length: attachment.size,
|
||||||
alg: attachment.alg,
|
alg: attachment.alg,
|
||||||
hash: attachment.metadata.hash,
|
hash: attachment.hash,
|
||||||
hashType: attachment.metadata.hashType,
|
hashType: attachment.hashType,
|
||||||
mime: attachment.metadata.type,
|
mime: attachment.mimeType,
|
||||||
fileName: options.cache ? undefined : filename,
|
fileName: options.cache ? undefined : filename,
|
||||||
uri: options.cache ? undefined : folder.uri,
|
uri: options.cache ? undefined : folder.uri,
|
||||||
chunkSize: attachment.chunkSize,
|
chunkSize: attachment.chunkSize,
|
||||||
appGroupId: IOS_APPGROUPID
|
appGroupId: IOS_APPGROUPID
|
||||||
};
|
};
|
||||||
|
|
||||||
let fileUri = await Sodium.decryptFile(
|
let fileUri = await Sodium.decryptFile(
|
||||||
key,
|
key,
|
||||||
info,
|
info,
|
||||||
@@ -254,7 +252,7 @@ export default async function downloadAttachment(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Download successful",
|
heading: "Download successful",
|
||||||
message: filename + " downloaded",
|
message: filename + " downloaded",
|
||||||
type: "success"
|
type: "success"
|
||||||
@@ -263,11 +261,11 @@ export default async function downloadAttachment(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
attachment.dateUploaded &&
|
attachment.dateUploaded &&
|
||||||
!isImage(attachment.metadata?.type) &&
|
!isImage(attachment.mimeType) &&
|
||||||
!isDocument(attachment.metadata?.type)
|
!isDocument(attachment.mimeType)
|
||||||
) {
|
) {
|
||||||
RNFetchBlob.fs
|
RNFetchBlob.fs
|
||||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||||
.catch(console.log);
|
.catch(console.log);
|
||||||
}
|
}
|
||||||
if (Platform.OS === "ios" && !options.cache) {
|
if (Platform.OS === "ios" && !options.cache) {
|
||||||
@@ -282,20 +280,23 @@ export default async function downloadAttachment(
|
|||||||
: "File Manager/Notesnook/downloads"
|
: "File Manager/Notesnook/downloads"
|
||||||
}`,
|
}`,
|
||||||
icon: "download",
|
icon: "download",
|
||||||
context: global ? null : attachment.metadata.hash,
|
context: global ? null : attachment.hash,
|
||||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileUri;
|
return fileUri;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("download attachment error: ", e);
|
DatabaseLogger.error(e);
|
||||||
if (attachment.dateUploaded) {
|
if (attachment.dateUploaded) {
|
||||||
RNFetchBlob.fs
|
RNFetchBlob.fs
|
||||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||||
|
.catch(console.log);
|
||||||
|
RNFetchBlob.fs
|
||||||
|
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||||
.catch(console.log);
|
.catch(console.log);
|
||||||
}
|
}
|
||||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
useAttachmentStore.getState().remove(attachment.hash);
|
||||||
if (options.throwError) {
|
if (options.throwError) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
import hosts from "@notesnook/core/dist/utils/constants";
|
import hosts from "@notesnook/core/dist/utils/constants";
|
||||||
import NetInfo from "@react-native-community/netinfo";
|
import NetInfo from "@react-native-community/netinfo";
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
import RNFetchBlob from "react-native-blob-util";
|
||||||
import { ToastEvent } from "../../services/event-manager";
|
import { ToastManager } from "../../services/event-manager";
|
||||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||||
import { db } from "../database";
|
import { db } from "../database";
|
||||||
import { cacheDir, fileCheck } from "./utils";
|
import { cacheDir, fileCheck } from "./utils";
|
||||||
@@ -29,13 +29,14 @@ import { createCacheDir, exists } from "./io";
|
|||||||
export async function downloadFile(filename, data, cancelToken) {
|
export async function downloadFile(filename, data, cancelToken) {
|
||||||
if (!data) return false;
|
if (!data) return false;
|
||||||
|
|
||||||
|
console.log("Downloading", filename);
|
||||||
await createCacheDir();
|
await createCacheDir();
|
||||||
|
|
||||||
let { url, headers } = data;
|
let { url, headers } = data;
|
||||||
let path = `${cacheDir}/${filename}`;
|
let path = `${cacheDir}/${filename}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await exists(filename)) {
|
if (await exists(filename)) {
|
||||||
|
console.log("Exists already", filename);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,13 +75,13 @@ export async function downloadFile(filename, data, cancelToken) {
|
|||||||
return status >= 200 && status < 300;
|
return status >= 200 && status < 300;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message !== "canceled") {
|
if (e.message !== "canceled") {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Error downloading file",
|
heading: "Error downloading file",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "global"
|
context: "global"
|
||||||
});
|
});
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Error downloading file",
|
heading: "Error downloading file",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -98,7 +99,7 @@ export async function downloadFile(filename, data, cancelToken) {
|
|||||||
export async function getUploadedFileSize(hash) {
|
export async function getUploadedFileSize(hash) {
|
||||||
try {
|
try {
|
||||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||||
const token = await db.user.tokenManager.getAccessToken();
|
const token = await db.tokenManager.getAccessToken();
|
||||||
|
|
||||||
const attachmentInfo = await fetch(url, {
|
const attachmentInfo = await fetch(url, {
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
@@ -119,7 +120,7 @@ export async function checkAttachment(hash) {
|
|||||||
const isInternetReachable =
|
const isInternetReachable =
|
||||||
internetState.isConnected && internetState.isInternetReachable;
|
internetState.isConnected && internetState.isInternetReachable;
|
||||||
if (!isInternetReachable) return { success: true };
|
if (!isInternetReachable) return { success: true };
|
||||||
const attachment = db.attachments.attachment(hash);
|
const attachment = await db.attachments.attachment(hash);
|
||||||
if (!attachment) return { failed: "Attachment not found." };
|
if (!attachment) return { failed: "Attachment not found." };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -17,13 +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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Platform } from "react-native";
|
|
||||||
import Sodium from "@ammarahmed/react-native-sodium";
|
import Sodium from "@ammarahmed/react-native-sodium";
|
||||||
|
import { Platform } from "react-native";
|
||||||
import RNFetchBlob from "react-native-blob-util";
|
import RNFetchBlob from "react-native-blob-util";
|
||||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
|
||||||
import { db } from "../database";
|
|
||||||
import { compressToBase64 } from "./compress";
|
|
||||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||||
|
import { db } from "../database";
|
||||||
|
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||||
|
|
||||||
export async function readEncrypted(filename, key, cipherData) {
|
export async function readEncrypted(filename, key, cipherData) {
|
||||||
await migrateFilesFromCache();
|
await migrateFilesFromCache();
|
||||||
@@ -35,14 +34,8 @@ export async function readEncrypted(filename, key, cipherData) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachment = db.attachments.attachment(filename);
|
const attachment = await db.attachments.attachment(filename);
|
||||||
const isPng = !attachment.metadata.type
|
console.log("decrypting....");
|
||||||
? false
|
|
||||||
: /(png)/g.test(attachment?.metadata.type);
|
|
||||||
const isJpeg = !attachment.metadata.type
|
|
||||||
? false
|
|
||||||
: /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
|
||||||
|
|
||||||
let output = await Sodium.decryptFile(
|
let output = await Sodium.decryptFile(
|
||||||
key,
|
key,
|
||||||
{
|
{
|
||||||
@@ -50,20 +43,9 @@ export async function readEncrypted(filename, key, cipherData) {
|
|||||||
hash: filename,
|
hash: filename,
|
||||||
appGroupId: IOS_APPGROUPID
|
appGroupId: IOS_APPGROUPID
|
||||||
},
|
},
|
||||||
cipherData.outputType === "base64"
|
cipherData.outputType === "base64" ? "base64" : "text"
|
||||||
? isPng || isJpeg
|
|
||||||
? "cache"
|
|
||||||
: "base64"
|
|
||||||
: "text"
|
|
||||||
);
|
);
|
||||||
console.log("file decrypted...");
|
console.log("file decrypted...", attachment?.mimeType);
|
||||||
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
|
|
||||||
const dCachePath = `${cacheDir}/${output}`;
|
|
||||||
output = await compressToBase64(
|
|
||||||
`file://${dCachePath}`,
|
|
||||||
isPng ? "PNG" : "JPEG"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -85,7 +67,7 @@ export async function hashBase64(data) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeEncryptedBase64({ data, key }) {
|
export async function writeEncryptedBase64(data, key) {
|
||||||
await createCacheDir();
|
await createCacheDir();
|
||||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||||
@@ -95,6 +77,8 @@ export async function writeEncryptedBase64({ data, key }) {
|
|||||||
});
|
});
|
||||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||||
console.log("encrypted file output: ", output);
|
console.log("encrypted file output: ", output);
|
||||||
|
output.size = output.length;
|
||||||
|
delete output.length;
|
||||||
return {
|
return {
|
||||||
...output,
|
...output,
|
||||||
alg: "xcha-stream"
|
alg: "xcha-stream"
|
||||||
@@ -194,10 +178,10 @@ export async function exists(filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exists || existsInAppGroup) {
|
if (exists || existsInAppGroup) {
|
||||||
const attachment = db.attachments.attachment(filename);
|
const attachment = await db.attachments.attachment(filename);
|
||||||
const totalChunks = Math.ceil(attachment.length / attachment.chunkSize);
|
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
|
||||||
const totalAbytes = totalChunks * ABYTES;
|
const totalAbytes = totalChunks * ABYTES;
|
||||||
const expectedFileSize = attachment.length + totalAbytes;
|
const expectedFileSize = attachment.size + totalAbytes;
|
||||||
|
|
||||||
const stat = await RNFetchBlob.fs.stat(
|
const stat = await RNFetchBlob.fs.stat(
|
||||||
existsInAppGroup ? appGroupPath : path
|
existsInAppGroup ? appGroupPath : path
|
||||||
|
|||||||
@@ -87,12 +87,9 @@ export async function uploadFile(filename, data, cancelToken) {
|
|||||||
DatabaseLogger.info(
|
DatabaseLogger.info(
|
||||||
`File upload status: ${filename}, ${status}, ${text}`
|
`File upload status: ${filename}, ${status}, ${text}`
|
||||||
);
|
);
|
||||||
let attachment = db.attachments.attachment(filename);
|
let attachment = await db.attachments.attachment(filename);
|
||||||
if (!attachment) return result;
|
if (!attachment) return result;
|
||||||
if (
|
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
|
||||||
!isImage(attachment.metadata.type) &&
|
|
||||||
!isDocument(attachment.metadata?.type)
|
|
||||||
) {
|
|
||||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,18 +17,20 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { FlatList, View } from "react-native";
|
|
||||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
|
||||||
import { useMessageStore } from "../../stores/use-message-store";
|
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
|
import React from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { useMessageStore } from "../../stores/use-message-store";
|
||||||
|
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||||
import { allowedOnPlatform, renderItem } from "./functions";
|
import { allowedOnPlatform, renderItem } from "./functions";
|
||||||
|
import { getContainerBorder } from "../../utils/colors";
|
||||||
|
|
||||||
export const Announcement = ({ color }) => {
|
export const Announcement = ({ color }) => {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const announcements = useMessageStore((state) => state.announcements);
|
const announcements = useMessageStore((state) => state.announcements);
|
||||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||||
|
|
||||||
return !announcement || selectionMode ? null : (
|
return !announcement || selectionMode ? null : (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -45,7 +47,8 @@ export const Announcement = ({ color }) => {
|
|||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: colors.secondary.background,
|
backgroundColor: colors.secondary.background,
|
||||||
paddingBottom: 12
|
paddingBottom: 12,
|
||||||
|
...getContainerBorder(colors.secondary.background)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
|||||||
key={item.title}
|
key={item.title}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
fontSize={SIZE.sm}
|
fontSize={SIZE.sm}
|
||||||
type="gray"
|
type="plain"
|
||||||
onPress={() => onPress(item)}
|
onPress={() => onPress(item)}
|
||||||
width={null}
|
width={null}
|
||||||
height={30}
|
height={30}
|
||||||
@@ -149,7 +149,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
|||||||
key={item.title}
|
key={item.title}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
fontSize={SIZE.xs}
|
fontSize={SIZE.xs}
|
||||||
type="gray"
|
type="plain"
|
||||||
onPress={() => onPress(item)}
|
onPress={() => onPress(item)}
|
||||||
width={null}
|
width={null}
|
||||||
height={30}
|
height={30}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const Title = ({ text, style = {}, inline }) => {
|
|||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="gray"
|
type="plain"
|
||||||
icon="close"
|
icon="close"
|
||||||
height={null}
|
height={null}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|||||||
@@ -1,215 +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 } from "react";
|
|
||||||
import { Platform, View } from "react-native";
|
|
||||||
import { enabled } from "react-native-privacy-snapshot";
|
|
||||||
import { db } from "../../common/database";
|
|
||||||
import { useAppState } from "../../hooks/use-app-state";
|
|
||||||
import BiometricService from "../../services/biometrics";
|
|
||||||
import { useSettingStore } from "../../stores/use-setting-store";
|
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
|
||||||
import { useUserStore } from "../../stores/use-user-store";
|
|
||||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
|
||||||
import { SIZE } from "../../utils/size";
|
|
||||||
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";
|
|
||||||
|
|
||||||
const AppLockedOverlay = () => {
|
|
||||||
const { colors } = useThemeColors();
|
|
||||||
const user = useUserStore((state) => state.user);
|
|
||||||
const appLocked = useUserStore((state) => state.appLocked);
|
|
||||||
const lockApp = useUserStore((state) => state.lockApp);
|
|
||||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
|
||||||
const passwordInputRef = useRef();
|
|
||||||
const password = useRef();
|
|
||||||
const appState = useAppState();
|
|
||||||
|
|
||||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
|
||||||
|
|
||||||
const onUnlockAppRequested = useCallback(async () => {
|
|
||||||
if (!(await BiometricService.isBiometryAvailable())) return;
|
|
||||||
if (Platform.OS === "android") {
|
|
||||||
const activityName = await NotesnookModule.getActivityName();
|
|
||||||
if (activityName !== "MainActivity") return;
|
|
||||||
}
|
|
||||||
useSettingStore.getState().setRequestBiometrics(true);
|
|
||||||
|
|
||||||
let unlocked = await BiometricService.validateUser(
|
|
||||||
"Unlock to access your notes",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
if (unlocked) {
|
|
||||||
lockApp(false);
|
|
||||||
enabled(false);
|
|
||||||
password.current = null;
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
biometricUnlockAwaitingUserInput.current = false;
|
|
||||||
useSettingStore.getState().setRequestBiometrics(false);
|
|
||||||
}, 1);
|
|
||||||
}, [lockApp]);
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
|
||||||
if (!password.current) return;
|
|
||||||
try {
|
|
||||||
let unlocked = await db.user.verifyPassword(password.current);
|
|
||||||
if (unlocked) {
|
|
||||||
lockApp(false);
|
|
||||||
enabled(false);
|
|
||||||
password.current = null;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
biometricUnlockAwaitingUserInput.current ||
|
|
||||||
useUserStore.getState().disableAppLockRequests
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
if (appLocked && appState === "active") {
|
|
||||||
biometricUnlockAwaitingUserInput.current = true;
|
|
||||||
onUnlockAppRequested();
|
|
||||||
}
|
|
||||||
}, [appState, onUnlockAppRequested, appLocked]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
appLocked && (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
backgroundColor: colors.primary.background,
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: 999,
|
|
||||||
justifyContent: "center"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: "center",
|
|
||||||
width:
|
|
||||||
deviceMode !== "mobile"
|
|
||||||
? "50%"
|
|
||||||
: Platform.OS == "ios"
|
|
||||||
? "95%"
|
|
||||||
: "100%",
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
marginBottom: 30,
|
|
||||||
marginTop: 15,
|
|
||||||
alignSelf: "center"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
name="fingerprint"
|
|
||||||
size={100}
|
|
||||||
customStyle={{
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
marginBottom: 20,
|
|
||||||
marginTop: user ? 0 : 50
|
|
||||||
}}
|
|
||||||
onPress={onUnlockAppRequested}
|
|
||||||
color={colors.primary.border}
|
|
||||||
/>
|
|
||||||
<Heading
|
|
||||||
color={colors.primary.heading}
|
|
||||||
style={{
|
|
||||||
alignSelf: "center",
|
|
||||||
textAlign: "center"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Unlock your notes
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Paragraph
|
|
||||||
style={{
|
|
||||||
alignSelf: "center",
|
|
||||||
textAlign: "center",
|
|
||||||
maxWidth: "90%"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{"Please verify it's you"}
|
|
||||||
</Paragraph>
|
|
||||||
<Seperator />
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
padding: 12,
|
|
||||||
backgroundColor: colors.primary.background
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{user ? (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
fwdRef={passwordInputRef}
|
|
||||||
secureTextEntry
|
|
||||||
placeholder="Enter account password"
|
|
||||||
onChangeText={(v) => (password.current = v)}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
marginTop: user ? 25 : 25
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{user ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
title="Continue"
|
|
||||||
type="accent"
|
|
||||||
onPress={onSubmit}
|
|
||||||
width={250}
|
|
||||||
height={45}
|
|
||||||
style={{
|
|
||||||
borderRadius: 150,
|
|
||||||
marginBottom: 10
|
|
||||||
}}
|
|
||||||
fontSize={SIZE.md}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title="Unlock with Biometrics"
|
|
||||||
width={250}
|
|
||||||
onPress={onUnlockAppRequested}
|
|
||||||
icon={"fingerprint"}
|
|
||||||
type="transparent"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLockedOverlay;
|
|
||||||
328
apps/mobile/app/components/app-lock-overlay/index.tsx
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
/*
|
||||||
|
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 } from "react";
|
||||||
|
import { AppStateStatus, Platform, TextInput, 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,
|
||||||
|
encrypt,
|
||||||
|
getCryptoKey,
|
||||||
|
getDatabaseKey,
|
||||||
|
setAppLockVerificationCipher,
|
||||||
|
validateAppLockPassword
|
||||||
|
} from "../../common/database/encryption";
|
||||||
|
import { MMKV } from "../../common/database/mmkv";
|
||||||
|
import { useAppState } from "../../hooks/use-app-state";
|
||||||
|
import BiometricService from "../../services/biometrics";
|
||||||
|
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 { 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";
|
||||||
|
|
||||||
|
const getUser = () => {
|
||||||
|
const user = MMKV.getString("user");
|
||||||
|
if (user) {
|
||||||
|
return JSON.parse(user);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyUserPassword = async (password: string) => {
|
||||||
|
try {
|
||||||
|
await getDatabaseKey();
|
||||||
|
const key = await getCryptoKey();
|
||||||
|
const user = getUser();
|
||||||
|
const cipher = await encrypt(
|
||||||
|
{
|
||||||
|
key: key,
|
||||||
|
salt: user.salt
|
||||||
|
},
|
||||||
|
"notesnook"
|
||||||
|
);
|
||||||
|
const plainText = await decrypt({ password }, cipher);
|
||||||
|
return plainText === "notesnook";
|
||||||
|
} catch (e) {
|
||||||
|
DatabaseLogger.error(e as Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppLockedOverlay = () => {
|
||||||
|
const initialLaunchBiometricRequest = useRef(true);
|
||||||
|
const { colors } = useThemeColors();
|
||||||
|
const user = getUser();
|
||||||
|
const appLocked = useUserStore((state) => state.appLocked);
|
||||||
|
const lockApp = useUserStore((state) => state.lockApp);
|
||||||
|
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||||
|
const passwordInputRef = useRef<TextInput>(null);
|
||||||
|
const password = useRef<string>();
|
||||||
|
const appState = useAppState();
|
||||||
|
const lastAppState = useRef<AppStateStatus>(appState);
|
||||||
|
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||||
|
const keyboardType = useSettingStore(
|
||||||
|
(state) => state.settings.applockKeyboardType
|
||||||
|
);
|
||||||
|
const appLockHasPasswordSecurity = useSettingStore(
|
||||||
|
(state) => state.settings.appLockHasPasswordSecurity
|
||||||
|
);
|
||||||
|
const biometricsAuthEnabled = useSettingStore(
|
||||||
|
(state) =>
|
||||||
|
state.settings.biometricsAuthEnabled === true ||
|
||||||
|
(state.settings.biometricsAuthEnabled === undefined &&
|
||||||
|
!state.settings.appLockHasPasswordSecurity)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onUnlockAppRequested = useCallback(async () => {
|
||||||
|
if (
|
||||||
|
!biometricsAuthEnabled ||
|
||||||
|
!(await BiometricService.isBiometryAvailable())
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
biometricUnlockAwaitingUserInput.current = true;
|
||||||
|
|
||||||
|
if (Platform.OS === "android") {
|
||||||
|
const activityName = await NotesnookModule.getActivityName();
|
||||||
|
if (activityName !== "MainActivity") return;
|
||||||
|
}
|
||||||
|
useSettingStore.getState().setRequestBiometrics(true);
|
||||||
|
|
||||||
|
const unlocked = await BiometricService.validateUser(
|
||||||
|
"Unlock to access your notes",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
if (unlocked) {
|
||||||
|
lockApp(false);
|
||||||
|
enabled(false);
|
||||||
|
password.current = undefined;
|
||||||
|
}
|
||||||
|
biometricUnlockAwaitingUserInput.current = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (biometricUnlockAwaitingUserInput.current) return;
|
||||||
|
useSettingStore.getState().setRequestBiometrics(false);
|
||||||
|
}, 500);
|
||||||
|
}, [biometricsAuthEnabled, lockApp]);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
if (!password.current) return;
|
||||||
|
try {
|
||||||
|
const unlocked = appLockHasPasswordSecurity
|
||||||
|
? await validateAppLockPassword(password.current)
|
||||||
|
: await verifyUserPassword(password.current);
|
||||||
|
if (unlocked) {
|
||||||
|
if (!appLockHasPasswordSecurity) {
|
||||||
|
await setAppLockVerificationCipher(password.current);
|
||||||
|
SettingsService.set({
|
||||||
|
appLockHasPasswordSecurity: true,
|
||||||
|
applockKeyboardType: "default"
|
||||||
|
});
|
||||||
|
DatabaseLogger.info("App lock migrated to password security");
|
||||||
|
}
|
||||||
|
|
||||||
|
lockApp(false);
|
||||||
|
enabled(false);
|
||||||
|
password.current = undefined;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appState === "active") {
|
||||||
|
enabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevState = lastAppState.current;
|
||||||
|
lastAppState.current = appState;
|
||||||
|
|
||||||
|
if (
|
||||||
|
appState === "active" &&
|
||||||
|
(prevState === "background" || initialLaunchBiometricRequest.current)
|
||||||
|
) {
|
||||||
|
if (SettingsService.shouldLockAppOnEnterForeground()) {
|
||||||
|
DatabaseLogger.info("Locking app on entering foreground");
|
||||||
|
useUserStore.getState().lockApp(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
biometricUnlockAwaitingUserInput.current ||
|
||||||
|
useUserStore.getState().disableAppLockRequests ||
|
||||||
|
!appLocked ||
|
||||||
|
useSettingStore.getState().requestBiometrics ||
|
||||||
|
useSettingStore.getState().appDidEnterBackgroundForAction
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
initialLaunchBiometricRequest.current = false;
|
||||||
|
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
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.primary.background,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 999,
|
||||||
|
justifyContent: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
width:
|
||||||
|
deviceMode !== "mobile"
|
||||||
|
? "50%"
|
||||||
|
: Platform.OS == "ios"
|
||||||
|
? "95%"
|
||||||
|
: "100%",
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
marginBottom: 30,
|
||||||
|
marginTop: 15,
|
||||||
|
alignSelf: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
name="fingerprint"
|
||||||
|
size={100}
|
||||||
|
style={{
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
marginBottom: 20,
|
||||||
|
marginTop: user ? 0 : 50
|
||||||
|
}}
|
||||||
|
onPress={onUnlockAppRequested}
|
||||||
|
color={colors.primary.border}
|
||||||
|
/>
|
||||||
|
<Heading
|
||||||
|
color={colors.primary.heading}
|
||||||
|
style={{
|
||||||
|
alignSelf: "center",
|
||||||
|
textAlign: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unlock your notes
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
alignSelf: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
maxWidth: "90%"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{"Please verify it's you"}
|
||||||
|
</Paragraph>
|
||||||
|
<Seperator />
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: colors.primary.background
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user || appLockHasPasswordSecurity ? (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
fwdRef={passwordInputRef}
|
||||||
|
secureTextEntry
|
||||||
|
keyboardType={
|
||||||
|
appLockHasPasswordSecurity ? keyboardType : "default"
|
||||||
|
}
|
||||||
|
placeholder={`Enter ${
|
||||||
|
appLockHasPasswordSecurity
|
||||||
|
? `app lock ${
|
||||||
|
keyboardType === "numeric" ? "pin" : "password"
|
||||||
|
}`
|
||||||
|
: "account password"
|
||||||
|
}`}
|
||||||
|
onChangeText={(v) => (password.current = v)}
|
||||||
|
onSubmit={() => {
|
||||||
|
onSubmit();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginTop: user ? 25 : 25
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user || appLockHasPasswordSecurity ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
title="Continue"
|
||||||
|
type="accent"
|
||||||
|
onPress={onSubmit}
|
||||||
|
width={250}
|
||||||
|
height={45}
|
||||||
|
style={{
|
||||||
|
borderRadius: 150,
|
||||||
|
marginBottom: 10
|
||||||
|
}}
|
||||||
|
fontSize={SIZE.md}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{biometricsAuthEnabled ? (
|
||||||
|
<Button
|
||||||
|
title="Unlock with Biometrics"
|
||||||
|
width={250}
|
||||||
|
onPress={onUnlockAppRequested}
|
||||||
|
icon={"fingerprint"}
|
||||||
|
type="transparent"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLockedOverlay;
|
||||||
@@ -17,9 +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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { formatBytes } from "@notesnook/common";
|
||||||
|
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
|
||||||
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
import Clipboard from "@react-native-clipboard/clipboard";
|
import Clipboard from "@react-native-clipboard/clipboard";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { RefObject, useEffect, useState } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
import { ScrollView } from "react-native-gesture-handler";
|
||||||
import { db } from "../../common/database";
|
import { db } from "../../common/database";
|
||||||
import filesystem from "../../common/filesystem";
|
import filesystem from "../../common/filesystem";
|
||||||
@@ -27,14 +31,18 @@ import downloadAttachment from "../../common/filesystem/download-attachment";
|
|||||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||||
import picker from "../../screens/editor/tiptap/picker";
|
import picker from "../../screens/editor/tiptap/picker";
|
||||||
import {
|
import {
|
||||||
|
ToastManager,
|
||||||
eSendEvent,
|
eSendEvent,
|
||||||
presentSheet,
|
presentSheet
|
||||||
ToastEvent
|
|
||||||
} from "../../services/event-manager";
|
} from "../../services/event-manager";
|
||||||
import PremiumService from "../../services/premium";
|
import PremiumService from "../../services/premium";
|
||||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import {
|
||||||
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
|
eCloseAttachmentDialog,
|
||||||
|
eCloseSheet,
|
||||||
|
eDBItemUpdate,
|
||||||
|
eOnLoadNote
|
||||||
|
} from "../../utils/events";
|
||||||
import { SIZE } from "../../utils/size";
|
import { SIZE } from "../../utils/size";
|
||||||
import { sleep } from "../../utils/time";
|
import { sleep } from "../../utils/time";
|
||||||
import { Dialog } from "../dialog";
|
import { Dialog } from "../dialog";
|
||||||
@@ -43,31 +51,42 @@ import { openNote } from "../list-items/note/wrapper";
|
|||||||
import { DateMeta } from "../properties/date-meta";
|
import { DateMeta } from "../properties/date-meta";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Notice } from "../ui/notice";
|
import { Notice } from "../ui/notice";
|
||||||
import { PressableButton } from "../ui/pressable";
|
import { Pressable } from "../ui/pressable";
|
||||||
import Heading from "../ui/typography/heading";
|
import Heading from "../ui/typography/heading";
|
||||||
import Paragraph from "../ui/typography/paragraph";
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
import { formatBytes } from "@notesnook/common";
|
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||||
|
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||||
|
|
||||||
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
const Actions = ({
|
||||||
|
attachment,
|
||||||
|
close,
|
||||||
|
setAttachments,
|
||||||
|
fwdRef
|
||||||
|
}: {
|
||||||
|
attachment: Attachment;
|
||||||
|
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
|
||||||
|
close?: () => void;
|
||||||
|
fwdRef: RefObject<ActionSheetRef>;
|
||||||
|
}) => {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const contextId = attachment.metadata.hash;
|
const contextId = attachment.hash;
|
||||||
const [filename, setFilename] = useState(attachment.metadata.filename);
|
const [filename, setFilename] = useState(attachment.filename);
|
||||||
const [currentProgress] = useAttachmentProgress(attachment);
|
const [currentProgress] = useAttachmentProgress(attachment);
|
||||||
const [failed, setFailed] = useState(attachment.failed);
|
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
|
||||||
const [notes, setNotes] = useState([]);
|
const [notes, setNotes] = useState<Note[]>([]);
|
||||||
const [loading, setLoading] = useState({
|
const [loading, setLoading] = useState<{
|
||||||
name: null
|
name?: string;
|
||||||
});
|
}>({});
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
{
|
{
|
||||||
name: "Download",
|
name: "Download",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
if (currentProgress) {
|
if (currentProgress) {
|
||||||
await db.fs.cancel(attachment.metadata.hash);
|
await db.fs().cancel(attachment.hash);
|
||||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
useAttachmentStore.getState().remove(attachment.hash);
|
||||||
}
|
}
|
||||||
downloadAttachment(attachment.metadata.hash, false);
|
downloadAttachment(attachment.hash, false);
|
||||||
eSendEvent(eCloseSheet, contextId);
|
eSendEvent(eCloseSheet, contextId);
|
||||||
},
|
},
|
||||||
icon: "download"
|
icon: "download"
|
||||||
@@ -76,7 +95,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
name: "Reupload",
|
name: "Reupload",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
if (!PremiumService.get()) {
|
if (!PremiumService.get()) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Upgrade to pro",
|
heading: "Upgrade to pro",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -85,9 +104,9 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
}
|
}
|
||||||
await picker.pick({
|
await picker.pick({
|
||||||
reupload: true,
|
reupload: true,
|
||||||
hash: attachment.metadata.hash,
|
hash: attachment.hash,
|
||||||
context: contextId,
|
context: contextId,
|
||||||
type: attachment.metadata.type
|
type: attachment.mimeType.startsWith("image") ? "image" : "file"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: "upload"
|
icon: "upload"
|
||||||
@@ -98,19 +117,20 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
setLoading({
|
setLoading({
|
||||||
name: "Run file check"
|
name: "Run file check"
|
||||||
});
|
});
|
||||||
let res = await filesystem.checkAttachment(attachment.metadata.hash);
|
const res = await filesystem.checkAttachment(attachment.hash);
|
||||||
if (res.failed) {
|
if (res.failed) {
|
||||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||||
setFailed(res.failed);
|
setFailed(res.failed);
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "File check failed with error: " + res.failed,
|
heading: "File check failed with error: " + res.failed,
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFailed(null);
|
setFailed(undefined);
|
||||||
db.attachments.markAsFailed(attachment.id, null);
|
db.attachments.markAsFailed(attachment.id);
|
||||||
ToastEvent.show({
|
eSendEvent(eDBItemUpdate, attachment.id);
|
||||||
|
ToastManager.show({
|
||||||
heading: "File check passed",
|
heading: "File check passed",
|
||||||
type: "success",
|
type: "success",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -119,7 +139,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
|
|
||||||
setAttachments();
|
setAttachments();
|
||||||
setLoading({
|
setLoading({
|
||||||
name: null
|
name: undefined
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: "file-check"
|
icon: "file-check"
|
||||||
@@ -128,19 +148,20 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
name: "Rename",
|
name: "Rename",
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
presentDialog({
|
presentDialog({
|
||||||
context: contextId,
|
context: contextId as any,
|
||||||
input: true,
|
input: true,
|
||||||
title: "Rename file",
|
title: "Rename file",
|
||||||
paragraph: "Enter a new name for the file",
|
paragraph: "Enter a new name for the file",
|
||||||
defaultValue: attachment.metadata.filename,
|
defaultValue: attachment.filename,
|
||||||
positivePress: async (value) => {
|
positivePress: async (value) => {
|
||||||
if (value && value.length > 0) {
|
if (value && value.length > 0) {
|
||||||
await db.attachments.add({
|
await db.attachments.add({
|
||||||
hash: attachment.metadata.hash,
|
hash: attachment.hash,
|
||||||
filename: value
|
filename: value
|
||||||
});
|
});
|
||||||
setFilename(value);
|
setFilename(value);
|
||||||
setAttachments();
|
setAttachments();
|
||||||
|
eSendEvent(eDBItemUpdate, attachment.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
positiveText: "Rename"
|
positiveText: "Rename"
|
||||||
@@ -151,34 +172,40 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
{
|
{
|
||||||
name: "Delete",
|
name: "Delete",
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
await db.attachments.remove(attachment.metadata.hash, false);
|
const relations = await db.relations.to(attachment, "note").get();
|
||||||
|
await db.attachments.remove(attachment.hash, false);
|
||||||
setAttachments();
|
setAttachments();
|
||||||
close();
|
eSendEvent(eDBItemUpdate, attachment.id);
|
||||||
|
relations
|
||||||
|
.map((relation) => relation.fromId)
|
||||||
|
.forEach(async (id) => {
|
||||||
|
const tab = useTabStore.getState().getTabForNote(id);
|
||||||
|
if (tab !== undefined) {
|
||||||
|
const isFocused = useTabStore.getState().currentTab === tab;
|
||||||
|
if (isFocused) {
|
||||||
|
eSendEvent(eOnLoadNote, {
|
||||||
|
item: await db.notes.note(id),
|
||||||
|
forced: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
editorController.current.commands.setLoading(true, tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
close?.();
|
||||||
},
|
},
|
||||||
icon: "delete-outline"
|
icon: "delete-outline"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const getNotes = useCallback(() => {
|
|
||||||
let allNotes = db.notes.all;
|
|
||||||
let attachmentNotes = attachment.noteIds?.map((id) => {
|
|
||||||
let index = allNotes?.findIndex((note) => id === note.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
return allNotes[index];
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
type: "notfound",
|
|
||||||
title: `Note with id ${id} does not exist.`,
|
|
||||||
id: id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return attachmentNotes;
|
|
||||||
}, [attachment.noteIds]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNotes(getNotes());
|
db.relations
|
||||||
}, [attachment, getNotes]);
|
.to(attachment, "note")
|
||||||
|
.selector.items()
|
||||||
|
.then((items) => {
|
||||||
|
setNotes(items);
|
||||||
|
});
|
||||||
|
}, [attachment]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -194,7 +221,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.secondary.background,
|
borderBottomColor: colors.primary.border,
|
||||||
marginBottom: notes && notes.length > 0 ? 0 : 12
|
marginBottom: notes && notes.length > 0 ? 0 : 12
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -221,7 +248,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
}}
|
}}
|
||||||
color={colors.secondary.paragraph}
|
color={colors.secondary.paragraph}
|
||||||
>
|
>
|
||||||
{attachment.metadata.type}
|
{attachment.type}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph
|
<Paragraph
|
||||||
style={{
|
style={{
|
||||||
@@ -230,10 +257,10 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
size={SIZE.xs}
|
size={SIZE.xs}
|
||||||
color={colors.secondary.paragraph}
|
color={colors.secondary.paragraph}
|
||||||
>
|
>
|
||||||
{formatBytes(attachment.length)}
|
{formatBytes(attachment.size)}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
{attachment.noteIds ? (
|
{notes.length ? (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
style={{
|
style={{
|
||||||
marginRight: 10
|
marginRight: 10
|
||||||
@@ -241,14 +268,14 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
size={SIZE.xs}
|
size={SIZE.xs}
|
||||||
color={colors.secondary.paragraph}
|
color={colors.secondary.paragraph}
|
||||||
>
|
>
|
||||||
{attachment.noteIds.length} note
|
{notes.length} note
|
||||||
{attachment.noteIds.length > 1 ? "s" : ""}
|
{notes.length > 1 ? "s" : ""}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
) : null}
|
) : null}
|
||||||
<Paragraph
|
<Paragraph
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
Clipboard.setString(attachment.metadata.hash);
|
Clipboard.setString(attachment.hash);
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
type: "success",
|
type: "success",
|
||||||
heading: "Attachment hash copied",
|
heading: "Attachment hash copied",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -257,7 +284,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
size={SIZE.xs}
|
size={SIZE.xs}
|
||||||
color={colors.secondary.paragraph}
|
color={colors.secondary.paragraph}
|
||||||
>
|
>
|
||||||
{attachment.metadata.hash}
|
{attachment.hash}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -268,7 +295,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.secondary.background,
|
borderBottomColor: colors.primary.border,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
paddingVertical: 12
|
paddingVertical: 12
|
||||||
}}
|
}}
|
||||||
@@ -284,25 +311,15 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
{notes.map((item) => (
|
{notes.map((item) => (
|
||||||
<PressableButton
|
<Pressable
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (item.type === "notfound") {
|
|
||||||
ToastEvent.show({
|
|
||||||
heading: "Note not found",
|
|
||||||
message:
|
|
||||||
"A note with the given id was not found. Maybe you have deleted the note or moved it to trash already.",
|
|
||||||
type: "error",
|
|
||||||
context: "local"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
eSendEvent(eCloseSheet, contextId);
|
eSendEvent(eCloseSheet, contextId);
|
||||||
await sleep(150);
|
await sleep(150);
|
||||||
eSendEvent(eCloseAttachmentDialog);
|
eSendEvent(eCloseAttachmentDialog);
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
openNote(item, item.type === "trash");
|
openNote(item, (item as any).type === "trash");
|
||||||
}}
|
}}
|
||||||
customStyle={{
|
style={{
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
|
|
||||||
@@ -311,7 +328,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
>
|
>
|
||||||
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
||||||
</PressableButton>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
</View>
|
</View>
|
||||||
@@ -321,17 +338,16 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
<Button
|
<Button
|
||||||
key={item.name}
|
key={item.name}
|
||||||
buttonType={{
|
buttonType={{
|
||||||
text: item.on
|
text:
|
||||||
? colors.primary.accent
|
item.name === "Delete"
|
||||||
: item.name === "Delete" || item.name === "PermDelete"
|
? colors.error.paragraph
|
||||||
? colors.error.paragraph
|
: colors.primary.paragraph
|
||||||
: colors.primary.paragraph
|
|
||||||
}}
|
}}
|
||||||
onPress={item.onPress}
|
onPress={item.onPress}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
loading={loading?.name === item.name}
|
loading={loading?.name === item.name}
|
||||||
type={item.on ? "shade" : "gray"}
|
type="plain"
|
||||||
fontSize={SIZE.sm}
|
fontSize={SIZE.sm}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
@@ -359,7 +375,11 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Actions.present = (attachment, set, context) => {
|
Actions.present = (
|
||||||
|
attachment: Attachment,
|
||||||
|
set: (attachments?: VirtualizedGrouping<Attachment>) => void,
|
||||||
|
context?: string
|
||||||
|
) => {
|
||||||
presentSheet({
|
presentSheet({
|
||||||
context: context,
|
context: context,
|
||||||
component: (ref, close) => (
|
component: (ref, close) => (
|
||||||
@@ -1,171 +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 { formatBytes } from "@notesnook/common";
|
|
||||||
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 { useThemeColors } from "@notesnook/theme";
|
|
||||||
import { SIZE } from "../../utils/size";
|
|
||||||
import { IconButton } from "../ui/icon-button";
|
|
||||||
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
|
||||||
import Paragraph from "../ui/typography/paragraph";
|
|
||||||
import Actions from "./actions";
|
|
||||||
|
|
||||||
function getFileExtension(filename) {
|
|
||||||
var ext = /^.+\.([^.]+)$/.exec(filename);
|
|
||||||
return ext == null ? "" : ext[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AttachmentItem = ({
|
|
||||||
attachment,
|
|
||||||
encryption,
|
|
||||||
setAttachments,
|
|
||||||
pressable = true,
|
|
||||||
hideWhenNotDownloading,
|
|
||||||
context
|
|
||||||
}) => {
|
|
||||||
const { colors } = useThemeColors();
|
|
||||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
|
||||||
attachment,
|
|
||||||
encryption
|
|
||||||
);
|
|
||||||
const onPress = () => {
|
|
||||||
if (!pressable) return;
|
|
||||||
Actions.present(attachment, setAttachments, context);
|
|
||||||
};
|
|
||||||
|
|
||||||
return hideWhenNotDownloading &&
|
|
||||||
(!currentProgress || !currentProgress.value) ? null : (
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={0.9}
|
|
||||||
onPress={onPress}
|
|
||||||
style={{
|
|
||||||
flexDirection: "row",
|
|
||||||
marginVertical: 5,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
padding: 12,
|
|
||||||
paddingVertical: 6,
|
|
||||||
borderRadius: 5,
|
|
||||||
backgroundColor: colors.secondary.background
|
|
||||||
}}
|
|
||||||
type="grayBg"
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexShrink: 1,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
marginLeft: -5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
|
|
||||||
|
|
||||||
<Paragraph
|
|
||||||
adjustsFontSizeToFit
|
|
||||||
size={6}
|
|
||||||
color={colors.static.white}
|
|
||||||
style={{
|
|
||||||
position: "absolute"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getFileExtension(attachment.metadata.filename).toUpperCase()}
|
|
||||||
</Paragraph>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexShrink: 1,
|
|
||||||
marginLeft: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paragraph
|
|
||||||
size={SIZE.sm - 1}
|
|
||||||
style={{
|
|
||||||
flexWrap: "wrap",
|
|
||||||
marginBottom: 2.5
|
|
||||||
}}
|
|
||||||
numberOfLines={1}
|
|
||||||
lineBreakMode="middle"
|
|
||||||
color={colors.primary.paragraph}
|
|
||||||
>
|
|
||||||
{attachment.metadata.filename}
|
|
||||||
</Paragraph>
|
|
||||||
|
|
||||||
{!hideWhenNotDownloading ? (
|
|
||||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
|
||||||
{formatBytes(attachment.length)}{" "}
|
|
||||||
{currentProgress?.type
|
|
||||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
|
||||||
: ""}
|
|
||||||
</Paragraph>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{currentProgress ? (
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={0.9}
|
|
||||||
onPress={() => {
|
|
||||||
if (encryption || !pressable) return;
|
|
||||||
db.fs.cancel(attachment.metadata.hash);
|
|
||||||
setCurrentProgress(null);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
justifyContent: "center",
|
|
||||||
marginLeft: 5,
|
|
||||||
marginTop: 5,
|
|
||||||
marginRight: -5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProgressCircleComponent
|
|
||||||
size={SIZE.xxl}
|
|
||||||
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
|
|
||||||
showsText
|
|
||||||
textStyle={{
|
|
||||||
fontSize: 10
|
|
||||||
}}
|
|
||||||
color={colors.primary.accent}
|
|
||||||
formatText={(progress) => (progress * 100).toFixed(0)}
|
|
||||||
borderWidth={0}
|
|
||||||
thickness={2}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{attachment.failed ? (
|
|
||||||
<IconButton
|
|
||||||
onPress={onPress}
|
|
||||||
name="alert-circle-outline"
|
|
||||||
color={colors.error.paragraph}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
191
apps/mobile/app/components/attachments/attachment-item.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
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 { formatBytes } from "@notesnook/common";
|
||||||
|
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";
|
||||||
|
import { SIZE } from "../../utils/size";
|
||||||
|
import { IconButton } from "../ui/icon-button";
|
||||||
|
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
||||||
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
|
import Actions from "./actions";
|
||||||
|
|
||||||
|
function getFileExtension(filename: string) {
|
||||||
|
const ext = /^.+\.([^.]+)$/.exec(filename);
|
||||||
|
return ext == null ? "" : ext[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AttachmentItem = ({
|
||||||
|
id,
|
||||||
|
attachments,
|
||||||
|
encryption,
|
||||||
|
setAttachments,
|
||||||
|
pressable = true,
|
||||||
|
hideWhenNotDownloading,
|
||||||
|
context
|
||||||
|
}: {
|
||||||
|
id: string | number;
|
||||||
|
attachments?: VirtualizedGrouping<Attachment>;
|
||||||
|
encryption?: boolean;
|
||||||
|
setAttachments: (attachments: any) => void;
|
||||||
|
pressable?: boolean;
|
||||||
|
hideWhenNotDownloading?: boolean;
|
||||||
|
context?: string;
|
||||||
|
}) => {
|
||||||
|
const [attachment] = useDBItem(id, "attachment", attachments);
|
||||||
|
|
||||||
|
const { colors } = useThemeColors();
|
||||||
|
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||||
|
attachment,
|
||||||
|
encryption
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPress = () => {
|
||||||
|
if (!pressable || !attachment) return;
|
||||||
|
Actions.present(attachment, setAttachments, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
return hideWhenNotDownloading &&
|
||||||
|
(!currentProgress || !currentProgress.value) ? null : (
|
||||||
|
<TouchableOpacity
|
||||||
|
activeOpacity={0.9}
|
||||||
|
onPress={onPress}
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
marginVertical: 5,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: colors.secondary.background,
|
||||||
|
minHeight: 45
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!attachment ? null : (
|
||||||
|
<>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexShrink: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginLeft: -5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
|
||||||
|
|
||||||
|
<Paragraph
|
||||||
|
adjustsFontSizeToFit
|
||||||
|
size={6}
|
||||||
|
color={colors.static.white}
|
||||||
|
style={{
|
||||||
|
position: "absolute"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getFileExtension(attachment.filename).toUpperCase()}
|
||||||
|
</Paragraph>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexShrink: 1,
|
||||||
|
marginLeft: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paragraph
|
||||||
|
size={SIZE.sm - 1}
|
||||||
|
style={{
|
||||||
|
flexWrap: "wrap",
|
||||||
|
marginBottom: 2.5
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
lineBreakMode="middle"
|
||||||
|
color={colors.primary.paragraph}
|
||||||
|
>
|
||||||
|
{attachment.filename}
|
||||||
|
</Paragraph>
|
||||||
|
|
||||||
|
{!hideWhenNotDownloading ? (
|
||||||
|
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||||
|
{formatBytes(attachment.size)}{" "}
|
||||||
|
{currentProgress?.type
|
||||||
|
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||||
|
: ""}
|
||||||
|
</Paragraph>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{currentProgress ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
activeOpacity={0.9}
|
||||||
|
onPress={() => {
|
||||||
|
if (encryption || !pressable) return;
|
||||||
|
db.fs().cancel(attachment.hash);
|
||||||
|
setCurrentProgress(undefined);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
justifyContent: "center",
|
||||||
|
marginLeft: 5,
|
||||||
|
marginTop: 5,
|
||||||
|
marginRight: -5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProgressCircleComponent
|
||||||
|
size={SIZE.xxl}
|
||||||
|
progress={
|
||||||
|
currentProgress?.value ? currentProgress?.value / 100 : 0
|
||||||
|
}
|
||||||
|
showsText
|
||||||
|
textStyle={{
|
||||||
|
fontSize: 10
|
||||||
|
}}
|
||||||
|
color={colors.primary.accent}
|
||||||
|
formatText={(progress) => (progress * 100).toFixed(0)}
|
||||||
|
borderWidth={0}
|
||||||
|
thickness={2}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{attachment.failed ? (
|
||||||
|
<IconButton
|
||||||
|
onPress={onPress}
|
||||||
|
name="alert-circle-outline"
|
||||||
|
color={colors.error.paragraph}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -21,7 +21,10 @@ import React, { useRef, useState } from "react";
|
|||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { db } from "../../common/database";
|
import { db } from "../../common/database";
|
||||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||||
import { presentSheet } from "../../services/event-manager";
|
import {
|
||||||
|
PresentSheetOptions,
|
||||||
|
presentSheet
|
||||||
|
} from "../../services/event-manager";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import Heading from "../ui/typography/heading";
|
import Heading from "../ui/typography/heading";
|
||||||
import Paragraph from "../ui/typography/paragraph";
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
@@ -29,8 +32,19 @@ import { ProgressBarComponent } from "../ui/svg/lazy";
|
|||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
import { FlatList } from "react-native-actions-sheet";
|
import { FlatList } from "react-native-actions-sheet";
|
||||||
import { AttachmentItem } from "./attachment-item";
|
import { AttachmentItem } from "./attachment-item";
|
||||||
|
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||||
|
|
||||||
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
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 { colors } = useThemeColors();
|
||||||
const [downloading, setDownloading] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
const [progress, setProgress] = useState({
|
const [progress, setProgress] = useState({
|
||||||
@@ -39,63 +53,58 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
|||||||
});
|
});
|
||||||
const [result, setResult] = useState(new Map());
|
const [result, setResult] = useState(new Map());
|
||||||
const canceled = useRef(false);
|
const canceled = useRef(false);
|
||||||
const groupId = useRef();
|
const groupId = useRef<string>();
|
||||||
|
|
||||||
const onDownload = async () => {
|
const onDownload = async () => {
|
||||||
update({
|
update?.({
|
||||||
disableClosing: true
|
disableClosing: true
|
||||||
});
|
} as PresentSheetOptions);
|
||||||
setDownloading(true);
|
setDownloading(true);
|
||||||
canceled.current = false;
|
canceled.current = false;
|
||||||
groupId.current = Date.now().toString();
|
groupId.current = Date.now().toString();
|
||||||
const result = await downloadAttachments(
|
const result = await downloadAttachments(
|
||||||
attachments,
|
await attachments.ids(),
|
||||||
(progress, statusText) => setProgress({ value: progress, statusText }),
|
(progress: number, statusText: string) =>
|
||||||
|
setProgress({ value: progress, statusText }),
|
||||||
canceled,
|
canceled,
|
||||||
groupId.current
|
groupId.current
|
||||||
);
|
);
|
||||||
if (canceled.current) return;
|
if (canceled.current) return;
|
||||||
setResult(result || new Map());
|
setResult(result || new Map());
|
||||||
setDownloading(false);
|
setDownloading(false);
|
||||||
update({
|
update?.({
|
||||||
disableClosing: false
|
disableClosing: false
|
||||||
});
|
} as PresentSheetOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = async () => {
|
const cancel = async () => {
|
||||||
update({
|
update?.({
|
||||||
disableClosing: false
|
disableClosing: false
|
||||||
});
|
} as PresentSheetOptions);
|
||||||
canceled.current = true;
|
canceled.current = true;
|
||||||
|
if (!groupId.current) return;
|
||||||
console.log(groupId.current, "canceling groupId downloads");
|
console.log(groupId.current, "canceling groupId downloads");
|
||||||
await db.fs.cancel(groupId.current);
|
await db.fs().cancel(groupId.current);
|
||||||
setDownloading(false);
|
setDownloading(false);
|
||||||
groupId.current = null;
|
setResult(new Map());
|
||||||
};
|
groupId.current = undefined;
|
||||||
|
|
||||||
const successResults = () => {
|
|
||||||
const results = [];
|
|
||||||
for (let [key, value] of result.entries()) {
|
|
||||||
if (value.status === 1) results.push(db.attachments.attachment(key));
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const failedResults = () => {
|
const failedResults = () => {
|
||||||
const results = [];
|
const results = [];
|
||||||
for (let [key, value] of result.entries()) {
|
for (const value of result.values()) {
|
||||||
if (value.status === 0) results.push(db.attachments.attachment(key));
|
if (value.status === 0) results.push(value.attachment);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getResultText() {
|
function getResultText() {
|
||||||
const downloadedAttachmentsCount =
|
const downloadedAttachmentsCount =
|
||||||
attachments.length - failedResults().length;
|
attachments?.placeholders?.length - failedResults().length;
|
||||||
if (downloadedAttachmentsCount === 0)
|
if (downloadedAttachmentsCount === 0)
|
||||||
return "Failed to download all attachments";
|
return "Failed to download all attachments";
|
||||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||||
attachments.length
|
attachments?.placeholders.length
|
||||||
} attachments as a zip file at ${
|
} attachments as a zip file at ${
|
||||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||||
}`;
|
}`;
|
||||||
@@ -157,7 +166,11 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
|||||||
width={null}
|
width={null}
|
||||||
animated={true}
|
animated={true}
|
||||||
useNativeDriver
|
useNativeDriver
|
||||||
progress={progress.value ? progress.value / attachments.length : 0}
|
progress={
|
||||||
|
progress.value
|
||||||
|
? progress.value / attachments.placeholders?.length
|
||||||
|
: 0
|
||||||
|
}
|
||||||
unfilledColor={colors.secondary.background}
|
unfilledColor={colors.secondary.background}
|
||||||
color={colors.primary.accent}
|
color={colors.primary.accent}
|
||||||
borderWidth={0}
|
borderWidth={0}
|
||||||
@@ -174,7 +187,7 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
|||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
marginVertical: 12
|
marginVertical: 12
|
||||||
}}
|
}}
|
||||||
data={downloading ? attachments : []}
|
data={downloading ? attachments.placeholders : undefined}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -189,14 +202,15 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
|||||||
</Paragraph>
|
</Paragraph>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(index) => "attachment_download" + index}
|
||||||
renderItem={({ item }) => {
|
renderItem={({ index }) => {
|
||||||
return (
|
return (
|
||||||
<AttachmentItem
|
<AttachmentItem
|
||||||
attachment={item}
|
id={index}
|
||||||
setAttachments={() => {}}
|
setAttachments={() => {}}
|
||||||
pressable={false}
|
pressable={false}
|
||||||
hideWhenNotDownloading={true}
|
hideWhenNotDownloading={true}
|
||||||
|
attachments={attachments}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -209,7 +223,9 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
|||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
marginTop: 20
|
marginTop: 20
|
||||||
}}
|
}}
|
||||||
onPress={close}
|
onPress={() => {
|
||||||
|
close?.();
|
||||||
|
}}
|
||||||
type="accent"
|
type="accent"
|
||||||
title="Done"
|
title="Done"
|
||||||
/>
|
/>
|
||||||
@@ -227,8 +243,10 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
|||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
marginRight: 5
|
marginRight: 5
|
||||||
}}
|
}}
|
||||||
onPress={close}
|
onPress={() => {
|
||||||
type="grayBg"
|
close?.();
|
||||||
|
}}
|
||||||
|
type="secondary"
|
||||||
title="No"
|
title="No"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -258,7 +276,11 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DownloadAttachments.present = (context, attachments, isNote) => {
|
DownloadAttachments.present = (
|
||||||
|
context: string,
|
||||||
|
attachments: VirtualizedGrouping<Attachment>,
|
||||||
|
isNote?: boolean
|
||||||
|
) => {
|
||||||
presentSheet({
|
presentSheet({
|
||||||
context: context,
|
context: context,
|
||||||
component: (ref, close, update) => (
|
component: (ref, close, update) => (
|
||||||
@@ -17,16 +17,25 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useRef, useState } from "react";
|
import {
|
||||||
|
Attachment,
|
||||||
|
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, ScrollView, View } from "react-native";
|
||||||
|
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||||
import { db } from "../../common/database";
|
import { db } from "../../common/database";
|
||||||
import filesystem from "../../common/filesystem";
|
import filesystem from "../../common/filesystem";
|
||||||
import { presentSheet } from "../../services/event-manager";
|
import { presentSheet } from "../../services/event-manager";
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useSettingStore } from "../../stores/use-setting-store";
|
||||||
import { SIZE } from "../../utils/size";
|
import { SIZE } from "../../utils/size";
|
||||||
import SheetProvider from "../sheet-provider";
|
import SheetProvider from "../sheet-provider";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
import { IconButton } from "../ui/icon-button";
|
import { IconButton } from "../ui/icon-button";
|
||||||
import Input from "../ui/input";
|
import Input from "../ui/input";
|
||||||
import Seperator from "../ui/seperator";
|
import Seperator from "../ui/seperator";
|
||||||
@@ -34,82 +43,92 @@ import Heading from "../ui/typography/heading";
|
|||||||
import Paragraph from "../ui/typography/paragraph";
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
import { AttachmentItem } from "./attachment-item";
|
import { AttachmentItem } from "./attachment-item";
|
||||||
import DownloadAttachments from "./download-attachments";
|
import DownloadAttachments from "./download-attachments";
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import {
|
|
||||||
isAudio,
|
|
||||||
isDocument,
|
|
||||||
isImage,
|
|
||||||
isVideo
|
|
||||||
} from "@notesnook/core/dist/utils/filename";
|
|
||||||
import { useSettingStore } from "../../stores/use-setting-store";
|
|
||||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
|
||||||
|
|
||||||
export const AttachmentDialog = ({ note }) => {
|
const DEFAULT_SORTING: SortOptions = {
|
||||||
|
sortBy: "dateEdited",
|
||||||
|
sortDirection: "desc"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const { height } = useSettingStore((state) => state.dimensions);
|
const { height } = useSettingStore((state) => state.dimensions);
|
||||||
const [attachments, setAttachments] = useState(
|
const [attachments, setAttachments] =
|
||||||
note
|
useState<VirtualizedGrouping<Attachment>>();
|
||||||
? db.attachments.ofNote(note.id, "all")
|
const attachmentSearchValue = useRef<string>();
|
||||||
: [...(db.attachments.all || [])]
|
const searchTimer = useRef<NodeJS.Timeout>();
|
||||||
);
|
|
||||||
|
|
||||||
const attachmentSearchValue = useRef();
|
|
||||||
const searchTimer = useRef();
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [currentFilter, setCurrentFilter] = useState("all");
|
const [currentFilter, setCurrentFilter] = useState("all");
|
||||||
|
|
||||||
const onChangeText = (text) => {
|
const refresh = React.useCallback(() => {
|
||||||
const attachments = note
|
if (note) {
|
||||||
? db.attachments.ofNote(note.id, "all")
|
db.attachments
|
||||||
: [...(db.attachments.all || [])];
|
.ofNote(note.id, "all")
|
||||||
|
.sorted({
|
||||||
|
...DEFAULT_SORTING,
|
||||||
|
sortBy: "dateModified"
|
||||||
|
})
|
||||||
|
.then((attachments) => {
|
||||||
|
setAttachments(attachments);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
db.attachments.all
|
||||||
|
.sorted({
|
||||||
|
...DEFAULT_SORTING,
|
||||||
|
sortBy: "dateModified"
|
||||||
|
})
|
||||||
|
.then((attachments) => setAttachments(attachments));
|
||||||
|
}
|
||||||
|
}, [note]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [note, refresh]);
|
||||||
|
|
||||||
|
const onChangeText = async (text: string) => {
|
||||||
attachmentSearchValue.current = text;
|
attachmentSearchValue.current = text;
|
||||||
if (
|
if (
|
||||||
!attachmentSearchValue.current ||
|
!attachmentSearchValue.current ||
|
||||||
attachmentSearchValue.current === ""
|
attachmentSearchValue.current === ""
|
||||||
) {
|
) {
|
||||||
setAttachments(filterAttachments(currentFilter));
|
setAttachments(await filterAttachments(currentFilter));
|
||||||
|
refresh();
|
||||||
}
|
}
|
||||||
clearTimeout(searchTimer.current);
|
clearTimeout(searchTimer.current);
|
||||||
searchTimer.current = setTimeout(() => {
|
searchTimer.current = setTimeout(async () => {
|
||||||
let results = db.lookup.attachments(
|
const results = await db.lookup
|
||||||
attachments,
|
.attachments(attachmentSearchValue.current as string)
|
||||||
attachmentSearchValue.current
|
.sorted();
|
||||||
);
|
|
||||||
if (results.length === 0) return;
|
if (results.length === 0) return;
|
||||||
|
setAttachments(results);
|
||||||
setAttachments(filterAttachments(currentFilter, results));
|
|
||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderItem = ({ item }) => (
|
const renderItem = ({ index }: { item: boolean; index: number }) => (
|
||||||
<AttachmentItem
|
<AttachmentItem
|
||||||
setAttachments={() => {
|
setAttachments={async () => {
|
||||||
setAttachments(filterAttachments(currentFilter));
|
setAttachments(await filterAttachments(currentFilter));
|
||||||
}}
|
}}
|
||||||
attachment={item}
|
attachments={attachments}
|
||||||
|
id={index}
|
||||||
context="attachments-list"
|
context="attachments-list"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const onCheck = async () => {
|
const onCheck = async () => {
|
||||||
|
if (!attachments) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const checkedAttachments = [];
|
|
||||||
for (let attachment of attachments) {
|
for (let i = 0; i < attachments.placeholders.length; i++) {
|
||||||
let result = await filesystem.checkAttachment(attachment.metadata.hash);
|
const attachment = (await attachments.item(i))?.item;
|
||||||
|
if (!attachment) continue;
|
||||||
|
const result = await filesystem.checkAttachment(attachment.hash);
|
||||||
if (result.failed) {
|
if (result.failed) {
|
||||||
await db.attachments.markAsFailed(
|
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
||||||
attachment.metadata.hash,
|
|
||||||
result.failed
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await db.attachments.markAsFailed(attachment.id, null);
|
await db.attachments.markAsFailed(attachment.id, undefined);
|
||||||
}
|
}
|
||||||
checkedAttachments.push(
|
|
||||||
db.attachments.attachment(attachment.metadata.hash)
|
|
||||||
);
|
|
||||||
setAttachments([...checkedAttachments]);
|
|
||||||
}
|
}
|
||||||
|
refresh();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,32 +155,40 @@ export const AttachmentDialog = ({ note }) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const filterAttachments = (type, _attachments) => {
|
const filterAttachments = async (type: string) => {
|
||||||
const attachments =
|
let items: FilteredSelector<Attachment> = db.attachments.all;
|
||||||
_attachments || note
|
|
||||||
? db.attachments.ofNote(note.id, "all")
|
|
||||||
: [...(db.attachments.all || [])];
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "all":
|
case "all":
|
||||||
return attachments;
|
items = note
|
||||||
|
? db.attachments.ofNote(note.id, "all")
|
||||||
|
: db.attachments.all;
|
||||||
|
break;
|
||||||
case "images":
|
case "images":
|
||||||
return attachments.filter((attachment) =>
|
items = note
|
||||||
isImage(attachment.metadata.type)
|
? db.attachments.ofNote(note.id, "images")
|
||||||
);
|
: db.attachments.images;
|
||||||
|
break;
|
||||||
case "video":
|
case "video":
|
||||||
return attachments.filter((attachment) =>
|
items = items = note
|
||||||
isVideo(attachment.metadata.type)
|
? db.attachments.ofNote(note.id, "videos")
|
||||||
);
|
: db.attachments.videos;
|
||||||
|
break;
|
||||||
case "audio":
|
case "audio":
|
||||||
return attachments.filter((attachment) =>
|
items = note
|
||||||
isAudio(attachment.metadata.type)
|
? db.attachments.ofNote(note.id, "audio")
|
||||||
);
|
: db.attachments.all;
|
||||||
|
break;
|
||||||
case "documents":
|
case "documents":
|
||||||
return attachments.filter((attachment) =>
|
items = note
|
||||||
isDocument(attachment.metadata.type)
|
? db.attachments.ofNote(note.id, "documents")
|
||||||
);
|
: db.attachments.documents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await items.sorted({
|
||||||
|
...DEFAULT_SORTING,
|
||||||
|
sortBy: "dateModified"
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -200,7 +227,7 @@ export const AttachmentDialog = ({ note }) => {
|
|||||||
) : (
|
) : (
|
||||||
<IconButton
|
<IconButton
|
||||||
name="check-all"
|
name="check-all"
|
||||||
customStyle={{
|
style={{
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
marginRight: 10
|
marginRight: 10
|
||||||
@@ -213,12 +240,13 @@ export const AttachmentDialog = ({ note }) => {
|
|||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
name="download"
|
name="download"
|
||||||
customStyle={{
|
style={{
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40
|
width: 40
|
||||||
}}
|
}}
|
||||||
color={colors.primary.paragraph}
|
color={colors.primary.paragraph}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
if (!attachments) return;
|
||||||
DownloadAttachments.present(
|
DownloadAttachments.present(
|
||||||
"attachments-list",
|
"attachments-list",
|
||||||
attachments,
|
attachments,
|
||||||
@@ -235,7 +263,7 @@ export const AttachmentDialog = ({ note }) => {
|
|||||||
placeholder="Filter attachments by filename, type or hash"
|
placeholder="Filter attachments by filename, type or hash"
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
onChangeText(attachmentSearchValue.current);
|
onChangeText(attachmentSearchValue.current as string);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -255,12 +283,11 @@ export const AttachmentDialog = ({ note }) => {
|
|||||||
>
|
>
|
||||||
{attachmentTypes.map((item) => (
|
{attachmentTypes.map((item) => (
|
||||||
<Button
|
<Button
|
||||||
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
|
type={
|
||||||
key={item.title}
|
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
|
||||||
title={
|
|
||||||
item.title +
|
|
||||||
` (${filterAttachments(item.filterBy)?.length || 0})`
|
|
||||||
}
|
}
|
||||||
|
key={item.title}
|
||||||
|
title={item.title}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
@@ -270,9 +297,9 @@ export const AttachmentDialog = ({ note }) => {
|
|||||||
? "transparent"
|
? "transparent"
|
||||||
: colors.primary.accent
|
: colors.primary.accent
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
setCurrentFilter(item.filterBy);
|
setCurrentFilter(item.filterBy);
|
||||||
setAttachments(filterAttachments(item.filterBy));
|
setAttachments(await filterAttachments(item.filterBy));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -303,7 +330,7 @@ export const AttachmentDialog = ({ note }) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
estimatedItemSize={50}
|
estimatedItemSize={50}
|
||||||
data={attachments}
|
data={attachments?.placeholders}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -326,7 +353,7 @@ export const AttachmentDialog = ({ note }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AttachmentDialog.present = (note) => {
|
AttachmentDialog.present = (note?: Note) => {
|
||||||
presentSheet({
|
presentSheet({
|
||||||
component: () => <AttachmentDialog note={note} />
|
component: () => <AttachmentDialog note={note} />
|
||||||
});
|
});
|
||||||
@@ -158,7 +158,7 @@ const AuthModal = () => {
|
|||||||
hideAuth();
|
hideAuth();
|
||||||
}}
|
}}
|
||||||
iconSize={16}
|
iconSize={16}
|
||||||
type="gray"
|
type="plain"
|
||||||
iconPosition="right"
|
iconPosition="right"
|
||||||
icon="chevron-right"
|
icon="chevron-right"
|
||||||
height={25}
|
height={25}
|
||||||
|
|||||||
@@ -23,15 +23,18 @@ import { db } from "../../common/database";
|
|||||||
import {
|
import {
|
||||||
eSendEvent,
|
eSendEvent,
|
||||||
presentSheet,
|
presentSheet,
|
||||||
ToastEvent
|
ToastManager
|
||||||
} from "../../services/event-manager";
|
} from "../../services/event-manager";
|
||||||
import { useUserStore } from "../../stores/use-user-store";
|
import { useUserStore } from "../../stores/use-user-store";
|
||||||
import { eCloseSheet } from "../../utils/events";
|
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||||
import DialogHeader from "../dialog/dialog-header";
|
import DialogHeader from "../dialog/dialog-header";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import Input from "../ui/input";
|
import Input from "../ui/input";
|
||||||
import { Notice } from "../ui/notice";
|
import { Notice } from "../ui/notice";
|
||||||
import Seperator from "../ui/seperator";
|
import Seperator from "../ui/seperator";
|
||||||
|
import { Dialog } from "../dialog";
|
||||||
|
import BackupService from "../../services/backup";
|
||||||
|
import { sleep } from "../../utils/time";
|
||||||
|
|
||||||
export const ChangePassword = () => {
|
export const ChangePassword = () => {
|
||||||
const passwordInputRef = useRef();
|
const passwordInputRef = useRef();
|
||||||
@@ -45,7 +48,7 @@ export const ChangePassword = () => {
|
|||||||
|
|
||||||
const changePassword = async () => {
|
const changePassword = async () => {
|
||||||
if (!user?.isEmailConfirmed) {
|
if (!user?.isEmailConfirmed) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Email not confirmed",
|
heading: "Email not confirmed",
|
||||||
message: "Please confirm your email to change account password",
|
message: "Please confirm your email to change account password",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -54,7 +57,7 @@ export const ChangePassword = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (error || !oldPassword.current || !password.current) {
|
if (error || !oldPassword.current || !password.current) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "All fields required",
|
heading: "All fields required",
|
||||||
message: "Fill all the fields and try again.",
|
message: "Fill all the fields and try again.",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -64,18 +67,23 @@ export const ChangePassword = () => {
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const result = await BackupService.run(false, "change-password-dialog");
|
||||||
|
if (!result) throw new Error("Failed to create backup");
|
||||||
|
|
||||||
await db.user.clearSessions();
|
await db.user.clearSessions();
|
||||||
await db.user.changePassword(oldPassword.current, password.current);
|
await db.user.changePassword(oldPassword.current, password.current);
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Account password updated",
|
heading: "Account password updated",
|
||||||
type: "success",
|
type: "success",
|
||||||
context: "global"
|
context: "global"
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
eSendEvent(eCloseSheet);
|
eSendEvent(eCloseSheet);
|
||||||
|
await sleep(300);
|
||||||
|
eSendEvent(eOpenRecoveryKeyDialog);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Failed to change password",
|
heading: "Failed to change password",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -92,6 +100,7 @@ export const ChangePassword = () => {
|
|||||||
padding: 12
|
padding: 12
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Dialog context="change-password-dialog" />
|
||||||
<DialogHeader
|
<DialogHeader
|
||||||
title="Change password"
|
title="Change password"
|
||||||
paragraph="Enter your old and new passwords"
|
paragraph="Enter your old and new passwords"
|
||||||
@@ -129,7 +138,14 @@ export const ChangePassword = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Notice
|
<Notice
|
||||||
text="Changing password is a non-undoable process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection."
|
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
|
||||||
|
type="alert"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={{ height: 10 }} />
|
||||||
|
|
||||||
|
<Notice
|
||||||
|
text={`Once your password is changed, please make sure to save the new account recovery key.`}
|
||||||
type="alert"
|
type="alert"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { View } from "react-native";
|
|||||||
import ActionSheet from "react-native-actions-sheet";
|
import ActionSheet from "react-native-actions-sheet";
|
||||||
import { db } from "../../common/database";
|
import { db } from "../../common/database";
|
||||||
import { DDS } from "../../services/device-detection";
|
import { DDS } from "../../services/device-detection";
|
||||||
import { ToastEvent } from "../../services/event-manager";
|
import { ToastManager } from "../../services/event-manager";
|
||||||
import SettingsService from "../../services/settings";
|
import SettingsService from "../../services/settings";
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
import DialogHeader from "../dialog/dialog-header";
|
import DialogHeader from "../dialog/dialog-header";
|
||||||
@@ -43,7 +43,7 @@ export const ForgotPassword = () => {
|
|||||||
|
|
||||||
const sendRecoveryEmail = async () => {
|
const sendRecoveryEmail = async () => {
|
||||||
if (!email.current || error) {
|
if (!email.current || error) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Account email is required.",
|
heading: "Account email is required.",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -63,7 +63,7 @@ export const ForgotPassword = () => {
|
|||||||
SettingsService.set({
|
SettingsService.set({
|
||||||
lastRecoveryEmailTime: Date.now()
|
lastRecoveryEmailTime: Date.now()
|
||||||
});
|
});
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Check your email to reset password",
|
heading: "Check your email to reset password",
|
||||||
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
|
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
|
||||||
type: "success",
|
type: "success",
|
||||||
@@ -74,7 +74,7 @@ export const ForgotPassword = () => {
|
|||||||
setSent(true);
|
setSent(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Recovery email not sent",
|
heading: "Recovery email not sent",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -113,7 +113,7 @@ export const ForgotPassword = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
customStyle={{
|
style={{
|
||||||
width: null,
|
width: null,
|
||||||
height: null
|
height: null
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -59,22 +59,6 @@ const Auth = ({ navigation, route }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* {initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
|
||||||
<IconButton
|
|
||||||
name="arrow-left"
|
|
||||||
onPress={() => {
|
|
||||||
hideAuth();
|
|
||||||
}}
|
|
||||||
color={colors.primary.paragraph}
|
|
||||||
customStyle={{
|
|
||||||
position: 'absolute',
|
|
||||||
zIndex: 999,
|
|
||||||
left: 12,
|
|
||||||
top: Platform.OS === 'ios' ? 12 + insets.top : insets.top
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
<Toast context="local" />
|
<Toast context="local" />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { useSettingStore } from "../../stores/use-setting-store";
|
|||||||
import { eUserLoggedIn } from "../../utils/events";
|
import { eUserLoggedIn } from "../../utils/events";
|
||||||
import { useUserStore } from "../../stores/use-user-store";
|
import { useUserStore } from "../../stores/use-user-store";
|
||||||
import Sync from "../../services/sync";
|
import Sync from "../../services/sync";
|
||||||
|
import { Notice } from "../ui/notice";
|
||||||
|
|
||||||
const LoginSteps = {
|
const LoginSteps = {
|
||||||
emailAuth: 1,
|
emailAuth: 1,
|
||||||
@@ -103,7 +104,7 @@ export const Login = ({ changeMode }) => {
|
|||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
backgroundColor: colors.secondary.background,
|
backgroundColor: colors.secondary.background,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 0.8,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
borderBottomColor: colors.primary.border,
|
borderBottomColor: colors.primary.border,
|
||||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||||
@@ -222,7 +223,7 @@ export const Login = ({ changeMode }) => {
|
|||||||
textDecorationLine: "underline"
|
textDecorationLine: "underline"
|
||||||
}}
|
}}
|
||||||
fontSize={SIZE.xs}
|
fontSize={SIZE.xs}
|
||||||
type="gray"
|
type="plain"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Modal, View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { db } from "../../common/database";
|
import { db } from "../../common/database";
|
||||||
import { MMKV } from "../../common/database/mmkv";
|
import { MMKV } from "../../common/database/mmkv";
|
||||||
import BiometricService from "../../services/biometrics";
|
import BiometricService from "../../services/biometrics";
|
||||||
import {
|
import {
|
||||||
ToastEvent,
|
ToastManager,
|
||||||
eSendEvent,
|
eSendEvent,
|
||||||
eSubscribeEvent
|
eSubscribeEvent
|
||||||
} from "../../services/event-manager";
|
} from "../../services/event-manager";
|
||||||
@@ -37,6 +37,7 @@ import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
|
|||||||
import { SIZE } from "../../utils/size";
|
import { SIZE } from "../../utils/size";
|
||||||
import { sleep } from "../../utils/time";
|
import { sleep } from "../../utils/time";
|
||||||
import { Dialog } from "../dialog";
|
import { Dialog } from "../dialog";
|
||||||
|
import BaseDialog from "../dialog/base-dialog";
|
||||||
import { presentDialog } from "../dialog/functions";
|
import { presentDialog } from "../dialog/functions";
|
||||||
import SheetProvider from "../sheet-provider";
|
import SheetProvider from "../sheet-provider";
|
||||||
import { Toast } from "../toast";
|
import { Toast } from "../toast";
|
||||||
@@ -46,7 +47,6 @@ import Input from "../ui/input";
|
|||||||
import Heading from "../ui/typography/heading";
|
import Heading from "../ui/typography/heading";
|
||||||
import Paragraph from "../ui/typography/paragraph";
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
import { LoginSteps, useLogin } from "./use-login";
|
import { LoginSteps, useLogin } from "./use-login";
|
||||||
import BaseDialog from "../dialog/base-dialog";
|
|
||||||
|
|
||||||
function getObfuscatedEmail(email) {
|
function getObfuscatedEmail(email) {
|
||||||
if (!email) return "";
|
if (!email) return "";
|
||||||
@@ -69,6 +69,9 @@ export const SessionExpired = () => {
|
|||||||
eSendEvent(eUserLoggedIn, true);
|
eSendEvent(eUserLoggedIn, true);
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
|
useUserStore.setState({
|
||||||
|
disableAppLockRequests: false
|
||||||
|
});
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -84,8 +87,11 @@ export const SessionExpired = () => {
|
|||||||
MMKV.clearStore();
|
MMKV.clearStore();
|
||||||
clearAllStores();
|
clearAllStores();
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
|
useUserStore.setState({
|
||||||
|
disableAppLockRequests: false
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: e.message,
|
heading: e.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -102,9 +108,9 @@ export const SessionExpired = () => {
|
|||||||
|
|
||||||
const open = React.useCallback(async () => {
|
const open = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
let res = await db.user.tokenManager.getToken();
|
let res = await db.tokenManager.getToken();
|
||||||
if (!res) throw new Error("no token found");
|
if (!res) throw new Error("no token found");
|
||||||
if (db.user.tokenManager._isTokenExpired(res))
|
if (db.tokenManager._isTokenExpired(res))
|
||||||
throw new Error("token expired");
|
throw new Error("token expired");
|
||||||
Sync.run("global", false, true, async (complete) => {
|
Sync.run("global", false, true, async (complete) => {
|
||||||
if (!complete) {
|
if (!complete) {
|
||||||
@@ -127,6 +133,9 @@ export const SessionExpired = () => {
|
|||||||
email.current = user.email;
|
email.current = user.email;
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
useUserStore.setState({
|
||||||
|
disableAppLockRequests: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [email]);
|
}, [email]);
|
||||||
|
|
||||||
@@ -139,9 +148,15 @@ export const SessionExpired = () => {
|
|||||||
animated={false}
|
animated={false}
|
||||||
centered={false}
|
centered={false}
|
||||||
onShow={async () => {
|
onShow={async () => {
|
||||||
|
useUserStore.setState({
|
||||||
|
disableAppLockRequests: true
|
||||||
|
});
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
passwordInputRef.current?.focus();
|
passwordInputRef.current?.focus();
|
||||||
setFocused(true);
|
setFocused(true);
|
||||||
|
useUserStore.setState({
|
||||||
|
disableAppLockRequests: true
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
enableSheetKeyboardHandler={true}
|
enableSheetKeyboardHandler={true}
|
||||||
visible={true}
|
visible={true}
|
||||||
@@ -166,7 +181,7 @@ export const SessionExpired = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
customStyle={{
|
style={{
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60
|
height: 60
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import React, { useRef, useState } from "react";
|
|||||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||||
import { db } from "../../common/database";
|
import { db } from "../../common/database";
|
||||||
import { DDS } from "../../services/device-detection";
|
import { DDS } from "../../services/device-detection";
|
||||||
import { ToastEvent } from "../../services/event-manager";
|
import { ToastManager } from "../../services/event-manager";
|
||||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||||
import PremiumService from "../../services/premium";
|
import PremiumService from "../../services/premium";
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
@@ -52,7 +52,7 @@ export const Signup = ({ changeMode, trial }) => {
|
|||||||
const { width, height } = useWindowDimensions();
|
const { width, height } = useWindowDimensions();
|
||||||
const validateInfo = () => {
|
const validateInfo = () => {
|
||||||
if (!password.current || !email.current || !confirmPassword.current) {
|
if (!password.current || !email.current || !confirmPassword.current) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "All fields required",
|
heading: "All fields required",
|
||||||
message: "Fill all the fields and try again",
|
message: "Fill all the fields and try again",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -85,7 +85,7 @@ export const Signup = ({ changeMode, trial }) => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Signup failed",
|
heading: "Signup failed",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -111,7 +111,7 @@ export const Signup = ({ changeMode, trial }) => {
|
|||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
backgroundColor: colors.secondary.background,
|
backgroundColor: colors.secondary.background,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 0.8,
|
||||||
borderBottomColor: colors.primary.border,
|
borderBottomColor: colors.primary.border,
|
||||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import useTimer from "../../hooks/use-timer";
|
|||||||
import {
|
import {
|
||||||
eSendEvent,
|
eSendEvent,
|
||||||
presentSheet,
|
presentSheet,
|
||||||
ToastEvent
|
ToastManager
|
||||||
} from "../../services/event-manager";
|
} from "../../services/event-manager";
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
import { eCloseSheet } from "../../utils/events";
|
import { eCloseSheet } from "../../utils/events";
|
||||||
@@ -32,7 +32,7 @@ import { SIZE } from "../../utils/size";
|
|||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { IconButton } from "../ui/icon-button";
|
import { IconButton } from "../ui/icon-button";
|
||||||
import Input from "../ui/input";
|
import Input from "../ui/input";
|
||||||
import { PressableButton } from "../ui/pressable";
|
import { Pressable } from "../ui/pressable";
|
||||||
import Seperator from "../ui/seperator";
|
import Seperator from "../ui/seperator";
|
||||||
import Heading from "../ui/typography/heading";
|
import Heading from "../ui/typography/heading";
|
||||||
import Paragraph from "../ui/typography/paragraph";
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
@@ -138,7 +138,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
|||||||
setSending(false);
|
setSending(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
ToastEvent.error(e, "Error sending 2FA Code", "local");
|
ToastManager.error(e, "Error sending 2FA Code", "local");
|
||||||
}
|
}
|
||||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
customStyle={{
|
style={{
|
||||||
width: 70,
|
width: 70,
|
||||||
height: 70
|
height: 70
|
||||||
}}
|
}}
|
||||||
@@ -183,7 +183,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
|||||||
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
|
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
|
||||||
<Button
|
<Button
|
||||||
onPress={onSendCode}
|
onPress={onSendCode}
|
||||||
type={seconds ? "gray" : "transparent"}
|
type={seconds ? "plain" : "transparent"}
|
||||||
title={
|
title={
|
||||||
sending
|
sending
|
||||||
? ""
|
? ""
|
||||||
@@ -247,7 +247,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
title={secondaryMethodsText[currentMethod.method]}
|
title={secondaryMethodsText[currentMethod.method]}
|
||||||
type="gray"
|
type="plain"
|
||||||
onPress={onRequestSecondaryMethod}
|
onPress={onRequestSecondaryMethod}
|
||||||
height={30}
|
height={30}
|
||||||
/>
|
/>
|
||||||
@@ -255,7 +255,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{getMethods().map((item) => (
|
{getMethods().map((item) => (
|
||||||
<PressableButton
|
<Pressable
|
||||||
key={item.title}
|
key={item.title}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setCurrentMethod({
|
setCurrentMethod({
|
||||||
@@ -263,7 +263,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
|||||||
isPrimary: false
|
isPrimary: false
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
customStyle={{
|
style={{
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
@@ -275,8 +275,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
type="grayAccent"
|
type="secondaryAccented"
|
||||||
customStyle={{
|
style={{
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
marginRight: 10
|
marginRight: 10
|
||||||
@@ -292,7 +292,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
|||||||
>
|
>
|
||||||
<Paragraph size={SIZE.md}>{item.title}</Paragraph>
|
<Paragraph size={SIZE.md}>{item.title}</Paragraph>
|
||||||
</View>
|
</View>
|
||||||
</PressableButton>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { db } from "../../common/database";
|
import { db } from "../../common/database";
|
||||||
import { eSendEvent, ToastEvent } from "../../services/event-manager";
|
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||||
import { clearMessage } from "../../services/message";
|
import { clearMessage } from "../../services/message";
|
||||||
import PremiumService from "../../services/premium";
|
import PremiumService from "../../services/premium";
|
||||||
import SettingsService from "../../services/settings";
|
import SettingsService from "../../services/settings";
|
||||||
@@ -48,7 +48,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
|||||||
(!password.current && step === LoginSteps.passwordAuth) ||
|
(!password.current && step === LoginSteps.passwordAuth) ||
|
||||||
(!email.current && step === LoginSteps.emailAuth)
|
(!email.current && step === LoginSteps.emailAuth)
|
||||||
) {
|
) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "All fields required",
|
heading: "All fields required",
|
||||||
message: "Fill all the fields and try again",
|
message: "Fill all the fields and try again",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -121,7 +121,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
|||||||
const finishWithError = async (e) => {
|
const finishWithError = async (e) => {
|
||||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Login failed",
|
heading: "Login failed",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -135,7 +135,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
|||||||
PremiumService.setPremiumStatus();
|
PremiumService.setPremiumStatus();
|
||||||
setUser(user);
|
setUser(user);
|
||||||
clearMessage();
|
clearMessage();
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Login successful",
|
heading: "Login successful",
|
||||||
message: `Logged in as ${user.email}`,
|
message: `Logged in as ${user.email}`,
|
||||||
type: "success",
|
type: "success",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
|
import { useRoute } from "@react-navigation/native";
|
||||||
import React, { useCallback, useEffect } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
import { Keyboard, View } from "react-native";
|
import { Keyboard, View } from "react-native";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -34,12 +35,13 @@ import { useSettingStore } from "../../stores/use-setting-store";
|
|||||||
import { getElevationStyle } from "../../utils/elevation";
|
import { getElevationStyle } from "../../utils/elevation";
|
||||||
import { SIZE, normalize } from "../../utils/size";
|
import { SIZE, normalize } from "../../utils/size";
|
||||||
import NativeTooltip from "../../utils/tooltip";
|
import NativeTooltip from "../../utils/tooltip";
|
||||||
import { PressableButton } from "../ui/pressable";
|
import { Pressable } from "../ui/pressable";
|
||||||
|
|
||||||
interface FloatingButton {
|
interface FloatingButtonProps {
|
||||||
title?: string;
|
title: string;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
shouldShow?: boolean;
|
||||||
alwaysVisible?: boolean;
|
alwaysVisible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +50,12 @@ const FloatingButton = ({
|
|||||||
onPress,
|
onPress,
|
||||||
color,
|
color,
|
||||||
alwaysVisible = false
|
alwaysVisible = false
|
||||||
}: FloatingButton) => {
|
}: FloatingButtonProps) => {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||||
const translate = useSharedValue(0);
|
const translate = useSharedValue(0);
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
@@ -115,11 +118,11 @@ const FloatingButton = ({
|
|||||||
animatedStyle
|
animatedStyle
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<PressableButton
|
<Pressable
|
||||||
testID={notesnook.buttons.add}
|
testID={notesnook.buttons.add}
|
||||||
type="accent"
|
type="accent"
|
||||||
accentColor={colors.static[color as keyof typeof colors.static]}
|
accentColor={color}
|
||||||
customStyle={{
|
style={{
|
||||||
...getElevationStyle(5),
|
...getElevationStyle(5),
|
||||||
borderRadius: 100
|
borderRadius: 100
|
||||||
}}
|
}}
|
||||||
@@ -139,12 +142,18 @@ const FloatingButton = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={title === "Clear all trash" ? "delete" : "plus"}
|
name={
|
||||||
|
route.name === "Notebooks"
|
||||||
|
? "notebook-plus"
|
||||||
|
: route.name === "Trash"
|
||||||
|
? "delete"
|
||||||
|
: "plus"
|
||||||
|
}
|
||||||
color={colors.primary.accentForeground}
|
color={colors.primary.accentForeground}
|
||||||
size={SIZE.xxl}
|
size={SIZE.xxl}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</PressableButton>
|
</Pressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,41 +17,49 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { PropsWithChildren } from "react";
|
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||||
import { KeyboardAvoidingView, Platform } from "react-native";
|
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
|
||||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||||
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
|
import useKeyboard from "../../hooks/use-keyboard";
|
||||||
import { useSettingStore } from "../../stores/use-setting-store";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
import { Header } from "../header";
|
|
||||||
import SelectionHeader from "../selection-header";
|
|
||||||
|
|
||||||
export const Container = ({ children }: PropsWithChildren) => {
|
export const Container = ({ children }: PropsWithChildren) => {
|
||||||
const floating = useIsFloatingKeyboard();
|
const { colors } = useThemeColors();
|
||||||
const introCompleted = useSettingStore(
|
|
||||||
(state) => state.settings.introCompleted
|
|
||||||
);
|
|
||||||
const insets = useGlobalSafeAreaInsets();
|
const insets = useGlobalSafeAreaInsets();
|
||||||
|
const keyboard = useKeyboard();
|
||||||
|
const [height, setHeight] = useState(0);
|
||||||
|
const windowHeightRef = useRef(Dimensions.get("window").height);
|
||||||
|
const { height: windowHeight } = useWindowDimensions();
|
||||||
|
const timerRef = useRef<NodeJS.Timeout>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (windowHeight !== windowHeightRef.current) {
|
||||||
|
setHeight(0);
|
||||||
|
windowHeightRef.current = windowHeight;
|
||||||
|
}
|
||||||
|
}, [windowHeight]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<View
|
||||||
behavior="padding"
|
|
||||||
enabled={Platform.OS === "ios" && !floating}
|
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||||
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
|
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom,
|
||||||
|
height: height || "100%",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: colors.primary.background
|
||||||
|
}}
|
||||||
|
onLayout={(event) => {
|
||||||
|
const height = event.nativeEvent.layout.height;
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
if (!keyboard.keyboardShown) {
|
||||||
|
setHeight(height);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!introCompleted ? null : (
|
|
||||||
<>
|
|
||||||
<SelectionHeader />
|
|
||||||
<Header />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</KeyboardAvoidingView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function DelayLayout({
|
|||||||
}: IDelayLayoutProps) {
|
}: IDelayLayoutProps) {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const loading = useDelayLayout(
|
const loading = useDelayLayout(
|
||||||
!props.delay || props.delay < 300 ? 300 : props.delay
|
!props.delay || props.delay < 300 ? 0 : props.delay
|
||||||
);
|
);
|
||||||
const Placeholder = placeholder[props.type || "default"];
|
const Placeholder = placeholder[props.type || "default"];
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { useNoteStore } from "../../stores/use-notes-store";
|
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
|
import React from "react";
|
||||||
import { AnnouncementDialog } from "../announcements";
|
import { AnnouncementDialog } from "../announcements";
|
||||||
import AuthModal from "../auth/auth-modal";
|
import AuthModal from "../auth/auth-modal";
|
||||||
import { SessionExpired } from "../auth/session-expired";
|
import { SessionExpired } from "../auth/session-expired";
|
||||||
import { Dialog } from "../dialog";
|
import { Dialog } from "../dialog";
|
||||||
import { AddTopicDialog } from "../dialogs/add-topic";
|
import JumpToSectionDialog from "../dialogs/jump-to-section";
|
||||||
import { LoadingDialog } from "../dialogs/loading";
|
import { LoadingDialog } from "../dialogs/loading";
|
||||||
|
import PDFPreview from "../dialogs/pdf-preview";
|
||||||
import ResultDialog from "../dialogs/result";
|
import ResultDialog from "../dialogs/result";
|
||||||
import { VaultDialog } from "../dialogs/vault";
|
import { VaultDialog } from "../dialogs/vault";
|
||||||
import ImagePreview from "../image-preview";
|
import ImagePreview from "../image-preview";
|
||||||
@@ -36,17 +36,16 @@ import SheetProvider from "../sheet-provider";
|
|||||||
import RateAppSheet from "../sheets/rate-app";
|
import RateAppSheet from "../sheets/rate-app";
|
||||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||||
import RestoreDataSheet from "../sheets/restore-data";
|
import RestoreDataSheet from "../sheets/restore-data";
|
||||||
import PDFPreview from "../dialogs/pdf-preview";
|
import { AppLockPassword } from "../dialogs/applock-password";
|
||||||
|
|
||||||
const DialogProvider = () => {
|
const DialogProvider = () => {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const loading = useNoteStore((state) => state.loading);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<AppLockPassword />
|
||||||
<LoadingDialog />
|
<LoadingDialog />
|
||||||
<Dialog context="global" />
|
<Dialog context="global" />
|
||||||
<AddTopicDialog colors={colors} />
|
|
||||||
<PremiumDialog colors={colors} />
|
<PremiumDialog colors={colors} />
|
||||||
<AuthModal colors={colors} />
|
<AuthModal colors={colors} />
|
||||||
<MergeConflicts />
|
<MergeConflicts />
|
||||||
@@ -58,10 +57,11 @@ const DialogProvider = () => {
|
|||||||
<VaultDialog colors={colors} />
|
<VaultDialog colors={colors} />
|
||||||
<RateAppSheet />
|
<RateAppSheet />
|
||||||
<ImagePreview />
|
<ImagePreview />
|
||||||
{loading ? null : <Expiring />}
|
<Expiring />
|
||||||
<AnnouncementDialog />
|
<AnnouncementDialog />
|
||||||
<SessionExpired />
|
<SessionExpired />
|
||||||
<PDFPreview />
|
<PDFPreview />
|
||||||
|
<JumpToSectionDialog />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,11 +21,9 @@ import { eSendEvent } from "../../services/event-manager";
|
|||||||
import {
|
import {
|
||||||
eCloseActionSheet,
|
eCloseActionSheet,
|
||||||
eCloseAddNotebookDialog,
|
eCloseAddNotebookDialog,
|
||||||
eCloseAddTopicDialog,
|
|
||||||
eCloseMoveNoteDialog,
|
eCloseMoveNoteDialog,
|
||||||
eOpenActionSheet,
|
eOpenActionSheet,
|
||||||
eOpenAddNotebookDialog,
|
eOpenAddNotebookDialog,
|
||||||
eOpenAddTopicDialog,
|
|
||||||
eOpenMoveNoteDialog
|
eOpenMoveNoteDialog
|
||||||
} from "../../utils/events";
|
} from "../../utils/events";
|
||||||
|
|
||||||
@@ -52,9 +50,3 @@ export const AddNotebookEvent = (notebook) => {
|
|||||||
export const HideAddNotebookEvent = (notebook) => {
|
export const HideAddNotebookEvent = (notebook) => {
|
||||||
eSendEvent(eCloseAddNotebookDialog, notebook);
|
eSendEvent(eCloseAddNotebookDialog, notebook);
|
||||||
};
|
};
|
||||||
export const AddTopicEvent = (topic) => {
|
|
||||||
eSendEvent(eOpenAddTopicDialog, topic);
|
|
||||||
};
|
|
||||||
export const HideAddTopicEvent = (notebook) => {
|
|
||||||
eSendEvent(eCloseAddTopicDialog, notebook);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
ColorValue,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Modal,
|
Modal,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -35,6 +36,25 @@ import SettingsService from "../../services/settings";
|
|||||||
import { useUserStore } from "../../stores/use-user-store";
|
import { useUserStore } from "../../stores/use-user-store";
|
||||||
import { useAppState } from "../../hooks/use-app-state";
|
import { useAppState } from "../../hooks/use-app-state";
|
||||||
|
|
||||||
|
export interface BaseDialogProps extends PropsWithChildren {
|
||||||
|
animation?: "fade" | "none" | "slide" | undefined;
|
||||||
|
visible: boolean;
|
||||||
|
onRequestClose?: () => void;
|
||||||
|
onShow?: () => void;
|
||||||
|
premium?: boolean;
|
||||||
|
statusBarTranslucent?: boolean;
|
||||||
|
transparent?: boolean;
|
||||||
|
centered?: boolean;
|
||||||
|
bottom?: boolean;
|
||||||
|
background?: string | ColorValue;
|
||||||
|
animated?: boolean;
|
||||||
|
bounce?: boolean;
|
||||||
|
closeOnTouch?: boolean;
|
||||||
|
useSafeArea?: boolean;
|
||||||
|
avoidKeyboardResize?: boolean;
|
||||||
|
enableSheetKeyboardHandler?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const BaseDialog = ({
|
const BaseDialog = ({
|
||||||
visible,
|
visible,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
@@ -46,14 +66,14 @@ const BaseDialog = ({
|
|||||||
transparent,
|
transparent,
|
||||||
centered = true,
|
centered = true,
|
||||||
bottom = false,
|
bottom = false,
|
||||||
background = null,
|
|
||||||
animated = true,
|
animated = true,
|
||||||
bounce = true,
|
bounce = true,
|
||||||
closeOnTouch = true,
|
closeOnTouch = true,
|
||||||
useSafeArea = true,
|
useSafeArea = true,
|
||||||
avoidKeyboardResize = false,
|
avoidKeyboardResize = false,
|
||||||
enableSheetKeyboardHandler = false
|
enableSheetKeyboardHandler,
|
||||||
}) => {
|
background
|
||||||
|
}: BaseDialogProps) => {
|
||||||
const floating = useIsFloatingKeyboard();
|
const floating = useIsFloatingKeyboard();
|
||||||
const appState = useAppState();
|
const appState = useAppState();
|
||||||
const lockEvents = useRef(false);
|
const lockEvents = useRef(false);
|
||||||
@@ -67,7 +87,8 @@ const BaseDialog = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useUserStore.getState().disableAppLockRequests) return;
|
if (useUserStore.getState().disableAppLockRequests) return;
|
||||||
if (SettingsService.get().appLockMode === "background") {
|
|
||||||
|
if (SettingsService.canLockAppInBackground()) {
|
||||||
if (appState === "background") {
|
if (appState === "background") {
|
||||||
setIntervalVisible(false);
|
setIntervalVisible(false);
|
||||||
if (useUserStore.getState().appLocked) {
|
if (useUserStore.getState().appLocked) {
|
||||||
@@ -148,7 +169,7 @@ const BaseDialog = ({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={closeOnTouch ? onRequestClose : null}
|
onPress={closeOnTouch ? onRequestClose : undefined}
|
||||||
style={styles.overlayButton}
|
style={styles.overlayButton}
|
||||||
/>
|
/>
|
||||||
{premium}
|
{premium}
|
||||||
@@ -25,6 +25,7 @@ import { useThemeColors } from "@notesnook/theme";
|
|||||||
import { SIZE } from "../../utils/size";
|
import { SIZE } from "../../utils/size";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import Paragraph from "../ui/typography/paragraph";
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
|
import { getColorLinearShade } from "../../utils/colors";
|
||||||
|
|
||||||
const DialogButtons = ({
|
const DialogButtons = ({
|
||||||
onPressPositive,
|
onPressPositive,
|
||||||
@@ -35,7 +36,7 @@ const DialogButtons = ({
|
|||||||
doneText,
|
doneText,
|
||||||
positiveType
|
positiveType
|
||||||
}) => {
|
}) => {
|
||||||
const { colors } = useThemeColors();
|
const { colors, isDark } = useThemeColors();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -44,9 +45,15 @@ const DialogButtons = ({
|
|||||||
{
|
{
|
||||||
backgroundColor: colors.secondary.background,
|
backgroundColor: colors.secondary.background,
|
||||||
height: 60,
|
height: 60,
|
||||||
borderBottomRightRadius: 10,
|
borderBottomRightRadius: 5,
|
||||||
borderBottomLeftRadius: 10,
|
borderBottomLeftRadius: 5,
|
||||||
paddingHorizontal: 12
|
paddingHorizontal: 12,
|
||||||
|
borderTopWidth: 0.7,
|
||||||
|
borderTopColor: getColorLinearShade(
|
||||||
|
colors.secondary.background,
|
||||||
|
0.05,
|
||||||
|
isDark
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -80,7 +87,7 @@ const DialogButtons = ({
|
|||||||
onPress={onPressNegative}
|
onPress={onPressNegative}
|
||||||
fontSize={SIZE.md}
|
fontSize={SIZE.md}
|
||||||
testID={notesnook.ids.default.dialog.no}
|
testID={notesnook.ids.default.dialog.no}
|
||||||
type="gray"
|
type="plain"
|
||||||
bold
|
bold
|
||||||
title={negativeTitle}
|
title={negativeTitle}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,12 +18,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native";
|
import { View, ViewProps } from "react-native";
|
||||||
import { DDS } from "../../services/device-detection";
|
import { DDS } from "../../services/device-detection";
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
import { getElevationStyle } from "../../utils/elevation";
|
import { getElevationStyle } from "../../utils/elevation";
|
||||||
|
import { getContainerBorder } from "../../utils/colors";
|
||||||
|
|
||||||
const DialogContainer = ({ width, height, ...restProps }) => {
|
const DialogContainer = ({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
style,
|
||||||
|
...restProps
|
||||||
|
}: ViewProps & {
|
||||||
|
width?: any;
|
||||||
|
height?: any;
|
||||||
|
}) => {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -35,7 +44,8 @@ const DialogContainer = ({ width, height, ...restProps }) => {
|
|||||||
maxHeight: height || 450,
|
maxHeight: height || 450,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
backgroundColor: colors.primary.background,
|
backgroundColor: colors.primary.background,
|
||||||
paddingTop: 12
|
paddingTop: 12,
|
||||||
|
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -22,7 +22,7 @@ import { Text, View, ViewStyle } from "react-native";
|
|||||||
import { useThemeColors } from "@notesnook/theme";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
import { SIZE } from "../../utils/size";
|
import { SIZE } from "../../utils/size";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { PressableButtonProps } from "../ui/pressable";
|
import { PressableProps } from "../ui/pressable";
|
||||||
import Heading from "../ui/typography/heading";
|
import Heading from "../ui/typography/heading";
|
||||||
import Paragraph from "../ui/typography/paragraph";
|
import Paragraph from "../ui/typography/paragraph";
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ type DialogHeaderProps = {
|
|||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
type?: PressableButtonProps["type"];
|
type?: PressableProps["type"];
|
||||||
icon?: string;
|
icon?: string;
|
||||||
};
|
};
|
||||||
paragraphColor?: string;
|
paragraphColor?: string;
|
||||||
@@ -86,7 +86,9 @@ const DialogHeader = ({
|
|||||||
>
|
>
|
||||||
{title}{" "}
|
{title}{" "}
|
||||||
{titlePart ? (
|
{titlePart ? (
|
||||||
<Text style={{ color: colors.primary.accent }}>{titlePart}</Text>
|
<Text style={{ color: colors.primary.accent }}>
|
||||||
|
{titlePart}
|
||||||
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
@@ -101,8 +103,8 @@ const DialogHeader = ({
|
|||||||
fontSize={13}
|
fontSize={13}
|
||||||
title={button.title}
|
title={button.title}
|
||||||
icon={button.icon}
|
icon={button.icon}
|
||||||
type={button.type || "grayBg"}
|
type={button.type || "secondary"}
|
||||||
height={25}
|
height={30}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ type DialogInfo = {
|
|||||||
paragraph?: string;
|
paragraph?: string;
|
||||||
positiveText?: string;
|
positiveText?: string;
|
||||||
negativeText?: string;
|
negativeText?: string;
|
||||||
positivePress?: (value: unknown) => void;
|
positivePress?: (value: any) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
positiveType?:
|
positiveType?:
|
||||||
| "transparent"
|
| "transparent"
|
||||||
| "gray"
|
| "plain"
|
||||||
| "grayBg"
|
| "secondary"
|
||||||
| "accent"
|
| "accent"
|
||||||
| "inverted"
|
| "inverted"
|
||||||
| "shade"
|
| "shade"
|
||||||
@@ -41,7 +41,9 @@ type DialogInfo = {
|
|||||||
input: boolean;
|
input: boolean;
|
||||||
inputPlaceholder: string;
|
inputPlaceholder: string;
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
context: "global" | "local";
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
context: "global" | "local" | (string & {});
|
||||||
|
secureTextEntry?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function presentDialog(data: Partial<DialogInfo>): void {
|
export function presentDialog(data: Partial<DialogInfo>): void {
|
||||||
|
|||||||
@@ -36,11 +36,15 @@ import DialogButtons from "./dialog-buttons";
|
|||||||
import DialogHeader from "./dialog-header";
|
import DialogHeader from "./dialog-header";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
import { getContainerBorder } from "../../utils/colors";
|
||||||
|
|
||||||
export const Dialog = ({ context = "global" }) => {
|
export const Dialog = ({ context = "global" }) => {
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [inputValue, setInputValue] = useState(null);
|
const [checked, setChecked] = useState(false);
|
||||||
|
const values = useRef({
|
||||||
|
inputValue: undefined
|
||||||
|
});
|
||||||
const inputRef = useRef();
|
const inputRef = useRef();
|
||||||
const [dialogInfo, setDialogInfo] = useState({
|
const [dialogInfo, setDialogInfo] = useState({
|
||||||
title: "",
|
title: "",
|
||||||
@@ -62,21 +66,12 @@ export const Dialog = ({ context = "global" }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
eSubscribeEvent(eOpenSimpleDialog, show);
|
|
||||||
eSubscribeEvent(eCloseSimpleDialog, hide);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
eUnSubscribeEvent(eOpenSimpleDialog, show);
|
|
||||||
eUnSubscribeEvent(eCloseSimpleDialog, hide);
|
|
||||||
};
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
const onPressPositive = async () => {
|
const onPressPositive = async () => {
|
||||||
if (dialogInfo.positivePress) {
|
if (dialogInfo.positivePress) {
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
let result = await dialogInfo.positivePress(
|
let result = await dialogInfo.positivePress(
|
||||||
inputValue || dialogInfo.defaultValue
|
values.current.inputValue || dialogInfo.defaultValue,
|
||||||
|
checked
|
||||||
);
|
);
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
return;
|
return;
|
||||||
@@ -91,14 +86,26 @@ export const Dialog = ({ context = "global" }) => {
|
|||||||
if (!data.context) data.context = "global";
|
if (!data.context) data.context = "global";
|
||||||
if (data.context !== context) return;
|
if (data.context !== context) return;
|
||||||
setDialogInfo(data);
|
setDialogInfo(data);
|
||||||
|
setChecked(false);
|
||||||
|
values.current.inputValue = data.defaultValue;
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
setInputValue(data.defaultValue);
|
|
||||||
},
|
},
|
||||||
[context]
|
[context]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
eSubscribeEvent(eOpenSimpleDialog, show);
|
||||||
|
eSubscribeEvent(eCloseSimpleDialog, hide);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eUnSubscribeEvent(eOpenSimpleDialog, show);
|
||||||
|
eUnSubscribeEvent(eCloseSimpleDialog, hide);
|
||||||
|
};
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
setInputValue(null);
|
setChecked(false);
|
||||||
|
values.current.inputValue = undefined;
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,7 +113,6 @@ export const Dialog = ({ context = "global" }) => {
|
|||||||
if (dialogInfo.onClose) {
|
if (dialogInfo.onClose) {
|
||||||
await dialogInfo.onClose();
|
await dialogInfo.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
hide();
|
hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,7 +122,8 @@ export const Dialog = ({ context = "global" }) => {
|
|||||||
maxHeight: 450,
|
maxHeight: 450,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
backgroundColor: colors.primary.background,
|
backgroundColor: colors.primary.background,
|
||||||
paddingTop: 12
|
paddingTop: 12,
|
||||||
|
...getContainerBorder(colors.primary.border, 0.5)
|
||||||
};
|
};
|
||||||
|
|
||||||
return visible ? (
|
return visible ? (
|
||||||
@@ -159,7 +166,7 @@ export const Dialog = ({ context = "global" }) => {
|
|||||||
fwdRef={inputRef}
|
fwdRef={inputRef}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
onChangeText={(value) => {
|
onChangeText={(value) => {
|
||||||
setInputValue(value);
|
values.current.inputValue = value;
|
||||||
}}
|
}}
|
||||||
testID="input-value"
|
testID="input-value"
|
||||||
secureTextEntry={dialogInfo.secureTextEntry}
|
secureTextEntry={dialogInfo.secureTextEntry}
|
||||||
@@ -176,10 +183,10 @@ export const Dialog = ({ context = "global" }) => {
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setInputValue(!inputValue);
|
setChecked(!checked);
|
||||||
}}
|
}}
|
||||||
icon={
|
icon={
|
||||||
inputValue
|
checked
|
||||||
? "check-circle-outline"
|
? "check-circle-outline"
|
||||||
: "checkbox-blank-circle-outline"
|
: "checkbox-blank-circle-outline"
|
||||||
}
|
}
|
||||||
@@ -187,9 +194,10 @@ export const Dialog = ({ context = "global" }) => {
|
|||||||
justifyContent: "flex-start"
|
justifyContent: "flex-start"
|
||||||
}}
|
}}
|
||||||
height={35}
|
height={35}
|
||||||
|
iconSize={20}
|
||||||
width="100%"
|
width="100%"
|
||||||
title={dialogInfo.check.info}
|
title={dialogInfo.check.info}
|
||||||
type={inputValue ? dialogInfo.check.type : "gray"}
|
type={checked ? dialogInfo.check.type || "selected" : "plain"}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,190 +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, { createRef } from "react";
|
|
||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
|
||||||
import {
|
|
||||||
eSendEvent,
|
|
||||||
eSubscribeEvent,
|
|
||||||
eUnSubscribeEvent,
|
|
||||||
ToastEvent
|
|
||||||
} from "../../../services/event-manager";
|
|
||||||
import Navigation from "../../../services/navigation";
|
|
||||||
import { db } from "../../../common/database";
|
|
||||||
import {
|
|
||||||
eCloseAddTopicDialog,
|
|
||||||
eOnTopicSheetUpdate,
|
|
||||||
eOpenAddTopicDialog
|
|
||||||
} from "../../../utils/events";
|
|
||||||
import { sleep } from "../../../utils/time";
|
|
||||||
import BaseDialog from "../../dialog/base-dialog";
|
|
||||||
import DialogButtons from "../../dialog/dialog-buttons";
|
|
||||||
import DialogContainer from "../../dialog/dialog-container";
|
|
||||||
import DialogHeader from "../../dialog/dialog-header";
|
|
||||||
import Input from "../../ui/input";
|
|
||||||
import Seperator from "../../ui/seperator";
|
|
||||||
import { Toast } from "../../toast";
|
|
||||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
|
||||||
|
|
||||||
export class AddTopicDialog extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
visible: false,
|
|
||||||
titleFocused: false,
|
|
||||||
loading: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ref = createRef();
|
|
||||||
this.title;
|
|
||||||
this.titleRef = createRef();
|
|
||||||
this.notebook = null;
|
|
||||||
this.toEdit = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
addNewTopic = async () => {
|
|
||||||
try {
|
|
||||||
if (!this.title || this.title?.trim() === "") {
|
|
||||||
ToastEvent.show({
|
|
||||||
heading: "Topic title is required",
|
|
||||||
type: "error",
|
|
||||||
context: "local"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.toEdit) {
|
|
||||||
await db.notebooks.notebook(this.notebook.id).topics.add(this.title);
|
|
||||||
} else {
|
|
||||||
let topic = this.toEdit;
|
|
||||||
topic.title = this.title;
|
|
||||||
|
|
||||||
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
|
|
||||||
}
|
|
||||||
this.close();
|
|
||||||
setTimeout(() => {
|
|
||||||
Navigation.queueRoutesForUpdate();
|
|
||||||
useMenuStore.getState().setMenuPins();
|
|
||||||
});
|
|
||||||
eSendEvent(eOnTopicSheetUpdate);
|
|
||||||
useRelationStore.getState().update();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
eSubscribeEvent(eOpenAddTopicDialog, this.open);
|
|
||||||
eSubscribeEvent(eCloseAddTopicDialog, this.close);
|
|
||||||
}
|
|
||||||
componentWillUnmount() {
|
|
||||||
eUnSubscribeEvent(eOpenAddTopicDialog, this.open);
|
|
||||||
eUnSubscribeEvent(eCloseAddTopicDialog, this.close);
|
|
||||||
}
|
|
||||||
|
|
||||||
open = async ({ notebookId, toEdit }) => {
|
|
||||||
let id = notebookId;
|
|
||||||
if (id) {
|
|
||||||
this.notebook = await db.notebooks.notebook(id).data;
|
|
||||||
}
|
|
||||||
this.toEdit = toEdit;
|
|
||||||
|
|
||||||
if (this.toEdit) {
|
|
||||||
this.title = this.toEdit.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
visible: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
close = () => {
|
|
||||||
this.title = null;
|
|
||||||
this.notebook = null;
|
|
||||||
this.toEdit = null;
|
|
||||||
this.setState({
|
|
||||||
visible: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { visible } = this.state;
|
|
||||||
if (!visible) return null;
|
|
||||||
return (
|
|
||||||
<BaseDialog
|
|
||||||
onShow={async () => {
|
|
||||||
if (this.toEdit) {
|
|
||||||
this.titleRef.current?.setNativeProps({
|
|
||||||
text: this.toEdit.title
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ref.current?.animateNextTransition();
|
|
||||||
await sleep(300);
|
|
||||||
this.titleRef.current?.focus();
|
|
||||||
}}
|
|
||||||
bounce={false}
|
|
||||||
statusBarTranslucent={false}
|
|
||||||
visible={true}
|
|
||||||
onRequestClose={this.close}
|
|
||||||
>
|
|
||||||
<DialogContainer>
|
|
||||||
<DialogHeader
|
|
||||||
icon="book-outline"
|
|
||||||
title={this.toEdit ? "Edit topic" : "New topic"}
|
|
||||||
paragraph={
|
|
||||||
this.toEdit
|
|
||||||
? "Edit title of the topic"
|
|
||||||
: "Add a new topic in " + this.notebook.title
|
|
||||||
}
|
|
||||||
padding={12}
|
|
||||||
/>
|
|
||||||
<Seperator half />
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
zIndex: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
fwdRef={this.titleRef}
|
|
||||||
testID="input-title"
|
|
||||||
onChangeText={(value) => {
|
|
||||||
this.title = value;
|
|
||||||
}}
|
|
||||||
blurOnSubmit={false}
|
|
||||||
placeholder="Enter title"
|
|
||||||
onSubmit={() => this.addNewTopic()}
|
|
||||||
returnKeyLabel="Done"
|
|
||||||
returnKeyType="done"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<DialogButtons
|
|
||||||
positiveTitle={this.toEdit ? "Save" : "Add"}
|
|
||||||
onPressNegative={() => this.close()}
|
|
||||||
onPressPositive={() => this.addNewTopic()}
|
|
||||||
/>
|
|
||||||
</DialogContainer>
|
|
||||||
<Toast context="local" />
|
|
||||||
</BaseDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
360
apps/mobile/app/components/dialogs/applock-password/index.tsx
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
/*
|
||||||
|
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, useRef, useState } from "react";
|
||||||
|
import { TextInput, View } from "react-native";
|
||||||
|
import {
|
||||||
|
clearAppLockVerificationCipher,
|
||||||
|
setAppLockVerificationCipher,
|
||||||
|
validateAppLockPassword
|
||||||
|
} from "../../../common/database/encryption";
|
||||||
|
import BiometricService from "../../../services/biometrics";
|
||||||
|
import { DDS } from "../../../services/device-detection";
|
||||||
|
import {
|
||||||
|
ToastManager,
|
||||||
|
eSendEvent,
|
||||||
|
eSubscribeEvent
|
||||||
|
} from "../../../services/event-manager";
|
||||||
|
import SettingsService from "../../../services/settings";
|
||||||
|
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||||
|
import { getElevationStyle } from "../../../utils/elevation";
|
||||||
|
import {
|
||||||
|
eCloseAppLocKPasswordDailog,
|
||||||
|
eOpenAppLockPasswordDialog
|
||||||
|
} from "../../../utils/events";
|
||||||
|
import { SIZE } from "../../../utils/size";
|
||||||
|
import { sleep } from "../../../utils/time";
|
||||||
|
import BaseDialog from "../../dialog/base-dialog";
|
||||||
|
import DialogButtons from "../../dialog/dialog-buttons";
|
||||||
|
import DialogHeader from "../../dialog/dialog-header";
|
||||||
|
import { Toast } from "../../toast";
|
||||||
|
import { IconButton } from "../../ui/icon-button";
|
||||||
|
import Input from "../../ui/input";
|
||||||
|
import Seperator from "../../ui/seperator";
|
||||||
|
|
||||||
|
export const AppLockPassword = () => {
|
||||||
|
const { colors } = useThemeColors();
|
||||||
|
const [mode, setMode] = useState<"create" | "change" | "remove">("create");
|
||||||
|
const [keyboardType, setKeyboardType] = useState<"pin" | "password">(
|
||||||
|
useSettingStore.getState().settings.applockKeyboardType === "default"
|
||||||
|
? "password"
|
||||||
|
: "pin"
|
||||||
|
);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const currentPasswordInputRef = useRef<TextInput>(null);
|
||||||
|
const passwordInputRef = useRef<TextInput>(null);
|
||||||
|
const confirmPasswordInputRef = useRef<TextInput>(null);
|
||||||
|
const values = useRef<{
|
||||||
|
currentPassword?: string;
|
||||||
|
password?: string;
|
||||||
|
confirmPassword?: string;
|
||||||
|
}>({});
|
||||||
|
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subs = [
|
||||||
|
eSubscribeEvent(
|
||||||
|
eOpenAppLockPasswordDialog,
|
||||||
|
(mode: "create" | "change" | "remove") => {
|
||||||
|
setMode(mode);
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
eSubscribeEvent(eCloseAppLocKPasswordDailog, () => {
|
||||||
|
values.current = {};
|
||||||
|
setVisible(false);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
return () => {
|
||||||
|
subs.forEach((sub) => sub?.unsubscribe());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
values.current = {};
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return !visible ? null : (
|
||||||
|
<BaseDialog
|
||||||
|
onShow={async () => {
|
||||||
|
await sleep(100);
|
||||||
|
if (mode !== "change") {
|
||||||
|
passwordInputRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
currentPasswordInputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
statusBarTranslucent={false}
|
||||||
|
onRequestClose={close}
|
||||||
|
visible={visible}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
...getElevationStyle(10),
|
||||||
|
width: DDS.isTab ? 350 : "85%",
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: colors.primary.background,
|
||||||
|
paddingTop: 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader
|
||||||
|
title={
|
||||||
|
mode === "change"
|
||||||
|
? `Change app lock ${keyboardType}`
|
||||||
|
: mode === "remove"
|
||||||
|
? `Remove app lock ${keyboardType}`
|
||||||
|
: `Set up app lock ${keyboardType}`
|
||||||
|
}
|
||||||
|
paragraph={
|
||||||
|
mode === "change"
|
||||||
|
? `Change app lock ${keyboardType}`
|
||||||
|
: mode === "remove"
|
||||||
|
? `Remove app lock ${keyboardType}`
|
||||||
|
: `Set up a custom app lock ${keyboardType} to unlock the app`
|
||||||
|
}
|
||||||
|
icon="shield"
|
||||||
|
padding={12}
|
||||||
|
/>
|
||||||
|
<Seperator half />
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mode === "change" ? (
|
||||||
|
<Input
|
||||||
|
fwdRef={currentPasswordInputRef}
|
||||||
|
autoCapitalize="none"
|
||||||
|
onChangeText={(value) => {
|
||||||
|
values.current.currentPassword = value;
|
||||||
|
}}
|
||||||
|
onSubmit={() => {
|
||||||
|
passwordInputRef.current?.focus();
|
||||||
|
}}
|
||||||
|
defaultValue={values.current.currentPassword}
|
||||||
|
autoComplete="password"
|
||||||
|
returnKeyLabel="Next"
|
||||||
|
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||||
|
returnKeyType="next"
|
||||||
|
secureTextEntry={secureTextEntry}
|
||||||
|
placeholder={`Current ${keyboardType}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
fwdRef={passwordInputRef}
|
||||||
|
autoCapitalize="none"
|
||||||
|
onChangeText={(value) => {
|
||||||
|
values.current.password = value;
|
||||||
|
}}
|
||||||
|
onSubmit={() => {
|
||||||
|
confirmPasswordInputRef.current?.focus();
|
||||||
|
}}
|
||||||
|
defaultValue={values.current.password}
|
||||||
|
keyboardType={keyboardType === "pin" ? "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}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
mode === "change"
|
||||||
|
? `New ${keyboardType}`
|
||||||
|
: `${keyboardType === "pin" ? "Pin" : "Password"}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{mode !== "remove" ? (
|
||||||
|
<Input
|
||||||
|
fwdRef={confirmPasswordInputRef}
|
||||||
|
autoCapitalize="none"
|
||||||
|
onChangeText={(value) => {
|
||||||
|
values.current.confirmPassword = value;
|
||||||
|
}}
|
||||||
|
onSubmit={() => {
|
||||||
|
confirmPasswordInputRef.current?.focus();
|
||||||
|
}}
|
||||||
|
defaultValue={values.current.confirmPassword}
|
||||||
|
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||||
|
customValidator={() => values.current.password || ""}
|
||||||
|
validationType="confirmPassword"
|
||||||
|
autoComplete="password"
|
||||||
|
returnKeyLabel="Done"
|
||||||
|
returnKeyType="done"
|
||||||
|
secureTextEntry={secureTextEntry}
|
||||||
|
placeholder={`Confirm ${keyboardType}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<DialogButtons
|
||||||
|
onPressNegative={close}
|
||||||
|
onPressPositive={async () => {
|
||||||
|
if (mode === "create") {
|
||||||
|
if (!values.current.password || !values.current.confirmPassword) {
|
||||||
|
ToastManager.error(
|
||||||
|
new Error("All inputs are required"),
|
||||||
|
undefined,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.current.password !== values.current.confirmPassword) {
|
||||||
|
ToastManager.error(
|
||||||
|
new Error(
|
||||||
|
`${
|
||||||
|
keyboardType === "pin" ? "Pin" : "Password"
|
||||||
|
} does not match`
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await setAppLockVerificationCipher(values.current.password);
|
||||||
|
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||||
|
} else if (mode === "change") {
|
||||||
|
if (
|
||||||
|
!values.current.currentPassword ||
|
||||||
|
!values.current.password ||
|
||||||
|
!values.current.confirmPassword
|
||||||
|
) {
|
||||||
|
ToastManager.error(
|
||||||
|
new Error("All inputs are required"),
|
||||||
|
undefined,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.current.password !== values.current.confirmPassword) {
|
||||||
|
ToastManager.error(
|
||||||
|
new Error(
|
||||||
|
`${
|
||||||
|
keyboardType === "pin" ? "Pin" : "Password"
|
||||||
|
} does not match`
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||||
|
values.current.currentPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isCurrentPasswordCorrect) {
|
||||||
|
ToastManager.error(
|
||||||
|
new Error(
|
||||||
|
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||||
|
await setAppLockVerificationCipher(values.current.password);
|
||||||
|
} else if (mode === "remove") {
|
||||||
|
if (!values.current.password) {
|
||||||
|
ToastManager.error(
|
||||||
|
new Error("All inputs are required"),
|
||||||
|
undefined,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||||
|
values.current.password
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isCurrentPasswordCorrect) {
|
||||||
|
ToastManager.error(
|
||||||
|
new Error(
|
||||||
|
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearAppLockVerificationCipher();
|
||||||
|
SettingsService.setProperty("appLockHasPasswordSecurity", false);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await BiometricService.isBiometryAvailable()) ||
|
||||||
|
SettingsService.getProperty("biometricsAuthEnabled") === false
|
||||||
|
) {
|
||||||
|
SettingsService.setProperty("appLockEnabled", false);
|
||||||
|
SettingsService.setPrivacyScreen(SettingsService.get());
|
||||||
|
ToastManager.show({
|
||||||
|
message: "App lock disabled",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsService.setProperty(
|
||||||
|
"applockKeyboardType",
|
||||||
|
keyboardType === "password" ? "default" : "numeric"
|
||||||
|
);
|
||||||
|
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
positiveTitle="Save"
|
||||||
|
negativeTitle="Cancel"
|
||||||
|
positiveType="transparent"
|
||||||
|
loading={false}
|
||||||
|
doneText=""
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Toast context="local" />
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AppLockPassword.present = (mode: "create" | "change" | "remove") => {
|
||||||
|
eSendEvent(eOpenAppLockPasswordDialog, mode);
|
||||||
|
};
|
||||||
181
apps/mobile/app/components/dialogs/attach-image-dialog/index.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
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 { Image, ScrollView, TouchableOpacity, View } from "react-native";
|
||||||
|
import { ImagePickerResponse } from "react-native-image-picker";
|
||||||
|
import { useThemeColors } from "../../../../../../packages/theme/dist";
|
||||||
|
import { presentSheet } from "../../../services/event-manager";
|
||||||
|
import { SIZE } from "../../../utils/size";
|
||||||
|
import { Button } from "../../ui/button";
|
||||||
|
import { IconButton } from "../../ui/icon-button";
|
||||||
|
import { Notice } from "../../ui/notice";
|
||||||
|
import Paragraph from "../../ui/typography/paragraph";
|
||||||
|
|
||||||
|
export default function AttachImage({
|
||||||
|
response,
|
||||||
|
onAttach,
|
||||||
|
close
|
||||||
|
}: {
|
||||||
|
response: ImagePickerResponse;
|
||||||
|
onAttach: ({ compress }: { compress: boolean }) => void;
|
||||||
|
close: ((ctx?: string | undefined) => void) | undefined;
|
||||||
|
}) {
|
||||||
|
const { colors } = useThemeColors();
|
||||||
|
const [compress, setCompress] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.secondary.background,
|
||||||
|
marginBottom: 12,
|
||||||
|
height: 140,
|
||||||
|
width: "100%",
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 5,
|
||||||
|
paddingHorizontal: 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
|
||||||
|
Attaching {response.assets?.length} image(s):
|
||||||
|
</Paragraph>
|
||||||
|
<ScrollView horizontal>
|
||||||
|
{response.assets?.map((item) => (
|
||||||
|
<TouchableOpacity key={item.fileName} activeOpacity={0.9}>
|
||||||
|
<Image
|
||||||
|
source={{
|
||||||
|
uri: item.uri
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: "black",
|
||||||
|
marginRight: 6
|
||||||
|
}}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
activeOpacity={1}
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignSelf: "center",
|
||||||
|
marginBottom: 12,
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
setCompress(!compress);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size={SIZE.lg}
|
||||||
|
name={compress ? "checkbox-marked" : "checkbox-blank-outline"}
|
||||||
|
color={compress ? colors.primary.accent : colors.primary.icon}
|
||||||
|
style={{
|
||||||
|
width: 25,
|
||||||
|
height: 25
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
setCompress(!compress);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
flexShrink: 1,
|
||||||
|
marginLeft: 3
|
||||||
|
}}
|
||||||
|
size={SIZE.sm}
|
||||||
|
>
|
||||||
|
Compress (recommended)
|
||||||
|
</Paragraph>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{!compress ? (
|
||||||
|
<Notice
|
||||||
|
type="alert"
|
||||||
|
text="Images uploaded without compression are slow to load and take more bandwidth. We recommend compressing images unless you need image in original quality."
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
marginBottom: 12
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Notice
|
||||||
|
type="information"
|
||||||
|
text="Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases."
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
marginBottom: 12
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
title={`${
|
||||||
|
(response.assets?.length || 0) > 1 ? "Attach Images" : "Attach Image"
|
||||||
|
}`}
|
||||||
|
type="accent"
|
||||||
|
width="100%"
|
||||||
|
onPress={() => {
|
||||||
|
onAttach({
|
||||||
|
compress
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AttachImage.present = (response: ImagePickerResponse) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let resolved = false;
|
||||||
|
presentSheet({
|
||||||
|
component: (ref, close, update) => (
|
||||||
|
<AttachImage
|
||||||
|
response={response}
|
||||||
|
close={close}
|
||||||
|
onAttach={(result) => {
|
||||||
|
resolved = true;
|
||||||
|
console.log("closing");
|
||||||
|
resolve(result);
|
||||||
|
close?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
onClose: () => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
199
apps/mobile/app/components/dialogs/color-picker/index.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
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 { Color } from "@notesnook/core";
|
||||||
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { TextInput, View } from "react-native";
|
||||||
|
import ColorPickerWheel from "react-native-wheel-color-picker";
|
||||||
|
import { db } from "../../../common/database";
|
||||||
|
import { useStoredRef } from "../../../hooks/use-stored-ref";
|
||||||
|
import { ToastManager } from "../../../services/event-manager";
|
||||||
|
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||||
|
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||||
|
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||||
|
import BaseDialog from "../../dialog/base-dialog";
|
||||||
|
import DialogContainer from "../../dialog/dialog-container";
|
||||||
|
import { Toast } from "../../toast";
|
||||||
|
import { Button } from "../../ui/button";
|
||||||
|
import Input from "../../ui/input";
|
||||||
|
import { Pressable } from "../../ui/pressable";
|
||||||
|
|
||||||
|
const HEX_COLOR_REGEX_ALPHA =
|
||||||
|
/^#(?:(?:[\da-fA-F]{3}){1,2}|(?:[\da-fA-F]{4}){1,2})$/;
|
||||||
|
|
||||||
|
const ColorPicker = ({
|
||||||
|
visible,
|
||||||
|
setVisible,
|
||||||
|
onColorAdded
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
setVisible: (value: boolean) => void;
|
||||||
|
onColorAdded: (color: Color) => void;
|
||||||
|
}) => {
|
||||||
|
const [selectedColor, setSelectedColor] = useState<string>();
|
||||||
|
const { colors } = useThemeColors();
|
||||||
|
const inputRef = useRef<TextInput>(null);
|
||||||
|
const title = useRef<string>();
|
||||||
|
const colorRef = useStoredRef("color-ref", "#f0f0f0");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
visible={visible}
|
||||||
|
onRequestClose={() => {
|
||||||
|
title.current = undefined;
|
||||||
|
setVisible(false);
|
||||||
|
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||||
|
}}
|
||||||
|
statusBarTranslucent={false}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Toast context="color-picker" />
|
||||||
|
<DialogContainer
|
||||||
|
style={{
|
||||||
|
paddingTop: 0,
|
||||||
|
maxHeight: 600
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: 250,
|
||||||
|
backgroundColor: colors.primary.background,
|
||||||
|
borderRadius: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorPickerWheel
|
||||||
|
onColorChangeComplete={(color) => {
|
||||||
|
if (color === selectedColor) return;
|
||||||
|
colorRef.current = color;
|
||||||
|
setSelectedColor(color);
|
||||||
|
inputRef.current?.setNativeProps({
|
||||||
|
placeholder: color,
|
||||||
|
text: color
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
useNativeLayout={true}
|
||||||
|
autoResetSlider={true}
|
||||||
|
color={colorRef.current}
|
||||||
|
sliderSize={30}
|
||||||
|
thumbSize={25}
|
||||||
|
shadeWheelThumb
|
||||||
|
shadeSliderThumb={true}
|
||||||
|
swatchesOnly={false}
|
||||||
|
swatches={false}
|
||||||
|
useNativeDriver={true}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingTop: 10,
|
||||||
|
columnGap: 10,
|
||||||
|
marginBottom: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
fwdRef={inputRef}
|
||||||
|
placeholder="#f0f0f0"
|
||||||
|
marginBottom={0}
|
||||||
|
onChangeText={(value) => {
|
||||||
|
if (HEX_COLOR_REGEX_ALPHA.test(value)) {
|
||||||
|
colorRef.current = value;
|
||||||
|
setSelectedColor(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
defaultValue={colorRef.current}
|
||||||
|
/>
|
||||||
|
<Pressable
|
||||||
|
type="accent"
|
||||||
|
accentColor={selectedColor || colors.secondary.background}
|
||||||
|
style={{
|
||||||
|
width: 45,
|
||||||
|
height: 45,
|
||||||
|
borderRadius: 100,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
marginBottom={10}
|
||||||
|
onChangeText={(value) => {
|
||||||
|
title.current = value;
|
||||||
|
}}
|
||||||
|
testID="color-title-input"
|
||||||
|
defaultValue={title.current}
|
||||||
|
placeholder={title.current || "Color title"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
title="Add color"
|
||||||
|
style={{
|
||||||
|
marginBottom: 10
|
||||||
|
}}
|
||||||
|
onPress={async () => {
|
||||||
|
if (!selectedColor)
|
||||||
|
return ToastManager.error(
|
||||||
|
new Error("Select a color"),
|
||||||
|
undefined,
|
||||||
|
"color-picker"
|
||||||
|
);
|
||||||
|
if (!title.current)
|
||||||
|
return ToastManager.error(
|
||||||
|
new Error("Enter a title for the color")
|
||||||
|
);
|
||||||
|
const exists = await db.colors.all.find((v) =>
|
||||||
|
v.and([v(`colorCode`, "==", selectedColor)])
|
||||||
|
);
|
||||||
|
if (exists)
|
||||||
|
return ToastManager.error(
|
||||||
|
new Error(`Color #${selectedColor} already exists`)
|
||||||
|
);
|
||||||
|
const id = await db.colors.add({
|
||||||
|
title: title.current,
|
||||||
|
colorCode: selectedColor
|
||||||
|
});
|
||||||
|
useRelationStore.getState().update();
|
||||||
|
useMenuStore.getState().setColorNotes();
|
||||||
|
setVisible(false);
|
||||||
|
const color = await db.colors.color(id);
|
||||||
|
if (color) {
|
||||||
|
onColorAdded?.(color);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type={selectedColor ? "secondaryAccented" : "secondary"}
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</DialogContainer>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorPicker;
|
||||||
@@ -17,15 +17,22 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import { GroupHeader, Item, VirtualizedGrouping } from "@notesnook/core";
|
||||||
import { ScrollView, View } from "react-native";
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
|
import React, {
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from "react";
|
||||||
|
import { ActivityIndicator, FlatList, ScrollView, View } from "react-native";
|
||||||
import { DDS } from "../../../services/device-detection";
|
import { DDS } from "../../../services/device-detection";
|
||||||
import {
|
import {
|
||||||
eSubscribeEvent,
|
eSubscribeEvent,
|
||||||
eUnSubscribeEvent
|
eUnSubscribeEvent
|
||||||
} from "../../../services/event-manager";
|
} from "../../../services/event-manager";
|
||||||
import { useMessageStore } from "../../../stores/use-message-store";
|
import { useMessageStore } from "../../../stores/use-message-store";
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
|
||||||
import { getElevationStyle } from "../../../utils/elevation";
|
import { getElevationStyle } from "../../../utils/elevation";
|
||||||
import {
|
import {
|
||||||
eCloseJumpToDialog,
|
eCloseJumpToDialog,
|
||||||
@@ -34,29 +41,51 @@ import {
|
|||||||
} from "../../../utils/events";
|
} from "../../../utils/events";
|
||||||
import { SIZE } from "../../../utils/size";
|
import { SIZE } from "../../../utils/size";
|
||||||
import BaseDialog from "../../dialog/base-dialog";
|
import BaseDialog from "../../dialog/base-dialog";
|
||||||
import { PressableButton } from "../../ui/pressable";
|
import { Pressable } from "../../ui/pressable";
|
||||||
import Paragraph from "../../ui/typography/paragraph";
|
import Paragraph from "../../ui/typography/paragraph";
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
const offsets = [];
|
const JumpToSectionDialog = () => {
|
||||||
let timeout = null;
|
const scrollRef = useRef<RefObject<FlatList>>();
|
||||||
const JumpToSectionDialog = ({ scrollRef, data, type }) => {
|
const [data, setData] = useState<VirtualizedGrouping<Item>>();
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const notes = data;
|
const notes = data;
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const currentScrollPosition = useRef(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const onPress = (item) => {
|
const [groups, setGroups] = useState<
|
||||||
let ind = notes.findIndex(
|
{
|
||||||
(i) => i.title === item.title && i.type === "header"
|
index: number;
|
||||||
);
|
group: GroupHeader;
|
||||||
scrollRef.current?.scrollToIndex({
|
}[]
|
||||||
index: ind,
|
>();
|
||||||
|
const offsets = useRef<number[]>([]);
|
||||||
|
|
||||||
|
const onPress = (item: { index: number; group: GroupHeader }) => {
|
||||||
|
scrollRef.current?.current?.scrollToIndex({
|
||||||
|
index: item.index,
|
||||||
animated: true
|
animated: true
|
||||||
});
|
});
|
||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const open = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
ref
|
||||||
|
}: {
|
||||||
|
data: VirtualizedGrouping<Item>;
|
||||||
|
ref: RefObject<FlatList>;
|
||||||
|
}) => {
|
||||||
|
setLoading(true);
|
||||||
|
setData(data);
|
||||||
|
scrollRef.current = ref;
|
||||||
|
setVisible(true);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
eSubscribeEvent(eOpenJumpToDialog, open);
|
eSubscribeEvent(eOpenJumpToDialog, open);
|
||||||
eSubscribeEvent(eCloseJumpToDialog, close);
|
eSubscribeEvent(eCloseJumpToDialog, close);
|
||||||
@@ -69,54 +98,46 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
|
|||||||
};
|
};
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const onScroll = (data) => {
|
const onScroll = (data: { x: number; y: number }) => {
|
||||||
let y = data.y;
|
currentScrollPosition.current = data.y;
|
||||||
if (timeout) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = null;
|
|
||||||
}
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
let index = offsets.findIndex((o, i) => o <= y && offsets[i + 1] > y);
|
|
||||||
setCurrentIndex(index || 0);
|
|
||||||
}, 200);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = useCallback(
|
|
||||||
(_type) => {
|
|
||||||
if (_type !== type) return;
|
|
||||||
setVisible(true);
|
|
||||||
},
|
|
||||||
[type]
|
|
||||||
);
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const loadGroupsAndOffsets = useCallback(() => {
|
||||||
loadOffsets();
|
notes?.groups?.().then((groups) => {
|
||||||
}, [loadOffsets, notes]);
|
setGroups(groups);
|
||||||
|
offsets.current = [];
|
||||||
const loadOffsets = useCallback(() => {
|
groups.map((item, index) => {
|
||||||
notes
|
|
||||||
.filter((i) => i.type === "header")
|
|
||||||
.map((item, index) => {
|
|
||||||
let offset = 35 * index;
|
let offset = 35 * index;
|
||||||
let ind = notes.findIndex(
|
let groupIndex = item.index;
|
||||||
(i) => i.title === item.title && i.type === "header"
|
const messageState = useMessageStore.getState().message;
|
||||||
);
|
const msgOffset = messageState?.visible ? 60 : 10;
|
||||||
let messageState = useMessageStore.getState().message;
|
|
||||||
let msgOffset = messageState?.visible ? 60 : 10;
|
groupIndex = groupIndex + 1;
|
||||||
ind = ind + 1;
|
groupIndex = groupIndex - (index + 1);
|
||||||
ind = ind - (index + 1);
|
offset = offset + groupIndex * 100 + msgOffset;
|
||||||
offset = offset + ind * 100 + msgOffset;
|
offsets.current.push(offset);
|
||||||
offsets.push(offset);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const index = offsets.current?.findIndex((o, i) => {
|
||||||
|
return (
|
||||||
|
o <= currentScrollPosition.current + 100 &&
|
||||||
|
offsets.current[i + 1] - 100 > currentScrollPosition.current
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
setCurrentIndex(index < 0 ? 0 : index);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
}, [notes]);
|
}, [notes]);
|
||||||
|
|
||||||
return !visible ? null : (
|
return !visible ? null : (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
onShow={() => {
|
onShow={() => {
|
||||||
loadOffsets();
|
loadGroupsAndOffsets();
|
||||||
}}
|
}}
|
||||||
onRequestClose={close}
|
onRequestClose={close}
|
||||||
visible={true}
|
visible={true}
|
||||||
@@ -135,35 +156,42 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
|
|||||||
paddingTop: 30
|
paddingTop: 30
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollView
|
{loading ? (
|
||||||
style={{
|
<ActivityIndicator
|
||||||
maxHeight: "100%"
|
size={SIZE.lg}
|
||||||
}}
|
color={colors.primary.accent}
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
marginBottom: 20
|
||||||
flexWrap: "wrap",
|
}}
|
||||||
alignSelf: "center",
|
/>
|
||||||
justifyContent: "center",
|
) : (
|
||||||
paddingBottom: 20
|
<ScrollView
|
||||||
|
style={{
|
||||||
|
maxHeight: "100%"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{notes
|
<View
|
||||||
.filter((i) => i.type === "header")
|
style={{
|
||||||
.map((item, index) => {
|
flexDirection: "row",
|
||||||
return item.title ? (
|
flexWrap: "wrap",
|
||||||
<PressableButton
|
alignSelf: "center",
|
||||||
key={item.title}
|
justifyContent: "center",
|
||||||
onPress={() => onPress(item, index)}
|
paddingBottom: 20
|
||||||
type={currentIndex === index ? "selected" : "transparent"}
|
}}
|
||||||
customStyle={{
|
>
|
||||||
|
{groups?.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={item.group.id}
|
||||||
|
onPress={() => onPress(item)}
|
||||||
|
type={currentIndex === index ? "accent" : "transparent"}
|
||||||
|
style={{
|
||||||
minWidth: "20%",
|
minWidth: "20%",
|
||||||
width: null,
|
width: null,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
margin: 5,
|
margin: 5,
|
||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
height: 25,
|
height: 30,
|
||||||
marginVertical: 10
|
marginVertical: 10
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -171,20 +199,21 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
|
|||||||
size={SIZE.sm}
|
size={SIZE.sm}
|
||||||
color={
|
color={
|
||||||
currentIndex === index
|
currentIndex === index
|
||||||
? colors.selected.accent
|
? colors.static.white
|
||||||
: colors.primary.accent
|
: colors.primary.paragraph
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
textAlign: "center"
|
textAlign: "center"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.group.title}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</PressableButton>
|
</Pressable>
|
||||||
) : null;
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
@@ -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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Dimensions, TextInput, View } from "react-native";
|
import { Dimensions, TextInput, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
addOrientationListener,
|
addOrientationListener,
|
||||||
@@ -85,7 +85,7 @@ const PDFPreview = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
eUnSubscribeEvent("PDFPreview", open);
|
eUnSubscribeEvent("PDFPreview", open);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [open]);
|
||||||
|
|
||||||
const onOrientationChange = (o) => {
|
const onOrientationChange = (o) => {
|
||||||
if (o.includes("LANDSCAPE")) {
|
if (o.includes("LANDSCAPE")) {
|
||||||
@@ -102,23 +102,26 @@ const PDFPreview = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const open = async (attachment) => {
|
const open = useCallback(
|
||||||
setVisible(true);
|
async (attachment) => {
|
||||||
setLoading(true);
|
setVisible(true);
|
||||||
setTimeout(async () => {
|
setLoading(true);
|
||||||
setAttachment(attachment);
|
setTimeout(async () => {
|
||||||
let hash = attachment.metadata.hash;
|
setAttachment(attachment);
|
||||||
if (!hash) return;
|
let hash = attachment.hash;
|
||||||
const uri = await downloadAttachment(hash, false, {
|
if (!hash) return;
|
||||||
silent: true,
|
const uri = await downloadAttachment(hash, false, {
|
||||||
cache: true
|
silent: true,
|
||||||
});
|
cache: true
|
||||||
const path = `${cacheDir}/${uri}`;
|
});
|
||||||
snapshotValue.current = snapshot.current;
|
const path = `${cacheDir}/${uri}`;
|
||||||
setPDFSource("file://" + path);
|
snapshotValue.current = snapshot.current;
|
||||||
setLoading(false);
|
setPDFSource("file://" + path);
|
||||||
}, 100);
|
setLoading(false);
|
||||||
};
|
}, 100);
|
||||||
|
},
|
||||||
|
[snapshot]
|
||||||
|
);
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
setPDFSource(null);
|
setPDFSource(null);
|
||||||
@@ -130,7 +133,7 @@ const PDFPreview = () => {
|
|||||||
if (error?.message === "Password required or incorrect password.") {
|
if (error?.message === "Password required or incorrect password.") {
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
presentDialog({
|
presentDialog({
|
||||||
context: attachment?.metadata?.hash,
|
context: attachment?.hash,
|
||||||
input: true,
|
input: true,
|
||||||
inputPlaceholder: "Enter password",
|
inputPlaceholder: "Enter password",
|
||||||
positiveText: "Unlock",
|
positiveText: "Unlock",
|
||||||
@@ -151,8 +154,8 @@ const PDFPreview = () => {
|
|||||||
return (
|
return (
|
||||||
visible && (
|
visible && (
|
||||||
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
|
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
|
||||||
<SheetProvider context={attachment?.metadata?.hash} />
|
<SheetProvider context={attachment?.hash} />
|
||||||
<Dialog context={attachment?.metadata?.hash} />
|
<Dialog context={attachment?.hash} />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -208,7 +211,7 @@ const PDFPreview = () => {
|
|||||||
color={colors.static.white}
|
color={colors.static.white}
|
||||||
name="arrow-left"
|
name="arrow-left"
|
||||||
onPress={close}
|
onPress={close}
|
||||||
customStyle={{
|
style={{
|
||||||
marginRight: 12
|
marginRight: 12
|
||||||
}}
|
}}
|
||||||
size={SIZE.xxl}
|
size={SIZE.xxl}
|
||||||
@@ -261,7 +264,7 @@ const PDFPreview = () => {
|
|||||||
color={colors.static.white}
|
color={colors.static.white}
|
||||||
name="download"
|
name="download"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
downloadAttachment(attachment.metadata.hash, false);
|
downloadAttachment(attachment.hash, false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -23,28 +23,26 @@ import { InteractionManager, View } from "react-native";
|
|||||||
import Share from "react-native-share";
|
import Share from "react-native-share";
|
||||||
import { notesnook } from "../../../../e2e/test.ids";
|
import { notesnook } from "../../../../e2e/test.ids";
|
||||||
import { db } from "../../../common/database";
|
import { db } from "../../../common/database";
|
||||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
|
||||||
import BiometricService from "../../../services/biometrics";
|
import BiometricService from "../../../services/biometrics";
|
||||||
import { DDS } from "../../../services/device-detection";
|
import { DDS } from "../../../services/device-detection";
|
||||||
import {
|
import {
|
||||||
|
ToastManager,
|
||||||
eSendEvent,
|
eSendEvent,
|
||||||
eSubscribeEvent,
|
eSubscribeEvent,
|
||||||
eUnSubscribeEvent,
|
eUnSubscribeEvent
|
||||||
ToastEvent
|
|
||||||
} from "../../../services/event-manager";
|
} from "../../../services/event-manager";
|
||||||
import Navigation from "../../../services/navigation";
|
import Navigation from "../../../services/navigation";
|
||||||
import SearchService from "../../../services/search";
|
|
||||||
import { convertNoteToText } from "../../../utils/note-to-text";
|
|
||||||
import { getElevationStyle } from "../../../utils/elevation";
|
import { getElevationStyle } from "../../../utils/elevation";
|
||||||
import {
|
import {
|
||||||
eClearEditor,
|
|
||||||
eCloseActionSheet,
|
eCloseActionSheet,
|
||||||
eCloseVaultDialog,
|
eCloseVaultDialog,
|
||||||
eOnLoadNote,
|
eOnLoadNote,
|
||||||
eOpenVaultDialog
|
eOpenVaultDialog,
|
||||||
|
eUpdateNoteInEditor
|
||||||
} from "../../../utils/events";
|
} from "../../../utils/events";
|
||||||
import { deleteItems } from "../../../utils/functions";
|
import { deleteItems } from "../../../utils/functions";
|
||||||
import { tabBarRef } from "../../../utils/global-refs";
|
import { tabBarRef } from "../../../utils/global-refs";
|
||||||
|
import { convertNoteToText } from "../../../utils/note-to-text";
|
||||||
import { sleep } from "../../../utils/time";
|
import { sleep } from "../../../utils/time";
|
||||||
import BaseDialog from "../../dialog/base-dialog";
|
import BaseDialog from "../../dialog/base-dialog";
|
||||||
import DialogButtons from "../../dialog/dialog-buttons";
|
import DialogButtons from "../../dialog/dialog-buttons";
|
||||||
@@ -55,10 +53,6 @@ import Input from "../../ui/input";
|
|||||||
import Seperator from "../../ui/seperator";
|
import Seperator from "../../ui/seperator";
|
||||||
import Paragraph from "../../ui/typography/paragraph";
|
import Paragraph from "../../ui/typography/paragraph";
|
||||||
|
|
||||||
let Keychain;
|
|
||||||
const passInputRef = createRef();
|
|
||||||
const confirmPassRef = createRef();
|
|
||||||
const changePassInputRef = createRef();
|
|
||||||
export class VaultDialog extends Component {
|
export class VaultDialog extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -86,8 +80,14 @@ export class VaultDialog extends Component {
|
|||||||
description: null,
|
description: null,
|
||||||
clearVault: false,
|
clearVault: false,
|
||||||
deleteVault: false,
|
deleteVault: false,
|
||||||
deleteAll: false
|
deleteAll: false,
|
||||||
|
noteLocked: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.passInputRef = createRef();
|
||||||
|
this.confirmPassRef = createRef();
|
||||||
|
this.changePassInputRef = createRef();
|
||||||
|
|
||||||
this.password = null;
|
this.password = null;
|
||||||
this.confirmPassword = null;
|
this.confirmPassword = null;
|
||||||
this.newPassword = null;
|
this.newPassword = null;
|
||||||
@@ -99,7 +99,7 @@ export class VaultDialog extends Component {
|
|||||||
? "Revoke Vault Fingerprint Unlock"
|
? "Revoke Vault Fingerprint Unlock"
|
||||||
: this.state.changePassword
|
: this.state.changePassword
|
||||||
? "Change Vault Password"
|
? "Change Vault Password"
|
||||||
: this.state.note.locked
|
: this.state.noteLocked
|
||||||
? this.state.deleteNote
|
? this.state.deleteNote
|
||||||
? "Delete note"
|
? "Delete note"
|
||||||
: this.state.share
|
: this.state.share
|
||||||
@@ -120,7 +120,7 @@ export class VaultDialog extends Component {
|
|||||||
? "Setup a new password for the vault."
|
? "Setup a new password for the vault."
|
||||||
: this.state.permanant
|
: this.state.permanant
|
||||||
? "Enter password to remove note from vault."
|
? "Enter password to remove note from vault."
|
||||||
: this.state.note.locked
|
: this.state.noteLocked
|
||||||
? this.state.deleteNote
|
? this.state.deleteNote
|
||||||
? "Unlock note to delete it. If biometrics are not working, you can enter device pin to unlock vault."
|
? "Unlock note to delete it. If biometrics are not working, you can enter device pin to unlock vault."
|
||||||
: this.state.share
|
: this.state.share
|
||||||
@@ -128,7 +128,7 @@ export class VaultDialog extends Component {
|
|||||||
: this.state.copyNote
|
: this.state.copyNote
|
||||||
? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault."
|
? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault."
|
||||||
: this.state.goToEditor
|
: this.state.goToEditor
|
||||||
? "Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault."
|
? "Unlock note to open it in editor."
|
||||||
: "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault."
|
: "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault."
|
||||||
: "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note.");
|
: "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note.");
|
||||||
}
|
}
|
||||||
@@ -148,9 +148,6 @@ export class VaultDialog extends Component {
|
|||||||
* @param {import('../../../services/event-manager').vaultType} data
|
* @param {import('../../../services/event-manager').vaultType} data
|
||||||
*/
|
*/
|
||||||
open = async (data) => {
|
open = async (data) => {
|
||||||
if (!Keychain) {
|
|
||||||
Keychain = require("react-native-keychain");
|
|
||||||
}
|
|
||||||
let biometry = await BiometricService.isBiometryAvailable();
|
let biometry = await BiometricService.isBiometryAvailable();
|
||||||
let available = false;
|
let available = false;
|
||||||
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
|
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
|
||||||
@@ -177,7 +174,8 @@ export class VaultDialog extends Component {
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
clearVault: data.clearVault,
|
clearVault: data.clearVault,
|
||||||
deleteVault: data.deleteVault
|
deleteVault: data.deleteVault,
|
||||||
|
noteLocked: data.item && (await db.vaults.itemExists(data.item))
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -199,7 +197,7 @@ export class VaultDialog extends Component {
|
|||||||
|
|
||||||
close = () => {
|
close = () => {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: this.state.title,
|
heading: this.state.title,
|
||||||
message: "Please wait and do not close the app.",
|
message: "Please wait and do not close the app.",
|
||||||
type: "success",
|
type: "success",
|
||||||
@@ -212,7 +210,6 @@ export class VaultDialog extends Component {
|
|||||||
|
|
||||||
this.password = null;
|
this.password = null;
|
||||||
this.confirmPassword = null;
|
this.confirmPassword = null;
|
||||||
SearchService.updateAndSearch();
|
|
||||||
this.setState({
|
this.setState({
|
||||||
visible: false,
|
visible: false,
|
||||||
note: {},
|
note: {},
|
||||||
@@ -236,7 +233,7 @@ export class VaultDialog extends Component {
|
|||||||
if (this.state.loading) return;
|
if (this.state.loading) return;
|
||||||
|
|
||||||
if (!this.password) {
|
if (!this.password) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Password not entered",
|
heading: "Password not entered",
|
||||||
message: "Enter a password for the vault and try again.",
|
message: "Enter a password for the vault and try again.",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -244,20 +241,10 @@ export class VaultDialog extends Component {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.password && this.password.length < 3) {
|
|
||||||
ToastEvent.show({
|
|
||||||
heading: "Password too short",
|
|
||||||
message: "Password must be longer than 3 characters.",
|
|
||||||
type: "error",
|
|
||||||
context: "local"
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.novault) {
|
if (!this.state.novault) {
|
||||||
if (this.password !== this.confirmPassword) {
|
if (this.password !== this.confirmPassword) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Passwords do not match",
|
heading: "Passwords do not match",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -283,7 +270,7 @@ export class VaultDialog extends Component {
|
|||||||
if (this.state.biometricUnlock) {
|
if (this.state.biometricUnlock) {
|
||||||
this._enrollFingerprint(this.newPassword);
|
this._enrollFingerprint(this.newPassword);
|
||||||
}
|
}
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Vault password updated successfully",
|
heading: "Vault password updated successfully",
|
||||||
type: "success",
|
type: "success",
|
||||||
context: "global"
|
context: "global"
|
||||||
@@ -295,17 +282,19 @@ export class VaultDialog extends Component {
|
|||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
if (e.message === db.vault.ERRORS.wrongPassword) {
|
if (e.message === db.vault.ERRORS.wrongPassword) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Incorrect password",
|
heading: "Incorrect password",
|
||||||
message: "Please enter the correct password and try again",
|
message: "Please enter the correct password and try again",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
ToastManager.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (this.state.locked) {
|
} else if (this.state.locked) {
|
||||||
if (!this.password || this.password.trim() === 0) {
|
if (!this.password || this.password.trim() === 0) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Incorrect password",
|
heading: "Incorrect password",
|
||||||
message: "Please enter the correct password and try again",
|
message: "Please enter the correct password and try again",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -316,7 +305,7 @@ export class VaultDialog extends Component {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.state.note.locked) {
|
if (this.state.noteLocked) {
|
||||||
await this._unlockNote();
|
await this._unlockNote();
|
||||||
} else {
|
} else {
|
||||||
db.vault
|
db.vault
|
||||||
@@ -348,14 +337,33 @@ export class VaultDialog extends Component {
|
|||||||
let verified = await db.user.verifyPassword(this.password);
|
let verified = await db.user.verifyPassword(this.password);
|
||||||
if (!(await db.user.getUser())) verified = true;
|
if (!(await db.user.getUser())) verified = true;
|
||||||
if (verified) {
|
if (verified) {
|
||||||
|
let noteIds = [];
|
||||||
|
if (this.state.deleteAll) {
|
||||||
|
const vault = await db.vaults.default();
|
||||||
|
const relations = await db.relations.from(vault, "note").get();
|
||||||
|
noteIds = relations.map((item) => item.toId);
|
||||||
|
}
|
||||||
|
|
||||||
await db.vault.delete(this.state.deleteAll);
|
await db.vault.delete(this.state.deleteAll);
|
||||||
|
|
||||||
|
noteIds.forEach((id) => {
|
||||||
|
eSendEvent(
|
||||||
|
eUpdateNoteInEditor,
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
deleted: true
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
eSendEvent("vaultUpdated");
|
eSendEvent("vaultUpdated");
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
this.close();
|
this.close();
|
||||||
} else {
|
} else {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Account password incorrect",
|
heading: "Account password incorrect",
|
||||||
message: "Please enter correct password for your account.",
|
message: "Please enter correct password for your account.",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -375,14 +383,29 @@ export class VaultDialog extends Component {
|
|||||||
loading: true
|
loading: true
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
const vault = await db.vaults.default();
|
||||||
|
const relations = await db.relations.from(vault, "note").get();
|
||||||
|
const noteIds = relations.map((item) => item.toId);
|
||||||
|
|
||||||
await db.vault.clear(this.password);
|
await db.vault.clear(this.password);
|
||||||
|
|
||||||
|
noteIds.forEach((id) => {
|
||||||
|
eSendEvent(
|
||||||
|
eUpdateNoteInEditor,
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
deleted: true
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
this.close();
|
this.close();
|
||||||
eSendEvent("vaultUpdated");
|
eSendEvent("vaultUpdated");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Vault password incorrect",
|
heading: "Vault password incorrect",
|
||||||
message: "Please enter correct password to clear vault.",
|
message: "Please enter correct password to clear vault.",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -396,7 +419,7 @@ export class VaultDialog extends Component {
|
|||||||
|
|
||||||
async _lockNote() {
|
async _lockNote() {
|
||||||
if (!this.password || this.password.trim() === 0) {
|
if (!this.password || this.password.trim() === 0) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Incorrect password",
|
heading: "Incorrect password",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -404,11 +427,12 @@ export class VaultDialog extends Component {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
await db.vault.add(this.state.note.id);
|
await db.vault.add(this.state.note.id);
|
||||||
if (this.state.note.id === editorController.current?.note?.id) {
|
|
||||||
eSendEvent(eClearEditor);
|
console.log("update note event...");
|
||||||
}
|
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
message: "Note locked successfully",
|
message: "Note locked successfully",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -421,7 +445,7 @@ export class VaultDialog extends Component {
|
|||||||
|
|
||||||
async _unlockNote() {
|
async _unlockNote() {
|
||||||
if (!this.password || this.password.trim() === 0) {
|
if (!this.password || this.password.trim() === 0) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Incorrect password",
|
heading: "Incorrect password",
|
||||||
message: "Please enter the correct password and try again",
|
message: "Please enter the correct password and try again",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -459,7 +483,7 @@ export class VaultDialog extends Component {
|
|||||||
async _deleteNote() {
|
async _deleteNote() {
|
||||||
try {
|
try {
|
||||||
await db.vault.remove(this.state.note.id, this.password);
|
await db.vault.remove(this.state.note.id, this.password);
|
||||||
await deleteItems(this.state.note);
|
await deleteItems([this.state.note.id], "note");
|
||||||
this.close();
|
this.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._takeErrorAction(e);
|
this._takeErrorAction(e);
|
||||||
@@ -479,7 +503,7 @@ export class VaultDialog extends Component {
|
|||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
eSendEvent("vaultUpdated");
|
eSendEvent("vaultUpdated");
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Biometric unlocking enabled!",
|
heading: "Biometric unlocking enabled!",
|
||||||
message: "Now you can unlock notes in vault with biometrics.",
|
message: "Now you can unlock notes in vault with biometrics.",
|
||||||
type: "success",
|
type: "success",
|
||||||
@@ -488,7 +512,7 @@ export class VaultDialog extends Component {
|
|||||||
this.close();
|
this.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.close();
|
this.close();
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Incorrect password",
|
heading: "Incorrect password",
|
||||||
message:
|
message:
|
||||||
"Please enter the correct vault password to enable biometrics.",
|
"Please enter the correct vault password to enable biometrics.",
|
||||||
@@ -511,13 +535,11 @@ export class VaultDialog extends Component {
|
|||||||
}
|
}
|
||||||
if (this.state.note?.id) {
|
if (this.state.note?.id) {
|
||||||
await db.vault.add(this.state.note.id);
|
await db.vault.add(this.state.note.id);
|
||||||
if (this.state.note.id === editorController.current?.note?.id) {
|
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||||
eSendEvent(eClearEditor);
|
|
||||||
}
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Note added to vault",
|
heading: "Note added to vault",
|
||||||
type: "success",
|
type: "success",
|
||||||
context: "global"
|
context: "global"
|
||||||
@@ -525,7 +547,7 @@ export class VaultDialog extends Component {
|
|||||||
this.close();
|
this.close();
|
||||||
} else {
|
} else {
|
||||||
eSendEvent("vaultUpdated");
|
eSendEvent("vaultUpdated");
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Vault created successfully",
|
heading: "Vault created successfully",
|
||||||
type: "success",
|
type: "success",
|
||||||
context: "global"
|
context: "global"
|
||||||
@@ -538,11 +560,12 @@ export class VaultDialog extends Component {
|
|||||||
db.vault
|
db.vault
|
||||||
.remove(this.state.note.id, this.password)
|
.remove(this.state.note.id, this.password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Note permanently unlocked.",
|
heading: "Note permanently unlocked.",
|
||||||
type: "success",
|
type: "success",
|
||||||
context: "global"
|
context: "global"
|
||||||
});
|
});
|
||||||
|
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||||
this.close();
|
this.close();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -553,7 +576,9 @@ export class VaultDialog extends Component {
|
|||||||
_openInEditor(note) {
|
_openInEditor(note) {
|
||||||
this.close();
|
this.close();
|
||||||
InteractionManager.runAfterInteractions(async () => {
|
InteractionManager.runAfterInteractions(async () => {
|
||||||
eSendEvent(eOnLoadNote, note);
|
eSendEvent(eOnLoadNote, {
|
||||||
|
item: note
|
||||||
|
});
|
||||||
if (!DDS.isTab) {
|
if (!DDS.isTab) {
|
||||||
tabBarRef.current?.goToPage(1);
|
tabBarRef.current?.goToPage(1);
|
||||||
}
|
}
|
||||||
@@ -562,7 +587,7 @@ export class VaultDialog extends Component {
|
|||||||
|
|
||||||
async _copyNote(note) {
|
async _copyNote(note) {
|
||||||
Clipboard.setString(await convertNoteToText(note));
|
Clipboard.setString(await convertNoteToText(note));
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Note copied",
|
heading: "Note copied",
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Note has been copied to clipboard!",
|
message: "Note has been copied to clipboard!",
|
||||||
@@ -594,7 +619,7 @@ export class VaultDialog extends Component {
|
|||||||
visible: true
|
visible: true
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Incorrect password",
|
heading: "Incorrect password",
|
||||||
type: "error",
|
type: "error",
|
||||||
context: "local"
|
context: "local"
|
||||||
@@ -609,13 +634,13 @@ export class VaultDialog extends Component {
|
|||||||
try {
|
try {
|
||||||
await BiometricService.resetCredentials();
|
await BiometricService.resetCredentials();
|
||||||
eSendEvent("vaultUpdated");
|
eSendEvent("vaultUpdated");
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Biometric unlocking disabled!",
|
heading: "Biometric unlocking disabled!",
|
||||||
type: "success",
|
type: "success",
|
||||||
context: "global"
|
context: "global"
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ToastEvent.show({
|
ToastManager.show({
|
||||||
heading: "Failed to disable Biometric unlocking.",
|
heading: "Failed to disable Biometric unlocking.",
|
||||||
message: e.message,
|
message: e.message,
|
||||||
type: "success",
|
type: "success",
|
||||||
@@ -667,7 +692,8 @@ export class VaultDialog extends Component {
|
|||||||
<BaseDialog
|
<BaseDialog
|
||||||
onShow={async () => {
|
onShow={async () => {
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
passInputRef.current?.focus();
|
|
||||||
|
this.passInputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
statusBarTranslucent={false}
|
statusBarTranslucent={false}
|
||||||
onRequestClose={this.close}
|
onRequestClose={this.close}
|
||||||
@@ -702,7 +728,7 @@ export class VaultDialog extends Component {
|
|||||||
!this.state.revokeFingerprintAccess ? (
|
!this.state.revokeFingerprintAccess ? (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
fwdRef={passInputRef}
|
fwdRef={this.passInputRef}
|
||||||
editable={!loading}
|
editable={!loading}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
testID={notesnook.ids.dialogs.vault.pwd}
|
testID={notesnook.ids.dialogs.vault.pwd}
|
||||||
@@ -719,7 +745,7 @@ export class VaultDialog extends Component {
|
|||||||
}
|
}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
changePassword
|
changePassword
|
||||||
? changePassInputRef.current?.focus()
|
? this.confirmPassRef.current?.focus()
|
||||||
: this.onPress;
|
: this.onPress;
|
||||||
}}
|
}}
|
||||||
autoComplete="password"
|
autoComplete="password"
|
||||||
@@ -771,7 +797,7 @@ export class VaultDialog extends Component {
|
|||||||
<>
|
<>
|
||||||
<Seperator half />
|
<Seperator half />
|
||||||
<Input
|
<Input
|
||||||
ref={changePassInputRef}
|
ref={this.confirmPassRef}
|
||||||
editable={!loading}
|
editable={!loading}
|
||||||
testID={notesnook.ids.dialogs.vault.changePwd}
|
testID={notesnook.ids.dialogs.vault.changePwd}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
@@ -791,7 +817,7 @@ export class VaultDialog extends Component {
|
|||||||
{!novault ? (
|
{!novault ? (
|
||||||
<View>
|
<View>
|
||||||
<Input
|
<Input
|
||||||
fwdRef={passInputRef}
|
fwdRef={this.passInputRef}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
testID={notesnook.ids.dialogs.vault.pwd}
|
testID={notesnook.ids.dialogs.vault.pwd}
|
||||||
onChangeText={(value) => {
|
onChangeText={(value) => {
|
||||||
@@ -802,13 +828,13 @@ export class VaultDialog extends Component {
|
|||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
confirmPassRef.current?.focus();
|
this.confirmPassRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
fwdRef={confirmPassRef}
|
fwdRef={this.confirmPassRef}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
testID={notesnook.ids.dialogs.vault.pwdAlt}
|
testID={notesnook.ids.dialogs.vault.pwdAlt}
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
@@ -865,10 +891,10 @@ export class VaultDialog extends Component {
|
|||||||
title="Biometric unlocking"
|
title="Biometric unlocking"
|
||||||
iconColor={
|
iconColor={
|
||||||
this.state.biometricUnlock
|
this.state.biometricUnlock
|
||||||
? colors.selected.icon
|
? colors.selected.accent
|
||||||
: colors.primary.icon
|
: colors.primary.icon
|
||||||
}
|
}
|
||||||
type={this.state.biometricUnlock ? "transparent" : "selected"}
|
type={this.state.biometricUnlock ? "transparent" : "plain"}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
@@ -891,7 +917,7 @@ export class VaultDialog extends Component {
|
|||||||
? "Revoke"
|
? "Revoke"
|
||||||
: changePassword
|
: changePassword
|
||||||
? "Change"
|
? "Change"
|
||||||
: note.locked
|
: this.state.noteLocked
|
||||||
? deleteNote
|
? deleteNote
|
||||||
? "Delete"
|
? "Delete"
|
||||||
: share
|
: share
|
||||||
|
|||||||
@@ -17,42 +17,76 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useThemeColors } from "@notesnook/theme";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { Platform, StyleSheet, View } from "react-native";
|
import { Platform, StyleSheet, View } from "react-native";
|
||||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||||
import { SearchBar } from "../../screens/search/search-bar";
|
|
||||||
import {
|
import {
|
||||||
eSubscribeEvent,
|
eSubscribeEvent,
|
||||||
eUnSubscribeEvent
|
eUnSubscribeEvent
|
||||||
} from "../../services/event-manager";
|
} from "../../services/event-manager";
|
||||||
import useNavigationStore from "../../stores/use-navigation-store";
|
import useNavigationStore, {
|
||||||
|
RouteName
|
||||||
|
} from "../../stores/use-navigation-store";
|
||||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||||
import { useThemeColors } from "@notesnook/theme";
|
|
||||||
import { eScrollEvent } from "../../utils/events";
|
import { eScrollEvent } from "../../utils/events";
|
||||||
import { LeftMenus } from "./left-menus";
|
import { LeftMenus } from "./left-menus";
|
||||||
import { RightMenus } from "./right-menus";
|
import { RightMenus } from "./right-menus";
|
||||||
import { Title } from "./title";
|
import { Title } from "./title";
|
||||||
|
import { useNavigation } from "@react-navigation/native";
|
||||||
|
|
||||||
|
type HeaderRightButton = {
|
||||||
|
title: string;
|
||||||
|
onPress: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Header = ({
|
||||||
|
renderedInRoute,
|
||||||
|
onLeftMenuButtonPress,
|
||||||
|
title,
|
||||||
|
titleHiddenOnRender,
|
||||||
|
headerRightButtons,
|
||||||
|
id,
|
||||||
|
accentColor,
|
||||||
|
isBeta,
|
||||||
|
canGoBack,
|
||||||
|
onPressDefaultRightButton,
|
||||||
|
hasSearch,
|
||||||
|
onSearch
|
||||||
|
}: {
|
||||||
|
onLeftMenuButtonPress?: () => void;
|
||||||
|
renderedInRoute: RouteName;
|
||||||
|
id?: string;
|
||||||
|
title: string;
|
||||||
|
headerRightButtons?: HeaderRightButton[];
|
||||||
|
titleHiddenOnRender?: boolean;
|
||||||
|
accentColor?: string;
|
||||||
|
isBeta?: boolean;
|
||||||
|
canGoBack?: boolean;
|
||||||
|
onPressDefaultRightButton?: () => void;
|
||||||
|
hasSearch?: boolean;
|
||||||
|
onSearch?: () => void;
|
||||||
|
}) => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
const _Header = () => {
|
|
||||||
const { colors } = useThemeColors();
|
const { colors } = useThemeColors();
|
||||||
const insets = useGlobalSafeAreaInsets();
|
const insets = useGlobalSafeAreaInsets();
|
||||||
const [hide, setHide] = useState(true);
|
const [borderHidden, setBorderHidden] = useState(true);
|
||||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||||
const currentScreen = useNavigationStore(
|
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
|
||||||
(state) => state.currentScreen?.name
|
|
||||||
);
|
|
||||||
|
|
||||||
const onScroll = useCallback(
|
const onScroll = useCallback(
|
||||||
(data: { x: number; y: number }) => {
|
(data: { x: number; y: number; id?: string; route: string }) => {
|
||||||
|
if (data.route !== renderedInRoute || data.id !== id) return;
|
||||||
if (data.y > 150) {
|
if (data.y > 150) {
|
||||||
if (!hide) return;
|
if (!borderHidden) return;
|
||||||
setHide(false);
|
setBorderHidden(false);
|
||||||
} else {
|
} else {
|
||||||
if (hide) return;
|
if (borderHidden) return;
|
||||||
setHide(true);
|
setBorderHidden(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[hide]
|
[borderHidden, id, renderedInRoute]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,9 +94,9 @@ const _Header = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
eUnSubscribeEvent(eScrollEvent, onScroll);
|
eUnSubscribeEvent(eScrollEvent, onScroll);
|
||||||
};
|
};
|
||||||
}, [hide, onScroll]);
|
}, [borderHidden, onScroll]);
|
||||||
|
|
||||||
return selectionMode ? null : (
|
return selectionMode && isFocused ? null : (
|
||||||
<>
|
<>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -72,29 +106,42 @@ const _Header = () => {
|
|||||||
backgroundColor: colors.primary.background,
|
backgroundColor: colors.primary.background,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: hide
|
borderBottomColor: borderHidden
|
||||||
? "transparent"
|
? "transparent"
|
||||||
: colors.secondary.background,
|
: colors.secondary.background,
|
||||||
justifyContent: "space-between"
|
justifyContent: "space-between"
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{currentScreen === "Search" ? (
|
<>
|
||||||
<SearchBar />
|
<View style={styles.leftBtnContainer}>
|
||||||
) : (
|
<LeftMenus
|
||||||
<>
|
canGoBack={canGoBack}
|
||||||
<View style={styles.leftBtnContainer}>
|
onLeftButtonPress={onLeftMenuButtonPress}
|
||||||
<LeftMenus />
|
/>
|
||||||
<Title />
|
|
||||||
</View>
|
<Title
|
||||||
<RightMenus />
|
isHiddenOnRender={titleHiddenOnRender}
|
||||||
</>
|
renderedInRoute={renderedInRoute}
|
||||||
)}
|
id={id}
|
||||||
|
accentColor={accentColor}
|
||||||
|
title={title}
|
||||||
|
isBeta={isBeta}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<RightMenus
|
||||||
|
renderedInRoute={renderedInRoute}
|
||||||
|
id={id}
|
||||||
|
headerRightButtons={headerRightButtons}
|
||||||
|
onPressDefaultRightButton={onPressDefaultRightButton}
|
||||||
|
search={hasSearch}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const Header = React.memo(_Header, () => true);
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|||||||