mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
1970 Commits
fix-file-e
...
editor/com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26b76f12dc | ||
|
|
97d21c818e | ||
|
|
3beb60d8b6 | ||
|
|
6343212d7e | ||
|
|
f728ad8826 | ||
|
|
b96af6c4cf | ||
|
|
6069a41fa4 | ||
|
|
7190207e5c | ||
|
|
015453a40b | ||
|
|
dad92a186c | ||
|
|
7e5e19e72c | ||
|
|
8e1484a029 | ||
|
|
4a3b3a14ec | ||
|
|
758157cf78 | ||
|
|
a1a171d648 | ||
|
|
92ba4d7624 | ||
|
|
d44ee5ca05 | ||
|
|
d7dde74675 | ||
|
|
5fde1c0f07 | ||
|
|
ffddbe2d7a | ||
|
|
eb3998d72d | ||
|
|
fca61afd7c | ||
|
|
33559250d8 | ||
|
|
38daca0099 | ||
|
|
36c8b8cc25 | ||
|
|
dd4ca817c7 | ||
|
|
594ceb1f52 | ||
|
|
fbc74ec359 | ||
|
|
0dfc1dfa52 | ||
|
|
b9c14ef9aa | ||
|
|
3fec2b397f | ||
|
|
ec841caef7 | ||
|
|
c163fc3be4 | ||
|
|
1e04315596 | ||
|
|
9aef28b03d | ||
|
|
88628c650f | ||
|
|
9523fd52d8 | ||
|
|
8cc6df92f4 | ||
|
|
042656856a | ||
|
|
5921c61e98 | ||
|
|
c5cd214d97 | ||
|
|
b30c35e59d | ||
|
|
ce4fa13758 | ||
|
|
1f47df0f45 | ||
|
|
91109278b6 | ||
|
|
78e4672871 | ||
|
|
dbecd6a466 | ||
|
|
e690973107 | ||
|
|
17b14e0ac6 | ||
|
|
0f8da747f9 | ||
|
|
df7d91e4f3 | ||
|
|
7eefecd961 | ||
|
|
2ec764d862 | ||
|
|
5a9b9da47e | ||
|
|
c5a82bd779 | ||
|
|
9c2a807025 | ||
|
|
4fa1ea4380 | ||
|
|
351d412fdf | ||
|
|
38600bf2a5 | ||
|
|
4720456d63 | ||
|
|
5a2221c5d3 | ||
|
|
91ef98df4f | ||
|
|
68b921887b | ||
|
|
dea91c48a2 | ||
|
|
f2e91d4a5c | ||
|
|
47be00355d | ||
|
|
05c4051cc9 | ||
|
|
71fe6bbebc | ||
|
|
74ed529f16 | ||
|
|
24a6d8c3f9 | ||
|
|
14c1567c07 | ||
|
|
fb778f56ed | ||
|
|
1eb0ef0cbe | ||
|
|
a81c8d0064 | ||
|
|
36099c6b4d | ||
|
|
8f7a15e692 | ||
|
|
de4a660eb2 | ||
|
|
c53fab603f | ||
|
|
a3e11b62e0 | ||
|
|
5d1775151d | ||
|
|
a0f5880e2c | ||
|
|
3dc7e70890 | ||
|
|
2fa1fc8662 | ||
|
|
879a65a41c | ||
|
|
d5b68bf0da | ||
|
|
fd460d3e90 | ||
|
|
a74bd4972c | ||
|
|
0ed8839ea9 | ||
|
|
d7b0f2684a | ||
|
|
b5be9b3abf | ||
|
|
911105c409 | ||
|
|
200c842671 | ||
|
|
5e5930e9e8 | ||
|
|
fd64c1cf2d | ||
|
|
48e2916874 | ||
|
|
a4b0e5bfef | ||
|
|
0b56121a26 | ||
|
|
f3f61d0e19 | ||
|
|
faaf1eeadf | ||
|
|
4fbaecc24e | ||
|
|
4fb056095c | ||
|
|
2d6142381b | ||
|
|
d97d14e830 | ||
|
|
f2f3f7e393 | ||
|
|
50176d2b3a | ||
|
|
ece7b29005 | ||
|
|
8cd8e93487 | ||
|
|
8900a6b080 | ||
|
|
19d90fa221 | ||
|
|
2e5ff859eb | ||
|
|
06747575ee | ||
|
|
98bb8ca186 | ||
|
|
d5f68602b6 | ||
|
|
312999aa5b | ||
|
|
607a4d26f4 | ||
|
|
a88b6615e4 | ||
|
|
f900098201 | ||
|
|
d51aaa8eb1 | ||
|
|
e9a48ae028 | ||
|
|
90a0f619ec | ||
|
|
ed3662a636 | ||
|
|
26eda3b105 | ||
|
|
8f43882e29 | ||
|
|
e239597eb1 | ||
|
|
bf1ef07d3a | ||
|
|
7d2e1dab4c | ||
|
|
80392d394a | ||
|
|
9ffa4d38c3 | ||
|
|
efec2dc898 | ||
|
|
5bd032e480 | ||
|
|
c1bd4cfb01 | ||
|
|
0b943d8ecd | ||
|
|
75e22a591e | ||
|
|
66498e12ce | ||
|
|
d5fefd2f10 | ||
|
|
e2556d3228 | ||
|
|
2eb38d236e | ||
|
|
d9c57162f1 | ||
|
|
9f8f0e8413 | ||
|
|
ea52893be1 | ||
|
|
eef63d2028 | ||
|
|
14e0112e17 | ||
|
|
8ec9034fb2 | ||
|
|
dc5973d75d | ||
|
|
12126b2f9f | ||
|
|
9fa256be0e | ||
|
|
c103328098 | ||
|
|
ea2022bbe4 | ||
|
|
d7ced57319 | ||
|
|
2cc200dc2b | ||
|
|
baa74d0353 | ||
|
|
72b906da14 | ||
|
|
6db8c2fac2 | ||
|
|
09d5c0a65e | ||
|
|
0b6b6a02cc | ||
|
|
f49c673e8e | ||
|
|
64a8e9a075 | ||
|
|
e998b07ca5 | ||
|
|
c189279030 | ||
|
|
12ddd6f958 | ||
|
|
da06afda8a | ||
|
|
bf808b7a2e | ||
|
|
13476a3f50 | ||
|
|
cfdf77a7b5 | ||
|
|
070c90362a | ||
|
|
c8d9858a0c | ||
|
|
2685f60409 | ||
|
|
8d3bb5185c | ||
|
|
1f748a4026 | ||
|
|
b27f064ad4 | ||
|
|
27f781cc63 | ||
|
|
1427958eca | ||
|
|
436e09c255 | ||
|
|
cc9b2dd5b0 | ||
|
|
0ba25419f3 | ||
|
|
9238de4cb4 | ||
|
|
2d5fe1058c | ||
|
|
b01d1fc87e | ||
|
|
9f4c0a4c9a | ||
|
|
9eebda86c2 | ||
|
|
d43e856fea | ||
|
|
7af1596038 | ||
|
|
652f6eab0b | ||
|
|
fdaddfdb92 | ||
|
|
9a1adf2065 | ||
|
|
f10b258f58 | ||
|
|
7f558cbd41 | ||
|
|
641eb28ce7 | ||
|
|
2c59a5f6f9 | ||
|
|
712f02e61e | ||
|
|
e5ad405a23 | ||
|
|
cd1b78d8df | ||
|
|
3b1ca46945 | ||
|
|
96c1636f88 | ||
|
|
a10f949856 | ||
|
|
9ff4757ed6 | ||
|
|
17c1e77f54 | ||
|
|
187a2a7262 | ||
|
|
4621230549 | ||
|
|
d7cb3317a2 | ||
|
|
6d81f26864 | ||
|
|
5219814b5a | ||
|
|
bef9fd0fd0 | ||
|
|
2341293d92 | ||
|
|
215387f8a9 | ||
|
|
534b8c5ad0 | ||
|
|
b7b2ead9eb | ||
|
|
b3e34d68e1 | ||
|
|
39079612aa | ||
|
|
be1af3111f | ||
|
|
8dcff49797 | ||
|
|
687fb4e105 | ||
|
|
25ae780ad8 | ||
|
|
b695367e6c | ||
|
|
bb274a84fe | ||
|
|
364dccbf7a | ||
|
|
105fb0fc69 | ||
|
|
89c408981c | ||
|
|
110534edec | ||
|
|
00e2190b2d | ||
|
|
e5c3fc667e | ||
|
|
9aef8194f9 | ||
|
|
f76e0b7931 | ||
|
|
0457e4417f | ||
|
|
dae8b5c542 | ||
|
|
60107dd77f | ||
|
|
4f3e19aaa2 | ||
|
|
8c621f9f47 | ||
|
|
74544efd62 | ||
|
|
c30ccbb68d | ||
|
|
c77050eccc | ||
|
|
f40ccdae41 | ||
|
|
0a0aa09eea | ||
|
|
e2c55fb758 | ||
|
|
12e3335d52 | ||
|
|
f29439539d | ||
|
|
13c9fdba52 | ||
|
|
2cb087972c | ||
|
|
41a32ede15 | ||
|
|
95f52fba51 | ||
|
|
e2159b7614 | ||
|
|
ee33875edf | ||
|
|
28681fb1b0 | ||
|
|
33194dc9f0 | ||
|
|
7509766df2 | ||
|
|
30a02a4977 | ||
|
|
f67f4138c3 | ||
|
|
49d51da1bd | ||
|
|
17bfe4e075 | ||
|
|
d5905a4e5f | ||
|
|
e1b46d33f4 | ||
|
|
ce7c003c54 | ||
|
|
7f473ce7c8 | ||
|
|
c239f490b0 | ||
|
|
1ad1915fcc | ||
|
|
14029de484 | ||
|
|
5d07db366b | ||
|
|
9e265dbecf | ||
|
|
717bce36e6 | ||
|
|
17831cdd8c | ||
|
|
02e37c6ed4 | ||
|
|
16b52e9633 | ||
|
|
77eee945ca | ||
|
|
f21f235d37 | ||
|
|
2949fcde38 | ||
|
|
6ae1674de5 | ||
|
|
2bd74e043a | ||
|
|
1a05acbe9a | ||
|
|
a0597e13fe | ||
|
|
d11d665395 | ||
|
|
ab2758ac79 | ||
|
|
bb682a6f6e | ||
|
|
4377b8b340 | ||
|
|
eb0c9b823b | ||
|
|
a595b12431 | ||
|
|
ebd9b45cac | ||
|
|
5ae024f20a | ||
|
|
9ae93dec4c | ||
|
|
93ecc9b1ac | ||
|
|
28455f8b31 | ||
|
|
b49be89b06 | ||
|
|
23e0748137 | ||
|
|
2f6d222cca | ||
|
|
ba1cfe05c4 | ||
|
|
6f80334296 | ||
|
|
86da8a3620 | ||
|
|
11702c9209 | ||
|
|
240d9b533f | ||
|
|
8dc12f01b9 | ||
|
|
3b0dce6c6f | ||
|
|
901dc4fe6d | ||
|
|
61748f40b6 | ||
|
|
871824f857 | ||
|
|
032c321568 | ||
|
|
4883f02b62 | ||
|
|
b43a8cbafe | ||
|
|
11c1fe3853 | ||
|
|
4a4576ebd5 | ||
|
|
0b5128b13b | ||
|
|
d1dbb85459 | ||
|
|
ae1005d5f9 | ||
|
|
11e6074e21 | ||
|
|
ea9aa2a136 | ||
|
|
203fe5bee0 | ||
|
|
4262641567 | ||
|
|
c0f79be78c | ||
|
|
239e301042 | ||
|
|
23bb80bbd6 | ||
|
|
d7232a4f1f | ||
|
|
2bfc1af8ff | ||
|
|
3ac6873be4 | ||
|
|
ce487a108c | ||
|
|
827930e28f | ||
|
|
8a13cf01b4 | ||
|
|
641c541305 | ||
|
|
9285bae0cc | ||
|
|
954ac3154f | ||
|
|
571f3242e2 | ||
|
|
9052948b8a | ||
|
|
af91a6ee98 | ||
|
|
16633dbd05 | ||
|
|
8a506edd37 | ||
|
|
fdbac8e608 | ||
|
|
cdca90bd54 | ||
|
|
fb7d8b0893 | ||
|
|
a49bc92a13 | ||
|
|
58c1f5e3ca | ||
|
|
f1a0c1b447 | ||
|
|
d8534da4df | ||
|
|
28daa38d80 | ||
|
|
5c275adda8 | ||
|
|
26ecf2a7fd | ||
|
|
e23c0f55b7 | ||
|
|
a6b03635c7 | ||
|
|
6587541729 | ||
|
|
bc4bfa63f7 | ||
|
|
bdda2d4987 | ||
|
|
ab34073870 | ||
|
|
152a59fe26 | ||
|
|
cf169eb6d6 | ||
|
|
16da396289 | ||
|
|
7ad3fd4057 | ||
|
|
aecf4f1ee8 | ||
|
|
8f48fcd788 | ||
|
|
cf64fb3743 | ||
|
|
a21111368e | ||
|
|
383b914c52 | ||
|
|
555255a923 | ||
|
|
cd5a2ab179 | ||
|
|
48903f924c | ||
|
|
7d46dd4fb0 | ||
|
|
cfa9f11f44 | ||
|
|
1377ac38db | ||
|
|
d88b30b538 | ||
|
|
cfe0cded15 | ||
|
|
322f6b7aae | ||
|
|
62ae26c464 | ||
|
|
c920c999eb | ||
|
|
288f601651 | ||
|
|
d4f902f63d | ||
|
|
7dddebef2e | ||
|
|
dc1498d672 | ||
|
|
eb36c1d7cb | ||
|
|
29c119a945 | ||
|
|
860bc10747 | ||
|
|
215ca23190 | ||
|
|
4b9d9e4353 | ||
|
|
fd5d4e56ea | ||
|
|
9801d117a1 | ||
|
|
3c4fb8cb53 | ||
|
|
ae17b34be9 | ||
|
|
1a544754ef | ||
|
|
1ddfd15ee9 | ||
|
|
035126ab2f | ||
|
|
e157ae8bc8 | ||
|
|
25b0503a25 | ||
|
|
ef235dd0aa | ||
|
|
699080048d | ||
|
|
3c9b315f78 | ||
|
|
1464006531 | ||
|
|
6921fcb3ae | ||
|
|
286f8dd7cc | ||
|
|
d3e3ef5986 | ||
|
|
ea1ffc59b8 | ||
|
|
5524886868 | ||
|
|
68b63cf635 | ||
|
|
74de09e441 | ||
|
|
b82e9598bd | ||
|
|
25889192c4 | ||
|
|
76eac69f94 | ||
|
|
97f8f1e3c3 | ||
|
|
8722b6bc68 | ||
|
|
8e8d2a4083 | ||
|
|
4caac77706 | ||
|
|
0f73f266b2 | ||
|
|
8c70abda04 | ||
|
|
6881a5f432 | ||
|
|
22e9ae6bf8 | ||
|
|
eb73de6d8d | ||
|
|
1af7f1bd7f | ||
|
|
3a0273a12b | ||
|
|
05795ca052 | ||
|
|
a5746c56eb | ||
|
|
80f66ab0ba | ||
|
|
44e62f0eb0 | ||
|
|
a6e107f61d | ||
|
|
31bb78bfca | ||
|
|
8bb9308173 | ||
|
|
46d279ca3b | ||
|
|
0a5bdaaa7f | ||
|
|
3480b8c5da | ||
|
|
32e1fb5f56 | ||
|
|
d94e8d04c8 | ||
|
|
26e2b380b3 | ||
|
|
06710ff416 | ||
|
|
32a7f866c3 | ||
|
|
86b650712e | ||
|
|
0178e455f9 | ||
|
|
39693966c1 | ||
|
|
dc08a39173 | ||
|
|
1be518510a | ||
|
|
e513081010 | ||
|
|
9bc8bddc12 | ||
|
|
986b99f62e | ||
|
|
16092a4185 | ||
|
|
26eafcfee9 | ||
|
|
f20c1ef4f4 | ||
|
|
3f076d90c8 | ||
|
|
df70fee70e | ||
|
|
4df9c7d9f7 | ||
|
|
59916b924c | ||
|
|
aafce93117 | ||
|
|
97ef91abdb | ||
|
|
b426d61c3a | ||
|
|
f08c484d40 | ||
|
|
b03a4559c5 | ||
|
|
d2f491ed66 | ||
|
|
1a2a1da086 | ||
|
|
872f4d7e91 | ||
|
|
bce063e1bd | ||
|
|
e6dc5ddfed | ||
|
|
bcbcd516f6 | ||
|
|
24da82313e | ||
|
|
5fe29af51a | ||
|
|
fbe551eb29 | ||
|
|
c097c4d457 | ||
|
|
a792a3d6e4 | ||
|
|
28f1f276ce | ||
|
|
9470d2b61f | ||
|
|
95795a20c2 | ||
|
|
64b7b57b3e | ||
|
|
5e05d96487 | ||
|
|
18c9912068 | ||
|
|
cd85753252 | ||
|
|
bdb135036c | ||
|
|
5d70f58a0d | ||
|
|
b7d2d66e82 | ||
|
|
6b028ef850 | ||
|
|
8f85795e53 | ||
|
|
a0b7dff5fd | ||
|
|
352018c042 | ||
|
|
2b1ac822ed | ||
|
|
b3c3890443 | ||
|
|
99e642780e | ||
|
|
47088a0a66 | ||
|
|
fd48684396 | ||
|
|
9b25ab8708 | ||
|
|
c725ba8530 | ||
|
|
98287fbfc4 | ||
|
|
3260695a09 | ||
|
|
3a3434b021 | ||
|
|
9f24bd2250 | ||
|
|
94f2a216ad | ||
|
|
aeb4c984eb | ||
|
|
78e3ee5fd2 | ||
|
|
e94a2b2aa1 | ||
|
|
1ddcf44eb0 | ||
|
|
5c5fb2ba2c | ||
|
|
9344eadf34 | ||
|
|
aa4995d871 | ||
|
|
e40c42afc3 | ||
|
|
f818c41a02 | ||
|
|
178d7e7471 | ||
|
|
3f9d954e2f | ||
|
|
48a3eee232 | ||
|
|
f1fcd227e7 | ||
|
|
84af1a9411 | ||
|
|
44d910121c | ||
|
|
1258a95aad | ||
|
|
295ab2c073 | ||
|
|
7aa350a9d2 | ||
|
|
ee76e43fca | ||
|
|
1379193065 | ||
|
|
0fa3598783 | ||
|
|
02960fb5a9 | ||
|
|
a7fa42e944 | ||
|
|
02ab8e3938 | ||
|
|
01c629f612 | ||
|
|
5c120676e1 | ||
|
|
3821dc29ef | ||
|
|
3c51f73427 | ||
|
|
8db373ee34 | ||
|
|
1be6e6b444 | ||
|
|
adcb73a2fc | ||
|
|
4c97b3481d | ||
|
|
38579c37e1 | ||
|
|
4785544e18 | ||
|
|
7812c15b48 | ||
|
|
251145ea74 | ||
|
|
5e4972c1d0 | ||
|
|
64bfc80a42 | ||
|
|
b841c6e0eb | ||
|
|
7241dce7a6 | ||
|
|
8f25f9ba3e | ||
|
|
6ab58c68e1 | ||
|
|
ae5b422965 | ||
|
|
55b6be0f15 | ||
|
|
f9324d9da5 | ||
|
|
2362902675 | ||
|
|
dfe6a73ac1 | ||
|
|
afcb8835d2 | ||
|
|
9dc9f186e1 | ||
|
|
875125cfe4 | ||
|
|
32d9cdd265 | ||
|
|
669f830dd7 | ||
|
|
09deae9dbf | ||
|
|
337582ac70 | ||
|
|
820c78ce81 | ||
|
|
23924a34a3 | ||
|
|
428f1e746a | ||
|
|
565e692af8 | ||
|
|
94e4721663 | ||
|
|
8157c0812a | ||
|
|
bc36837cd0 | ||
|
|
33540b000c | ||
|
|
5a63a45d00 | ||
|
|
6eb1f2a4a7 | ||
|
|
8fce6b5eb6 | ||
|
|
5bd1999269 | ||
|
|
4e44661e1b | ||
|
|
827ca2e427 | ||
|
|
81fd1c097e | ||
|
|
aaf82f359b | ||
|
|
aa7f14ae79 | ||
|
|
0ad63da4a2 | ||
|
|
2885b9ac3e | ||
|
|
1ae9e04e7a | ||
|
|
693311a3db | ||
|
|
a15ab99056 | ||
|
|
222cdbfa63 | ||
|
|
a1d8b34fb4 | ||
|
|
35d08a6dfa | ||
|
|
874352e753 | ||
|
|
6bb5e44442 | ||
|
|
49d1ab4be5 | ||
|
|
54a0d0fd0c | ||
|
|
65a80dbd46 | ||
|
|
dfeb01320d | ||
|
|
e55456f492 | ||
|
|
d33209afcc | ||
|
|
3f9fce0d34 | ||
|
|
2ae094cf1f | ||
|
|
5530270b06 | ||
|
|
bc224f1674 | ||
|
|
8bb1582cca | ||
|
|
c759408fdc | ||
|
|
d02125a93c | ||
|
|
30553ed0f1 | ||
|
|
d91ce4e936 | ||
|
|
b315e70e1e | ||
|
|
deb902549b | ||
|
|
d6c6db68ba | ||
|
|
e41ed07805 | ||
|
|
f6e8dc0a88 | ||
|
|
0738b96161 | ||
|
|
044dd5ce59 | ||
|
|
6cca06fcbc | ||
|
|
50feebc0b0 | ||
|
|
b24830666c | ||
|
|
470802c3e2 | ||
|
|
0e869708f7 | ||
|
|
7e0e1eac93 | ||
|
|
58cbf653b2 | ||
|
|
4b35f2ecd2 | ||
|
|
48d9a863ec | ||
|
|
1d616cf68f | ||
|
|
efda6f86ae | ||
|
|
cdf6a4bb5b | ||
|
|
1b3735e2b2 | ||
|
|
54008a0de2 | ||
|
|
850fc6b066 | ||
|
|
67a398f583 | ||
|
|
8a62070c8e | ||
|
|
8aa31b8380 | ||
|
|
f445a963f9 | ||
|
|
d6e3f27612 | ||
|
|
4688d74edb | ||
|
|
e88c755ba8 | ||
|
|
1985007039 | ||
|
|
039c8faabe | ||
|
|
fb1eb234b6 | ||
|
|
7e70cd96b9 | ||
|
|
f9a89783ed | ||
|
|
26282352cb | ||
|
|
7f3c74b55b | ||
|
|
301ac781d6 | ||
|
|
ce3c98ae9b | ||
|
|
1c3a6ffbde | ||
|
|
2447b3bea7 | ||
|
|
63888ff70e | ||
|
|
c6e3d1bd47 | ||
|
|
e03e02d702 | ||
|
|
1abf51ae2b | ||
|
|
c41dfa3185 | ||
|
|
52a07da230 | ||
|
|
f572bb1637 | ||
|
|
773ede4b6c | ||
|
|
e4d42c8bac | ||
|
|
5b85067e15 | ||
|
|
0d111f3a79 | ||
|
|
f6b6f2b4b7 | ||
|
|
d55a2aa04d | ||
|
|
0185eea740 | ||
|
|
d44e8874b8 | ||
|
|
e34d33dd61 | ||
|
|
4b2fb80c23 | ||
|
|
acbac95818 | ||
|
|
3c908ac079 | ||
|
|
563f068948 | ||
|
|
6bf4300372 | ||
|
|
1344b958cb | ||
|
|
32d7cab70c | ||
|
|
6f8f6d47f8 | ||
|
|
04a48979eb | ||
|
|
1293fa2dec | ||
|
|
ff3a8fa738 | ||
|
|
8542cbf37e | ||
|
|
62c6fba9a0 | ||
|
|
2d84154226 | ||
|
|
c4e306d569 | ||
|
|
83b6c6de1c | ||
|
|
e86f7491ef | ||
|
|
5151d83af9 | ||
|
|
e462cb0596 | ||
|
|
a30c0e4608 | ||
|
|
dbe8c1b9e6 | ||
|
|
f90e7982f7 | ||
|
|
40c69d7cba | ||
|
|
c9e6f3e532 | ||
|
|
23ee50f688 | ||
|
|
a7c857fb71 | ||
|
|
0f6622360c | ||
|
|
578bc6e546 | ||
|
|
70fd038207 | ||
|
|
b30110a48a | ||
|
|
21978c4c1d | ||
|
|
ec48f2350d | ||
|
|
fe1e3bb847 | ||
|
|
69776f26d8 | ||
|
|
ab4092c088 | ||
|
|
4948a8d7de | ||
|
|
0a607bce18 | ||
|
|
e59820e3d6 | ||
|
|
85a2d25c1b | ||
|
|
3bc7fc84f9 | ||
|
|
249ce1ecbb | ||
|
|
be7f8d9916 | ||
|
|
0a0495f1a7 | ||
|
|
2f7afa8556 | ||
|
|
2f40905372 | ||
|
|
a6d0177153 | ||
|
|
b9e24888e7 | ||
|
|
976199e5c6 | ||
|
|
9017dae664 | ||
|
|
94acc241d9 | ||
|
|
765f3a476d | ||
|
|
449287e25a | ||
|
|
db2669d699 | ||
|
|
2cdb916233 | ||
|
|
5f14261aba | ||
|
|
5ae7855c7f | ||
|
|
20138505c4 | ||
|
|
49c09bfd85 | ||
|
|
d8ed47ddb8 | ||
|
|
39bfc41d33 | ||
|
|
68a8ac588f | ||
|
|
49d9ece15a | ||
|
|
475edf6732 | ||
|
|
76ea89b668 | ||
|
|
56b3a8df6b | ||
|
|
e02de0c0f6 | ||
|
|
84bfab2a72 | ||
|
|
a6825a2df2 | ||
|
|
f9dc539057 | ||
|
|
833b40bdc3 | ||
|
|
9ba58d1a46 | ||
|
|
29a99d3f97 | ||
|
|
a8b6fe78ba | ||
|
|
4abd69b460 | ||
|
|
0ca1d82861 | ||
|
|
3dd25b9f83 | ||
|
|
5c55b49976 | ||
|
|
c932961dd4 | ||
|
|
20cd5bef16 | ||
|
|
9b058bb17f | ||
|
|
47357c3e25 | ||
|
|
3ec5a1cf2c | ||
|
|
13d8a7baf7 | ||
|
|
19b1e74d06 | ||
|
|
82de9b9b94 | ||
|
|
2325f4e31c | ||
|
|
867af58695 | ||
|
|
cdd50f0258 | ||
|
|
8c8b92cc4e | ||
|
|
c0a3df0aa8 | ||
|
|
31dd1619f6 | ||
|
|
ba6be6c84d | ||
|
|
b126b98eb0 | ||
|
|
afb010e3ae | ||
|
|
f14e3d0f5e | ||
|
|
ddebdcda43 | ||
|
|
29c69418e9 | ||
|
|
3e565e5195 | ||
|
|
a13878d4a8 | ||
|
|
1b352d6162 | ||
|
|
258aa77cdd | ||
|
|
bc8de87cda | ||
|
|
7789bd145c | ||
|
|
c1457cc83f | ||
|
|
3ca0089ee8 | ||
|
|
29c9476c16 | ||
|
|
9e72f51851 | ||
|
|
a349611a30 | ||
|
|
1cd21087c2 | ||
|
|
eecb8f9c05 | ||
|
|
e36fd28f47 | ||
|
|
7ce44f4d84 | ||
|
|
6d8e4e8ebc | ||
|
|
deb507859a | ||
|
|
a165675b2a | ||
|
|
8b316d0abf | ||
|
|
def5059ff1 | ||
|
|
e1febfae31 | ||
|
|
82aa506902 | ||
|
|
de1aa40ecf | ||
|
|
53724b437e | ||
|
|
1592620141 | ||
|
|
28ef87a95b | ||
|
|
d4019eea99 | ||
|
|
d13b46cbba | ||
|
|
f1eb215bb9 | ||
|
|
92c7e1a0c1 | ||
|
|
b81cc178b5 | ||
|
|
dd42b82dbf | ||
|
|
ca6b50081a | ||
|
|
4fa27046bb | ||
|
|
8cadc6eaa4 | ||
|
|
ccae629833 | ||
|
|
4a1cbe632c | ||
|
|
579813d93a | ||
|
|
00d00c02ea | ||
|
|
765949cc40 | ||
|
|
87cea6a120 | ||
|
|
83526d4d46 | ||
|
|
fca4faa114 | ||
|
|
171628c7b9 | ||
|
|
d456132dd6 | ||
|
|
4ac7941b6c | ||
|
|
e7f5463917 | ||
|
|
6e41ce459d | ||
|
|
f5095fa643 | ||
|
|
e6a5b85011 | ||
|
|
409c5a2517 | ||
|
|
35cd6b101d | ||
|
|
2440e28168 | ||
|
|
0078d509c5 | ||
|
|
0c2d57716d | ||
|
|
678aeb98c3 | ||
|
|
5f422f4eb7 | ||
|
|
2b26524fcd | ||
|
|
7326533cc1 | ||
|
|
c8aae5be61 | ||
|
|
be1a8bf849 | ||
|
|
2ffc2a27a5 | ||
|
|
b0a9f3320b | ||
|
|
0ef844f08c | ||
|
|
bbe00b0f9a | ||
|
|
4dcaf70346 | ||
|
|
76e5e3758a | ||
|
|
f460772d75 | ||
|
|
1cf1605564 | ||
|
|
0abbd41ff2 | ||
|
|
f16818ea93 | ||
|
|
719ab6dff3 | ||
|
|
ed50d8041e | ||
|
|
9a0de8dff8 | ||
|
|
caba714798 | ||
|
|
154cf897b3 | ||
|
|
323e53a1ef | ||
|
|
9b097cf233 | ||
|
|
75cb2fe194 | ||
|
|
7aa975457c | ||
|
|
1932d27f1c | ||
|
|
f226bdbff1 | ||
|
|
7843943130 | ||
|
|
a92afab3fc | ||
|
|
1354ee683f | ||
|
|
891e311374 | ||
|
|
881099c2c9 | ||
|
|
2dde6b6150 | ||
|
|
a1f75dfed3 | ||
|
|
493ee81c42 | ||
|
|
7109a36006 | ||
|
|
6fccac9aea | ||
|
|
fa43f97bb4 | ||
|
|
e3a973d61e | ||
|
|
542a628435 | ||
|
|
fdd25a11e7 | ||
|
|
449ae78700 | ||
|
|
6bda4a9a73 | ||
|
|
2d316ed1ec | ||
|
|
570ae00453 | ||
|
|
c845f7c1e1 | ||
|
|
659e963b39 | ||
|
|
63b8f7ef91 | ||
|
|
841e229866 | ||
|
|
4098a27df0 | ||
|
|
918824f26b | ||
|
|
83735705d2 | ||
|
|
8ac6ccb701 | ||
|
|
9ae00db676 | ||
|
|
c389a1ce4d | ||
|
|
817a58611e | ||
|
|
bfa0f09fa0 | ||
|
|
0e461c19ed | ||
|
|
7b289f8e58 | ||
|
|
071052ce37 | ||
|
|
ec6ad79586 | ||
|
|
96205fd5b7 | ||
|
|
4028a29bdb | ||
|
|
d812024c25 | ||
|
|
92f4e33ac7 | ||
|
|
ad056e0e3b | ||
|
|
dc40601fb5 | ||
|
|
65f3338df4 | ||
|
|
4f9a5cb074 | ||
|
|
eb550e95dc | ||
|
|
84dbab5bdf | ||
|
|
c2aff26dfc | ||
|
|
5dd9ce9f25 | ||
|
|
e5998f51e9 | ||
|
|
538f1f1910 | ||
|
|
9c2fa4eb30 | ||
|
|
ab04b13582 | ||
|
|
a6ef69905e | ||
|
|
565a2396f1 | ||
|
|
5a395aca3c | ||
|
|
f99ee88499 | ||
|
|
dc7e5dfa50 | ||
|
|
4f1bb994f2 | ||
|
|
6357a8f3a1 | ||
|
|
6c083bd76d | ||
|
|
c53fbb7441 | ||
|
|
6c28075070 | ||
|
|
bb027c6f25 | ||
|
|
7dc5daffe8 | ||
|
|
3e4862d768 | ||
|
|
29b0c2eeda | ||
|
|
08e5f889f8 | ||
|
|
bc198b4cd7 | ||
|
|
5c71b77b98 | ||
|
|
5660178760 | ||
|
|
8dac907ead | ||
|
|
5b7c8eaa70 | ||
|
|
7ab4a2210a | ||
|
|
c861b11c5f | ||
|
|
4f540dfdcb | ||
|
|
0e0add2f71 | ||
|
|
29edb3a3c3 | ||
|
|
ff723e5c78 | ||
|
|
ea76929c80 | ||
|
|
5d4d84bded | ||
|
|
0faf267735 | ||
|
|
197e95e66f | ||
|
|
9d4ae71bfe | ||
|
|
422869c3d2 | ||
|
|
b7d478088b | ||
|
|
1827cbeb93 | ||
|
|
66041a45f6 | ||
|
|
ccb0c858a2 | ||
|
|
659b937852 | ||
|
|
de48cea9eb | ||
|
|
404e3147b0 | ||
|
|
c0d64ff674 | ||
|
|
bbcce64549 | ||
|
|
1ca30b8a5b | ||
|
|
22b037ea1d | ||
|
|
2d74e3d4c0 | ||
|
|
1be0e4790e | ||
|
|
3770091b58 | ||
|
|
0c86f11e21 | ||
|
|
73bf283fd7 | ||
|
|
208bd13cdd | ||
|
|
42887a1dd5 | ||
|
|
0ec2f07be0 | ||
|
|
82b70d2951 | ||
|
|
8da961f300 | ||
|
|
4fba380f2d | ||
|
|
e1b046ec0c | ||
|
|
5d01159e20 | ||
|
|
aeef53df55 | ||
|
|
29433c4149 | ||
|
|
0edac2ee9d | ||
|
|
4607dc3084 | ||
|
|
41babda9b7 | ||
|
|
0fec38610c | ||
|
|
3ecc43d976 | ||
|
|
9730560fb7 | ||
|
|
6c0e51dc9d | ||
|
|
2a313a0026 | ||
|
|
5d4e60b829 | ||
|
|
c0666d8547 | ||
|
|
a5fa261e3b | ||
|
|
fa8aa4d88f | ||
|
|
b63be2de15 | ||
|
|
173caf41d0 | ||
|
|
30e4f55218 | ||
|
|
f3895f534e | ||
|
|
ea14db77de | ||
|
|
9bb48f4896 | ||
|
|
62b5cc853b | ||
|
|
90900628a2 | ||
|
|
ebf98bc652 | ||
|
|
ddd3fe6ac5 | ||
|
|
2964e7a8f0 | ||
|
|
f6d1f5890a | ||
|
|
1cc0cf19c8 | ||
|
|
efc4f3566a | ||
|
|
85b7760e1e | ||
|
|
40ee0ecf47 | ||
|
|
513c9ee8a7 | ||
|
|
2aacadc24d | ||
|
|
716bb67b96 | ||
|
|
916b6a17aa | ||
|
|
813401ea91 | ||
|
|
70e5efbcea | ||
|
|
6c8dc223f2 | ||
|
|
5b09528e7e | ||
|
|
5de5ffe068 | ||
|
|
1b511c8ddd | ||
|
|
68d1a3f50e | ||
|
|
74edd466d4 | ||
|
|
34aa21790c | ||
|
|
e7ec9e2aa3 | ||
|
|
20bbb6dd5b | ||
|
|
f937b164e9 | ||
|
|
5b4e888ddd | ||
|
|
410d983842 | ||
|
|
16cd92f03a | ||
|
|
c6ee1e6303 | ||
|
|
0f00d0e508 | ||
|
|
21fdc39db7 | ||
|
|
bb5d5a5731 | ||
|
|
bf5e68a82e | ||
|
|
cc7f479627 | ||
|
|
9eb183f6b5 | ||
|
|
cb29d71619 | ||
|
|
a6120ed90f | ||
|
|
48040c955d | ||
|
|
18b43882e1 | ||
|
|
37dc409b60 | ||
|
|
8ab05c5a98 | ||
|
|
38509c66c5 | ||
|
|
fc40a05254 | ||
|
|
8078c3ed3c | ||
|
|
57280f3ebb | ||
|
|
dc54644b4f | ||
|
|
ce31b0495d | ||
|
|
a62d4b5d4b | ||
|
|
6f585c5e17 | ||
|
|
ffe7d944a6 | ||
|
|
bf0d6857d3 | ||
|
|
69f117bab7 | ||
|
|
cac945501d | ||
|
|
85a0762ccf | ||
|
|
0ce51bf481 | ||
|
|
6adc5065fb | ||
|
|
3b90a6d90e | ||
|
|
aa5e6e7199 | ||
|
|
ca53ec38d1 | ||
|
|
80a3cc2fc1 | ||
|
|
32c4e987dc | ||
|
|
ce0ef0a1cf | ||
|
|
95fc9bd3d0 | ||
|
|
fb21fef82f | ||
|
|
7760f5e898 | ||
|
|
fcc9915b04 | ||
|
|
8347dd5db7 | ||
|
|
c2d3ba087a | ||
|
|
e5883f179c | ||
|
|
f7f1d3b3f0 | ||
|
|
df3674f570 | ||
|
|
a62beab172 | ||
|
|
528810155f | ||
|
|
5c80b5300e | ||
|
|
36c33160f4 | ||
|
|
5cca28daa1 | ||
|
|
5298ccd638 | ||
|
|
a5db1797e3 | ||
|
|
b1d034cf3f | ||
|
|
4620ed0c1c | ||
|
|
5c87b6f3b0 | ||
|
|
29ad783b3f | ||
|
|
3eae13e237 | ||
|
|
ee222b6302 | ||
|
|
ae795a837d | ||
|
|
0396264842 | ||
|
|
e9fcd16fb5 | ||
|
|
944fcf0567 | ||
|
|
251cc08a7d | ||
|
|
d2719123eb | ||
|
|
88c0e2a2ce | ||
|
|
b4366058dd | ||
|
|
b103cc09f2 | ||
|
|
d35c0be116 | ||
|
|
c9cf15e519 | ||
|
|
408ae26b76 | ||
|
|
3159d8fcc5 | ||
|
|
b976788f07 | ||
|
|
a0311c1516 | ||
|
|
e17351f702 | ||
|
|
ffafae9970 | ||
|
|
fa3d993ffd | ||
|
|
2b7b8d526e | ||
|
|
270394dc36 | ||
|
|
552e8f50b7 | ||
|
|
b6d0d124fd | ||
|
|
7e3abc5819 | ||
|
|
6b3065617d | ||
|
|
9752ccf7cb | ||
|
|
ad1921a938 | ||
|
|
ed2201d525 | ||
|
|
09b9719a3f | ||
|
|
673ea6da86 | ||
|
|
77d81ecc12 | ||
|
|
00c38b6bcc | ||
|
|
24d9568c5d | ||
|
|
3b89b0c436 | ||
|
|
940a3de130 | ||
|
|
81b291ae37 | ||
|
|
f5ad31f0a3 | ||
|
|
59d4b92839 | ||
|
|
530c864d10 | ||
|
|
fae6bf8f43 | ||
|
|
c395bc829f | ||
|
|
19ee33ec3a | ||
|
|
64ab6a3436 | ||
|
|
b758cd28e5 | ||
|
|
db8e21f83b | ||
|
|
c23f31f284 | ||
|
|
84d5420b51 | ||
|
|
02bdc61543 | ||
|
|
71811ebf0d | ||
|
|
b07d7e4f3f | ||
|
|
b141907066 | ||
|
|
a0b46d49d6 | ||
|
|
85386c945f | ||
|
|
9de1373bdd | ||
|
|
6f4c35672d | ||
|
|
ed32552155 | ||
|
|
88858308fb | ||
|
|
78f875ab0f | ||
|
|
9104c64170 | ||
|
|
aabb33a435 | ||
|
|
ad43591f2c | ||
|
|
8bc779d882 | ||
|
|
193bddbe9e | ||
|
|
e03d394555 | ||
|
|
72e4116773 | ||
|
|
9b682163db | ||
|
|
076312da7a | ||
|
|
856a9b16e9 | ||
|
|
9de4e3eba6 | ||
|
|
d90d263b04 | ||
|
|
0170e51843 | ||
|
|
b7f5a5dc66 | ||
|
|
20866ab2f5 | ||
|
|
4efc13a602 | ||
|
|
96303f5d0f | ||
|
|
020c9b254e | ||
|
|
24c0f2f409 | ||
|
|
1b608f01ac | ||
|
|
fd98ab0a64 | ||
|
|
3d1dc879e0 | ||
|
|
16253b8bf7 | ||
|
|
3dba3079f7 | ||
|
|
1fe4ae199f | ||
|
|
55842f1265 | ||
|
|
6644940115 | ||
|
|
dfd0dc72b8 | ||
|
|
d7c4b9c776 | ||
|
|
728553115c | ||
|
|
6ee0d28a8b | ||
|
|
ead23c36c6 | ||
|
|
e62891b9c0 | ||
|
|
9483c63cdc | ||
|
|
3eab773fd1 | ||
|
|
30dd0e8657 | ||
|
|
5d05df7f83 | ||
|
|
ebf80aacc5 | ||
|
|
55c6be4622 | ||
|
|
8f1545916f | ||
|
|
7f05043e03 | ||
|
|
e663655924 | ||
|
|
2fab9dc6f1 | ||
|
|
aba79c11d3 | ||
|
|
4e897dde47 | ||
|
|
3375439e35 | ||
|
|
89dce0b655 | ||
|
|
ff53ea322d | ||
|
|
d6f6bc2c5b | ||
|
|
905080e743 | ||
|
|
d33f3c4769 | ||
|
|
b6a1c9b73b | ||
|
|
20823f103c | ||
|
|
25a5d3277b | ||
|
|
72a80c8f57 | ||
|
|
a082c7d33a | ||
|
|
99cac4b041 | ||
|
|
f804d7848f | ||
|
|
446841e0f5 | ||
|
|
1d347fe26c | ||
|
|
abfa5d719a | ||
|
|
32bc6ed328 | ||
|
|
ea2bc359cb | ||
|
|
c540cb0fe4 | ||
|
|
1642495754 | ||
|
|
b034a59422 | ||
|
|
2486c055e4 | ||
|
|
79b43b1058 | ||
|
|
b30fa436bd | ||
|
|
9e79a311cf | ||
|
|
2f695d0b34 | ||
|
|
952a484994 | ||
|
|
74b5e42492 | ||
|
|
7f8dc88c5a | ||
|
|
c9a9b8dffb | ||
|
|
b315a834c2 | ||
|
|
8f84c85c23 | ||
|
|
707ff935fe | ||
|
|
39af23b349 | ||
|
|
318b95beec | ||
|
|
174bb9a935 | ||
|
|
f0fd20051e | ||
|
|
e3d72bd271 | ||
|
|
4dcd0cd356 | ||
|
|
e3793e67aa | ||
|
|
3f344c7249 | ||
|
|
8a1af7d01f | ||
|
|
117fc44f00 | ||
|
|
703456b808 | ||
|
|
b90cc90ea8 | ||
|
|
45730e2a07 | ||
|
|
c0272b9755 | ||
|
|
a27a8804c4 | ||
|
|
073d23aed3 | ||
|
|
7d9a795a1c | ||
|
|
8359fb8919 | ||
|
|
7c0ffff93f | ||
|
|
46d2ea508c | ||
|
|
fb48faa3af | ||
|
|
048bb36165 | ||
|
|
0f01a03587 | ||
|
|
fe17a6e9da | ||
|
|
a17414dd89 | ||
|
|
5d3f2182f0 | ||
|
|
03cd320665 | ||
|
|
2415ede4fa | ||
|
|
844efd5958 | ||
|
|
4ba88b6f03 | ||
|
|
d17d0c190b | ||
|
|
beec9fab22 | ||
|
|
710b3c412f | ||
|
|
6bf31bf0ea | ||
|
|
2f8bef5f3e | ||
|
|
2dcf886322 | ||
|
|
0a362e9ce7 | ||
|
|
e186615163 | ||
|
|
7abf19071d | ||
|
|
30838749e1 | ||
|
|
06df49d5d7 | ||
|
|
2c5e1f611b | ||
|
|
0cc33f7a81 | ||
|
|
8a431fb79b | ||
|
|
12cfc0dafd | ||
|
|
93c8a1827c | ||
|
|
ca60eda7f2 | ||
|
|
c22c51791e | ||
|
|
d5a719c9f6 | ||
|
|
904c452cfe | ||
|
|
2078ef9081 | ||
|
|
fc9ee5bee1 | ||
|
|
d27595cfd9 | ||
|
|
b618386e6b | ||
|
|
2e79b2c7a2 | ||
|
|
4e4990bd98 | ||
|
|
4c8a44353a | ||
|
|
7dd8b13bc8 | ||
|
|
e1aa3b59be | ||
|
|
d79888793a | ||
|
|
b56e381022 | ||
|
|
15b32b312d | ||
|
|
6a0a00f126 | ||
|
|
9876c2ef6d | ||
|
|
fba29e3c80 | ||
|
|
b8275f5a3f | ||
|
|
b7e8365117 | ||
|
|
8df7b5c56b | ||
|
|
04519d1db7 | ||
|
|
7b808bea0b | ||
|
|
2878f1d9cf | ||
|
|
651f6011b8 | ||
|
|
f17cbb5aa3 | ||
|
|
a2a4a165a5 | ||
|
|
9c7ff38f99 | ||
|
|
76f27adbea | ||
|
|
85a5f38590 | ||
|
|
dcdd876a4c | ||
|
|
d055908809 | ||
|
|
89806148b2 | ||
|
|
433be12b2b | ||
|
|
9ff235e767 | ||
|
|
9b6f7ff225 | ||
|
|
99f97ac565 | ||
|
|
7e598098fe | ||
|
|
02afbf6c90 | ||
|
|
b464fafc18 | ||
|
|
e06320108b | ||
|
|
a377b13e2b | ||
|
|
4728212531 | ||
|
|
71fc2a3cac | ||
|
|
303282624e | ||
|
|
2ca02937df | ||
|
|
00b62df2e8 | ||
|
|
6d377ded9a | ||
|
|
087c404225 | ||
|
|
4932763468 | ||
|
|
abcc19d5da | ||
|
|
f1ce1e9b60 | ||
|
|
5a6f99a41b | ||
|
|
1862802052 | ||
|
|
1adddbdb4c | ||
|
|
15f4f2accb | ||
|
|
725f3c3522 | ||
|
|
efa73a1c32 | ||
|
|
00250c6669 | ||
|
|
d11afda1a8 | ||
|
|
1350784c33 | ||
|
|
1852a216a2 | ||
|
|
422c2492c4 | ||
|
|
c9c72741b3 | ||
|
|
99dbcaff7d | ||
|
|
8d83f5474d | ||
|
|
a912a1a22e | ||
|
|
ea7afbe93e | ||
|
|
7b2a8c3e20 | ||
|
|
911adfc151 | ||
|
|
23f710856e | ||
|
|
7f6274821c | ||
|
|
4700843517 | ||
|
|
8823054227 | ||
|
|
7c63a3e004 | ||
|
|
2a59154268 | ||
|
|
ed15d53484 | ||
|
|
fc8a4bb55d | ||
|
|
dd1c1bb11d | ||
|
|
3ffbad5727 | ||
|
|
207ae39d13 | ||
|
|
ae141ac9c5 | ||
|
|
40f058b14b | ||
|
|
865c9f3e32 | ||
|
|
26df9bfdd8 | ||
|
|
1e52e01fb8 | ||
|
|
5fece5faaa | ||
|
|
d7fdaf6c46 | ||
|
|
8e1faf9f60 | ||
|
|
9dce39b32d | ||
|
|
80a0b75887 | ||
|
|
c9704e1031 | ||
|
|
31e32cd154 | ||
|
|
61c296fc37 | ||
|
|
c8d38235b2 | ||
|
|
b717a64607 | ||
|
|
0f2b1d3e14 | ||
|
|
4abefe63db | ||
|
|
fc31b2a8d7 | ||
|
|
2708bac4fb | ||
|
|
a47967dd53 | ||
|
|
53ba5562d3 | ||
|
|
347df32943 | ||
|
|
ac8c63526b | ||
|
|
a48bd360ca | ||
|
|
f476969e9e | ||
|
|
c45539dea6 | ||
|
|
d8f5815b84 | ||
|
|
2eaf07118a | ||
|
|
b636b09a96 | ||
|
|
c16bbcaad5 | ||
|
|
49ffcbea03 | ||
|
|
f772cf0f14 | ||
|
|
3f1406a05b | ||
|
|
21760b2b74 | ||
|
|
d8011f0239 | ||
|
|
b9a8b054e2 | ||
|
|
2ab321869e | ||
|
|
fcb45a24d6 | ||
|
|
7cdb13def6 | ||
|
|
2ab58f9203 | ||
|
|
df74448e17 | ||
|
|
ada98fca93 | ||
|
|
1d3650659f | ||
|
|
9b43af475b | ||
|
|
d9c592c7fb | ||
|
|
f10ac9fbb5 | ||
|
|
b7334c09d4 | ||
|
|
0cd3bd5739 | ||
|
|
33d69480eb | ||
|
|
5cae4eb0d9 | ||
|
|
840c7fda5e | ||
|
|
4ce24ac8fd | ||
|
|
ae9e80e12e | ||
|
|
43356e5b6a | ||
|
|
7b13f35424 | ||
|
|
0517c6ce41 | ||
|
|
54d06328c1 | ||
|
|
c74677acd4 | ||
|
|
7689e1a7c5 | ||
|
|
5ffae2a08b | ||
|
|
5fa80abdc8 | ||
|
|
a30598fedf | ||
|
|
48ffecfb5f | ||
|
|
89fe712104 | ||
|
|
5dd298ee86 | ||
|
|
275b285a8b | ||
|
|
96310a46e9 | ||
|
|
42f9bf1593 | ||
|
|
25aab6913a | ||
|
|
3bd7da68be | ||
|
|
a218738eff | ||
|
|
8bf283b3bc | ||
|
|
cb09430ff4 | ||
|
|
66d75492bb | ||
|
|
d772d38dc7 | ||
|
|
46a74f9512 | ||
|
|
7df1037e3f | ||
|
|
5c3136d07d | ||
|
|
6a9ddb770e | ||
|
|
f4c54fd27f | ||
|
|
05b12df7d7 | ||
|
|
dcf053eede | ||
|
|
5430966b02 | ||
|
|
7271140627 | ||
|
|
eb5ae0773b | ||
|
|
9661dd0c5e | ||
|
|
8e70860918 | ||
|
|
3540ecf4ab | ||
|
|
f1dba9ffc2 | ||
|
|
3f1761a540 | ||
|
|
a25d21038a | ||
|
|
d55a6a6fc0 | ||
|
|
e5cf902780 | ||
|
|
a99996fcf6 | ||
|
|
0cc9c31bc3 | ||
|
|
62da8484ad | ||
|
|
821b8eebaa | ||
|
|
269941c297 | ||
|
|
e37ff88f1f | ||
|
|
1a1b4bc43c | ||
|
|
2c59df9fb7 | ||
|
|
4fc30cc224 | ||
|
|
9bd7d392af | ||
|
|
3bcd288bc7 | ||
|
|
3b86e51c49 | ||
|
|
c286b1d595 | ||
|
|
2f6b1e6bcc | ||
|
|
abb918e179 | ||
|
|
dd0dc09977 | ||
|
|
211ab47165 | ||
|
|
bc44e28e1e | ||
|
|
ade60e4272 | ||
|
|
3f5bfcb499 | ||
|
|
782a979397 | ||
|
|
e7f1e2d533 | ||
|
|
12e5f2acc1 | ||
|
|
dccc79ace3 | ||
|
|
a9a5ba09ff | ||
|
|
a6c8bbbf0a | ||
|
|
e19e9eb96f | ||
|
|
9208140b97 | ||
|
|
8241bb8ae5 | ||
|
|
329c2e220f | ||
|
|
140e343289 | ||
|
|
0d99367a93 | ||
|
|
b802b3e165 | ||
|
|
46583e12d9 | ||
|
|
c9a3f31e8f | ||
|
|
53f68aaf81 | ||
|
|
f31df9a6cc | ||
|
|
c812581bb9 | ||
|
|
1e6e940f17 | ||
|
|
9ab670d933 | ||
|
|
c46e57418b | ||
|
|
83557401f5 | ||
|
|
2277644e89 | ||
|
|
5eb09a1d3e | ||
|
|
d957be7a7b | ||
|
|
44ddb49d0e | ||
|
|
24fcd5cd0c | ||
|
|
2a34225d49 | ||
|
|
e81c825fc7 | ||
|
|
c80286b587 | ||
|
|
e85f8b60b0 | ||
|
|
5daa99d969 | ||
|
|
073bb576b5 | ||
|
|
eaeac130df | ||
|
|
b21519c24e | ||
|
|
4f9fe9b1e0 | ||
|
|
f657380ec7 | ||
|
|
05b2e559d8 | ||
|
|
ab21f02c2d | ||
|
|
12b51ed72a | ||
|
|
b672fd540c | ||
|
|
95a20b3740 | ||
|
|
6f5a7a4923 | ||
|
|
1b4d98ac0e | ||
|
|
ef2ed2bba0 | ||
|
|
abf4612977 | ||
|
|
9c6637908d | ||
|
|
e1006ed501 | ||
|
|
7d034f7f16 | ||
|
|
76eec5f8bb | ||
|
|
698866f53d | ||
|
|
21e1e73bee | ||
|
|
52e89c7842 | ||
|
|
8b357559fa | ||
|
|
2be35fadff | ||
|
|
5fe366f4f3 | ||
|
|
a13cbadd44 | ||
|
|
aa993a2fce | ||
|
|
975af5701b | ||
|
|
3add56cfe4 | ||
|
|
522ea1520d | ||
|
|
c680121eb0 | ||
|
|
a9de0696c9 | ||
|
|
4a4c52e4c8 | ||
|
|
92f5e04788 | ||
|
|
0cad098551 | ||
|
|
46105b2175 | ||
|
|
8ffbda9626 | ||
|
|
1fc55a69c9 | ||
|
|
c417d4df91 | ||
|
|
43ccf72773 | ||
|
|
a177d36571 | ||
|
|
4428ccd3a0 | ||
|
|
218ffbd758 | ||
|
|
5ace95c9d1 | ||
|
|
77a2f86119 | ||
|
|
71db85458d | ||
|
|
0be59c71de | ||
|
|
2357e3de0b | ||
|
|
53b89a59ed | ||
|
|
202c9135c2 | ||
|
|
69e6263d34 | ||
|
|
ebe830bbcc | ||
|
|
44bb9752d2 | ||
|
|
2f6af50fe2 | ||
|
|
1d80ca7d3c | ||
|
|
d33baebac8 | ||
|
|
25aaef361a | ||
|
|
f3af39a093 | ||
|
|
149c2013a2 | ||
|
|
8fb0b6f94a | ||
|
|
f09e5cadcf | ||
|
|
724c69655b | ||
|
|
800f3ca5d3 | ||
|
|
193007370a | ||
|
|
c8aff3764b | ||
|
|
d72b502977 | ||
|
|
263c970320 | ||
|
|
aeebc9878d | ||
|
|
4f37c92ecd | ||
|
|
3180a88ef0 | ||
|
|
6672db6ac2 | ||
|
|
b6da62469e | ||
|
|
e67e6374c3 | ||
|
|
ff1fc5b391 | ||
|
|
2f8bf191aa | ||
|
|
c4af2b2159 | ||
|
|
690b157387 | ||
|
|
5a276a43a4 | ||
|
|
a364ae2e9d | ||
|
|
1e2055c81b | ||
|
|
31d110252c | ||
|
|
702976c6cf | ||
|
|
3ec8500515 | ||
|
|
2c03420f33 | ||
|
|
868ae597ae | ||
|
|
59d3e06935 | ||
|
|
b296f5dd5c | ||
|
|
2dc5e4fef5 | ||
|
|
f836de3854 | ||
|
|
631a591477 | ||
|
|
1bb27145ce | ||
|
|
79b89c2e85 | ||
|
|
ceb2a8c747 | ||
|
|
bee06ecf18 | ||
|
|
33c7eb5a74 | ||
|
|
2cb788712a | ||
|
|
90d2338412 | ||
|
|
4bebf5a7b5 | ||
|
|
68eb5023f8 | ||
|
|
8692c87ed7 | ||
|
|
ea0623c6bc | ||
|
|
b526dc4d39 | ||
|
|
76a1025f59 | ||
|
|
85e046a379 | ||
|
|
58a6bbd262 | ||
|
|
07aabf9a2a | ||
|
|
8c7ba6b963 | ||
|
|
d125e0f6d9 | ||
|
|
43d017d64a | ||
|
|
bc3e7c6a53 | ||
|
|
6e8610c358 | ||
|
|
ab00645c23 | ||
|
|
e041976ae0 | ||
|
|
9d7e6b40a0 | ||
|
|
db40a648ab | ||
|
|
805e60a808 | ||
|
|
8c7093189b | ||
|
|
3092696c83 | ||
|
|
c2e7f7576e | ||
|
|
6b4f7900ec | ||
|
|
523ae5b07b | ||
|
|
a628b3fd92 | ||
|
|
e9f3037129 | ||
|
|
46d4b9dece | ||
|
|
c701baaa2c | ||
|
|
a798f8e280 | ||
|
|
baebf79046 | ||
|
|
5727aa6c4d | ||
|
|
36cf1feff9 | ||
|
|
91a3b0f5ed | ||
|
|
293219d781 | ||
|
|
2cd4f44a92 | ||
|
|
4b9a95ded7 | ||
|
|
565b0382f9 | ||
|
|
1e837f2a9c | ||
|
|
765ebbe10c | ||
|
|
e95d1601a6 | ||
|
|
c6dd1d1615 | ||
|
|
bb50850995 | ||
|
|
83083134e9 | ||
|
|
61b8960aa5 | ||
|
|
5229408156 | ||
|
|
0921fef267 | ||
|
|
60989359e2 | ||
|
|
8fe381afa9 | ||
|
|
fea9c4e42b | ||
|
|
1d78402d5d | ||
|
|
aa72c33aea | ||
|
|
69504a150e | ||
|
|
a78a2e4ae2 | ||
|
|
0ffbf83141 | ||
|
|
75c346a460 | ||
|
|
93af79926d | ||
|
|
1b354e4bca | ||
|
|
6d53b78f6a | ||
|
|
8e90adccab | ||
|
|
c19635b16a | ||
|
|
94e4b00294 | ||
|
|
76908b1fbd | ||
|
|
7409c5382d | ||
|
|
2462eb1631 | ||
|
|
986ff32293 | ||
|
|
8f7fd9d8ee | ||
|
|
54d6a5c307 | ||
|
|
d672a43a83 | ||
|
|
48343e5d5c | ||
|
|
0d3a8c88c8 | ||
|
|
31367066f7 | ||
|
|
f7c097988a | ||
|
|
b7f1f36090 | ||
|
|
8756299d80 | ||
|
|
3525d60f8c | ||
|
|
d08a485111 | ||
|
|
c203b035c4 | ||
|
|
0f4b2bdae7 | ||
|
|
cb0bc39cd1 | ||
|
|
84abfa3478 | ||
|
|
a40a48e2c1 | ||
|
|
124499cabb | ||
|
|
e874d1efd4 | ||
|
|
fb1a17566f | ||
|
|
8dd8447212 | ||
|
|
eba7341d45 | ||
|
|
08bd36d518 | ||
|
|
7a45522ff3 | ||
|
|
4ee2923181 | ||
|
|
48459efdb1 | ||
|
|
c848f3c588 | ||
|
|
52a75cfdaf | ||
|
|
9331b5f919 | ||
|
|
2ff53d4650 | ||
|
|
7f2c8ca05a | ||
|
|
9e752e9406 | ||
|
|
b8c3e2098f | ||
|
|
4308e3bf49 | ||
|
|
25956ccadb | ||
|
|
af3fba5176 | ||
|
|
4afaa6a36e | ||
|
|
e98e1a5f36 | ||
|
|
abfdad0fec | ||
|
|
99e50ddad6 | ||
|
|
67a61dd3f3 | ||
|
|
13df01d55a | ||
|
|
af4ea036c8 | ||
|
|
97a75d9498 | ||
|
|
950028e3b4 | ||
|
|
874726c712 | ||
|
|
0f380b9962 | ||
|
|
2ce569726c | ||
|
|
a38fe7da43 | ||
|
|
057b563608 | ||
|
|
3741e26219 | ||
|
|
86bf8f68bf | ||
|
|
e7f67d1bb9 | ||
|
|
9c28e6bb93 | ||
|
|
0f84fab810 | ||
|
|
228a8e612f | ||
|
|
175c6cdba0 | ||
|
|
d3e2fcc3ac | ||
|
|
dafc2de2b3 | ||
|
|
f9eb9a1ad9 | ||
|
|
1f5001210b | ||
|
|
3150eafb89 | ||
|
|
ad1e3f2c67 | ||
|
|
d67b78d86e | ||
|
|
25f74ff04d | ||
|
|
be7c1453fe | ||
|
|
daa60939c3 | ||
|
|
ac659b05e4 | ||
|
|
8391d51e02 | ||
|
|
ad78b3a931 | ||
|
|
e3a1fdd87d | ||
|
|
12f24db3af | ||
|
|
2af805017c | ||
|
|
97d4ca0afe | ||
|
|
8b5c246531 | ||
|
|
8f2113b155 | ||
|
|
a378bd397b | ||
|
|
a315473ea4 | ||
|
|
7dc581b9f3 | ||
|
|
5ef392ca31 | ||
|
|
846c949cd1 | ||
|
|
d88ead4609 | ||
|
|
055c109563 | ||
|
|
378a7197c2 | ||
|
|
89ee0f8d27 | ||
|
|
c9ea9b1d4e | ||
|
|
a91532839f | ||
|
|
b32f6b2460 | ||
|
|
e02970b936 | ||
|
|
1f24fd4758 | ||
|
|
5a16403a13 | ||
|
|
48bbaad1b8 | ||
|
|
520711eec9 | ||
|
|
3f82a4ebd4 | ||
|
|
4feb22fc12 | ||
|
|
4e0e8dbb66 | ||
|
|
431c4eae14 | ||
|
|
6373cbc9a4 | ||
|
|
bbf3477512 | ||
|
|
ca30ac86d7 | ||
|
|
88aecb3af2 | ||
|
|
c5a78b0293 | ||
|
|
b0bc1ec4a6 | ||
|
|
84b3892053 | ||
|
|
d985683c0f | ||
|
|
008ce9222e | ||
|
|
c360fa8c9b | ||
|
|
c1afd4692d | ||
|
|
7748cf453c | ||
|
|
34d0123351 | ||
|
|
af1cfc92c2 | ||
|
|
59e06ef938 | ||
|
|
9d12ef82c0 | ||
|
|
0a9bfd4b69 | ||
|
|
329248ebbd | ||
|
|
cd51f2f2c9 | ||
|
|
a3e7d5bd84 | ||
|
|
6a261d284f | ||
|
|
eddf9f6d04 | ||
|
|
33e0f81988 | ||
|
|
0df8f80b48 | ||
|
|
2f4590fbb6 | ||
|
|
a011857286 | ||
|
|
cc9e7e10b6 | ||
|
|
78e04bfac5 | ||
|
|
652231d5fd | ||
|
|
343920d376 | ||
|
|
1f3e7574f5 | ||
|
|
ea9b10ff23 | ||
|
|
232a347f4a | ||
|
|
00053cb99d | ||
|
|
03a2a2f9cc | ||
|
|
09c8c66841 | ||
|
|
53e4bda69d | ||
|
|
4c77c8e22c | ||
|
|
9afb4a52c2 | ||
|
|
b3d6fd7f5b | ||
|
|
245d022f58 | ||
|
|
2d226db271 | ||
|
|
9281f9a390 | ||
|
|
d30bb56e75 | ||
|
|
eb92989478 | ||
|
|
d0dcdcd337 | ||
|
|
60aa92ca03 | ||
|
|
e2bbc1425b | ||
|
|
fc01103353 | ||
|
|
22919561fe | ||
|
|
745cbc082f | ||
|
|
6671a97181 | ||
|
|
b7aaa382b1 | ||
|
|
ecfa0e2c6b | ||
|
|
58c6cce4e4 | ||
|
|
b388a550a5 | ||
|
|
f0c5f6b949 | ||
|
|
459cd70c32 | ||
|
|
ece142e8d8 | ||
|
|
5f1f308540 | ||
|
|
7437b29518 | ||
|
|
7acb0a8c0f | ||
|
|
a1a24ee865 | ||
|
|
02008ad098 | ||
|
|
0a02431f6a | ||
|
|
9291711036 | ||
|
|
f3e0fda5fa | ||
|
|
0e721a4948 | ||
|
|
e0a704d022 | ||
|
|
e36bc816ea | ||
|
|
f8a62ca437 | ||
|
|
22ecb079b0 | ||
|
|
06333cf9aa | ||
|
|
1ad199ce53 | ||
|
|
ceebda5314 | ||
|
|
e2d33381cc | ||
|
|
2417f8e233 | ||
|
|
a240b0b3d7 | ||
|
|
fab085579a | ||
|
|
a5faa4acea | ||
|
|
4014f2a5b5 | ||
|
|
fb62839dad | ||
|
|
7bf8f1cfc1 | ||
|
|
b8a727eee8 | ||
|
|
52e12de296 | ||
|
|
0bb2345ccb | ||
|
|
78e957eed4 | ||
|
|
865efaa860 | ||
|
|
2931fd0b3e | ||
|
|
faae68928b | ||
|
|
21ada0e2d2 | ||
|
|
2337b8cd85 | ||
|
|
52c8d0b86c | ||
|
|
ce98cea4e7 | ||
|
|
b66a093cc2 | ||
|
|
a3e5ae4cf5 | ||
|
|
cddd08c32d | ||
|
|
5c3ad96cbe | ||
|
|
55b441d14e | ||
|
|
f740a211ce | ||
|
|
3cf00cb0f5 | ||
|
|
5659576ce3 | ||
|
|
55842d44ea | ||
|
|
43238692e4 | ||
|
|
fa1f64f505 | ||
|
|
25e3d56435 | ||
|
|
8722524c98 | ||
|
|
c9b1b42461 | ||
|
|
fa54f7e7f7 | ||
|
|
26d1de0620 | ||
|
|
b21548780a | ||
|
|
c8ba9266a7 | ||
|
|
9e9ef15737 | ||
|
|
4d7d2fbf31 | ||
|
|
b7ddae6950 | ||
|
|
1d60f2132d | ||
|
|
e9e4a76ac2 | ||
|
|
4d3a2cc574 | ||
|
|
c516c8a0ba | ||
|
|
24d5a0746b | ||
|
|
dee6c86d15 | ||
|
|
33e64416f3 | ||
|
|
a74637e9c0 | ||
|
|
ccd1b6d69b | ||
|
|
3750dd38dc | ||
|
|
1506cc257d | ||
|
|
fe7c7889e8 | ||
|
|
3f54eee11d | ||
|
|
11b5922c2c | ||
|
|
71b85d2a4d | ||
|
|
289832d166 | ||
|
|
a88e612c35 | ||
|
|
bda456362b | ||
|
|
f6f644c55d | ||
|
|
08c92333d6 | ||
|
|
4d72b77f29 | ||
|
|
4f9d06737f | ||
|
|
bfc494a3d4 | ||
|
|
e3b004d35f | ||
|
|
bd05ad6548 | ||
|
|
ffe068dbd5 | ||
|
|
b1f21c1abe | ||
|
|
b494efb8ce | ||
|
|
99ec45ec2a | ||
|
|
9cea027506 | ||
|
|
8bfc158c51 | ||
|
|
d6ed99282d | ||
|
|
ac92f3685f | ||
|
|
05fea49baf | ||
|
|
ed16143a2c | ||
|
|
6618a21a63 | ||
|
|
5b472748a1 | ||
|
|
a117aab558 | ||
|
|
ad2ab03be7 | ||
|
|
514112c077 | ||
|
|
6dd939af5b | ||
|
|
82f9c907be | ||
|
|
3cd6c6a371 | ||
|
|
8437394891 | ||
|
|
29cb444746 | ||
|
|
41eb22b5ca | ||
|
|
8577c32c1f | ||
|
|
3e55492318 | ||
|
|
bb53b0167b | ||
|
|
ec07c402c9 | ||
|
|
cb50bccbeb | ||
|
|
bf7c7ebc82 | ||
|
|
1184c9f506 | ||
|
|
f781aaa6f5 | ||
|
|
e78b245eea | ||
|
|
bb1ec22197 | ||
|
|
1fff0f6a1c | ||
|
|
d28d34fad1 | ||
|
|
40ae83dcad | ||
|
|
0a18f43d95 | ||
|
|
c639bfb227 | ||
|
|
014866c330 | ||
|
|
dfe7b2e4a6 | ||
|
|
f371c05446 | ||
|
|
d31f6688f8 | ||
|
|
3e5b740ef0 | ||
|
|
f282265fbb | ||
|
|
05df9b77ed | ||
|
|
69ba20009c | ||
|
|
9bf490bcb4 | ||
|
|
5a40080347 | ||
|
|
f42155ca5d | ||
|
|
3b06450113 | ||
|
|
2fb3c3a02b | ||
|
|
c8648659c7 | ||
|
|
83a663da2b | ||
|
|
5f0d14f01c | ||
|
|
069ec39de8 | ||
|
|
bef06a2a20 | ||
|
|
1a98fb8996 | ||
|
|
dc15bf75c6 | ||
|
|
7357422053 | ||
|
|
7918c089d3 | ||
|
|
ba50b75c54 | ||
|
|
0d7f32bdd0 | ||
|
|
8251e9437b | ||
|
|
416d5aef90 | ||
|
|
edb2e3a0ed | ||
|
|
64031a7814 | ||
|
|
1bd3c3d0f4 | ||
|
|
21b6fc4982 | ||
|
|
b881023e7b | ||
|
|
6387156596 | ||
|
|
2b2cfe9d33 | ||
|
|
e81580024e | ||
|
|
8b207b47dd | ||
|
|
15aab4ec29 | ||
|
|
bdb0474645 | ||
|
|
8d25d03390 | ||
|
|
ea2c7d2ea9 | ||
|
|
0ff38c7ca3 | ||
|
|
595f2e9ba7 | ||
|
|
b02bf3d4ad | ||
|
|
1e295d8562 | ||
|
|
27a0aeb170 | ||
|
|
7e355c3ecb | ||
|
|
69468487c2 | ||
|
|
0809b5b701 | ||
|
|
171f28f7c0 | ||
|
|
0a2af0ec7d | ||
|
|
c53354c368 | ||
|
|
be414f869c | ||
|
|
de40285e21 | ||
|
|
a7fa0bde00 | ||
|
|
19c2d2007f | ||
|
|
ecc9736b90 | ||
|
|
ffd022cfa6 | ||
|
|
89aba00aac | ||
|
|
bcc739aa57 | ||
|
|
fc1015b3fb | ||
|
|
f1fa32c7f5 | ||
|
|
a1e794c583 | ||
|
|
c787ef29d6 | ||
|
|
7c257cfbd2 | ||
|
|
99c77e6c5f | ||
|
|
f060c65bd5 | ||
|
|
22559cdf42 | ||
|
|
1fb86cb27b | ||
|
|
7f31066e8f | ||
|
|
50e418dc53 | ||
|
|
59739067b6 | ||
|
|
f100e157d3 | ||
|
|
870ce259f0 | ||
|
|
95b50eb5ca | ||
|
|
f243d13b81 | ||
|
|
749a13efbc | ||
|
|
275d5d7f04 | ||
|
|
54de071e85 | ||
|
|
9647e2513b | ||
|
|
239781eabd | ||
|
|
21573d6cc8 | ||
|
|
9b091f0bb0 | ||
|
|
e0beef745e | ||
|
|
2f2653d223 | ||
|
|
7458aa53ae | ||
|
|
f7db306c26 | ||
|
|
7e8d6f2041 | ||
|
|
d424a6c48f | ||
|
|
7293224514 | ||
|
|
7706cfed9a | ||
|
|
bbfe0b237d | ||
|
|
2c7cddd853 | ||
|
|
37c1d39c41 | ||
|
|
5d4960a26f | ||
|
|
78f07b23df | ||
|
|
7aeb047b50 | ||
|
|
0e3a661dd0 | ||
|
|
fba350ed1e | ||
|
|
76bfbc7316 | ||
|
|
7683d8fa5b | ||
|
|
3fe5352b56 | ||
|
|
a86d9a9c0c | ||
|
|
a68f74bc1b | ||
|
|
18310698f6 | ||
|
|
020244231f | ||
|
|
59085d7b19 | ||
|
|
c15410e168 | ||
|
|
2962f13457 | ||
|
|
bfc77fc9a4 | ||
|
|
7e88ec88ee | ||
|
|
9ef07f22af | ||
|
|
268343be3e | ||
|
|
956e5373d1 | ||
|
|
919fa9bdf1 | ||
|
|
e06192c5b4 |
@@ -10,11 +10,12 @@ const authors = readFileSync("AUTHORS", "utf-8");
|
||||
const isAuthor = authors.includes(`<${authorEmail}>`);
|
||||
|
||||
const SCOPES = [
|
||||
// for full list of scopes + details see: https://github.com/streetwriters/notesnook-private/blob/master/CONTRIBUTING.md#commit-guidelines
|
||||
// for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
|
||||
|
||||
"mobile",
|
||||
"web",
|
||||
"vericrypt",
|
||||
"monograph",
|
||||
"desktop",
|
||||
"crypto",
|
||||
"editor",
|
||||
@@ -34,7 +35,9 @@ const SCOPES = [
|
||||
"common",
|
||||
"global",
|
||||
"docs",
|
||||
"themebuilder"
|
||||
"themebuilder",
|
||||
"intl",
|
||||
"webclipper"
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,8 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Are you facing a bug or a crash in Notesnook?
|
||||
labels: ["Type: Bug", "Status: Pending"]
|
||||
assignees:
|
||||
- thecodrr
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Notesnook Discord Community
|
||||
url: https://discord.gg/6mHHyncHJE
|
||||
url: https://go.notesnook.com/discord
|
||||
about: Reach out to us directly & discuss your issues, suggestions & other feedback!
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Feature request
|
||||
description: Suggest a feature you are missing in Notesnook
|
||||
labels: ["Type: Feature Request", "Status: Pending"]
|
||||
type: Feature
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -7,13 +7,15 @@ runs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 22.x
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
apps/mobile/package-lock.json
|
||||
apps/desktop/package-lock.json
|
||||
apps/web/package-lock.json
|
||||
apps/monograph/package-lock.json
|
||||
apps/theme-builder/package-lock.json
|
||||
extensions/web-clipper/package-lock.json
|
||||
packages/core/package-lock.json
|
||||
packages/crypto/package-lock.json
|
||||
@@ -24,3 +26,4 @@ runs:
|
||||
packages/logger/package-lock.json
|
||||
packages/streamable-fs/package-lock.json
|
||||
packages/theme/package-lock.json
|
||||
packages/intl/package-lock.json
|
||||
|
||||
105
.github/workflows/android.e2e.yml
vendored
Normal file
105
.github/workflows/android.e2e.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
name: e2e-android
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
e2e-android:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
API_LEVEL: 34
|
||||
ARCH: x86_64
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Free Disk Space
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
gradle-version: wrapper
|
||||
cache-read-only: false
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install Detox CLI
|
||||
run: npm install detox-cli --global
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Detox build
|
||||
run: |
|
||||
yarn build:android
|
||||
ls apps/mobile/native/android/app/build/outputs/apk
|
||||
ls apps/mobile/native/android/app/build/outputs/apk/release
|
||||
|
||||
- name: Get device name
|
||||
id: device
|
||||
run: |
|
||||
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
|
||||
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: AVD cache
|
||||
uses: actions/cache@v4
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: ${{ steps.device.outputs.AVD_NAME }}
|
||||
|
||||
- name: create AVD and generate snapshot for caching
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ env.API_LEVEL }}
|
||||
arch: ${{ env.ARCH }}
|
||||
avd-name: ${{ steps.device.outputs.AVD_NAME }}
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
script: echo "Generated AVD snapshot for caching."
|
||||
|
||||
- name: Detox test
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ env.API_LEVEL }}
|
||||
arch: ${{ env.ARCH }}
|
||||
avd-name: ${{ steps.device.outputs.AVD_NAME }}
|
||||
disable-animations: true
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
force-avd-creation: false
|
||||
script: yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing
|
||||
|
||||
- name: Upload artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: detox-artifacts
|
||||
path: artifacts
|
||||
retention-days: 14
|
||||
19
.github/workflows/android.publish.internal.yml
vendored
19
.github/workflows/android.publish.internal.yml
vendored
@@ -4,7 +4,7 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "11"
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install node modules
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
ccache -p
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -69,12 +69,16 @@ jobs:
|
||||
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: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
@@ -129,7 +133,7 @@ jobs:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: com.streetwriters.notesnook
|
||||
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
track: alpha
|
||||
track: beta
|
||||
status: completed
|
||||
whatsNewDirectory: apps/mobile/native/android/releasenotes/
|
||||
|
||||
@@ -138,7 +142,7 @@ jobs:
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
|
||||
name: Notesnook Android v${{ steps.package-version.outputs.current-version}} Beta
|
||||
name: Notesnook Android v${{ steps.package-version.outputs.current-version}}
|
||||
repository: streetwriters/notesnook
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
@@ -146,6 +150,7 @@ 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
|
||||
${{steps.sign_app.outputs.signedReleaseFile}}
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
12
.github/workflows/android.publish.yml
vendored
12
.github/workflows/android.publish.yml
vendored
@@ -4,7 +4,7 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
CMAKE_C_COMPILER_LAUNCHER: ccache
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
ccache -p
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -69,12 +69,16 @@ jobs:
|
||||
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: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: Build unsigned app bundle
|
||||
run: yarn release:android:bundle
|
||||
|
||||
|
||||
27
.github/workflows/core.tests.yml
vendored
27
.github/workflows/core.tests.yml
vendored
@@ -9,7 +9,13 @@ on:
|
||||
- "packages/core/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/core.tests.yml"
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "packages/core/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/core.tests.yml"
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
@@ -17,11 +23,21 @@ on:
|
||||
- "reopened"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
authorize:
|
||||
environment: ${{ github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
'external' || 'internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo true
|
||||
|
||||
test:
|
||||
needs: authorize
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -36,7 +52,6 @@ jobs:
|
||||
echo "USER_PASSWORD=${{ secrets.USER_PASSWORD }}" >> $GITHUB_ENV
|
||||
echo "USER_TOTP_SECRET=${{ secrets.USER_TOTP_SECRET }}" >> $GITHUB_ENV
|
||||
echo "USER_HASHED_PASSWORD=${{ secrets.USER_HASHED_PASSWORD }}" >> $GITHUB_ENV
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Run all @notesnook/core tests
|
||||
run: npx nx test:e2e @notesnook/core
|
||||
run: npm run tx @notesnook/core:test:e2e
|
||||
|
||||
187
.github/workflows/desktop.publish.yml
vendored
187
.github/workflows/desktop.publish.yml
vendored
@@ -33,14 +33,22 @@ on:
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for macOS?"
|
||||
release-track:
|
||||
type: choice
|
||||
required: true
|
||||
default: stable
|
||||
description: "Select the release track"
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -49,13 +57,25 @@ jobs:
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Setup environment
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Generate desktop build
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
- name: Generate desktop build (stable)
|
||||
if: ${{ inputs.release-track == 'stable' }}
|
||||
run: npm run tx @notesnook/web:build:desktop
|
||||
|
||||
- name: Generate desktop build (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
run: BETA=true npm run tx @notesnook/web:build:desktop
|
||||
|
||||
- name: Build desktop bundle
|
||||
working-directory: ./apps/desktop
|
||||
run: npm run bundle
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -71,7 +91,8 @@ jobs:
|
||||
|
||||
- name: Package desktop build
|
||||
run: |
|
||||
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/web/build/
|
||||
cp -r ./apps/web/build ./apps/desktop/
|
||||
zip -r ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip ./apps/desktop/build/
|
||||
echo "Build folder archived to ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip"
|
||||
|
||||
- name: Upload desktop build
|
||||
@@ -82,6 +103,28 @@ 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: ${{ inputs.release-track == 'beta' }}
|
||||
target_commitish: ${{ github.ref }}
|
||||
|
||||
- name: Generate flatpak sources
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
|
||||
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
|
||||
cd /tmp/flatpak-builder-tools/node
|
||||
pipx install .
|
||||
cd -
|
||||
node scripts/generate-sources.mjs
|
||||
|
||||
- name: Upload flatpak sources
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
with:
|
||||
draft: true
|
||||
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
|
||||
tag_name: v${{ steps.app_metadata.outputs.app_version }}
|
||||
files: ./generated-sources.json
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
build-macos:
|
||||
@@ -92,7 +135,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -136,6 +179,12 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
|
||||
working-directory: ./apps/desktop
|
||||
@@ -146,9 +195,9 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.mac_certs }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop -- --variant=mas
|
||||
yarn electron-builder --mac mas --universal -p never
|
||||
working-directory: ./apps/desktop
|
||||
npm run tx @notesnook/desktop:release -- --variant=mas
|
||||
cd apps/desktop
|
||||
yarn electron-builder --config=electron-builder.config.js --mac mas --universal -p never
|
||||
|
||||
- name: Build zip and dmg
|
||||
env:
|
||||
@@ -159,13 +208,13 @@ jobs:
|
||||
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
|
||||
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
|
||||
run: |
|
||||
npx nx run release --project @notesnook/desktop
|
||||
npm run tx @notesnook/desktop:release
|
||||
cd apps/desktop
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p always
|
||||
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p always
|
||||
else
|
||||
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
|
||||
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Deploy to Testflight
|
||||
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
|
||||
@@ -180,15 +229,15 @@ jobs:
|
||||
xcrun altool --upload-package $package -t osx --apiKey $API_KEY_ID --apiIssuer $API_KEY_ISSUER_ID --apple-id ${{ steps.app_metadata.outputs.apple_app_id }} --bundle-id ${{ steps.app_metadata.outputs.app_bundle_id }} --bundle-short-version-string ${{ steps.app_metadata.outputs.app_version }} --bundle-version ${{ steps.app_metadata.outputs.bundle_version }}
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
build-linux-x64:
|
||||
name: Build for Linux x64
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -198,7 +247,7 @@ jobs:
|
||||
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
uses: samuelmeuli/action-snapcraft@v3
|
||||
if: inputs.publish-snap
|
||||
|
||||
- name: Download build
|
||||
@@ -212,14 +261,18 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
yarn electron-builder --linux snap:x64 -p never
|
||||
yarn electron-builder --config=electron-builder.config.js --linux snap:x64 -p never
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build AppImage
|
||||
@@ -227,18 +280,85 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p always
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p always
|
||||
else
|
||||
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Publish on Snapcraft
|
||||
if: inputs.publish-snap
|
||||
run: |
|
||||
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
|
||||
if [ ${{ inputs.release-track }} == 'beta' ]; then
|
||||
snapcraft upload --release=beta ./output/notesnook_linux_amd64.snap
|
||||
else
|
||||
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
build-linux-arm64:
|
||||
name: Build for Linux arm64
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04-arm
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
# - name: Setup Snapcraft Auth
|
||||
# if: inputs.publish-snap
|
||||
# run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
|
||||
|
||||
# - name: Install Snapcraft
|
||||
# uses: samuelmeuli/action-snapcraft@v1
|
||||
# if: inputs.publish-snap
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
# - name: Build snap
|
||||
# if: inputs.publish-snap
|
||||
# run: |
|
||||
# yarn electron-builder --config=electron-builder.config.js --linux snap:arm64 -p never
|
||||
# working-directory: ./apps/desktop
|
||||
|
||||
- name: Build AppImage
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ ${{ inputs.publish-github }} == true ]; then
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p always
|
||||
else
|
||||
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
|
||||
fi
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
# - name: Publish on Snapcraft
|
||||
# if: inputs.publish-snap
|
||||
# run: |
|
||||
# snapcraft upload --release=stable ./output/notesnook_linux_arm64.snap
|
||||
# working-directory: ./apps/desktop
|
||||
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
@@ -247,7 +367,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET Core SDK
|
||||
uses: actions/setup-dotnet@v2
|
||||
@@ -272,8 +392,17 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
run: node scripts/execute.mjs @notesnook/desktop:release
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
@@ -283,8 +412,8 @@ jobs:
|
||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
yarn electron-builder --win --publish always
|
||||
yarn electron-builder --config=electron-builder.config.js --win --publish always
|
||||
} else {
|
||||
yarn electron-builder --win --publish never
|
||||
yarn electron-builder --config=electron-builder.config.js --win --publish never
|
||||
}
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
230
.github/workflows/desktop.tests.yml
vendored
Normal file
230
.github/workflows/desktop.tests.yml
vendored
Normal file
@@ -0,0 +1,230 @@
|
||||
name: Test @notesnook/desktop
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/desktop/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/desktop/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Generate desktop build
|
||||
run: npm run tx @notesnook/web:build:desktop
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
needs: build
|
||||
runs-on: macos-13
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests x64
|
||||
run: npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-macos-x64
|
||||
path: apps/desktop/test-results
|
||||
retention-days: 5
|
||||
|
||||
test-macos:
|
||||
name: Test macOS
|
||||
needs: build
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests arm64
|
||||
run: npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-macos
|
||||
path: apps/desktop/test-results
|
||||
retention-days: 5
|
||||
|
||||
test-linux:
|
||||
name: Test for Linux
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-linux
|
||||
path: apps/desktop/test-results
|
||||
retention-days: 5
|
||||
|
||||
test-windows:
|
||||
name: Test for Windows
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: node scripts/execute.mjs @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-win
|
||||
path: apps/desktop/test-results
|
||||
retention-days: 5
|
||||
18
.github/workflows/editor.tests.yml
vendored
18
.github/workflows/editor.tests.yml
vendored
@@ -10,6 +10,12 @@ on:
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/editor.tests.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "packages/editor/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/editor.tests.yml"
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
@@ -19,10 +25,10 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -32,12 +38,8 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=editor
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build editor
|
||||
run: npx nx build @notesnook/editor
|
||||
run: npm run tx @notesnook/editor:build
|
||||
|
||||
- name: Run all @notesnook/editor tests
|
||||
run: npx nx test @notesnook/editor
|
||||
run: npm run tx @notesnook/editor:test
|
||||
|
||||
6
.github/workflows/help.publish.yml
vendored
6
.github/workflows/help.publish.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Publish Notesnook Help
|
||||
|
||||
on:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
@@ -11,10 +11,10 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
24
.github/workflows/ios.publish.yml
vendored
24
.github/workflows/ios.publish.yml
vendored
@@ -4,23 +4,28 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-13
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "16.1"
|
||||
|
||||
- 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
|
||||
run: npm run tx @notesnook/mobile:build
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.11
|
||||
@@ -47,14 +52,23 @@ jobs:
|
||||
ccache -p
|
||||
|
||||
- name: Cache Pods
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
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
|
||||
run: |
|
||||
cd apps/mobile/native/ios
|
||||
bundle install
|
||||
RCT_NEW_ARCH_ENABLED=0 bundle exec pod install
|
||||
|
||||
- name: Check for typescript errors
|
||||
run: |
|
||||
npm run tx mobile:build
|
||||
cd apps/mobile
|
||||
npx tsc --noEmit
|
||||
|
||||
- name: CCache Stats Before Build
|
||||
run: ccache -sv
|
||||
|
||||
89
.github/workflows/monograph.publish.yml
vendored
Normal file
89
.github/workflows/monograph.publish.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# GitHub recommends pinning actions to a commit SHA.
|
||||
# To get a newer version, you will need to update the SHA.
|
||||
# You can also reference a tag or branch, but the action may change without warning.
|
||||
|
||||
name: Publish @notesnook/monograph
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=monograph
|
||||
|
||||
- name: Collect package metadata
|
||||
id: package_metadata
|
||||
working-directory: ./apps/monograph
|
||||
run: |
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
|
||||
- name: Generate build
|
||||
run: npm run tx @notesnook/monograph:build
|
||||
|
||||
# Setup Buildx
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
ecr: auto
|
||||
logout: true
|
||||
|
||||
# Pull previous image from docker hub to use it as cache to improve the image build time.
|
||||
- name: docker pull cache image
|
||||
continue-on-error: true
|
||||
run: docker pull streetwriters/monograph:latest
|
||||
|
||||
# Setup QEMU
|
||||
# - name: Set up QEMU
|
||||
# uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: streetwriters/monograph
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: apps/monograph
|
||||
file: apps/monograph/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
|
||||
cache-from: streetwriters/monograph:latest
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: index.docker.io/streetwriters/monograph
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
5
.github/workflows/theme-builder.publish.yml
vendored
5
.github/workflows/theme-builder.publish.yml
vendored
@@ -5,10 +5,10 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -20,7 +20,6 @@ jobs:
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
5
.github/workflows/vericrypt.publish.yml
vendored
5
.github/workflows/vericrypt.publish.yml
vendored
@@ -5,10 +5,10 @@ on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -20,7 +20,6 @@ jobs:
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
40
.github/workflows/web.benchmarks.yml
vendored
Normal file
40
.github/workflows/web.benchmarks.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Benchmark @notesnook/web
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.benchmarks.yml"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: Bench
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Build
|
||||
run: npm run build:web
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: apps/web
|
||||
|
||||
- name: Run benchmarks
|
||||
id: benchmark
|
||||
run: node __bench__/app.bench.mjs
|
||||
working-directory: apps/web
|
||||
35
.github/workflows/web.publish.yml
vendored
35
.github/workflows/web.publish.yml
vendored
@@ -1,14 +1,24 @@
|
||||
name: Publish @notesnook/web
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-track:
|
||||
type: choice
|
||||
required: true
|
||||
default: stable
|
||||
description: "Select the release track"
|
||||
options:
|
||||
- stable
|
||||
- beta
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -20,12 +30,23 @@ jobs:
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate build
|
||||
- name: Generate build (stable)
|
||||
if: ${{ inputs.release-track == 'stable' }}
|
||||
run: npm run build:web
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./apps/web/build/
|
||||
- name: Publish to Cloudflare Pages (stable)
|
||||
if: ${{ inputs.release-track == 'stable' }}
|
||||
working-directory: ./apps/web
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./build/
|
||||
|
||||
- name: Generate build (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
run: npm run build:beta:web
|
||||
|
||||
- name: Publish to Cloudflare Pages (beta)
|
||||
if: ${{ inputs.release-track == 'beta' }}
|
||||
working-directory: ./apps/web
|
||||
run: npx --yes wrangler pages deploy --branch=beta --project-name=notesnook-app-beta ./build/
|
||||
|
||||
35
.github/workflows/web.tests.yml
vendored
35
.github/workflows/web.tests.yml
vendored
@@ -9,7 +9,13 @@ on:
|
||||
- "apps/web/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.tests.yml"
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.tests.yml"
|
||||
types:
|
||||
- "ready_for_review"
|
||||
- "opened"
|
||||
@@ -17,12 +23,23 @@ on:
|
||||
- "reopened"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
authorize:
|
||||
environment: ${{ github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
'external' || 'internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo true
|
||||
|
||||
build:
|
||||
needs: authorize
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -32,10 +49,6 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate test build
|
||||
run: npm run build:test:web
|
||||
|
||||
@@ -49,11 +62,13 @@ jobs:
|
||||
name: 🧪 Test (${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ nx-cloud.env
|
||||
site
|
||||
node_modules.backup
|
||||
.nx
|
||||
/*.patch
|
||||
.taskcache
|
||||
@@ -27,7 +27,6 @@ Notesnook is built using the following technologies:
|
||||
3. React Native — For mobile apps we are using React Native
|
||||
4. Electron — For desktop app
|
||||
5. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
|
||||
6. Nx — maintaining monorepos is hard but Nx makes it easier.
|
||||
|
||||
> **Note: Each project in the monorepo contains its own architecture details which you can refer to.**
|
||||
|
||||
@@ -66,7 +65,7 @@ We take all queries, issues and bug reports that you might have. Feel free to as
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Migrating & Importing your data from other apps — Importer](https://importer.notesnook.com/)
|
||||
- [Migrating & Importing your data from other apps — Importer](https://help.notesnook.com/importing-notes)
|
||||
- [Privacy policy](https://notesnook.com/privacy) & [Terms of service](https://notesnook.com/terms)
|
||||
- [Verify Notesnook encryption claims yourself — Vericrypt](https://vericrypt.notesnook.com/)
|
||||
- [Why Notesnook requires an email address?](https://blog.notesnook.com/why-notesnook-requires-an-email-address/)
|
||||
|
||||
4
apps/desktop/.gitignore
vendored
4
apps/desktop/.gitignore
vendored
@@ -2,4 +2,6 @@ node_modules
|
||||
build
|
||||
output
|
||||
dist
|
||||
_catalog
|
||||
_catalog
|
||||
test-results
|
||||
test-artifacts
|
||||
|
||||
@@ -24,7 +24,7 @@ Requirements:
|
||||
|
||||
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
|
||||
|
||||
Once you have completed the setup, the first step is to `clone` the monorepo:
|
||||
1. `clone` the monorepo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/streetwriters/notesnook.git
|
||||
@@ -33,19 +33,28 @@ git clone https://github.com/streetwriters/notesnook.git
|
||||
cd notesnook
|
||||
```
|
||||
|
||||
Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
# this might take a while to complete
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you can finally start the desktop app for development:
|
||||
3. Run the webapp for desktop environment:
|
||||
|
||||
```bash
|
||||
cd apps/web
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
4. In a separate terminal session, run the desktop app from the root of the project:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
### Release mode
|
||||
|
||||
To run the app in release mode:
|
||||
|
||||
```bash
|
||||
@@ -58,13 +67,13 @@ This will compile and run the app in production mode but it won't generate any p
|
||||
npm run release -- --rebuild
|
||||
|
||||
# For macOS
|
||||
npx electron-builder --mac dmg --arm64 --x64 -publish never
|
||||
npx electron-builder --config=electron-builder.config.js --mac dmg --arm64 --x64 --publish never
|
||||
|
||||
# For Linux (AppImage)
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
|
||||
npx electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 --publish never
|
||||
|
||||
# For Windows
|
||||
npx electron-builder --win --publish never
|
||||
npx electron-builder --config=electron-builder.config.js --win --publish never
|
||||
```
|
||||
|
||||
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
|
||||
|
||||
242
apps/desktop/__tests__/auto-updates.test.ts
Normal file
242
apps/desktop/__tests__/auto-updates.test.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
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 { testCleanup, test } from "./test-override.js";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { Page } from "playwright";
|
||||
import { gt, lt } from "semver";
|
||||
import { describe } from "vitest";
|
||||
|
||||
test("update starts downloading if version is outdated", async ({
|
||||
ctx: { page },
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /updating/i })
|
||||
.waitFor({ state: "attached" });
|
||||
});
|
||||
|
||||
test("update is only shown if version is outdated and auto updates are disabled", async ({
|
||||
ctx,
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.waitFor({ state: "attached" });
|
||||
});
|
||||
|
||||
describe("update to stable if it is newer", () => {
|
||||
test.scoped({ options: { version: "3.0.0-beta.0" } });
|
||||
test("test", async ({ ctx, expect, onTestFinished }) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
expect(gt(version, "3.0.0-beta.0")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update is not available if it latest stable version is older", () => {
|
||||
test.scoped({ options: { version: "99.0.0-beta.0" } });
|
||||
test("test", async ({ ctx, expect, onTestFinished }) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "beta"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
expect(
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i })
|
||||
.isHidden()
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("downgrade to stable on switching to stable release track", () => {
|
||||
test.scoped({ options: { version: "99.0.0-beta.0" } });
|
||||
test("test", async ({ ctx, expect, onTestFinished }) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await ctx.app.close();
|
||||
await writeFile(
|
||||
ctx.configPath,
|
||||
JSON.stringify({
|
||||
automaticUpdates: false,
|
||||
releaseTrack: "stable"
|
||||
})
|
||||
);
|
||||
|
||||
await ctx.relaunch();
|
||||
|
||||
const { page } = ctx;
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await skipDialog(page);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /checking for updates/i })
|
||||
.waitFor({ state: "hidden" });
|
||||
|
||||
const updateButton = page
|
||||
.locator(".theme-scope-statusBar")
|
||||
.getByRole("button", { name: /available/i });
|
||||
await updateButton.waitFor({ state: "visible" });
|
||||
const content = await updateButton.textContent();
|
||||
const version = content?.split(" ")?.[0] || "";
|
||||
expect(lt(version, "99.0.0-beta.0")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
async function skipDialog(page: Page) {
|
||||
try {
|
||||
const dialog = page.locator(".ReactModal__Content");
|
||||
const positiveButton = dialog.locator(
|
||||
"button[data-role='positive-button']"
|
||||
);
|
||||
const negativeButton = dialog.locator(
|
||||
"button[data-role='negative-button']"
|
||||
);
|
||||
if (await positiveButton.isVisible())
|
||||
await positiveButton.click({ timeout: 1000 });
|
||||
else if (await negativeButton.isVisible())
|
||||
await negativeButton.click({ timeout: 1000 });
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,8 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function isMobile(): boolean {
|
||||
return window.matchMedia("(max-width: 480px)").matches;
|
||||
}
|
||||
import { buildApp } from "./utils";
|
||||
|
||||
function isTablet(): boolean {
|
||||
return window.matchMedia("(min-width: 480px) and (max-width: 1000px)")
|
||||
.matches;
|
||||
export default async function setup() {
|
||||
await buildApp();
|
||||
}
|
||||
|
||||
export { isMobile, isTablet };
|
||||
40
apps/desktop/__tests__/launch.test.ts
Normal file
40
apps/desktop/__tests__/launch.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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 { testCleanup, test } from "./test-override.js";
|
||||
|
||||
test("make sure app loads", async ({
|
||||
ctx: { page },
|
||||
expect,
|
||||
onTestFinished
|
||||
}) => {
|
||||
onTestFinished(testCleanup);
|
||||
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
expect(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
).toBe(true);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
});
|
||||
64
apps/desktop/__tests__/test-override.ts
Normal file
64
apps/desktop/__tests__/test-override.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test as vitestTest, TestContext } from "vitest";
|
||||
import { buildAndLaunchApp, Fixtures, TestOptions } from "./utils";
|
||||
import { mkdir, rm } from "fs/promises";
|
||||
import path from "path";
|
||||
import slugify from "slugify";
|
||||
|
||||
export const test = vitestTest.extend<Fixtures>({
|
||||
options: { version: "3.0.0" } as TestOptions,
|
||||
ctx: async ({ options }, use) => {
|
||||
const ctx = await buildAndLaunchApp(options);
|
||||
await use(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
export async function testCleanup(context: TestContext) {
|
||||
const ctx = (context.task.context as unknown as Fixtures).ctx;
|
||||
if (context.task.result?.state === "fail") {
|
||||
await mkdir("test-results", { recursive: true });
|
||||
await ctx.page.screenshot({
|
||||
path: path.join(
|
||||
"test-results",
|
||||
`${slugify(context.task.name)}-${process.platform}-${
|
||||
process.arch
|
||||
}-error.png`
|
||||
)
|
||||
});
|
||||
}
|
||||
await ctx.app.close();
|
||||
await rm(ctx.userDataDir, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
maxRetries: 3,
|
||||
retryDelay: 5000
|
||||
}).catch(() => {
|
||||
/*ignore */
|
||||
});
|
||||
await rm(ctx.outputDir, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
maxRetries: 3,
|
||||
retryDelay: 5000
|
||||
}).catch(() => {
|
||||
/*ignore */
|
||||
});
|
||||
}
|
||||
226
apps/desktop/__tests__/utils.ts
Normal file
226
apps/desktop/__tests__/utils.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
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 { execSync } from "child_process";
|
||||
import { cp } from "fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path, { join, resolve } from "path";
|
||||
import { _electron as electron } from "playwright";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
|
||||
const productName = `NotesnookTestHarness`;
|
||||
const SOURCE_DIR = resolve("output", productName);
|
||||
|
||||
export interface AppContext {
|
||||
app: import("playwright").ElectronApplication;
|
||||
page: import("playwright").Page;
|
||||
configPath: string;
|
||||
userDataDir: string;
|
||||
outputDir: string;
|
||||
relaunch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface TestOptions {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface Fixtures {
|
||||
options: TestOptions;
|
||||
ctx: AppContext;
|
||||
}
|
||||
|
||||
export async function buildAndLaunchApp(
|
||||
options?: TestOptions
|
||||
): Promise<AppContext> {
|
||||
const productName = `notesnooktest${makeid(10)}`;
|
||||
const outputDir = path.join("test-artifacts", `${productName}-output`);
|
||||
const executablePath = await copyBuild({
|
||||
...options,
|
||||
outputDir
|
||||
});
|
||||
const { app, page, configPath, userDataDir } = await launchApp(
|
||||
executablePath,
|
||||
productName,
|
||||
options?.version
|
||||
);
|
||||
const ctx: AppContext = {
|
||||
app,
|
||||
page,
|
||||
configPath,
|
||||
userDataDir,
|
||||
outputDir,
|
||||
relaunch: async () => {
|
||||
const { app, page, configPath, userDataDir } = await launchApp(
|
||||
executablePath,
|
||||
productName,
|
||||
options?.version
|
||||
);
|
||||
ctx.app = app;
|
||||
ctx.page = page;
|
||||
ctx.userDataDir = userDataDir;
|
||||
ctx.configPath = configPath;
|
||||
}
|
||||
};
|
||||
return ctx;
|
||||
}
|
||||
|
||||
async function launchApp(
|
||||
executablePath: string,
|
||||
packageName: string,
|
||||
version?: string
|
||||
) {
|
||||
const userDataDir = resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"test-artifacts",
|
||||
"user_data_dirs",
|
||||
packageName
|
||||
);
|
||||
const app = await electron.launch({
|
||||
executablePath,
|
||||
args: IS_DEBUG ? [] : ["--hidden"],
|
||||
env: {
|
||||
...(process.platform === "linux"
|
||||
? {
|
||||
...(process.env as Record<string, string>),
|
||||
APPIMAGE: "true"
|
||||
}
|
||||
: (process.env as Record<string, string>)),
|
||||
CUSTOM_USER_DATA_DIR: userDataDir,
|
||||
...(version
|
||||
? {
|
||||
CUSTOM_APP_VERSION: version
|
||||
}
|
||||
: {})
|
||||
}
|
||||
});
|
||||
|
||||
const page = await app.firstWindow();
|
||||
|
||||
const configPath = path.join(userDataDir, "UserData", "config.json");
|
||||
return {
|
||||
app,
|
||||
page,
|
||||
configPath,
|
||||
userDataDir
|
||||
};
|
||||
}
|
||||
|
||||
let MAX_RETRIES = 3;
|
||||
export async function buildApp(version?: string) {
|
||||
if (!existsSync(SOURCE_DIR)) {
|
||||
const args = [
|
||||
"electron-builder",
|
||||
"--dir",
|
||||
`--${process.arch}`,
|
||||
`--config electron-builder.config.js`,
|
||||
`--c.extraMetadata.productName=${productName}`,
|
||||
`--c.compression=store`,
|
||||
"--publish=never"
|
||||
];
|
||||
if (version) args.push(`--c.extraMetadata.version=${version}`);
|
||||
try {
|
||||
execSync(`npx ${args.join(" ")}`, {
|
||||
stdio: IS_DEBUG ? "inherit" : "ignore",
|
||||
env: {
|
||||
...process.env,
|
||||
NOTESNOOK_STAGING: "true",
|
||||
NN_PRODUCT_NAME: productName,
|
||||
NN_APP_ID: `com.notesnook.test.${productName}`,
|
||||
NN_OUTPUT_DIR: SOURCE_DIR
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (--MAX_RETRIES) {
|
||||
console.log("retrying...");
|
||||
return await buildApp(version);
|
||||
} else throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyBuild({ outputDir }: { outputDir: string }) {
|
||||
return process.platform === "win32"
|
||||
? await makeBuildCopyWindows(outputDir, productName)
|
||||
: process.platform === "darwin"
|
||||
? await makeBuildCopyMacOS(outputDir, productName)
|
||||
: await makeBuildCopyLinux(outputDir, productName);
|
||||
}
|
||||
|
||||
async function makeBuildCopyLinux(outputDir: string, productName: string) {
|
||||
const platformDir =
|
||||
process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked";
|
||||
const appDir = await makeBuildCopy(outputDir, platformDir);
|
||||
return resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
appDir,
|
||||
productName.toLowerCase().replace(/\s+/g, "-")
|
||||
);
|
||||
}
|
||||
|
||||
async function makeBuildCopyWindows(outputDir: string, productName: string) {
|
||||
const platformDir =
|
||||
process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked";
|
||||
const appDir = await makeBuildCopy(outputDir, platformDir);
|
||||
return resolve(__dirname, "..", appDir, `${productName}.exe`);
|
||||
}
|
||||
|
||||
async function makeBuildCopyMacOS(outputDir: string, productName: string) {
|
||||
const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac";
|
||||
const appDir = await makeBuildCopy(outputDir, platformDir);
|
||||
return resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
appDir,
|
||||
`${productName}.app`,
|
||||
"Contents",
|
||||
"MacOS",
|
||||
productName
|
||||
);
|
||||
}
|
||||
|
||||
async function makeBuildCopy(outputDir: string, platformDir: string) {
|
||||
const appDir = outputDir;
|
||||
await cp(join(SOURCE_DIR, platformDir), outputDir, {
|
||||
recursive: true,
|
||||
preserveTimestamps: true,
|
||||
verbatimSymlinks: true,
|
||||
dereference: false,
|
||||
force: true
|
||||
});
|
||||
|
||||
return appDir;
|
||||
}
|
||||
|
||||
function makeid(length: number) {
|
||||
let result = "";
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const charactersLength = characters.length;
|
||||
let counter = 0;
|
||||
while (counter < length) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
counter += 1;
|
||||
}
|
||||
return result.toLowerCase();
|
||||
}
|
||||
Binary file not shown.
223
apps/desktop/electron-builder.config.js
Normal file
223
apps/desktop/electron-builder.config.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
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 path = require("path");
|
||||
const pkg = require("./package.json");
|
||||
|
||||
const buildRoot = process.env.NN_BUILD_ROOT || ".";
|
||||
const buildFiles = [
|
||||
`${buildRoot}/build/`,
|
||||
`!${buildRoot}/build/screenshots\${/*}`,
|
||||
`!${buildRoot}/build/banner.jpg`,
|
||||
`!${buildRoot}/build/*.ico`,
|
||||
`!${buildRoot}/build/*.png`
|
||||
];
|
||||
|
||||
const productName = process.env.NN_PRODUCT_NAME || "Notesnook";
|
||||
const appId = process.env.NN_APP_ID || "org.streetwriters.notesnook";
|
||||
const outputDir = process.env.NN_OUTPUT_DIR || "output";
|
||||
const linuxExecutableName = process.env.NN_PRODUCT_NAME
|
||||
? process.env.NN_PRODUCT_NAME.toLowerCase().replace(/\s+/g, "-")
|
||||
: "notesnook";
|
||||
const year = new Date().getFullYear();
|
||||
const isBeta = pkg.version.includes("-beta");
|
||||
|
||||
/**
|
||||
* @type {import("app-builder-lib").Configuration}
|
||||
*/
|
||||
module.exports = {
|
||||
appId: appId,
|
||||
productName: productName,
|
||||
copyright: `Copyright © ${year} Streetwriters (Private) Limited`,
|
||||
artifactName: "notesnook_${os}_${arch}.${ext}",
|
||||
generateUpdatesFilesForAllChannels: true,
|
||||
asar: true,
|
||||
asarUnpack: [
|
||||
"node_modules/sqlite-better-trigram-@(linux|darwin|windows)-${arch}/**/*",
|
||||
"node_modules/sqlite3-fts5-html-@(linux|darwin|windows)-${arch}/**/*"
|
||||
],
|
||||
files: [
|
||||
"!*.chunk.js.map",
|
||||
"!*.chunk.js.LICENSE.txt",
|
||||
...buildFiles,
|
||||
"!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/sqlite-better-trigram",
|
||||
"node_modules/sqlite3-fts5-html",
|
||||
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
|
||||
{
|
||||
from: "node_modules/sqlite-better-trigram-linux-${arch}",
|
||||
to: "node_modules/sqlite-better-trigram-linux-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite-better-trigram-darwin-${arch}",
|
||||
to: "node_modules/sqlite-better-trigram-darwin-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite-better-trigram-windows-${arch}",
|
||||
to: "node_modules/sqlite-better-trigram-windows-${arch}"
|
||||
},
|
||||
|
||||
{
|
||||
from: "node_modules/sqlite3-fts5-html-linux-${arch}",
|
||||
to: "node_modules/sqlite3-fts5-html-linux-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite3-fts5-html-darwin-${arch}",
|
||||
to: "node_modules/sqlite3-fts5-html-darwin-${arch}"
|
||||
},
|
||||
{
|
||||
from: "node_modules/sqlite3-fts5-html-windows-${arch}",
|
||||
to: "node_modules/sqlite3-fts5-html-windows-${arch}"
|
||||
},
|
||||
|
||||
"node_modules/sodium-native/index.js",
|
||||
"node_modules/sodium-native/package.json"
|
||||
],
|
||||
afterPack: "./scripts/removeLocales.js",
|
||||
mac: {
|
||||
bundleVersion: "240",
|
||||
minimumSystemVersion: "10.12.0",
|
||||
target: [
|
||||
{
|
||||
target: "dmg",
|
||||
arch: ["arm64", "x64"]
|
||||
},
|
||||
{
|
||||
target: "zip",
|
||||
arch: ["arm64", "x64"]
|
||||
}
|
||||
],
|
||||
category: "public.app-category.productivity",
|
||||
darkModeSupport: true,
|
||||
type: "distribution",
|
||||
hardenedRuntime: true,
|
||||
entitlements: "assets/entitlements.mac.plist",
|
||||
entitlementsInherit: "assets/entitlements.mac.plist",
|
||||
gatekeeperAssess: false,
|
||||
icon: "assets/icons/app.icns",
|
||||
notarize: true
|
||||
},
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
x: 130,
|
||||
y: 220
|
||||
},
|
||||
{
|
||||
x: 410,
|
||||
y: 220,
|
||||
type: "link",
|
||||
path: "/Applications"
|
||||
}
|
||||
],
|
||||
icon: "assets/icons/app.icns",
|
||||
title: "Install Notesnook"
|
||||
},
|
||||
mas: {
|
||||
entitlements: "assets/entitlements.mas.plist",
|
||||
entitlementsInherit: "assets/entitlements.mas.inherit.plist",
|
||||
entitlementsLoginHelper: "assets/entitlements.mas.loginhelper.plist",
|
||||
hardenedRuntime: true
|
||||
},
|
||||
win: {
|
||||
target: [
|
||||
{
|
||||
target: "nsis",
|
||||
arch: ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
target: "portable",
|
||||
arch: ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
signtoolOptions: {
|
||||
signingHashAlgorithms: ["sha256"],
|
||||
sign: "./scripts/sign.js"
|
||||
},
|
||||
icon: "assets/icons/app.ico"
|
||||
},
|
||||
portable: {
|
||||
artifactName: "notesnook_${os}_${arch}_portable.${ext}"
|
||||
},
|
||||
nsis: {
|
||||
oneClick: true,
|
||||
createDesktopShortcut: "always",
|
||||
deleteAppDataOnUninstall: true
|
||||
},
|
||||
linux: {
|
||||
target: [
|
||||
{
|
||||
target: "AppImage",
|
||||
arch: ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
target: "snap",
|
||||
arch: ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
category: "Office",
|
||||
icon: "assets/icons/app.icns",
|
||||
description: "Your private note taking space",
|
||||
executableName: linuxExecutableName,
|
||||
desktop: {
|
||||
desktopActions: {
|
||||
"new-note": {
|
||||
Name: "New note",
|
||||
Exec: `${linuxExecutableName} new note`
|
||||
},
|
||||
"new-notebook": {
|
||||
Name: "New notebook",
|
||||
Exec: `${linuxExecutableName} new notebook`
|
||||
},
|
||||
"new-reminder": {
|
||||
Name: "New reminder",
|
||||
Exec: `${linuxExecutableName} new reminder`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
snap: {
|
||||
autoStart: false,
|
||||
confinement: "strict",
|
||||
allowNativeWayland: true,
|
||||
base: "core22"
|
||||
},
|
||||
extraResources: ["app-update.yml", "./assets/**"],
|
||||
extraMetadata: {
|
||||
main: path.join(buildRoot, "build", "electron.js")
|
||||
},
|
||||
directories: {
|
||||
buildResources: "assets",
|
||||
output: outputDir
|
||||
},
|
||||
publish: [
|
||||
{
|
||||
provider: "github",
|
||||
repo: "notesnook",
|
||||
owner: "streetwriters",
|
||||
channel: isBeta ? "beta" : "latest"
|
||||
}
|
||||
]
|
||||
};
|
||||
5912
apps/desktop/package-lock.json
generated
5912
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,226 +2,82 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.17",
|
||||
"version": "3.3.5",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"sideEffects": [
|
||||
"src/overrides.ts"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"require": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/cjs/index.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homepage": "https://notesnook.com/",
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@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.2.1",
|
||||
"icojs": "^0.17.1",
|
||||
"@lingui/core": "5.1.2",
|
||||
"@notesnook/intl": "file:../../packages/intl",
|
||||
"@notesnook/ui": "file:../../packages/ui",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"better-sqlite3-multiple-ciphers": "^12.4.1",
|
||||
"electron-trpc": "0.7.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"icojs": "^0.19.5",
|
||||
"sqlite-better-trigram": "0.0.3",
|
||||
"sqlite3-fts5-html": "^0.0.4",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.6.2",
|
||||
"zod": "^3.21.4"
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^29.3.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"esbuild": "^0.23.0",
|
||||
"kysely": "^0.27.3",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.15.3",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^37.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"esbuild": "0.21.5",
|
||||
"node-abi": "^4.5.0",
|
||||
"node-gyp-build": "^4.8.4",
|
||||
"playwright": "1.48.2",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^6.14.1"
|
||||
"undici": "^7.8.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
},
|
||||
"overrides": {
|
||||
"@electron/node-gyp": "^10.2.0-electron.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/dev.mjs",
|
||||
"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 --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",
|
||||
"build": "node ../../scripts/build.mjs",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"postinstall": "patch-package"
|
||||
"postinstall": "patch-package",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
"email": "support@streetwriters.co",
|
||||
"url": "https://streetwriters.co"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.streetwriters.notesnook",
|
||||
"productName": "Notesnook",
|
||||
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
||||
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"asar": false,
|
||||
"files": [
|
||||
"!*.chunk.js.map",
|
||||
"!*.chunk.js.LICENSE.txt",
|
||||
"build/",
|
||||
"!build/screenshots${/*}",
|
||||
"!build/banner.jpg",
|
||||
"!build/*.ico",
|
||||
"!build/*.png",
|
||||
"!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"
|
||||
],
|
||||
"afterPack": "./scripts/removeLocales.js",
|
||||
"mac": {
|
||||
"bundleVersion": "240",
|
||||
"minimumSystemVersion": "10.12.0",
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"category": "public.app-category.productivity",
|
||||
"darkModeSupport": true,
|
||||
"type": "distribution",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "assets/entitlements.mac.plist",
|
||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||
"gatekeeperAssess": false,
|
||||
"icon": "assets/icons/app.icns",
|
||||
"notarize": true
|
||||
},
|
||||
"dmg": {
|
||||
"contents": [
|
||||
{
|
||||
"x": 130,
|
||||
"y": 220
|
||||
},
|
||||
{
|
||||
"x": 410,
|
||||
"y": 220,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"icon": "assets/icons/app.icns",
|
||||
"title": "Install Notesnook"
|
||||
},
|
||||
"mas": {
|
||||
"entitlements": "assets/entitlements.mas.plist",
|
||||
"entitlementsInherit": "assets/entitlements.mas.inherit.plist",
|
||||
"entitlementsLoginHelper": "assets/entitlements.mas.loginhelper.plist",
|
||||
"hardenedRuntime": true
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./scripts/sign.js",
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "notesnook_${os}_${arch}_portable.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": true,
|
||||
"createDesktopShortcut": "always"
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "snap",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"category": "Office",
|
||||
"icon": "assets/icons/app.icns",
|
||||
"description": "Your private note taking space",
|
||||
"executableName": "notesnook",
|
||||
"desktop": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "new-note",
|
||||
"name": "New note",
|
||||
"args": "new note"
|
||||
},
|
||||
{
|
||||
"id": "new-notebook",
|
||||
"name": "New notebook",
|
||||
"args": "new notebook"
|
||||
},
|
||||
{
|
||||
"id": "new-reminder",
|
||||
"name": "New reminder",
|
||||
"args": "new reminder"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"snap": {
|
||||
"autoStart": false,
|
||||
"confinement": "strict",
|
||||
"allowNativeWayland": true
|
||||
},
|
||||
"extraResources": [
|
||||
"app-update.yml",
|
||||
"./assets/**"
|
||||
],
|
||||
"extraMetadata": {
|
||||
"main": "./build/electron.js"
|
||||
},
|
||||
"directories": {
|
||||
"buildResources": "assets",
|
||||
"output": "./output/"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"repo": "notesnook",
|
||||
"owner": "streetwriters"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
index fcb7f54..cb1c7f7 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
+ actions: undefined
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
+
|
||||
+ if (targetSpecificOptions.desktop.actions) {
|
||||
+ let actionsData = "";
|
||||
+ const validActions = [];
|
||||
+ for (const action of targetSpecificOptions.desktop.actions) {
|
||||
+ if (!action.id || !action.name || !action.args) continue;
|
||||
+ actionsData += "\n";
|
||||
+ actionsData += `[Desktop Action ${action.id}]
|
||||
+Name=${action.name}
|
||||
+Exec=${desktopMeta.Exec} ${action.args}`;
|
||||
+ actionsData += "\n";
|
||||
+
|
||||
+ validActions.push(action.id);
|
||||
+ }
|
||||
+
|
||||
+ data += `Actions=${validActions.join(";")};\n${actionsData}`
|
||||
+ }
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
}
|
||||
13
apps/desktop/patches/app-builder-lib+26.0.12.patch
Normal file
13
apps/desktop/patches/app-builder-lib+26.0.12.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/snap.js b/node_modules/app-builder-lib/out/targets/snap.js
|
||||
index 0b5405e..753f6c6 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/snap.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/snap.js
|
||||
@@ -115,7 +115,7 @@ class SnapTarget extends core_1.Target {
|
||||
else {
|
||||
const archTriplet = archNameToTriplet(arch);
|
||||
appDescriptor.environment = {
|
||||
- DISABLE_WAYLAND: options.allowNativeWayland ? "" : "1",
|
||||
+ DISABLE_WAYLAND: options.allowNativeWayland ? "0" : "1",
|
||||
PATH: "$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH",
|
||||
SNAP_DESKTOP_RUNTIME: "$SNAP/gnome-platform",
|
||||
LD_LIBRARY_PATH: [
|
||||
@@ -0,0 +1,11 @@
|
||||
diff --git a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
|
||||
index 1a14ecd..ff938f1 100644
|
||||
--- a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
|
||||
+++ b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
|
||||
@@ -38,5 +38,6 @@
|
||||
'SQLITE_TRACE_SIZE_LIMIT=32',
|
||||
'SQLITE_USER_AUTHENTICATION=0',
|
||||
'SQLITE_USE_URI=0',
|
||||
+ 'SQLITE_ENABLE_REGEXP',
|
||||
],
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||
index 379cf3b..2644e5d 100644
|
||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||
@@ -221,9 +221,16 @@ class G {
|
||||
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
|
||||
}
|
||||
detachWindow(r) {
|
||||
+
|
||||
y(this, c, i(this, c).filter((n) => n !== r));
|
||||
- for (const [n, t] of i(this, u).entries())
|
||||
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ for (const [n, t] of i(this, u).entries()) {
|
||||
+ try {
|
||||
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ } catch(e) {
|
||||
+ console.error(e);
|
||||
+ // ignore
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {
|
||||
@@ -1,46 +0,0 @@
|
||||
diff --git a/node_modules/node-gyp-build/bin.js b/node_modules/node-gyp-build/bin.js
|
||||
index 3fbcdf0..7ca3ab5 100644
|
||||
--- a/node_modules/node-gyp-build/bin.js
|
||||
+++ b/node_modules/node-gyp-build/bin.js
|
||||
@@ -16,7 +16,8 @@ if (!buildFromSource()) {
|
||||
}
|
||||
|
||||
function build () {
|
||||
- var args = [os.platform() === 'win32' ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
|
||||
+ var win32 = os.platform() === 'win32'
|
||||
+ var args = [win32 ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
|
||||
|
||||
try {
|
||||
var pkg = require('node-gyp/package.json')
|
||||
@@ -27,7 +28,7 @@ function build () {
|
||||
]
|
||||
} catch (_) {}
|
||||
|
||||
- proc.spawn(args[0], args.slice(1), { stdio: 'inherit' }).on('exit', function (code) {
|
||||
+ proc.spawn(args[0], args.slice(1), { stdio: 'inherit', shell: win32, windowsHide: true }).on('exit', function (code) {
|
||||
if (code || !process.argv[3]) process.exit(code)
|
||||
exec(process.argv[3]).on('exit', function (code) {
|
||||
process.exit(code)
|
||||
@@ -45,15 +46,18 @@ function preinstall () {
|
||||
|
||||
function exec (cmd) {
|
||||
if (process.platform !== 'win32') {
|
||||
- var shell = os.platform() === 'android' ? 'sh' : '/bin/sh'
|
||||
- return proc.spawn(shell, ['-c', '--', cmd], {
|
||||
+ var shell = os.platform() === 'android' ? 'sh' : true
|
||||
+ return proc.spawn(cmd, [], {
|
||||
+ shell,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
}
|
||||
|
||||
- return proc.spawn(process.env.comspec || 'cmd.exe', ['/s', '/c', '"' + cmd + '"'], {
|
||||
+ return proc.spawn(cmd, [], {
|
||||
windowsVerbatimArguments: true,
|
||||
- stdio: 'inherit'
|
||||
+ stdio: 'inherit',
|
||||
+ shell: true,
|
||||
+ windowsHide: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,60 +17,72 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
console.log("starting build...");
|
||||
import path from "path";
|
||||
import fs, { readFile, writeFile } from "fs/promises";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
import yargs from "yargs-parser";
|
||||
import os from "os";
|
||||
import * as childProcess from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
|
||||
|
||||
console.log("imports done...");
|
||||
const args = yargs(process.argv);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
|
||||
);
|
||||
const root = args.root || path.join(__dirname, "..");
|
||||
const skipTscBuild = args.skipTscBuild || false;
|
||||
|
||||
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
console.log("loaded args", {
|
||||
args,
|
||||
__filename,
|
||||
__dirname,
|
||||
root,
|
||||
skipTscBuild,
|
||||
webAppPath
|
||||
});
|
||||
await fs.rm(path.join(root, "build"), { force: true, recursive: true });
|
||||
|
||||
console.log("removed build folder");
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
console.log("rebuilding...");
|
||||
await exec(
|
||||
"yarn nx build:desktop @notesnook/web",
|
||||
"node scripts/execute.mjs @notesnook/web:build:desktop",
|
||||
path.join(__dirname, "..", "..", "..")
|
||||
);
|
||||
}
|
||||
|
||||
// temporary until there's support for prebuilt binaries for linux ARM
|
||||
if (os.platform() === "linux") await patchBetterSQLite3();
|
||||
await patchBetterSQLite3();
|
||||
|
||||
// if (os.platform() === "win32")
|
||||
// await exec(
|
||||
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
// path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
// );
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
await fs.cp(path.join(webAppPath, "build"), path.join(root, "build"), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
if (args.variant === "mas") {
|
||||
await exec(`yarn run bundle:mas`);
|
||||
await exec(`yarn run bundle:mas --outdir=${path.join(root, "build")}`);
|
||||
} else {
|
||||
await exec(`yarn run bundle`);
|
||||
await exec(`yarn run bundle --outdir=${path.join(root, "build")}`);
|
||||
}
|
||||
|
||||
await exec(`yarn tsc`);
|
||||
if (!skipTscBuild) {
|
||||
await exec(`yarn run build`);
|
||||
}
|
||||
|
||||
if (args.run) {
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
await exec(
|
||||
`yarn electron-builder --dir --${process.arch} --config=electron-builder.config.js`
|
||||
);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
if (process.arch === "arm64")
|
||||
await exec(`./output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
else await exec(`./output/mac/Notesnook.app/Contents/MacOS/Notesnook`);
|
||||
} else {
|
||||
await exec(`./output/linux-unpacked/Notesnook`);
|
||||
}
|
||||
@@ -83,21 +95,3 @@ async function exec(cmd, cwd) {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ const ENV = {
|
||||
FORCE_COLOR: "false",
|
||||
COLOR: "0"
|
||||
};
|
||||
process.chdir(path.join(__dirname, ".."));
|
||||
|
||||
await onChange(true);
|
||||
|
||||
console.log("Watching...");
|
||||
@@ -53,11 +55,13 @@ async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await exec("npm rebuild electron --verbose --foreground-scripts");
|
||||
|
||||
await exec("yarn electron-builder install-app-deps");
|
||||
}
|
||||
|
||||
await exec(`yarn run bundle`);
|
||||
execAsync(`yarn`, [`tsc`]);
|
||||
await exec(`yarn run build`);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
@@ -66,8 +70,8 @@ async function onChange(first) {
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
["npm", "run", "start:desktop"],
|
||||
path.join(__dirname, "..", "..", "web"),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
}
|
||||
|
||||
58
apps/desktop/scripts/patch-better-sqlite3.mjs
Normal file
58
apps/desktop/scripts/patch-better-sqlite3.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 { readFile, rm, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export async function patchBetterSQLite3() {
|
||||
console.log("Patching better-sqlite3");
|
||||
|
||||
const jsonPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"better-sqlite3-multiple-ciphers",
|
||||
"package.json"
|
||||
);
|
||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||
|
||||
delete json.homepage;
|
||||
delete json.repository;
|
||||
|
||||
await writeFile(jsonPath, JSON.stringify(json));
|
||||
|
||||
await rm(
|
||||
path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"better-sqlite3-multiple-ciphers",
|
||||
"build"
|
||||
),
|
||||
{ force: true, recursive: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (process.argv[1] === __filename) {
|
||||
patchBetterSQLite3();
|
||||
}
|
||||
@@ -19,7 +19,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { app, dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import {
|
||||
app,
|
||||
dialog,
|
||||
Menu,
|
||||
MenuItem,
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
Notification,
|
||||
shell
|
||||
} from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
@@ -29,10 +38,11 @@ import { dirname } from "path";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak } from "../utils";
|
||||
import { isFlatpak, isSnap } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
import { rm } from "fs/promises";
|
||||
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
|
||||
import type { MenuItem as NNMenuItem } from "@notesnook/ui";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -49,6 +59,7 @@ const NotificationOptions = z.object({
|
||||
|
||||
export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
isSnap: t.procedure.query(() => isSnap()),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
@@ -224,5 +235,63 @@ export const osIntegrationRouter = t.router({
|
||||
nativeTheme.off("updated", updated);
|
||||
};
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
showMenu: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
menuItems: z.array(z.any())
|
||||
})
|
||||
)
|
||||
.subscription(({ input: { menuItems } }) =>
|
||||
observable<string[]>((emit) => {
|
||||
const items = menuItems as NNMenuItem[];
|
||||
const menu = new Menu();
|
||||
for (const item of items) {
|
||||
const menuItem = toMenuItem(item, (id) => emit.next(id));
|
||||
if (menuItem) menu.append(menuItem);
|
||||
}
|
||||
if (menu.items.length > 0) menu.popup();
|
||||
return () => {
|
||||
menu.removeAllListeners();
|
||||
menu.closePopup();
|
||||
};
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
function toMenuItem(
|
||||
item: NNMenuItem,
|
||||
onClick: (id: string[]) => void,
|
||||
parentKey?: string
|
||||
): MenuItem | undefined {
|
||||
switch (item.type) {
|
||||
case "lazy-loader":
|
||||
return undefined;
|
||||
case "separator":
|
||||
return new MenuItem({ type: "separator" });
|
||||
case "button": {
|
||||
const submenu = item.menu ? new Menu() : undefined;
|
||||
if (submenu && item.menu) {
|
||||
for (const subitem of item.menu.items) {
|
||||
const subMenuItem = toMenuItem(subitem, onClick, item.key);
|
||||
if (subMenuItem) submenu.append(subMenuItem);
|
||||
}
|
||||
}
|
||||
|
||||
return new MenuItem({
|
||||
label: item.title,
|
||||
enabled: !item.isDisabled,
|
||||
visible: !item.isHidden,
|
||||
toolTip: item.tooltip,
|
||||
sublabel: item.tooltip,
|
||||
checked: item.isChecked,
|
||||
type: submenu ? "submenu" : item.isChecked ? "checkbox" : "normal",
|
||||
id: item.key,
|
||||
submenu,
|
||||
click: () => onClick(parentKey ? [parentKey, item.key] : [item.key]),
|
||||
accelerator: item.modifier?.replace("Mod", "CommandOrControl")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { Database, Statement } from "better-sqlite3-multiple-ciphers";
|
||||
import type { QueryResult } from "kysely";
|
||||
import type { QueryResult } from "@streetwriters/kysely";
|
||||
|
||||
type SQLiteCompatibleType =
|
||||
| number
|
||||
@@ -33,6 +33,8 @@ export class SQLite {
|
||||
initialized = false;
|
||||
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
|
||||
retryCounter: Record<string, number> = {};
|
||||
extensionsLoaded = false;
|
||||
|
||||
constructor() {
|
||||
console.log("new sqlite worker");
|
||||
}
|
||||
@@ -113,9 +115,29 @@ export class SQLite {
|
||||
} catch (e) {
|
||||
if (e instanceof Error) e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
} finally {
|
||||
// Since SQLite 3.48.0 (SQLite3MC v2.0.2) it's not possible to load fts5
|
||||
// extensions before database has been decrypting. This is because
|
||||
// executing a `SELECT` now accesses the underlying databases resulting in
|
||||
// an error. Since FTS5 extensions depend on `SELECT fts5` to load the
|
||||
// fts5 API, we must wait decrypt the database before we can load
|
||||
// the extensions.
|
||||
if (!this.extensionsLoaded && (await this.isDatabaseReady())) {
|
||||
this.loadExtensions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadExtensions() {
|
||||
this.sqlite?.loadExtension(
|
||||
getExtensionPath("sqlite-better-trigram", "better-trigram")
|
||||
);
|
||||
this.sqlite?.loadExtension(
|
||||
getExtensionPath("sqlite3-fts5-html", "fts5-html")
|
||||
);
|
||||
this.extensionsLoaded = true;
|
||||
}
|
||||
|
||||
async run<R>(
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
@@ -140,4 +162,53 @@ export class SQLite {
|
||||
retryDelay: 500
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This just executes `SELECT 1` on the database to make sure its ready.
|
||||
* On an encrypted database, this will fail until `PRAGMA key` has been
|
||||
* called.
|
||||
*/
|
||||
private async isDatabaseReady() {
|
||||
// return this.exec(`SELECT 1;`)
|
||||
// .then(() => true)
|
||||
// .catch(() => false);
|
||||
if (!this.sqlite) return false;
|
||||
try {
|
||||
this.sqlite.prepare(`SELECT 1;`).run();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExtensionPath(extensionName: string, entryPoint: string) {
|
||||
const path = require("path");
|
||||
const { statSync } = require("fs");
|
||||
|
||||
const os = process.platform === "win32" ? "windows" : process.platform;
|
||||
const packageName = `${extensionName}-${os}-${process.arch}`;
|
||||
const extensionSuffix =
|
||||
process.platform === "win32"
|
||||
? "dll"
|
||||
: process.platform === "darwin"
|
||||
? "dylib"
|
||||
: "so";
|
||||
let loadablePath = path.join(
|
||||
require.resolve(extensionName),
|
||||
"..",
|
||||
"..",
|
||||
packageName,
|
||||
`${entryPoint}.${extensionSuffix}`
|
||||
);
|
||||
|
||||
if (loadablePath.includes(".asar"))
|
||||
loadablePath = loadablePath
|
||||
.replace("electron.asar", "app.asar")
|
||||
.replace(".asar", ".asar.unpacked");
|
||||
|
||||
if (!statSync(loadablePath, { throwIfNoEntry: false })) {
|
||||
throw new Error(`${extensionName} not found at ${loadablePath}.`);
|
||||
}
|
||||
return loadablePath;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { CancellationToken, autoUpdater } from "electron-updater";
|
||||
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
|
||||
import { z } from "zod";
|
||||
import { config } from "../utils/config";
|
||||
import { app } from "electron";
|
||||
|
||||
type UpdateInfo = { version: string };
|
||||
type Progress = { percent: number };
|
||||
@@ -33,6 +34,7 @@ let downloadTimeout: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
export const updaterRouter = t.router({
|
||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||
releaseTrack: t.procedure.query(() => config.releaseTrack),
|
||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||
download: t.procedure.query(async () => {
|
||||
if (cancellationToken) return;
|
||||
@@ -67,7 +69,13 @@ export const updaterRouter = t.router({
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
config.automaticUpdates = enabled;
|
||||
}),
|
||||
|
||||
changeReleaseTrack: t.procedure
|
||||
.input(z.object({ track: z.string() }))
|
||||
.mutation(({ input: { track } }) => {
|
||||
config.releaseTrack = track;
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}),
|
||||
onChecking: createSubscription("checking-for-update"),
|
||||
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
|
||||
"update-downloaded"
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./constants";
|
||||
export { PATHS } from "./constants";
|
||||
export type { AppRouter } from "./api";
|
||||
export { type UpdateInfo } from "builder-util-runtime";
|
||||
export { type DesktopIntegration } from "./utils/config";
|
||||
|
||||
@@ -17,7 +17,8 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, nativeTheme, shell } from "electron";
|
||||
import "./overrides";
|
||||
import { app, BrowserWindow, nativeTheme, shell, dialog } from "electron";
|
||||
import { isDevelopment } from "./utils";
|
||||
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
|
||||
import { configureAutoUpdater } from "./utils/autoupdater";
|
||||
@@ -36,6 +37,20 @@ import { bringToFront } from "./utils/bring-to-front";
|
||||
import { bridge } from "./api/bridge";
|
||||
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
||||
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
|
||||
import { Messages, setI18nGlobal } from "@notesnook/intl";
|
||||
import { i18n } from "@lingui/core";
|
||||
|
||||
const locale =
|
||||
process.env.NODE_ENV === "development"
|
||||
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
|
||||
: import("@notesnook/intl/locales/$en.json");
|
||||
locale.then(({ default: locale }) => {
|
||||
i18n.load({
|
||||
en: locale.messages as unknown as Messages
|
||||
});
|
||||
i18n.activate("en");
|
||||
});
|
||||
setI18nGlobal(i18n);
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
@@ -71,6 +86,8 @@ async function createWindow() {
|
||||
const mainWindowState = new WindowState({});
|
||||
const mainWindow = new BrowserWindow({
|
||||
show: !cliOptions.hidden,
|
||||
paintWhenInitiallyHidden: cliOptions.hidden,
|
||||
skipTaskbar: cliOptions.hidden,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
@@ -87,7 +104,10 @@ async function createWindow() {
|
||||
...(config.desktopSettings.nativeTitlebar
|
||||
? {}
|
||||
: {
|
||||
titleBarStyle: "hidden",
|
||||
titleBarStyle:
|
||||
process.platform === "win32" || process.platform === "darwin"
|
||||
? "hidden"
|
||||
: "default",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
@@ -128,6 +148,11 @@ async function createWindow() {
|
||||
await AssetManager.loadIcons();
|
||||
setupDesktopIntegration(config.desktopSettings);
|
||||
|
||||
mainWindow.webContents.session.setPermissionRequestHandler(
|
||||
(webContents, permission, callback) => {
|
||||
callback(permission === "geolocation" ? false : true);
|
||||
}
|
||||
);
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
@@ -157,6 +182,17 @@ async function createWindow() {
|
||||
app.once("ready", async () => {
|
||||
console.info("App ready. Opening window.");
|
||||
|
||||
if (app.runningUnderARM64Translation) {
|
||||
console.log("App is running under ARM64 translation");
|
||||
dialog.showMessageBoxSync({
|
||||
message:
|
||||
"Notesnook detected that it is running under ARM64 translation. For the best performance, please download the ARM64 build of Notesnook from our website.",
|
||||
type: "warning",
|
||||
buttons: ["Okay"],
|
||||
title: "Degraded Performance Warning"
|
||||
});
|
||||
}
|
||||
|
||||
if (config.customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
|
||||
|
||||
41
apps/desktop/src/overrides.ts
Normal file
41
apps/desktop/src/overrides.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { app } from "electron";
|
||||
import path from "path";
|
||||
|
||||
const customVersion = process.env.CUSTOM_APP_VERSION;
|
||||
if (customVersion) {
|
||||
app.getVersion = () => customVersion;
|
||||
console.log("setting custom version:", customVersion);
|
||||
}
|
||||
|
||||
if (process.env.CUSTOM_USER_DATA_DIR) {
|
||||
app.setPath(
|
||||
"appData",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "AppData")
|
||||
);
|
||||
app.setPath(
|
||||
"userData",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "UserData")
|
||||
);
|
||||
app.setPath(
|
||||
"documents",
|
||||
path.join(process.env.CUSTOM_USER_DATA_DIR, "Documents")
|
||||
);
|
||||
}
|
||||
@@ -19,22 +19,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/* eslint-disable no-var */
|
||||
|
||||
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
||||
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
// import type { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { platform } from "os";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
var electronTRPC: any;
|
||||
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
const electronTRPC: RendererGlobalElectronTRPC = {
|
||||
sendMessage: (operation) =>
|
||||
const electronTRPC = {
|
||||
sendMessage: (operation: any) =>
|
||||
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
||||
onMessage: (callback) =>
|
||||
onMessage: (callback: any) =>
|
||||
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
|
||||
};
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { NativeImage, nativeImage } from "electron";
|
||||
import path from "path";
|
||||
import { isDevelopment } from "./index";
|
||||
import { parse, ParsedImage } from "icojs";
|
||||
import { ParsedImage, parseICO } from "icojs";
|
||||
import { getSystemTheme } from "./theme";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
@@ -71,7 +71,7 @@ export class AssetManager {
|
||||
`${icon}${prefix}.ico`
|
||||
);
|
||||
const icoBuffer = await readFile(icoPath);
|
||||
const images = await parse(icoBuffer, "image/png");
|
||||
const images = await parseICO(icoBuffer, "image/png");
|
||||
ALL_ICONS.push({ id: icon, images, prefix });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,19 +20,22 @@ 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() {
|
||||
const releaseTrack =
|
||||
config.releaseTrack === "stable" ? "latest" : config.releaseTrack;
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "generic",
|
||||
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
|
||||
url: `https://notesnook.com/api/v1/releases/${process.platform}/${releaseTrack}`,
|
||||
useMultipleRangeRequest: false,
|
||||
channel: CHANNEL
|
||||
channel: releaseTrack
|
||||
});
|
||||
|
||||
autoUpdater.autoDownload = config.automaticUpdates;
|
||||
autoUpdater.allowDowngrade = false;
|
||||
autoUpdater.allowDowngrade =
|
||||
// only allow downgrade if the current version is a prerelease
|
||||
// and the user has changed the release track to stable
|
||||
config.releaseTrack === "stable" &&
|
||||
autoUpdater.currentVersion.prerelease.length > 0;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { nativeTheme } from "electron";
|
||||
import { JSONStorage } from "./json-storage";
|
||||
import { z } from "zod";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
@@ -46,6 +47,9 @@ export const config = {
|
||||
automaticUpdates: true,
|
||||
proxyRules: "",
|
||||
customDns: true,
|
||||
releaseTrack: autoUpdater.currentVersion.raw.includes("-beta")
|
||||
? "beta"
|
||||
: "stable",
|
||||
|
||||
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
|
||||
windowControlsIconColor:
|
||||
|
||||
@@ -29,3 +29,7 @@ export function isDevelopment() {
|
||||
export function isFlatpak() {
|
||||
return existsSync("/.flatpak-info");
|
||||
}
|
||||
|
||||
export function isSnap() {
|
||||
return process.env.SNAP !== undefined;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Menu, MenuItem, clipboard, shell } from "electron";
|
||||
|
||||
function setupMenu() {
|
||||
@@ -40,7 +41,7 @@ function setupMenu() {
|
||||
if (params.misspelledWord) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Add to dictionary",
|
||||
label: strings.addToDictionary(),
|
||||
click: () =>
|
||||
globalThis.window?.webContents.session.addWordToSpellCheckerDictionary(
|
||||
params.misspelledWord
|
||||
@@ -59,7 +60,7 @@ function setupMenu() {
|
||||
if (params.linkURL.length) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Open in browser",
|
||||
label: strings.openInBrowser(),
|
||||
click: () => shell.openExternal(params.linkURL)
|
||||
})
|
||||
);
|
||||
@@ -68,7 +69,7 @@ function setupMenu() {
|
||||
if (params.isEditable) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Undo",
|
||||
label: strings.undo(),
|
||||
role: "undo",
|
||||
enabled: params.isEditable,
|
||||
accelerator: "CommandOrControl+Z"
|
||||
@@ -77,7 +78,7 @@ function setupMenu() {
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Redo",
|
||||
label: strings.redo(),
|
||||
role: "redo",
|
||||
enabled: params.isEditable,
|
||||
accelerator: "CommandOrControl+Y"
|
||||
@@ -94,7 +95,7 @@ function setupMenu() {
|
||||
if (params.isEditable)
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Cut",
|
||||
label: strings.cut(),
|
||||
role: "cut",
|
||||
enabled: params.selectionText.length > 0,
|
||||
accelerator: "CommandOrControl+X"
|
||||
@@ -104,7 +105,7 @@ function setupMenu() {
|
||||
if (params.linkURL?.length) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy link",
|
||||
label: strings.copyLink(),
|
||||
click() {
|
||||
clipboard.writeText(params.linkURL);
|
||||
}
|
||||
@@ -113,7 +114,7 @@ function setupMenu() {
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy link text",
|
||||
label: strings.copyLinkText(),
|
||||
click() {
|
||||
clipboard.writeText(params.linkText);
|
||||
}
|
||||
@@ -124,7 +125,7 @@ function setupMenu() {
|
||||
if (params.selectionText.length) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy",
|
||||
label: strings.copy(),
|
||||
role: "copy",
|
||||
accelerator: "CommandOrControl+C"
|
||||
})
|
||||
@@ -135,23 +136,51 @@ function setupMenu() {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
id: "copy-image",
|
||||
label: "Copy Image",
|
||||
label: strings.copyImage(),
|
||||
click() {
|
||||
globalThis.window?.webContents.copyImageAt(params.x, params.y);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (params.isEditable)
|
||||
if (params.isEditable) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Paste",
|
||||
role: "pasteAndMatchStyle",
|
||||
label: strings.paste(),
|
||||
role: "paste",
|
||||
enabled: clipboard.readText("clipboard").length > 0,
|
||||
accelerator: "CommandOrControl+V"
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label:
|
||||
process.platform === "darwin"
|
||||
? strings.pasteAndMatchStyle()
|
||||
: strings.pasteWithoutFormatting(),
|
||||
role: "pasteAndMatchStyle",
|
||||
enabled: clipboard.readText("clipboard").length > 0,
|
||||
accelerator:
|
||||
process.platform === "darwin"
|
||||
? "Option+Shift+Command+V"
|
||||
: "Shift+CommandOrControl+V"
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
type: "separator"
|
||||
})
|
||||
);
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: strings.spellCheck(),
|
||||
role: "toggleSpellChecker"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (menu.items.length > 0) menu.popup();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ function getSystemTheme() {
|
||||
nativeTheme.themeSource = oldThemeSource;
|
||||
|
||||
setTimeout(
|
||||
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||
() =>
|
||||
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
|
||||
1000
|
||||
);
|
||||
return currentTheme;
|
||||
@@ -72,7 +73,8 @@ function changeTheme(theme: Theme) {
|
||||
nativeTheme.themeSource = theme;
|
||||
|
||||
setTimeout(
|
||||
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||
() =>
|
||||
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
|
||||
1000
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"lib": ["ESNext"]
|
||||
"lib": ["ESNext"],
|
||||
"moduleResolution": "Bundler"
|
||||
},
|
||||
"include": ["src", "global.d.ts"]
|
||||
}
|
||||
|
||||
42
apps/desktop/vitest.config.ts
Normal file
42
apps/desktop/vitest.config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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 { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120 * 1000,
|
||||
hookTimeout: 120 * 1000,
|
||||
sequence: {
|
||||
concurrent: true,
|
||||
shuffle: true
|
||||
},
|
||||
globalSetup: "./__tests__/global-setup.ts",
|
||||
dir: "./__tests__/",
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/cypress/**",
|
||||
"**/.{idea,git,cache,output,temp}/**",
|
||||
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*",
|
||||
"**/test-results/**",
|
||||
"**/test-artifacts/**"
|
||||
]
|
||||
}
|
||||
});
|
||||
2
apps/mobile/.gitignore
vendored
2
apps/mobile/.gitignore
vendored
@@ -39,6 +39,7 @@ DerivedData
|
||||
*.xcuserstate
|
||||
*.hprof
|
||||
**/.xcode.env.local
|
||||
native/cache
|
||||
# Android/IntelliJ
|
||||
#
|
||||
rn-build-deps/
|
||||
@@ -68,6 +69,7 @@ buck-out/
|
||||
*/fastlane/report.xml
|
||||
*/fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
native/vendor
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
@@ -16,26 +16,33 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import {
|
||||
ScopedThemeProvider,
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import { Appearance, I18nManager, StatusBar } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
import DialogProvider from "./components/dialog-provider";
|
||||
import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
import { Toast } from "./components/toast";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { ApplicationHolder } from "./navigation";
|
||||
import { NotePreviewConfigure } from "./screens/note-preview-configure";
|
||||
import { RootNavigation } from "./navigation/navigation-stack";
|
||||
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 { changeSystemBarColors, useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import RNBootSplash from "react-native-bootsplash";
|
||||
import AppLocked from "./components/app-lock";
|
||||
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
@@ -45,11 +52,13 @@ if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
RNBootSplash.hide();
|
||||
const App = (props: { configureMode: "note-preview" }) => {
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
useEffect(() => {
|
||||
changeSystemBarColors();
|
||||
SettingsService.onFirstLaunch();
|
||||
setTimeout(async () => {
|
||||
await Notifications.get();
|
||||
@@ -59,37 +68,29 @@ const App = () => {
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
}, []);
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: -1
|
||||
}}
|
||||
pointerEvents="none"
|
||||
>
|
||||
<SafeAreaProvider>
|
||||
<GlobalSafeAreaProvider />
|
||||
</SafeAreaProvider>
|
||||
</View>
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<GestureHandlerRootView
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<ApplicationHolder />
|
||||
<GlobalSafeAreaProvider />
|
||||
{props.configureMode === "note-preview" ? (
|
||||
<NotePreviewConfigure />
|
||||
) : (
|
||||
<RootNavigation />
|
||||
)}
|
||||
<ScopedThemeProvider value="dialog">
|
||||
<Toast />
|
||||
</ScopedThemeProvider>
|
||||
<DialogProvider />
|
||||
<AppLocked />
|
||||
</GestureHandlerRootView>
|
||||
<AppLockedOverlay />
|
||||
</View>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -99,8 +100,8 @@ let currTheme =
|
||||
: SettingsService.getProperty("lighTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element: () => JSX.Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
export const withTheme = (Element: (props: any) => JSX.Element) => {
|
||||
return function AppWithThemeProvider(props: any) {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
state.darkTheme,
|
||||
@@ -119,26 +120,42 @@ export const withTheme = (Element: () => JSX.Element) => {
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
console.log(theme.version, "theme updated");
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const listener = Appearance.addChangeListener(({ colorScheme }) => {
|
||||
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
|
||||
useThemeStore.getState().setColorScheme(colorScheme);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
useEffect(() => {
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
}, [colorScheme, darkTheme, lightTheme]);
|
||||
|
||||
return <Element />;
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<Element {...props} />
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,385 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 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 { ToastManager } from "../../services/event-manager";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
// app lock with password enabled.
|
||||
export const CipherStorage = new MMKVLoader()
|
||||
.withInstanceID("cipher_storage")
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
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,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
}
|
||||
const charset =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||
let secret = "";
|
||||
for (const index of indexes) {
|
||||
secret += charset[index % charset.length];
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
|
||||
const key = getDatabaseKey();
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = await encrypt(appLockCredentials, key);
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem(DB_KEY_CIPHER);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
try {
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
);
|
||||
return typeof decrypted === "string";
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let DB_KEY;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
DatabaseLogger.info("Cleared database key");
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
try {
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
DatabaseLogger.info("Getting database key from cipher");
|
||||
DB_KEY = databaseKey;
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
const hasKey = await Keychain.hasInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
);
|
||||
if (hasKey) {
|
||||
let credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
DatabaseLogger.info("Getting database key from Keychain");
|
||||
DB_KEY = credentials.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
DatabaseLogger.info("Generating new database key");
|
||||
const password = generatePassword();
|
||||
const derivedDatabaseKey = await Sodium.deriveKey(
|
||||
password,
|
||||
NOTESNOOK_DB_KEY_SALT
|
||||
);
|
||||
|
||||
DB_KEY = derivedDatabaseKey.key;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
DB_KEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
}
|
||||
DatabaseLogger.info("Migrated user credentials to cipher storage");
|
||||
}
|
||||
|
||||
return DB_KEY;
|
||||
} catch (e) {
|
||||
ToastManager.error(e, "Error getting database key");
|
||||
console.log(e, "error");
|
||||
DatabaseLogger.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(data) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
try {
|
||||
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
console.log("getCryptoKey", e);
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCryptoKey(_name) {
|
||||
try {
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRandomBytes(length) {
|
||||
return await generateSecureRandom(length);
|
||||
}
|
||||
|
||||
export async function hash(password, email) {
|
||||
let result = await Sodium.hashPassword(password, email);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function generateCryptoKey(password, salt) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(password, salt || null);
|
||||
return credentials;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAlgorithm(base64Variant) {
|
||||
return `xcha-argon2i13-${base64Variant}`;
|
||||
}
|
||||
|
||||
export async function decrypt(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
let _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
|
||||
if (data.length && !password.salt) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
return {
|
||||
encryptionAlgorithm: enc,
|
||||
kdfAlgorithm: kdf,
|
||||
compressionAlgorithm: compressionAlg,
|
||||
isCompress: compressed === "1",
|
||||
base64_variant: base64variant
|
||||
};
|
||||
}
|
||||
|
||||
export async function encrypt(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let message = {
|
||||
type: "plain",
|
||||
data: data
|
||||
};
|
||||
let result = await Sodium.encrypt(password, message);
|
||||
|
||||
return {
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let results = await Sodium.encryptMulti(
|
||||
password,
|
||||
data.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
);
|
||||
|
||||
return !results
|
||||
? []
|
||||
: results.map((result) => ({
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
}));
|
||||
}
|
||||
479
apps/mobile/app/common/database/encryption.ts
Normal file
479
apps/mobile/app/common/database/encryption.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
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 Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
|
||||
import { SerializedKey } from "@notesnook/crypto";
|
||||
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 { ToastManager } from "../../services/event-manager";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
// app lock with password enabled.
|
||||
export const CipherStorage = new MMKVLoader()
|
||||
.withInstanceID("cipher_storage")
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
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,
|
||||
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
|
||||
service: IOS_KEYCHAIN_SERVICE_NAME
|
||||
},
|
||||
android: {}
|
||||
});
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
//@ts-ignore
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
"Crypto API is not supported. Please upgrade your web browser"
|
||||
);
|
||||
}
|
||||
const charset =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&+_{}[]():<>/?;";
|
||||
const indexes = crypto.getRandomValues(new Uint32Array(length));
|
||||
let secret = "";
|
||||
for (const index of indexes) {
|
||||
secret += charset[index % charset.length];
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
|
||||
const key = (await getDatabaseKey()) as string;
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(
|
||||
DB_KEY_CIPHER
|
||||
) as Cipher;
|
||||
const databaseKey = (await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
)) as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
databaseKey,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
MMKV.removeItem(DB_KEY_CIPHER);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = (await encrypt(
|
||||
appLockCredentials,
|
||||
generatePassword()
|
||||
)) as Cipher;
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCipher: Cipher = CipherStorage.getMap(
|
||||
APPLOCK_CIPHER
|
||||
) as Cipher;
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
);
|
||||
return typeof decrypted === "string";
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let DB_KEY: string | undefined;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
DatabaseLogger.info("Cleared database key");
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword?: string) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(
|
||||
"databaseKeyCipher"
|
||||
) as Cipher;
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
DatabaseLogger.info("Getting database key from cipher");
|
||||
DB_KEY = databaseKey;
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
const hasKey = await Keychain.hasInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
if (hasKey) {
|
||||
const credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
);
|
||||
|
||||
DatabaseLogger.info("Getting database key from Keychain");
|
||||
DB_KEY = (credentials as Keychain.UserCredentials).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 as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
"notesnook",
|
||||
DB_KEY,
|
||||
KEYSTORE_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook"
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher: Cipher = (await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
)) as Cipher;
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
}
|
||||
DatabaseLogger.info("Migrated user credentials to cipher storage");
|
||||
}
|
||||
|
||||
if (!DB_KEY) {
|
||||
throw new Error(
|
||||
`Failed to get database key, ${await Keychain.hasInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return DB_KEY;
|
||||
}
|
||||
|
||||
export async function deriveCryptoKeyFallback(data: SerializedKey) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
try {
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error(
|
||||
"Invalid password and salt provided to deriveCryptoKeyFallback"
|
||||
);
|
||||
|
||||
const credentials = await Sodium.deriveKeyFallback?.(
|
||||
data.password,
|
||||
data.salt
|
||||
);
|
||||
|
||||
if (!credentials) return;
|
||||
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key fallback stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(data: SerializedKey) {
|
||||
try {
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error("Invalid password and salt provided to deriveCryptoKey");
|
||||
|
||||
const credentials = (await Sodium.deriveKey(
|
||||
data.password,
|
||||
data.salt
|
||||
)) as Password;
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey() {
|
||||
try {
|
||||
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER) as Cipher;
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCryptoKey() {
|
||||
try {
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
return true;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRandomBytes(length: number) {
|
||||
return await generateSecureRandom(length);
|
||||
}
|
||||
|
||||
export async function hash(
|
||||
password: string,
|
||||
email: string,
|
||||
options?: { usesFallback?: boolean }
|
||||
) {
|
||||
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
|
||||
|
||||
if (options?.usesFallback && Platform.OS !== "ios") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
options?.usesFallback
|
||||
? await Sodium.hashPasswordFallback?.(password, email)
|
||||
: await Sodium.hashPassword(password, email)
|
||||
) as string;
|
||||
}
|
||||
|
||||
export async function generateCryptoKey(password: string, salt?: string) {
|
||||
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export async function generateCryptoKeyFallback(
|
||||
password: string,
|
||||
salt?: string
|
||||
): Promise<SerializedKey> {
|
||||
return Sodium.deriveKeyFallback?.(
|
||||
password,
|
||||
salt as string
|
||||
) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export function getAlgorithm(base64Variant: number) {
|
||||
return `xcha-argon2i13-${base64Variant}`;
|
||||
}
|
||||
|
||||
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
|
||||
const _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decrypt(key, _data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decrypt(fallbackKey, _data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(
|
||||
password: Password,
|
||||
data: Cipher<"base64">[]
|
||||
) {
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
|
||||
if (data.length && !password.salt) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decryptMulti(key, data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt as string
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decryptMulti(fallbackKey, data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg: string) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
return {
|
||||
encryptionAlgorithm: enc,
|
||||
kdfAlgorithm: kdf,
|
||||
compressionAlgorithm: compressionAlg,
|
||||
isCompress: compressed === "1",
|
||||
base64_variant: base64variant
|
||||
};
|
||||
}
|
||||
|
||||
export async function encrypt(password: SerializedKey, plainText: string) {
|
||||
const result = await Sodium.encrypt<"base64">(password, {
|
||||
type: "plain",
|
||||
data: plainText
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(
|
||||
password: SerializedKey,
|
||||
plainText: string[]
|
||||
) {
|
||||
const results = await Sodium.encryptMulti<"base64">(
|
||||
password,
|
||||
plainText.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
);
|
||||
|
||||
return !results
|
||||
? []
|
||||
: results.map((result) => ({
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
}));
|
||||
}
|
||||
@@ -16,26 +16,40 @@ 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 "./logger";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
import { database, getFeature, getFeatureLimit } from "@notesnook/common";
|
||||
import { logger as dbLogger, ICompressor } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { FileStorage } from "../filesystem";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import "./logger";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { Storage } from "./storage";
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
export async function setupDatabase(password) {
|
||||
export async function setupDatabase(password?: string) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key)
|
||||
throw new Error("Database setup failed, could not get database key");
|
||||
if (!key) throw new Error(strings.databaseSetupFailed());
|
||||
|
||||
console.log("Opening database with key:", !!key);
|
||||
// const base = `http://192.168.100.92`;
|
||||
|
||||
// database.host({
|
||||
// API_HOST: `${base}:5264`,
|
||||
// AUTH_HOST: `${base}:8264`,
|
||||
// SSE_HOST: `${base}:7264`,
|
||||
// ISSUES_HOST: `${base}:2624`,
|
||||
// SUBSCRIPTIONS_HOST: `${base}:9264`,
|
||||
// MONOGRAPH_HOST: `${base}:6264`,
|
||||
// NOTESNOOK_HOST: `${base}:8788`
|
||||
// });
|
||||
|
||||
database.host({
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
@@ -43,17 +57,22 @@ export async function setupDatabase(password) {
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
MONOGRAPH_HOST: "https://monogr.ph",
|
||||
NOTESNOOK_HOST: "https://notesnook.com",
|
||||
...(SettingsService.getProperty("serverUrls") || {})
|
||||
});
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: {
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
},
|
||||
eventsource: (Platform.OS === "ios"
|
||||
? EventSource
|
||||
: AndroidEventSource) as any,
|
||||
fs: FileStorage,
|
||||
compressor: async () =>
|
||||
({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
} as ICompressor),
|
||||
batchSize: 100,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
@@ -67,6 +86,10 @@ export async function setupDatabase(password) {
|
||||
tempStore: "memory",
|
||||
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL",
|
||||
password: key
|
||||
},
|
||||
maxNoteVersions: async () => {
|
||||
const limit = await getFeatureLimit(getFeature("maxNoteVersions"));
|
||||
return typeof limit.caption === "number" ? limit.caption : undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -17,8 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initialize } from "@notesnook/core/dist/logger";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import { initialize } from "@notesnook/core";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import { Platform } from "react-native";
|
||||
import { setLogger } from ".";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
@@ -17,11 +17,16 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { DatabaseConnection, Driver, QueryResult } from "kysely";
|
||||
import { CompiledQuery } from "kysely";
|
||||
import type {
|
||||
DatabaseConnection,
|
||||
Driver,
|
||||
QueryResult
|
||||
} from "@streetwriters/kysely";
|
||||
import { CompiledQuery } from "@streetwriters/kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
type Config = { dbName: string; async: boolean; location?: string };
|
||||
|
||||
export class RNSqliteDriver implements Driver {
|
||||
private connection?: DatabaseConnection;
|
||||
@@ -93,7 +98,7 @@ class RNSqliteConnection implements DatabaseConnection {
|
||||
constructor(private readonly db: QuickSQLiteConnection) {}
|
||||
|
||||
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||
throw new Error("wasqlite driver doesn't support streaming");
|
||||
throw new Error(strings.streamingNotSupported());
|
||||
}
|
||||
|
||||
async executeQuery<R>(
|
||||
|
||||
@@ -16,58 +16,52 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { IStorage } from "@notesnook/core";
|
||||
import { MMKVInstance } from "react-native-mmkv-storage";
|
||||
import {
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
deriveCryptoKey,
|
||||
deriveCryptoKeyFallback,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
generateCryptoKey,
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
removeCryptoKey
|
||||
generateCryptoKeyFallback
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
export class KV {
|
||||
/**
|
||||
* @type {typeof MMKV}
|
||||
*/
|
||||
storage = null;
|
||||
constructor(storage) {
|
||||
storage: MMKVInstance;
|
||||
constructor(storage: MMKVInstance) {
|
||||
this.storage = storage;
|
||||
}
|
||||
async read(key) {
|
||||
if (!key) return null;
|
||||
let data = this.storage.getString(key);
|
||||
if (!data) return null;
|
||||
|
||||
async read<T>(key: string, isArray?: boolean) {
|
||||
if (!key) return undefined;
|
||||
const data = this.storage.getString(key);
|
||||
if (!data) return undefined;
|
||||
try {
|
||||
let parse = JSON.parse(data);
|
||||
return parse;
|
||||
return JSON.parse(data) as T;
|
||||
} catch (e) {
|
||||
return data;
|
||||
return data as T;
|
||||
}
|
||||
}
|
||||
|
||||
async write(key, data) {
|
||||
async write<T>(key: string, data: T) {
|
||||
this.storage.setString(
|
||||
key,
|
||||
typeof data === "string" ? data : JSON.stringify(data)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async readMulti(keys) {
|
||||
async readMulti<T>(keys: string[]) {
|
||||
if (keys.length <= 0) {
|
||||
return [];
|
||||
} else {
|
||||
try {
|
||||
let data = await this.storage.getMultipleItemsAsync(
|
||||
const data = await this.storage.getMultipleItemsAsync<any>(
|
||||
keys.slice(),
|
||||
"string"
|
||||
);
|
||||
@@ -79,24 +73,24 @@ export class KV {
|
||||
obj = value;
|
||||
}
|
||||
return [key, obj];
|
||||
});
|
||||
}) as [string, T][];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
return this.storage.removeItem(key);
|
||||
async remove(key: string) {
|
||||
this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async removeMulti(keys) {
|
||||
if (!keys) return true;
|
||||
return this.storage.removeItems(keys);
|
||||
async removeMulti(keys: string[]) {
|
||||
if (!keys) return;
|
||||
this.storage.removeItems(keys);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
return this.storage.clearStore();
|
||||
this.storage.clearStore();
|
||||
}
|
||||
|
||||
async getAllKeys() {
|
||||
@@ -113,54 +107,52 @@ export class KV {
|
||||
return keys;
|
||||
}
|
||||
|
||||
async writeMulti(items) {
|
||||
return this.storage.setMultipleItemsAsync(items, "object");
|
||||
async writeMulti(items: [string, any][]) {
|
||||
await this.storage.setMultipleItemsAsync(items, "object");
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultStorage = new KV(MMKV);
|
||||
|
||||
async function requestPermission() {
|
||||
if (Platform.OS === "ios") return true;
|
||||
return true;
|
||||
}
|
||||
async function checkAndCreateDir(path) {
|
||||
let dir =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.DocumentDir + path
|
||||
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
|
||||
|
||||
try {
|
||||
let exists = await RNFetchBlob.fs.exists(dir);
|
||||
let isDir = await RNFetchBlob.fs.isDir(dir);
|
||||
if (!exists || !isDir) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
} catch (e) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
export default {
|
||||
read: (key) => DefaultStorage.read(key),
|
||||
write: (key, value) => DefaultStorage.write(key, value),
|
||||
readMulti: (keys) => DefaultStorage.readMulti(keys),
|
||||
remove: (key) => DefaultStorage.remove(key),
|
||||
clear: () => DefaultStorage.clear(),
|
||||
getAllKeys: () => DefaultStorage.getAllKeys(),
|
||||
writeMulti: (items) => DefaultStorage.writeMulti(items),
|
||||
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
|
||||
export const Storage: IStorage = {
|
||||
write<T>(key: string, data: T): Promise<void> {
|
||||
return DefaultStorage.write(key, data);
|
||||
},
|
||||
writeMulti<T>(entries: [string, T][]): Promise<void> {
|
||||
return DefaultStorage.writeMulti(entries);
|
||||
},
|
||||
readMulti<T>(keys: string[]): Promise<[string, T][]> {
|
||||
return DefaultStorage.readMulti(keys);
|
||||
},
|
||||
read<T>(key: string, isArray?: boolean): Promise<T | undefined> {
|
||||
return DefaultStorage.read(key, isArray);
|
||||
},
|
||||
remove(key: string): Promise<void> {
|
||||
return DefaultStorage.remove(key);
|
||||
},
|
||||
removeMulti(keys: string[]): Promise<void> {
|
||||
return DefaultStorage.removeMulti(keys);
|
||||
},
|
||||
clear(): Promise<void> {
|
||||
return DefaultStorage.clear();
|
||||
},
|
||||
generateCryptoKeyPair() {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
decryptAsymmetric() {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
getAllKeys(): Promise<string[]> {
|
||||
return DefaultStorage.getAllKeys();
|
||||
},
|
||||
hash,
|
||||
getCryptoKey,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
getRandomBytes,
|
||||
checkAndCreateDir,
|
||||
requestPermission,
|
||||
deriveCryptoKey,
|
||||
getCryptoKey,
|
||||
removeCryptoKey,
|
||||
hash,
|
||||
generateCryptoKey,
|
||||
encryptMulti
|
||||
generateCryptoKeyFallback,
|
||||
deriveCryptoKeyFallback
|
||||
};
|
||||
@@ -27,13 +27,9 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
*/
|
||||
export async function compressToBase64(path, type) {
|
||||
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,
|
||||
@@ -55,10 +51,12 @@ export async function compressToBase64(path, type) {
|
||||
: response.uri,
|
||||
"base64"
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
RNFetchBlob.fs
|
||||
.unlink(response.uri.replace("file://", ""))
|
||||
.catch(console.log);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
resolve(base64);
|
||||
});
|
||||
@@ -80,6 +78,8 @@ export async function compressToFile(path, type) {
|
||||
onlyScaleDown: true
|
||||
}
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
return response.uri;
|
||||
}
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { getFileNameWithExtension } from "@notesnook/core/dist/utils/filename";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
Fail: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Download all user's attachments
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAllAttachments() {
|
||||
const attachments = await db.attachments.all.ids();
|
||||
return downloadAttachments(attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param {string[]} attachments
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(
|
||||
attachments,
|
||||
onProgress,
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
await createCacheDir();
|
||||
if (!attachments || !attachments.length) return;
|
||||
const result = new Map();
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
// Ask the user to select a directory to store the file
|
||||
let file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
|
||||
const zipOutputFile =
|
||||
Platform.OS === "ios"
|
||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
||||
: `${cacheDir}/notesnook-attachments.zip`;
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
let attachment = await db.attachments.attachment(attachments[i]);
|
||||
const hash = attachment.hash;
|
||||
try {
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
onProgress?.(
|
||||
i + 1 / attachments.length,
|
||||
`Downloading attachments (${i + 1}/${
|
||||
attachments.length
|
||||
})... Please wait`
|
||||
);
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
});
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Success,
|
||||
attachment: attachment
|
||||
});
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
ToastManager.error(e, "Error downloading attachment");
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (result?.size) {
|
||||
let sub;
|
||||
try {
|
||||
onProgress?.(0, `Zipping... Please wait`);
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
onProgress(
|
||||
progress,
|
||||
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
|
||||
);
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
onProgress(1, `Saving zip file... Please wait`);
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
|
||||
onProgress?.(1, `Done`);
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
hash,
|
||||
global = true,
|
||||
options = {
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
let attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
DatabaseLogger.log("Attachment not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let folder = {};
|
||||
if (!options.cache) {
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder.uri = await Storage.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.base64 || options.text) {
|
||||
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
|
||||
return await db.attachments.read(
|
||||
attachment.hash,
|
||||
options.base64 ? "base64" : "text"
|
||||
);
|
||||
}
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
attachment.filename,
|
||||
attachment.mimeType
|
||||
);
|
||||
|
||||
let key = await db.attachments.decryptKey(attachment.key);
|
||||
|
||||
let info = {
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
length: attachment.size,
|
||||
alg: attachment.alg,
|
||||
hash: attachment.hash,
|
||||
hashType: attachment.hashType,
|
||||
mime: attachment.mimeType,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
options.cache ? "cache" : "file"
|
||||
);
|
||||
|
||||
if (!options.silent) {
|
||||
ToastManager.show({
|
||||
heading: "Download successful",
|
||||
message: filename + " downloaded",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
if (!options.silent) {
|
||||
presentSheet({
|
||||
title: "File downloaded",
|
||||
paragraph: `${filename} saved to ${
|
||||
Platform.OS === "android"
|
||||
? "selected path"
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
icon: "download",
|
||||
context: global ? null : attachment.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(console.log);
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||
.catch(console.log);
|
||||
}
|
||||
DatabaseLogger.error(e);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
353
apps/mobile/app/common/filesystem/download-attachment.tsx
Normal file
353
apps/mobile/app/common/filesystem/download-attachment.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
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 Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { getFileNameWithExtension } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import filesystem from ".";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
Fail: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Download all user's attachments
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAllAttachments() {
|
||||
const attachments = await db.attachments.all.ids();
|
||||
return downloadAttachments(attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param {string[]} attachmentIds
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(attachmentIds: string[]) {
|
||||
await createCacheDir();
|
||||
if (!attachmentIds || !attachmentIds.length) return;
|
||||
const groupId = `download-all-${Date.now()}`;
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
// Ask the user to select a directory to store the file
|
||||
const file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
|
||||
const zipOutputFile =
|
||||
Platform.OS === "ios"
|
||||
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
|
||||
: `${cacheDir}/notesnook-attachments.zip`;
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder)) {
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
const isCancelled = () => {
|
||||
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
current: 0,
|
||||
total: 0,
|
||||
success: false,
|
||||
message: strings.network.dowloadCancelled(),
|
||||
canceled: true
|
||||
});
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < attachmentIds.length; i++) {
|
||||
if (isCancelled()) return;
|
||||
const attachment = await db.attachments.attachment(attachmentIds[i]);
|
||||
if (!attachment) continue;
|
||||
|
||||
const hash = attachment.hash;
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: i + 1,
|
||||
total: attachmentIds.length,
|
||||
filename: attachment.hash
|
||||
});
|
||||
// Download to cache
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
});
|
||||
|
||||
if (isCancelled()) return;
|
||||
|
||||
if (!uri) throw new Error(strings.failedToDownloadFile());
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: 0,
|
||||
total: 0,
|
||||
success: true
|
||||
});
|
||||
|
||||
if (isCancelled()) return;
|
||||
|
||||
let sub;
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 1,
|
||||
message: strings.savingZipFile(),
|
||||
groupId
|
||||
});
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
current: progress,
|
||||
total: 1,
|
||||
message: strings.savingZipFile((progress * 100).toFixed(1))
|
||||
});
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 0,
|
||||
message: undefined,
|
||||
success: true,
|
||||
groupId
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 0,
|
||||
message: undefined,
|
||||
success: false,
|
||||
groupId
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e as Error, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
hashOrId: string,
|
||||
global = true,
|
||||
options?: {
|
||||
silent?: boolean;
|
||||
cache?: boolean;
|
||||
throwError?: boolean;
|
||||
groupId?: string;
|
||||
base64?: boolean;
|
||||
text?: boolean;
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
const attachment = await db.attachments.attachment(hashOrId);
|
||||
if (!attachment) {
|
||||
DatabaseLogger.log("Attachment not found", {
|
||||
hash: hashOrId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let folder: {
|
||||
uri: string;
|
||||
} | null = null;
|
||||
if (!options?.cache) {
|
||||
if (Platform.OS === "android") {
|
||||
folder = await ScopedStorage.openDocumentTree();
|
||||
if (!folder) return;
|
||||
} else {
|
||||
folder = {
|
||||
uri: await filesystem.checkAndCreateDir("/downloads/")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
current: 0,
|
||||
total: 1,
|
||||
filename: attachment.filename
|
||||
});
|
||||
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options?.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
current: 1,
|
||||
total: 1,
|
||||
filename: attachment.filename,
|
||||
success: true
|
||||
});
|
||||
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options?.base64 || options?.text) {
|
||||
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
|
||||
return await db.attachments.read(
|
||||
attachment.hash,
|
||||
options.base64 ? "base64" : "text"
|
||||
);
|
||||
}
|
||||
|
||||
const filename = await getFileNameWithExtension(
|
||||
attachment.filename,
|
||||
attachment.mimeType
|
||||
);
|
||||
|
||||
const key = await db.attachments.decryptKey(attachment.key);
|
||||
|
||||
if (!key) return;
|
||||
|
||||
const info = {
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
length: attachment.size,
|
||||
alg: attachment.alg,
|
||||
hash: attachment.hash,
|
||||
hashType: attachment.hashType,
|
||||
mime: attachment.mimeType,
|
||||
fileName: options?.cache ? undefined : filename,
|
||||
uri: options?.cache ? undefined : folder?.uri,
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
key,
|
||||
info,
|
||||
options?.cache ? "cache" : "file"
|
||||
);
|
||||
|
||||
if (!options?.silent) {
|
||||
ToastManager.show({
|
||||
heading: strings.network.downloadSuccess(),
|
||||
message: strings.network.fileDownloaded(filename),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios" && !options?.cache) {
|
||||
fileUri = folder?.uri + `/${filename}`;
|
||||
}
|
||||
if (!options?.silent) {
|
||||
presentSheet({
|
||||
title: strings.network.fileDownloaded(),
|
||||
paragraph: strings.fileSaved(filename, Platform.OS),
|
||||
icon: "download",
|
||||
context: global ? "global" : attachment.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
});
|
||||
}
|
||||
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
current: 0,
|
||||
total: 0,
|
||||
filename: attachment.filename,
|
||||
success: false
|
||||
});
|
||||
DatabaseLogger.error(e);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
if (options?.throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ 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 { RequestOptions } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -25,7 +27,13 @@ import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
||||
|
||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
export async function downloadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
@@ -35,24 +43,29 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
let { url, headers, chunkSize } = requestOptions;
|
||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
let originalFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const { url, headers, chunkSize } = requestOptions;
|
||||
const tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
const originalFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
DatabaseLogger.log(`File Exists already: ${filename}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
const error = `${strings.fileVerificationFailed()} (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (size === 0) {
|
||||
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
const error = `${strings.fileLengthError()} (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
@@ -61,38 +74,44 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
|
||||
if (attachment && attachment.size !== decryptedLength) {
|
||||
const error = `File length mismatch. Expected ${attachment.size} but got ${decryptedLength} bytes. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
const error = `${strings.fileLengthMismatch(
|
||||
attachment.size,
|
||||
decryptedLength
|
||||
)} (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
const resolveUrlResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (!resolveUrlResponse.ok) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
|
||||
`Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error(
|
||||
`${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
);
|
||||
throw new Error(`${res.status}: Unable to resolve download url`);
|
||||
}
|
||||
|
||||
const downloadUrl = await res.text();
|
||||
const downloadUrl = await resolveUrlResponse.text();
|
||||
|
||||
if (!downloadUrl) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error("Unable to resolve download url");
|
||||
throw new Error(strings.failedToResolvedDownloadUrl());
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
let request = RNFetchBlob.config({
|
||||
const request = RNFetchBlob.config({
|
||||
path: tempFilePath,
|
||||
IOSBackgroundTask: true
|
||||
IOSBackgroundTask: true,
|
||||
overwrite: true
|
||||
})
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.fetch("GET", downloadUrl)
|
||||
.progress(async (recieved, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
@@ -101,15 +120,16 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
cancelToken.cancel = async (reason) => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
|
||||
};
|
||||
|
||||
let response = await request;
|
||||
console.log(response.info().headers);
|
||||
const response = await request;
|
||||
|
||||
const contentType =
|
||||
response.info().headers?.["content-type"] ||
|
||||
@@ -120,9 +140,18 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
let status = response.info().status;
|
||||
const status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
||||
|
||||
if (await exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
if (!(await exists(filename))) {
|
||||
throw new Error("File size mismatch");
|
||||
@@ -130,33 +159,36 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
ToastManager.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
if (
|
||||
(e as Error).message !== "canceled" &&
|
||||
!(e as Error).message.includes("NoSuchKey")
|
||||
) {
|
||||
const toast = {
|
||||
heading: strings.downloadError((e as Error).message),
|
||||
message: (e as Error).message,
|
||||
type: "error" as const,
|
||||
context: "global"
|
||||
});
|
||||
ToastManager.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
ToastManager.show(toast);
|
||||
toast.context = "local";
|
||||
ToastManager.show(toast);
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.error(e, "Download failed: ", {
|
||||
url
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash) {
|
||||
export async function checkAttachment(hash: string) {
|
||||
const internetState = await NetInfo.fetch();
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
@@ -166,7 +198,7 @@ export async function checkAttachment(hash) {
|
||||
|
||||
try {
|
||||
const size = await getUploadedFileSize(hash);
|
||||
console.log("File Size", size);
|
||||
|
||||
if (size === -1) return { success: true };
|
||||
|
||||
if (size === 0)
|
||||
@@ -174,7 +206,7 @@ export async function checkAttachment(hash) {
|
||||
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
} catch (e) {
|
||||
return { failed: e?.message };
|
||||
return { failed: (e as Error)?.message };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
@@ -17,24 +17,41 @@ 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 { IFileStorage } from "@notesnook/core";
|
||||
import { checkAttachment, downloadFile } from "./download";
|
||||
import {
|
||||
bulkExists,
|
||||
clearCache,
|
||||
clearFileStorage,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
deleteFile,
|
||||
exists,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
getCacheSize,
|
||||
hashBase64,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import {
|
||||
cancelable,
|
||||
checkAndCreateDir,
|
||||
getUploadedFileSize,
|
||||
requestPermission
|
||||
} from "./utils";
|
||||
|
||||
export default {
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable, getUploadedFileSize } from "./utils";
|
||||
getCacheSize,
|
||||
requestPermission,
|
||||
checkAndCreateDir,
|
||||
getUploadedFileSize
|
||||
};
|
||||
|
||||
export default {
|
||||
export const FileStorage: IFileStorage = {
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
@@ -44,10 +61,5 @@ export default {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
bulkExists
|
||||
};
|
||||
@@ -1,257 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
DatabaseLogger.log("Read encrypted file...");
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (!(await exists(filename))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
...cipherData,
|
||||
hash: filename,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
},
|
||||
cipherData.outputType === "base64" ? "base64" : "text"
|
||||
);
|
||||
|
||||
DatabaseLogger.log("File decrypted...");
|
||||
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashBase64(data) {
|
||||
const hash = await Sodium.hashFile({
|
||||
type: "base64",
|
||||
data,
|
||||
uri: ""
|
||||
});
|
||||
return {
|
||||
hash: hash,
|
||||
type: "xxh64"
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64(data, key) {
|
||||
await createCacheDir();
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
|
||||
type: "url"
|
||||
});
|
||||
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"
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteFile(filename, data) {
|
||||
await createCacheDir();
|
||||
let delFilePath = cacheDir + `/${filename}`;
|
||||
if (!data) {
|
||||
if (!filename) return;
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
return true;
|
||||
}
|
||||
|
||||
let { url, headers } = data;
|
||||
try {
|
||||
let response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
let status = response.info().status;
|
||||
let ok = status >= 200 && status < 300;
|
||||
if (ok) {
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
}
|
||||
return ok;
|
||||
} catch (e) {
|
||||
console.log("delete file: ", e, url, headers);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
for (let file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
|
||||
}
|
||||
for (let file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("clearFileStorage", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCacheDir() {
|
||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||
DatabaseLogger.log("Cache directory created");
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateFilesFromCache() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const migratedFilesPath = cacheDir + "/.migrated_1";
|
||||
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
|
||||
if (migrated) {
|
||||
console.log("Files migrated already");
|
||||
return;
|
||||
}
|
||||
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
console.log("Files to migrate:", files.join(","));
|
||||
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (let file of oldCache) {
|
||||
if (file.startsWith("org.") || file.startsWith("com.")) continue;
|
||||
RNFetchBlob.fs
|
||||
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
|
||||
.catch(console.log);
|
||||
console.log("Moved", file);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
|
||||
} catch (e) {
|
||||
console.log("migrateFilesFromCache", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearCache() {
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
|
||||
await createCacheDir();
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
// Check if file is present in app group path.
|
||||
let existsInAppGroup = false;
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
|
||||
}
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.size + totalAbytes;
|
||||
|
||||
const stat = await RNFetchBlob.fs.stat(
|
||||
existsInAppGroup ? appGroupPath : path
|
||||
);
|
||||
|
||||
if (stat.size !== expectedFileSize) {
|
||||
DatabaseLogger.log(
|
||||
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
|
||||
);
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(console.log);
|
||||
return false;
|
||||
}
|
||||
|
||||
exists = true;
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files) {
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter((file) => !appGroupFiles.includes(file));
|
||||
}
|
||||
|
||||
return missingFiles;
|
||||
}
|
||||
|
||||
export async function getCacheSize() {
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
console.log("Total files", stat.length);
|
||||
stat.forEach((s) => {
|
||||
total += parseInt(s.size);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
333
apps/mobile/app/common/filesystem/io.ts
Normal file
333
apps/mobile/app/common/filesystem/io.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
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 Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
FileEncryptionMetadataWithHash,
|
||||
FileEncryptionMetadataWithOutputType,
|
||||
Output,
|
||||
RequestOptions
|
||||
} from "@notesnook/core";
|
||||
import { DataFormat, SerializedKey } from "@notesnook/crypto";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted<TOutputFormat extends DataFormat>(
|
||||
filename: string,
|
||||
key: SerializedKey,
|
||||
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
|
||||
) {
|
||||
await migrateFilesFromCache();
|
||||
DatabaseLogger.log("Read encrypted file...");
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (!(await exists(filename))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
...cipherData,
|
||||
hash: filename,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
},
|
||||
cipherData.outputType === "base64" ? "base64" : "text"
|
||||
);
|
||||
|
||||
DatabaseLogger.log("File decrypted...");
|
||||
|
||||
return output as Output<TOutputFormat>;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashBase64(data: string) {
|
||||
const hash = await Sodium.hashFile({
|
||||
type: "base64",
|
||||
data,
|
||||
uri: ""
|
||||
});
|
||||
return {
|
||||
hash: hash,
|
||||
type: "xxh64"
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64(
|
||||
data: string,
|
||||
encryptionKey: SerializedKey,
|
||||
mimeType: string
|
||||
): Promise<FileEncryptionMetadataWithHash> {
|
||||
await createCacheDir();
|
||||
const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
const output = await Sodium.encryptFile(encryptionKey, {
|
||||
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
|
||||
type: "url"
|
||||
});
|
||||
|
||||
RNFetchBlob.fs.unlink(filepath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
return {
|
||||
...output,
|
||||
alg: "xcha-stream"
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteFile(
|
||||
filename: string,
|
||||
requestOptions?: RequestOptions
|
||||
): Promise<boolean> {
|
||||
await createCacheDir();
|
||||
const localFilePath = cacheDir + `/${filename}`;
|
||||
if (!requestOptions) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const { url, headers } = requestOptions;
|
||||
|
||||
try {
|
||||
const response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
const status = response.info().status;
|
||||
const ok = status >= 200 && status < 300;
|
||||
if (ok) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
return ok;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e, "Delete file", {
|
||||
url: url
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
for (const file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
for (const file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e, "clearFileStorage");
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCacheDir() {
|
||||
try {
|
||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||
DatabaseLogger.log("Cache directory created");
|
||||
}
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateFilesFromCache() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const migratedFilesPath = cacheDir + "/.migrated_1";
|
||||
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
|
||||
if (migrated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (const file of oldCache) {
|
||||
if (file.startsWith("org.") || file.startsWith("com.")) continue;
|
||||
RNFetchBlob.fs
|
||||
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e, "migrateFilesFromCache");
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearCache() {
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await createCacheDir();
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path: string) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name: string) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
await createCacheDir();
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache") || file.startsWith("NN_")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCachePathForFile(filename: string) {
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
const exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
// Check if file is present in app group path.
|
||||
let existsInAppGroup = false;
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
|
||||
}
|
||||
|
||||
return existsInAppGroup ? appGroupPath : path;
|
||||
}
|
||||
|
||||
export async function exists(filename: string) {
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
// Check if file is present in app group path.
|
||||
let existsInAppGroup = false;
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
|
||||
}
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.size + totalAbytes;
|
||||
|
||||
const stat = await RNFetchBlob.fs.stat(
|
||||
existsInAppGroup ? appGroupPath : path
|
||||
);
|
||||
|
||||
if (stat.size !== expectedFileSize) {
|
||||
DatabaseLogger.log(
|
||||
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
|
||||
);
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
exists = true;
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files: string[]) {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter(
|
||||
(file) => !appGroupFiles.includes(file)
|
||||
);
|
||||
}
|
||||
return missingFiles;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCacheSize() {
|
||||
await createCacheDir();
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
|
||||
stat.forEach((file) => {
|
||||
total += parseInt(file.size as unknown as string);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RequestOptions } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -24,71 +25,69 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir } from "./io";
|
||||
import { cacheDir, getUploadedFileSize } from "./utils";
|
||||
import {
|
||||
cacheDir,
|
||||
checkUpload,
|
||||
FileSizeResult,
|
||||
getUploadedFileSize
|
||||
} from "./utils";
|
||||
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
export async function uploadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
if (!requestOptions) return false;
|
||||
let { url, headers } = requestOptions;
|
||||
const { url, headers } = requestOptions;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
const uploadedFileSize = await getUploadedFileSize(filename);
|
||||
|
||||
if (uploadedFileSize === -1) {
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (uploadedFileSize !== 0) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
|
||||
const uploadUrl = res.ok ? await res.text() : await res.json();
|
||||
|
||||
if (typeof uploadUrl !== "string") {
|
||||
throw new Error(
|
||||
uploadUrl.error || "Unable to resolve attachment upload url."
|
||||
);
|
||||
}
|
||||
|
||||
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
let filePath = `${cacheDir}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(filePath);
|
||||
// Check for file in appGroupPath if it doesn't exist in cacheDir
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
uploadFilePath = appGroupPath;
|
||||
exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
filePath = appGroupPath;
|
||||
exists = await RNFetchBlob.fs.exists(filePath);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
throw new Error(
|
||||
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
|
||||
`Trying to upload file at path ${filePath} that doest not exist.`
|
||||
);
|
||||
}
|
||||
|
||||
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
|
||||
|
||||
const remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === FileSizeResult.Error) return false;
|
||||
|
||||
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
|
||||
let request = RNFetchBlob.config({
|
||||
const uploadRequest = RNFetchBlob.config({
|
||||
//@ts-ignore
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
})
|
||||
.fetch(
|
||||
"PUT",
|
||||
uploadUrl,
|
||||
url,
|
||||
{
|
||||
...headers,
|
||||
"content-type": ""
|
||||
},
|
||||
RNFetchBlob.wrap(uploadFilePath)
|
||||
RNFetchBlob.wrap(filePath)
|
||||
)
|
||||
.uploadProgress((sent, total) => {
|
||||
useAttachmentStore
|
||||
@@ -99,35 +98,33 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
cancelToken.cancel = async () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
uploadRequest.cancel();
|
||||
};
|
||||
let response = await request;
|
||||
|
||||
let status = response.info().status;
|
||||
let text = await response.text();
|
||||
let result = status >= 200 && status < 300 && text.length === 0;
|
||||
const uploadResponse = await uploadRequest;
|
||||
const status = uploadResponse.info().status;
|
||||
const uploaded = status >= 200 && status < 300;
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
if (result) {
|
||||
DatabaseLogger.info(
|
||||
`File upload status: ${filename}, ${status}, ${text}`
|
||||
);
|
||||
let attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
} else {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
|
||||
|
||||
if (!uploaded) {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(filePath);
|
||||
throw new Error(
|
||||
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
|
||||
`${status}, name: ${fileInfo.filename}, length: ${
|
||||
fileInfo.size
|
||||
}, info: ${JSON.stringify(response.info())}`
|
||||
}, info: ${JSON.stringify(uploadResponse.info())}`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
|
||||
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
|
||||
return uploaded;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
ToastManager.error(e, "File upload failed");
|
||||
ToastManager.error(e as Error, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
@@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { hosts, RequestOptions } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import hosts from "@notesnook/core/dist/utils/constants";
|
||||
|
||||
export const ABYTES = 17;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
@@ -31,18 +31,13 @@ export const cacheDir =
|
||||
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
|
||||
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
|
||||
|
||||
export function getRandomId(prefix) {
|
||||
export function getRandomId(prefix: string) {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
export function parseS3Error(data?: string) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
|
||||
const error = {
|
||||
@@ -70,11 +65,19 @@ export function parseS3Error(data) {
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelable(operation) {
|
||||
export function cancelable(
|
||||
operation: (
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) => Promise<boolean>
|
||||
) {
|
||||
const cancelToken = {
|
||||
cancel: () => {}
|
||||
cancel: async (reason?: string) => {}
|
||||
};
|
||||
return (filename, requestOptions) => {
|
||||
return (filename: string, requestOptions: RequestOptions) => {
|
||||
return {
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
cancel: async () => {
|
||||
@@ -84,29 +87,35 @@ export function cancelable(operation) {
|
||||
};
|
||||
}
|
||||
|
||||
export function copyFileAsync(source, dest) {
|
||||
export function copyFileAsync(source: string, dest: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
//@ts-ignore
|
||||
ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function releasePermissions(path) {
|
||||
export async function releasePermissions(path: string) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (let uri of uris) {
|
||||
for (const uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
export const FileSizeResult = {
|
||||
Empty: 0,
|
||||
Error: -1
|
||||
};
|
||||
|
||||
export async function getUploadedFileSize(hash: string, retry = 0) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
@@ -114,12 +123,69 @@ export async function getUploadedFileSize(hash) {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (
|
||||
!attachmentInfo.ok ||
|
||||
attachmentInfo.headers?.get("content-length") === null
|
||||
) {
|
||||
if (retry < 3) {
|
||||
DatabaseLogger.log(`Retrying file size check: ${hash}, ${retry}`);
|
||||
return getUploadedFileSize(hash, retry + 1);
|
||||
}
|
||||
throw new Error(
|
||||
`File size check failed: ${hash}, ${
|
||||
attachmentInfo.status
|
||||
}, ${attachmentInfo.headers?.get("content-length")}`
|
||||
);
|
||||
}
|
||||
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
attachmentInfo.headers?.get("content-length") as string
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return -1;
|
||||
return FileSizeResult.Error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUpload(
|
||||
filename: string,
|
||||
chunkSize: number,
|
||||
expectedSize: number
|
||||
) {
|
||||
const size = await getUploadedFileSize(filename);
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
const error =
|
||||
size === 0
|
||||
? `File size is 0.`
|
||||
: size === -1
|
||||
? `File verification check failed.`
|
||||
: expectedSize !== decryptedLength
|
||||
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
|
||||
: undefined;
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
export async function requestPermission() {
|
||||
if (Platform.OS === "ios") return true;
|
||||
return true;
|
||||
}
|
||||
export async function checkAndCreateDir(path: string) {
|
||||
const dir =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.DocumentDir + path
|
||||
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
|
||||
|
||||
try {
|
||||
const exists = await RNFetchBlob.fs.exists(dir);
|
||||
const isDir = await RNFetchBlob.fs.isDir(dir);
|
||||
if (!exists || !isDir) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
} catch (e) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
@@ -17,9 +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/>.
|
||||
*/
|
||||
|
||||
function info(context: string, ...logs: unknown[]) {
|
||||
console.log(`${new Date().toLocaleDateString()}::info::${context}:`, ...logs);
|
||||
}
|
||||
function info(context: string, ...logs: unknown[]) {}
|
||||
|
||||
function error(context: string, ...logs: unknown[]) {
|
||||
console.log(
|
||||
|
||||
@@ -24,10 +24,18 @@ import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const Announcement = ({ color }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
const [announcements, remove] = useMessageStore((state) => [
|
||||
state.announcements,
|
||||
state.remove
|
||||
]);
|
||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
@@ -36,9 +44,8 @@ export const Announcement = ({ color }) => {
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -54,9 +61,41 @@ export const Announcement = ({ color }) => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 12
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.secondary.heading} size={AppFontSize.xxs}>
|
||||
{strings.announcement()}
|
||||
</Heading>
|
||||
|
||||
<Button
|
||||
type="plain"
|
||||
icon="close"
|
||||
height={null}
|
||||
onPress={() => {
|
||||
remove(announcement.id);
|
||||
}}
|
||||
iconSize={20}
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{announcement?.body
|
||||
.filter((item) => allowedOnPlatform(item.platforms))
|
||||
.map((item, index) =>
|
||||
|
||||
@@ -20,12 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Body = ({ text, style = {} }) => {
|
||||
return (
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
...getStyle(style)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -23,12 +23,12 @@ import { Linking, View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eCloseAnnouncementDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { PricingPlans } from "../premium/pricing-plans";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { allowedOnPlatform, getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -41,27 +41,18 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
await sleep(500);
|
||||
}
|
||||
if (item.type === "link") {
|
||||
Linking.openURL(item.data).catch(console.log);
|
||||
} else if (item.type === "promo") {
|
||||
presentSheet({
|
||||
component: (
|
||||
<PricingPlans
|
||||
marginTop={1}
|
||||
promo={{
|
||||
promoCode: item.data,
|
||||
text: item.title
|
||||
}}
|
||||
/>
|
||||
)
|
||||
Linking.openURL(item.data).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
...getStyle(style),
|
||||
flexDirection: inline ? "row" : "column"
|
||||
flexDirection: inline ? "row" : "column",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="premium_cta" />
|
||||
@@ -73,7 +64,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.sm}
|
||||
fontSize={AppFontSize.sm}
|
||||
type="transparent"
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
@@ -81,7 +72,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
onPress={() => onPress(item)}
|
||||
bold
|
||||
style={{
|
||||
height: 30,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
@@ -93,15 +84,13 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.sm}
|
||||
fontSize={AppFontSize.sm}
|
||||
type="plain"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
height={30}
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
marginLeft: 12
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
@@ -116,7 +105,6 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.md}
|
||||
buttonType={{
|
||||
color: color ? color : colors.primary.accent,
|
||||
text: color
|
||||
@@ -126,10 +114,8 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
opacity: 1
|
||||
}}
|
||||
onPress={() => onPress(item)}
|
||||
width={250}
|
||||
style={{
|
||||
marginBottom: 5,
|
||||
borderRadius: 100
|
||||
width: "100%"
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -139,14 +125,14 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
<Button
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.xs}
|
||||
fontSize={AppFontSize.xs}
|
||||
type="plain"
|
||||
onPress={() => onPress(item)}
|
||||
width={null}
|
||||
height={30}
|
||||
style={{
|
||||
minWidth: "50%",
|
||||
marginTop: 5
|
||||
width: "100%"
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
|
||||
@@ -18,19 +18,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Description = ({ text, style = {}, inline }) => {
|
||||
return (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginHorizontal: 12,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
...getStyle(style),
|
||||
textAlign: inline ? "left" : style?.textAlign
|
||||
}}
|
||||
size={inline ? SIZE.sm : SIZE.md}
|
||||
size={AppFontSize.sm}
|
||||
>
|
||||
{text}
|
||||
</Paragraph>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { List } from "./list";
|
||||
import { Photo } from "./photo";
|
||||
import { SubHeading } from "./subheading";
|
||||
import { Title } from "./title";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export function allowedOnPlatform(platforms) {
|
||||
if (!platforms) return true;
|
||||
@@ -53,7 +54,7 @@ const Features = () => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
import { useCallback } from "react";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
/**
|
||||
* Test announcement
|
||||
@@ -138,9 +139,9 @@ export const AnnouncementDialog = () => {
|
||||
maxHeight: DDS.isTab ? "90%" : "100%",
|
||||
borderRadius: DDS.isTab ? 10 : 0,
|
||||
overflow: "hidden",
|
||||
marginBottom: DDS.isTab ? 20 : 0,
|
||||
borderTopRightRadius: 10,
|
||||
borderTopLeftRadius: 10
|
||||
paddingVertical: DefaultAppStyles.GAP,
|
||||
borderTopRightRadius: 15,
|
||||
borderTopLeftRadius: 15
|
||||
}}
|
||||
>
|
||||
<FlatList
|
||||
|
||||
@@ -22,12 +22,13 @@ import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const List = ({ items, listType, style = {} }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingLeft: listType === "ordered" ? 25 : 25,
|
||||
...getStyle(style)
|
||||
}}
|
||||
@@ -36,7 +37,7 @@ export const List = ({ items, listType, style = {} }) => {
|
||||
<View
|
||||
key={item.text}
|
||||
style={{
|
||||
paddingVertical: 6,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -18,17 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const SubHeading = ({ text, style = {} }) => {
|
||||
return (
|
||||
<Heading
|
||||
size={SIZE.md + 2}
|
||||
size={AppFontSize.md + 2}
|
||||
style={{
|
||||
marginHorizontal: 12,
|
||||
marginTop: 12,
|
||||
marginHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
...getStyle(style)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -20,69 +20,41 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { getStyle } from "./functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Title = ({ text, style = {}, inline }) => {
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
let announcement = announcements.length > 0 ? announcements[0] : null;
|
||||
const remove = useMessageStore((state) => state.remove);
|
||||
|
||||
return inline ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: inline ? 5 : 0
|
||||
marginBottom: inline ? DefaultAppStyles.GAP_VERTICAL_SMALL : 0
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
marginHorizontal: 12,
|
||||
marginTop: 12,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
...getStyle(style),
|
||||
textAlign: inline ? "left" : style?.textAlign,
|
||||
flexShrink: 1
|
||||
}}
|
||||
numberOfLines={1}
|
||||
size={inline ? SIZE.md : SIZE.xl}
|
||||
size={inline ? AppFontSize.md : AppFontSize.xl}
|
||||
>
|
||||
{inline ? text?.toUpperCase() : text}
|
||||
</Heading>
|
||||
|
||||
<Button
|
||||
type="plain"
|
||||
icon="close"
|
||||
height={null}
|
||||
onPress={() => {
|
||||
remove(announcement.id);
|
||||
}}
|
||||
hitSlop={{
|
||||
left: 15,
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
right: 0
|
||||
}}
|
||||
iconSize={24}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
marginRight: 12,
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Heading
|
||||
style={{
|
||||
marginHorizontal: 12,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
...getStyle(style),
|
||||
marginTop: style?.marginTop || 12
|
||||
marginTop: style?.marginTop || DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -44,7 +44,6 @@ import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
@@ -53,6 +52,10 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
const getUser = () => {
|
||||
const user = MMKV.getString("user");
|
||||
@@ -81,7 +84,7 @@ const verifyUserPassword = async (password: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const AppLockedOverlay = () => {
|
||||
const AppLocked = () => {
|
||||
const initialLaunchBiometricRequest = useRef(true);
|
||||
const { colors } = useThemeColors();
|
||||
const user = getUser();
|
||||
@@ -108,17 +111,25 @@ const AppLockedOverlay = () => {
|
||||
);
|
||||
|
||||
const onUnlockAppRequested = useCallback(async () => {
|
||||
editorController.current?.commands.blur(useTabStore.getState().currentTab);
|
||||
editorController.current?.commands.blurPassInput();
|
||||
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
!(await BiometricService.isBiometryAvailable())
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
biometricUnlockAwaitingUserInput.current = true;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const activityName = await NotesnookModule.getActivityName();
|
||||
if (activityName !== "MainActivity") return;
|
||||
if (
|
||||
activityName !== "MainActivity" &&
|
||||
activityName !== "NotePreviewConfigureActivity"
|
||||
)
|
||||
return;
|
||||
}
|
||||
useSettingStore.getState().setRequestBiometrics(true);
|
||||
|
||||
@@ -157,7 +168,7 @@ const AppLockedOverlay = () => {
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
|
||||
heading: strings.invalid(keyboardType),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -176,10 +187,15 @@ const AppLockedOverlay = () => {
|
||||
(prevState === "background" || initialLaunchBiometricRequest.current)
|
||||
) {
|
||||
if (SettingsService.shouldLockAppOnEnterForeground()) {
|
||||
if (useSettingStore.getState().appDidEnterBackgroundForAction) {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseLogger.info("Locking app on entering foreground");
|
||||
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
biometricUnlockAwaitingUserInput.current ||
|
||||
@@ -250,7 +266,7 @@ const AppLockedOverlay = () => {
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Unlock your notes
|
||||
{strings.unlockNotes()}
|
||||
</Heading>
|
||||
|
||||
<Paragraph
|
||||
@@ -260,7 +276,7 @@ const AppLockedOverlay = () => {
|
||||
maxWidth: "90%"
|
||||
}}
|
||||
>
|
||||
{"Please verify it's you"}
|
||||
{strings.verifyItsYou()}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<View
|
||||
@@ -278,13 +294,23 @@ const AppLockedOverlay = () => {
|
||||
keyboardType={
|
||||
appLockHasPasswordSecurity ? keyboardType : "default"
|
||||
}
|
||||
placeholder={`Enter ${
|
||||
onLayout={async () => {
|
||||
if (
|
||||
!biometricsAuthEnabled ||
|
||||
!(await BiometricService.isBiometryAvailable())
|
||||
) {
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 32);
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
appLockHasPasswordSecurity
|
||||
? `app lock ${
|
||||
keyboardType === "numeric" ? "pin" : "password"
|
||||
}`
|
||||
: "account password"
|
||||
}`}
|
||||
? keyboardType === "numeric"
|
||||
? strings.enterApplockPassword()
|
||||
: strings.enterApplockPin()
|
||||
: strings.enterAccountPassword()
|
||||
}
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
@@ -301,7 +327,7 @@ const AppLockedOverlay = () => {
|
||||
{user || appLockHasPasswordSecurity ? (
|
||||
<>
|
||||
<Button
|
||||
title="Continue"
|
||||
title={strings.continue()}
|
||||
type="accent"
|
||||
onPress={onSubmit}
|
||||
width={250}
|
||||
@@ -310,14 +336,13 @@ const AppLockedOverlay = () => {
|
||||
borderRadius: 150,
|
||||
marginBottom: 10
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{biometricsAuthEnabled ? (
|
||||
<Button
|
||||
title="Unlock with Biometrics"
|
||||
title={strings.unlockWithBiometrics()}
|
||||
width={250}
|
||||
onPress={onUnlockAppRequested}
|
||||
icon={"fingerprint"}
|
||||
@@ -331,4 +356,4 @@ const AppLockedOverlay = () => {
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default AppLockedOverlay;
|
||||
export default AppLocked;
|
||||
@@ -30,6 +30,8 @@ import filesystem from "../../common/filesystem";
|
||||
import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import picker from "../../screens/editor/tiptap/picker";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
@@ -43,28 +45,30 @@ import {
|
||||
eDBItemUpdate,
|
||||
eOnLoadNote
|
||||
} from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { openNote } from "../list-items/note/wrapper";
|
||||
import { DateMeta } from "../properties/date-meta";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { Notice } from "../ui/notice";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
close,
|
||||
setAttachments,
|
||||
fwdRef
|
||||
fwdRef,
|
||||
context
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
context: string;
|
||||
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
|
||||
close?: () => void;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
@@ -78,26 +82,25 @@ const Actions = ({
|
||||
const [loading, setLoading] = useState<{
|
||||
name?: string;
|
||||
}>({});
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: "Download",
|
||||
name: strings.network.download(),
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs().cancel(attachment.hash);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
}
|
||||
downloadAttachment(attachment.hash, false);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
downloadAttachment(attachment.hash, context === "global");
|
||||
fwdRef.current?.hide();
|
||||
},
|
||||
icon: "download"
|
||||
},
|
||||
{
|
||||
name: "Reupload",
|
||||
name: strings.network.reupload(),
|
||||
onPress: async () => {
|
||||
if (!PremiumService.get()) {
|
||||
ToastManager.show({
|
||||
heading: "Upgrade to pro",
|
||||
heading: strings.upgradeToPro(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -113,10 +116,10 @@ const Actions = ({
|
||||
icon: "upload"
|
||||
},
|
||||
{
|
||||
name: "Run file check",
|
||||
name: strings.fileCheck(),
|
||||
onPress: async () => {
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
name: strings.fileCheck()
|
||||
});
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
@@ -125,7 +128,7 @@ const Actions = ({
|
||||
db.attachments.markAsFailed(attachment.id, result.failed);
|
||||
setFailed(result.failed);
|
||||
ToastManager.show({
|
||||
heading: "File check failed with error: " + result.failed,
|
||||
heading: strings.fileCheckFailed(result.failed),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -134,7 +137,7 @@ const Actions = ({
|
||||
db.attachments.markAsFailed(attachment.id);
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
ToastManager.show({
|
||||
heading: "File check passed",
|
||||
heading: strings.fileCheckPassed(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -148,13 +151,11 @@ const Actions = ({
|
||||
icon: "file-check"
|
||||
},
|
||||
{
|
||||
name: "Rename",
|
||||
name: strings.rename(),
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
context: contextId as any,
|
||||
input: true,
|
||||
title: "Rename file",
|
||||
paragraph: "Enter a new name for the file",
|
||||
title: strings.renameFile(),
|
||||
defaultValue: attachment.filename,
|
||||
positivePress: async (value) => {
|
||||
if (value && value.length > 0) {
|
||||
@@ -166,14 +167,15 @@ const Actions = ({
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
positiveText: "Rename"
|
||||
positiveText: strings.rename()
|
||||
});
|
||||
},
|
||||
icon: "form-textbox"
|
||||
},
|
||||
{
|
||||
name: "Delete",
|
||||
name: strings.delete(),
|
||||
onPress: async () => {
|
||||
const relations = await db.relations.to(attachment, "note").get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
@@ -182,18 +184,17 @@ const Actions = ({
|
||||
relations
|
||||
.map((relation) => relation.fromId)
|
||||
.forEach(async (id) => {
|
||||
const tab = useTabStore.getState().getTabForNote(id);
|
||||
if (tab !== undefined) {
|
||||
const isFocused = useTabStore.getState().currentTab === tab;
|
||||
useTabStore.getState().forEachNoteTab(id, async (tab) => {
|
||||
const isFocused = useTabStore.getState().currentTab === tab.id;
|
||||
if (isFocused) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(id),
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
editorController.current.commands.setLoading(true, tab);
|
||||
editorController.current.commands.setLoading(true, tab.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
close?.();
|
||||
},
|
||||
@@ -219,6 +220,7 @@ const Actions = ({
|
||||
style={{
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
keyboardShouldPersistTaps="never"
|
||||
>
|
||||
<Dialog context={contextId} />
|
||||
<SheetProvider context={contextId} />
|
||||
@@ -226,14 +228,15 @@ const Actions = ({
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
marginBottom: notes && notes.length > 0 ? 0 : 12
|
||||
marginBottom:
|
||||
notes && notes.length > 0 ? 0 : DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
size={AppFontSize.lg}
|
||||
>
|
||||
{filename}
|
||||
</Heading>
|
||||
@@ -241,23 +244,22 @@ const Actions = ({
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: 10,
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 6,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{attachment.mimeType}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
|
||||
{notes.length ? (
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{notes.length} note
|
||||
{notes.length > 1 ? "s" : ""}
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{strings.notes(notes.length)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<Paragraph
|
||||
@@ -265,11 +267,11 @@ const Actions = ({
|
||||
Clipboard.setString(attachment.hash);
|
||||
ToastManager.show({
|
||||
type: "success",
|
||||
heading: "Attachment hash copied",
|
||||
heading: strings.hashCopied(),
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.hash}
|
||||
@@ -284,18 +286,18 @@ const Actions = ({
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
marginBottom: 12,
|
||||
paddingVertical: 12
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Heading
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
size={AppFontSize.sm}
|
||||
>
|
||||
List of notes:
|
||||
{strings.listOf()} {strings.dataTypesPlural.note()}:
|
||||
</Heading>
|
||||
|
||||
{notes.map((item) => (
|
||||
@@ -308,14 +310,14 @@ const Actions = ({
|
||||
openNote(item, (item as any).type === "trash");
|
||||
}}
|
||||
style={{
|
||||
paddingVertical: 12,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
alignItems: "flex-start",
|
||||
|
||||
paddingHorizontal: 12
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
key={item.id}
|
||||
>
|
||||
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
|
||||
<Paragraph size={AppFontSize.xs}>{item.title}</Paragraph>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
@@ -336,7 +338,7 @@ const Actions = ({
|
||||
icon={item.icon}
|
||||
loading={loading?.name === item.name}
|
||||
type="plain"
|
||||
fontSize={SIZE.sm}
|
||||
fontSize={AppFontSize.sm}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
justifyContent: "flex-start",
|
||||
@@ -348,13 +350,13 @@ const Actions = ({
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
{failed ? (
|
||||
<Notice
|
||||
type="alert"
|
||||
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
|
||||
text={strings.fileCheckFailed(failed)}
|
||||
size="small"
|
||||
/>
|
||||
) : null}
|
||||
@@ -376,6 +378,7 @@ Actions.present = (
|
||||
setAttachments={set}
|
||||
close={close}
|
||||
attachment={attachment}
|
||||
context={context || "global"}
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
@@ -22,15 +22,17 @@ import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Actions from "./actions";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
function getFileExtension(filename: string) {
|
||||
const ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
@@ -44,7 +46,8 @@ export const AttachmentItem = ({
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading,
|
||||
context
|
||||
context,
|
||||
errorOnly
|
||||
}: {
|
||||
id: string | number;
|
||||
attachments?: VirtualizedGrouping<Attachment>;
|
||||
@@ -53,9 +56,9 @@ export const AttachmentItem = ({
|
||||
pressable?: boolean;
|
||||
hideWhenNotDownloading?: boolean;
|
||||
context?: string;
|
||||
errorOnly?: boolean;
|
||||
}) => {
|
||||
const [attachment] = useDBItem(id, "attachment", attachments);
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
attachment,
|
||||
@@ -67,19 +70,15 @@ export const AttachmentItem = ({
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
return errorOnly && attachment && !attachment?.failed ? null : (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
minHeight: 45
|
||||
}}
|
||||
>
|
||||
@@ -89,41 +88,46 @@ export const AttachmentItem = ({
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: -5
|
||||
marginLeft: -5,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.border,
|
||||
paddingHorizontal: 2,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: defaultBorderRadius
|
||||
}}
|
||||
>
|
||||
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
|
||||
|
||||
<Paragraph
|
||||
adjustsFontSizeToFit
|
||||
size={6}
|
||||
color={colors.static.white}
|
||||
size={8}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
position: "absolute"
|
||||
maxWidth: 30
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{getFileExtension(attachment.filename).toUpperCase()}
|
||||
{getFileExtension(attachment.filename).toUpperCase() ||
|
||||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 10
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.sm - 1}
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginBottom: 2.5
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
numberOfLines={1}
|
||||
lineBreakMode="middle"
|
||||
@@ -133,11 +137,11 @@ export const AttachmentItem = ({
|
||||
</Paragraph>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{formatBytes(attachment.size)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xxs}
|
||||
>
|
||||
{strings.fileSize()}: {formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -159,7 +163,7 @@ export const AttachmentItem = ({
|
||||
}}
|
||||
>
|
||||
<ProgressCircleComponent
|
||||
size={SIZE.xxl}
|
||||
size={AppFontSize.xxl}
|
||||
progress={
|
||||
currentProgress?.value ? currentProgress?.value / 100 : 0
|
||||
}
|
||||
@@ -186,6 +190,6 @@ export const AttachmentItem = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import {
|
||||
PresentSheetOptions,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
|
||||
const DownloadAttachments = ({
|
||||
close,
|
||||
attachments,
|
||||
isNote,
|
||||
update
|
||||
}: {
|
||||
attachments: VirtualizedGrouping<Attachment>;
|
||||
close?: ((ctx?: string | undefined) => void) | undefined;
|
||||
isNote?: boolean;
|
||||
update?: (props: PresentSheetOptions) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
value: 0,
|
||||
statusText: "Download started... Please wait"
|
||||
});
|
||||
const [result, setResult] = useState(new Map());
|
||||
const canceled = useRef(false);
|
||||
const groupId = useRef<string>();
|
||||
|
||||
const onDownload = async () => {
|
||||
update?.({
|
||||
disableClosing: true
|
||||
} as PresentSheetOptions);
|
||||
setDownloading(true);
|
||||
canceled.current = false;
|
||||
groupId.current = Date.now().toString();
|
||||
const result = await downloadAttachments(
|
||||
await attachments.ids(),
|
||||
(progress: number, statusText: string) =>
|
||||
setProgress({ value: progress, statusText }),
|
||||
canceled,
|
||||
groupId.current
|
||||
);
|
||||
if (canceled.current) return;
|
||||
setResult(result || new Map());
|
||||
setDownloading(false);
|
||||
update?.({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
update?.({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
canceled.current = true;
|
||||
if (!groupId.current) return;
|
||||
console.log(groupId.current, "canceling groupId downloads");
|
||||
await db.fs().cancel(groupId.current);
|
||||
setDownloading(false);
|
||||
setResult(new Map());
|
||||
groupId.current = undefined;
|
||||
};
|
||||
|
||||
const failedResults = () => {
|
||||
const results = [];
|
||||
for (const value of result.values()) {
|
||||
if (value.status === 0) results.push(value.attachment);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
function getResultText() {
|
||||
const downloadedAttachmentsCount =
|
||||
attachments?.placeholders?.length - failedResults().length;
|
||||
if (downloadedAttachmentsCount === 0)
|
||||
return "Failed to download all attachments";
|
||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||
attachments?.placeholders.length
|
||||
} attachments as a zip file at ${
|
||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||
}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading>
|
||||
{downloading
|
||||
? "Downloading attachments"
|
||||
: result?.size
|
||||
? "Downloaded attachments"
|
||||
: "Download attachments"}
|
||||
</Heading>
|
||||
|
||||
{downloading ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{progress.statusText}
|
||||
</Paragraph>
|
||||
) : result?.size ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{getResultText()}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Are you sure you want to download all attachments
|
||||
{isNote ? " of this note?" : "?"}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{downloading ? (
|
||||
<View
|
||||
style={{
|
||||
width: 200,
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={
|
||||
progress.value
|
||||
? progress.value / attachments.placeholders?.length
|
||||
: 0
|
||||
}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
width: "100%",
|
||||
minHeight: 60,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginVertical: 12
|
||||
}}
|
||||
data={downloading ? attachments.placeholders : undefined}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 60
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
No downloads in progress.
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(index) => "attachment_download" + index}
|
||||
renderItem={({ index }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
id={index}
|
||||
setAttachments={() => {}}
|
||||
pressable={false}
|
||||
hideWhenNotDownloading={true}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{result?.size ? (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="accent"
|
||||
title="Done"
|
||||
/>
|
||||
) : !downloading ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
marginTop: 20
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="secondary"
|
||||
title="No"
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginLeft: 5
|
||||
}}
|
||||
onPress={onDownload}
|
||||
type="accent"
|
||||
title="Yes"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={cancel}
|
||||
type="error"
|
||||
title="Cancel"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
DownloadAttachments.present = (
|
||||
context: string,
|
||||
attachments: VirtualizedGrouping<Attachment>,
|
||||
isNote?: boolean
|
||||
) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close, update) => (
|
||||
<DownloadAttachments
|
||||
close={close}
|
||||
attachments={attachments}
|
||||
isNote={isNote}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default DownloadAttachments;
|
||||
@@ -19,21 +19,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {
|
||||
Attachment,
|
||||
FilteredSelector,
|
||||
Note,
|
||||
SortOptions,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import create, { State } from "zustand";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Header } from "../header";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
@@ -42,22 +50,106 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import DownloadAttachments from "./download-attachments";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const DEFAULT_SORTING: SortOptions = {
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
};
|
||||
|
||||
export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
type RecheckerProgress = {
|
||||
failed: number;
|
||||
passed: number;
|
||||
isWorking: boolean;
|
||||
shown: boolean;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
interface RecheckerState extends State {
|
||||
progress: {
|
||||
[key: string]: RecheckerProgress;
|
||||
};
|
||||
setProgress: (key: string, progress: RecheckerProgress) => void;
|
||||
currentFilter: string;
|
||||
}
|
||||
|
||||
const useRechecker = create<RecheckerState>((set) => ({
|
||||
progress: {
|
||||
all: {
|
||||
failed: 0,
|
||||
passed: 0,
|
||||
isWorking: false,
|
||||
shown: false,
|
||||
filter: "all"
|
||||
}
|
||||
},
|
||||
currentFilter: "all",
|
||||
setProgress: (key: string, progress: RecheckerProgress) => {
|
||||
set((state) => ({
|
||||
progress: {
|
||||
...state.progress,
|
||||
[key]: progress
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
const attachmentTypes = [
|
||||
{
|
||||
title: strings.mediaTypes.all(),
|
||||
filterBy: "all"
|
||||
},
|
||||
{
|
||||
title: strings.mediaTypes.image(),
|
||||
filterBy: "images"
|
||||
},
|
||||
{
|
||||
title: strings.mediaTypes.audio(),
|
||||
filterBy: "audio"
|
||||
},
|
||||
{
|
||||
title: strings.mediaTypes.video(),
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: strings.mediaTypes.document(),
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: strings.mediaTypes.orphaned(),
|
||||
filterBy: "orphaned"
|
||||
},
|
||||
{
|
||||
title: strings.mediaTypes.errors(),
|
||||
filterBy: "errors"
|
||||
}
|
||||
];
|
||||
|
||||
export const AttachmentDialog = ({
|
||||
note,
|
||||
isSheet
|
||||
}: {
|
||||
note?: Note;
|
||||
isSheet: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const [attachments, setAttachments] =
|
||||
useState<VirtualizedGrouping<Attachment>>();
|
||||
const attachmentSearchValue = useRef<string>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const searchTimer = useRef<NodeJS.Timeout>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
const rechecker = useRechecker((state) =>
|
||||
note ? state.progress[note.id] || {} : state.progress.all
|
||||
);
|
||||
const currentFilterRef = useRef(currentFilter);
|
||||
currentFilterRef.current = currentFilter;
|
||||
|
||||
useEffect(() => {
|
||||
useRechecker.setState({
|
||||
currentFilter: currentFilter
|
||||
});
|
||||
}, [currentFilter]);
|
||||
|
||||
const refresh = React.useCallback(() => {
|
||||
if (note) {
|
||||
@@ -68,6 +160,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => {
|
||||
setLoading(false);
|
||||
setAttachments(attachments);
|
||||
});
|
||||
} else {
|
||||
@@ -76,7 +169,10 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => setAttachments(attachments));
|
||||
.then((attachments) => {
|
||||
setAttachments(attachments);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [note]);
|
||||
|
||||
@@ -108,53 +204,70 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
setAttachments={async () => {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}}
|
||||
errorOnly={currentFilter === "errors"}
|
||||
attachments={attachments}
|
||||
id={index}
|
||||
context="attachments-list"
|
||||
context={!isSheet ? "global" : "attachments-list"}
|
||||
/>
|
||||
);
|
||||
|
||||
const onCheck = async () => {
|
||||
if (!attachments) return;
|
||||
setLoading(true);
|
||||
const getState = () =>
|
||||
useRechecker.getState().progress[note?.id || "all"] || {};
|
||||
const setState = (state: Partial<RecheckerProgress>) =>
|
||||
useRechecker.getState().setProgress(note?.id || "all", {
|
||||
...getState(),
|
||||
...state
|
||||
});
|
||||
|
||||
for (let i = 0; i < attachments.placeholders.length; i++) {
|
||||
if (!attachments || getState().isWorking) return;
|
||||
setState({
|
||||
isWorking: true,
|
||||
failed: 0,
|
||||
passed: 0,
|
||||
shown: true,
|
||||
filter: currentFilterRef.current
|
||||
});
|
||||
const filter = currentFilterRef.current;
|
||||
const filteredAttachments = await filterAttachments(
|
||||
currentFilterRef.current
|
||||
);
|
||||
|
||||
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
|
||||
if (!getState().isWorking) {
|
||||
ToastManager.show({
|
||||
message: strings.attachmentRecheckCancelled(),
|
||||
type: "info",
|
||||
context: isSheet ? "local" : "global"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const attachment = (await attachments.item(i))?.item;
|
||||
if (currentFilter == "errors" && !attachment?.failed) continue;
|
||||
|
||||
if (!attachment) continue;
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
if (result.failed) {
|
||||
setState({
|
||||
failed: getState().failed + 1
|
||||
});
|
||||
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
||||
} else {
|
||||
setState({
|
||||
passed: getState().passed + 1
|
||||
});
|
||||
await db.attachments.markAsFailed(attachment.id);
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const attachmentTypes = [
|
||||
{
|
||||
title: "All",
|
||||
filterBy: "all"
|
||||
},
|
||||
{
|
||||
title: "Images",
|
||||
filterBy: "images"
|
||||
},
|
||||
{
|
||||
title: "Documents",
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: "Video",
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
filterBy: "audio"
|
||||
if (filter === useRechecker.getState().currentFilter) {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}
|
||||
];
|
||||
|
||||
setState({
|
||||
isWorking: false
|
||||
});
|
||||
};
|
||||
|
||||
const filterAttachments = async (type: string) => {
|
||||
let items: FilteredSelector<Attachment> = db.attachments.all;
|
||||
@@ -184,6 +297,14 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "documents")
|
||||
: db.attachments.documents;
|
||||
break;
|
||||
case "orphaned":
|
||||
items = db.attachments.orphaned;
|
||||
break;
|
||||
case "errors":
|
||||
items = items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: db.attachments.all;
|
||||
}
|
||||
|
||||
return await items.sorted({
|
||||
@@ -193,170 +314,303 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
height: height * 0.85
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="attachments-list" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading>Attachments</Heading>
|
||||
|
||||
<>
|
||||
{isSheet ? (
|
||||
<>
|
||||
<SheetProvider context="attachments-list" />
|
||||
<Dialog context="local" />
|
||||
</>
|
||||
) : null}
|
||||
{!isSheet ? (
|
||||
<Header
|
||||
title={strings.manageAttachments()}
|
||||
renderedInRoute="SettingsGroup"
|
||||
canGoBack
|
||||
// TODO: Add headerRightButtons
|
||||
// headerRightButtons={[
|
||||
// {
|
||||
// onPress() {
|
||||
// onCheck();
|
||||
// },
|
||||
// title: strings.recheckAll()
|
||||
// },
|
||||
// {
|
||||
// onPress() {
|
||||
// if (!attachments) return;
|
||||
// presentDialog({
|
||||
// title: strings.doActions.download.attachment(
|
||||
// attachments.placeholders.length
|
||||
// ),
|
||||
// positiveText: strings.network.download(),
|
||||
// positivePress: async () => {
|
||||
// downloadAttachments(await attachments.ids());
|
||||
// },
|
||||
// negativeText: strings.cancel()
|
||||
// });
|
||||
// },
|
||||
// title: strings.downloadAllAttachments()
|
||||
// }
|
||||
// ]}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
) : (
|
||||
<Heading>{strings.dataTypesPluralCamelCase.attachment()}</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="check-all"
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
size={SIZE.lg}
|
||||
size={AppFontSize.lg}
|
||||
onPress={onCheck}
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
name="download"
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
DownloadAttachments.present(
|
||||
"attachments-list",
|
||||
attachments,
|
||||
!!note
|
||||
);
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
}}
|
||||
/>
|
||||
|
||||
<View>
|
||||
<ScrollView
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minWidth: "100%",
|
||||
height: 50
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{attachmentTypes.map((item) => (
|
||||
<Button
|
||||
type={
|
||||
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
|
||||
}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
flexGrow: 1,
|
||||
borderBottomColor:
|
||||
currentFilter !== item.filterBy
|
||||
? "transparent"
|
||||
: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
setCurrentFilter(item.filterBy);
|
||||
setAttachments(await filterAttachments(item.filterBy));
|
||||
<IconButton
|
||||
name="download"
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
presentDialog({
|
||||
title: strings.doActions.download.attachment(
|
||||
attachments.placeholders.length
|
||||
),
|
||||
positiveText: strings.network.download(),
|
||||
positivePress: async () => {
|
||||
downloadAttachments(await attachments.ids());
|
||||
},
|
||||
negativeText: strings.cancel()
|
||||
});
|
||||
}}
|
||||
size={AppFontSize.lg}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<FlashList
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon name="attachment" size={60} color={colors.secondary.icon} />
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={attachments?.placeholders}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
<View
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="shield-key-outline"
|
||||
size={SIZE.xs}
|
||||
color={colors.primary.icon}
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder={strings.filterAttachments()}
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
}}
|
||||
/>
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
|
||||
{rechecker.shown ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
gap: 12,
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{rechecker.isWorking ? (
|
||||
<ActivityIndicator color={colors.primary.accent} />
|
||||
) : (
|
||||
<Icon
|
||||
name="file-check"
|
||||
size={30}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View>
|
||||
<Paragraph>
|
||||
{rechecker.isWorking
|
||||
? note
|
||||
? strings.checkingNoteAttachments()
|
||||
: strings.checkingAllAttachments()
|
||||
: strings.attachmentRecheckComplete()}
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
{`${
|
||||
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
|
||||
}${strings.passed()}: ${
|
||||
rechecker.passed
|
||||
}, ${strings.failed()}: ${rechecker.failed}`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
type={rechecker.isWorking ? "errorShade" : "plain"}
|
||||
name={rechecker.isWorking ? "close" : "check"}
|
||||
size={AppFontSize.lg}
|
||||
color={
|
||||
rechecker.isWorking ? colors.error.icon : colors.primary.accent
|
||||
}
|
||||
onPress={() => {
|
||||
useRechecker.getState().setProgress(note?.id || "all", {
|
||||
...useRechecker.getState().progress[note?.id || "all"],
|
||||
shown: false,
|
||||
isWorking: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View>
|
||||
<ScrollView
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignItems: "center"
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{attachmentTypes.map((item) =>
|
||||
item.filterBy === "orphaned" && note ? null : (
|
||||
<Button
|
||||
type={
|
||||
currentFilter === item.filterBy
|
||||
? "secondaryAccented"
|
||||
: "plain"
|
||||
}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={AppFontSize.sm}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
height: 40,
|
||||
minWidth: 80
|
||||
}}
|
||||
onPress={async () => {
|
||||
const filterBy = item.filterBy;
|
||||
setCurrentFilter(filterBy);
|
||||
setLoading(true);
|
||||
filterAttachments(filterBy)
|
||||
.then((results) => {
|
||||
if (filterBy !== currentFilterRef.current) return;
|
||||
setAttachments(results);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<FlashList
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator size={40} color={colors.primary.accent} />
|
||||
) : (
|
||||
<>
|
||||
<Icon
|
||||
name="attachment"
|
||||
size={60}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph>{strings.noAttachments()}</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
ListHeaderComponent={<AllProgress note={note} />}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={loading ? [] : attachments?.placeholders}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AllProgress = ({ note }: { note?: Note }) => {
|
||||
const progress = useAttachmentStore((state) =>
|
||||
note
|
||||
? {
|
||||
downloading: {
|
||||
[note.id]: state.downloading?.[note.id]
|
||||
},
|
||||
uploading: {
|
||||
[note.id]: state.uploading?.[note.id]
|
||||
}
|
||||
}
|
||||
: state
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
gap: 10,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{Object.keys(progress.downloading || {}).map((groupId) => (
|
||||
<AttachmentGroupProgress key={groupId} groupId={groupId} />
|
||||
))}
|
||||
|
||||
{Object.keys(progress.uploading || {}).map((groupId) => (
|
||||
<AttachmentGroupProgress key={groupId} groupId={groupId} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note?: Note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} />,
|
||||
component: () => <AttachmentDialog note={note} isSheet={true} />,
|
||||
keyboardHandlerDisabled: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,9 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
@@ -31,11 +28,11 @@ import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth, initialAuthMode } from "./common";
|
||||
import { initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const AuthMode = {
|
||||
login: 0,
|
||||
@@ -49,7 +46,6 @@ const AuthModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
|
||||
const actionSheetRef = useRef();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenLoginDialog, open);
|
||||
@@ -97,81 +93,18 @@ const AuthModal = () => {
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent:
|
||||
initialAuthMode.current !== AuthMode.welcomeSignup
|
||||
? "space-between"
|
||||
: "flex-end"
|
||||
}}
|
||||
>
|
||||
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
|
||||
@@ -17,24 +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 { strings } from "@notesnook/intl";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import BackupService from "../../services/backup";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Dialog } from "../dialog";
|
||||
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();
|
||||
@@ -49,8 +45,8 @@ export const ChangePassword = () => {
|
||||
const changePassword = async () => {
|
||||
if (!user?.isEmailConfirmed) {
|
||||
ToastManager.show({
|
||||
heading: "Email not confirmed",
|
||||
message: "Please confirm your email to change account password",
|
||||
heading: strings.emailNotConfirmed(),
|
||||
message: strings.emailNotConfirmedDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -58,8 +54,8 @@ export const ChangePassword = () => {
|
||||
}
|
||||
if (error || !oldPassword.current || !password.current) {
|
||||
ToastManager.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again.",
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -67,25 +63,28 @@ export const ChangePassword = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (result.error)
|
||||
throw new Error(`Failed to create backup: ${result.error}`);
|
||||
const result = await BackupService.run(
|
||||
false,
|
||||
"change-password-dialog",
|
||||
"partial"
|
||||
);
|
||||
if (result.error) {
|
||||
throw new Error(strings.backupFailed() + `: ${result.error}`);
|
||||
}
|
||||
|
||||
await db.user.clearSessions();
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
ToastManager.show({
|
||||
heading: "Account password updated",
|
||||
heading: strings.passwordChangedSuccessfully(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
setLoading(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Navigation.goBack();
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Failed to change password",
|
||||
heading: strings.passwordChangeFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -98,16 +97,10 @@ export const ChangePassword = () => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: 12
|
||||
padding: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<DialogHeader
|
||||
title="Change password"
|
||||
paragraph="Enter your old and new passwords"
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={oldPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
@@ -119,7 +112,7 @@ export const ChangePassword = () => {
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="Old Password"
|
||||
placeholder={strings.oldPassword()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
@@ -128,44 +121,32 @@ export const ChangePassword = () => {
|
||||
password.current = value;
|
||||
}}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
validationType="password"
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="New password"
|
||||
placeholder={strings.newPassword()}
|
||||
/>
|
||||
|
||||
<Notice
|
||||
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
|
||||
type="alert"
|
||||
/>
|
||||
<Notice text={strings.changePasswordNotice()} type="alert" />
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Notice
|
||||
text={`Once your password is changed, please make sure to save the new account recovery key.`}
|
||||
type="alert"
|
||||
/>
|
||||
<Notice text={strings.changePasswordNotice2()} type="alert" />
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={changePassword}
|
||||
type="accent"
|
||||
title={loading ? null : "I understand, change my password"}
|
||||
title={loading ? null : strings.changePasswordConfirm()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ChangePassword.present = () => {
|
||||
presentSheet({
|
||||
component: <ChangePassword />
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,23 +20,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { createRef } from "react";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { eCloseLoginDialog } from "../../utils/events";
|
||||
import Navigation from "../../services/navigation";
|
||||
export const AuthMode = {
|
||||
login: 0,
|
||||
signup: 1,
|
||||
welcomeSignup: 2,
|
||||
welcomeLogin: 3,
|
||||
trialSignup: 4
|
||||
};
|
||||
|
||||
export const initialAuthMode = createRef(0);
|
||||
export function hideAuth() {
|
||||
export function hideAuth(context) {
|
||||
eSendEvent(eCloseLoginDialog);
|
||||
|
||||
// if (initialAuthMode.current === 2) {
|
||||
// Navigation.replace(
|
||||
// {
|
||||
// name: 'Notes'
|
||||
// },
|
||||
// {
|
||||
// menu: true
|
||||
// }
|
||||
// );
|
||||
// } else {
|
||||
// Navigation.goBack();
|
||||
// }
|
||||
|
||||
// tabBarRef.current?.unlock();
|
||||
if (
|
||||
initialAuthMode.current === AuthMode.welcomeSignup ||
|
||||
initialAuthMode.current === AuthMode.welcomeLogin ||
|
||||
context === "intro"
|
||||
) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const ForgotPassword = () => {
|
||||
const { colors } = useThemeColors("sheet");
|
||||
@@ -44,7 +46,7 @@ export const ForgotPassword = () => {
|
||||
const sendRecoveryEmail = async () => {
|
||||
if (!email.current || error) {
|
||||
ToastManager.show({
|
||||
heading: "Account email is required.",
|
||||
heading: strings.emailRequired(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -57,15 +59,15 @@ export const ForgotPassword = () => {
|
||||
lastRecoveryEmailTime &&
|
||||
Date.now() - JSON.parse(lastRecoveryEmailTime) < 60000 * 3
|
||||
) {
|
||||
throw new Error("Please wait before requesting another email");
|
||||
throw new Error(strings.pleaseWaitBeforeSendEmail());
|
||||
}
|
||||
await db.user.recoverAccount(email.current.toLowerCase());
|
||||
SettingsService.set({
|
||||
lastRecoveryEmailTime: Date.now()
|
||||
});
|
||||
ToastManager.show({
|
||||
heading: "Check your email to reset password",
|
||||
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
|
||||
heading: strings.recoveryEmailSent(),
|
||||
message: strings.recoveryEmailSentDesc(),
|
||||
type: "success",
|
||||
context: "local",
|
||||
duration: 7000
|
||||
@@ -75,7 +77,7 @@ export const ForgotPassword = () => {
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Recovery email not sent",
|
||||
heading: strings.recoveryEmailFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -106,7 +108,7 @@ export const ForgotPassword = () => {
|
||||
{sent ? (
|
||||
<View
|
||||
style={{
|
||||
padding: 12,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: 50
|
||||
@@ -121,13 +123,13 @@ export const ForgotPassword = () => {
|
||||
name="email"
|
||||
size={50}
|
||||
/>
|
||||
<Heading>Recovery email sent!</Heading>
|
||||
<Heading>{strings.recoveryEmailSent()}</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Please follow the link in the email to recover your account.
|
||||
{strings.recoveryEmailSentDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
@@ -137,13 +139,10 @@ export const ForgotPassword = () => {
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
padding: 12
|
||||
padding: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title="Account recovery"
|
||||
paragraph="We will send you an email with steps on how to reset your password."
|
||||
/>
|
||||
<DialogHeader title={strings.accountRecovery()} />
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
@@ -153,26 +152,26 @@ export const ForgotPassword = () => {
|
||||
}}
|
||||
defaultValue={email.current}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Email"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
onSubmit={() => {}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={sendRecoveryEmail}
|
||||
type="accent"
|
||||
title={loading ? null : "Next"}
|
||||
title={loading ? null : strings.next()}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
78
apps/mobile/app/components/auth/header.tsx
Normal file
78
apps/mobile/app/components/auth/header.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth } from "./common";
|
||||
export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const route = useRoute();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: !props.welcome ? "space-between" : "flex-end"
|
||||
}}
|
||||
>
|
||||
{props.welcome ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth((route.params as any)?.context);
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!props.welcome ? null : (
|
||||
<Button
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -18,35 +18,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import { Toast } from "../toast";
|
||||
import { initialAuthMode } from "./common";
|
||||
import { AuthMode, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
|
||||
export const AuthMode = {
|
||||
login: 0,
|
||||
signup: 1,
|
||||
welcomeSignup: 2,
|
||||
trialSignup: 3
|
||||
};
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
const Auth = ({ navigation, route }) => {
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(
|
||||
route?.params?.mode || AuthMode.login
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
initialAuthMode.current = route?.params.mode || AuthMode.login;
|
||||
useNavigationFocus(navigation, {
|
||||
onFocus: () => {
|
||||
//tabBarRef?.current.lock();
|
||||
initialAuthMode.current = route?.params.mode || AuthMode.login;
|
||||
}
|
||||
});
|
||||
useNavigationFocus(navigation, {});
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: colors.primary.background }}
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login &&
|
||||
currentAuthMode !== AuthMode.welcomeLogin ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
@@ -60,7 +53,7 @@ const Auth = ({ navigation, route }) => {
|
||||
)}
|
||||
|
||||
<Toast context="local" />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,19 +17,24 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
@@ -37,7 +42,9 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { AuthHeader } from "./header";
|
||||
import { useLogin } from "./use-login";
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -48,6 +55,7 @@ const LoginSteps = {
|
||||
export const Login = ({ changeMode }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [focused, setFocused] = useState(false);
|
||||
const route = useRoute();
|
||||
const {
|
||||
step,
|
||||
setStep,
|
||||
@@ -60,18 +68,27 @@ export const Login = ({ changeMode }) => {
|
||||
setError,
|
||||
login
|
||||
} = useLogin(async () => {
|
||||
hideAuth();
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
await sleep(500);
|
||||
Progress.present();
|
||||
hideAuth();
|
||||
setTimeout(() => {
|
||||
if (!useUserStore.getState().syncing) {
|
||||
Sync.run("global", false, "full");
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
if (!PremiumService.get() && !SettingsService.getProperty("serverUrls")) {
|
||||
Navigation.navigate("PayWall", {
|
||||
context: "signup",
|
||||
state: route.params.state,
|
||||
canGoBack: false
|
||||
});
|
||||
} else {
|
||||
Progress.present();
|
||||
}
|
||||
});
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
useEffect(() => {
|
||||
async () => {
|
||||
setStep(LoginSteps.emailAuth);
|
||||
@@ -87,222 +104,227 @@ export const Login = ({ changeMode }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<ForgotPassword />
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<View
|
||||
<Dialog context="two_factor_verify" />
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Login to your {"\n"}account
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : null,
|
||||
borderColor: isTablet ? colors.primary.border : null,
|
||||
borderRadius: isTablet ? 20 : null,
|
||||
marginTop: isTablet ? 50 : null,
|
||||
width: !isTablet ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Enter your email"
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
<Button
|
||||
title="Forgot your password?"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
height: 30,
|
||||
paddingHorizontal: 0
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
extraBold
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.loginToYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<Button
|
||||
title="Cancel logging in"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
height: 30,
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="errorShade"
|
||||
/>
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
<Button
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
<View>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
login();
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
fontSize={AppFontSize.sm}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<Button
|
||||
title={strings.cancelLogin()}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
type="secondaryAccented"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Sign up
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.signUp()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ import Sync from "../../services/sync";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
@@ -47,18 +47,9 @@ import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
|
||||
function getObfuscatedEmail(email) {
|
||||
if (!email) return "";
|
||||
const [username, provider] = email.split("@");
|
||||
if (username.length === 1) return `****@${provider}`;
|
||||
return email.replace(/(.{1})(.*)(?=@)/, function (gp1, gp2, gp3) {
|
||||
for (let i = 0; i < gp3.length; i++) {
|
||||
gp2 += "*";
|
||||
}
|
||||
return gp2;
|
||||
});
|
||||
}
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { getObfuscatedEmail } from "../../utils/functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const SessionExpired = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -167,7 +158,7 @@ export const SessionExpired = () => {
|
||||
<View
|
||||
style={{
|
||||
width: focused ? "100%" : "99.9%",
|
||||
padding: 12,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
justifyContent: "center",
|
||||
flex: 1,
|
||||
backgroundColor: colors.primary.background
|
||||
@@ -192,16 +183,15 @@ export const SessionExpired = () => {
|
||||
color={colors.error.icon}
|
||||
size={50}
|
||||
/>
|
||||
<Heading size={SIZE.xxxl} color={colors.primary.heading}>
|
||||
Session expired
|
||||
<Heading size={AppFontSize.xxxl} color={colors.primary.heading}>
|
||||
{strings.sessionExpired()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Your session on this device has expired. Please enter password for{" "}
|
||||
{getObfuscatedEmail(email.current)} to continue.
|
||||
{strings.sessionExpiredDesc(getObfuscatedEmail(email.current))}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -211,47 +201,46 @@ export const SessionExpired = () => {
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
loading={loading}
|
||||
onPress={() => login()}
|
||||
type="accent"
|
||||
title={loading ? null : "Login"}
|
||||
title={loading ? null : strings.login()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
context: "session_expiry",
|
||||
title: "Logout",
|
||||
paragraph:
|
||||
"Are you sure you want to logout from this device? Any unsynced changes will be lost.",
|
||||
positiveText: "Logout",
|
||||
title: strings.logoutFromDevice(),
|
||||
paragraph: strings.logoutDesc(),
|
||||
positiveText: strings.logout(),
|
||||
positiveType: "errorShade",
|
||||
positivePress: logout
|
||||
});
|
||||
}}
|
||||
type="errorShade"
|
||||
title={loading ? null : "Logout from this device"}
|
||||
title={loading ? null : strings.logoutFromDevice()}
|
||||
/>
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
|
||||
@@ -16,13 +16,12 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { Flex, Box, Image, Text } from "@theme-ui/components";
|
||||
import { Input } from "@theme-ui/components";
|
||||
export const SignupContext = React.createContext<{
|
||||
signup?: () => Promise<boolean>;
|
||||
}>({
|
||||
signup: undefined
|
||||
});
|
||||
|
||||
export const AnimatedFlex = motion(Flex);
|
||||
export const AnimatedBox = motion(Box);
|
||||
export const AnimatedImage = motion(Image);
|
||||
export const AnimatedText = motion(Text);
|
||||
export const AnimatedInput = motion(Input);
|
||||
export const useSignupContext = () => useContext(SignupContext);
|
||||
@@ -17,27 +17,38 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Loading } from "../loading";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { AuthHeader } from "./header";
|
||||
import { SignupContext } from "./signup-context";
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
selectPlan: 1,
|
||||
createAccount: 2
|
||||
};
|
||||
|
||||
export const Signup = ({ changeMode, welcome }) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
const { colors } = useThemeColors();
|
||||
const email = useRef();
|
||||
const emailInputRef = useRef();
|
||||
@@ -49,13 +60,15 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
const route = useRoute();
|
||||
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again",
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -69,239 +82,291 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const signup = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
await db.user.signup(email.current.toLowerCase(), password.current);
|
||||
let user = await db.user.getUser();
|
||||
setUser(user);
|
||||
setLastSynced(await db.lastSynced());
|
||||
clearMessage();
|
||||
setEmailVerifyMessage();
|
||||
hideAuth();
|
||||
SettingsService.setProperty("encryptedBackup", true);
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
} else {
|
||||
PremiumService.showVerifyEmailDialog();
|
||||
if (!SettingsService.getProperty("serverUrls")) {
|
||||
Navigation.navigate("PayWall", {
|
||||
canGoBack: false,
|
||||
state: route.params.state,
|
||||
context: "signup"
|
||||
});
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
setCurrentStep(SignupSteps.signup);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Signup failed",
|
||||
heading: strings.signupFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<SignupContext.Provider
|
||||
value={{
|
||||
signup: signup
|
||||
}}
|
||||
>
|
||||
{currentStep === SignupSteps.signup ? (
|
||||
<>
|
||||
<AuthHeader welcome={welcome} />
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Create your {"\n"}account
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Email"
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Signup"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder="Confirm password"
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
By signing up, you agree to our{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Terms of Service{" "}
|
||||
</Paragraph>
|
||||
and{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Privacy Policy.
|
||||
</Paragraph>{" "}
|
||||
You also agree to recieve marketing emails from us which you can
|
||||
opt-out of from app settings.
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.secondary.paragraph}>
|
||||
Already have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : null,
|
||||
borderColor: isTablet ? colors.primary.border : null,
|
||||
borderRadius: isTablet ? 20 : null,
|
||||
marginTop: isTablet ? 50 : null,
|
||||
width: !isTablet ? null : "50%",
|
||||
minHeight: height * 0.25
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.createAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: DDS.isTab ? 0 : 16,
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
defaultValue={email.current}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
blurOnSubmit={false}
|
||||
onSubmit={() => {
|
||||
if (!email.current) return;
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
defaultValue={password.current}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
blurOnSubmit={false}
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => {
|
||||
if (!password.current) return;
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
defaultValue={confirmPassword.current}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Signup"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
signup();
|
||||
}}
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.login()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[1]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser(
|
||||
"https://notesnook.com/privacy",
|
||||
colors
|
||||
);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[3]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[4]()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Loading
|
||||
title={"Setting up your account..."}
|
||||
description="Your account is almost ready, please wait..."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SignupContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user