Compare commits
1106 Commits
feat/at-re
...
fix-notebo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac7a2e495d | ||
|
|
d99b6a0bdb | ||
|
|
7b5fa09c71 | ||
|
|
d85974ed7a | ||
|
|
9598ea7542 | ||
|
|
42048ad53a | ||
|
|
1fbd2efb92 | ||
|
|
fe91ef79a0 | ||
|
|
583d585b2f | ||
|
|
94b28f86ca | ||
|
|
f927f05f86 | ||
|
|
bde8dea048 | ||
|
|
511bea9b23 | ||
|
|
eda95530de | ||
|
|
5173decaa5 | ||
|
|
00dcb55f6e | ||
|
|
6dbe1073cd | ||
|
|
9be3e4fcfa | ||
|
|
f9f0a866bd | ||
|
|
daafeb2fa8 | ||
|
|
a92b34b81c | ||
|
|
e29b6838b3 | ||
|
|
16ff7c92e4 | ||
|
|
07e1ea6edf | ||
|
|
b26ede95fb | ||
|
|
971793a3ad | ||
|
|
6a2b5e1499 | ||
|
|
d18b775d06 | ||
|
|
d6cc899d95 | ||
|
|
ea10058276 | ||
|
|
568c88be3e | ||
|
|
f047552f18 | ||
|
|
9a58430b5d | ||
|
|
205885edf5 | ||
|
|
8ffb49b700 | ||
|
|
25b18a2edd | ||
|
|
cfea85c8fb | ||
|
|
82fc6d31c3 | ||
|
|
fb2de1753e | ||
|
|
ceb5ce5cdd | ||
|
|
98d443001c | ||
|
|
5115dee069 | ||
|
|
0dc29c7026 | ||
|
|
a5eb5e9df3 | ||
|
|
b680575328 | ||
|
|
c94ef616a5 | ||
|
|
3ae40c09fc | ||
|
|
5de4e00ce2 | ||
|
|
99e4e61a55 | ||
|
|
1ad7bfb82f | ||
|
|
30ef679432 | ||
|
|
bb97f5e757 | ||
|
|
6cd5a04470 | ||
|
|
0c32ebf163 | ||
|
|
e150e08f29 | ||
|
|
bbd603f10c | ||
|
|
677944cba9 | ||
|
|
dd2cee53d3 | ||
|
|
60c8d5ac89 | ||
|
|
59276de406 | ||
|
|
67981be516 | ||
|
|
77dfcec08c | ||
|
|
c4360ab95f | ||
|
|
17c4b72381 | ||
|
|
e91cc2bdca | ||
|
|
811f2305ec | ||
|
|
98a707b5f5 | ||
|
|
aa9b24fa10 | ||
|
|
0b3060e4c0 | ||
|
|
d523db84cc | ||
|
|
6a6a9ae6b3 | ||
|
|
b1bc28e069 | ||
|
|
f95c068bc1 | ||
|
|
0e4cd087d4 | ||
|
|
f217b8cfec | ||
|
|
d3787efbf0 | ||
|
|
e279878b12 | ||
|
|
154a227be0 | ||
|
|
756259c907 | ||
|
|
453ed4820a | ||
|
|
10ab612366 | ||
|
|
8dc4f7ed3d | ||
|
|
a73538bafc | ||
|
|
014887c7dc | ||
|
|
246e49bd8c | ||
|
|
146ac12935 | ||
|
|
4f5245adcf | ||
|
|
0bc0cc5729 | ||
|
|
1136072eff | ||
|
|
4cc8a538a4 | ||
|
|
271913ba20 | ||
|
|
3efe541869 | ||
|
|
b3bcdd4f9c | ||
|
|
13f739763c | ||
|
|
bde2b26133 | ||
|
|
6eef65952d | ||
|
|
3d85795216 | ||
|
|
f1a1989990 | ||
|
|
76ff5eddd0 | ||
|
|
068ee8dc68 | ||
|
|
bbf0fb641b | ||
|
|
3cae22faf1 | ||
|
|
34a4a27ba0 | ||
|
|
d96a78c741 | ||
|
|
c9e5ee9ddb | ||
|
|
f43c2a40f5 | ||
|
|
b12a0e979c | ||
|
|
d0372b82db | ||
|
|
dac1441add | ||
|
|
a65ebec7ae | ||
|
|
d5cf65e568 | ||
|
|
424073298e | ||
|
|
580c83687e | ||
|
|
de226a8ab6 | ||
|
|
8b94b08066 | ||
|
|
18c460a8b2 | ||
|
|
fbb3a56c0c | ||
|
|
48f602d4fe | ||
|
|
ad2b68c8ed | ||
|
|
c2eafccf2a | ||
|
|
5ca74b6127 | ||
|
|
143a38ade6 | ||
|
|
40cff39713 | ||
|
|
3efa1140b2 | ||
|
|
64f28aedbf | ||
|
|
74e3a28e2a | ||
|
|
58c324b2ac | ||
|
|
25fa2dc406 | ||
|
|
3e76f3b8c6 | ||
|
|
a12a3fbdc8 | ||
|
|
46c492c92a | ||
|
|
dbace71754 | ||
|
|
ece5b56bdd | ||
|
|
c35dd826dc | ||
|
|
675324f6af | ||
|
|
8788a14229 | ||
|
|
c8f697c5b1 | ||
|
|
48a4d67c13 | ||
|
|
e4289bb69e | ||
|
|
7000c67535 | ||
|
|
0eb61c80fa | ||
|
|
4bbd43e54b | ||
|
|
ce949d94f2 | ||
|
|
bdbf205745 | ||
|
|
899ab2ddb1 | ||
|
|
a302276af9 | ||
|
|
ee14a292ab | ||
|
|
646949c96c | ||
|
|
ebfb5c29dd | ||
|
|
3932ae62b0 | ||
|
|
f671d2dec0 | ||
|
|
6261f3f2c6 | ||
|
|
1b9802b8e0 | ||
|
|
ca95af7043 | ||
|
|
0aac2eb1e0 | ||
|
|
c10d083dab | ||
|
|
27c6cdec47 | ||
|
|
8fd0f97dfe | ||
|
|
59d65a2cbb | ||
|
|
9bd978b8c6 | ||
|
|
d6e22f2e3e | ||
|
|
a9a1290530 | ||
|
|
b8eab5bddc | ||
|
|
6836d6551f | ||
|
|
8e07ae8b26 | ||
|
|
b013a1cfa7 | ||
|
|
0650d00ca1 | ||
|
|
c048f789e7 | ||
|
|
9c158dfa53 | ||
|
|
168be74277 | ||
|
|
016532ef42 | ||
|
|
7368796a72 | ||
|
|
b3e10de0a1 | ||
|
|
dd674d38df | ||
|
|
a427e32796 | ||
|
|
b44d2e5c9b | ||
|
|
c42d046188 | ||
|
|
ae5f9f0f4a | ||
|
|
1681a587b9 | ||
|
|
b43d51aa72 | ||
|
|
f99d4bc013 | ||
|
|
430c2f51f6 | ||
|
|
b55b521def | ||
|
|
c26e2d800d | ||
|
|
310de9f950 | ||
|
|
9109c5180a | ||
|
|
db57c125d6 | ||
|
|
3ec177c631 | ||
|
|
fa561df14f | ||
|
|
b0abf56f20 | ||
|
|
a4ac051af0 | ||
|
|
7a2db59516 | ||
|
|
76c33e3cbc | ||
|
|
fd295b226a | ||
|
|
9bb82aebfe | ||
|
|
38c6dd3953 | ||
|
|
0517f61164 | ||
|
|
17289f2df6 | ||
|
|
44a7f057ac | ||
|
|
be5d0820d5 | ||
|
|
23bd4a564f | ||
|
|
be41cf1252 | ||
|
|
cf643fe0b0 | ||
|
|
988efa4577 | ||
|
|
631160c2c9 | ||
|
|
ebe3f2b771 | ||
|
|
3e300d98c4 | ||
|
|
1a1e673a6c | ||
|
|
6ce08b5aa3 | ||
|
|
2ef0d5b5cd | ||
|
|
a15978bf6a | ||
|
|
8fb8c54157 | ||
|
|
9f4647be47 | ||
|
|
1a38da30f4 | ||
|
|
c3d244b3b6 | ||
|
|
751af3c2d1 | ||
|
|
ec00779062 | ||
|
|
165ba064a9 | ||
|
|
2763068e21 | ||
|
|
210bdeaed0 | ||
|
|
3037a153dd | ||
|
|
474fb597c6 | ||
|
|
6c19968ef6 | ||
|
|
38dedcbc3c | ||
|
|
0e066d5d63 | ||
|
|
d45a65edea | ||
|
|
98e3c2955b | ||
|
|
9c0eca0891 | ||
|
|
32c377a0ea | ||
|
|
02145ec17e | ||
|
|
9e1a864f54 | ||
|
|
4989dc4301 | ||
|
|
4f43e4eab5 | ||
|
|
d59219e62b | ||
|
|
5f5209af8c | ||
|
|
6a9e6065d1 | ||
|
|
00119dbefe | ||
|
|
c466af60a6 | ||
|
|
e5512974d5 | ||
|
|
7861fa0b11 | ||
|
|
c20dbf1158 | ||
|
|
9e7c302693 | ||
|
|
e007f2592c | ||
|
|
3a5ea3e8ef | ||
|
|
0a0edd4814 | ||
|
|
62bbea01e6 | ||
|
|
3730596030 | ||
|
|
7f176942ba | ||
|
|
72a09f48e0 | ||
|
|
632291e901 | ||
|
|
17352e783b | ||
|
|
d8729f8171 | ||
|
|
8ac8f8f56a | ||
|
|
a2cfa8873f | ||
|
|
13a7f39d27 | ||
|
|
4e22a5605b | ||
|
|
0afcf8cbab | ||
|
|
c54759e766 | ||
|
|
b1d13c125f | ||
|
|
85aab9506c | ||
|
|
728b63b001 | ||
|
|
b91567e2cf | ||
|
|
c3179542a6 | ||
|
|
7ea575a1b8 | ||
|
|
d2eeb93339 | ||
|
|
ca59670b68 | ||
|
|
a1117d77f3 | ||
|
|
e7ee455ee3 | ||
|
|
9ad9c0c633 | ||
|
|
ec2a6f26e6 | ||
|
|
ead4f59ba7 | ||
|
|
c4f0cd18ad | ||
|
|
77d5bd71f9 | ||
|
|
eb0aff2fc3 | ||
|
|
e4e1eb0ced | ||
|
|
116a017938 | ||
|
|
6dfbb27c33 | ||
|
|
097c6915fb | ||
|
|
97490a5e37 | ||
|
|
799678efca | ||
|
|
e3afb1d068 | ||
|
|
d32db74ccc | ||
|
|
e24452c386 | ||
|
|
68e97f17d0 | ||
|
|
9040a4561d | ||
|
|
1ed8205f45 | ||
|
|
feaa207f68 | ||
|
|
76e766583c | ||
|
|
f15446b1d6 | ||
|
|
18be9fda41 | ||
|
|
9b3430b5d9 | ||
|
|
5b79fbd7b5 | ||
|
|
cc0be4565d | ||
|
|
41865fb286 | ||
|
|
37507d4f0a | ||
|
|
755a8d45e9 | ||
|
|
357d872362 | ||
|
|
63be4088a5 | ||
|
|
7e4d123a51 | ||
|
|
1a2ce6ba72 | ||
|
|
adb3805ec7 | ||
|
|
328f324d9e | ||
|
|
108c9e2944 | ||
|
|
f2a5ed6642 | ||
|
|
e1339617b5 | ||
|
|
4f22010c56 | ||
|
|
831b8497b2 | ||
|
|
37dd691270 | ||
|
|
4349445910 | ||
|
|
669849018a | ||
|
|
71ad15b4b6 | ||
|
|
732185a69d | ||
|
|
479dec6998 | ||
|
|
ff6b65c55c | ||
|
|
6e5493f149 | ||
|
|
b185338be2 | ||
|
|
b4161644d0 | ||
|
|
61e0fdea50 | ||
|
|
cb124c7445 | ||
|
|
fce0d50061 | ||
|
|
dac2a22824 | ||
|
|
655bdf3798 | ||
|
|
907bfd04ed | ||
|
|
38ca451d7e | ||
|
|
52800b39e4 | ||
|
|
9d700f6fd1 | ||
|
|
0a5f0d407e | ||
|
|
a84f3a4434 | ||
|
|
1dbd2d8f5f | ||
|
|
ec1001067d | ||
|
|
0374aa17c4 | ||
|
|
78cbce5ca0 | ||
|
|
65c988cf36 | ||
|
|
1487bf027a | ||
|
|
c5ec5f127d | ||
|
|
2627c83e79 | ||
|
|
d54f2351f6 | ||
|
|
460c2b4662 | ||
|
|
cf5fac7d41 | ||
|
|
8d4ae8e970 | ||
|
|
f9db4d49b1 | ||
|
|
728b22f337 | ||
|
|
de42a895dd | ||
|
|
0b813b3fa1 | ||
|
|
d13ff079a4 | ||
|
|
eca9f221ac | ||
|
|
48a3f11215 | ||
|
|
dc7fcb67f0 | ||
|
|
78e8f932c2 | ||
|
|
65678a0995 | ||
|
|
2b9381d40e | ||
|
|
0f795ff913 | ||
|
|
43cb5d278c | ||
|
|
c7142e409b | ||
|
|
3876cb5ec3 | ||
|
|
47862c3182 | ||
|
|
533bef014d | ||
|
|
f5ac0b93f6 | ||
|
|
d5a27b052c | ||
|
|
4a36606121 | ||
|
|
b69933444c | ||
|
|
48866efa57 | ||
|
|
c56445f817 | ||
|
|
18afc0a49c | ||
|
|
f38c4bbbac | ||
|
|
de125a33d1 | ||
|
|
81b7e34b0d | ||
|
|
5d2853d002 | ||
|
|
ad41544403 | ||
|
|
c8bf1fc2cd | ||
|
|
3d25a1d5c2 | ||
|
|
a8896b847f | ||
|
|
a1b3327aa8 | ||
|
|
e9431af138 | ||
|
|
8b07508204 | ||
|
|
3e0d9f92af | ||
|
|
0e4a2c7593 | ||
|
|
a7a2dcf791 | ||
|
|
47221dc81b | ||
|
|
6fb8f11d2f | ||
|
|
0cfd607806 | ||
|
|
a86565a8ec | ||
|
|
3ee231e459 | ||
|
|
335021090d | ||
|
|
b001107135 | ||
|
|
dba1d8645d | ||
|
|
9dff23bdd5 | ||
|
|
90e48cbc16 | ||
|
|
4a1a912d0f | ||
|
|
8a6328ef07 | ||
|
|
a66c72e099 | ||
|
|
652cbcda52 | ||
|
|
199c5d3e64 | ||
|
|
8b1b569fee | ||
|
|
d869a69477 | ||
|
|
00995431f4 | ||
|
|
cec7c5f3b1 | ||
|
|
47ae03ba8a | ||
|
|
3aa3858981 | ||
|
|
67fa1d6bb4 | ||
|
|
00ecb149df | ||
|
|
d931d20006 | ||
|
|
b4a0e97e3f | ||
|
|
8edcfcf2f1 | ||
|
|
6a8ccf9401 | ||
|
|
e2eab24574 | ||
|
|
6a4e31e972 | ||
|
|
04f54f79fa | ||
|
|
c148c6b9cf | ||
|
|
030cbefe3f | ||
|
|
85342e5f74 | ||
|
|
46565aa45c | ||
|
|
8496423b50 | ||
|
|
e39be83f70 | ||
|
|
3a6d1e7a76 | ||
|
|
15fc1d0cca | ||
|
|
685322f02a | ||
|
|
b849861146 | ||
|
|
91e041108c | ||
|
|
76f2ac2c1d | ||
|
|
bc11999201 | ||
|
|
103b1b88f5 | ||
|
|
5b703fe53a | ||
|
|
ab33aa7ebd | ||
|
|
4db043b111 | ||
|
|
3efe977c49 | ||
|
|
08c03bc3a4 | ||
|
|
5cb373f730 | ||
|
|
123f1a2658 | ||
|
|
72645ce0ad | ||
|
|
3f375065fc | ||
|
|
3d816751f6 | ||
|
|
f44c553749 | ||
|
|
9c0ee7e63c | ||
|
|
ae3a73c334 | ||
|
|
fe0129e1a4 | ||
|
|
274395d01b | ||
|
|
5deb5253c1 | ||
|
|
55e3213e5c | ||
|
|
bc9b5c39ee | ||
|
|
7a48c7c1e3 | ||
|
|
324256a499 | ||
|
|
eaea7bdd2a | ||
|
|
5985e22777 | ||
|
|
5de7fc5bd7 | ||
|
|
b54457f522 | ||
|
|
f94341facc | ||
|
|
90a19e7152 | ||
|
|
32998b9c8c | ||
|
|
88532ae13b | ||
|
|
64a138fa80 | ||
|
|
8fe2774d8f | ||
|
|
8ef42dd605 | ||
|
|
5b44206931 | ||
|
|
29f92a0806 | ||
|
|
03f8332c67 | ||
|
|
144d362527 | ||
|
|
7a6ccb932d | ||
|
|
94c1556a5c | ||
|
|
5974ffc0d4 | ||
|
|
6184281694 | ||
|
|
c51406ca04 | ||
|
|
07db957f67 | ||
|
|
d31194bb39 | ||
|
|
44827d13fe | ||
|
|
c847ca0817 | ||
|
|
bd102ea346 | ||
|
|
688dde9890 | ||
|
|
adfef8faef | ||
|
|
7785bcd5a8 | ||
|
|
569f4537e7 | ||
|
|
43a03079a1 | ||
|
|
852802da4b | ||
|
|
ce4e0a1916 | ||
|
|
ce53495b76 | ||
|
|
42571b0794 | ||
|
|
3a2eba3242 | ||
|
|
2d9f41e114 | ||
|
|
8dada6d936 | ||
|
|
2c4170bacc | ||
|
|
86ff1890f1 | ||
|
|
2ef17b800c | ||
|
|
0c0d91f8fd | ||
|
|
ba7b88f5d8 | ||
|
|
47a396ffaf | ||
|
|
f60fcefd1c | ||
|
|
57e1629b0c | ||
|
|
5988640d3f | ||
|
|
6512ace33b | ||
|
|
0a36b5ba5b | ||
|
|
832652730a | ||
|
|
ae7908e000 | ||
|
|
b40fe805e7 | ||
|
|
09f37ae872 | ||
|
|
ef6c4bb6fb | ||
|
|
4fc5ec00ae | ||
|
|
9267433d74 | ||
|
|
dbda1eff82 | ||
|
|
c8f9925973 | ||
|
|
ff87bebe8f | ||
|
|
54f1945a58 | ||
|
|
2a547c879b | ||
|
|
cfa98578ea | ||
|
|
270cb16d30 | ||
|
|
06113db2ff | ||
|
|
3efb79308a | ||
|
|
b0b5bd235f | ||
|
|
4a2fcf950e | ||
|
|
e166a634f2 | ||
|
|
b97ec3948b | ||
|
|
e14bb065dc | ||
|
|
80a77871a0 | ||
|
|
51963f799c | ||
|
|
71d94853ba | ||
|
|
c33c365b62 | ||
|
|
2ce31fc81c | ||
|
|
d9521b5058 | ||
|
|
366948cd28 | ||
|
|
ad338aeefc | ||
|
|
6b6ee76c39 | ||
|
|
3f23507a74 | ||
|
|
815c9eb7de | ||
|
|
aab2b0307e | ||
|
|
b9e7cbd608 | ||
|
|
0cf0b00c58 | ||
|
|
29e07036b1 | ||
|
|
61b1aa919a | ||
|
|
90b7b5f87b | ||
|
|
7103167693 | ||
|
|
77e0f5af17 | ||
|
|
0618341cf8 | ||
|
|
42a4acb688 | ||
|
|
131b1a0aa6 | ||
|
|
e327c92e1a | ||
|
|
d22301ccf9 | ||
|
|
5d2c615a30 | ||
|
|
4a2088ec1e | ||
|
|
238510132f | ||
|
|
b7a8277749 | ||
|
|
0251363d65 | ||
|
|
a784f47430 | ||
|
|
cf28b27da6 | ||
|
|
1fa020c569 | ||
|
|
ead87f7068 | ||
|
|
e35f3c5a96 | ||
|
|
c5282c9ddd | ||
|
|
fc21a8be97 | ||
|
|
c3e315a2ae | ||
|
|
ca30433fd5 | ||
|
|
03240345eb | ||
|
|
156b844fd9 | ||
|
|
f1a6662f01 | ||
|
|
4e5b5e6e3f | ||
|
|
b80dd8c81e | ||
|
|
2cee5020dd | ||
|
|
1a00fc6e30 | ||
|
|
64697a7771 | ||
|
|
d97be2d4fb | ||
|
|
6504beda78 | ||
|
|
3795c69a7b | ||
|
|
77b6ad5408 | ||
|
|
480b4398da | ||
|
|
7ed7d26d6f | ||
|
|
2463ad2820 | ||
|
|
85c50bf0fa | ||
|
|
54ba654b97 | ||
|
|
e58fc38a6d | ||
|
|
f0ef9e072e | ||
|
|
cd62069b9b | ||
|
|
8089f7e181 | ||
|
|
c1ec580fdc | ||
|
|
ae43d58a38 | ||
|
|
2713f26f90 | ||
|
|
783a97e340 | ||
|
|
dcea1544b9 | ||
|
|
9e0286052f | ||
|
|
a41b7d8fd8 | ||
|
|
9473c30426 | ||
|
|
8bf7609d2f | ||
|
|
c63572abd5 | ||
|
|
a003e396b8 | ||
|
|
c4323b7814 | ||
|
|
f27c7e9cf4 | ||
|
|
613c1ca30f | ||
|
|
b4542a532b | ||
|
|
6d8e5a1d1d | ||
|
|
951f628351 | ||
|
|
3ff6b5afd1 | ||
|
|
5aef851eb0 | ||
|
|
f14a72ec1c | ||
|
|
484fc4f5a4 | ||
|
|
6b9567a0be | ||
|
|
0f2a88124b | ||
|
|
740a7b4918 | ||
|
|
6fd3c77348 | ||
|
|
70818c24cd | ||
|
|
69c20045c2 | ||
|
|
c65a6e5e62 | ||
|
|
4a2da72fa3 | ||
|
|
2d83fde2c5 | ||
|
|
3898cc7752 | ||
|
|
c633864073 | ||
|
|
77ee56dd14 | ||
|
|
6c47a485f0 | ||
|
|
9f241959cf | ||
|
|
da20e7977a | ||
|
|
0931c0d5f2 | ||
|
|
cdebfdd7c0 | ||
|
|
ef6ff34ec6 | ||
|
|
9a42273a50 | ||
|
|
75a358a4ce | ||
|
|
6c122c6498 | ||
|
|
963f331290 | ||
|
|
9798ada335 | ||
|
|
2f17ad69a1 | ||
|
|
505d71f1c7 | ||
|
|
4ed6bdd668 | ||
|
|
b87dce6a51 | ||
|
|
295eb8ffec | ||
|
|
c7b2046e46 | ||
|
|
254c2acede | ||
|
|
4ea4bbd782 | ||
|
|
e3f08668ed | ||
|
|
d6113f75ca | ||
|
|
1dcd227014 | ||
|
|
8af190e83d | ||
|
|
dd06e5fd52 | ||
|
|
cd55f443d9 | ||
|
|
974221d938 | ||
|
|
02c084ed6d | ||
|
|
456927548a | ||
|
|
53490cc28b | ||
|
|
15014d2d49 | ||
|
|
3ee14b9463 | ||
|
|
9e46f1ec06 | ||
|
|
47c433b2c9 | ||
|
|
72d5072cac | ||
|
|
6b14b29103 | ||
|
|
e3ef25714a | ||
|
|
ac1bf9e84e | ||
|
|
54abea488f | ||
|
|
9d1ff3231a | ||
|
|
eef8b98b62 | ||
|
|
a4e0fecfe9 | ||
|
|
370837278b | ||
|
|
c03d25ece6 | ||
|
|
34be765ac5 | ||
|
|
bf208921ad | ||
|
|
14b469d8b6 | ||
|
|
cc239d927c | ||
|
|
1c7e619b55 | ||
|
|
3205dae30c | ||
|
|
57da24821f | ||
|
|
16f4184e25 | ||
|
|
4c51fbf816 | ||
|
|
f7835f43f6 | ||
|
|
aeabd9929c | ||
|
|
94a0d841ac | ||
|
|
3172d5a552 | ||
|
|
a042f6d418 | ||
|
|
d9ff5e0053 | ||
|
|
7ce9121f8e | ||
|
|
e0c1d6344d | ||
|
|
6c378a0122 | ||
|
|
1371b14005 | ||
|
|
bf7f3eb86f | ||
|
|
c8716df6a6 | ||
|
|
fd12d4bd00 | ||
|
|
29d0f40c1a | ||
|
|
63fd526db8 | ||
|
|
0fbfc709da | ||
|
|
19f0488745 | ||
|
|
f81c30222a | ||
|
|
26ae76d644 | ||
|
|
f370118498 | ||
|
|
d3a32421ee | ||
|
|
af9a83117b | ||
|
|
ce418487ad | ||
|
|
9bd7dbc940 | ||
|
|
bc91a41bbe | ||
|
|
7efbc666a0 | ||
|
|
5ecd70e121 | ||
|
|
e4d165edb7 | ||
|
|
d24f29e98f | ||
|
|
5559738654 | ||
|
|
54a321cb4c | ||
|
|
b2f0e74ade | ||
|
|
a105c09841 | ||
|
|
6ad8d7ba1b | ||
|
|
c58bb57de0 | ||
|
|
aec183bd78 | ||
|
|
75ed6de07f | ||
|
|
8f56953915 | ||
|
|
7d98f93dd6 | ||
|
|
e74123bc9c | ||
|
|
d21b4c021d | ||
|
|
ca05865021 | ||
|
|
b30ed83f78 | ||
|
|
a4d0b2d0dc | ||
|
|
fa1c48be41 | ||
|
|
ec29cc0801 | ||
|
|
44e9a69faa | ||
|
|
920598f943 | ||
|
|
5afdd88eb7 | ||
|
|
81ebc3906f | ||
|
|
ec7d581c3d | ||
|
|
60e1da412c | ||
|
|
27cf0a3c6b | ||
|
|
e24beebd43 | ||
|
|
9bea47e358 | ||
|
|
779f000f3e | ||
|
|
37eeeb1d64 | ||
|
|
c231c07633 | ||
|
|
4ee82b4fd6 | ||
|
|
d0f0c32ffa | ||
|
|
2c931d72f9 | ||
|
|
1cf832f0fe | ||
|
|
1e49fd8145 | ||
|
|
bd93048aaa | ||
|
|
1b5dc9eb14 | ||
|
|
e93490acfc | ||
|
|
487306b943 | ||
|
|
573a37f671 | ||
|
|
8bfee794c8 | ||
|
|
aea31102f6 | ||
|
|
bfe9aa5330 | ||
|
|
a45c5db34e | ||
|
|
3e588e243c | ||
|
|
03dba9f624 | ||
|
|
b707690817 | ||
|
|
3b7660834d | ||
|
|
f4b14e37ae | ||
|
|
146223e2b6 | ||
|
|
d6b037c178 | ||
|
|
3df8cc53e3 | ||
|
|
63ab54cc42 | ||
|
|
569954b767 | ||
|
|
9722fab8f3 | ||
|
|
bad138d2fa | ||
|
|
a7e57354dd | ||
|
|
45fa486a6d | ||
|
|
a65d271cbd | ||
|
|
feba11626d | ||
|
|
a5afc47d82 | ||
|
|
9edf6d165d | ||
|
|
12b8a9e487 | ||
|
|
9e8b5487b2 | ||
|
|
305a522a65 | ||
|
|
2f77390a8e | ||
|
|
0ab704b115 | ||
|
|
c353f7a872 | ||
|
|
9a242f63eb | ||
|
|
cc07cedd4a | ||
|
|
f1f4cd67f7 | ||
|
|
ceb948bd1f | ||
|
|
90a0878600 | ||
|
|
26a63591bd | ||
|
|
87ee599da1 | ||
|
|
7843a153a7 | ||
|
|
72331faee7 | ||
|
|
28470d60c6 | ||
|
|
ab47c89ef7 | ||
|
|
b086e652c3 | ||
|
|
ef2d8bf5af | ||
|
|
d3221abd7e | ||
|
|
448d19e9fd | ||
|
|
0a973cd83a | ||
|
|
4bff11bf12 | ||
|
|
f127203a5e | ||
|
|
dc3caa16d0 | ||
|
|
1f33ea9db3 | ||
|
|
4e157455c8 | ||
|
|
2bb3652be2 | ||
|
|
becc00d2ee | ||
|
|
e6186c0bbd | ||
|
|
6538540f3b | ||
|
|
a437125730 | ||
|
|
339e5965bb | ||
|
|
e3eb3c8000 | ||
|
|
2ef0f41612 | ||
|
|
0a38a734f5 | ||
|
|
c2d11cdc79 | ||
|
|
0fcab638a3 | ||
|
|
b37853866f | ||
|
|
e6642ac9ab | ||
|
|
c294e80230 | ||
|
|
996f4d29be | ||
|
|
0c5a5c7438 | ||
|
|
9926c0b6da | ||
|
|
8e691769ae | ||
|
|
add2d8af9b | ||
|
|
bc72c8b87c | ||
|
|
b73964afb9 | ||
|
|
96b6115e7b | ||
|
|
ea46488d94 | ||
|
|
df0e8ed567 | ||
|
|
1e86b4d772 | ||
|
|
b1e1ed982a | ||
|
|
500388a634 | ||
|
|
7a60ac17ce | ||
|
|
a6a2f848d7 | ||
|
|
25e6350c04 | ||
|
|
e0329c273f | ||
|
|
fd7b33fe30 | ||
|
|
6d70f8d2f9 | ||
|
|
c6bb28c139 | ||
|
|
6796206099 | ||
|
|
ca40342978 | ||
|
|
36150fb55b | ||
|
|
7b3f671159 | ||
|
|
0cd6191190 | ||
|
|
cbccb97d42 | ||
|
|
2ce9386759 | ||
|
|
f5b85a82d0 | ||
|
|
0123da10f7 | ||
|
|
1228f1b6fa | ||
|
|
3126c2d325 | ||
|
|
a9c9ca29c8 | ||
|
|
665f44bcee | ||
|
|
c444f7ddb8 | ||
|
|
787c36c754 | ||
|
|
3502469c1d | ||
|
|
c58b16cba6 | ||
|
|
15ea57b692 | ||
|
|
655cf0617e | ||
|
|
57311397e0 | ||
|
|
94db2387c8 | ||
|
|
bd1cf0fadc | ||
|
|
cfd53a8b2c | ||
|
|
baa2d1a389 | ||
|
|
8cd963a893 | ||
|
|
6e3ee9171b | ||
|
|
088ef2285f | ||
|
|
e127b8f4ab | ||
|
|
e218962c55 | ||
|
|
fa080b5ce2 | ||
|
|
d7d019f870 | ||
|
|
74b5c4d894 | ||
|
|
4ebc3af1e4 | ||
|
|
3188b33d19 | ||
|
|
c2c69c282e | ||
|
|
f57c7b5998 | ||
|
|
56faf3ed48 | ||
|
|
2fe8c87caa | ||
|
|
b7ecb7b6fb | ||
|
|
569b6c7939 | ||
|
|
e2f9b9c58d | ||
|
|
94150a92aa | ||
|
|
f81814a426 | ||
|
|
443b6ad7e9 | ||
|
|
2ceb68ec87 | ||
|
|
29f66d457d | ||
|
|
304d69f8b5 | ||
|
|
900a5b69a5 | ||
|
|
ebe1b641fd | ||
|
|
f26ea526e8 | ||
|
|
b36e6faac5 | ||
|
|
528fb3092b | ||
|
|
d5ee925c52 | ||
|
|
208e32d7ea | ||
|
|
5429161f91 | ||
|
|
81cdfcf61a | ||
|
|
6186e22006 | ||
|
|
4ff051130b | ||
|
|
97ae36dcda | ||
|
|
800b5f9d9a | ||
|
|
d4b1f32516 | ||
|
|
6626b856fb | ||
|
|
44df437c4e | ||
|
|
189a9da5b5 | ||
|
|
73def8a682 | ||
|
|
7c97db74dc | ||
|
|
ceb6e94d0c | ||
|
|
1a61c4ee8f | ||
|
|
d4aa4fdb90 | ||
|
|
3e7db7051b | ||
|
|
6b6e903322 | ||
|
|
2f62c72cfe | ||
|
|
8951352f63 | ||
|
|
6c9e265a11 | ||
|
|
1a14d37c72 | ||
|
|
5bfad0149b | ||
|
|
2168609577 | ||
|
|
7e8e981145 | ||
|
|
300e65f5c4 | ||
|
|
69590f9547 | ||
|
|
ae023bdf5b | ||
|
|
4556e4d230 | ||
|
|
a77c88bddd | ||
|
|
8672f92942 | ||
|
|
a8e81a32bf | ||
|
|
95b638a135 | ||
|
|
4302cdf0ea | ||
|
|
36d097f86c | ||
|
|
b364178fc0 | ||
|
|
2c7927bb08 | ||
|
|
af6041128b | ||
|
|
6ccee9f6eb | ||
|
|
404f2072a1 | ||
|
|
01e774bff5 | ||
|
|
df9fb59f40 | ||
|
|
9ac9e21645 | ||
|
|
44846cc7b5 | ||
|
|
96e36a6f73 | ||
|
|
37ad3ea31a | ||
|
|
07d2a701c7 | ||
|
|
94f7e8cd08 | ||
|
|
51819e9d4a | ||
|
|
860ec7fea8 | ||
|
|
21b3866df4 | ||
|
|
a91717aca3 | ||
|
|
91a272ee3d | ||
|
|
d51079fc88 | ||
|
|
37ae106713 | ||
|
|
b93b5cc8d6 | ||
|
|
9d6733deff | ||
|
|
86652230bc | ||
|
|
588e37aa20 | ||
|
|
79b75c5ba7 | ||
|
|
cc60558839 | ||
|
|
c7a6cd8964 | ||
|
|
5b537ab470 | ||
|
|
48af6c460f | ||
|
|
89c9c58631 | ||
|
|
c10f518e3a | ||
|
|
1f4cd6504a | ||
|
|
9ef40c01ce | ||
|
|
843be2a62c | ||
|
|
a881a6d51e | ||
|
|
22221afb7a | ||
|
|
21a7c64b0b | ||
|
|
41ea254301 | ||
|
|
3b15717150 | ||
|
|
94f02cdb07 | ||
|
|
061433d8cd | ||
|
|
c8ba88bb99 | ||
|
|
3b181cd9b2 | ||
|
|
5384bed382 | ||
|
|
ac0d831f22 | ||
|
|
8776058655 | ||
|
|
5c8c806228 | ||
|
|
aa2110b736 | ||
|
|
cbfaf6ae18 | ||
|
|
d815608701 | ||
|
|
696c280eaf | ||
|
|
b8c785fa3f | ||
|
|
d80fdfbfc1 | ||
|
|
f783dad211 | ||
|
|
c335d10167 | ||
|
|
bf7a02df5e | ||
|
|
bf12385099 | ||
|
|
f98b53e9b8 | ||
|
|
8f379d253e | ||
|
|
e1f07618d3 | ||
|
|
9a0c129844 | ||
|
|
aa2a8ab6f4 | ||
|
|
8e59ff1318 | ||
|
|
ad99c87f72 | ||
|
|
ba776b5854 | ||
|
|
e526145e48 | ||
|
|
45c5e65bf6 | ||
|
|
87bec4e2ab | ||
|
|
433e67a6dc | ||
|
|
6acba51b88 | ||
|
|
cf1322952d | ||
|
|
b5ac9dd430 | ||
|
|
a3b590bcb7 | ||
|
|
d4f7e3ef07 | ||
|
|
16119f6958 | ||
|
|
e39c2583a5 | ||
|
|
eecff009dd | ||
|
|
d9ed61a7da | ||
|
|
58db265702 | ||
|
|
bd39f0cbf8 | ||
|
|
255015108a | ||
|
|
b80f366f8d | ||
|
|
c845c7fce6 | ||
|
|
6e7b7ce0e1 | ||
|
|
9b728902d4 | ||
|
|
9a1f54c41a | ||
|
|
b3251eb016 | ||
|
|
f4064b7939 | ||
|
|
f014f08ff4 | ||
|
|
dee1cf4b5a | ||
|
|
f35a3d8fa3 | ||
|
|
ca9a8d14fa | ||
|
|
35c35fa9d7 | ||
|
|
09fff95d58 | ||
|
|
287307a1ba | ||
|
|
d628e62302 | ||
|
|
a070bc46ad | ||
|
|
9027c44511 | ||
|
|
722aa7b9c6 | ||
|
|
a4e777b24b | ||
|
|
af7120e953 | ||
|
|
d5adc3bbac | ||
|
|
aa3d778ad1 | ||
|
|
3465a816c6 | ||
|
|
3d1726c5aa | ||
|
|
488de2d583 | ||
|
|
c415b114a6 | ||
|
|
da36784e9c | ||
|
|
78fafdc272 | ||
|
|
7d57055f73 | ||
|
|
b84cc747cb | ||
|
|
9a4f3ccbad | ||
|
|
68de3cc928 | ||
|
|
6e135cdd36 | ||
|
|
696116ac85 | ||
|
|
08a13ff35a | ||
|
|
af1cea81dc | ||
|
|
2c22a7ca90 | ||
|
|
6fd6bd0375 | ||
|
|
1a96dc965a | ||
|
|
9550350f25 | ||
|
|
0ed605a5b9 | ||
|
|
18910d931c | ||
|
|
87648b7d8b | ||
|
|
7e7a1aeecb | ||
|
|
cb353358b7 | ||
|
|
ba1619711b | ||
|
|
b3cc9475ad | ||
|
|
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 |
@@ -5,7 +5,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: "npm"
|
||||
|
||||
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 |
1497
apps/desktop/package-lock.json
generated
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.13",
|
||||
"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.8",
|
||||
"electron-builder": "^24.9.1",
|
||||
"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
|
||||
);
|
||||
};
|
||||
});
|
||||
})
|
||||
});
|
||||
@@ -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,23 +30,16 @@ 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";
|
||||
|
||||
// How app lock works
|
||||
// 1. User goes to settings and setup app lock with a Pin/Password.
|
||||
// 2. The Pin/Password is used to encrypt a random value or user's encryption key.
|
||||
// 3. The encrypted value is stored in MMKV
|
||||
// 4. When the app launches, the same value is decrypted with user provided key, if it works, we launch the app otherwise it remains locked.
|
||||
// 5. If Biometrics are enabled, the app lock pin/password is stored in keychain. the value can be accessed if fingerprint auth works ONLY.
|
||||
// 6. User can manually enter the pin if biometrics fails.
|
||||
// 7. There is no way to enter the app if user forgets the PIN. The only way is to reset app data and start fresh again.
|
||||
|
||||
// How to handle app lock for existing users...
|
||||
// 1.
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 108 KiB |
@@ -21,9 +21,10 @@ import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ProcessingModes, MMKVLoader } from "react-native-mmkv-storage";
|
||||
|
||||
// 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
|
||||
@@ -42,6 +43,14 @@ const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
|
||||
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
|
||||
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
const APPLOCK_CIPHER = "applockCipher";
|
||||
|
||||
const KEYSTORE_CONFIG = Platform.select({
|
||||
ios: {
|
||||
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
@@ -73,17 +82,17 @@ export async function encryptDatabaseKeyWithPassword(appLockPassword) {
|
||||
const key = getDatabaseKey();
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
"notesnook_applock_key"
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = await encrypt(appLockCredentials, key);
|
||||
MMKV.setMap("databaseKeyCipher", databaseKeyCipher);
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials("notesnook:db");
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
@@ -97,42 +106,46 @@ export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem("databaseKeyCipher");
|
||||
MMKV.removeItem(DB_KEY_CIPHER);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
try {
|
||||
console.log("key", appLockPassword);
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
"notesnook_applock_key_salt"
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, "applock_password");
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
|
||||
CipherStorage.setMap("appLockCipher", encrypted);
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem("appLockCipher");
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap("appLockCipher");
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const decrypted = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
|
||||
appLockCipher
|
||||
);
|
||||
return decrypted === "applock_password";
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
);
|
||||
return typeof decrypted === "string";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -140,6 +153,7 @@ export async function validateAppLockPassword(appLockPassword) {
|
||||
let DB_KEY;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
DatabaseLogger.info("Cleared database key");
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword) {
|
||||
@@ -153,104 +167,124 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
console.log("Getting database key from cipher");
|
||||
DatabaseLogger.info("Getting database key from cipher");
|
||||
DB_KEY = databaseKey;
|
||||
return databaseKey;
|
||||
}
|
||||
|
||||
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
if (hasKey) {
|
||||
let credentials = await Keychain.getInternetCredentials(
|
||||
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
|
||||
);
|
||||
console.log("Getting database key from Keychain");
|
||||
DB_KEY = credentials.password;
|
||||
return credentials.password;
|
||||
}
|
||||
console.log("Generating new database key");
|
||||
const password = generatePassword();
|
||||
const derivedDatabaseKey = await Sodium.deriveKey(
|
||||
password,
|
||||
"notesnook_database_key"
|
||||
);
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
derivedDatabaseKey.key,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: derivedDatabaseKey.key
|
||||
},
|
||||
userKeyCredentials.password
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap("userKeyCipher", userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
console.log("Migrated user credentials to cipher");
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
DB_KEY = derivedDatabaseKey.key;
|
||||
|
||||
return derivedDatabaseKey.key;
|
||||
return DB_KEY;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e, "error");
|
||||
DatabaseLogger.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(name, data) {
|
||||
export async function deriveCryptoKey(data) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey()
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap("userKeyCipher", userKeyCipher);
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
const keyCipher = MMKV.getMap("userKeyCipher");
|
||||
if (!key) return null;
|
||||
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
|
||||
const key = decrypt(
|
||||
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 {
|
||||
MMKV.removeItem("userKeyCipher");
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,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);
|
||||
}
|
||||
|
||||
@@ -294,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,16 +32,16 @@ import { getDatabaseKey } from "./encryption";
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
// API_HOST: "https://api.notesnook.com",
|
||||
// AUTH_HOST: "https://auth.streetwriters.co",
|
||||
// SSE_HOST: "https://events.streetwriters.co",
|
||||
// SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
// ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
API_HOST: "http://192.168.43.5:5264",
|
||||
AUTH_HOST: "http://192.168.43.5:8264",
|
||||
SSE_HOST: "http://192.168.43.5:7264",
|
||||
SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
|
||||
ISSUES_HOST: "http://192.168.43.5:2624"
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// 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",
|
||||
@@ -57,7 +57,8 @@ export async function setupDatabase(password) {
|
||||
if (!key)
|
||||
throw new Error("Database setup failed, could not get database key");
|
||||
|
||||
console.log("Opening database with key:", key);
|
||||
console.log("Opening database with key:", !!key);
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
@@ -77,6 +78,7 @@ export async function setupDatabase(password) {
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
}),
|
||||
tempStore: "memory",
|
||||
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL",
|
||||
password: key
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
@@ -83,8 +84,10 @@ 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++) {
|
||||
@@ -126,7 +129,7 @@ export async function downloadAttachments(
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
console.log("Error downloading attachment", e);
|
||||
ToastManager.error(e, "Error downloading attachment");
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
@@ -163,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);
|
||||
@@ -182,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;
|
||||
}
|
||||
|
||||
@@ -204,16 +209,26 @@ export default async function downloadAttachment(
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(
|
||||
"starting download attachment",
|
||||
attachment.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(options.groupId || attachment.hash, attachment.hash);
|
||||
if (!(await RNFetchBlob.fs.exists(`${cacheDir}/${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.filename,
|
||||
@@ -221,6 +236,7 @@ export default async function downloadAttachment(
|
||||
);
|
||||
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
|
||||
let info = {
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
@@ -231,9 +247,9 @@ export default async function downloadAttachment(
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -36,8 +35,6 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
}
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
const isPng = /(png)/g.test(attachment?.mimeType);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.mimeType);
|
||||
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"
|
||||
|
||||
@@ -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";
|
||||
@@ -35,18 +45,49 @@ import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { validateAppLockPassword } from "../../common/database/encryption";
|
||||
|
||||
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
|
||||
);
|
||||
@@ -64,6 +105,8 @@ const AppLockedOverlay = () => {
|
||||
)
|
||||
return;
|
||||
|
||||
biometricUnlockAwaitingUserInput.current = true;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await NotesnookModule.getActivityName();
|
||||
if (activityName !== "MainActivity") return;
|
||||
@@ -79,20 +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);
|
||||
}, 500);
|
||||
}, [biometricsAuthEnabled, lockApp]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!password.current) return;
|
||||
try {
|
||||
const unlocked = appLockHasPasswordSecurity
|
||||
? validateAppLockPassword(password.current)
|
||||
: await db.user.verifyPassword(password.current);
|
||||
|
||||
? 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;
|
||||
@@ -103,14 +155,46 @@ 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]);
|
||||
|
||||
@@ -144,7 +228,7 @@ const AppLockedOverlay = () => {
|
||||
<IconButton
|
||||
name="fingerprint"
|
||||
size={100}
|
||||
customStyle={{
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
marginBottom: 20,
|
||||
@@ -186,11 +270,13 @@ const AppLockedOverlay = () => {
|
||||
fwdRef={passwordInputRef}
|
||||
secureTextEntry
|
||||
keyboardType={
|
||||
appLockHasPasswordSecurity ? "number-pad" : "default"
|
||||
appLockHasPasswordSecurity ? keyboardType : "default"
|
||||
}
|
||||
placeholder={`Enter ${
|
||||
appLockHasPasswordSecurity
|
||||
? `app lock pin`
|
||||
? `app lock ${
|
||||
keyboardType === "numeric" ? "pin" : "password"
|
||||
}`
|
||||
: "account password"
|
||||
}`}
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -67,9 +67,8 @@ export const AttachmentItem = ({
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
|
||||
return (hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.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?.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, 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>
|
||||
|
||||
@@ -84,8 +84,9 @@ 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;
|
||||
};
|
||||
|
||||
@@ -201,7 +202,7 @@ const DownloadAttachments = ({
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(index) => "attachment" + index}
|
||||
keyExtractor={(index) => "attachment_download" + index}
|
||||
renderItem={({ index }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
@@ -245,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,12 +84,13 @@ 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);
|
||||
@@ -86,13 +98,16 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
const results = await db.lookup
|
||||
.attachments(attachmentSearchValue.current as string)
|
||||
.sorted();
|
||||
if (results.length === 0) return;
|
||||
setAttachments(results);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const renderItem = ({ index }: { item: boolean; index: number }) => (
|
||||
<AttachmentItem
|
||||
setAttachments={setAttachments}
|
||||
setAttachments={async () => {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}}
|
||||
attachments={attachments}
|
||||
id={index}
|
||||
context="attachments-list"
|
||||
@@ -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={{
|
||||
|
||||
@@ -158,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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ const DialogProvider = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppLockPassword />
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<PremiumDialog colors={colors} />
|
||||
@@ -61,7 +62,6 @@ const DialogProvider = () => {
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
<AppLockPassword />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,8 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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: "",
|
||||
@@ -66,7 +70,8 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (dialogInfo.positivePress) {
|
||||
inputRef.current?.blur();
|
||||
let result = await dialogInfo.positivePress(
|
||||
inputValue || dialogInfo.defaultValue
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
checked
|
||||
);
|
||||
if (result === false) {
|
||||
return;
|
||||
@@ -81,8 +86,9 @@ 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]
|
||||
);
|
||||
@@ -98,7 +104,8 @@ export const Dialog = ({ context = "global" }) => {
|
||||
}, [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}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
setAppLockVerificationCipher,
|
||||
validateAppLockPassword
|
||||
} from "../../../common/database/encryption";
|
||||
import BiometicService from "../../../services/biometrics";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
@@ -31,22 +32,30 @@ import {
|
||||
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);
|
||||
@@ -56,6 +65,7 @@ export const AppLockPassword = () => {
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
}>({});
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const subs = [
|
||||
@@ -72,7 +82,7 @@ export const AppLockPassword = () => {
|
||||
})
|
||||
];
|
||||
return () => {
|
||||
subs.forEach((sub) => sub.unsubscribe());
|
||||
subs.forEach((sub) => sub?.unsubscribe());
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -81,11 +91,15 @@ export const AppLockPassword = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
onShow={async () => {
|
||||
await sleep(100);
|
||||
passwordInputRef.current?.focus();
|
||||
if (mode !== "change") {
|
||||
passwordInputRef.current?.focus();
|
||||
} else {
|
||||
currentPasswordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
statusBarTranslucent={false}
|
||||
onRequestClose={close}
|
||||
@@ -103,17 +117,17 @@ export const AppLockPassword = () => {
|
||||
<DialogHeader
|
||||
title={
|
||||
mode === "change"
|
||||
? "Change app lock pin"
|
||||
? `Change app lock ${keyboardType}`
|
||||
: mode === "remove"
|
||||
? "Remove app lock pin"
|
||||
: "Set up app lock pin"
|
||||
? `Remove app lock ${keyboardType}`
|
||||
: `Set up app lock ${keyboardType}`
|
||||
}
|
||||
paragraph={
|
||||
mode === "change"
|
||||
? "Change app lock pin"
|
||||
? `Change app lock ${keyboardType}`
|
||||
: mode === "remove"
|
||||
? "Remove app lock pin"
|
||||
: "Set up a custom app lock pin to unlock the app"
|
||||
? `Remove app lock ${keyboardType}`
|
||||
: `Set up a custom app lock ${keyboardType} to unlock the app`
|
||||
}
|
||||
icon="shield"
|
||||
padding={12}
|
||||
@@ -135,12 +149,13 @@ export const AppLockPassword = () => {
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
defaultValue={values.current.currentPassword}
|
||||
autoComplete="password"
|
||||
returnKeyLabel="Next"
|
||||
keyboardType="number-pad"
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
placeholder={"Current pin"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
placeholder={`Current ${keyboardType}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -153,12 +168,37 @@ export const AppLockPassword = () => {
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
keyboardType="number-pad"
|
||||
defaultValue={values.current.password}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
|
||||
returnKeyType={mode !== "remove" ? "next" : "done"}
|
||||
secureTextEntry
|
||||
placeholder={mode === "change" ? "New pin" : "Pin"}
|
||||
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" ? (
|
||||
@@ -171,14 +211,15 @@ export const AppLockPassword = () => {
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
keyboardType="number-pad"
|
||||
defaultValue={values.current.confirmPassword}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
customValidator={() => values.current.password || ""}
|
||||
validationType="confirmPassword"
|
||||
autoComplete="password"
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
placeholder={"Confirm pin"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
placeholder={`Confirm ${keyboardType}`}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -198,7 +239,11 @@ export const AppLockPassword = () => {
|
||||
|
||||
if (values.current.password !== values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error("Pin does not match"),
|
||||
new Error(
|
||||
`${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} does not match`
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
@@ -222,7 +267,11 @@ export const AppLockPassword = () => {
|
||||
|
||||
if (values.current.password !== values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error("Pin does not match"),
|
||||
new Error(
|
||||
`${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} does not match`
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
@@ -235,7 +284,9 @@ export const AppLockPassword = () => {
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error("Pin incorrect"),
|
||||
new Error(
|
||||
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
@@ -259,13 +310,36 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(new Error("Pin incorrect"), "local");
|
||||
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"
|
||||
|
||||
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;
|
||||
@@ -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 = () => {
|
||||
@@ -181,11 +181,11 @@ const JumpToSectionDialog = () => {
|
||||
>
|
||||
{groups?.map((item, index) => {
|
||||
return (
|
||||
<PressableButton
|
||||
<Pressable
|
||||
key={item.group.id}
|
||||
onPress={() => onPress(item)}
|
||||
type={currentIndex === index ? "accent" : "transparent"}
|
||||
customStyle={{
|
||||
style={{
|
||||
minWidth: "20%",
|
||||
width: null,
|
||||
paddingHorizontal: 12,
|
||||
@@ -208,7 +208,7 @@ const JumpToSectionDialog = () => {
|
||||
>
|
||||
{item.group.title}
|
||||
</Paragraph>
|
||||
</PressableButton>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
@@ -211,7 +211,7 @@ const PDFPreview = () => {
|
||||
color={colors.static.white}
|
||||
name="arrow-left"
|
||||
onPress={close}
|
||||
customStyle={{
|
||||
style={{
|
||||
marginRight: 12
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,7 @@ import { eScrollEvent } from "../../utils/events";
|
||||
import { LeftMenus } from "./left-menus";
|
||||
import { RightMenus } from "./right-menus";
|
||||
import { Title } from "./title";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
type HeaderRightButton = {
|
||||
title: string;
|
||||
@@ -66,6 +67,8 @@ export const Header = ({
|
||||
hasSearch?: boolean;
|
||||
onSearch?: () => void;
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [borderHidden, setBorderHidden] = useState(true);
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,11 +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 { 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";
|
||||
@@ -45,10 +46,6 @@ import { ReminderTime } from "../../ui/reminder-time";
|
||||
import { TimeSince } from "../../ui/time-since";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import {
|
||||
NotebooksWithDateEdited,
|
||||
TagsWithDateEdited
|
||||
} from "../../../stores/resolve-items";
|
||||
|
||||
type NoteItemProps = {
|
||||
item: Note | BaseTrashItem<Note>;
|
||||
@@ -61,6 +58,7 @@ type NoteItemProps = {
|
||||
date: number;
|
||||
isTrash?: boolean;
|
||||
noOpen?: boolean;
|
||||
locked?: boolean;
|
||||
};
|
||||
|
||||
const NoteItem = ({
|
||||
@@ -72,10 +70,12 @@ const NoteItem = ({
|
||||
reminder,
|
||||
tags,
|
||||
attachmentsCount,
|
||||
locked,
|
||||
noOpen = false
|
||||
}: NoteItemProps) => {
|
||||
const isEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote === item.id
|
||||
const isEditingNote = useTabStore(
|
||||
(state) =>
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
@@ -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
|
||||
}}
|
||||
@@ -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,
|
||||
|
||||
@@ -26,20 +26,14 @@ import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
hideSheet,
|
||||
openVault,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
|
||||
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||
import NotePreview from "../../note-history/preview";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import {
|
||||
NotebooksWithDateEdited,
|
||||
TagsWithDateEdited
|
||||
} from "../../../stores/resolve-items";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
|
||||
export const openNote = async (
|
||||
item: Note,
|
||||
@@ -52,51 +46,22 @@ export const openNote = async (
|
||||
if (!isTrash) {
|
||||
note = (await db.notes.note(item.id)) as Note;
|
||||
}
|
||||
if (useSelectionStore.getState().selectionMode === item.type) {
|
||||
const {
|
||||
selectedItemsList,
|
||||
selectionMode,
|
||||
clearSelection,
|
||||
setSelectedItem
|
||||
} = useSelectionStore.getState();
|
||||
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(note.id);
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (selectItem(item)) return;
|
||||
|
||||
if (note.conflicted) {
|
||||
eSendEvent(eShowMergeDialog, note);
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.locked) {
|
||||
openVault({
|
||||
item: note,
|
||||
novault: true,
|
||||
locked: true,
|
||||
goToEditor: true,
|
||||
title: "Open note",
|
||||
description: "Unlock note to open it in editor."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isTrash) {
|
||||
if (!note.contentId) return;
|
||||
|
||||
const content = await db.content.get(note.contentId as string);
|
||||
presentSheet({
|
||||
component: (
|
||||
<NotePreview note={item} content={{ type: "tiptap", data: content }} />
|
||||
)
|
||||
component: <NotePreview note={item} content={content} />
|
||||
});
|
||||
} else {
|
||||
if (note?.readonly) {
|
||||
useEditorStore.getState().setReadonly(note?.readonly);
|
||||
}
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
@@ -116,6 +81,7 @@ type NoteWrapperProps = {
|
||||
attachmentsCount: number;
|
||||
date: number;
|
||||
isRenderedInActionSheet: boolean;
|
||||
locked?: boolean;
|
||||
};
|
||||
|
||||
export const NoteWrapper = React.memo<
|
||||
|
||||
@@ -198,7 +198,7 @@ export const NotebookItem = ({
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
size={SIZE.xl}
|
||||
onPress={() => Properties.present(item)}
|
||||
customStyle={{
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
width: 35,
|
||||
|
||||
@@ -27,18 +27,12 @@ import Navigation from "../../../services/navigation";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../../stores/use-trash-store";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
|
||||
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
const isTrash = item.type === "trash";
|
||||
const { selectedItemsList, setSelectedItem, selectionMode, clearSelection } =
|
||||
useSelectionStore.getState();
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
if (selectItem(item)) return;
|
||||
|
||||
if (isTrash) {
|
||||
presentDialog({
|
||||
|
||||
@@ -22,7 +22,6 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Properties } from "../../properties";
|
||||
import ReminderSheet from "../../sheets/reminder";
|
||||
@@ -30,7 +29,7 @@ import { IconButton } from "../../ui/icon-button";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
|
||||
const ReminderItem = React.memo(
|
||||
({
|
||||
@@ -44,18 +43,7 @@ const ReminderItem = React.memo(
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const openReminder = () => {
|
||||
const {
|
||||
selectedItemsList,
|
||||
setSelectedItem,
|
||||
selectionMode,
|
||||
clearSelection
|
||||
} = useSelectionStore.getState();
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
if (selectItem(item)) return;
|
||||
|
||||
ReminderSheet.present(item, undefined, isSheet);
|
||||
};
|
||||
@@ -63,8 +51,9 @@ const ReminderItem = React.memo(
|
||||
<SelectionWrapper onPress={openReminder} item={item} isSheet={isSheet}>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
opacity: item.disabled ? 0.5 : 1
|
||||
opacity: item.disabled ? 0.5 : 1,
|
||||
maxWidth: "80%",
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
@@ -175,7 +164,7 @@ const ReminderItem = React.memo(
|
||||
name="dots-horizontal"
|
||||
size={SIZE.xl}
|
||||
onPress={() => Properties.present(item, [], isSheet)}
|
||||
customStyle={{
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
width: 35,
|
||||
|
||||
@@ -17,17 +17,18 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Item } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import useIsSelected from "../../../hooks/use-selected";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { Item } from "@notesnook/core";
|
||||
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const isEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote === item.id
|
||||
const isEditingNote = useTabStore(
|
||||
(state) =>
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
|
||||
);
|
||||
|
||||
const [selected] = useIsSelected(item);
|
||||
@@ -41,8 +42,8 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
|
||||
backgroundColor: colors.selected.background,
|
||||
borderLeftWidth: 5,
|
||||
borderLeftColor: isEditingNote
|
||||
? item.color
|
||||
? colors.static[item.color]
|
||||
? color
|
||||
? color
|
||||
: colors.selected.accent
|
||||
: "transparent"
|
||||
}}
|
||||
|
||||
@@ -21,11 +21,28 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { PropsWithChildren, useRef } from "react";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import { Filler } from "./back-fill";
|
||||
import { SelectionIcon } from "./selection";
|
||||
import { Item, TrashItem } from "@notesnook/core";
|
||||
|
||||
export function selectItem(item: Item) {
|
||||
if (useSelectionStore.getState().selectionMode === item.type) {
|
||||
const { selectionMode, clearSelection, setSelectedItem } =
|
||||
useSelectionStore.getState();
|
||||
|
||||
if (selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
}
|
||||
|
||||
if (useSelectionStore.getState().selectedItemsList.length === 0) {
|
||||
clearSelection();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
type SelectionWrapperProps = PropsWithChildren<{
|
||||
item: Item;
|
||||
onPress: () => void;
|
||||
@@ -60,7 +77,7 @@ const SelectionWrapper = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
<Pressable
|
||||
customColor={isSheet ? colors.primary.hover : "transparent"}
|
||||
testID={testID}
|
||||
onLongPress={onLongPress}
|
||||
@@ -68,7 +85,7 @@ const SelectionWrapper = ({
|
||||
customSelectedColor={colors.primary.hover}
|
||||
customAlpha={!isDark ? -0.02 : 0.02}
|
||||
customOpacity={1}
|
||||
customStyle={{
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
@@ -83,7 +100,7 @@ const SelectionWrapper = ({
|
||||
{item.type === "note" ? <Filler item={item} color={color} /> : null}
|
||||
<SelectionIcon item={item} />
|
||||
{children}
|
||||
</PressableButton>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||