Compare commits
1126 Commits
background
...
fix-sideba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b1f168e4c | ||
|
|
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 | ||
|
|
ee5c4077fe | ||
|
|
47bbfc618e | ||
|
|
b377329a8b | ||
|
|
ae8bcf3865 | ||
|
|
2f67611829 | ||
|
|
058c612f4f | ||
|
|
be8df90ccb | ||
|
|
aa730b5b93 | ||
|
|
34bbcf6387 | ||
|
|
33c3c78e2f | ||
|
|
bf144dcda4 | ||
|
|
284ebdb86f | ||
|
|
9dac4cf7f4 | ||
|
|
fb973a5514 | ||
|
|
f9226510f9 | ||
|
|
5c0a964699 | ||
|
|
41ffaab3dc | ||
|
|
764c721f1d | ||
|
|
3d267f91ef | ||
|
|
f10f28a7f8 | ||
|
|
8d04f89a18 | ||
|
|
9d1374dcd4 | ||
|
|
094a65e514 | ||
|
|
372da979e1 | ||
|
|
d3371326f2 | ||
|
|
57452bd0e6 | ||
|
|
993b8460bf | ||
|
|
081201aa42 | ||
|
|
270e6778d1 | ||
|
|
2074320583 | ||
|
|
cd90030616 | ||
|
|
807bd683b7 | ||
|
|
35929026a4 | ||
|
|
41eb94ee47 | ||
|
|
ba51e110ad | ||
|
|
ecad06d249 | ||
|
|
19a151f637 | ||
|
|
7c4a6f1e17 | ||
|
|
bc88e953d1 | ||
|
|
7d89ec676a | ||
|
|
c8f8bcca93 | ||
|
|
9fa904e849 | ||
|
|
05cc947cb4 | ||
|
|
a20a683b17 | ||
|
|
5fc43c931e | ||
|
|
0879c3b4d6 | ||
|
|
2eded71de3 | ||
|
|
9e30ee50b8 | ||
|
|
08621a5873 | ||
|
|
17f5bd28ba | ||
|
|
4558c0b2eb | ||
|
|
a442677e91 | ||
|
|
95666bee36 | ||
|
|
c4232ac95a | ||
|
|
0283037289 | ||
|
|
b6fd0df715 | ||
|
|
f53fb58024 | ||
|
|
21d452308d | ||
|
|
823608cca1 | ||
|
|
f3a0b998e4 | ||
|
|
a47a38b16b | ||
|
|
a184c53dd5 | ||
|
|
a3464255ec | ||
|
|
088c6670a9 | ||
|
|
27840b9bd6 | ||
|
|
ae819504f0 | ||
|
|
af792e394c | ||
|
|
76a4d5c2a5 | ||
|
|
4e23acd608 | ||
|
|
3c65dcdd8b | ||
|
|
efea6e3972 | ||
|
|
185cca7880 | ||
|
|
fbded93405 | ||
|
|
674881b873 | ||
|
|
248fb6dd2d | ||
|
|
3211e7a149 | ||
|
|
08496c699e | ||
|
|
060fa25814 | ||
|
|
a6b1effc77 | ||
|
|
5cf9c4808e | ||
|
|
d5a44067fd | ||
|
|
0de463526f | ||
|
|
5fd79875ef | ||
|
|
bdb19ad325 | ||
|
|
7a12efc823 | ||
|
|
2f3bddf860 | ||
|
|
6127eb97af | ||
|
|
86386e14f3 | ||
|
|
9cc17c29f6 | ||
|
|
a31e863c7b | ||
|
|
15a32ac83a | ||
|
|
fa5b84ec0d | ||
|
|
7905584eac | ||
|
|
afc9b96a11 | ||
|
|
4e6466fc18 | ||
|
|
233baf78ce | ||
|
|
42d0bee37f | ||
|
|
ccadab6a10 | ||
|
|
bfe5a5cb96 | ||
|
|
88fc24c4cf | ||
|
|
8be4fea7a3 | ||
|
|
a3d39d8b6d | ||
|
|
ff36f0259a | ||
|
|
d13654088a | ||
|
|
8a6b97c757 | ||
|
|
d71e0c109b | ||
|
|
7e345303fe | ||
|
|
d881bdb77c | ||
|
|
4961102d8f | ||
|
|
4f2ab4f1da |
@@ -15,6 +15,8 @@ apps/mobile/e2e/
|
||||
|
||||
# web
|
||||
apps/web/public/an.js
|
||||
apps/web/src/common/sqlite/wa-sqlite-async.js
|
||||
apps/web/src/common/sqlite/wa-sqlite.js
|
||||
|
||||
# editor
|
||||
packages/editor/styles/
|
||||
|
||||
@@ -5,9 +5,9 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 20.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
|
||||
111
.github/workflows/android.publish.internal.yml
vendored
@@ -6,6 +6,11 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||
NDK_CCACHE: ccache
|
||||
USE_CCACHE: 1
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -24,30 +29,126 @@ jobs:
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
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
|
||||
run: yarn release:android:bundle
|
||||
|
||||
- name: CCache Stats after build
|
||||
run: ccache -sv
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
uses: r0adkll/sign-android-release@master
|
||||
with:
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ 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
|
||||
id: deploy
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
uses: r0adkll/upload-google-play@v1.1.1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: internal
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: alpha
|
||||
status: completed
|
||||
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}}
|
||||
|
||||
87
.github/workflows/android.publish.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Publish @notesnook/mobile
|
||||
name: Publish @notesnook/android
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
@@ -6,6 +6,11 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
CMAKE_CXX_COMPILER_LAUNCHER: ccache
|
||||
NDK_CCACHE: ccache
|
||||
USE_CCACHE: 1
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -28,29 +33,65 @@ jobs:
|
||||
- name: Make Gradlew Executable
|
||||
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
|
||||
run: yarn release:android:bundle
|
||||
|
||||
- name: CCache Stats after build
|
||||
run: ccache -sv
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
uses: r0adkll/sign-android-release@master
|
||||
with:
|
||||
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- 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/
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
|
||||
- name: Build apks for Github release
|
||||
run: yarn release:android
|
||||
@@ -64,6 +105,8 @@ jobs:
|
||||
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: |
|
||||
@@ -79,6 +122,18 @@ jobs:
|
||||
with:
|
||||
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
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
@@ -92,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-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}}
|
||||
|
||||
58
.github/workflows/desktop.publish.yml
vendored
@@ -18,6 +18,21 @@ on:
|
||||
required: true
|
||||
default: true
|
||||
description: "Publish on GitHub releases?"
|
||||
build-windows:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Windows?"
|
||||
build-linux:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Linux?"
|
||||
build-mac:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for macOS?"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -43,7 +58,7 @@ jobs:
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
@@ -67,11 +82,13 @@ jobs:
|
||||
name: Notesnook Desktop 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
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
runs-on: macos-12
|
||||
if: inputs.build-mac
|
||||
runs-on: macos-14
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
@@ -80,6 +97,9 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install setuptools
|
||||
run: brew install python-setuptools
|
||||
|
||||
- name: Setup notarization
|
||||
run: |
|
||||
mkdir -p ~/private_keys/
|
||||
@@ -106,7 +126,7 @@ jobs:
|
||||
platform: "MAC_OS"
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
@@ -127,7 +147,7 @@ jobs:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: Build zip and dmg
|
||||
@@ -137,13 +157,13 @@ jobs:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
|
||||
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: |
|
||||
npx nx run release --project @notesnook/desktop
|
||||
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
|
||||
npx electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
@@ -163,6 +183,7 @@ jobs:
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
@@ -181,7 +202,7 @@ jobs:
|
||||
if: inputs.publish-snap
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
@@ -198,7 +219,7 @@ jobs:
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
npx electron-builder --linux snap:x64 -p never
|
||||
yarn electron-builder --linux snap:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build AppImage
|
||||
@@ -206,9 +227,9 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
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
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
@@ -221,17 +242,27 @@ jobs:
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
if: inputs.build-windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET Core SDK
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
|
||||
- name: Install TrustedSigning
|
||||
run: Install-Module -Name TrustedSigning -RequiredVersion 0.3.15 -Force -Repository PSGallery
|
||||
shell: pwsh
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
@@ -247,6 +278,9 @@ jobs:
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
yarn electron-builder --win --publish always
|
||||
|
||||
104
.github/workflows/ios.publish.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Publish @notesnook/ios
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Build packages
|
||||
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
|
||||
run: npm run prepare:ios
|
||||
|
||||
- name: CCache Stats Before Build
|
||||
run: ccache -sv
|
||||
|
||||
- name: Build iOS App
|
||||
uses: ammarahm-ed/ios-build-action@master
|
||||
with:
|
||||
bundle-identifier: org.streetwriters.notesnook
|
||||
scheme: Notesnook
|
||||
configuration: "Release"
|
||||
export-options: apps/mobile/native/ios/ExportOptions.plist
|
||||
project-path: apps/mobile/native/ios/Notesnook.xcodeproj
|
||||
workspace-path: apps/mobile/native/ios/Notesnook.xcworkspace
|
||||
update-targets: |
|
||||
Notesnook
|
||||
Make Note
|
||||
NotesWidgetExtension
|
||||
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
|
||||
code-signing-identity: Apple Distribution
|
||||
team-id: ${{ secrets.APPLE_TEAM_ID }}
|
||||
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
|
||||
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
|
||||
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
|
||||
output-path: Notesnook.ipa
|
||||
mobileprovision-base64: |
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_APP }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
||||
|
||||
- name: CCache Stats After Build
|
||||
run: ccache -sv
|
||||
|
||||
- name: "Upload app to TestFlight"
|
||||
uses: apple-actions/upload-testflight-build@v1
|
||||
with:
|
||||
app-path: Notesnook.ipa
|
||||
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.API_KEY_ID }}
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload Notesnook.ipa to Github
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Notesnook.ipa
|
||||
path: Notesnook.ipa
|
||||
4
.github/workflows/web.beta.publish.yml
vendored
@@ -1,10 +1,11 @@
|
||||
name: Publish @notesnook/web Beta
|
||||
|
||||
on:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- "!v3-beta"
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
@@ -22,6 +23,7 @@ on:
|
||||
- synchronize
|
||||
branches:
|
||||
- master
|
||||
- "!v3-beta"
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
|
||||
2
.github/workflows/web.tests.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Install packages
|
||||
run: |
|
||||
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
|
||||
run: ls -ld ./apps/web/build/index.html
|
||||
|
||||
32
.github/workflows/web.v3-beta.publish.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
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: Build
|
||||
run: npm run build:web
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-v3-beta ./apps/web/build/
|
||||
3
.gitignore
vendored
@@ -9,4 +9,5 @@ nx-cloud.env
|
||||
.env.local
|
||||
.nyc_output
|
||||
site
|
||||
node_modules.backup
|
||||
node_modules.backup
|
||||
.nx
|
||||
|
||||
@@ -11,4 +11,5 @@ apps/mobile/native/
|
||||
apps/web/public/an.js
|
||||
|
||||
# editor
|
||||
packages/editor/styles/
|
||||
packages/editor/styles/
|
||||
/.nx/cache
|
||||
3
apps/desktop/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
node_modules
|
||||
build
|
||||
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 |
1541
apps/desktop/package-lock.json
generated
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.12",
|
||||
"version": "3.0.0",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -14,9 +14,11 @@
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/server": "10.38.3",
|
||||
"better-sqlite3-multiple-ciphers": "^9.5.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.4",
|
||||
"icojs": "^0.17.1",
|
||||
"sodium-native": "^4.1.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.6.2",
|
||||
"zod": "^3.21.4"
|
||||
@@ -25,12 +27,13 @@
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "25.9.3",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
"electron": "^29.3.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"esbuild": "^0.20.0",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"prebuildify": "^6.0.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^5.23.0"
|
||||
"undici": "^6.14.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
@@ -40,7 +43,7 @@
|
||||
"staging": "node scripts/build.mjs --run",
|
||||
"release": "node scripts/build.mjs",
|
||||
"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",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
@@ -54,7 +57,7 @@
|
||||
"productName": "Notesnook",
|
||||
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
||||
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
||||
"npmRebuild": false,
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"asar": false,
|
||||
"files": [
|
||||
"!*.chunk.js.map",
|
||||
@@ -64,9 +67,17 @@
|
||||
"!build/banner.jpg",
|
||||
"!build/*.ico",
|
||||
"!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",
|
||||
"mac": {
|
||||
"bundleVersion": "240",
|
||||
@@ -94,7 +105,8 @@
|
||||
"entitlements": "assets/entitlements.mac.plist",
|
||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||
"gatekeeperAssess": false,
|
||||
"icon": "assets/icons/app.icns"
|
||||
"icon": "assets/icons/app.icns",
|
||||
"notarize": true
|
||||
},
|
||||
"dmg": {
|
||||
"contents": [
|
||||
@@ -135,6 +147,10 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./scripts/sign.js",
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
"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 fs from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
import fs, { readFile, writeFile } from "fs/promises";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import yargs from "yargs-parser";
|
||||
import os from "os";
|
||||
import * as childProcess from "child_process";
|
||||
@@ -28,27 +28,30 @@ import { fileURLToPath } from "url";
|
||||
const args = yargs(process.argv);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const sodiumNativePrebuildPath = (arch) =>
|
||||
path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`,
|
||||
`${os.platform()}-${arch}`
|
||||
);
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
|
||||
);
|
||||
|
||||
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
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", {
|
||||
recursive: true,
|
||||
force: true
|
||||
@@ -62,56 +65,39 @@ if (args.variant === "mas") {
|
||||
|
||||
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) {
|
||||
await exec(`npx electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
await exec(`./output/darwin-unpacked/Notesnook`);
|
||||
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
} else {
|
||||
await exec(`./output/linux-unpacked/Notesnook`);
|
||||
}
|
||||
}
|
||||
|
||||
async function exec(cmd) {
|
||||
async function exec(cmd, cwd) {
|
||||
return childProcess.execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit"
|
||||
env: { ...process.env, NOTESNOOK_STAGING: true },
|
||||
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 __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 RESTARTABLE_PROCESSES = [];
|
||||
let lastBundleHash = null;
|
||||
|
||||
const ENV = {
|
||||
...process.env,
|
||||
NO_COLOR: "true",
|
||||
FORCE_COLOR: "false",
|
||||
COLOR: "0"
|
||||
};
|
||||
await onChange(true);
|
||||
|
||||
console.log("Watching...");
|
||||
@@ -58,6 +52,8 @@ process.on("SIGINT", async (s) => {
|
||||
async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await exec("npx electron-builder install-app-deps");
|
||||
}
|
||||
|
||||
await exec(`npm run bundle`);
|
||||
@@ -68,13 +64,6 @@ async function onChange(first) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||
@@ -102,7 +91,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
|
||||
|
||||
const s = spawn(cmd[0], cmd.slice(1), {
|
||||
cwd,
|
||||
env: { ...process.env, NO_COLOR: "true" },
|
||||
env: ENV,
|
||||
shell: false
|
||||
});
|
||||
|
||||
@@ -116,14 +105,15 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
|
||||
});
|
||||
}
|
||||
|
||||
async function exec(cmd) {
|
||||
async function exec(cmd, cwd) {
|
||||
try {
|
||||
console.log(">", cmd);
|
||||
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
env: ENV,
|
||||
stdio: "inherit",
|
||||
shell: false
|
||||
shell: false,
|
||||
cwd: cwd || process.cwd()
|
||||
});
|
||||
} catch {
|
||||
//ignore
|
||||
@@ -136,7 +126,7 @@ function execAsync(cmd, args, restartable, onExit) {
|
||||
|
||||
const proc = spawn(cmd, args, {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
env: ENV,
|
||||
shell: false
|
||||
});
|
||||
|
||||
@@ -179,9 +169,10 @@ async function cleanup() {
|
||||
}
|
||||
|
||||
async function isBundleSame() {
|
||||
const bundle = await fs.readFile(
|
||||
path.join(__dirname, "..", "build", "electron.js")
|
||||
);
|
||||
const bundle = Buffer.concat([
|
||||
await fs.readFile(path.join(__dirname, "..", "build", "electron.js")),
|
||||
await fs.readFile(path.join(__dirname, "..", "build", "preload.js"))
|
||||
]);
|
||||
|
||||
const hashSum = crypto.createHash("sha256");
|
||||
hashSum.update(bundle);
|
||||
|
||||
59
apps/desktop/scripts/sign.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
const { writeFileSync, rmSync } = require("fs");
|
||||
const { execSync } = require("child_process");
|
||||
const { relative, join } = require("path");
|
||||
|
||||
module.exports = async function (configuration) {
|
||||
if (process.env.NOTESNOOK_STAGING) return;
|
||||
|
||||
const Endpoint = "https://weu.codesigning.azure.net";
|
||||
const CodeSigningAccountName = "Notesnook";
|
||||
const CertificateProfileName = "Notesnook";
|
||||
const FileDigest = configuration.hash.toUpperCase();
|
||||
const TimestampRfc3161 = "http://timestamp.acs.microsoft.com";
|
||||
const TimestampDigest = configuration.hash.toUpperCase();
|
||||
const Description = "The Notesnook app";
|
||||
const DescriptionUrl = "https://notesnook.com/";
|
||||
const FilesCatalog = createCatalog(configuration.path);
|
||||
|
||||
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);
|
||||
|
||||
psexec(command);
|
||||
|
||||
console.debug("Signed", configuration.path);
|
||||
|
||||
rmSync(FilesCatalog);
|
||||
};
|
||||
|
||||
function createCatalog(path) {
|
||||
const catalogPath = join(__dirname, "_catalog");
|
||||
writeFileSync(catalogPath, relative(__dirname, path));
|
||||
return catalogPath;
|
||||
}
|
||||
|
||||
function psexec(cmd) {
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: "pwsh"
|
||||
});
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import { osIntegrationRouter } from "./os-integration";
|
||||
import { spellCheckerRouter } from "./spell-checker";
|
||||
import { updaterRouter } from "./updater";
|
||||
import { bridgeRouter } from "./bridge";
|
||||
import { safeStorageRouter } from "./safe-storage";
|
||||
import { windowRouter } from "./window";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -31,7 +33,9 @@ export const router = t.router({
|
||||
integration: osIntegrationRouter,
|
||||
spellChecker: spellCheckerRouter,
|
||||
updater: updaterRouter,
|
||||
bridge: bridgeRouter
|
||||
bridge: bridgeRouter,
|
||||
safeStorage: safeStorageRouter,
|
||||
window: windowRouter
|
||||
});
|
||||
|
||||
export const api = router.createCaller({});
|
||||
|
||||
@@ -31,6 +31,7 @@ import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
import { rm } from "fs/promises";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -54,6 +55,14 @@ export const osIntegrationRouter = t.router({
|
||||
config.zoomFactor = factor;
|
||||
}),
|
||||
|
||||
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),
|
||||
setPrivacyMode: t.procedure
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
@@ -126,10 +135,13 @@ export const osIntegrationRouter = t.router({
|
||||
.input(z.object({ filePath: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { filePath } = input;
|
||||
if (!filePath) return;
|
||||
return resolvePath(filePath);
|
||||
}),
|
||||
|
||||
deleteFile: t.procedure.input(z.string()).query(async ({ input }) => {
|
||||
await rm(input);
|
||||
}),
|
||||
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
@@ -158,16 +170,42 @@ export const osIntegrationRouter = t.router({
|
||||
}),
|
||||
bringToFront: t.procedure.query(() => bringToFront()),
|
||||
changeTheme: t.procedure
|
||||
.input(Theme)
|
||||
.mutation(({ input }) => setTheme(input)),
|
||||
.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(() =>
|
||||
observable<"dark" | "light">((emit) => {
|
||||
nativeTheme.on("updated", () => {
|
||||
const updated = () => {
|
||||
if (getTheme() === "system") {
|
||||
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 } }) => {
|
||||
globalThis.window?.webContents.session.setSpellCheckerEnabled(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 };
|
||||
|
||||
const t = initTRPC.create();
|
||||
let cancellationToken: CancellationToken | undefined = undefined;
|
||||
|
||||
export const updaterRouter = t.router({
|
||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||
download: t.procedure.query(async () => {
|
||||
const cancellationToken = new CancellationToken();
|
||||
await autoUpdater.downloadUpdate(cancellationToken);
|
||||
if (cancellationToken) return;
|
||||
cancellationToken = new CancellationToken();
|
||||
await autoUpdater
|
||||
.downloadUpdate(cancellationToken)
|
||||
.finally(() => (cancellationToken = undefined));
|
||||
}),
|
||||
check: t.procedure.query(async () => {
|
||||
await autoUpdater.checkForUpdates();
|
||||
await autoUpdater.checkForUpdates().catch(console.error);
|
||||
}),
|
||||
|
||||
toggleAutoUpdates: t.procedure
|
||||
@@ -71,6 +75,7 @@ function createSubscription<
|
||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||
emit.next(args[0]);
|
||||
};
|
||||
autoUpdater.removeAllListeners(eventName);
|
||||
autoUpdater.addListener(eventName, listener);
|
||||
return () => {
|
||||
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 type { AppRouter } from "./api";
|
||||
export { type UpdateInfo } from "builder-util-runtime";
|
||||
export { type DesktopIntegration } from "./utils/config";
|
||||
|
||||
@@ -82,6 +82,19 @@ async function createWindow() {
|
||||
size: 512,
|
||||
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: {
|
||||
zoomFactor: config.zoomFactor,
|
||||
nodeIntegration: true,
|
||||
@@ -112,6 +125,7 @@ async function createWindow() {
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
|
||||
|
||||
mainWindow.once("closed", () => {
|
||||
globalThis.window = null;
|
||||
@@ -121,7 +135,7 @@ async function createWindow() {
|
||||
setupJumplist();
|
||||
|
||||
if (isDevelopment())
|
||||
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
|
||||
mainWindow.webContents.openDevTools({ mode: "bottom", activate: true });
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
@@ -137,6 +151,15 @@ async function createWindow() {
|
||||
app.once("ready", async () => {
|
||||
console.info("App ready. Opening window.");
|
||||
|
||||
app.configureHostResolver({
|
||||
secureDnsServers: [
|
||||
"https://mozilla.cloudflare-dns.com/dns-query",
|
||||
"https://dns.quad9.net/dns-query"
|
||||
],
|
||||
enableBuiltInResolver: true,
|
||||
secureDnsMode: "automatic"
|
||||
});
|
||||
|
||||
if (!isDevelopment()) registerProtocol();
|
||||
await createWindow();
|
||||
configureAutoUpdater();
|
||||
|
||||
@@ -23,11 +23,13 @@ import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
import type { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { platform } from "os";
|
||||
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
var createSQLite3Database: (filename: string) => Database;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
@@ -40,11 +42,6 @@ process.once("loaded", async () => {
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
});
|
||||
|
||||
globalThis.NativeNNCrypto =
|
||||
process.platform === "win32" &&
|
||||
process.arch !== "x64" &&
|
||||
process.arch !== "ia32"
|
||||
? undefined
|
||||
: require("@notesnook/crypto").NNCrypto;
|
||||
|
||||
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
|
||||
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||
|
||||
@@ -25,7 +25,11 @@ Type=Application
|
||||
Version=${app.getVersion()}
|
||||
Name=${app.getName()}
|
||||
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
|
||||
Terminal=false`;
|
||||
|
||||
|
||||
@@ -20,11 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { config } from "./config";
|
||||
|
||||
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
|
||||
? "beta"
|
||||
: "latest";
|
||||
async function configureAutoUpdater() {
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "generic",
|
||||
url: `https://notesnook.com/releases/${process.platform}/`,
|
||||
useMultipleRangeRequest: false
|
||||
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
|
||||
useMultipleRangeRequest: false,
|
||||
channel: CHANNEL
|
||||
});
|
||||
|
||||
autoUpdater.autoDownload = config.automaticUpdates;
|
||||
|
||||
@@ -41,7 +41,12 @@ export const config = {
|
||||
isSpellCheckerEnabled: true,
|
||||
zoomFactor: 1,
|
||||
theme: nativeTheme.themeSource,
|
||||
automaticUpdates: true
|
||||
automaticUpdates: true,
|
||||
proxyRules: "",
|
||||
|
||||
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
|
||||
windowControlsIconColor:
|
||||
nativeTheme.themeSource === "dark" ? "#ffffff" : "#000000"
|
||||
};
|
||||
|
||||
type ConfigKey = keyof typeof config;
|
||||
|
||||
@@ -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)
|
||||
menu.append(
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { protocol } from "electron";
|
||||
import { net, protocol } from "electron";
|
||||
import { isDevelopment } from "./index";
|
||||
import { createReadStream } from "fs";
|
||||
import { extname, normalize } from "path";
|
||||
@@ -62,26 +62,7 @@ function registerProtocol() {
|
||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
});
|
||||
} else {
|
||||
if (request.headers.has("X-Content-Length")) {
|
||||
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"
|
||||
});
|
||||
return net.fetch(request, { bypassCustomProtocolHandlers: true });
|
||||
}
|
||||
});
|
||||
console.info(`${SCHEME} protocol inteception "successful"`);
|
||||
|
||||
@@ -41,7 +41,10 @@ function setTheme(theme: Theme) {
|
||||
}
|
||||
|
||||
function getBackgroundColor() {
|
||||
return nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff";
|
||||
return (
|
||||
config.backgroundColor ||
|
||||
(nativeTheme.shouldUseDarkColors ? "#0f0f0f" : "#ffffff")
|
||||
);
|
||||
}
|
||||
|
||||
function getSystemTheme() {
|
||||
|
||||
@@ -16,15 +16,13 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "@azure/core-asynciterator-polyfill";
|
||||
import "react-native-gesture-handler";
|
||||
import SettingsService from "./services/settings";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
@@ -32,19 +30,25 @@ import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { ApplicationHolder } from "./navigation";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
import { useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useEffect(() => {
|
||||
const { appLockMode } = SettingsService.get();
|
||||
if (appLockMode && appLockMode !== "none") {
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
|
||||
|
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/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
// app lock with password enabled.
|
||||
export const CipherStorage = new MMKVLoader()
|
||||
.withInstanceID("cipher_storage")
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const 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({
|
||||
ios: {
|
||||
@@ -37,61 +60,231 @@ const KEYSTORE_CONFIG = Platform.select({
|
||||
android: {}
|
||||
});
|
||||
|
||||
export async function deriveCryptoKey(name, data) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
name,
|
||||
credentials.key,
|
||||
KEYSTORE_CONFIG
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
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) {
|
||||
console.error(e);
|
||||
DatabaseLogger.error(e);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeIOSKeychain(username, password) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
|
||||
await Keychain.setInternetCredentials(
|
||||
"notesnook",
|
||||
username,
|
||||
password,
|
||||
KEYSTORE_CONFIG
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const decrypted = await decrypt(
|
||||
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
|
||||
appLockCipher
|
||||
);
|
||||
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) {
|
||||
try {
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
let credentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
// 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 {
|
||||
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey()
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
DatabaseLogger.info("User key decrypted: ", !!key);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.log("getCryptoKey", e);
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCryptoKey(_name) {
|
||||
try {
|
||||
let result = await Keychain.resetInternetCredentials("notesnook");
|
||||
return result;
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} 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);
|
||||
return credentials;
|
||||
} catch (e) {
|
||||
console.log("generateCryptoKey: ", e);
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +316,8 @@ export async function decrypt(password, data) {
|
||||
return undefined;
|
||||
let _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
@@ -135,6 +330,11 @@ export async function decryptMulti(password, data) {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
|
||||
if (data.length && !password.salt) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
@@ -36,11 +37,11 @@ database.host(
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.43.108:5264",
|
||||
// AUTH_HOST: "http://192.168.43.108:8264",
|
||||
// SSE_HOST: "http://192.168.43.108:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.43.108:9264",
|
||||
// ISSUES_HOST: "http://192.168.43.108:2624"
|
||||
// API_HOST: "http://192.168.43.5:5264",
|
||||
// AUTH_HOST: "http://192.168.43.5:8264",
|
||||
// SSE_HOST: "http://192.168.43.5:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
|
||||
// ISSUES_HOST: "http://192.168.43.5:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
@@ -51,30 +52,37 @@ database.host(
|
||||
}
|
||||
);
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: {
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
},
|
||||
batchSize: 500,
|
||||
sqliteOptions: {
|
||||
dialect: {
|
||||
createDriver: () =>
|
||||
new RNSqliteDriver({ async: true, dbName: "test.db" }),
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key)
|
||||
throw new Error("Database setup failed, could not get database key");
|
||||
|
||||
console.log("Opening database with key:", !!key);
|
||||
|
||||
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
|
||||
}
|
||||
// journalMode: "MEMORY",
|
||||
// synchronous: "normal",
|
||||
// pageSize: 8192,
|
||||
// cacheSize: -16000,
|
||||
// lockingMode: "exclusive"
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
@@ -45,15 +45,15 @@ export class RNSqliteDriver implements Driver {
|
||||
}
|
||||
|
||||
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("begin"));
|
||||
await connection.executeQuery(CompiledQuery.raw("BEGIN TRANSACTION"));
|
||||
}
|
||||
|
||||
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("commit"));
|
||||
await connection.executeQuery(CompiledQuery.raw("COMMIT"));
|
||||
}
|
||||
|
||||
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
||||
await connection.executeQuery(CompiledQuery.raw("ROLLBACK"));
|
||||
}
|
||||
|
||||
async releaseConnection(): Promise<void> {
|
||||
@@ -106,9 +106,9 @@ class RNSqliteConnection implements DatabaseConnection {
|
||||
: query.kind === "RawNode"
|
||||
? "raw"
|
||||
: "exec";
|
||||
|
||||
const result = await this.db.executeAsync(sql, parameters as any[]);
|
||||
|
||||
// console.log("SQLITE result:", result?.rows?._array);
|
||||
if (mode === "query" || !result.insertId)
|
||||
return {
|
||||
rows: result.rows?._array || []
|
||||
|
||||
@@ -91,7 +91,8 @@ export class KV {
|
||||
}
|
||||
|
||||
async removeMulti(keys) {
|
||||
return this.storage.removeItems(...keys);
|
||||
if (!keys) return true;
|
||||
return this.storage.removeItems(keys);
|
||||
}
|
||||
|
||||
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
|
||||
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 RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
console.log("hello world");
|
||||
/**
|
||||
* Scale down & compress images to screen width
|
||||
* for loading in editor.
|
||||
* @returns
|
||||
*/
|
||||
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(
|
||||
path,
|
||||
width * scale,
|
||||
9999,
|
||||
1920,
|
||||
99999,
|
||||
type,
|
||||
80,
|
||||
0,
|
||||
@@ -40,11 +81,6 @@ export async function compressToBase64(path, type) {
|
||||
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);
|
||||
return base64;
|
||||
return response.uri;
|
||||
}
|
||||
|
||||
@@ -31,10 +31,11 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir } from "./io";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -45,15 +46,15 @@ export const FileDownloadStatus = {
|
||||
* Download all user's attachments
|
||||
* @returns
|
||||
*/
|
||||
export function downloadAllAttachments() {
|
||||
const attachments = db.attachments.all;
|
||||
export async function downloadAllAttachments() {
|
||||
const attachments = await db.attachments.all.ids();
|
||||
return downloadAttachments(attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param attachments
|
||||
* @param {string[]} attachments
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
@@ -83,13 +84,15 @@ export async function downloadAttachments(
|
||||
Platform.OS === "ios"
|
||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.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.mkdir(zipSourceFolder);
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
let attachment = attachments[i];
|
||||
const hash = attachment.metadata.hash;
|
||||
let attachment = await db.attachments.attachment(attachments[i]);
|
||||
const hash = attachment.hash;
|
||||
try {
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
@@ -113,19 +116,20 @@ export async function downloadAttachments(
|
||||
}
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
|
||||
const filePath = `${zipSourceFolder}/${attachment.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
status: FileDownloadStatus.Success
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Success,
|
||||
attachment: attachment
|
||||
});
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.metadata.filename,
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
console.log("Error downloading attachment", e);
|
||||
ToastManager.error(e, "Error downloading attachment");
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
@@ -162,7 +166,7 @@ export async function downloadAttachments(
|
||||
} catch (e) {
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
console.log("Error zipping attachments", e);
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
@@ -181,14 +185,16 @@ export default async function downloadAttachment(
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
let attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
DatabaseLogger.log("Attachment not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,41 +209,47 @@ export default async function downloadAttachment(
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(
|
||||
"starting download attachment",
|
||||
attachment.metadata.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (
|
||||
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
|
||||
)
|
||||
.downloadFile(options.groupId || attachment.hash, attachment.hash);
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.base64 || options.text) {
|
||||
console.log(
|
||||
"starting decrypt base64 file...",
|
||||
options.base64,
|
||||
options.text,
|
||||
attachment.hash
|
||||
);
|
||||
return await db.attachments.read(
|
||||
attachment.hash,
|
||||
options.base64 ? "base64" : "text"
|
||||
);
|
||||
}
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
attachment.metadata.filename,
|
||||
attachment.metadata.type
|
||||
attachment.filename,
|
||||
attachment.mimeType
|
||||
);
|
||||
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
|
||||
let info = {
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
length: attachment.length,
|
||||
length: attachment.size,
|
||||
alg: attachment.alg,
|
||||
hash: attachment.metadata.hash,
|
||||
hashType: attachment.metadata.hashType,
|
||||
mime: attachment.metadata.type,
|
||||
hash: attachment.hash,
|
||||
hashType: attachment.hashType,
|
||||
mime: attachment.mimeType,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
@@ -254,11 +266,11 @@ export default async function downloadAttachment(
|
||||
|
||||
if (
|
||||
attachment.dateUploaded &&
|
||||
!isImage(attachment.metadata?.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
!isImage(attachment.mimeType) &&
|
||||
!isDocument(attachment.mimeType)
|
||||
) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(console.log);
|
||||
}
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
@@ -273,7 +285,7 @@ export default async function downloadAttachment(
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
icon: "download",
|
||||
context: global ? null : attachment.metadata.hash,
|
||||
context: global ? null : attachment.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
@@ -283,10 +295,13 @@ export default async function downloadAttachment(
|
||||
console.log("download attachment error: ", e);
|
||||
if (attachment.dateUploaded) {
|
||||
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);
|
||||
}
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,14 @@ import { createCacheDir, exists } from "./io";
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
|
||||
console.log("Downloading", filename);
|
||||
await createCacheDir();
|
||||
|
||||
let { url, headers } = data;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
console.log("Exists already", filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ export async function checkAttachment(hash) {
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
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." };
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
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 { db } from "../database";
|
||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
@@ -35,9 +34,7 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
console.log("decrypting....");
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
@@ -46,20 +43,9 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
hash: filename,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
},
|
||||
cipherData.outputType === "base64"
|
||||
? isPng || isJpeg
|
||||
? "cache"
|
||||
: "base64"
|
||||
: "text"
|
||||
cipherData.outputType === "base64" ? "base64" : "text"
|
||||
);
|
||||
console.log("file decrypted...");
|
||||
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
|
||||
const dCachePath = `${cacheDir}/${output}`;
|
||||
output = await compressToBase64(
|
||||
`file://${dCachePath}`,
|
||||
isPng ? "PNG" : "JPEG"
|
||||
);
|
||||
}
|
||||
console.log("file decrypted...", attachment?.mimeType);
|
||||
|
||||
return output;
|
||||
} catch (e) {
|
||||
@@ -81,7 +67,7 @@ export async function hashBase64(data) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64({ data, key }) {
|
||||
export async function writeEncryptedBase64(data, key) {
|
||||
await createCacheDir();
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
@@ -91,6 +77,8 @@ export async function writeEncryptedBase64({ data, key }) {
|
||||
});
|
||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||
console.log("encrypted file output: ", output);
|
||||
output.size = output.length;
|
||||
delete output.length;
|
||||
return {
|
||||
...output,
|
||||
alg: "xcha-stream"
|
||||
@@ -190,10 +178,10 @@ export async function exists(filename) {
|
||||
}
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const totalChunks = Math.ceil(attachment.length / attachment.chunkSize);
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.length + totalAbytes;
|
||||
const expectedFileSize = attachment.size + totalAbytes;
|
||||
|
||||
const stat = await RNFetchBlob.fs.stat(
|
||||
existsInAppGroup ? appGroupPath : path
|
||||
|
||||
@@ -87,12 +87,9 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
DatabaseLogger.info(
|
||||
`File upload status: ${filename}, ${status}, ${text}`
|
||||
);
|
||||
let attachment = db.attachments.attachment(filename);
|
||||
let attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
if (
|
||||
!isImage(attachment.metadata.type) &&
|
||||
!isDocument(attachment.metadata?.type)
|
||||
) {
|
||||
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
} 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/>.
|
||||
*/
|
||||
|
||||
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 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 { getContainerBorder } from "../../utils/colors";
|
||||
|
||||
export const Announcement = ({ color }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return !announcement || selectionMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
@@ -45,7 +47,8 @@ export const Announcement = ({ color }) => {
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.secondary.background,
|
||||
paddingBottom: 12
|
||||
paddingBottom: 12,
|
||||
...getContainerBorder(colors.secondary.background)
|
||||
}}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -103,7 +103,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.sm}
|
||||
type="gray"
|
||||
type="plain"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
height={30}
|
||||
@@ -149,7 +149,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.xs}
|
||||
type="gray"
|
||||
type="plain"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
height={30}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const Title = ({ text, style = {}, inline }) => {
|
||||
</Heading>
|
||||
|
||||
<Button
|
||||
type="gray"
|
||||
type="plain"
|
||||
icon="close"
|
||||
height={null}
|
||||
onPress={() => {
|
||||
|
||||
@@ -18,14 +18,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { Platform, TextInput, View } from "react-native";
|
||||
import { AppStateStatus, Platform, TextInput, View } from "react-native";
|
||||
//@ts-ignore
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { db } from "../../common/database";
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { SIZE } from "../../utils/size";
|
||||
@@ -36,20 +46,67 @@ 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 = useUserStore((state) => state.user);
|
||||
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 (!(await BiometricService.isBiometryAvailable())) return;
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
!(await BiometricService.isBiometryAvailable())
|
||||
)
|
||||
return;
|
||||
|
||||
biometricUnlockAwaitingUserInput.current = true;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await NotesnookModule.getActivityName();
|
||||
if (activityName !== "MainActivity") return;
|
||||
@@ -65,17 +122,29 @@ const AppLockedOverlay = () => {
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
}
|
||||
biometricUnlockAwaitingUserInput.current = false;
|
||||
setTimeout(() => {
|
||||
biometricUnlockAwaitingUserInput.current = false;
|
||||
if (biometricUnlockAwaitingUserInput.current) return;
|
||||
useSettingStore.getState().setRequestBiometrics(false);
|
||||
}, 1);
|
||||
}, [lockApp]);
|
||||
}, 500);
|
||||
}, [biometricsAuthEnabled, lockApp]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!password.current) return;
|
||||
try {
|
||||
const unlocked = await db.user.verifyPassword(password.current);
|
||||
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;
|
||||
@@ -86,120 +155,161 @@ const AppLockedOverlay = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (appState === "active") {
|
||||
enabled(false);
|
||||
}
|
||||
|
||||
const prevState = lastAppState.current;
|
||||
lastAppState.current = appState;
|
||||
|
||||
if (
|
||||
biometricUnlockAwaitingUserInput.current ||
|
||||
useUserStore.getState().disableAppLockRequests
|
||||
)
|
||||
return;
|
||||
if (appLocked && appState === "active") {
|
||||
biometricUnlockAwaitingUserInput.current = true;
|
||||
onUnlockAppRequested();
|
||||
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 && (
|
||||
return appLocked ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
width:
|
||||
deviceMode !== "mobile"
|
||||
? "50%"
|
||||
: Platform.OS == "ios"
|
||||
? "95%"
|
||||
: "100%",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: 15,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<IconButton
|
||||
name="fingerprint"
|
||||
size={100}
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
width:
|
||||
deviceMode !== "mobile"
|
||||
? "50%"
|
||||
: Platform.OS == "ios"
|
||||
? "95%"
|
||||
: "100%",
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 30,
|
||||
marginTop: 15,
|
||||
alignSelf: "center"
|
||||
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"
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
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}
|
||||
|
||||
<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
|
||||
marginTop: user ? 25 : 25
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
{user || appLockHasPasswordSecurity ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
secureTextEntry
|
||||
placeholder="Enter account password"
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
<Button
|
||||
title="Continue"
|
||||
type="accent"
|
||||
onPress={onSubmit}
|
||||
width={250}
|
||||
height={45}
|
||||
style={{
|
||||
borderRadius: 150,
|
||||
marginBottom: 10
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
</>
|
||||
) : 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}
|
||||
|
||||
{biometricsAuthEnabled ? (
|
||||
<Button
|
||||
title="Unlock with Biometrics"
|
||||
width={250}
|
||||
@@ -207,12 +317,12 @@ const AppLockedOverlay = () => {
|
||||
icon={"fingerprint"}
|
||||
type="transparent"
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default AppLockedOverlay;
|
||||
|
||||
@@ -40,7 +40,8 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import {
|
||||
eCloseAttachmentDialog,
|
||||
eCloseSheet,
|
||||
eDBItemUpdate
|
||||
eDBItemUpdate,
|
||||
eOnLoadNote
|
||||
} from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
@@ -50,15 +51,21 @@ import { openNote } from "../list-items/note/wrapper";
|
||||
import { DateMeta } from "../properties/date-meta";
|
||||
import { Button } from "../ui/button";
|
||||
import { Notice } from "../ui/notice";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
close,
|
||||
setAttachments,
|
||||
fwdRef
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
|
||||
close?: () => void;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -76,7 +83,7 @@ const Actions = ({
|
||||
name: "Download",
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs().cancel(attachment.hash, "download");
|
||||
await db.fs().cancel(attachment.hash);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
}
|
||||
downloadAttachment(attachment.hash, false);
|
||||
@@ -99,7 +106,7 @@ const Actions = ({
|
||||
reupload: true,
|
||||
hash: attachment.hash,
|
||||
context: contextId,
|
||||
type: attachment.type
|
||||
type: attachment.mimeType.startsWith("image") ? "image" : "file"
|
||||
});
|
||||
},
|
||||
icon: "upload"
|
||||
@@ -110,7 +117,7 @@ const Actions = ({
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
});
|
||||
let res = await filesystem.checkAttachment(attachment.hash);
|
||||
const res = await filesystem.checkAttachment(attachment.hash);
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
@@ -130,6 +137,7 @@ const Actions = ({
|
||||
});
|
||||
}
|
||||
|
||||
setAttachments();
|
||||
setLoading({
|
||||
name: undefined
|
||||
});
|
||||
@@ -152,6 +160,7 @@ const Actions = ({
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
}
|
||||
},
|
||||
@@ -163,9 +172,27 @@ const Actions = ({
|
||||
{
|
||||
name: "Delete",
|
||||
onPress: async () => {
|
||||
const relations = await db.relations.to(attachment, "note").get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
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"
|
||||
}
|
||||
@@ -194,7 +221,7 @@ const Actions = ({
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
borderBottomColor: colors.primary.border,
|
||||
marginBottom: notes && notes.length > 0 ? 0 : 12
|
||||
}}
|
||||
>
|
||||
@@ -268,7 +295,7 @@ const Actions = ({
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
borderBottomColor: colors.primary.border,
|
||||
marginBottom: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
@@ -284,7 +311,7 @@ const Actions = ({
|
||||
</Heading>
|
||||
|
||||
{notes.map((item) => (
|
||||
<PressableButton
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
await sleep(150);
|
||||
@@ -292,7 +319,7 @@ const Actions = ({
|
||||
await sleep(300);
|
||||
openNote(item, (item as any).type === "trash");
|
||||
}}
|
||||
customStyle={{
|
||||
style={{
|
||||
paddingVertical: 12,
|
||||
alignItems: "flex-start",
|
||||
|
||||
@@ -301,7 +328,7 @@ const Actions = ({
|
||||
key={item.id}
|
||||
>
|
||||
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
||||
</PressableButton>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
</View>
|
||||
@@ -312,7 +339,7 @@ const Actions = ({
|
||||
key={item.name}
|
||||
buttonType={{
|
||||
text:
|
||||
item.name === "Delete" || item.name === "PermDelete"
|
||||
item.name === "Delete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
@@ -320,7 +347,7 @@ const Actions = ({
|
||||
title={item.name}
|
||||
icon={item.icon}
|
||||
loading={loading?.name === item.name}
|
||||
type="gray"
|
||||
type="plain"
|
||||
fontSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
@@ -350,12 +377,19 @@ const Actions = ({
|
||||
|
||||
Actions.present = (
|
||||
attachment: Attachment,
|
||||
set: (attachments: VirtualizedGrouping<Attachment>) => void,
|
||||
set: (attachments?: VirtualizedGrouping<Attachment>) => void,
|
||||
context?: string
|
||||
) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref) => <Actions fwdRef={ref} attachment={attachment} />
|
||||
component: (ref, close) => (
|
||||
<Actions
|
||||
fwdRef={ref}
|
||||
setAttachments={set}
|
||||
close={close}
|
||||
attachment={attachment}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -18,22 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import React, { useEffect, useState } from "react";
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
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";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
|
||||
function getFileExtension(filename: string) {
|
||||
var ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
const ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
return ext == null ? "" : ext[1];
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const AttachmentItem = ({
|
||||
hideWhenNotDownloading,
|
||||
context
|
||||
}: {
|
||||
id: string;
|
||||
id: string | number;
|
||||
attachments?: VirtualizedGrouping<Attachment>;
|
||||
encryption?: boolean;
|
||||
setAttachments: (attachments: any) => void;
|
||||
@@ -54,7 +54,7 @@ export const AttachmentItem = ({
|
||||
hideWhenNotDownloading?: boolean;
|
||||
context?: string;
|
||||
}) => {
|
||||
const [attachment] = useDBItem(id, "attachment", attachments?.item);
|
||||
const [attachment] = useDBItem(id, "attachment", attachments);
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
@@ -67,9 +67,8 @@ export const AttachmentItem = ({
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
|
||||
return (hideWhenNotDownloading &&
|
||||
(!currentProgress || !(currentProgress as any).value)) ||
|
||||
!attachment ? null : (
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
@@ -80,104 +79,111 @@ export const AttachmentItem = ({
|
||||
padding: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background
|
||||
backgroundColor: colors.secondary.background,
|
||||
minHeight: 45
|
||||
}}
|
||||
>
|
||||
<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 as any)?.type
|
||||
? "(" + (currentProgress as any).type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{currentProgress ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
db.fs().cancel(attachment.hash, currentProgress?.type as any);
|
||||
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 ? null : (
|
||||
<>
|
||||
{attachment.failed ? (
|
||||
<IconButton
|
||||
onPress={onPress}
|
||||
name="alert-circle-outline"
|
||||
color={colors.error.paragraph}
|
||||
/>
|
||||
) : 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>
|
||||
|
||||
@@ -63,7 +63,7 @@ const DownloadAttachments = ({
|
||||
canceled.current = false;
|
||||
groupId.current = Date.now().toString();
|
||||
const result = await downloadAttachments(
|
||||
attachments,
|
||||
await attachments.ids(),
|
||||
(progress: number, statusText: string) =>
|
||||
setProgress({ value: progress, statusText }),
|
||||
canceled,
|
||||
@@ -84,34 +84,27 @@ const DownloadAttachments = ({
|
||||
canceled.current = true;
|
||||
if (!groupId.current) return;
|
||||
console.log(groupId.current, "canceling groupId downloads");
|
||||
await db.fs().cancel(groupId.current, "download");
|
||||
await db.fs().cancel(groupId.current);
|
||||
setDownloading(false);
|
||||
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 results = [];
|
||||
for (let [key, value] of result.entries()) {
|
||||
if (value.status === 0) results.push(db.attachments.attachment(key));
|
||||
for (const value of result.values()) {
|
||||
if (value.status === 0) results.push(value.attachment);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
function getResultText() {
|
||||
const downloadedAttachmentsCount =
|
||||
attachments?.ids?.length - failedResults().length;
|
||||
attachments?.placeholders?.length - failedResults().length;
|
||||
if (downloadedAttachmentsCount === 0)
|
||||
return "Failed to download all attachments";
|
||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||
attachments?.ids.length
|
||||
attachments?.placeholders.length
|
||||
} attachments as a zip file at ${
|
||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||
}`;
|
||||
@@ -174,7 +167,9 @@ const DownloadAttachments = ({
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={
|
||||
progress.value ? progress.value / attachments.ids?.length : 0
|
||||
progress.value
|
||||
? progress.value / attachments.placeholders?.length
|
||||
: 0
|
||||
}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
@@ -192,7 +187,7 @@ const DownloadAttachments = ({
|
||||
borderRadius: 5,
|
||||
marginVertical: 12
|
||||
}}
|
||||
data={downloading ? attachments.ids : undefined}
|
||||
data={downloading ? attachments.placeholders : undefined}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
@@ -207,11 +202,11 @@ const DownloadAttachments = ({
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(item) => item as string}
|
||||
renderItem={({ item }) => {
|
||||
keyExtractor={(index) => "attachment_download" + index}
|
||||
renderItem={({ index }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
id={item as string}
|
||||
id={index}
|
||||
setAttachments={() => {}}
|
||||
pressable={false}
|
||||
hideWhenNotDownloading={true}
|
||||
@@ -251,7 +246,7 @@ const DownloadAttachments = ({
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="grayBg"
|
||||
type="secondary"
|
||||
title="No"
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -61,10 +61,21 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
|
||||
const refresh = React.useCallback(() => {
|
||||
if (note) {
|
||||
db.attachments.ofNote(note.id, "all").sorted(DEFAULT_SORTING);
|
||||
db.attachments
|
||||
.ofNote(note.id, "all")
|
||||
.sorted({
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => {
|
||||
setAttachments(attachments);
|
||||
});
|
||||
} else {
|
||||
db.attachments.all
|
||||
.sorted(DEFAULT_SORTING)
|
||||
.sorted({
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => setAttachments(attachments));
|
||||
}
|
||||
}, [note]);
|
||||
@@ -73,28 +84,32 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
refresh();
|
||||
}, [note, refresh]);
|
||||
|
||||
const onChangeText = (text: string) => {
|
||||
const onChangeText = async (text: string) => {
|
||||
attachmentSearchValue.current = text;
|
||||
if (
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
refresh();
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(async () => {
|
||||
let results = await db.lookup.attachments(
|
||||
attachmentSearchValue.current as string
|
||||
);
|
||||
const results = await db.lookup
|
||||
.attachments(attachmentSearchValue.current as string)
|
||||
.sorted();
|
||||
if (results.length === 0) return;
|
||||
setAttachments(results);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: string }) => (
|
||||
const renderItem = ({ index }: { item: boolean; index: number }) => (
|
||||
<AttachmentItem
|
||||
setAttachments={setAttachments}
|
||||
setAttachments={async () => {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}}
|
||||
attachments={attachments}
|
||||
id={item}
|
||||
id={index}
|
||||
context="attachments-list"
|
||||
/>
|
||||
);
|
||||
@@ -102,15 +117,15 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
const onCheck = async () => {
|
||||
if (!attachments) return;
|
||||
setLoading(true);
|
||||
for (let id of attachments.ids) {
|
||||
const attachment = await attachments.item(id as string);
|
||||
if (!attachment) continue;
|
||||
|
||||
let result = await filesystem.checkAttachment(attachment.hash);
|
||||
for (let i = 0; i < attachments.placeholders.length; i++) {
|
||||
const attachment = (await attachments.item(i))?.item;
|
||||
if (!attachment) continue;
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (result.failed) {
|
||||
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
||||
} else {
|
||||
await db.attachments.markAsFailed(id as string, undefined);
|
||||
await db.attachments.markAsFailed(attachment.id, undefined);
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
@@ -145,7 +160,9 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
|
||||
switch (type) {
|
||||
case "all":
|
||||
items = db.attachments.all;
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: db.attachments.all;
|
||||
break;
|
||||
case "images":
|
||||
items = note
|
||||
@@ -154,19 +171,24 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
break;
|
||||
case "video":
|
||||
items = items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
? db.attachments.ofNote(note.id, "videos")
|
||||
: db.attachments.videos;
|
||||
break;
|
||||
case "audio":
|
||||
items = db.attachments.all;
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "audio")
|
||||
: db.attachments.all;
|
||||
break;
|
||||
case "documents":
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
? db.attachments.ofNote(note.id, "documents")
|
||||
: db.attachments.documents;
|
||||
}
|
||||
|
||||
return await items.sorted(DEFAULT_SORTING);
|
||||
return await items.sorted({
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -205,7 +227,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
) : (
|
||||
<IconButton
|
||||
name="check-all"
|
||||
customStyle={{
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
@@ -218,7 +240,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
|
||||
<IconButton
|
||||
name="download"
|
||||
customStyle={{
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
@@ -261,7 +283,9 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
>
|
||||
{attachmentTypes.map((item) => (
|
||||
<Button
|
||||
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
|
||||
type={
|
||||
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
|
||||
}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
style={{
|
||||
@@ -306,7 +330,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={attachments?.ids as string[]}
|
||||
data={attachments?.placeholders}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
|
||||
@@ -17,14 +17,16 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
@@ -34,7 +36,6 @@ import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
|
||||
export const AuthMode = {
|
||||
login: 0,
|
||||
@@ -68,6 +69,9 @@ const AuthModal = () => {
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
actionSheetRef.current?.hide();
|
||||
setCurrentAuthMode(AuthMode.login);
|
||||
setVisible(false);
|
||||
@@ -77,6 +81,11 @@ const AuthModal = () => {
|
||||
<BaseDialog
|
||||
overlayOpacity={0}
|
||||
statusBarTranslucent={false}
|
||||
onShow={() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}}
|
||||
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
|
||||
visible={true}
|
||||
onClose={close}
|
||||
@@ -86,6 +95,7 @@ const AuthModal = () => {
|
||||
transparent={false}
|
||||
animated={false}
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
@@ -148,7 +158,7 @@ const AuthModal = () => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="gray"
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
|
||||
@@ -26,12 +26,15 @@ import {
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
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 { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Seperator from "../ui/seperator";
|
||||
import { Dialog } from "../dialog";
|
||||
import BackupService from "../../services/backup";
|
||||
import { sleep } from "../../utils/time";
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const passwordInputRef = useRef();
|
||||
@@ -64,6 +67,9 @@ export const ChangePassword = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
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.changePassword(oldPassword.current, password.current);
|
||||
ToastManager.show({
|
||||
@@ -73,6 +79,8 @@ export const ChangePassword = () => {
|
||||
});
|
||||
setLoading(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
@@ -92,6 +100,7 @@ export const ChangePassword = () => {
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<DialogHeader
|
||||
title="Change password"
|
||||
paragraph="Enter your old and new passwords"
|
||||
@@ -129,7 +138,14 @@ export const ChangePassword = () => {
|
||||
/>
|
||||
|
||||
<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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ export const ForgotPassword = () => {
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
style={{
|
||||
width: 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" />
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import Sync from "../../services/sync";
|
||||
import { Notice } from "../ui/notice";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -103,7 +104,7 @@ export const Login = ({ changeMode }) => {
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
@@ -222,7 +223,7 @@ export const Login = ({ changeMode }) => {
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="gray"
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
@@ -37,6 +37,7 @@ import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Toast } from "../toast";
|
||||
@@ -46,7 +47,6 @@ import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
|
||||
function getObfuscatedEmail(email) {
|
||||
if (!email) return "";
|
||||
@@ -69,6 +69,9 @@ export const SessionExpired = () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -84,6 +87,9 @@ export const SessionExpired = () => {
|
||||
MMKV.clearStore();
|
||||
clearAllStores();
|
||||
setVisible(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
heading: e.message,
|
||||
@@ -127,6 +133,9 @@ export const SessionExpired = () => {
|
||||
email.current = user.email;
|
||||
setFocused(false);
|
||||
setVisible(true);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}
|
||||
}, [email]);
|
||||
|
||||
@@ -139,9 +148,15 @@ export const SessionExpired = () => {
|
||||
animated={false}
|
||||
centered={false}
|
||||
onShow={async () => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
await sleep(300);
|
||||
passwordInputRef.current?.focus();
|
||||
setFocused(true);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}}
|
||||
enableSheetKeyboardHandler={true}
|
||||
visible={true}
|
||||
@@ -166,7 +181,7 @@ export const SessionExpired = () => {
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
style={{
|
||||
width: 60,
|
||||
height: 60
|
||||
}}
|
||||
|
||||
@@ -111,7 +111,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
|
||||
@@ -32,7 +32,7 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -151,7 +151,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
style={{
|
||||
width: 70,
|
||||
height: 70
|
||||
}}
|
||||
@@ -183,7 +183,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
type={seconds ? "gray" : "transparent"}
|
||||
type={seconds ? "plain" : "transparent"}
|
||||
title={
|
||||
sending
|
||||
? ""
|
||||
@@ -247,7 +247,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
|
||||
<Button
|
||||
title={secondaryMethodsText[currentMethod.method]}
|
||||
type="gray"
|
||||
type="plain"
|
||||
onPress={onRequestSecondaryMethod}
|
||||
height={30}
|
||||
/>
|
||||
@@ -255,7 +255,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
) : (
|
||||
<>
|
||||
{getMethods().map((item) => (
|
||||
<PressableButton
|
||||
<Pressable
|
||||
key={item.title}
|
||||
onPress={() => {
|
||||
setCurrentMethod({
|
||||
@@ -263,7 +263,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
isPrimary: false
|
||||
});
|
||||
}}
|
||||
customStyle={{
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
marginTop: 0,
|
||||
@@ -275,8 +275,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
type="grayAccent"
|
||||
customStyle={{
|
||||
type="secondaryAccented"
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
marginRight: 10
|
||||
@@ -292,7 +292,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
>
|
||||
<Paragraph size={SIZE.md}>{item.title}</Paragraph>
|
||||
</View>
|
||||
</PressableButton>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Keyboard, View } from "react-native";
|
||||
import Animated, {
|
||||
@@ -34,7 +35,7 @@ import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { SIZE, normalize } from "../../utils/size";
|
||||
import NativeTooltip from "../../utils/tooltip";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
|
||||
interface FloatingButtonProps {
|
||||
title: string;
|
||||
@@ -54,6 +55,7 @@ const FloatingButton = ({
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const translate = useSharedValue(0);
|
||||
const route = useRoute();
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
@@ -116,11 +118,11 @@ const FloatingButton = ({
|
||||
animatedStyle
|
||||
]}
|
||||
>
|
||||
<PressableButton
|
||||
<Pressable
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={color}
|
||||
customStyle={{
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
}}
|
||||
@@ -140,12 +142,18 @@ const FloatingButton = ({
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={title === "Clear all trash" ? "delete" : "plus"}
|
||||
name={
|
||||
route.name === "Notebooks"
|
||||
? "notebook-plus"
|
||||
: route.name === "Trash"
|
||||
? "delete"
|
||||
: "plus"
|
||||
}
|
||||
color={colors.primary.accentForeground}
|
||||
size={SIZE.xxl}
|
||||
/>
|
||||
</View>
|
||||
</PressableButton>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,28 +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/>.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { KeyboardAvoidingView, Platform } from "react-native";
|
||||
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
export const Container = ({ children }: PropsWithChildren) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const { colors } = useThemeColors();
|
||||
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 (
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
enabled={Platform.OS === "ios" && !floating}
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
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);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function DelayLayout({
|
||||
}: IDelayLayoutProps) {
|
||||
const { colors } = useThemeColors();
|
||||
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"];
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { AnnouncementDialog } from "../announcements";
|
||||
import AuthModal from "../auth/auth-modal";
|
||||
import { SessionExpired } from "../auth/session-expired";
|
||||
@@ -37,13 +36,14 @@ import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const loading = useNoteStore((state) => state.loading);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppLockPassword />
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<PremiumDialog colors={colors} />
|
||||
@@ -57,7 +57,7 @@ const DialogProvider = () => {
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
{loading ? null : <Expiring />}
|
||||
<Expiring />
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
|
||||
@@ -86,7 +86,9 @@ const BaseDialog = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (SettingsService.get().appLockMode === "background") {
|
||||
if (useUserStore.getState().disableAppLockRequests) return;
|
||||
|
||||
if (SettingsService.canLockAppInBackground()) {
|
||||
if (appState === "background") {
|
||||
setIntervalVisible(false);
|
||||
if (useUserStore.getState().appLocked) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
|
||||
const DialogButtons = ({
|
||||
onPressPositive,
|
||||
@@ -35,7 +36,7 @@ const DialogButtons = ({
|
||||
doneText,
|
||||
positiveType
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { colors, isDark } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -44,9 +45,15 @@ const DialogButtons = ({
|
||||
{
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 60,
|
||||
borderBottomRightRadius: 10,
|
||||
borderBottomLeftRadius: 10,
|
||||
paddingHorizontal: 12
|
||||
borderBottomRightRadius: 5,
|
||||
borderBottomLeftRadius: 5,
|
||||
paddingHorizontal: 12,
|
||||
borderTopWidth: 0.7,
|
||||
borderTopColor: getColorLinearShade(
|
||||
colors.secondary.background,
|
||||
0.05,
|
||||
isDark
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -80,7 +87,7 @@ const DialogButtons = ({
|
||||
onPress={onPressNegative}
|
||||
fontSize={SIZE.md}
|
||||
testID={notesnook.ids.default.dialog.no}
|
||||
type="gray"
|
||||
type="plain"
|
||||
bold
|
||||
title={negativeTitle}
|
||||
/>
|
||||
|
||||
@@ -18,12 +18,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
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();
|
||||
|
||||
return (
|
||||
@@ -35,7 +44,8 @@ const DialogContainer = ({ width, height, ...restProps }) => {
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
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 { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { PressableButtonProps } from "../ui/pressable";
|
||||
import { PressableProps } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
@@ -34,7 +34,7 @@ type DialogHeaderProps = {
|
||||
onPress?: () => void;
|
||||
loading?: boolean;
|
||||
title?: string;
|
||||
type?: PressableButtonProps["type"];
|
||||
type?: PressableProps["type"];
|
||||
icon?: string;
|
||||
};
|
||||
paragraphColor?: string;
|
||||
@@ -86,7 +86,9 @@ const DialogHeader = ({
|
||||
>
|
||||
{title}{" "}
|
||||
{titlePart ? (
|
||||
<Text style={{ color: colors.primary.accent }}>{titlePart}</Text>
|
||||
<Text style={{ color: colors.primary.accent }}>
|
||||
{titlePart}
|
||||
</Text>
|
||||
) : null}
|
||||
</Heading>
|
||||
|
||||
@@ -101,8 +103,8 @@ const DialogHeader = ({
|
||||
fontSize={13}
|
||||
title={button.title}
|
||||
icon={button.icon}
|
||||
type={button.type || "grayBg"}
|
||||
height={25}
|
||||
type={button.type || "secondary"}
|
||||
height={30}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -29,8 +29,8 @@ type DialogInfo = {
|
||||
onClose?: () => void;
|
||||
positiveType?:
|
||||
| "transparent"
|
||||
| "gray"
|
||||
| "grayBg"
|
||||
| "plain"
|
||||
| "secondary"
|
||||
| "accent"
|
||||
| "inverted"
|
||||
| "shade"
|
||||
@@ -41,7 +41,9 @@ type DialogInfo = {
|
||||
input: boolean;
|
||||
inputPlaceholder: 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 {
|
||||
|
||||
@@ -36,11 +36,15 @@ import DialogButtons from "./dialog-buttons";
|
||||
import DialogHeader from "./dialog-header";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(null);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const values = useRef({
|
||||
inputValue: undefined
|
||||
});
|
||||
const inputRef = useRef();
|
||||
const [dialogInfo, setDialogInfo] = useState({
|
||||
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 () => {
|
||||
if (dialogInfo.positivePress) {
|
||||
inputRef.current?.blur();
|
||||
let result = await dialogInfo.positivePress(
|
||||
inputValue || dialogInfo.defaultValue
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
checked
|
||||
);
|
||||
if (result === false) {
|
||||
return;
|
||||
@@ -91,14 +86,26 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(false);
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
setInputValue(data.defaultValue);
|
||||
},
|
||||
[context]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenSimpleDialog, show);
|
||||
eUnSubscribeEvent(eCloseSimpleDialog, hide);
|
||||
};
|
||||
}, [show]);
|
||||
|
||||
const hide = () => {
|
||||
setInputValue(null);
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
@@ -106,7 +113,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (dialogInfo.onClose) {
|
||||
await dialogInfo.onClose();
|
||||
}
|
||||
|
||||
hide();
|
||||
};
|
||||
|
||||
@@ -116,7 +122,8 @@ export const Dialog = ({ context = "global" }) => {
|
||||
maxHeight: 450,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
paddingTop: 12,
|
||||
...getContainerBorder(colors.primary.border, 0.5)
|
||||
};
|
||||
|
||||
return visible ? (
|
||||
@@ -159,7 +166,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
setInputValue(value);
|
||||
values.current.inputValue = value;
|
||||
}}
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
@@ -176,10 +183,10 @@ export const Dialog = ({ context = "global" }) => {
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setInputValue(!inputValue);
|
||||
setChecked(!checked);
|
||||
}}
|
||||
icon={
|
||||
inputValue
|
||||
checked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
@@ -187,9 +194,10 @@ export const Dialog = ({ context = "global" }) => {
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
height={35}
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={inputValue ? dialogInfo.check.type : "gray"}
|
||||
type={checked ? dialogInfo.check.type || "selected" : "plain"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
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 BiometicService 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 BiometicService.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;
|
||||
@@ -26,7 +26,7 @@ import React, {
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { FlatList, ScrollView, View } from "react-native";
|
||||
import { ActivityIndicator, FlatList, ScrollView, View } from "react-native";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
} from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
const JumpToSectionDialog = () => {
|
||||
@@ -51,19 +51,20 @@ const JumpToSectionDialog = () => {
|
||||
const notes = data;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const offsets = useRef<number[]>([]);
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
const currentScrollPosition = useRef(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onPress = (item: GroupHeader) => {
|
||||
const index = notes?.ids?.findIndex((i) => {
|
||||
if (typeof i === "object") {
|
||||
return i.title === item.title && i.type === "header";
|
||||
} else {
|
||||
false;
|
||||
}
|
||||
});
|
||||
const [groups, setGroups] = useState<
|
||||
{
|
||||
index: number;
|
||||
group: GroupHeader;
|
||||
}[]
|
||||
>();
|
||||
const offsets = useRef<number[]>([]);
|
||||
|
||||
const onPress = (item: { index: number; group: GroupHeader }) => {
|
||||
scrollRef.current?.current?.scrollToIndex({
|
||||
index: index as number,
|
||||
index: item.index,
|
||||
animated: true
|
||||
});
|
||||
close();
|
||||
@@ -77,6 +78,7 @@ const JumpToSectionDialog = () => {
|
||||
data: VirtualizedGrouping<Item>;
|
||||
ref: RefObject<FlatList>;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
setData(data);
|
||||
scrollRef.current = ref;
|
||||
setVisible(true);
|
||||
@@ -97,55 +99,45 @@ const JumpToSectionDialog = () => {
|
||||
}, [open]);
|
||||
|
||||
const onScroll = (data: { x: number; y: number }) => {
|
||||
const y = data.y;
|
||||
if (timeout) {
|
||||
clearTimeout(timeout.current);
|
||||
timeout.current = undefined;
|
||||
}
|
||||
timeout.current = setTimeout(() => {
|
||||
setCurrentIndex(
|
||||
offsets.current?.findIndex(
|
||||
(o, i) => o <= y && offsets.current[i + 1] > y
|
||||
) || 0
|
||||
);
|
||||
}, 200);
|
||||
currentScrollPosition.current = data.y;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const loadOffsets = useCallback(() => {
|
||||
notes?.ids
|
||||
.filter((i) => typeof i === "object" && i.type === "header")
|
||||
.map((item, index) => {
|
||||
if (typeof item === "string") return;
|
||||
|
||||
const loadGroupsAndOffsets = useCallback(() => {
|
||||
notes?.groups?.().then((groups) => {
|
||||
setGroups(groups);
|
||||
offsets.current = [];
|
||||
groups.map((item, index) => {
|
||||
let offset = 35 * index;
|
||||
let ind = notes.ids.findIndex(
|
||||
(i) =>
|
||||
typeof i === "object" &&
|
||||
i.title === item.title &&
|
||||
i.type === "header"
|
||||
);
|
||||
let groupIndex = item.index;
|
||||
const messageState = useMessageStore.getState().message;
|
||||
const msgOffset = messageState?.visible ? 60 : 10;
|
||||
|
||||
ind = ind + 1;
|
||||
ind = ind - (index + 1);
|
||||
offset = offset + ind * 100 + msgOffset;
|
||||
groupIndex = groupIndex + 1;
|
||||
groupIndex = groupIndex - (index + 1);
|
||||
offset = offset + groupIndex * 100 + msgOffset;
|
||||
offsets.current.push(offset);
|
||||
});
|
||||
}, [notes]);
|
||||
|
||||
useEffect(() => {
|
||||
loadOffsets();
|
||||
}, [loadOffsets, notes]);
|
||||
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]);
|
||||
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
onShow={() => {
|
||||
loadOffsets();
|
||||
loadGroupsAndOffsets();
|
||||
}}
|
||||
onRequestClose={close}
|
||||
visible={true}
|
||||
@@ -164,35 +156,42 @@ const JumpToSectionDialog = () => {
|
||||
paddingTop: 30
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
style={{
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
size={SIZE.lg}
|
||||
color={colors.primary.accent}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
paddingBottom: 20
|
||||
marginBottom: 20
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ScrollView
|
||||
style={{
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
>
|
||||
{notes?.ids
|
||||
.filter((i) => typeof i === "object" && i.type === "header")
|
||||
.map((item, index) => {
|
||||
return typeof item === "object" && item.title ? (
|
||||
<PressableButton
|
||||
key={item.title}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
paddingBottom: 20
|
||||
}}
|
||||
>
|
||||
{groups?.map((item, index) => {
|
||||
return (
|
||||
<Pressable
|
||||
key={item.group.id}
|
||||
onPress={() => onPress(item)}
|
||||
type={currentIndex === index ? "selected" : "transparent"}
|
||||
customStyle={{
|
||||
type={currentIndex === index ? "accent" : "transparent"}
|
||||
style={{
|
||||
minWidth: "20%",
|
||||
width: null,
|
||||
paddingHorizontal: 12,
|
||||
margin: 5,
|
||||
borderRadius: 100,
|
||||
height: 25,
|
||||
height: 30,
|
||||
marginVertical: 10
|
||||
}}
|
||||
>
|
||||
@@ -200,20 +199,21 @@ const JumpToSectionDialog = () => {
|
||||
size={SIZE.sm}
|
||||
color={
|
||||
currentIndex === index
|
||||
? colors.selected.accent
|
||||
: colors.primary.accent
|
||||
? colors.static.white
|
||||
: colors.primary.paragraph
|
||||
}
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
{item.group.title}
|
||||
</Paragraph>
|
||||
</PressableButton>
|
||||
) : null;
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
</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/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Dimensions, TextInput, View } from "react-native";
|
||||
import {
|
||||
addOrientationListener,
|
||||
@@ -85,7 +85,7 @@ const PDFPreview = () => {
|
||||
return () => {
|
||||
eUnSubscribeEvent("PDFPreview", open);
|
||||
};
|
||||
}, []);
|
||||
}, [open]);
|
||||
|
||||
const onOrientationChange = (o) => {
|
||||
if (o.includes("LANDSCAPE")) {
|
||||
@@ -102,23 +102,26 @@ const PDFPreview = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const open = async (attachment) => {
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
setAttachment(attachment);
|
||||
let hash = attachment.metadata.hash;
|
||||
if (!hash) return;
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true
|
||||
});
|
||||
const path = `${cacheDir}/${uri}`;
|
||||
snapshotValue.current = snapshot.current;
|
||||
setPDFSource("file://" + path);
|
||||
setLoading(false);
|
||||
}, 100);
|
||||
};
|
||||
const open = useCallback(
|
||||
async (attachment) => {
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
setAttachment(attachment);
|
||||
let hash = attachment.hash;
|
||||
if (!hash) return;
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true
|
||||
});
|
||||
const path = `${cacheDir}/${uri}`;
|
||||
snapshotValue.current = snapshot.current;
|
||||
setPDFSource("file://" + path);
|
||||
setLoading(false);
|
||||
}, 100);
|
||||
},
|
||||
[snapshot]
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
setPDFSource(null);
|
||||
@@ -130,7 +133,7 @@ const PDFPreview = () => {
|
||||
if (error?.message === "Password required or incorrect password.") {
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
context: attachment?.metadata?.hash,
|
||||
context: attachment?.hash,
|
||||
input: true,
|
||||
inputPlaceholder: "Enter password",
|
||||
positiveText: "Unlock",
|
||||
@@ -151,8 +154,8 @@ const PDFPreview = () => {
|
||||
return (
|
||||
visible && (
|
||||
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
|
||||
<SheetProvider context={attachment?.metadata?.hash} />
|
||||
<Dialog context={attachment?.metadata?.hash} />
|
||||
<SheetProvider context={attachment?.hash} />
|
||||
<Dialog context={attachment?.hash} />
|
||||
|
||||
<View
|
||||
style={{
|
||||
@@ -208,7 +211,7 @@ const PDFPreview = () => {
|
||||
color={colors.static.white}
|
||||
name="arrow-left"
|
||||
onPress={close}
|
||||
customStyle={{
|
||||
style={{
|
||||
marginRight: 12
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
@@ -261,7 +264,7 @@ const PDFPreview = () => {
|
||||
color={colors.static.white}
|
||||
name="download"
|
||||
onPress={() => {
|
||||
downloadAttachment(attachment.metadata.hash, false);
|
||||
downloadAttachment(attachment.hash, false);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -23,7 +23,6 @@ import { InteractionManager, View } from "react-native";
|
||||
import Share from "react-native-share";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import BiometricService from "../../../services/biometrics";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
@@ -35,11 +34,11 @@ import {
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import {
|
||||
eClearEditor,
|
||||
eCloseActionSheet,
|
||||
eCloseVaultDialog,
|
||||
eOnLoadNote,
|
||||
eOpenVaultDialog
|
||||
eOpenVaultDialog,
|
||||
eUpdateNoteInEditor
|
||||
} from "../../../utils/events";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
@@ -81,7 +80,8 @@ export class VaultDialog extends Component {
|
||||
description: null,
|
||||
clearVault: false,
|
||||
deleteVault: false,
|
||||
deleteAll: false
|
||||
deleteAll: false,
|
||||
noteLocked: false
|
||||
};
|
||||
|
||||
this.passInputRef = createRef();
|
||||
@@ -99,7 +99,7 @@ export class VaultDialog extends Component {
|
||||
? "Revoke Vault Fingerprint Unlock"
|
||||
: this.state.changePassword
|
||||
? "Change Vault Password"
|
||||
: this.state.note.locked
|
||||
: this.state.noteLocked
|
||||
? this.state.deleteNote
|
||||
? "Delete note"
|
||||
: this.state.share
|
||||
@@ -120,7 +120,7 @@ export class VaultDialog extends Component {
|
||||
? "Setup a new password for the vault."
|
||||
: this.state.permanant
|
||||
? "Enter password to remove note from vault."
|
||||
: this.state.note.locked
|
||||
: this.state.noteLocked
|
||||
? this.state.deleteNote
|
||||
? "Unlock note to delete it. If biometrics are not working, you can enter device pin to unlock vault."
|
||||
: this.state.share
|
||||
@@ -174,7 +174,8 @@ export class VaultDialog extends Component {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
clearVault: data.clearVault,
|
||||
deleteVault: data.deleteVault
|
||||
deleteVault: data.deleteVault,
|
||||
noteLocked: data.item && (await db.vaults.itemExists(data.item))
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -240,16 +241,6 @@ export class VaultDialog extends Component {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.password && this.password.length < 3) {
|
||||
ToastManager.show({
|
||||
heading: "Password too short",
|
||||
message: "Password must be longer than 3 characters.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.novault) {
|
||||
if (this.password !== this.confirmPassword) {
|
||||
@@ -314,7 +305,7 @@ export class VaultDialog extends Component {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.state.note.locked) {
|
||||
if (this.state.noteLocked) {
|
||||
await this._unlockNote();
|
||||
} else {
|
||||
db.vault
|
||||
@@ -346,7 +337,26 @@ export class VaultDialog extends Component {
|
||||
let verified = await db.user.verifyPassword(this.password);
|
||||
if (!(await db.user.getUser())) verified = true;
|
||||
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);
|
||||
|
||||
noteIds.forEach((id) => {
|
||||
eSendEvent(
|
||||
eUpdateNoteInEditor,
|
||||
{
|
||||
id: id,
|
||||
deleted: true
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
eSendEvent("vaultUpdated");
|
||||
this.setState({
|
||||
loading: false
|
||||
@@ -373,7 +383,22 @@ export class VaultDialog extends Component {
|
||||
loading: true
|
||||
});
|
||||
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);
|
||||
|
||||
noteIds.forEach((id) => {
|
||||
eSendEvent(
|
||||
eUpdateNoteInEditor,
|
||||
{
|
||||
id: id,
|
||||
deleted: true
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
@@ -402,9 +427,10 @@ export class VaultDialog extends Component {
|
||||
return;
|
||||
} else {
|
||||
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();
|
||||
ToastManager.show({
|
||||
message: "Note locked successfully",
|
||||
@@ -509,9 +535,7 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
if (this.state.note?.id) {
|
||||
await db.vault.add(this.state.note.id);
|
||||
if (this.state.note.id === editorController.current?.note?.id) {
|
||||
eSendEvent(eClearEditor);
|
||||
}
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
@@ -541,6 +565,7 @@ export class VaultDialog extends Component {
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
this.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -866,10 +891,10 @@ export class VaultDialog extends Component {
|
||||
title="Biometric unlocking"
|
||||
iconColor={
|
||||
this.state.biometricUnlock
|
||||
? colors.selected.icon
|
||||
? colors.selected.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
type={this.state.biometricUnlock ? "transparent" : "selected"}
|
||||
type={this.state.biometricUnlock ? "transparent" : "plain"}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -892,7 +917,7 @@ export class VaultDialog extends Component {
|
||||
? "Revoke"
|
||||
: changePassword
|
||||
? "Change"
|
||||
: note.locked
|
||||
: this.state.noteLocked
|
||||
? deleteNote
|
||||
? "Delete"
|
||||
: share
|
||||
|
||||
@@ -54,7 +54,7 @@ export const LeftMenus = ({
|
||||
return isTablet && !canGoBack ? null : (
|
||||
<IconButton
|
||||
testID={notesnook.ids.default.header.buttons.left}
|
||||
customStyle={{
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 40,
|
||||
|
||||
@@ -23,8 +23,6 @@ import { Platform, StyleSheet, View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Menu from "react-native-reanimated-material-menu";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SearchService from "../../services/search";
|
||||
import {
|
||||
HeaderRightButton,
|
||||
RouteName
|
||||
@@ -63,7 +61,7 @@ export const RightMenus = ({
|
||||
testID="icon-search"
|
||||
name="magnify"
|
||||
color={colors.primary.paragraph}
|
||||
customStyle={styles.rightBtn}
|
||||
style={styles.rightBtn}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -73,7 +71,7 @@ export const RightMenus = ({
|
||||
testID={notesnook.ids.default.addBtn}
|
||||
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
|
||||
iconSize={SIZE.xl}
|
||||
type="shade"
|
||||
type="accent"
|
||||
hitSlop={{
|
||||
top: 10,
|
||||
right: 10,
|
||||
@@ -84,7 +82,7 @@ export const RightMenus = ({
|
||||
marginLeft: 10,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 5,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.accent
|
||||
@@ -110,7 +108,7 @@ export const RightMenus = ({
|
||||
}}
|
||||
name="dots-vertical"
|
||||
color={colors.primary.paragraph}
|
||||
customStyle={styles.rightBtn}
|
||||
style={styles.rightBtn}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -121,7 +119,7 @@ export const RightMenus = ({
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 0
|
||||
}}
|
||||
type="gray"
|
||||
type="plain"
|
||||
buttonType={{
|
||||
text: contextMenuColors.primary.paragraph
|
||||
}}
|
||||
|
||||
@@ -49,6 +49,8 @@ export const Title = ({
|
||||
const isTag = title.startsWith("#");
|
||||
const onScroll = useCallback(
|
||||
(data: { x: number; y: number; id?: string; route: string }) => {
|
||||
if (data.route !== "Notebook") return;
|
||||
|
||||
if (data.route !== renderedInRoute || data.id !== id) return;
|
||||
if (data.y > 150) {
|
||||
if (!visible) return;
|
||||
|
||||
@@ -52,7 +52,7 @@ const ImagePreview = () => {
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
let hash = image.hash;
|
||||
if (!hash && dataurl.toObject(image.src)) {
|
||||
if (!hash && image.src && dataurl.toObject(image.src)) {
|
||||
const data = dataurl.toObject(image.src);
|
||||
if (!data) return;
|
||||
hash = await Sodium.hashFile({
|
||||
|
||||
@@ -36,6 +36,10 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { eOpenLoginDialog } from "../../utils/events";
|
||||
import { AuthMode } from "../auth";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
|
||||
const Intro = ({ navigation }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -193,9 +197,15 @@ const Intro = ({ navigation }) => {
|
||||
<Button
|
||||
width={250}
|
||||
onPress={async () => {
|
||||
navigation.navigate("AppLock", {
|
||||
welcome: true
|
||||
});
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
}, 1000);
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
|
||||
@@ -108,7 +108,7 @@ export const WelcomeNotice = () => {
|
||||
eSendEvent(eCloseLoading);
|
||||
}}
|
||||
iconSize={20}
|
||||
type="gray"
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
|
||||
@@ -109,12 +109,12 @@ export const NotebookHeader = ({
|
||||
name={isPinnedToMenu ? "link-variant-off" : "link-variant"}
|
||||
onPress={onPinNotebook}
|
||||
tooltipText={"Create shortcut in side menu"}
|
||||
customStyle={{
|
||||
style={{
|
||||
marginRight: 15,
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
type={isPinnedToMenu ? "grayBg" : "grayBg"}
|
||||
type={isPinnedToMenu ? "secondary" : "secondary"}
|
||||
color={isPinnedToMenu ? colors.primary.accent : colors.primary.icon}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
@@ -123,9 +123,9 @@ export const NotebookHeader = ({
|
||||
onPress={onEditNotebook}
|
||||
tooltipText="Edit this notebook"
|
||||
name="pencil"
|
||||
type="grayBg"
|
||||
type="secondary"
|
||||
color={colors.primary.icon}
|
||||
customStyle={{
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
@@ -148,10 +148,6 @@ export const NotebookHeader = ({
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{/* {notebook.topics.length === 1
|
||||
? "1 topic"
|
||||
: `${notebook.topics.length} topics`}
|
||||
,{" "} */}
|
||||
{notebook && totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enab
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { RouteName } from "../../../stores/use-navigation-store";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { getContainerBorder } from "../../../utils/colors";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import Sort from "../../sheets/sort";
|
||||
@@ -80,7 +80,8 @@ export const SectionHeader = React.memo<
|
||||
backgroundColor: colors.secondary.background,
|
||||
alignSelf: "center",
|
||||
borderRadius: 5,
|
||||
marginVertical: 5
|
||||
marginVertical: 5,
|
||||
...getContainerBorder(colors.secondary.background, 0.8)
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
@@ -95,13 +96,9 @@ export const SectionHeader = React.memo<
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
color={
|
||||
ColorValues[color?.toLowerCase() as keyof typeof ColorValues] ||
|
||||
colors.primary.accent
|
||||
}
|
||||
color={color || colors.primary.accent}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
minWidth: 60,
|
||||
alignSelf: "center",
|
||||
textAlignVertical: "center"
|
||||
}}
|
||||
@@ -116,7 +113,7 @@ export const SectionHeader = React.memo<
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{index === 0 && (
|
||||
{index === 0 ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
@@ -125,7 +122,6 @@ export const SectionHeader = React.memo<
|
||||
component: <Sort screen={screen} type={dataType} />
|
||||
});
|
||||
}}
|
||||
hidden={screen === "Reminders"}
|
||||
tooltipText="Change sorting of items in list"
|
||||
title={groupBy}
|
||||
icon={
|
||||
@@ -145,12 +141,12 @@ export const SectionHeader = React.memo<
|
||||
? 10
|
||||
: 0
|
||||
}}
|
||||
type="gray"
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
customStyle={{
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25
|
||||
}}
|
||||
@@ -179,7 +175,7 @@ export const SectionHeader = React.memo<
|
||||
size={SIZE.lg - 2}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -33,14 +33,12 @@ import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import NotebookScreen from "../../../screens/notebook";
|
||||
import { TaggedNotes } from "../../../screens/notes/tagged";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import {
|
||||
NotebooksWithDateEdited,
|
||||
TagsWithDateEdited
|
||||
} from "../../list/list-item.wrapper";
|
||||
|
||||
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||
import { Properties } from "../../properties";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
@@ -60,6 +58,7 @@ type NoteItemProps = {
|
||||
date: number;
|
||||
isTrash?: boolean;
|
||||
noOpen?: boolean;
|
||||
locked?: boolean;
|
||||
};
|
||||
|
||||
const NoteItem = ({
|
||||
@@ -71,18 +70,19 @@ const NoteItem = ({
|
||||
reminder,
|
||||
tags,
|
||||
attachmentsCount,
|
||||
noOpen
|
||||
locked,
|
||||
noOpen = false
|
||||
}: NoteItemProps) => {
|
||||
const currentEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote
|
||||
const isEditingNote = useTabStore(
|
||||
(state) =>
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
(item as TrashItem).itemType || item.type
|
||||
);
|
||||
const _update = useRelationStore((state) => state.updater);
|
||||
const primaryColors =
|
||||
currentEditingNote === item.id ? colors.selected : colors.primary;
|
||||
const primaryColors = isEditingNote ? colors.selected : colors.primary;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -118,8 +118,8 @@ const NoteItem = ({
|
||||
tooltipText={item.title}
|
||||
key={item.id}
|
||||
height={25}
|
||||
icon={item.type === "notebook" ? "bookmark" : "book-outline"}
|
||||
type="grayBg"
|
||||
icon="book-outline"
|
||||
type="secondary"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
textStyle={{
|
||||
@@ -128,8 +128,6 @@ const NoteItem = ({
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
marginRight: 5,
|
||||
borderWidth: 0.5,
|
||||
borderColor: primaryColors.border,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
@@ -156,10 +154,10 @@ const NoteItem = ({
|
||||
<Paragraph
|
||||
numberOfLines={1}
|
||||
color={color?.colorCode || primaryColors.heading}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
paddingRight: 10
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
@@ -167,10 +165,10 @@ const NoteItem = ({
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
color={color?.colorCode || primaryColors.heading}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={SIZE.md}
|
||||
style={{
|
||||
paddingRight: 10
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
@@ -213,6 +211,31 @@ const NoteItem = ({
|
||||
color={colors.error.accent}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.localOnly ? (
|
||||
<Icon
|
||||
testID="sync-off"
|
||||
name="sync-off"
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.readonly ? (
|
||||
<Icon
|
||||
testID="pencil-lock"
|
||||
name="pencil-lock"
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TimeSince
|
||||
style={{
|
||||
fontSize: SIZE.xs,
|
||||
@@ -228,12 +251,13 @@ const NoteItem = ({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginRight: 6
|
||||
marginRight: 6,
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="attachment"
|
||||
size={SIZE.md}
|
||||
size={SIZE.sm}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
<Paragraph
|
||||
@@ -257,7 +281,7 @@ const NoteItem = ({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.locked ? (
|
||||
{locked ? (
|
||||
<Icon
|
||||
name="lock"
|
||||
testID="note-locked-icon"
|
||||
@@ -273,7 +297,7 @@ const NoteItem = ({
|
||||
<Icon
|
||||
testID="icon-star"
|
||||
name="star-outline"
|
||||
size={SIZE.md}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
@@ -288,9 +312,10 @@ const NoteItem = ({
|
||||
title={"#" + item.title}
|
||||
key={item.id}
|
||||
height={23}
|
||||
type="gray"
|
||||
type="plain"
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
textDecorationLine: "underline",
|
||||
color: colors.secondary.paragraph
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 12, left: 0, right: 0 }}
|
||||
fontSize={SIZE.xs}
|
||||
@@ -356,7 +381,7 @@ const NoteItem = ({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{item.locked ? (
|
||||
{locked ? (
|
||||
<Icon
|
||||
name="lock"
|
||||
testID="note-locked-icon"
|
||||
@@ -372,7 +397,7 @@ const NoteItem = ({
|
||||
<Icon
|
||||
testID="icon-star"
|
||||
name="star-outline"
|
||||
size={SIZE.md}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
@@ -398,7 +423,7 @@ const NoteItem = ({
|
||||
name="dots-horizontal"
|
||||
size={SIZE.xl}
|
||||
onPress={() => !noOpen && Properties.present(item)}
|
||||
customStyle={{
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
width: 35,
|
||||
|
||||