Compare commits
714 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f44ed2598 | ||
|
|
5c0cfdcf38 | ||
|
|
2de5378208 | ||
|
|
a2aca89550 | ||
|
|
8d658dc159 | ||
|
|
fb8e0afd85 | ||
|
|
5263839731 | ||
|
|
484063a596 | ||
|
|
5650c7f778 | ||
|
|
74728782f3 | ||
|
|
0a0980f9e3 | ||
|
|
26c46b8488 | ||
|
|
c469696687 | ||
|
|
27550a7781 | ||
|
|
e79bacd268 | ||
|
|
fd2dfcc4f2 | ||
|
|
74b9e364fe | ||
|
|
d2412679ea | ||
|
|
98e6fc3b4a | ||
|
|
b64b2a3091 | ||
|
|
6275f3abf7 | ||
|
|
22e836c98a | ||
|
|
d6c0489fa3 | ||
|
|
78728ce4ca | ||
|
|
6803a34b51 | ||
|
|
42b6383283 | ||
|
|
147f009279 | ||
|
|
b899781b69 | ||
|
|
64d0819ab4 | ||
|
|
3ea4af816a | ||
|
|
037d2c6974 | ||
|
|
fe2c664474 | ||
|
|
a3d429b9c3 | ||
|
|
d0e6f4118d | ||
|
|
75fb58e454 | ||
|
|
6563b7fd11 | ||
|
|
e1b78174e2 | ||
|
|
aab7e25f80 | ||
|
|
24b2ba7b35 | ||
|
|
20ae667b3b | ||
|
|
d8cb9da483 | ||
|
|
8abce2ccf3 | ||
|
|
aa3ebd3bd2 | ||
|
|
4929437283 | ||
|
|
389dd19a8a | ||
|
|
a8b6b3335b | ||
|
|
a925e2fae0 | ||
|
|
dc79b5923e | ||
|
|
b39bbaf6fb | ||
|
|
14b40d820b | ||
|
|
354d5963ec | ||
|
|
6a32b94336 | ||
|
|
5c68437b1c | ||
|
|
0106f280f0 | ||
|
|
b567aaac69 | ||
|
|
36b419202b | ||
|
|
c80d01c648 | ||
|
|
1d00646b17 | ||
|
|
e4b98fe65a | ||
|
|
12443db891 | ||
|
|
03f08de32f | ||
|
|
c18f488be7 | ||
|
|
099012fac9 | ||
|
|
fa510af65a | ||
|
|
46b514cc61 | ||
|
|
019c8814f6 | ||
|
|
46249935b2 | ||
|
|
ebecc6bb81 | ||
|
|
130ddca135 | ||
|
|
f2661989dc | ||
|
|
3f6cb8c356 | ||
|
|
228d9cca6c | ||
|
|
d08a1dd2ca | ||
|
|
7d76a60db7 | ||
|
|
e4f0865067 | ||
|
|
cad4bd7c04 | ||
|
|
f85aaf0370 | ||
|
|
312d78617d | ||
|
|
af5aa12fa1 | ||
|
|
1c7e2edae0 | ||
|
|
a49eee9685 | ||
|
|
61938c8e66 | ||
|
|
76d10f5920 | ||
|
|
2cf85926e9 | ||
|
|
afb099f9df | ||
|
|
343627eb82 | ||
|
|
86f2477c63 | ||
|
|
2684150141 | ||
|
|
3f8d8d055d | ||
|
|
e775e0542e | ||
|
|
7992ba5bc1 | ||
|
|
d24bbaa65a | ||
|
|
58a91e1b86 | ||
|
|
cfed1d0230 | ||
|
|
b14c70f4b6 | ||
|
|
ad77fc20c6 | ||
|
|
029633f3d3 | ||
|
|
78459d759c | ||
|
|
a0d5a3dd07 | ||
|
|
d76d983d5d | ||
|
|
68b8e7dc7c | ||
|
|
1a7aff5102 | ||
|
|
677edffb80 | ||
|
|
2cd90c998b | ||
|
|
346e604f34 | ||
|
|
8d41111cd6 | ||
|
|
3163edcbe4 | ||
|
|
18df43c9cb | ||
|
|
4f2179c4d7 | ||
|
|
b89546ac22 | ||
|
|
c0ad7635f2 | ||
|
|
a82688163e | ||
|
|
29c36ee110 | ||
|
|
43698f8d61 | ||
|
|
b5b29aeb17 | ||
|
|
fdcf4c152a | ||
|
|
dcfda9521b | ||
|
|
950c9cdaeb | ||
|
|
1e9641a40e | ||
|
|
3fd2537311 | ||
|
|
6d470b8eba | ||
|
|
71ad81a67d | ||
|
|
9a1852ea05 | ||
|
|
629a86de99 | ||
|
|
3539868683 | ||
|
|
71d4566df5 | ||
|
|
caaa613ce9 | ||
|
|
cf36a33aea | ||
|
|
a777a808ee | ||
|
|
a78150b7ad | ||
|
|
deb177c6bb | ||
|
|
1c5e47b4c4 | ||
|
|
aae61f9451 | ||
|
|
807c44f057 | ||
|
|
9782007f7e | ||
|
|
77d5f1e603 | ||
|
|
81ee6de0a3 | ||
|
|
82fe65ada2 | ||
|
|
ce79d7b745 | ||
|
|
755cc4f5ec | ||
|
|
fde4d311e3 | ||
|
|
b08f40aaa3 | ||
|
|
e12ade6b31 | ||
|
|
43fc80ef41 | ||
|
|
6ca96157f6 | ||
|
|
856181ea54 | ||
|
|
f84bd46cdc | ||
|
|
0b43924ee2 | ||
|
|
16d3fd3828 | ||
|
|
cf5defa6a9 | ||
|
|
3de760db12 | ||
|
|
1366f0b68e | ||
|
|
be498e0bd3 | ||
|
|
a4e13f032a | ||
|
|
fef3136b1b | ||
|
|
6b318c248f | ||
|
|
fd6a3a5579 | ||
|
|
2292267e39 | ||
|
|
dbfae53222 | ||
|
|
d39ed267f3 | ||
|
|
9d04ad704a | ||
|
|
11cbb4df23 | ||
|
|
232911f725 | ||
|
|
41a0c6c73f | ||
|
|
10e80edb1d | ||
|
|
af3848586c | ||
|
|
48807bf030 | ||
|
|
21a14cb225 | ||
|
|
b725d07744 | ||
|
|
94e707da8a | ||
|
|
4fb4041f13 | ||
|
|
545da556d2 | ||
|
|
9f3da3454f | ||
|
|
15105426b1 | ||
|
|
b2d2e27945 | ||
|
|
d93a4cf9f3 | ||
|
|
8c3ec5da3d | ||
|
|
d067b1d55a | ||
|
|
55070ace8b | ||
|
|
0f35961173 | ||
|
|
b63f68bf74 | ||
|
|
8c6d6db07e | ||
|
|
a74a70fd8c | ||
|
|
97ad674be2 | ||
|
|
81534d2c13 | ||
|
|
51b7036456 | ||
|
|
e40fdad251 | ||
|
|
e4b0e97ac0 | ||
|
|
003320ca34 | ||
|
|
780addc50b | ||
|
|
8892a5f43d | ||
|
|
75e52ebca7 | ||
|
|
e7b3bbcd0e | ||
|
|
b9d60639e2 | ||
|
|
2ce0ce65e1 | ||
|
|
a32bdcb06a | ||
|
|
f9f8dff8b4 | ||
|
|
5c5cd1e501 | ||
|
|
89bcd1c4a8 | ||
|
|
6b7b142961 | ||
|
|
565e47aef8 | ||
|
|
dd448cb3ed | ||
|
|
7a9a19e3b9 | ||
|
|
a6e94441aa | ||
|
|
68d66d8ab3 | ||
|
|
aa5cc7ff9f | ||
|
|
eb8ede8376 | ||
|
|
4dc92bae4e | ||
|
|
9e1a975b23 | ||
|
|
e5a26fa0be | ||
|
|
4035a3a1e5 | ||
|
|
0380d286cd | ||
|
|
91dc3aa5d2 | ||
|
|
4bbffcf35e | ||
|
|
bb73eeb7ea | ||
|
|
546f8a12e4 | ||
|
|
3a865d4aaa | ||
|
|
05feb6b1b0 | ||
|
|
efa6fe3ef1 | ||
|
|
e917927979 | ||
|
|
61689ed451 | ||
|
|
b74919c376 | ||
|
|
67cc4f9600 | ||
|
|
ec749de3fa | ||
|
|
b99cd7ed3f | ||
|
|
87b9c955ca | ||
|
|
00aee2771e | ||
|
|
291276494d | ||
|
|
fc3823f0af | ||
|
|
855ef73a9a | ||
|
|
517a8d2104 | ||
|
|
a042795ec1 | ||
|
|
e427d65bf5 | ||
|
|
5ce319456f | ||
|
|
03828bfdaa | ||
|
|
7c836d121c | ||
|
|
b2ee3c5f30 | ||
|
|
04205783b8 | ||
|
|
f27e71fbd8 | ||
|
|
fbca679334 | ||
|
|
5b594ba0b4 | ||
|
|
3d4b5b0c1e | ||
|
|
c7574de7dc | ||
|
|
c8a3bb5f81 | ||
|
|
118edef773 | ||
|
|
050a1aff83 | ||
|
|
9ad74e9cbf | ||
|
|
09f8decc59 | ||
|
|
0decdf156b | ||
|
|
9316655b8c | ||
|
|
1bcc4fd2d7 | ||
|
|
239a4da145 | ||
|
|
d8e66c9b6a | ||
|
|
72279f4995 | ||
|
|
48b21de011 | ||
|
|
f7dc86ab2b | ||
|
|
39bfe6d2cb | ||
|
|
d6f534c3c0 | ||
|
|
45067d0354 | ||
|
|
da1ff63f72 | ||
|
|
5d715c50de | ||
|
|
47072ae1fe | ||
|
|
72f301fa45 | ||
|
|
e9b89629a6 | ||
|
|
3ef5ef166f | ||
|
|
37ca7a706a | ||
|
|
734c65fbda | ||
|
|
145605d628 | ||
|
|
7ac432fbc5 | ||
|
|
0c92cec2ea | ||
|
|
f6a788b36f | ||
|
|
72dffcf46b | ||
|
|
3140cdd148 | ||
|
|
d87adbce63 | ||
|
|
ffdf2bc0cd | ||
|
|
be5d7a1c9f | ||
|
|
94288b5cef | ||
|
|
63abbf5949 | ||
|
|
49214281f7 | ||
|
|
9688dde1a4 | ||
|
|
fdcc31f049 | ||
|
|
55ed6100e0 | ||
|
|
7fb11ba912 | ||
|
|
f3d77fdcf2 | ||
|
|
6489ab6a56 | ||
|
|
bace117ada | ||
|
|
76175d61af | ||
|
|
87110095a0 | ||
|
|
50ba8bec5a | ||
|
|
1741b1c686 | ||
|
|
99097baf9d | ||
|
|
bac1cc8243 | ||
|
|
096489d486 | ||
|
|
9811a9a3e1 | ||
|
|
910cde4380 | ||
|
|
4f1ccf83c8 | ||
|
|
9501c1ce4b | ||
|
|
069f0e53e1 | ||
|
|
aedfba795e | ||
|
|
9f162c0703 | ||
|
|
7b96c46e39 | ||
|
|
3d48ea71b9 | ||
|
|
24ee984a2e | ||
|
|
4255cbe540 | ||
|
|
fe16f24c41 | ||
|
|
a6e1fc5c44 | ||
|
|
8434312728 | ||
|
|
46f641aaec | ||
|
|
c246d8d517 | ||
|
|
4ec1aeafaf | ||
|
|
27cfd04ea7 | ||
|
|
797cfcb98d | ||
|
|
96a9b52e6d | ||
|
|
3b7462070b | ||
|
|
f08dd5960b | ||
|
|
9972196f70 | ||
|
|
ebbf06787c | ||
|
|
1d2b0cb093 | ||
|
|
6f27c6e4aa | ||
|
|
7b6008c37e | ||
|
|
cf5405fbe4 | ||
|
|
b4ec7402fc | ||
|
|
ff9a107a29 | ||
|
|
417ad87bcc | ||
|
|
265f99f327 | ||
|
|
0f8c3caf18 | ||
|
|
3a7677b46d | ||
|
|
3fd64d9956 | ||
|
|
1d26f4b24f | ||
|
|
1a4cea7b4f | ||
|
|
bbb1c42641 | ||
|
|
7ce2320eda | ||
|
|
1729b30f89 | ||
|
|
25d6595d5e | ||
|
|
3b31b9d65b | ||
|
|
e9e64fed3f | ||
|
|
3b88ea9b1c | ||
|
|
623f76fa6c | ||
|
|
19efcb1a0d | ||
|
|
31c8916622 | ||
|
|
7a0b189a1d | ||
|
|
fa0d56d57a | ||
|
|
85670bbc6a | ||
|
|
fc9a85b6ad | ||
|
|
8760b132da | ||
|
|
b80ee8d778 | ||
|
|
c8e168aa3e | ||
|
|
106aef579f | ||
|
|
d7084829c3 | ||
|
|
243d72eed5 | ||
|
|
aa60e97d5d | ||
|
|
4ae12db99c | ||
|
|
98ad058e2e | ||
|
|
58cc381abf | ||
|
|
d039c38f00 | ||
|
|
81f3347981 | ||
|
|
b96d2949f7 | ||
|
|
a2e745a349 | ||
|
|
0fe02b18ce | ||
|
|
655eb8c253 | ||
|
|
d9356f8171 | ||
|
|
be7d23163c | ||
|
|
7ac2e57484 | ||
|
|
727ea1283a | ||
|
|
3314057059 | ||
|
|
df1b00fa2c | ||
|
|
6434329f61 | ||
|
|
454853dc93 | ||
|
|
b663bf94e4 | ||
|
|
081cf081c5 | ||
|
|
f86b164c62 | ||
|
|
92b79e8272 | ||
|
|
335086c9d7 | ||
|
|
60806a3954 | ||
|
|
e0e96e7b9b | ||
|
|
cda35eb127 | ||
|
|
bf1ef345a2 | ||
|
|
e54ae7a51f | ||
|
|
a5a2d654ae | ||
|
|
b6340d8657 | ||
|
|
cdb87e2ac2 | ||
|
|
a0baed4e14 | ||
|
|
573a2d4fb1 | ||
|
|
df3ae9e22d | ||
|
|
52bffa024e | ||
|
|
459499d5f5 | ||
|
|
7cc94f6829 | ||
|
|
53d5b08559 | ||
|
|
c517b074f1 | ||
|
|
503ba43ebc | ||
|
|
b0755f4bc5 | ||
|
|
89ece1ec4c | ||
|
|
9fcaefbd99 | ||
|
|
81f33c7bbd | ||
|
|
0ce1bf2360 | ||
|
|
bb10c1aa9d | ||
|
|
b2c1daa6c7 | ||
|
|
da4338ed69 | ||
|
|
1849d712b1 | ||
|
|
28a7e0f717 | ||
|
|
ddbfc79cae | ||
|
|
ec8900d38f | ||
|
|
8aadd5d775 | ||
|
|
268f9306d9 | ||
|
|
866552254c | ||
|
|
42f87424c6 | ||
|
|
4899fb27ec | ||
|
|
c2c8d2bdaa | ||
|
|
daa373967f | ||
|
|
dccc9bfbcd | ||
|
|
f51a873752 | ||
|
|
bad5efc5e8 | ||
|
|
edf8384348 | ||
|
|
65c2f0e191 | ||
|
|
427a09296c | ||
|
|
ba11d62003 | ||
|
|
900dbe781e | ||
|
|
ee19aa8f2e | ||
|
|
e9cff66852 | ||
|
|
f63894bea7 | ||
|
|
35a285d71a | ||
|
|
ba80b01150 | ||
|
|
c9be359db4 | ||
|
|
46a3933dbb | ||
|
|
7b4060f9e1 | ||
|
|
beb3a6e67b | ||
|
|
b393c35284 | ||
|
|
ad371553ba | ||
|
|
0107fb9486 | ||
|
|
7cf70317e8 | ||
|
|
7d8aaac5d2 | ||
|
|
2b5f7d126f | ||
|
|
3cbc71ca1b | ||
|
|
9c4ac38a6c | ||
|
|
9ae64a97ba | ||
|
|
2807b1cad5 | ||
|
|
157e832c95 | ||
|
|
d78fcd3721 | ||
|
|
ac7ab79aef | ||
|
|
00fb353465 | ||
|
|
f0ec5a9496 | ||
|
|
7a31082da1 | ||
|
|
b54f4575ee | ||
|
|
58840e2c00 | ||
|
|
490a1e88eb | ||
|
|
2f8a3c9904 | ||
|
|
e7fdf76120 | ||
|
|
d0d41b743a | ||
|
|
a2995411d6 | ||
|
|
3246c4a621 | ||
|
|
48faf929a4 | ||
|
|
b7f922a999 | ||
|
|
bfcdf9340d | ||
|
|
4ed515f5a3 | ||
|
|
84b2737ffb | ||
|
|
deabca5a94 | ||
|
|
e9a49e23e8 | ||
|
|
cfdef23365 | ||
|
|
7c4b6a9de4 | ||
|
|
bb8866f73f | ||
|
|
33c42648ef | ||
|
|
7feddd2eee | ||
|
|
1344b84cf5 | ||
|
|
ef03b750c5 | ||
|
|
0c07d78a37 | ||
|
|
b5885e446c | ||
|
|
0fa3d4481a | ||
|
|
49e7c2e05b | ||
|
|
2dc7d046ef | ||
|
|
3e0bd41efd | ||
|
|
929eaca2d8 | ||
|
|
60517c5ab6 | ||
|
|
97a8a2b305 | ||
|
|
8fc54bdbe2 | ||
|
|
6ff251b24a | ||
|
|
7237800a91 | ||
|
|
b4cc8e92c7 | ||
|
|
2e233da16d | ||
|
|
b703f78db9 | ||
|
|
13e792cf4d | ||
|
|
552c2d74f3 | ||
|
|
593f23b021 | ||
|
|
1baf83bac3 | ||
|
|
d43ca3190e | ||
|
|
c4c7860876 | ||
|
|
0decdeb37c | ||
|
|
47505dcc31 | ||
|
|
e71d6e792b | ||
|
|
29796cfec8 | ||
|
|
4bdf255c3a | ||
|
|
11efe8b8d1 | ||
|
|
3dd2d09584 | ||
|
|
ed4e8e8f25 | ||
|
|
181a74df88 | ||
|
|
166b2f3a52 | ||
|
|
bd5cdc52f9 | ||
|
|
5d0318c102 | ||
|
|
17adc644fb | ||
|
|
f48159dc0b | ||
|
|
360697c034 | ||
|
|
45dd833b27 | ||
|
|
98491eed01 | ||
|
|
fdafc1c59e | ||
|
|
a32638ed4c | ||
|
|
39080a6046 | ||
|
|
a124d36714 | ||
|
|
7f9e619643 | ||
|
|
db12f2f5c8 | ||
|
|
415e0b70f3 | ||
|
|
0e4b1e5ec7 | ||
|
|
054a97371c | ||
|
|
2b25b6a6ea | ||
|
|
780ed3120e | ||
|
|
bdd3cbd4c7 | ||
|
|
0c8013038f | ||
|
|
c020d59c56 | ||
|
|
a5c336494b | ||
|
|
aaa4655f45 | ||
|
|
ed18ec0bc5 | ||
|
|
1de43f31b6 | ||
|
|
1a04c86edd | ||
|
|
2bbdcae82e | ||
|
|
9b0aa5d601 | ||
|
|
cdc261e30a | ||
|
|
09133a66b0 | ||
|
|
64f1a31533 | ||
|
|
9a92a50a5f | ||
|
|
2afd93e82f | ||
|
|
cb62cc1e9d | ||
|
|
2cd3fc5af9 | ||
|
|
bac710a17f | ||
|
|
fb8b0f78ca | ||
|
|
e18ce15753 | ||
|
|
ed8ce9e3ca | ||
|
|
af5ef04115 | ||
|
|
9530a3df52 | ||
|
|
2794ac653f | ||
|
|
43eb758d73 | ||
|
|
d0364cd101 | ||
|
|
6a008bf312 | ||
|
|
dfb271410c | ||
|
|
789d67209c | ||
|
|
a31f6b75d9 | ||
|
|
f814427a7d | ||
|
|
b0307e92d4 | ||
|
|
7fa3d69aa1 | ||
|
|
58555a6b85 | ||
|
|
52113395db | ||
|
|
8dd1309c21 | ||
|
|
202e428412 | ||
|
|
6e7ed3cea3 | ||
|
|
41cb49141b | ||
|
|
82a8283b6e | ||
|
|
a5d28adc44 | ||
|
|
82e206bccf | ||
|
|
1e4d6646c6 | ||
|
|
acbf9fc32f | ||
|
|
046f227003 | ||
|
|
50ac9e32be | ||
|
|
3459dcaa15 | ||
|
|
9410defab6 | ||
|
|
b5a26e11f8 | ||
|
|
c51481628d | ||
|
|
24fa51a12c | ||
|
|
7328520d05 | ||
|
|
409d206f1e | ||
|
|
b0b393f3d9 | ||
|
|
c4499088c8 | ||
|
|
4cccd6ac5c | ||
|
|
188b28fce3 | ||
|
|
24adda6c7d | ||
|
|
4b49302fbe | ||
|
|
402ab350de | ||
|
|
47b68770af | ||
|
|
60aa16a327 | ||
|
|
5702a4806b | ||
|
|
74c4bdb660 | ||
|
|
203a2cf7fb | ||
|
|
1faa2733b3 | ||
|
|
ffa432a876 | ||
|
|
009fd29265 | ||
|
|
8f05c2324e | ||
|
|
e1ab515883 | ||
|
|
a68a7a60a7 | ||
|
|
b4dc274646 | ||
|
|
b31892bdc6 | ||
|
|
f388a1348d | ||
|
|
52dacbddf9 | ||
|
|
cc52f60aa1 | ||
|
|
1af818b691 | ||
|
|
d76f7758e7 | ||
|
|
48ab2f2400 | ||
|
|
0aa844eebc | ||
|
|
481b02ccf2 | ||
|
|
67e6ef6fda | ||
|
|
7d19f86d7a | ||
|
|
2d27d8a47c | ||
|
|
10fac130ef | ||
|
|
c6b632543d | ||
|
|
32eb8157eb | ||
|
|
3628f22114 | ||
|
|
177fa37041 | ||
|
|
717f6576ea | ||
|
|
f1b2ffa0fa | ||
|
|
ac73c23c73 | ||
|
|
ac40308b1c | ||
|
|
6fb81aa78c | ||
|
|
92430c78c2 | ||
|
|
cb7ddaa295 | ||
|
|
786d079632 | ||
|
|
4af25a505a | ||
|
|
3218803aae | ||
|
|
2311d5bcef | ||
|
|
e56d92334f | ||
|
|
bc24a069da | ||
|
|
837747f8f7 | ||
|
|
a8c32ae49c | ||
|
|
32c5b414de | ||
|
|
12c81a22e8 | ||
|
|
0c5d0d4bb2 | ||
|
|
234f9c624d | ||
|
|
da669b44ff | ||
|
|
3c39f5f085 | ||
|
|
6de91b5872 | ||
|
|
ff9a0979f6 | ||
|
|
e1e8af2489 | ||
|
|
1eb000f615 | ||
|
|
e20fd97e59 | ||
|
|
d10ceacd67 | ||
|
|
208c28ee01 | ||
|
|
cdd1bb3c29 | ||
|
|
3d9c4fa320 | ||
|
|
9c4d18ef3b | ||
|
|
ce9ff3959f | ||
|
|
51aef4e1e5 | ||
|
|
90247059d0 | ||
|
|
c97abb46ed | ||
|
|
b8f5e371c7 | ||
|
|
401311a05f | ||
|
|
652b8e4e15 | ||
|
|
99b7e7c0f1 | ||
|
|
81442bb6f2 | ||
|
|
137f474b69 | ||
|
|
8e14d3f8f7 | ||
|
|
07b7bc003a | ||
|
|
df691c1516 | ||
|
|
17e61bb09d | ||
|
|
14e21988b2 | ||
|
|
205de5a633 | ||
|
|
eabdc903b9 | ||
|
|
0628e71ec9 | ||
|
|
ebbcb38c7a | ||
|
|
5c1f535291 | ||
|
|
2c8e488611 | ||
|
|
6f3eac659c | ||
|
|
86bca790bc | ||
|
|
895d196876 | ||
|
|
3d8b38ffe4 | ||
|
|
fddc415c86 | ||
|
|
7a8e9b5de1 | ||
|
|
bbaeb44b26 | ||
|
|
a95f659474 | ||
|
|
4d311b134f | ||
|
|
5f90de577f | ||
|
|
3c32ce946a | ||
|
|
5ca89d0e0d | ||
|
|
781d1f5b0a | ||
|
|
ef9bfae319 | ||
|
|
7abbc60e17 | ||
|
|
15244e1a64 | ||
|
|
4b39d61ff4 | ||
|
|
74718d6361 | ||
|
|
ced640c24a | ||
|
|
cdaa92c86d | ||
|
|
57012f0660 | ||
|
|
6fde6e0a79 | ||
|
|
182bde30fa | ||
|
|
59e2267513 | ||
|
|
01a064ef7f | ||
|
|
9759338e6a | ||
|
|
5ac4560157 | ||
|
|
8e60ca1ac9 | ||
|
|
131a8b5564 | ||
|
|
663023a204 | ||
|
|
2b5c9dfb35 | ||
|
|
a9a34fdd0a | ||
|
|
1655094ce3 | ||
|
|
9ae7e3df11 | ||
|
|
0a31e2d521 | ||
|
|
b6849661a6 | ||
|
|
53e822964e | ||
|
|
b2f76d2df9 | ||
|
|
574a3ce894 | ||
|
|
c68f796891 | ||
|
|
c592f84d7d | ||
|
|
31faaf147e | ||
|
|
09c20f5933 | ||
|
|
fa4ab7bd5c | ||
|
|
de4a24a7f8 | ||
|
|
8f1c404724 | ||
|
|
1b975e7ba7 | ||
|
|
1d7473c489 | ||
|
|
a7d3992ba1 | ||
|
|
d81e9a76db | ||
|
|
0624e59776 | ||
|
|
546064a7ee | ||
|
|
9d49cc876c | ||
|
|
0bf8481fd0 | ||
|
|
ae0758ac14 | ||
|
|
e9405f49ee | ||
|
|
a1d0eef8a5 | ||
|
|
254b806fb4 | ||
|
|
a7aee1450f | ||
|
|
fa5573a5ff |
@@ -1,12 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.git
|
.git
|
||||||
.DS_Store
|
.DS_Store
|
||||||
static
|
|
||||||
test
|
|
||||||
scripts
|
|
||||||
docs
|
|
||||||
firefox
|
firefox
|
||||||
|
assets
|
||||||
|
docs
|
||||||
public
|
public
|
||||||
views
|
test
|
||||||
webpack
|
coverage
|
||||||
frontend
|
.nyc_output
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
public
|
dist
|
||||||
test/frontend/bundle.js
|
assets
|
||||||
firefox
|
firefox
|
||||||
|
coverage
|
||||||
|
|||||||
10
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
node_modules
|
node_modules
|
||||||
static/*
|
coverage
|
||||||
!static/info.txt
|
dist
|
||||||
test/frontend/bundle.js
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
.nyc_output
|
||||||
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
assets/*.js
|
||||||
|
coverage
|
||||||
300
CHANGELOG.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
## Change Log
|
||||||
|
|
||||||
|
### upcoming (2018/02/27 01:52 +00:00)
|
||||||
|
- [#769](https://github.com/mozilla/send/pull/769) removed unsafe-inline styles via svgo-loader (@dannycoates)
|
||||||
|
- [#767](https://github.com/mozilla/send/pull/767) added coverage artifact to circleci (@dannycoates)
|
||||||
|
- [#766](https://github.com/mozilla/send/pull/766) Some frontend unit tests [WIP] (@dannycoates)
|
||||||
|
- [#761](https://github.com/mozilla/send/pull/761) added maxPasswordLength and passwordError messages (@dannycoates)
|
||||||
|
- [#764](https://github.com/mozilla/send/pull/764) added indefinite progress mode (@dannycoates)
|
||||||
|
- [#760](https://github.com/mozilla/send/pull/760) refactored css: phase 1 (@dannycoates)
|
||||||
|
- [#759](https://github.com/mozilla/send/pull/759) Switch en-US FTL file to new syntax (@flodolo)
|
||||||
|
- [#758](https://github.com/mozilla/send/pull/758) refactored server (@dannycoates)
|
||||||
|
- [#757](https://github.com/mozilla/send/pull/757) Update to fluent 0.4.3 (@stasm)
|
||||||
|
|
||||||
|
### v2.3.0 (2018/02/01 23:27 +00:00)
|
||||||
|
- [#536](https://github.com/mozilla/send/pull/536) use redis expire event to delete stored data immediately (@ehuggett)
|
||||||
|
- [#744](https://github.com/mozilla/send/pull/744) Gradient experiment (@dannycoates)
|
||||||
|
- [#739](https://github.com/mozilla/send/pull/739) added /api/info/:id route (@dannycoates)
|
||||||
|
- [#737](https://github.com/mozilla/send/pull/737) big refactor (@dannycoates)
|
||||||
|
- [#722](https://github.com/mozilla/send/pull/722) Add localization note to 'Time' and 'Downloads' string (@flodolo)
|
||||||
|
- [#721](https://github.com/mozilla/send/pull/721) show download Limits on page; Fixes #661 (@shikhar-scs)
|
||||||
|
- [#694](https://github.com/mozilla/send/pull/694) Passwords can now be changed (#687) (@himanish-star)
|
||||||
|
- [#702](https://github.com/mozilla/send/pull/702) Restricted the banner from showing on unsupported browsers (@himanish-star)
|
||||||
|
- [#701](https://github.com/mozilla/send/pull/701) improved popup for mobile display; Fixes #699 (@shikhar-scs)
|
||||||
|
- [#683](https://github.com/mozilla/send/pull/683) API changes to accommodate 3rd party clients (@ehuggett)
|
||||||
|
- [#698](https://github.com/mozilla/send/pull/698) Popup for delete button attached (@himanish-star)
|
||||||
|
- [#695](https://github.com/mozilla/send/pull/695) Show Warning, Cancel and Redirect on size > 2GB ; fixes #578 (@shikhar-scs)
|
||||||
|
- [#684](https://github.com/mozilla/send/pull/684) delete btn popup attached (@himanish-star)
|
||||||
|
- [#686](https://github.com/mozilla/send/pull/686) Hide password while Typing and after Entering: Fixes #670 (@shikhar-scs)
|
||||||
|
- [#679](https://github.com/mozilla/send/pull/679) changed font to sans sherif: Solves #676 (@shikhar-scs)
|
||||||
|
- [#693](https://github.com/mozilla/send/pull/693) README: Fix query link for "good first bugs" (@jspam)
|
||||||
|
- [#685](https://github.com/mozilla/send/pull/685) checkbox now has a hover effect: fixes #635 (@himanish-star)
|
||||||
|
- [#668](https://github.com/mozilla/send/pull/668) Add possibility to bind to a specific IP address (@TwizzyDizzy)
|
||||||
|
- [#682](https://github.com/mozilla/send/pull/682) [Docs] - README.md - minor spelling fixes (@tmm2018)
|
||||||
|
- [#672](https://github.com/mozilla/send/pull/672) Use EXPIRE_SECONDS to calculate file ttl for static content (@derektamsen)
|
||||||
|
- [#680](https://github.com/mozilla/send/pull/680) adjusted line height of label : fixes #609 (@himanish-star)
|
||||||
|
|
||||||
|
### v2.2.2 (2017/12/19 18:06 +00:00)
|
||||||
|
- [#667](https://github.com/mozilla/send/pull/667) Make develop the default NODE_ENV (@claudijd)
|
||||||
|
|
||||||
|
### v2.2.1 (2017/12/08 18:00 +00:00)
|
||||||
|
- [#665](https://github.com/mozilla/send/pull/665) stop drag target from flickering when dragging over children (@ericawright)
|
||||||
|
|
||||||
|
### v2.2.0 (2017/12/06 23:57 +00:00)
|
||||||
|
- [#654](https://github.com/mozilla/send/pull/654) Multiple download UI (@dannycoates)
|
||||||
|
- [#650](https://github.com/mozilla/send/pull/650) #634: overwrite appearance of password submit input (@ovlb)
|
||||||
|
- [#649](https://github.com/mozilla/send/pull/649) #609 share interface: align text in input and button (@ovlb)
|
||||||
|
|
||||||
|
### v2.1.2 (2017/11/16 19:03 +00:00)
|
||||||
|
- [#645](https://github.com/mozilla/send/pull/645) Remove the leak of the password into the console (@laurentj)
|
||||||
|
|
||||||
|
### v2.1.0 (2017/11/15 03:07 +00:00)
|
||||||
|
- [#641](https://github.com/mozilla/send/pull/641) Added experiment for firefox download promo (@dannycoates)
|
||||||
|
- [#640](https://github.com/mozilla/send/pull/640) use fluent-langneg for subtag support (@dannycoates)
|
||||||
|
- [#639](https://github.com/mozilla/send/pull/639) wrap number localization in try/catch (@dannycoates)
|
||||||
|
|
||||||
|
### v2.0.0 (2017/11/08 05:31 +00:00)
|
||||||
|
- [#633](https://github.com/mozilla/send/pull/633) Keyboard navigation/visual feedback regression (@ehuggett)
|
||||||
|
- [#632](https://github.com/mozilla/send/pull/632) display the 'add password' button only when the input field isn't empty (@dannycoates)
|
||||||
|
- [#626](https://github.com/mozilla/send/pull/626) Partial fix for #623 (@ehuggett)
|
||||||
|
- [#624](https://github.com/mozilla/send/pull/624) set a default MIME type in file metadata (@ehuggett)
|
||||||
|
- [#612](https://github.com/mozilla/send/pull/612) Password UI nits (@dannycoates, @ericawright)
|
||||||
|
- [#617](https://github.com/mozilla/send/pull/617) allow drag and drop if navigating from shared page (@ericawright)
|
||||||
|
- [#608](https://github.com/mozilla/send/pull/608) disable copying link when password not completed (@ericawright)
|
||||||
|
- [#605](https://github.com/mozilla/send/pull/605) align the "Password" and "Copy to clipboard" fields. (@ericawright)
|
||||||
|
- [#582](https://github.com/mozilla/send/pull/582) Add optional password to the download url (@dannycoates)
|
||||||
|
|
||||||
|
### v1.2.4 (2017/10/10 17:34 +00:00)
|
||||||
|
- [#583](https://github.com/mozilla/send/pull/583) Promote the beefy UI to default (@dannycoates)
|
||||||
|
- [#581](https://github.com/mozilla/send/pull/581) introducing ToC to README.md (@tmm2018)
|
||||||
|
- [#579](https://github.com/mozilla/send/pull/579) Hide cancel button when upload reaches 100% (@ericawright)
|
||||||
|
- [#580](https://github.com/mozilla/send/pull/580) Change Favicon in to look better in a variety of cases (@ericawright)
|
||||||
|
- [#571](https://github.com/mozilla/send/pull/571) Centre logo (@ehuggett)
|
||||||
|
- [#574](https://github.com/mozilla/send/pull/574) Make upload button focusable (accessibility/tab navigation) (@ehuggett)
|
||||||
|
|
||||||
|
### v1.2.0 (2017/09/12 22:42 +00:00)
|
||||||
|
- [#559](https://github.com/mozilla/send/pull/559) added first A/B experiment (@dannycoates)
|
||||||
|
- [#542](https://github.com/mozilla/send/pull/542) fix docker link typo (@ehuggett)
|
||||||
|
- [#541](https://github.com/mozilla/send/pull/541) removed .title and .alt attributes from ftl (@dannycoates)
|
||||||
|
- [#537](https://github.com/mozilla/send/pull/537) a few changes to make A/B testing easier (@dannycoates)
|
||||||
|
- [#533](https://github.com/mozilla/send/pull/533) minor UI fixes (@youwenliang)
|
||||||
|
- [#531](https://github.com/mozilla/send/pull/531) Add CHANGELOG script (@pdehaan)
|
||||||
|
- [#535](https://github.com/mozilla/send/pull/535) Fixed minimum NodeJS version in README (@LuFlo)
|
||||||
|
- [#528](https://github.com/mozilla/send/pull/528) adding separators to README (@tmm2018)
|
||||||
|
|
||||||
|
### v1.1.1 (2017/08/17 01:29 +00:00)
|
||||||
|
- [#516](https://github.com/mozilla/send/pull/516) cache assets (@dannycoates)
|
||||||
|
- [#520](https://github.com/mozilla/send/pull/520) fix drag & drop (@dannycoates)
|
||||||
|
- [#515](https://github.com/mozilla/send/pull/515) removed jquery from upload.js (@dannycoates)
|
||||||
|
- [#514](https://github.com/mozilla/send/pull/514) use async and removed jquery from download.js (@dannycoates)
|
||||||
|
- [#513](https://github.com/mozilla/send/pull/513) use svg for progress (@dannycoates)
|
||||||
|
- [#510](https://github.com/mozilla/send/pull/510) added precommit hook for format (@dannycoates)
|
||||||
|
- [#502](https://github.com/mozilla/send/pull/502) extracted filelist into its own file (@dannycoates)
|
||||||
|
- [#428](https://github.com/mozilla/send/pull/428) add twitter and open graph cards (@dannycoates, @johngruen)
|
||||||
|
- [#506](https://github.com/mozilla/send/pull/506) 404 page (@varghesethomase)
|
||||||
|
- [#508](https://github.com/mozilla/send/pull/508) fixes 478 (@abhinadduri)
|
||||||
|
- [#504](https://github.com/mozilla/send/pull/504) fix japanese browse button (@johngruen)
|
||||||
|
- [#503](https://github.com/mozilla/send/pull/503) Added editorconfig (@skystar-p)
|
||||||
|
- [#499](https://github.com/mozilla/send/pull/499) use import/export in the frontend code (@dannycoates)
|
||||||
|
- [#500](https://github.com/mozilla/send/pull/500) fixed build:css on windows (@dannycoates)
|
||||||
|
- [#481](https://github.com/mozilla/send/pull/481) Cater for mobile and desktop (@pdehaan, @hubdotcom)
|
||||||
|
- [#493](https://github.com/mozilla/send/pull/493) added webpack-dev-middleware (@dannycoates)
|
||||||
|
- [#491](https://github.com/mozilla/send/pull/491) added missing exit event cases (@dannycoates)
|
||||||
|
- [#492](https://github.com/mozilla/send/pull/492) make the site mostly work when cookies (localStorage) are disabled (@dannycoates)
|
||||||
|
- [#490](https://github.com/mozilla/send/pull/490) set the mime type in the download blob (@dannycoates)
|
||||||
|
- [#485](https://github.com/mozilla/send/pull/485) added progress to tab title when not in focus (@dannycoates)
|
||||||
|
- [#474](https://github.com/mozilla/send/pull/474) Fixing bug #438 by adding role attribute to anchor tags and alt attribute images (@varghesethomase)
|
||||||
|
- [#480](https://github.com/mozilla/send/pull/480) Increase font weight to 500 on <button>s and <label>s (@pdehaan)
|
||||||
|
- [#419](https://github.com/mozilla/send/pull/419) Add autoprefixer and cssnano support (@pdehaan)
|
||||||
|
|
||||||
|
### v1.1.0 (2017/08/08 03:59 +00:00)
|
||||||
|
- [#473](https://github.com/mozilla/send/pull/473) Sort contributors alphabetically to prevent churn (@pdehaan)
|
||||||
|
- [#472](https://github.com/mozilla/send/pull/472) removed references to checksums in frontend tests (@abhinadduri)
|
||||||
|
- [#470](https://github.com/mozilla/send/pull/470) removed the file sha256 hash (@dannycoates)
|
||||||
|
- [#469](https://github.com/mozilla/send/pull/469) Increase mimimum node version to 8.2.0 (@ehuggett)
|
||||||
|
- [#468](https://github.com/mozilla/send/pull/468) attach delete-file handler only after upload (@dannycoates)
|
||||||
|
- [#466](https://github.com/mozilla/send/pull/466) added webpack (@dannycoates)
|
||||||
|
- [#427](https://github.com/mozilla/send/pull/427) Extended system font list fixes:#408 (@gautamkrishnar)
|
||||||
|
- [#448](https://github.com/mozilla/send/pull/448) Migrate width attribute to CSS (Fixes #436) (@nskins)
|
||||||
|
- [#457](https://github.com/mozilla/send/pull/457) factored out progress into progress.js (@dannycoates)
|
||||||
|
- [#452](https://github.com/mozilla/send/pull/452) refactored metrics (@dannycoates)
|
||||||
|
- [#455](https://github.com/mozilla/send/pull/455) Add a few missing strings from es-CL and tr locales (@pdehaan)
|
||||||
|
- [#444](https://github.com/mozilla/send/pull/444) Chain jQuery calls, do not use events alias and store selectors (@Johann-S)
|
||||||
|
- [#416](https://github.com/mozilla/send/pull/416) WIP: use webcrypto-liner to support Safari 10 (@dannycoates)
|
||||||
|
- [#451](https://github.com/mozilla/send/pull/451) Add rel noopener noreferrer to target='_blank' anchor elements (Fixes #439) (@boopeshmahendran)
|
||||||
|
- [#449](https://github.com/mozilla/send/pull/449) Add X-UA-Compatible meta tag (@kenrick95)
|
||||||
|
- [#433](https://github.com/mozilla/send/pull/433) Prevent download button from being clicked multiple times (@pdehaan)
|
||||||
|
- [#432](https://github.com/mozilla/send/pull/432) Add contributors script (@pdehaan)
|
||||||
|
- [#409](https://github.com/mozilla/send/pull/409) Handle copy clipboard disabled (@Johann-S)
|
||||||
|
|
||||||
|
### v1.0.4 (2017/08/03 23:05 +00:00)
|
||||||
|
- [#418](https://github.com/mozilla/send/pull/418) _blank all footer links (@dannycoates)
|
||||||
|
- [#386](https://github.com/mozilla/send/pull/386) fix percentage view on mobile layout (@ariestiyansyah)
|
||||||
|
- [#414](https://github.com/mozilla/send/pull/414) Add link to FAQ in unsupported view (@pdehaan)
|
||||||
|
- [#415](https://github.com/mozilla/send/pull/415) Only include Fira CSS on /unsupported/* route (@pdehaan)
|
||||||
|
- [#412](https://github.com/mozilla/send/pull/412) throw key errors before download begins (@dannycoates)
|
||||||
|
- [#404](https://github.com/mozilla/send/pull/404) Use async function instead of promise (#325) (@weihanglo)
|
||||||
|
- [#406](https://github.com/mozilla/send/pull/406) Add noscript tag (@pdehaan)
|
||||||
|
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
|
||||||
|
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
|
||||||
|
|
||||||
|
### v1.0.3 (2017/08/02 23:59 +00:00)
|
||||||
|
- [#402](https://github.com/mozilla/send/pull/402) filter the hash from error reports (@dannycoates)
|
||||||
|
- [#400](https://github.com/mozilla/send/pull/400) fix link that breaks download by opening in new tab (@johngruen)
|
||||||
|
- [#369](https://github.com/mozilla/send/pull/369) Add ESLint no-alert shame rule (@pdehaan)
|
||||||
|
- [#396](https://github.com/mozilla/send/pull/396) add babel-polyfill (@dannycoates)
|
||||||
|
- [#394](https://github.com/mozilla/send/pull/394) catch JSON.parse errors of storage metadata (@dannycoates)
|
||||||
|
- [#367](https://github.com/mozilla/send/pull/367) Generate production locales using 'compare-locales' (@pdehaan)
|
||||||
|
- [#392](https://github.com/mozilla/send/pull/392) Adjust hover behavior on send-logo (#382)
|
||||||
|
Fixes: #382. (@weihanglo)
|
||||||
|
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
|
||||||
|
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
|
||||||
|
- [#380](https://github.com/mozilla/send/pull/380) Add Pontoon URL to README (@pdehaan)
|
||||||
|
|
||||||
|
### v1.0.2 (2017/07/31 18:58 +00:00)
|
||||||
|
- [#365](https://github.com/mozilla/send/pull/365) revert the IE fix to fix footer on chrome (@dannycoates)
|
||||||
|
|
||||||
|
### v1.0.1 (2017/07/31 17:28 +00:00)
|
||||||
|
- [#353](https://github.com/mozilla/send/pull/353) redirect ie to /unsupported (@abhinadduri, @dannycoates)
|
||||||
|
- [#360](https://github.com/mozilla/send/pull/360) Fix some linting nits (@pdehaan)
|
||||||
|
- [#362](https://github.com/mozilla/send/pull/362) Adjusts category of unsupported event (fixes #350). (@chuckharmston)
|
||||||
|
- [#355](https://github.com/mozilla/send/pull/355) Make order of uploaded files in list consistent (@pdehaan)
|
||||||
|
- [#356](https://github.com/mozilla/send/pull/356) Get rid of console.log statements (@pdehaan)
|
||||||
|
- [#358](https://github.com/mozilla/send/pull/358) Fix some missing .title attributes in dev-only locales (@pdehaan)
|
||||||
|
- [#354](https://github.com/mozilla/send/pull/354) Remove /en-US/ from cookies link in footer (@pdehaan)
|
||||||
|
- [#339](https://github.com/mozilla/send/pull/339) Show error page on firefox v49 and below (@ericawright, @abhinadduri)
|
||||||
|
- [#346](https://github.com/mozilla/send/pull/346) Add docs/CODEOWNERS file (@pdehaan)
|
||||||
|
- [#345](https://github.com/mozilla/send/pull/345) wrap long file names (@dnarcese)
|
||||||
|
- [#344](https://github.com/mozilla/send/pull/344) don't wrap file list headers (@dnarcese)
|
||||||
|
- [#327](https://github.com/mozilla/send/pull/327) Modify popup delete dialog (@youwenliang)
|
||||||
|
- [#341](https://github.com/mozilla/send/pull/341) center percentage text on all browser versions (@dnarcese)
|
||||||
|
- [#340](https://github.com/mozilla/send/pull/340) Remove duplicate entities in localized FTL files (@flodolo)
|
||||||
|
- [#337](https://github.com/mozilla/send/pull/337) support v 50 and 51 by not allowing const in loops (@ericawright)
|
||||||
|
- [#338](https://github.com/mozilla/send/pull/338) Remove duplicated strings in en-US, fix nn-NO file (@flodolo)
|
||||||
|
- [#336](https://github.com/mozilla/send/pull/336) German(de): Fixed missing value for deleteFileButton (#336) (@flodolo)
|
||||||
|
- [#334](https://github.com/mozilla/send/pull/334) fix functionality on firefox 50 and 51 (@dnarcese)
|
||||||
|
|
||||||
|
### v1.0.0 (2017/07/26 19:08 +00:00)
|
||||||
|
- [#323](https://github.com/mozilla/send/pull/323) disable upload/download notifications (@dannycoates)
|
||||||
|
- [#322](https://github.com/mozilla/send/pull/322) fix feedback button jump (@dnarcese)
|
||||||
|
- [#320](https://github.com/mozilla/send/pull/320) fix German footer (@dnarcese)
|
||||||
|
|
||||||
|
### v0.2.2 (2017/07/26 04:50 +00:00)
|
||||||
|
- [#314](https://github.com/mozilla/send/pull/314) added L10N_DEV environment variable for making all languages available (@dannycoates)
|
||||||
|
- [#313](https://github.com/mozilla/send/pull/313) removing timeout limit for front end tests (@abhinadduri)
|
||||||
|
- [#311](https://github.com/mozilla/send/pull/311) expired ids should reject instead of returning null (@dannycoates)
|
||||||
|
- [#302](https://github.com/mozilla/send/pull/302) UX Refine WIP (@youwenliang)
|
||||||
|
- [#310](https://github.com/mozilla/send/pull/310) if the download card is pressed, the expired card shows up properly (@abhinadduri)
|
||||||
|
- [#269](https://github.com/mozilla/send/pull/269) refactored ftl file (@abhinadduri)
|
||||||
|
- [#291](https://github.com/mozilla/send/pull/291) added legal page (@dannycoates)
|
||||||
|
- [#307](https://github.com/mozilla/send/pull/307) don't show error page on upload cancel (@dnarcese)
|
||||||
|
- [#299](https://github.com/mozilla/send/pull/299) use CIRCLE_TAG as version.json version if present (@dannycoates)
|
||||||
|
|
||||||
|
### v0.2.1 (2017/07/24 23:34 +00:00)
|
||||||
|
- [#296](https://github.com/mozilla/send/pull/296) restyle delete popup (@dnarcese)
|
||||||
|
- [#295](https://github.com/mozilla/send/pull/295) renamed environment variables to remove P2P_ prefix (@dannycoates)
|
||||||
|
- [#294](https://github.com/mozilla/send/pull/294) dealing with invalid drag and drops (@abhinadduri)
|
||||||
|
- [#297](https://github.com/mozilla/send/pull/297) added environment variable for expire time (@dannycoates)
|
||||||
|
- [#292](https://github.com/mozilla/send/pull/292) Fixes289 (@abhinadduri)
|
||||||
|
- [#288](https://github.com/mozilla/send/pull/288) fix: Don`t allow upload when not on the upload page. (@ericawright)
|
||||||
|
- [#285](https://github.com/mozilla/send/pull/285) added messages for processing phases (@dannycoates)
|
||||||
|
- [#267](https://github.com/mozilla/send/pull/267) make site responsive and add feedback link (@johngruen)
|
||||||
|
- [#286](https://github.com/mozilla/send/pull/286) Update download progress bar color (@pdehaan)
|
||||||
|
- [#281](https://github.com/mozilla/send/pull/281) Stop ESLint from linting the /public/ directory (@pdehaan)
|
||||||
|
- [#280](https://github.com/mozilla/send/pull/280) created /unsupported page and added gcmCompliant to /download page (@dannycoates)
|
||||||
|
- [#279](https://github.com/mozilla/send/pull/279) create separate js bundles for upload/download pages (@dannycoates)
|
||||||
|
- [#268](https://github.com/mozilla/send/pull/268) Testpilot ga (@abhinadduri)
|
||||||
|
|
||||||
|
### v0.2.0 (2017/07/21 19:27 +00:00)
|
||||||
|
- [#266](https://github.com/mozilla/send/pull/266) abort uploads over maxfilesize (@dannycoates)
|
||||||
|
- [#264](https://github.com/mozilla/send/pull/264) Remove duplicate custom metric. (@chuckharmston)
|
||||||
|
- [#259](https://github.com/mozilla/send/pull/259) add alert when uploading multiple files (@dnarcese)
|
||||||
|
- [#262](https://github.com/mozilla/send/pull/262) sync download progress bar with percentage (@dnarcese)
|
||||||
|
- [#258](https://github.com/mozilla/send/pull/258) better sync percent with progress bar (@dnarcese)
|
||||||
|
- [#257](https://github.com/mozilla/send/pull/257) add a dynamic js script for page config (@dannycoates)
|
||||||
|
- [#256](https://github.com/mozilla/send/pull/256) add file size limit message (@dnarcese)
|
||||||
|
- [#253](https://github.com/mozilla/send/pull/253) Add favicon.ico version of the Send logo (@pdehaan)
|
||||||
|
- [#254](https://github.com/mozilla/send/pull/254) Add nsp check to circle ci (@pdehaan)
|
||||||
|
- [#245](https://github.com/mozilla/send/pull/245) Localization (@abhinadduri)
|
||||||
|
- [#252](https://github.com/mozilla/send/pull/252) only allow drag and drop on upload page (@dnarcese)
|
||||||
|
- [#250](https://github.com/mozilla/send/pull/250) make footer not overlap (@dnarcese)
|
||||||
|
- [#251](https://github.com/mozilla/send/pull/251) minify all images (@ericawright)
|
||||||
|
- [#249](https://github.com/mozilla/send/pull/249) change how the file upload box expands (@dnarcese)
|
||||||
|
- [#246](https://github.com/mozilla/send/pull/246) remove P2P references. Fixes #224 (@clouserw)
|
||||||
|
- [#242](https://github.com/mozilla/send/pull/242) Make only icons clickable in file list (@dnarcese)
|
||||||
|
- [#236](https://github.com/mozilla/send/pull/236) add FAQ. Fixes #186 (@clouserw)
|
||||||
|
- [#235](https://github.com/mozilla/send/pull/235) allow send another file link to open in new tab (@dnarcese)
|
||||||
|
- [#234](https://github.com/mozilla/send/pull/234) fix download svg (@dnarcese)
|
||||||
|
- [#232](https://github.com/mozilla/send/pull/232) escape filename in the ui (@dannycoates)
|
||||||
|
- [#226](https://github.com/mozilla/send/pull/226) added functionality to cancel uploads (@abhinadduri)
|
||||||
|
- [#231](https://github.com/mozilla/send/pull/231) move head and html tags to main template (@dnarcese)
|
||||||
|
- [#228](https://github.com/mozilla/send/pull/228) add send logo (@dnarcese)
|
||||||
|
- [#229](https://github.com/mozilla/send/pull/229) change learn more and github links (@dnarcese)
|
||||||
|
- [#201](https://github.com/mozilla/send/pull/201) Adds metrics documentation (closes #5). (@chuckharmston)
|
||||||
|
- [#223](https://github.com/mozilla/send/pull/223) change size of send another file links (@dnarcese)
|
||||||
|
- [#222](https://github.com/mozilla/send/pull/222) add footer (@dnarcese)
|
||||||
|
- [#197](https://github.com/mozilla/send/pull/197) fixes issues 195 and 192 (@abhinadduri)
|
||||||
|
- [#204](https://github.com/mozilla/send/pull/204) added HSTS header (@dannycoates)
|
||||||
|
- [#193](https://github.com/mozilla/send/pull/193) Frontend tests (@abhinadduri)
|
||||||
|
- [#191](https://github.com/mozilla/send/pull/191) New ui! (@dnarcese)
|
||||||
|
|
||||||
|
### v0.1.4 (2017/07/12 18:21 +00:00)
|
||||||
|
- [#189](https://github.com/mozilla/send/pull/189) Add CSP directives (@dannycoates)
|
||||||
|
- [#188](https://github.com/mozilla/send/pull/188) fixes delete button error (@abhinadduri)
|
||||||
|
- [#185](https://github.com/mozilla/send/pull/185) added loading, hashing, and encrypting events for uploader; decryptin… (@abhinadduri)
|
||||||
|
- [#183](https://github.com/mozilla/send/pull/183) rename to 'Send' (@dannycoates)
|
||||||
|
- [#184](https://github.com/mozilla/send/pull/184) Server tests (@abhinadduri)
|
||||||
|
- [#178](https://github.com/mozilla/send/pull/178) fixed issues in branch title (@abhinadduri)
|
||||||
|
- [#177](https://github.com/mozilla/send/pull/177) Gcm compliance (@abhinadduri)
|
||||||
|
- [#106](https://github.com/mozilla/send/pull/106) Gcm (@abhinadduri, @dannycoates)
|
||||||
|
- [#168](https://github.com/mozilla/send/pull/168) Show error page if upload fails (@dnarcese)
|
||||||
|
- [#148](https://github.com/mozilla/send/pull/148) WIP: Add basic contribute.json (@pdehaan)
|
||||||
|
- [#162](https://github.com/mozilla/send/pull/162) Fix dev server URL in README.md file (@pdehaan)
|
||||||
|
- [#167](https://github.com/mozilla/send/pull/167) build docker image with new name (@relud)
|
||||||
|
- [#164](https://github.com/mozilla/send/pull/164) Add word wraps to table (@dnarcese)
|
||||||
|
- [#149](https://github.com/mozilla/send/pull/149) Add robots.txt (@pdehaan)
|
||||||
|
- [#161](https://github.com/mozilla/send/pull/161) Hide table header on empty list (@dnarcese)
|
||||||
|
- [#154](https://github.com/mozilla/send/pull/154) Remove expired uploads (@dnarcese)
|
||||||
|
- [#146](https://github.com/mozilla/send/pull/146) Update README with some more details (@pdehaan)
|
||||||
|
|
||||||
|
### v0.1.2 (2017/06/24 03:38 +00:00)
|
||||||
|
- [#138](https://github.com/mozilla/send/pull/138) remove notLocalHost (@dannycoates)
|
||||||
|
|
||||||
|
### v0.1.0 (2017/06/24 01:24 +00:00)
|
||||||
|
- [#137](https://github.com/mozilla/send/pull/137) refactored docker build (@dannycoates)
|
||||||
|
- [#132](https://github.com/mozilla/send/pull/132) Add /__version__ route (@pdehaan)
|
||||||
|
- [#135](https://github.com/mozilla/send/pull/135) make dockerfile more dockerflowy (@dannycoates)
|
||||||
|
- [#134](https://github.com/mozilla/send/pull/134) Load previous uploads (@dannycoates, @dnarcese)
|
||||||
|
- [#131](https://github.com/mozilla/send/pull/131) added __heartbeat__ (@dannycoates)
|
||||||
|
- [#133](https://github.com/mozilla/send/pull/133) Add LICENSE file (@pdehaan)
|
||||||
|
- [#130](https://github.com/mozilla/send/pull/130) added sentry to server code (@abhinadduri)
|
||||||
|
- [#124](https://github.com/mozilla/send/pull/124) Remove unused [dev]dependencies (@pdehaan)
|
||||||
|
- [#119](https://github.com/mozilla/send/pull/119) Move cross-env to a dep (@pdehaan)
|
||||||
|
- [#123](https://github.com/mozilla/send/pull/123) removed bitly integration (@abhinadduri)
|
||||||
|
- [#122](https://github.com/mozilla/send/pull/122) fix docker build (@dannycoates)
|
||||||
|
- [#121](https://github.com/mozilla/send/pull/121) added docker service to circle.yml (@dannycoates)
|
||||||
|
- [#120](https://github.com/mozilla/send/pull/120) added sentry (@abhinadduri)
|
||||||
|
- [#118](https://github.com/mozilla/send/pull/118) change docker image name and add builds for tags (@relud)
|
||||||
|
- [#116](https://github.com/mozilla/send/pull/116) add /__lbheartbeat__ endpoint (@relud)
|
||||||
|
- [#79](https://github.com/mozilla/send/pull/79) Optimize/minimize bundle.js for production (@pdehaan)
|
||||||
|
- [#104](https://github.com/mozilla/send/pull/104) Fix a bunch of ESLint and HTMLLint errors (@pdehaan)
|
||||||
|
- [#105](https://github.com/mozilla/send/pull/105) Progress bars (@dnarcese)
|
||||||
|
- [#111](https://github.com/mozilla/send/pull/111) added in anonmyized ip google analytics (@abhinadduri)
|
||||||
|
- [#110](https://github.com/mozilla/send/pull/110) added notifications (@abhinadduri)
|
||||||
|
- [#103](https://github.com/mozilla/send/pull/103) added Dockerfile (@dannycoates)
|
||||||
|
- [#100](https://github.com/mozilla/send/pull/100) Added Helmet Middleware (@abhinadduri)
|
||||||
|
- [#99](https://github.com/mozilla/send/pull/99) Testing (@abhinadduri)
|
||||||
|
- [#77](https://github.com/mozilla/send/pull/77) Fix the linter errors (@pdehaan)
|
||||||
|
- [#54](https://github.com/mozilla/send/pull/54) Adding basic ESLint config (@pdehaan)
|
||||||
|
- [#71](https://github.com/mozilla/send/pull/71) Drag & drop (@dnarcese)
|
||||||
|
- [#72](https://github.com/mozilla/send/pull/72) Logging (@abhinadduri, @dannycoates)
|
||||||
|
- [#45](https://github.com/mozilla/send/pull/45) S3 integration (@abhinadduri, @dannycoates)
|
||||||
|
- [#46](https://github.com/mozilla/send/pull/46) Download page and share link UI (@dnarcese)
|
||||||
|
- [#41](https://github.com/mozilla/send/pull/41) Added upload page and file list UI (@dnarcese)
|
||||||
|
- [#40](https://github.com/mozilla/send/pull/40) Tweak the package.json file (@pdehaan)
|
||||||
|
- [#43](https://github.com/mozilla/send/pull/43) added return (@abhinadduri)
|
||||||
|
- [#42](https://github.com/mozilla/send/pull/42) changed to handle 404 during download, also removing progress listene… (@abhinadduri)
|
||||||
|
- [#39](https://github.com/mozilla/send/pull/39) Refactor riff (@abhinadduri, @dannycoates)
|
||||||
|
- [#36](https://github.com/mozilla/send/pull/36) added prettier for js formatting (@dannycoates)
|
||||||
|
- [#28](https://github.com/mozilla/send/pull/28) Added a UI for the uploader end, made stylistic changes, implemented deleting (@abhinadduri)
|
||||||
|
- [#25](https://github.com/mozilla/send/pull/25) Changed naming for some pages, no longer stores files by name on server (@abhinadduri)
|
||||||
72
CONTRIBUTORS
@@ -1,64 +1,122 @@
|
|||||||
|
Abdalrahman Hwoij
|
||||||
Abhinav Adduri
|
Abhinav Adduri
|
||||||
|
Adnan Kičin
|
||||||
|
Alberto Castro
|
||||||
Alexander Slovesnik
|
Alexander Slovesnik
|
||||||
Amin Mahmudian
|
Amin Mahmudian
|
||||||
Andreas Pettersson
|
Andreas Pettersson
|
||||||
|
Arash Mousavi
|
||||||
|
Artem Polivanchuk
|
||||||
|
Ashikur Rahman
|
||||||
Balázs Meskó
|
Balázs Meskó
|
||||||
|
Belayet Hossain
|
||||||
|
Besnik Bleta
|
||||||
Bjørn I
|
Bjørn I
|
||||||
Boopesh Mahendran
|
Boopesh Mahendran
|
||||||
|
Breana Gonzales
|
||||||
Chuck Harmston
|
Chuck Harmston
|
||||||
|
Cláudio Esperança
|
||||||
|
Cristian Silaghi
|
||||||
Cynthia Pereira
|
Cynthia Pereira
|
||||||
Daniel Thorn
|
Daniel Thorn
|
||||||
Daniela Arcese
|
Daniela Arcese
|
||||||
Danny Coates
|
Danny Coates
|
||||||
|
Derek Tamsen
|
||||||
|
Edmund Huggett
|
||||||
|
Elisa X
|
||||||
Emin Mastizada
|
Emin Mastizada
|
||||||
|
Enol
|
||||||
Erica
|
Erica
|
||||||
Erica Wright
|
Erica Wright
|
||||||
|
Filip Hruška
|
||||||
Fjoerfoks
|
Fjoerfoks
|
||||||
Francesco Lodolo
|
Francesco Lodolo
|
||||||
Francesco Lodolo [:flod]
|
Francesco Lodolo [:flod]
|
||||||
|
Frederick Villaluna
|
||||||
Gautam krishna.R
|
Gautam krishna.R
|
||||||
|
Georgianizator
|
||||||
|
Hyeonseok Shin
|
||||||
Håvar Henriksen
|
Håvar Henriksen
|
||||||
|
Jae Hyeon Park
|
||||||
|
Jakub Rychlý
|
||||||
|
Jamie
|
||||||
Jim Spentzos
|
Jim Spentzos
|
||||||
|
Jobava
|
||||||
Johann-S
|
Johann-S
|
||||||
John Gruen
|
John Gruen
|
||||||
|
Jon Vadillo
|
||||||
|
Jonathan Claudius
|
||||||
|
Jordi Cuevas
|
||||||
Jordi Serratosa
|
Jordi Serratosa
|
||||||
|
Juan Esteban Ajsivinac Sián
|
||||||
Juraj Cigáň
|
Juraj Cigáň
|
||||||
|
Kerim Kalamujić
|
||||||
|
Khaled Hosny
|
||||||
Kohei Yoshino
|
Kohei Yoshino
|
||||||
Lan Glad
|
Lan Glad
|
||||||
|
Laurent Jouanneau
|
||||||
|
Lobodzets
|
||||||
|
LuFlo
|
||||||
|
Luiz Carlos de Morais
|
||||||
Luna Jernberg
|
Luna Jernberg
|
||||||
Marcelo Poli
|
Marcelo Poli
|
||||||
Marco Aurélio
|
Marco Aurélio
|
||||||
|
Mark Heijl
|
||||||
Mark Liang
|
Mark Liang
|
||||||
|
Marko Andrejić
|
||||||
Matjaž Horvat
|
Matjaž Horvat
|
||||||
Maykon Chagas
|
Maykon Chagas
|
||||||
|
Melo46
|
||||||
|
Merike Sell
|
||||||
Michael Köhler
|
Michael Köhler
|
||||||
Michael Wolf
|
Michael Wolf
|
||||||
Michal Stanke
|
Michal Stanke
|
||||||
Michal Vašíček
|
Michal Vašíček
|
||||||
|
Mozilla Pontoon
|
||||||
Moḥend Belqasem
|
Moḥend Belqasem
|
||||||
|
Muḥend Belqasem
|
||||||
Nicholas Skinsacos
|
Nicholas Skinsacos
|
||||||
|
Nihad
|
||||||
|
Nihad Suljić
|
||||||
|
Oscar
|
||||||
Peter deHaan
|
Peter deHaan
|
||||||
Pierre Neter
|
Pierre Neter
|
||||||
Pin-guang Chen
|
Pin-guang Chen
|
||||||
|
Radu Popescu
|
||||||
Rhoslyn Prys
|
Rhoslyn Prys
|
||||||
|
RickieES
|
||||||
Rizky Ariestiyansyah
|
Rizky Ariestiyansyah
|
||||||
Roberto Alvarado
|
Roberto Alvarado
|
||||||
Rodrigo
|
Rodrigo
|
||||||
|
Rodrigo Guerra
|
||||||
Rok Žerdin
|
Rok Žerdin
|
||||||
Sahithi
|
Sahithi
|
||||||
Sairam Raavi
|
Sairam Raavi
|
||||||
|
Sander Lepik
|
||||||
Sandro
|
Sandro
|
||||||
|
Sara Todaro
|
||||||
|
Sav22999
|
||||||
|
Schieck :)
|
||||||
Selim Şumlu
|
Selim Şumlu
|
||||||
Slimane Amiri
|
Slimane Amiri
|
||||||
|
Soumya Himanish Mohapatra
|
||||||
|
Staś Małolepszy
|
||||||
|
Tema
|
||||||
|
Thomas Dalichow
|
||||||
Théo Chevalier
|
Théo Chevalier
|
||||||
|
Tiago Morais Morgado
|
||||||
Tomáš Zelina
|
Tomáš Zelina
|
||||||
Ton
|
Ton
|
||||||
Tymur Faradzhev
|
Tymur Faradzhev
|
||||||
|
Uccen Marzuq
|
||||||
|
Varghese Thomas
|
||||||
Victor Bychek
|
Victor Bychek
|
||||||
Weihang Lo
|
Weihang Lo
|
||||||
Wil Clouser
|
Wil Clouser
|
||||||
YFdyh000
|
YFdyh000
|
||||||
You-Wen Liang (Mark)
|
You-Wen Liang (Mark)
|
||||||
|
aefgh39622
|
||||||
|
albertdcastro
|
||||||
alex_mayorga
|
alex_mayorga
|
||||||
ariestiyansyah
|
ariestiyansyah
|
||||||
avelper
|
avelper
|
||||||
@@ -67,12 +125,26 @@ ehuggett
|
|||||||
eljuno
|
eljuno
|
||||||
erdem cobanoglu
|
erdem cobanoglu
|
||||||
gautamkrishnar
|
gautamkrishnar
|
||||||
|
gmontagu
|
||||||
goofy
|
goofy
|
||||||
|
hello
|
||||||
hi
|
hi
|
||||||
|
jesferman1993
|
||||||
|
jlG
|
||||||
|
josotrix
|
||||||
|
jspam
|
||||||
kenrick95
|
kenrick95
|
||||||
manxmensch
|
manxmensch
|
||||||
|
mirzet.omerovic.1992
|
||||||
ravmn
|
ravmn
|
||||||
|
reza.habibi2008
|
||||||
|
savemore99.sm
|
||||||
|
shikhar-scs
|
||||||
siparon
|
siparon
|
||||||
|
skystar-p
|
||||||
|
tiagomoraismorgado
|
||||||
xcffl
|
xcffl
|
||||||
|
ybouhamam
|
||||||
Μιχάλης
|
Μιχάλης
|
||||||
Марко Костић (Marko Kostić)
|
Марко Костић (Marko Kostić)
|
||||||
|
صفا الفليج
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ RUN npm install --production && npm cache clean --force
|
|||||||
ENV PORT=1443
|
ENV PORT=1443
|
||||||
EXPOSE $PORT
|
EXPOSE $PORT
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "run", "prod"]
|
||||||
|
|||||||
63
README.md
@@ -5,38 +5,79 @@
|
|||||||
|
|
||||||
**Docs:** [Docker](docs/docker.md), [Metrics](docs/metrics.md)
|
**Docs:** [Docker](docs/docker.md), [Metrics](docs/metrics.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
* [What it does](#what-it-does)
|
||||||
|
* [Requirements](#requirements)
|
||||||
|
* [Development](#development)
|
||||||
|
* [Commands](#commands)
|
||||||
|
* [Configuration](#configuration)
|
||||||
|
* [Localization](#localization)
|
||||||
|
* [Contributing](#contributing)
|
||||||
|
* [Testing](#testing)
|
||||||
|
* [License](#license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
A file sharing experiment which allows you to send encrypted files to other users.
|
A file sharing experiment which allows you to send encrypted files to other users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [Node.js 8+](https://nodejs.org/)
|
- [Node.js 8.2+](https://nodejs.org/)
|
||||||
- [Redis server](https://redis.io/)
|
- [Redis server](https://redis.io/) (optional for development)
|
||||||
|
- [AWS S3](https://aws.amazon.com/s3/) or compatible service. (optional)
|
||||||
|
|
||||||
**NOTE:** To run the project, make sure you have a Redis server running locally:
|
---
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
To start an ephemeral development server run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ redis-server /usr/local/etc/redis.conf
|
npm install
|
||||||
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use it
|
Then browse to http://localhost:8080
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|------------------|-------------|
|
|------------------|-------------|
|
||||||
| `npm run dev` | Builds and starts the web server locally for development.
|
|
||||||
| `npm run format` | Formats the frontend and server code using **prettier**.
|
| `npm run format` | Formats the frontend and server code using **prettier**.
|
||||||
| `npm run lint` | Lints the CSS and JavaScript code.
|
| `npm run lint` | Lints the CSS and JavaScript code.
|
||||||
| `npm start` | Starts the Express web server.
|
|
||||||
| `npm test` | Runs the suite of mocha tests.
|
| `npm test` | Runs the suite of mocha tests.
|
||||||
|
| `npm start` | Runs the server in development configuration.
|
||||||
|
| `npm run build` | Builds the production assets.
|
||||||
|
| `npm run prod` | Runs the server in production configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The server is configured with environment variables. See [server/config.js](server/config.js) for all options and [docs/docker.md](docs/docker.md) for examples.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Localization
|
## Localization
|
||||||
|
|
||||||
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language, or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Pull requests are always welcome! Feel free to check out the list of ["good first bugs"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+bug%22).
|
Pull requests are always welcome! Feel free to check out the list of ["good first issues"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@@ -46,6 +87,10 @@ Pull requests are always welcome! Feel free to check out the list of ["good firs
|
|||||||
| Stage | <https://send.stage.mozaws.net/>
|
| Stage | <https://send.stage.mozaws.net/>
|
||||||
| Development | <https://send.dev.mozaws.net/>
|
| Development | <https://send.dev.mozaws.net/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[Mozilla Public License Version 2.0](LICENSE)
|
[Mozilla Public License Version 2.0](LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
env:
|
env:
|
||||||
browser: true
|
browser: true
|
||||||
node: false
|
node: true
|
||||||
|
|
||||||
parserOptions:
|
parserOptions:
|
||||||
sourceType: module
|
sourceType: module
|
||||||
199
app/api.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
|
||||||
|
function post(obj) {
|
||||||
|
return {
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Headers({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}),
|
||||||
|
body: JSON.stringify(obj)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNonce(header) {
|
||||||
|
header = header || '';
|
||||||
|
return header.split(' ')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuth(url, params, keychain) {
|
||||||
|
const result = {};
|
||||||
|
params = params || {};
|
||||||
|
const h = await keychain.authHeader();
|
||||||
|
params.headers = new Headers({ Authorization: h });
|
||||||
|
const response = await fetch(url, params);
|
||||||
|
result.response = response;
|
||||||
|
result.ok = response.ok;
|
||||||
|
const nonce = parseNonce(response.headers.get('WWW-Authenticate'));
|
||||||
|
result.shouldRetry = response.status === 401 && nonce !== keychain.nonce;
|
||||||
|
keychain.nonce = nonce;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithAuthAndRetry(url, params, keychain) {
|
||||||
|
const result = await fetchWithAuth(url, params, keychain);
|
||||||
|
if (result.shouldRetry) {
|
||||||
|
return fetchWithAuth(url, params, keychain);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function del(id, owner_token) {
|
||||||
|
const response = await fetch(`/api/delete/${id}`, post({ owner_token }));
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setParams(id, owner_token, params) {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/params/${id}`,
|
||||||
|
post({
|
||||||
|
owner_token,
|
||||||
|
dlimit: params.dlimit
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fileInfo(id, owner_token) {
|
||||||
|
const response = await fetch(`/api/info/${id}`, post({ owner_token }));
|
||||||
|
if (response.ok) {
|
||||||
|
const obj = await response.json();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
throw new Error(response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function metadata(id, keychain) {
|
||||||
|
const result = await fetchWithAuthAndRetry(
|
||||||
|
`/api/metadata/${id}`,
|
||||||
|
{ method: 'GET' },
|
||||||
|
keychain
|
||||||
|
);
|
||||||
|
if (result.ok) {
|
||||||
|
const data = await result.response.json();
|
||||||
|
const meta = await keychain.decryptMetadata(b64ToArray(data.metadata));
|
||||||
|
return {
|
||||||
|
size: data.size,
|
||||||
|
ttl: data.ttl,
|
||||||
|
iv: meta.iv,
|
||||||
|
name: meta.name,
|
||||||
|
type: meta.type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(result.response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setPassword(id, owner_token, keychain) {
|
||||||
|
const auth = await keychain.authKeyB64();
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/password/${id}`,
|
||||||
|
post({ owner_token, auth })
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadFile(
|
||||||
|
encrypted,
|
||||||
|
metadata,
|
||||||
|
verifierB64,
|
||||||
|
keychain,
|
||||||
|
onprogress
|
||||||
|
) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const upload = {
|
||||||
|
cancel: function() {
|
||||||
|
xhr.abort();
|
||||||
|
},
|
||||||
|
result: new Promise(function(resolve, reject) {
|
||||||
|
xhr.addEventListener('loadend', function() {
|
||||||
|
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||||
|
if (authHeader) {
|
||||||
|
keychain.nonce = parseNonce(authHeader);
|
||||||
|
}
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const responseObj = JSON.parse(xhr.responseText);
|
||||||
|
return resolve({
|
||||||
|
url: responseObj.url,
|
||||||
|
id: responseObj.id,
|
||||||
|
ownerToken: responseObj.owner
|
||||||
|
});
|
||||||
|
}
|
||||||
|
reject(new Error(xhr.status));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
const dataView = new DataView(encrypted);
|
||||||
|
const blob = new Blob([dataView], { type: 'application/octet-stream' });
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('data', blob);
|
||||||
|
xhr.upload.addEventListener('progress', function(event) {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
onprogress([event.loaded, event.total]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
xhr.open('post', '/api/upload', true);
|
||||||
|
xhr.setRequestHeader('X-File-Metadata', arrayToB64(new Uint8Array(metadata)));
|
||||||
|
xhr.setRequestHeader('Authorization', `send-v1 ${verifierB64}`);
|
||||||
|
xhr.send(fd);
|
||||||
|
return upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function download(id, keychain, onprogress, canceller) {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
canceller.oncancel = function() {
|
||||||
|
xhr.abort();
|
||||||
|
};
|
||||||
|
return new Promise(async function(resolve, reject) {
|
||||||
|
xhr.addEventListener('loadend', function() {
|
||||||
|
canceller.oncancel = function() {};
|
||||||
|
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||||
|
if (authHeader) {
|
||||||
|
keychain.nonce = parseNonce(authHeader);
|
||||||
|
}
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
return reject(new Error(xhr.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([xhr.response]);
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.readAsArrayBuffer(blob);
|
||||||
|
fileReader.onload = function() {
|
||||||
|
resolve(this.result);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
xhr.addEventListener('progress', function(event) {
|
||||||
|
if (event.lengthComputable && event.target.status === 200) {
|
||||||
|
onprogress([event.loaded, event.total]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const auth = await keychain.authHeader();
|
||||||
|
xhr.open('get', `/api/download/${id}`);
|
||||||
|
xhr.setRequestHeader('Authorization', auth);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryDownload(id, keychain, onprogress, canceller, tries = 1) {
|
||||||
|
try {
|
||||||
|
const result = await download(id, keychain, onprogress, canceller);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401' && --tries > 0) {
|
||||||
|
return tryDownload(id, keychain, onprogress, canceller, tries);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadFile(id, keychain, onprogress) {
|
||||||
|
const canceller = {
|
||||||
|
oncancel: function() {} // download() sets this
|
||||||
|
};
|
||||||
|
function cancel() {
|
||||||
|
canceller.oncancel();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
cancel,
|
||||||
|
result: tryDownload(id, keychain, onprogress, canceller, 2)
|
||||||
|
};
|
||||||
|
}
|
||||||
267
app/base.css
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
:root {
|
||||||
|
--pageBGColor: #fff;
|
||||||
|
--primaryControlBGColor: #0297f8;
|
||||||
|
--primaryControlFGColor: #fff;
|
||||||
|
--primaryControlHoverColor: #0287e8;
|
||||||
|
--inputTextColor: #737373;
|
||||||
|
--errorColor: #d70022;
|
||||||
|
--linkColor: #0094fb;
|
||||||
|
--textColor: #0c0c0d;
|
||||||
|
--lightTextColor: #737373;
|
||||||
|
--successControlBGColor: #05a700;
|
||||||
|
--successControlFGColor: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: url('../assets/send_bg.svg');
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||||
|
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||||
|
font-weight: 200;
|
||||||
|
background-size: 110%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center top;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||||
|
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: auto;
|
||||||
|
max-width: 650px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 96%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noscript {
|
||||||
|
text-align: center;
|
||||||
|
border: 3px solid var(--errorColor);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--primaryControlBGColor);
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--cancel {
|
||||||
|
color: var(--errorColor);
|
||||||
|
background: var(--pageBGColor);
|
||||||
|
font-size: 15px;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--cancel:disabled {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--cancel:hover {
|
||||||
|
background-color: var(--pageBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 2 0 auto;
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--inputTextColor);
|
||||||
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 23px;
|
||||||
|
font-weight: 300;
|
||||||
|
height: 46px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--error {
|
||||||
|
border-color: var(--errorColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--noBtn {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn {
|
||||||
|
flex: auto;
|
||||||
|
background: var(--primaryControlBGColor);
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
/* Force flat button look */
|
||||||
|
appearance: none;
|
||||||
|
font-size: 15px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn:disabled {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn:hover {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor--pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--linkColor);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:focus,
|
||||||
|
.link:active,
|
||||||
|
.link:hover {
|
||||||
|
color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link--action {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
margin: 0 auto 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressSection {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressSection__text {
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
letter-spacing: -0.4px;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 74px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect--fadeOut {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeout 200ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect--fadeIn {
|
||||||
|
opacity: 1;
|
||||||
|
animation: fadein 200ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--errorColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 33px;
|
||||||
|
line-height: 40px;
|
||||||
|
margin: 20px auto;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 520px;
|
||||||
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 23px;
|
||||||
|
max-width: 630px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto 60px;
|
||||||
|
color: var(--textColor);
|
||||||
|
width: 92%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 768px), (max-width: 768px) {
|
||||||
|
.description {
|
||||||
|
margin: 0 auto 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.input {
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 10px 10px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn {
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
flex: 0 1 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--noBtn {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/dragManager.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/* global MAXFILESIZE */
|
||||||
|
const { bytes } = require('./utils');
|
||||||
|
|
||||||
|
export default function(state, emitter) {
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
document.body.addEventListener('dragover', event => {
|
||||||
|
if (state.route === '/') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.body.addEventListener('drop', event => {
|
||||||
|
if (state.route === '/' && !state.uploading) {
|
||||||
|
event.preventDefault();
|
||||||
|
document
|
||||||
|
.querySelector('.uploadArea')
|
||||||
|
.classList.remove('uploadArea--dragging');
|
||||||
|
const target = event.dataTransfer;
|
||||||
|
if (target.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.files.length > 1) {
|
||||||
|
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
||||||
|
}
|
||||||
|
const file = target.files[0];
|
||||||
|
if (file.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > MAXFILESIZE) {
|
||||||
|
window.alert(
|
||||||
|
state.translate('fileTooBig', { size: bytes(MAXFILESIZE) })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emitter.emit('upload', { file, type: 'drop' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
82
app/experiments.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import hash from 'string-hash';
|
||||||
|
|
||||||
|
const experiments = {
|
||||||
|
S9wqVl2SQ4ab2yZtqDI3Dw: {
|
||||||
|
id: 'S9wqVl2SQ4ab2yZtqDI3Dw',
|
||||||
|
run: function(variant, state, emitter) {
|
||||||
|
switch (variant) {
|
||||||
|
case 1:
|
||||||
|
state.promo = 'blue';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
state.promo = 'pink';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state.promo = 'grey';
|
||||||
|
}
|
||||||
|
emitter.emit('render');
|
||||||
|
},
|
||||||
|
eligible: function() {
|
||||||
|
return (
|
||||||
|
!/firefox|fxios/i.test(navigator.userAgent) &&
|
||||||
|
document.querySelector('html').lang === 'en-US'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
variant: function(state) {
|
||||||
|
const n = this.luckyNumber(state);
|
||||||
|
if (n < 0.33) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return n < 0.66 ? 1 : 2;
|
||||||
|
},
|
||||||
|
luckyNumber: function(state) {
|
||||||
|
return luckyNumber(
|
||||||
|
`${this.id}:${state.storage.get('testpilot_ga__cid')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Returns a number between 0 and 1
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
function luckyNumber(str) {
|
||||||
|
return hash(str) / 0xffffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkExperiments(state, emitter) {
|
||||||
|
const all = Object.keys(experiments);
|
||||||
|
const id = all.find(id => experiments[id].eligible(state));
|
||||||
|
if (id) {
|
||||||
|
const variant = experiments[id].variant(state);
|
||||||
|
state.storage.enroll(id, variant);
|
||||||
|
experiments[id].run(variant, state, emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function initialize(state, emitter) {
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
const xp = experiments[state.query.x];
|
||||||
|
if (xp) {
|
||||||
|
xp.run(+state.query.v, state, emitter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!state.storage.get('testpilot_ga__cid')) {
|
||||||
|
// first ever visit. check again after cid is assigned.
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
checkExperiments(state, emitter);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const enrolled = state.storage.enrolled.filter(([id, variant]) => {
|
||||||
|
const xp = experiments[id];
|
||||||
|
if (xp) {
|
||||||
|
xp.run(variant, state, emitter);
|
||||||
|
}
|
||||||
|
return !!xp;
|
||||||
|
});
|
||||||
|
// single experiment per session for now
|
||||||
|
if (enrolled.length === 0) {
|
||||||
|
checkExperiments(state, emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
app/fileManager.js
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import FileSender from './fileSender';
|
||||||
|
import FileReceiver from './fileReceiver';
|
||||||
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
delay,
|
||||||
|
fadeOut,
|
||||||
|
openLinksInNewTab,
|
||||||
|
percent
|
||||||
|
} from './utils';
|
||||||
|
import * as metrics from './metrics';
|
||||||
|
|
||||||
|
export default function(state, emitter) {
|
||||||
|
let lastRender = 0;
|
||||||
|
let updateTitle = false;
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
emitter.emit('render');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkFiles() {
|
||||||
|
const files = state.storage.files.slice();
|
||||||
|
let rerender = false;
|
||||||
|
for (const file of files) {
|
||||||
|
const oldLimit = file.dlimit;
|
||||||
|
const oldTotal = file.dtotal;
|
||||||
|
await file.updateDownloadCount();
|
||||||
|
if (file.dtotal === file.dlimit) {
|
||||||
|
state.storage.remove(file.id);
|
||||||
|
rerender = true;
|
||||||
|
} else if (oldLimit !== file.dlimit || oldTotal !== file.dtotal) {
|
||||||
|
rerender = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rerender) {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress() {
|
||||||
|
if (updateTitle) {
|
||||||
|
emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
document.addEventListener('blur', () => (updateTitle = true));
|
||||||
|
document.addEventListener('focus', () => {
|
||||||
|
updateTitle = false;
|
||||||
|
emitter.emit('DOMTitleChange', 'Firefox Send');
|
||||||
|
});
|
||||||
|
checkFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('navigate', checkFiles);
|
||||||
|
|
||||||
|
emitter.on('render', () => {
|
||||||
|
lastRender = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('changeLimit', async ({ file, value }) => {
|
||||||
|
await file.changeLimit(value);
|
||||||
|
state.storage.writeFile(file);
|
||||||
|
metrics.changedDownloadLimit(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('delete', async ({ file, location }) => {
|
||||||
|
try {
|
||||||
|
metrics.deletedUpload({
|
||||||
|
size: file.size,
|
||||||
|
time: file.time,
|
||||||
|
speed: file.speed,
|
||||||
|
type: file.type,
|
||||||
|
ttl: file.expiresAt - Date.now(),
|
||||||
|
location
|
||||||
|
});
|
||||||
|
state.storage.remove(file.id);
|
||||||
|
await file.del();
|
||||||
|
} catch (e) {
|
||||||
|
state.raven.captureException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('cancel', () => {
|
||||||
|
state.transfer.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('upload', async ({ file, type }) => {
|
||||||
|
const size = file.size;
|
||||||
|
const sender = new FileSender(file);
|
||||||
|
sender.on('progress', updateProgress);
|
||||||
|
sender.on('encrypting', render);
|
||||||
|
state.transfer = sender;
|
||||||
|
state.uploading = true;
|
||||||
|
render();
|
||||||
|
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
await delay(200);
|
||||||
|
try {
|
||||||
|
metrics.startedUpload({ size, type });
|
||||||
|
const ownedFile = await sender.upload();
|
||||||
|
ownedFile.type = type;
|
||||||
|
state.storage.totalUploads += 1;
|
||||||
|
metrics.completedUpload(ownedFile);
|
||||||
|
|
||||||
|
state.storage.addFile(ownedFile);
|
||||||
|
|
||||||
|
document.getElementById('cancel-upload').hidden = 'hidden';
|
||||||
|
await delay(1000);
|
||||||
|
await fadeOut('.page');
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
emitter.emit('pushState', `/share/${ownedFile.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
if (err.message === '0') {
|
||||||
|
//cancelled. do nothing
|
||||||
|
metrics.cancelledUpload({ size, type });
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedUpload({ size, type, err });
|
||||||
|
emitter.emit('pushState', '/error');
|
||||||
|
} finally {
|
||||||
|
state.uploading = false;
|
||||||
|
state.transfer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('password', async ({ password, file }) => {
|
||||||
|
try {
|
||||||
|
state.settingPassword = true;
|
||||||
|
render();
|
||||||
|
await file.setPassword(password);
|
||||||
|
state.storage.writeFile(file);
|
||||||
|
metrics.addedPassword({ size: file.size });
|
||||||
|
await delay(1000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
state.passwordSetError = err;
|
||||||
|
} finally {
|
||||||
|
state.settingPassword = false;
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('getMetadata', async () => {
|
||||||
|
const file = state.fileInfo;
|
||||||
|
const receiver = new FileReceiver(file);
|
||||||
|
try {
|
||||||
|
await receiver.getMetadata();
|
||||||
|
state.transfer = receiver;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '401') {
|
||||||
|
file.password = null;
|
||||||
|
if (!file.requiresPassword) {
|
||||||
|
return emitter.emit('pushState', '/404');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('download', async file => {
|
||||||
|
state.transfer.on('progress', updateProgress);
|
||||||
|
state.transfer.on('decrypting', render);
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
const size = file.size;
|
||||||
|
try {
|
||||||
|
const start = Date.now();
|
||||||
|
metrics.startedDownload({ size: file.size, ttl: file.ttl });
|
||||||
|
const dl = state.transfer.download();
|
||||||
|
render();
|
||||||
|
await dl;
|
||||||
|
const time = Date.now() - start;
|
||||||
|
const speed = size / (time / 1000);
|
||||||
|
await delay(1000);
|
||||||
|
await fadeOut('.page');
|
||||||
|
state.storage.totalDownloads += 1;
|
||||||
|
state.transfer.reset();
|
||||||
|
metrics.completedDownload({ size, time, speed });
|
||||||
|
emitter.emit('pushState', '/completed');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message === '0') {
|
||||||
|
// download cancelled
|
||||||
|
state.transfer.reset();
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
console.error(err);
|
||||||
|
state.transfer = null;
|
||||||
|
const location = err.message === '404' ? '/404' : '/error';
|
||||||
|
if (location === '/error') {
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedDownload({ size, err });
|
||||||
|
}
|
||||||
|
emitter.emit('pushState', location);
|
||||||
|
} finally {
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('copy', ({ url, location }) => {
|
||||||
|
copyToClipboard(url);
|
||||||
|
metrics.copiedLink({ location });
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// poll for updates of the download counts
|
||||||
|
// TODO something for the share page: || state.route === '/share/:id'
|
||||||
|
if (state.route === '/') {
|
||||||
|
checkFiles();
|
||||||
|
}
|
||||||
|
}, 2 * 60 * 1000);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// poll for rerendering the file list countdown timers
|
||||||
|
if (
|
||||||
|
state.route === '/' &&
|
||||||
|
state.storage.files.length > 0 &&
|
||||||
|
Date.now() - lastRender > 30000
|
||||||
|
) {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
124
app/fileReceiver.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import Nanobus from 'nanobus';
|
||||||
|
import Keychain from './keychain';
|
||||||
|
import { bytes } from './utils';
|
||||||
|
import { metadata, downloadFile } from './api';
|
||||||
|
|
||||||
|
export default class FileReceiver extends Nanobus {
|
||||||
|
constructor(fileInfo) {
|
||||||
|
super('FileReceiver');
|
||||||
|
this.keychain = new Keychain(fileInfo.secretKey, fileInfo.nonce);
|
||||||
|
if (fileInfo.requiresPassword) {
|
||||||
|
this.keychain.setPassword(fileInfo.password, fileInfo.url);
|
||||||
|
}
|
||||||
|
this.fileInfo = fileInfo;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
get progressRatio() {
|
||||||
|
return this.progress[0] / this.progress[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get progressIndefinite() {
|
||||||
|
return this.state !== 'downloading';
|
||||||
|
}
|
||||||
|
|
||||||
|
get sizes() {
|
||||||
|
return {
|
||||||
|
partialSize: bytes(this.progress[0]),
|
||||||
|
totalSize: bytes(this.progress[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
if (this.downloadRequest) {
|
||||||
|
this.downloadRequest.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
|
this.state = 'initialized';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata() {
|
||||||
|
const meta = await metadata(this.fileInfo.id, this.keychain);
|
||||||
|
this.keychain.setIV(meta.iv);
|
||||||
|
this.fileInfo.name = meta.name;
|
||||||
|
this.fileInfo.type = meta.type;
|
||||||
|
this.fileInfo.iv = meta.iv;
|
||||||
|
this.fileInfo.size = meta.size;
|
||||||
|
this.state = 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(noSave = false) {
|
||||||
|
this.state = 'downloading';
|
||||||
|
this.downloadRequest = await downloadFile(
|
||||||
|
this.fileInfo.id,
|
||||||
|
this.keychain,
|
||||||
|
p => {
|
||||||
|
this.progress = p;
|
||||||
|
this.emit('progress');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const ciphertext = await this.downloadRequest.result;
|
||||||
|
this.downloadRequest = null;
|
||||||
|
this.msg = 'decryptingFile';
|
||||||
|
this.state = 'decrypting';
|
||||||
|
this.emit('decrypting');
|
||||||
|
const plaintext = await this.keychain.decryptFile(ciphertext);
|
||||||
|
if (!noSave) {
|
||||||
|
await saveFile({
|
||||||
|
plaintext,
|
||||||
|
name: decodeURIComponent(this.fileInfo.name),
|
||||||
|
type: this.fileInfo.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.msg = 'downloadFinish';
|
||||||
|
this.state = 'complete';
|
||||||
|
} catch (e) {
|
||||||
|
this.downloadRequest = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFile(file) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const dataView = new DataView(file.plaintext);
|
||||||
|
const blob = new Blob([dataView], { type: file.type });
|
||||||
|
|
||||||
|
if (navigator.msSaveBlob) {
|
||||||
|
navigator.msSaveBlob(blob, file.name);
|
||||||
|
return resolve();
|
||||||
|
} else if (/iPhone|fxios/i.test(navigator.userAgent)) {
|
||||||
|
// This method is much slower but createObjectURL
|
||||||
|
// is buggy on iOS
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('loadend', function() {
|
||||||
|
if (reader.error) {
|
||||||
|
return reject(reader.error);
|
||||||
|
}
|
||||||
|
if (reader.result) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = reader.result;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
} else {
|
||||||
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
112
app/fileSender.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/* global EXPIRE_SECONDS */
|
||||||
|
import Nanobus from 'nanobus';
|
||||||
|
import OwnedFile from './ownedFile';
|
||||||
|
import Keychain from './keychain';
|
||||||
|
import { arrayToB64, bytes } from './utils';
|
||||||
|
import { uploadFile } from './api';
|
||||||
|
|
||||||
|
export default class FileSender extends Nanobus {
|
||||||
|
constructor(file) {
|
||||||
|
super('FileSender');
|
||||||
|
this.file = file;
|
||||||
|
this.keychain = new Keychain();
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
get progressRatio() {
|
||||||
|
return this.progress[0] / this.progress[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get progressIndefinite() {
|
||||||
|
return ['fileSizeProgress', 'notifyUploadDone'].indexOf(this.msg) === -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sizes() {
|
||||||
|
return {
|
||||||
|
partialSize: bytes(this.progress[0]),
|
||||||
|
totalSize: bytes(this.progress[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.uploadRequest = null;
|
||||||
|
this.msg = 'importingFile';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
this.cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.cancelled = true;
|
||||||
|
if (this.uploadRequest) {
|
||||||
|
this.uploadRequest.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readFile() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsArrayBuffer(this.file);
|
||||||
|
// TODO: progress?
|
||||||
|
reader.onload = function(event) {
|
||||||
|
const plaintext = new Uint8Array(this.result);
|
||||||
|
resolve(plaintext);
|
||||||
|
};
|
||||||
|
reader.onerror = function(err) {
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload() {
|
||||||
|
const start = Date.now();
|
||||||
|
const plaintext = await this.readFile();
|
||||||
|
if (this.cancelled) {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
|
this.msg = 'encryptingFile';
|
||||||
|
this.emit('encrypting');
|
||||||
|
const encrypted = await this.keychain.encryptFile(plaintext);
|
||||||
|
const metadata = await this.keychain.encryptMetadata(this.file);
|
||||||
|
const authKeyB64 = await this.keychain.authKeyB64();
|
||||||
|
if (this.cancelled) {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
|
this.uploadRequest = uploadFile(
|
||||||
|
encrypted,
|
||||||
|
metadata,
|
||||||
|
authKeyB64,
|
||||||
|
this.keychain,
|
||||||
|
p => {
|
||||||
|
this.progress = p;
|
||||||
|
this.emit('progress', p);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
|
try {
|
||||||
|
const result = await this.uploadRequest.result;
|
||||||
|
const time = Date.now() - start;
|
||||||
|
this.msg = 'notifyUploadDone';
|
||||||
|
this.uploadRequest = null;
|
||||||
|
this.progress = [1, 1];
|
||||||
|
const secretKey = arrayToB64(this.keychain.rawSecret);
|
||||||
|
const ownedFile = new OwnedFile({
|
||||||
|
id: result.id,
|
||||||
|
url: `${result.url}#${secretKey}`,
|
||||||
|
name: this.file.name,
|
||||||
|
size: this.file.size,
|
||||||
|
time: time,
|
||||||
|
speed: this.file.size / (time / 1000),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + EXPIRE_SECONDS * 1000,
|
||||||
|
secretKey: secretKey,
|
||||||
|
nonce: this.keychain.nonce,
|
||||||
|
ownerToken: result.ownerToken
|
||||||
|
});
|
||||||
|
return ownedFile;
|
||||||
|
} catch (e) {
|
||||||
|
this.msg = 'errorPageHeader';
|
||||||
|
this.uploadRequest = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
209
app/keychain.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
export default class Keychain {
|
||||||
|
constructor(secretKeyB64, nonce, ivB64) {
|
||||||
|
this._nonce = nonce || 'yRCdyQ1EMSA3mo4rqSkuNQ==';
|
||||||
|
if (ivB64) {
|
||||||
|
this.iv = b64ToArray(ivB64);
|
||||||
|
} else {
|
||||||
|
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
}
|
||||||
|
if (secretKeyB64) {
|
||||||
|
this.rawSecret = b64ToArray(secretKeyB64);
|
||||||
|
} else {
|
||||||
|
this.rawSecret = window.crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
}
|
||||||
|
this.secretKeyPromise = window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
this.rawSecret,
|
||||||
|
'HKDF',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
this.encryptKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('encryption'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('metadata'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.authKeyPromise = this.secretKeyPromise.then(function(secretKey) {
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'HKDF',
|
||||||
|
salt: new Uint8Array(),
|
||||||
|
info: encoder.encode('authentication'),
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
secretKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: { name: 'SHA-256' }
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get nonce() {
|
||||||
|
return this._nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
set nonce(n) {
|
||||||
|
if (n && n !== this._nonce) {
|
||||||
|
this._nonce = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIV(ivB64) {
|
||||||
|
this.iv = b64ToArray(ivB64);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPassword(password, shareUrl) {
|
||||||
|
this.authKeyPromise = window.crypto.subtle
|
||||||
|
.importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
|
||||||
|
'deriveKey'
|
||||||
|
])
|
||||||
|
.then(passwordKey =>
|
||||||
|
window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: encoder.encode(shareUrl),
|
||||||
|
iterations: 100,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
passwordKey,
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthKey(authKeyB64) {
|
||||||
|
this.authKeyPromise = window.crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
b64ToArray(authKeyB64),
|
||||||
|
{
|
||||||
|
name: 'HMAC',
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async authKeyB64() {
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const rawAuth = await window.crypto.subtle.exportKey('raw', authKey);
|
||||||
|
return arrayToB64(new Uint8Array(rawAuth));
|
||||||
|
}
|
||||||
|
|
||||||
|
async authHeader() {
|
||||||
|
const authKey = await this.authKeyPromise;
|
||||||
|
const sig = await window.crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: 'HMAC'
|
||||||
|
},
|
||||||
|
authKey,
|
||||||
|
b64ToArray(this.nonce)
|
||||||
|
);
|
||||||
|
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptFile(plaintext) {
|
||||||
|
const encryptKey = await this.encryptKeyPromise;
|
||||||
|
const ciphertext = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: this.iv,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
encryptKey,
|
||||||
|
plaintext
|
||||||
|
);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptMetadata(metadata) {
|
||||||
|
const metaKey = await this.metaKeyPromise;
|
||||||
|
const ciphertext = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
encoder.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
iv: arrayToB64(this.iv),
|
||||||
|
name: metadata.name,
|
||||||
|
type: metadata.type || 'application/octet-stream'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptFile(ciphertext) {
|
||||||
|
const encryptKey = await this.encryptKeyPromise;
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: this.iv,
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
encryptKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptMetadata(ciphertext) {
|
||||||
|
const metaKey = await this.metaKeyPromise;
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: new Uint8Array(12),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
metaKey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
return JSON.parse(decoder.decode(plaintext));
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/main.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@import './base.css';
|
||||||
|
@import './templates/header/header.css';
|
||||||
|
@import './templates/downloadButton/downloadButton.css';
|
||||||
|
@import './templates/progress/progress.css';
|
||||||
|
@import './templates/passwordInput/passwordInput.css';
|
||||||
|
@import './templates/downloadPassword/downloadPassword.css';
|
||||||
|
@import './templates/setPasswordSection/setPasswordSection.css';
|
||||||
|
@import './templates/footer/footer.css';
|
||||||
|
@import './templates/fxPromo/fxPromo.css';
|
||||||
|
@import './templates/selectbox/selectbox.css';
|
||||||
|
@import './templates/fileList/fileList.css';
|
||||||
|
@import './templates/file/file.css';
|
||||||
|
@import './templates/popup/popup.css';
|
||||||
|
@import './pages/welcome/welcome.css';
|
||||||
|
@import './pages/share/share.css';
|
||||||
|
@import './pages/unsupported/unsupported.css';
|
||||||
55
app/main.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import 'fluent-intl-polyfill';
|
||||||
|
import app from './routes';
|
||||||
|
import locale from '../common/locales';
|
||||||
|
import fileManager from './fileManager';
|
||||||
|
import dragManager from './dragManager';
|
||||||
|
import { canHasSend } from './utils';
|
||||||
|
import assets from '../common/assets';
|
||||||
|
import storage from './storage';
|
||||||
|
import metrics from './metrics';
|
||||||
|
import experiments from './experiments';
|
||||||
|
import Raven from 'raven-js';
|
||||||
|
|
||||||
|
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||||
|
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use((state, emitter) => {
|
||||||
|
state.transfer = null;
|
||||||
|
state.fileInfo = null;
|
||||||
|
state.translate = locale.getTranslator();
|
||||||
|
state.storage = storage;
|
||||||
|
state.raven = Raven;
|
||||||
|
window.appState = state;
|
||||||
|
emitter.on('DOMContentLoaded', async function checkSupport() {
|
||||||
|
let unsupportedReason = null;
|
||||||
|
if (
|
||||||
|
// Firefox < 50
|
||||||
|
/firefox/i.test(navigator.userAgent) &&
|
||||||
|
parseInt(navigator.userAgent.match(/firefox\/*([^\n\r]*)\./i)[1], 10) < 50
|
||||||
|
) {
|
||||||
|
unsupportedReason = 'outdated';
|
||||||
|
}
|
||||||
|
if (/edge\/\d+/i.test(navigator.userAgent)) {
|
||||||
|
unsupportedReason = 'edge';
|
||||||
|
}
|
||||||
|
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||||
|
if (!ok) {
|
||||||
|
unsupportedReason = /firefox/i.test(navigator.userAgent)
|
||||||
|
? 'outdated'
|
||||||
|
: 'gcm';
|
||||||
|
}
|
||||||
|
if (unsupportedReason) {
|
||||||
|
setTimeout(() =>
|
||||||
|
emitter.emit('replaceState', `/unsupported/${unsupportedReason}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(metrics);
|
||||||
|
app.use(fileManager);
|
||||||
|
app.use(dragManager);
|
||||||
|
app.use(experiments);
|
||||||
|
|
||||||
|
app.mount('body');
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
||||||
import Storage from './storage';
|
import storage from './storage';
|
||||||
const storage = new Storage();
|
|
||||||
|
|
||||||
let hasLocalStorage = false;
|
let hasLocalStorage = false;
|
||||||
try {
|
try {
|
||||||
hasLocalStorage = !!localStorage;
|
hasLocalStorage = typeof localStorage !== 'undefined';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// don't care
|
// when disabled, any mention of localStorage throws an error
|
||||||
}
|
}
|
||||||
|
|
||||||
const analytics = new testPilotGA({
|
const analytics = new testPilotGA({
|
||||||
@@ -15,19 +14,47 @@ const analytics = new testPilotGA({
|
|||||||
tid: window.GOOGLE_ANALYTICS_ID
|
tid: window.GOOGLE_ANALYTICS_ID
|
||||||
});
|
});
|
||||||
|
|
||||||
const category = location.pathname.includes('/download')
|
let appState = null;
|
||||||
? 'recipient'
|
let experiment = null;
|
||||||
: 'sender';
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
export default function initialize(state, emitter) {
|
||||||
addExitHandlers();
|
appState = state;
|
||||||
addRestartHandlers();
|
emitter.on('DOMContentLoaded', () => {
|
||||||
});
|
addExitHandlers();
|
||||||
|
experiment = storage.enrolled[0];
|
||||||
|
sendEvent(category(), 'visit', {
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: storage.files.length,
|
||||||
|
cm7: storage.totalDownloads
|
||||||
|
});
|
||||||
|
//TODO restart handlers... somewhere
|
||||||
|
});
|
||||||
|
emitter.on('exit', exitEvent);
|
||||||
|
emitter.on('experiment', experimentEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function category() {
|
||||||
|
switch (appState.route) {
|
||||||
|
case '/':
|
||||||
|
case '/share/:id':
|
||||||
|
return 'sender';
|
||||||
|
case '/download/:id/:key':
|
||||||
|
case '/download/:id':
|
||||||
|
case '/completed':
|
||||||
|
return 'recipient';
|
||||||
|
default:
|
||||||
|
return 'other';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sendEvent() {
|
function sendEvent() {
|
||||||
|
const args = Array.from(arguments);
|
||||||
|
if (experiment && args[2]) {
|
||||||
|
args[2].xid = experiment[0];
|
||||||
|
args[2].xvar = experiment[1];
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
hasLocalStorage &&
|
hasLocalStorage && analytics.sendEvent.apply(analytics, args).catch(() => 0)
|
||||||
analytics.sendEvent.apply(analytics, arguments).catch(() => 0)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,17 +83,19 @@ function urlToMetric(url) {
|
|||||||
case 'https://testpilot.firefox.com/':
|
case 'https://testpilot.firefox.com/':
|
||||||
case 'https://testpilot.firefox.com/experiments/send':
|
case 'https://testpilot.firefox.com/experiments/send':
|
||||||
return 'testpilot';
|
return 'testpilot';
|
||||||
|
case 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com':
|
||||||
|
return 'promo';
|
||||||
default:
|
default:
|
||||||
return 'other';
|
return 'other';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setReferrer(state) {
|
function setReferrer(state) {
|
||||||
if (category === 'sender') {
|
if (category() === 'sender') {
|
||||||
if (state) {
|
if (state) {
|
||||||
storage.referrer = `${state}-upload`;
|
storage.referrer = `${state}-upload`;
|
||||||
}
|
}
|
||||||
} else if (category === 'recipient') {
|
} else if (category() === 'recipient') {
|
||||||
if (state) {
|
if (state) {
|
||||||
storage.referrer = `${state}-download`;
|
storage.referrer = `${state}-download`;
|
||||||
}
|
}
|
||||||
@@ -87,10 +116,10 @@ function takeReferrer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startedUpload(params) {
|
function startedUpload(params) {
|
||||||
return sendEvent(category, 'upload-started', {
|
return sendEvent('sender', 'upload-started', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles + 1,
|
cm6: storage.files.length + 1,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd5: takeReferrer()
|
cd5: takeReferrer()
|
||||||
@@ -99,10 +128,10 @@ function startedUpload(params) {
|
|||||||
|
|
||||||
function cancelledUpload(params) {
|
function cancelledUpload(params) {
|
||||||
setReferrer('cancelled');
|
setReferrer('cancelled');
|
||||||
return sendEvent(category, 'upload-stopped', {
|
return sendEvent('sender', 'upload-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd2: 'cancelled'
|
cd2: 'cancelled'
|
||||||
@@ -110,33 +139,42 @@ function cancelledUpload(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function completedUpload(params) {
|
function completedUpload(params) {
|
||||||
return sendEvent(category, 'upload-stopped', {
|
return sendEvent('sender', 'upload-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm2: params.time,
|
cm2: params.time,
|
||||||
cm3: params.speed,
|
cm3: params.speed,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd2: 'completed'
|
cd2: 'completed'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addedPassword(params) {
|
||||||
|
return sendEvent('sender', 'password-added', {
|
||||||
|
cm1: params.size,
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: storage.files.length,
|
||||||
|
cm7: storage.totalDownloads
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function startedDownload(params) {
|
function startedDownload(params) {
|
||||||
return sendEvent(category, 'download-started', {
|
return sendEvent('recipient', 'download-started', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm4: params.ttl,
|
cm4: params.ttl,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads
|
cm7: storage.totalDownloads
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stoppedDownload(params) {
|
function stoppedDownload(params) {
|
||||||
return sendEvent(category, 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd2: 'errored',
|
cd2: 'errored',
|
||||||
cd6: params.err
|
cd6: params.err
|
||||||
@@ -145,20 +183,20 @@ function stoppedDownload(params) {
|
|||||||
|
|
||||||
function cancelledDownload(params) {
|
function cancelledDownload(params) {
|
||||||
setReferrer('cancelled');
|
setReferrer('cancelled');
|
||||||
return sendEvent(category, 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd2: 'cancelled'
|
cd2: 'cancelled'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stoppedUpload(params) {
|
function stoppedUpload(params) {
|
||||||
return sendEvent(category, 'upload-stopped', {
|
return sendEvent('sender', 'upload-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd2: 'errored',
|
cd2: 'errored',
|
||||||
@@ -166,26 +204,36 @@ function stoppedUpload(params) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changedDownloadLimit(params) {
|
||||||
|
return sendEvent('sender', 'download-limit-changed', {
|
||||||
|
cm1: params.size,
|
||||||
|
cm5: storage.totalUploads,
|
||||||
|
cm6: storage.files.length,
|
||||||
|
cm7: storage.totalDownloads,
|
||||||
|
cm8: params.dlimit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function completedDownload(params) {
|
function completedDownload(params) {
|
||||||
return sendEvent(category, 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm2: params.time,
|
cm2: params.time,
|
||||||
cm3: params.speed,
|
cm3: params.speed,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd2: 'completed'
|
cd2: 'completed'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletedUpload(params) {
|
function deletedUpload(params) {
|
||||||
return sendEvent(category, 'upload-deleted', {
|
return sendEvent(category(), 'upload-deleted', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm2: params.time,
|
cm2: params.time,
|
||||||
cm3: params.speed,
|
cm3: params.speed,
|
||||||
cm4: params.ttl,
|
cm4: params.ttl,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd4: params.location
|
cd4: params.location
|
||||||
@@ -193,23 +241,28 @@ function deletedUpload(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unsupported(params) {
|
function unsupported(params) {
|
||||||
return sendEvent(category, 'unsupported', {
|
return sendEvent(category(), 'unsupported', {
|
||||||
cd6: params.err
|
cd6: params.err
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copiedLink(params) {
|
function copiedLink(params) {
|
||||||
return sendEvent(category, 'copied', {
|
return sendEvent('sender', 'copied', {
|
||||||
cd4: params.location
|
cd4: params.location
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitEvent(target) {
|
function exitEvent(target) {
|
||||||
return sendEvent(category, 'exited', {
|
return sendEvent(category(), 'exited', {
|
||||||
cd3: urlToMetric(target.currentTarget.href)
|
cd3: urlToMetric(target.currentTarget.href)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function experimentEvent(params) {
|
||||||
|
return sendEvent(category(), 'experiment', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
function addExitHandlers() {
|
function addExitHandlers() {
|
||||||
const links = Array.from(document.querySelectorAll('a'));
|
const links = Array.from(document.querySelectorAll('a'));
|
||||||
links.forEach(l => {
|
links.forEach(l => {
|
||||||
@@ -219,31 +272,26 @@ function addExitHandlers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartEvent(state) {
|
function restart(state) {
|
||||||
setReferrer(state);
|
setReferrer(state);
|
||||||
return sendEvent(category, 'restarted', {
|
return sendEvent(category(), 'restarted', {
|
||||||
cd2: state
|
cd2: state
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRestartHandlers() {
|
|
||||||
const elements = Array.from(document.querySelectorAll('.send-new'));
|
|
||||||
elements.forEach(el => {
|
|
||||||
const state = el.getAttribute('data-state');
|
|
||||||
el.addEventListener('click', restartEvent.bind(null, state));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
copiedLink,
|
copiedLink,
|
||||||
startedUpload,
|
startedUpload,
|
||||||
cancelledUpload,
|
cancelledUpload,
|
||||||
stoppedUpload,
|
stoppedUpload,
|
||||||
completedUpload,
|
completedUpload,
|
||||||
|
changedDownloadLimit,
|
||||||
deletedUpload,
|
deletedUpload,
|
||||||
startedDownload,
|
startedDownload,
|
||||||
cancelledDownload,
|
cancelledDownload,
|
||||||
stoppedDownload,
|
stoppedDownload,
|
||||||
completedDownload,
|
completedDownload,
|
||||||
|
addedPassword,
|
||||||
|
restart,
|
||||||
unsupported
|
unsupported
|
||||||
};
|
};
|
||||||
84
app/ownedFile.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import Keychain from './keychain';
|
||||||
|
import { arrayToB64 } from './utils';
|
||||||
|
import { del, fileInfo, setParams, setPassword } from './api';
|
||||||
|
|
||||||
|
export default class OwnedFile {
|
||||||
|
constructor(obj) {
|
||||||
|
this.id = obj.id;
|
||||||
|
this.url = obj.url;
|
||||||
|
this.name = obj.name;
|
||||||
|
this.size = obj.size;
|
||||||
|
this.type = obj.type;
|
||||||
|
this.time = obj.time;
|
||||||
|
this.speed = obj.speed;
|
||||||
|
this.createdAt = obj.createdAt;
|
||||||
|
this.expiresAt = obj.expiresAt;
|
||||||
|
this.ownerToken = obj.ownerToken;
|
||||||
|
this.dlimit = obj.dlimit || 1;
|
||||||
|
this.dtotal = obj.dtotal || 0;
|
||||||
|
this.keychain = new Keychain(obj.secretKey, obj.nonce);
|
||||||
|
this._hasPassword = !!obj.hasPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPassword(password) {
|
||||||
|
try {
|
||||||
|
this.password = password;
|
||||||
|
this._hasPassword = true;
|
||||||
|
this.keychain.setPassword(password, this.url);
|
||||||
|
const result = await setPassword(this.id, this.ownerToken, this.keychain);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
this.password = null;
|
||||||
|
this._hasPassword = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
del() {
|
||||||
|
return del(this.id, this.ownerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLimit(dlimit) {
|
||||||
|
if (this.dlimit !== dlimit) {
|
||||||
|
this.dlimit = dlimit;
|
||||||
|
return setParams(this.id, this.ownerToken, { dlimit });
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasPassword() {
|
||||||
|
return !!this._hasPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDownloadCount() {
|
||||||
|
try {
|
||||||
|
const result = await fileInfo(this.id, this.ownerToken);
|
||||||
|
this.dtotal = result.dtotal;
|
||||||
|
this.dlimit = result.dlimit;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message === '404') {
|
||||||
|
this.dtotal = this.dlimit;
|
||||||
|
}
|
||||||
|
// ignore other errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
url: this.url,
|
||||||
|
name: this.name,
|
||||||
|
size: this.size,
|
||||||
|
type: this.type,
|
||||||
|
time: this.time,
|
||||||
|
speed: this.speed,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
expiresAt: this.expiresAt,
|
||||||
|
secretKey: arrayToB64(this.keychain.rawSecret),
|
||||||
|
ownerToken: this.ownerToken,
|
||||||
|
dlimit: this.dlimit,
|
||||||
|
dtotal: this.dtotal,
|
||||||
|
hasPassword: this.hasPassword
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
5
app/pages/blank.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
return html`<div></div>`;
|
||||||
|
};
|
||||||
26
app/pages/completed/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('../../templates/progress');
|
||||||
|
const { fadeOut } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
return html`
|
||||||
|
<div class="page effect--fadeIn">
|
||||||
|
<div class="title">
|
||||||
|
${state.translate('downloadFinish')}
|
||||||
|
</div>
|
||||||
|
<div class="description"></div>
|
||||||
|
${progress(1)}
|
||||||
|
<div class="progressSection">
|
||||||
|
<div class="progressSection__text"></div>
|
||||||
|
</div>
|
||||||
|
<a class="link link--action"
|
||||||
|
href="/"
|
||||||
|
onclick=${sendNew}>${state.translate('sendYourFilesLink')}</a>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
async function sendNew(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await fadeOut('.page');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
};
|
||||||
42
app/pages/download/index.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('../../templates/progress');
|
||||||
|
const { bytes } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
const cancelBtn = html`
|
||||||
|
<button
|
||||||
|
id="cancel"
|
||||||
|
class="btn btn--cancel"
|
||||||
|
title="${state.translate('deletePopupCancel')}"
|
||||||
|
onclick=${cancel}>
|
||||||
|
${state.translate('deletePopupCancel')}
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="page effect--fadeIn">
|
||||||
|
<div class="title">
|
||||||
|
${state.translate('downloadingPageProgress', {
|
||||||
|
filename: state.fileInfo.name,
|
||||||
|
size: bytes(state.fileInfo.size)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
${state.translate('downloadingPageMessage')}
|
||||||
|
</div>
|
||||||
|
${progress(transfer.progressRatio, transfer.progressIndefinite)}
|
||||||
|
<div class="progressSection">
|
||||||
|
<div class="progressSection__text">
|
||||||
|
${state.translate(transfer.msg, transfer.sizes)}
|
||||||
|
</div>
|
||||||
|
${transfer.state === 'downloading' ? cancelBtn : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
const btn = document.getElementById('cancel');
|
||||||
|
btn.remove();
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
};
|
||||||
10
app/pages/error/index.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
return html`
|
||||||
|
<div class="page">
|
||||||
|
<div class="title">${state.translate('errorPageHeader')}</div>
|
||||||
|
<img src="${assets.get('illustration_error.svg')}"/>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
32
app/pages/legal.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const raw = require('choo/html/raw');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<div class="title">${state.translate('legalHeader')}</div>
|
||||||
|
${raw(
|
||||||
|
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
||||||
|
'https://testpilot.firefox.com/terms',
|
||||||
|
'https://testpilot.firefox.com/privacy',
|
||||||
|
'https://testpilot.firefox.com/experiments/send'
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
${raw(
|
||||||
|
replaceLinks(state.translate('legalNoticeMozilla'), [
|
||||||
|
'https://www.mozilla.org/privacy/websites/',
|
||||||
|
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function replaceLinks(str, urls) {
|
||||||
|
let i = 0;
|
||||||
|
const s = str.replace(
|
||||||
|
/<a>([^<]+)<\/a>/g,
|
||||||
|
(m, v) => `<a href="${urls[i++]}">${v}</a>`
|
||||||
|
);
|
||||||
|
return `<div class="description">${s}</div>`;
|
||||||
|
}
|
||||||
16
app/pages/notFound/index.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
return html`
|
||||||
|
<div class="page">
|
||||||
|
<div class="title">${state.translate('expiredPageHeader')}</div>
|
||||||
|
<img src="${assets.get('illustration_expired.svg')}" id="expired-img">
|
||||||
|
<div class="description">
|
||||||
|
${state.translate('uploadPageExplainer')}
|
||||||
|
</div>
|
||||||
|
<a class="link link--action" href="/">
|
||||||
|
${state.translate('sendYourFilesLink')}
|
||||||
|
</a>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
40
app/pages/preview/index.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
const { bytes } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, pageAction) {
|
||||||
|
const fileInfo = state.fileInfo;
|
||||||
|
|
||||||
|
const size = fileInfo.size
|
||||||
|
? state.translate('downloadFileSize', { size: bytes(fileInfo.size) })
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const title = fileInfo.name
|
||||||
|
? state.translate('downloadFileName', { filename: fileInfo.name })
|
||||||
|
: state.translate('downloadFileTitle');
|
||||||
|
|
||||||
|
const info = html`
|
||||||
|
<div id="dl-file"
|
||||||
|
data-nonce="${fileInfo.nonce}"
|
||||||
|
data-requires-password="${fileInfo.requiresPassword}"></div>`;
|
||||||
|
if (!pageAction) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="page">
|
||||||
|
<div class="title">
|
||||||
|
<span>${title}</span>
|
||||||
|
<span>${' ' + size}</span>
|
||||||
|
</div>
|
||||||
|
<div class="description">${state.translate('downloadMessage')}</div>
|
||||||
|
<img
|
||||||
|
src="${assets.get('illustration_download.svg')}"
|
||||||
|
title="${state.translate('downloadAltText')}"/>
|
||||||
|
${pageAction}
|
||||||
|
<a class="link link--action" href="/">
|
||||||
|
${state.translate('sendYourFilesLink')}
|
||||||
|
</a>
|
||||||
|
${info}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
115
app/pages/share/index.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/* global EXPIRE_SECONDS */
|
||||||
|
const html = require('choo/html');
|
||||||
|
const raw = require('choo/html/raw');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
const notFound = require('../notFound');
|
||||||
|
const setPasswordSection = require('../../templates/setPasswordSection');
|
||||||
|
const selectbox = require('../../templates/selectbox');
|
||||||
|
const deletePopup = require('../../templates/popup');
|
||||||
|
const { allowedCopy, delay, fadeOut } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const file = state.storage.getFileById(state.params.id);
|
||||||
|
if (!file) {
|
||||||
|
return notFound(state, emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div id="shareWrapper" class="effect--fadeIn">
|
||||||
|
<div class="title">${expireInfo(file, state.translate, emit)}</div>
|
||||||
|
<div class="sharePage">
|
||||||
|
<div class="sharePage__copyText">
|
||||||
|
${state.translate('copyUrlFormLabelWithName', { filename: file.name })}
|
||||||
|
</div>
|
||||||
|
<div class="copySection">
|
||||||
|
<input
|
||||||
|
id="fileUrl"
|
||||||
|
class="copySection__url"
|
||||||
|
type="url"
|
||||||
|
value="${file.url}"
|
||||||
|
readonly="true"/>
|
||||||
|
<button id="copyBtn"
|
||||||
|
class="inputBtn inputBtn--copy"
|
||||||
|
title="${state.translate('copyUrlFormButton')}"
|
||||||
|
onclick=${copyLink}>${state.translate('copyUrlFormButton')}</button>
|
||||||
|
</div>
|
||||||
|
${setPasswordSection(state, emit)}
|
||||||
|
<button
|
||||||
|
class="btn btn--delete"
|
||||||
|
title="${state.translate('deleteFileButton')}"
|
||||||
|
onclick=${showPopup}>${state.translate('deleteFileButton')}
|
||||||
|
</button>
|
||||||
|
<div class="sharePage__deletePopup">
|
||||||
|
${deletePopup(
|
||||||
|
state.translate('deletePopupText'),
|
||||||
|
state.translate('deletePopupYes'),
|
||||||
|
state.translate('deletePopupCancel'),
|
||||||
|
deleteFile
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<a class="link link--action"
|
||||||
|
href="/"
|
||||||
|
onclick=${sendNew}>${state.translate('sendAnotherFileLink')}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function showPopup() {
|
||||||
|
const popup = document.querySelector('.popup');
|
||||||
|
popup.classList.add('popup--show');
|
||||||
|
popup.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendNew(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await fadeOut('#shareWrapper');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyLink() {
|
||||||
|
if (allowedCopy()) {
|
||||||
|
emit('copy', { url: file.url, location: 'success-screen' });
|
||||||
|
const input = document.getElementById('fileUrl');
|
||||||
|
input.disabled = true;
|
||||||
|
input.classList.add('input--copied');
|
||||||
|
const copyBtn = document.getElementById('copyBtn');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
copyBtn.classList.add('inputBtn--copied');
|
||||||
|
copyBtn.replaceChild(
|
||||||
|
html`<img src="${assets.get('check-16.svg')}" class="cursor--pointer">`,
|
||||||
|
copyBtn.firstChild
|
||||||
|
);
|
||||||
|
await delay(2000);
|
||||||
|
input.disabled = false;
|
||||||
|
input.classList.remove('input--copied');
|
||||||
|
copyBtn.disabled = false;
|
||||||
|
copyBtn.classList.remove('inputBtn--copied');
|
||||||
|
copyBtn.textContent = state.translate('copyUrlFormButton');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFile() {
|
||||||
|
emit('delete', { file, location: 'success-screen' });
|
||||||
|
await fadeOut('#shareWrapper');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function expireInfo(file, translate, emit) {
|
||||||
|
const hours = Math.floor(EXPIRE_SECONDS / 60 / 60);
|
||||||
|
const el = html`<div>${raw(
|
||||||
|
translate('expireInfo', {
|
||||||
|
downloadCount: '<select></select>',
|
||||||
|
timespan: translate('timespanHours', { num: hours })
|
||||||
|
})
|
||||||
|
)}</div>`;
|
||||||
|
const select = el.querySelector('select');
|
||||||
|
const options = [1, 2, 3, 4, 5, 20].filter(i => i > (file.dtotal || 0));
|
||||||
|
const t = num => translate('downloadCount', { num });
|
||||||
|
const changed = value => emit('changeLimit', { file, value });
|
||||||
|
select.parentNode.replaceChild(
|
||||||
|
selectbox(file.dlimit || 1, options, t, changed),
|
||||||
|
select
|
||||||
|
);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
112
app/pages/share/share.css
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
.sharePage {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharePage__copyText {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 60px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--textColor);
|
||||||
|
max-width: 614px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharePage__deletePopup {
|
||||||
|
position: relative;
|
||||||
|
align-self: center;
|
||||||
|
bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection__url {
|
||||||
|
flex: 1;
|
||||||
|
height: 56px;
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--inputTextColor);
|
||||||
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 23px;
|
||||||
|
font-weight: 300;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection__url:disabled {
|
||||||
|
border: 1px solid var(--successControlBGColor);
|
||||||
|
background: var(--successControlFGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--copy {
|
||||||
|
flex: 0 1 165px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--copied {
|
||||||
|
border-color: var(--successControlBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--copied,
|
||||||
|
.inputBtn--copied:hover {
|
||||||
|
background: var(--successControlBGColor);
|
||||||
|
border: 1px solid var(--successControlBGColor);
|
||||||
|
color: var(--successControlFGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--delete {
|
||||||
|
align-self: center;
|
||||||
|
width: 176px;
|
||||||
|
height: 44px;
|
||||||
|
background: #fff;
|
||||||
|
border-color: rgba(12, 12, 13, 0.3);
|
||||||
|
margin-top: 50px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #313131;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--delete:hover {
|
||||||
|
background: #efeff1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 768px), (max-width: 768px) {
|
||||||
|
.copySection {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection__url {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.copySection {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copySection__url {
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharePage__copyText {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--copy {
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
flex: 0 1 65px;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/pages/unsupported/index.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
let strings = {};
|
||||||
|
let why = '';
|
||||||
|
let url = '';
|
||||||
|
let buttonAction = '';
|
||||||
|
if (state.params.reason !== 'outdated') {
|
||||||
|
strings = unsupportedStrings(state);
|
||||||
|
why = html`
|
||||||
|
<div class="description">
|
||||||
|
<a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">
|
||||||
|
${state.translate('notSupportedLink')}
|
||||||
|
</a>
|
||||||
|
</div>`;
|
||||||
|
url =
|
||||||
|
'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com';
|
||||||
|
buttonAction = html`
|
||||||
|
<div class="firefoxDownload__action">
|
||||||
|
Firefox<br><span class="firefoxDownload__text">${strings.button}</span>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
strings = outdatedStrings(state);
|
||||||
|
url = 'https://support.mozilla.org/kb/update-firefox-latest-version';
|
||||||
|
buttonAction = html`
|
||||||
|
<div class="firefoxDownload__action">
|
||||||
|
${strings.button}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="unsupportedPage">
|
||||||
|
<div class="title">${strings.title}</div>
|
||||||
|
<div class="description">
|
||||||
|
${strings.description}
|
||||||
|
</div>
|
||||||
|
${why}
|
||||||
|
<a href="${url}" class="firefoxDownload">
|
||||||
|
<img
|
||||||
|
src="${assets.get('firefox_logo-only.svg')}"
|
||||||
|
class="firefoxDownload__logo"
|
||||||
|
alt="Firefox"/>
|
||||||
|
${buttonAction}
|
||||||
|
</a>
|
||||||
|
<div class="unsupportedPage__info">
|
||||||
|
${strings.explainer}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function outdatedStrings(state) {
|
||||||
|
return {
|
||||||
|
title: state.translate('notSupportedHeader'),
|
||||||
|
description: state.translate('notSupportedOutdatedDetail'),
|
||||||
|
button: state.translate('updateFirefox'),
|
||||||
|
explainer: state.translate('uploadPageExplainer')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsupportedStrings(state) {
|
||||||
|
return {
|
||||||
|
title: state.translate('notSupportedHeader'),
|
||||||
|
description: state.translate('notSupportedDetail'),
|
||||||
|
button: state.translate('downloadFirefoxButtonSub'),
|
||||||
|
explainer: state.translate('uploadPageExplainer')
|
||||||
|
};
|
||||||
|
}
|
||||||
49
app/pages/unsupported/unsupported.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
.unsupportedPage {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsupportedPage__info {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 23px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
margin: 0 auto 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firefoxDownload {
|
||||||
|
margin-bottom: 181px;
|
||||||
|
height: 80px;
|
||||||
|
background: #98e02b;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 5px 3px rgb(234, 234, 234);
|
||||||
|
font-family: 'Fira Sans', 'segoe ui', sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
font-size: 26px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firefoxDownload__logo {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firefoxDownload__action {
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 20.4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firefoxDownload__text {
|
||||||
|
font-family: 'Fira Sans', 'segoe ui', sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: -0.69px;
|
||||||
|
}
|
||||||
39
app/pages/upload/index.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('../../templates/progress');
|
||||||
|
const { bytes } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="page effect--fadeIn">
|
||||||
|
<div class="title">
|
||||||
|
${state.translate('uploadingPageProgress', {
|
||||||
|
filename: transfer.file.name,
|
||||||
|
size: bytes(transfer.file.size)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="description"></div>
|
||||||
|
${progress(transfer.progressRatio, transfer.progressIndefinite)}
|
||||||
|
<div class="progressSection">
|
||||||
|
<div class="progressSection__text">
|
||||||
|
${state.translate(transfer.msg, transfer.sizes)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="cancel-upload"
|
||||||
|
class="btn btn--cancel"
|
||||||
|
title="${state.translate('uploadingPageCancel')}"
|
||||||
|
onclick=${cancel}>
|
||||||
|
${state.translate('uploadingPageCancel')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
const btn = document.getElementById('cancel-upload');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = state.translate('uploadCancelNotification');
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
};
|
||||||
83
app/pages/welcome/index.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/* global MAXFILESIZE */
|
||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
const fileList = require('../../templates/fileList');
|
||||||
|
const { bytes, fadeOut } = require('../../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
// the page flickers if both the server and browser set 'effect--fadeIn'
|
||||||
|
const fade = state.layout ? '' : 'effect--fadeIn';
|
||||||
|
return html`
|
||||||
|
<div id="page-one" class="${fade}">
|
||||||
|
<div class="title">${state.translate('uploadPageHeader')}</div>
|
||||||
|
<div class="description">
|
||||||
|
<div>${state.translate('uploadPageExplainer')}</div>
|
||||||
|
<a
|
||||||
|
href="https://testpilot.firefox.com/experiments/send"
|
||||||
|
class="link">
|
||||||
|
${state.translate('uploadPageLearnMore')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="uploadArea"
|
||||||
|
ondragover=${dragover}
|
||||||
|
ondragleave=${dragleave}>
|
||||||
|
<img
|
||||||
|
src="${assets.get('upload.svg')}"
|
||||||
|
title="${state.translate('uploadSvgAlt')}"/>
|
||||||
|
<div class="uploadArea__msg">
|
||||||
|
${state.translate('uploadPageDropMessage')}
|
||||||
|
</div>
|
||||||
|
<span class="uploadArea__sizeMsg">
|
||||||
|
${state.translate('uploadPageSizeMessage')}
|
||||||
|
</span>
|
||||||
|
<input id="file-upload"
|
||||||
|
class="inputFile"
|
||||||
|
type="file"
|
||||||
|
name="fileUploaded"
|
||||||
|
onfocus=${onfocus}
|
||||||
|
onblur=${onblur}
|
||||||
|
onchange=${upload} />
|
||||||
|
<label for="file-upload"
|
||||||
|
class="btn btn--file"
|
||||||
|
title="${state.translate('uploadPageBrowseButton1')}">
|
||||||
|
${state.translate('uploadPageBrowseButton1')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
${fileList(state, emit)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function dragover(event) {
|
||||||
|
const div = document.querySelector('.uploadArea');
|
||||||
|
div.classList.add('uploadArea--dragging');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragleave(event) {
|
||||||
|
const div = document.querySelector('.uploadArea');
|
||||||
|
div.classList.remove('uploadArea--dragging');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onfocus(event) {
|
||||||
|
event.target.classList.add('inputFile--focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onblur(event) {
|
||||||
|
event.target.classList.remove('inputFile--focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const target = event.target;
|
||||||
|
const file = target.files[0];
|
||||||
|
if (file.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > MAXFILESIZE) {
|
||||||
|
window.alert(state.translate('fileTooBig', { size: bytes(MAXFILESIZE) }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fadeOut('#page-one');
|
||||||
|
emit('upload', { file, type: 'click' });
|
||||||
|
}
|
||||||
|
};
|
||||||
65
app/pages/welcome/welcome.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.uploadArea {
|
||||||
|
border: 3px dashed rgba(0, 148, 251, 0.5);
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
height: 255px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 150ms;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadArea__msg {
|
||||||
|
font-size: 22px;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
font-family: 'SF Pro Text', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadArea__sizeMsg {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadArea--dragging {
|
||||||
|
border: 5px dashed rgba(0, 148, 251, 0.5);
|
||||||
|
height: 251px;
|
||||||
|
transform: scale(1.04);
|
||||||
|
border-radius: 4.2px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadArea--dragging * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--file {
|
||||||
|
font-size: 20px;
|
||||||
|
min-width: 240px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputFile {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputFile--focused + .btn--file {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
outline: 1px dotted #000;
|
||||||
|
outline: -webkit-focus-ring-color auto 5px;
|
||||||
|
}
|
||||||
60
app/routes/download.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const preview = require('../pages/preview');
|
||||||
|
const download = require('../pages/download');
|
||||||
|
const notFound = require('../pages/notFound');
|
||||||
|
const downloadPassword = require('../templates/downloadPassword');
|
||||||
|
const downloadButton = require('../templates/downloadButton');
|
||||||
|
|
||||||
|
function hasFileInfo() {
|
||||||
|
return !!document.getElementById('dl-file');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileInfoFromDOM() {
|
||||||
|
const el = document.getElementById('dl-file');
|
||||||
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nonce: el.getAttribute('data-nonce'),
|
||||||
|
requiresPassword: !!+el.getAttribute('data-requires-password')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFileInfo(state) {
|
||||||
|
const metadata = getFileInfoFromDOM();
|
||||||
|
return {
|
||||||
|
id: state.params.id,
|
||||||
|
secretKey: state.params.key,
|
||||||
|
nonce: metadata.nonce,
|
||||||
|
requiresPassword: metadata.requiresPassword
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (!state.fileInfo) {
|
||||||
|
// This is a fresh page load
|
||||||
|
// We need to parse the file info from the server's html
|
||||||
|
if (!hasFileInfo()) {
|
||||||
|
return notFound(state, emit);
|
||||||
|
}
|
||||||
|
state.fileInfo = createFileInfo(state);
|
||||||
|
|
||||||
|
if (!state.fileInfo.requiresPassword) {
|
||||||
|
emit('getMetadata');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pageAction = null; //default state: we don't have file metadata
|
||||||
|
if (state.transfer) {
|
||||||
|
const s = state.transfer.state;
|
||||||
|
if (['downloading', 'decrypting', 'complete'].indexOf(s) > -1) {
|
||||||
|
// Downloading is in progress
|
||||||
|
return download(state, emit);
|
||||||
|
}
|
||||||
|
// we have file metadata
|
||||||
|
pageAction = downloadButton(state, emit);
|
||||||
|
} else if (state.fileInfo.requiresPassword && !state.fileInfo.password) {
|
||||||
|
// we're waiting on the user for a valid password
|
||||||
|
pageAction = downloadPassword(state, emit);
|
||||||
|
}
|
||||||
|
return preview(state, pageAction);
|
||||||
|
};
|
||||||
9
app/routes/home.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const welcome = require('../pages/welcome');
|
||||||
|
const upload = require('../pages/upload');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (state.uploading) {
|
||||||
|
return upload(state, emit);
|
||||||
|
}
|
||||||
|
return welcome(state, emit);
|
||||||
|
};
|
||||||
58
app/routes/index.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const choo = require('choo');
|
||||||
|
const html = require('choo/html');
|
||||||
|
const nanotiming = require('nanotiming');
|
||||||
|
const download = require('./download');
|
||||||
|
const header = require('../templates/header');
|
||||||
|
const footer = require('../templates/footer');
|
||||||
|
const fxPromo = require('../templates/fxPromo');
|
||||||
|
|
||||||
|
nanotiming.disabled = true;
|
||||||
|
const app = choo();
|
||||||
|
|
||||||
|
function banner(state, emit) {
|
||||||
|
if (state.promo && !state.route.startsWith('/unsupported/')) {
|
||||||
|
return fxPromo(state, emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function body(template) {
|
||||||
|
return function(state, emit) {
|
||||||
|
const b = html`<body>
|
||||||
|
${banner(state, emit)}
|
||||||
|
${header(state)}
|
||||||
|
<main class="main">
|
||||||
|
<noscript>
|
||||||
|
<div class="noscript">
|
||||||
|
<h2>${state.translate('javascriptRequired')}</h2>
|
||||||
|
<p>
|
||||||
|
<a class="link" href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">
|
||||||
|
${state.translate('whyJavascript')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>${state.translate('enableJavascript')}</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
${template(state, emit)}
|
||||||
|
</main>
|
||||||
|
${footer(state)}
|
||||||
|
</body>`;
|
||||||
|
if (state.layout) {
|
||||||
|
// server side only
|
||||||
|
return state.layout(state, b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.route('/', body(require('./home')));
|
||||||
|
app.route('/share/:id', body(require('../pages/share')));
|
||||||
|
app.route('/download/:id', body(download));
|
||||||
|
app.route('/download/:id/:key', body(download));
|
||||||
|
app.route('/completed', body(require('../pages/completed')));
|
||||||
|
app.route('/unsupported/:reason', body(require('../pages/unsupported')));
|
||||||
|
app.route('/legal', body(require('../pages/legal')));
|
||||||
|
app.route('/error', body(require('../pages/error')));
|
||||||
|
app.route('/blank', body(require('../pages/blank')));
|
||||||
|
app.route('*', body(require('../pages/notFound')));
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isFile } from './utils';
|
import { isFile } from './utils';
|
||||||
|
import OwnedFile from './ownedFile';
|
||||||
|
|
||||||
class Mem {
|
class Mem {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -26,13 +27,34 @@ class Mem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Storage {
|
class Storage {
|
||||||
constructor() {
|
constructor() {
|
||||||
try {
|
try {
|
||||||
this.engine = localStorage || new Mem();
|
this.engine = localStorage || new Mem();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.engine = new Mem();
|
this.engine = new Mem();
|
||||||
}
|
}
|
||||||
|
this._files = this.loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFiles() {
|
||||||
|
const fs = [];
|
||||||
|
for (let i = 0; i < this.engine.length; i++) {
|
||||||
|
const k = this.engine.key(i);
|
||||||
|
if (isFile(k)) {
|
||||||
|
try {
|
||||||
|
const f = new OwnedFile(JSON.parse(this.engine.getItem(k)));
|
||||||
|
if (!f.id) {
|
||||||
|
f.id = f.fileId;
|
||||||
|
}
|
||||||
|
fs.push(f);
|
||||||
|
} catch (err) {
|
||||||
|
// obviously you're not a golfer
|
||||||
|
this.engine.removeItem(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fs.sort((a, b) => a.createdAt - b.createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalDownloads() {
|
get totalDownloads() {
|
||||||
@@ -53,51 +75,50 @@ export default class Storage {
|
|||||||
set referrer(str) {
|
set referrer(str) {
|
||||||
this.engine.setItem('referrer', str);
|
this.engine.setItem('referrer', str);
|
||||||
}
|
}
|
||||||
|
get enrolled() {
|
||||||
get files() {
|
return JSON.parse(this.engine.getItem('experiments') || '[]');
|
||||||
const fs = [];
|
|
||||||
for (let i = 0; i < this.engine.length; i++) {
|
|
||||||
const k = this.engine.key(i);
|
|
||||||
if (isFile(k)) {
|
|
||||||
try {
|
|
||||||
fs.push(JSON.parse(this.engine.getItem(k)));
|
|
||||||
} catch (err) {
|
|
||||||
// obviously you're not a golfer
|
|
||||||
this.engine.removeItem(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fs.sort((file1, file2) => {
|
|
||||||
const creationDate1 = new Date(file1.creationDate);
|
|
||||||
const creationDate2 = new Date(file2.creationDate);
|
|
||||||
return creationDate1 - creationDate2;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get numFiles() {
|
enroll(id, variant) {
|
||||||
let length = 0;
|
const enrolled = this.enrolled;
|
||||||
for (let i = 0; i < this.engine.length; i++) {
|
// eslint-disable-next-line no-unused-vars
|
||||||
const k = this.engine.key(i);
|
if (!enrolled.find(([i, v]) => i === id)) {
|
||||||
if (isFile(k)) {
|
enrolled.push([id, variant]);
|
||||||
length += 1;
|
this.engine.setItem('experiments', JSON.stringify(enrolled));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return length;
|
}
|
||||||
|
|
||||||
|
get files() {
|
||||||
|
return this._files;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileById(id) {
|
getFileById(id) {
|
||||||
try {
|
return this._files.find(f => f.id === id);
|
||||||
return JSON.parse(this.engine.getItem(id));
|
}
|
||||||
} catch (e) {
|
|
||||||
return null;
|
get(id) {
|
||||||
}
|
return this.engine.getItem(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(property) {
|
remove(property) {
|
||||||
|
if (isFile(property)) {
|
||||||
|
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
||||||
|
}
|
||||||
this.engine.removeItem(property);
|
this.engine.removeItem(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFile(id, file) {
|
addFile(file) {
|
||||||
this.engine.setItem(id, JSON.stringify(file));
|
this._files.push(file);
|
||||||
|
this.writeFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(file) {
|
||||||
|
this.engine.setItem(file.id, JSON.stringify(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFiles() {
|
||||||
|
this._files.forEach(f => this.writeFile(f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default new Storage();
|
||||||
6
app/templates/downloadButton/downloadButton.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.btn--download {
|
||||||
|
width: 180px;
|
||||||
|
height: 44px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
13
app/templates/downloadButton/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
return html`
|
||||||
|
<button class="btn btn--download"
|
||||||
|
onclick=${download}>${state.translate('downloadButtonLabel')}
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
function download(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('download', state.fileInfo);
|
||||||
|
}
|
||||||
|
};
|
||||||
22
app/templates/downloadPassword/downloadPassword.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.passwordSection {
|
||||||
|
text-align: left;
|
||||||
|
padding: 40px 0;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordForm {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.passwordSection {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordForm {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/templates/downloadPassword/index.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const fileInfo = state.fileInfo;
|
||||||
|
const invalid = fileInfo.password === null;
|
||||||
|
const label = invalid
|
||||||
|
? html`
|
||||||
|
<label class="error" for="password-input">
|
||||||
|
${state.translate('passwordTryAgain')}
|
||||||
|
</label>`
|
||||||
|
: html`
|
||||||
|
<label for="password-input">
|
||||||
|
${state.translate('unlockInputLabel')}
|
||||||
|
</label>`;
|
||||||
|
const inputClass = invalid
|
||||||
|
? 'input input--noBtn input--error'
|
||||||
|
: 'input input--noBtn';
|
||||||
|
const div = html`
|
||||||
|
<div class="passwordSection">
|
||||||
|
${label}
|
||||||
|
<form class="passwordForm" onsubmit=${checkPassword} data-no-csrf>
|
||||||
|
<input id="password-input"
|
||||||
|
class="${inputClass}"
|
||||||
|
maxlength="64"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="${state.translate('unlockInputPlaceholder')}"
|
||||||
|
oninput=${inputChanged}
|
||||||
|
type="password" />
|
||||||
|
<input type="submit"
|
||||||
|
id="password-btn"
|
||||||
|
class="inputBtn inputBtn--hidden"
|
||||||
|
value="${state.translate('unlockButtonLabel')}"/>
|
||||||
|
</form>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if (!(div instanceof String)) {
|
||||||
|
setTimeout(() => document.getElementById('password-input').focus());
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputChanged() {
|
||||||
|
const input = document.getElementById('password-input');
|
||||||
|
const btn = document.getElementById('password-btn');
|
||||||
|
input.classList.remove('input--error');
|
||||||
|
if (input.value.length > 0) {
|
||||||
|
btn.classList.remove('inputBtn--hidden');
|
||||||
|
input.classList.remove('input--noBtn');
|
||||||
|
} else {
|
||||||
|
btn.classList.add('inputBtn--hidden');
|
||||||
|
input.classList.add('input--noBtn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const password = document.getElementById('password-input').value;
|
||||||
|
if (password.length > 0) {
|
||||||
|
document.getElementById('password-btn').disabled = true;
|
||||||
|
state.fileInfo.url = window.location.href;
|
||||||
|
state.fileInfo.password = password;
|
||||||
|
emit('getMetadata');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
||||||
26
app/templates/file/file.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.fileData {
|
||||||
|
font-size: 15px;
|
||||||
|
vertical-align: top;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
padding: 17px 19px 0;
|
||||||
|
line-height: 23px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileData--overflow {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileData--center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.fileData {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 17px 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
app/templates/file/index.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
const number = require('../../utils').number;
|
||||||
|
const deletePopup = require('../popup');
|
||||||
|
|
||||||
|
module.exports = function(file, state, emit) {
|
||||||
|
const ttl = file.expiresAt - Date.now();
|
||||||
|
const remainingTime =
|
||||||
|
timeLeft(ttl, state) || state.translate('linkExpiredAlt');
|
||||||
|
const downloadLimit = file.dlimit || 1;
|
||||||
|
const totalDownloads = file.dtotal || 0;
|
||||||
|
return html`
|
||||||
|
<tr id="${file.id}">
|
||||||
|
<td class="fileData fileData--overflow" title="${file.name}">
|
||||||
|
<a class="link" href="/share/${file.id}">${file.name}</a>
|
||||||
|
</td>
|
||||||
|
<td class="fileData fileData--center">
|
||||||
|
<img
|
||||||
|
onclick=${copyClick}
|
||||||
|
src="${assets.get('copy-16.svg')}"
|
||||||
|
class="cursor--pointer"
|
||||||
|
title="${state.translate('copyUrlHover')}">
|
||||||
|
<span hidden="true">
|
||||||
|
${state.translate('copiedUrl')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="fileData fileData--overflow">${remainingTime}</td>
|
||||||
|
<td class="fileData fileData--center">${number(totalDownloads)} / ${number(
|
||||||
|
downloadLimit
|
||||||
|
)}</td>
|
||||||
|
<td class="fileData fileData--center">
|
||||||
|
<img
|
||||||
|
onclick=${showPopup}
|
||||||
|
src="${assets.get('close-16.svg')}"
|
||||||
|
class="cursor--pointer"
|
||||||
|
title="${state.translate('deleteButtonHover')}">
|
||||||
|
${deletePopup(
|
||||||
|
state.translate('deletePopupText'),
|
||||||
|
state.translate('deletePopupYes'),
|
||||||
|
state.translate('deletePopupCancel'),
|
||||||
|
deleteFile
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function copyClick(e) {
|
||||||
|
emit('copy', { url: file.url, location: 'upload-list' });
|
||||||
|
const icon = e.target;
|
||||||
|
const text = e.target.nextSibling;
|
||||||
|
icon.hidden = true;
|
||||||
|
text.hidden = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
icon.hidden = false;
|
||||||
|
text.hidden = true;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPopup() {
|
||||||
|
const tr = document.getElementById(file.id);
|
||||||
|
const popup = tr.querySelector('.popup');
|
||||||
|
popup.classList.add('popup--show');
|
||||||
|
popup.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile() {
|
||||||
|
emit('delete', { file, location: 'upload-list' });
|
||||||
|
emit('render');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function timeLeft(milliseconds, state) {
|
||||||
|
const minutes = Math.floor(milliseconds / 1000 / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours >= 1) {
|
||||||
|
return state.translate('expiresHoursMinutes', {
|
||||||
|
hours,
|
||||||
|
minutes: minutes % 60
|
||||||
|
});
|
||||||
|
} else if (hours === 0) {
|
||||||
|
if (minutes === 0) {
|
||||||
|
return state.translate('expiresMinutes', { minutes: '< 1' });
|
||||||
|
}
|
||||||
|
return state.translate('expiresMinutes', { minutes });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
52
app/templates/fileList/fileList.css
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
.fileList {
|
||||||
|
margin: 45.3px auto;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__header {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
font-weight: lighter;
|
||||||
|
text-align: left;
|
||||||
|
background: rgba(0, 148, 251, 0.05);
|
||||||
|
height: 40px;
|
||||||
|
border-top: 1px solid rgba(0, 148, 251, 0.1);
|
||||||
|
padding: 0 19px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__body {
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__nameCol {
|
||||||
|
width: 35%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__copyCol {
|
||||||
|
text-align: center;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__expireCol {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__dlCol {
|
||||||
|
width: 8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList__delCol {
|
||||||
|
text-align: center;
|
||||||
|
width: 7%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.fileList__header {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/templates/fileList/index.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const file = require('../file');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (state.storage.files.length) {
|
||||||
|
return html`
|
||||||
|
<table class="fileList">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="fileList__header fileList__nameCol">
|
||||||
|
${state.translate('uploadedFile')}
|
||||||
|
</th>
|
||||||
|
<th class="fileList__header fileList__copyCol">
|
||||||
|
${state.translate('copyFileList')}
|
||||||
|
</th>
|
||||||
|
<th class="fileList__header fileList__expireCol" >
|
||||||
|
${state.translate('timeFileList')}
|
||||||
|
</th>
|
||||||
|
<th class="fileList__header fileList__dlCol" >
|
||||||
|
${state.translate('downloadsFileList')}
|
||||||
|
</th>
|
||||||
|
<th class="fileList__header fileList__delCol">
|
||||||
|
${state.translate('deleteFileList')}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="fileList__body">
|
||||||
|
${state.storage.files.map(f => file(f, state, emit))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
93
app/templates/footer/footer.css
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
.footer {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 50px 31px 41px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection {
|
||||||
|
max-width: 81vw;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__link {
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
opacity: 0.9;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 2vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__link:visited {
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__mozLogo {
|
||||||
|
width: 112px;
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 94px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection__link {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection__link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection__icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 768px), (max-width: 768px) {
|
||||||
|
.footer {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
max-width: 630px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__mozLogo {
|
||||||
|
margin-left: -7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection {
|
||||||
|
flex-direction: column;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legalSection__link {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socialSection {
|
||||||
|
margin-top: 20px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
app/templates/footer/index.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
return html`<footer class="footer">
|
||||||
|
<div class="legalSection">
|
||||||
|
<a
|
||||||
|
href="https://www.mozilla.org"
|
||||||
|
class="legalSection__link"
|
||||||
|
role="presentation">
|
||||||
|
<img
|
||||||
|
class="legalSection__mozLogo"
|
||||||
|
src="${assets.get('mozilla-logo.svg')}"
|
||||||
|
alt="mozilla"/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.mozilla.org/about/legal"
|
||||||
|
class="legalSection__link">
|
||||||
|
${state.translate('footerLinkLegal')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://testpilot.firefox.com/about"
|
||||||
|
class="legalSection__link">
|
||||||
|
${state.translate('footerLinkAbout')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/legal"
|
||||||
|
class="legalSection__link">${state.translate('footerLinkPrivacy')}</a>
|
||||||
|
<a
|
||||||
|
href="/legal"
|
||||||
|
class="legalSection__link">${state.translate('footerLinkTerms')}</a>
|
||||||
|
<a
|
||||||
|
href="https://www.mozilla.org/privacy/websites/#cookies"
|
||||||
|
class="legalSection__link">
|
||||||
|
${state.translate('footerLinkCookies')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.mozilla.org/about/legal/report-infringement/"
|
||||||
|
class="legalSection__link">
|
||||||
|
${state.translate('reportIPInfringement')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="socialSection">
|
||||||
|
<a
|
||||||
|
href="https://github.com/mozilla/send"
|
||||||
|
class="socialSection__link"
|
||||||
|
role="presentation">
|
||||||
|
<img
|
||||||
|
class="socialSection__icon"
|
||||||
|
src="${assets.get('github-icon.svg')}"
|
||||||
|
alt="github"/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/FxTestPilot"
|
||||||
|
class="socialSection__link"
|
||||||
|
role="presentation">
|
||||||
|
<img
|
||||||
|
class="socialSection__icon"
|
||||||
|
src="${assets.get('twitter-icon.svg')}"
|
||||||
|
alt="twitter"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>`;
|
||||||
|
};
|
||||||
56
app/templates/fxPromo/fxPromo.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
.fxPromo {
|
||||||
|
padding: 0 15px;
|
||||||
|
height: 48px;
|
||||||
|
background-color: #efeff1;
|
||||||
|
color: #4a4a4f;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo > div > span {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo__logo {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--blue {
|
||||||
|
background: linear-gradient(-180deg, #45a1ff 0%, #00feff 94%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--pink {
|
||||||
|
background: linear-gradient(-180deg, #ff9400 0%, #ff1ad9 94%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--blue a {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--pink a {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--blue a:hover {
|
||||||
|
color: #eee;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fxPromo--pink a:hover {
|
||||||
|
color: #eee;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
34
app/templates/fxPromo/index.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
let classes = 'fxPromo';
|
||||||
|
switch (state.promo) {
|
||||||
|
case 'blue':
|
||||||
|
classes = 'fxPromo fxPromo--blue';
|
||||||
|
break;
|
||||||
|
case 'pink':
|
||||||
|
classes = 'fxPromo fxPromo--pink';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="${classes}">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="${assets.get('firefox_logo-only.svg')}"
|
||||||
|
class="fxPromo__logo"
|
||||||
|
alt="Firefox"/>
|
||||||
|
<span>Send is brought to you by the all-new Firefox.
|
||||||
|
<a
|
||||||
|
class="link"
|
||||||
|
href="https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com"
|
||||||
|
onclick=${clicked}
|
||||||
|
>Download Firefox now ≫</a></span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function clicked() {
|
||||||
|
emit('experiment', { cd3: 'promo' });
|
||||||
|
}
|
||||||
|
};
|
||||||
104
app/templates/header/header.css
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
.header {
|
||||||
|
align-items: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 31px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__title {
|
||||||
|
color: #3e3d40;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-left: 8px;
|
||||||
|
transition: color 50ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__title:hover {
|
||||||
|
color: var(--primaryControlBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__subtitle {
|
||||||
|
color: #3e3d40;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__subtitle-link {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3e3d40;
|
||||||
|
transition: color 50ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo__subtitle-link:hover {
|
||||||
|
color: var(--primaryControlBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
background-color: var(--primaryControlBGColor);
|
||||||
|
background-image: url('../assets/feedback.svg');
|
||||||
|
background-position: 2px 4px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 18px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
padding: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 12px;
|
||||||
|
max-width: 12px;
|
||||||
|
text-indent: 17px;
|
||||||
|
transition: all 250ms ease-in-out;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback:hover,
|
||||||
|
.feedback:focus {
|
||||||
|
min-width: 30px;
|
||||||
|
max-width: 300px;
|
||||||
|
text-indent: 2px;
|
||||||
|
padding: 5px 5px 5px 20px;
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback:active {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
margin-top: 10px;
|
||||||
|
min-width: 30px;
|
||||||
|
max-width: 300px;
|
||||||
|
text-indent: 2px;
|
||||||
|
padding: 5px 5px 5px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/templates/header/index.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../../common/assets');
|
||||||
|
/*
|
||||||
|
The current weback config uses package.json to generate
|
||||||
|
version.json for /__version__ meaning `require` returns the
|
||||||
|
string 'version.json' in the frontend context but the json
|
||||||
|
on the server.
|
||||||
|
|
||||||
|
We want `version` to be constant at build time so this file
|
||||||
|
has a custom loader (/build/version_loader.js) just to replace
|
||||||
|
string with the value from package.json. 🤢
|
||||||
|
*/
|
||||||
|
const version = require('../../../package.json').version || 'VERSION';
|
||||||
|
const browser = browserName();
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const feedbackUrl = `https://qsurvey.mozilla.com/s3/txp-firefox-send?ver=${version}&browser=${browser}`;
|
||||||
|
return html`<header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<a class="logo__link" href="/">
|
||||||
|
<img
|
||||||
|
src="${assets.get('send_logo.svg')}"
|
||||||
|
alt="Send"/>
|
||||||
|
<h1 class="logo__title">Send</h1>
|
||||||
|
</a>
|
||||||
|
<div class="logo__subtitle">
|
||||||
|
<a class="logo__subtitle-link" href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
||||||
|
<div>${state.translate('siteSubtitle')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="${feedbackUrl}"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
class="feedback"
|
||||||
|
target="_blank">${state.translate('siteFeedback')}</a>
|
||||||
|
</header>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function browserName() {
|
||||||
|
try {
|
||||||
|
if (/firefox/i.test(navigator.userAgent)) {
|
||||||
|
return 'firefox';
|
||||||
|
}
|
||||||
|
if (/edge/i.test(navigator.userAgent)) {
|
||||||
|
return 'edge';
|
||||||
|
}
|
||||||
|
if (/trident/i.test(navigator.userAgent)) {
|
||||||
|
return 'ie';
|
||||||
|
}
|
||||||
|
if (/chrome/i.test(navigator.userAgent)) {
|
||||||
|
return 'chrome';
|
||||||
|
}
|
||||||
|
if (/safari/i.test(navigator.userAgent)) {
|
||||||
|
return 'safari';
|
||||||
|
}
|
||||||
|
return 'other';
|
||||||
|
} catch (e) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
109
app/templates/passwordInput/index.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const MAX_LENGTH = 32;
|
||||||
|
|
||||||
|
module.exports = function(file, state, emit) {
|
||||||
|
const loading = state.settingPassword;
|
||||||
|
const pwd = file.hasPassword;
|
||||||
|
const sectionClass =
|
||||||
|
pwd || state.passwordSetError
|
||||||
|
? 'passwordInput'
|
||||||
|
: 'passwordInput passwordInput--hidden';
|
||||||
|
const inputClass = loading || pwd ? 'input' : 'input input--noBtn';
|
||||||
|
let btnClass = 'inputBtn inputBtn--password inputBtn--hidden';
|
||||||
|
if (loading) {
|
||||||
|
btnClass = 'inputBtn inputBtn--password inputBtn--loading';
|
||||||
|
} else if (pwd) {
|
||||||
|
btnClass = 'inputBtn inputBtn--password';
|
||||||
|
}
|
||||||
|
const action = pwd
|
||||||
|
? state.translate('changePasswordButton')
|
||||||
|
: state.translate('addPasswordButton');
|
||||||
|
return html`
|
||||||
|
<div class="${sectionClass}">
|
||||||
|
<form
|
||||||
|
class="passwordInput__form"
|
||||||
|
onsubmit=${setPassword}
|
||||||
|
data-no-csrf>
|
||||||
|
<input id="password-input"
|
||||||
|
${loading ? 'disabled' : ''}
|
||||||
|
class="${inputClass}"
|
||||||
|
maxlength="${MAX_LENGTH}"
|
||||||
|
autocomplete="off"
|
||||||
|
type="password"
|
||||||
|
oninput=${inputChanged}
|
||||||
|
onfocus=${focused}
|
||||||
|
placeholder="${
|
||||||
|
pwd && !state.passwordSetError
|
||||||
|
? passwordPlaceholder(file.password)
|
||||||
|
: state.translate('unlockInputPlaceholder')
|
||||||
|
}">
|
||||||
|
<input type="submit"
|
||||||
|
id="password-btn"
|
||||||
|
${loading ? 'disabled' : ''}
|
||||||
|
class="${btnClass}"
|
||||||
|
value="${loading ? '' : action}">
|
||||||
|
</form>
|
||||||
|
<label
|
||||||
|
class="passwordInput__msg ${
|
||||||
|
state.passwordSetError ? 'passwordInput__msg--error' : ''
|
||||||
|
}"
|
||||||
|
for="password-input">${message(state, pwd)}</label>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function inputChanged() {
|
||||||
|
state.passwordSetError = null;
|
||||||
|
const resetInput = document.getElementById('password-input');
|
||||||
|
const resetBtn = document.getElementById('password-btn');
|
||||||
|
const pwdmsg = document.querySelector('.passwordInput__msg');
|
||||||
|
const length = resetInput.value.length;
|
||||||
|
|
||||||
|
if (length === MAX_LENGTH) {
|
||||||
|
pwdmsg.textContent = state.translate('maxPasswordLength', {
|
||||||
|
length: MAX_LENGTH
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pwdmsg.textContent = '';
|
||||||
|
}
|
||||||
|
if (length > 0) {
|
||||||
|
resetBtn.classList.remove('inputBtn--hidden');
|
||||||
|
resetInput.classList.remove('input--noBtn');
|
||||||
|
} else {
|
||||||
|
resetBtn.classList.add('inputBtn--hidden');
|
||||||
|
resetInput.classList.add('input--noBtn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focused(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const el = document.getElementById('password-input');
|
||||||
|
if (el.placeholder !== state.translate('unlockInputPlaceholder')) {
|
||||||
|
el.placeholder = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const el = document.getElementById('password-input');
|
||||||
|
const password = el.value;
|
||||||
|
if (password.length > 0) {
|
||||||
|
emit('password', { password, file });
|
||||||
|
} else {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function passwordPlaceholder(password) {
|
||||||
|
return password ? password.replace(/./g, '●') : '●●●●●●●●●●●●';
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(state, pwd) {
|
||||||
|
if (state.passwordSetError) {
|
||||||
|
return state.translate('passwordSetError');
|
||||||
|
}
|
||||||
|
if (state.settingPassword || !pwd) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return state.translate('passwordIsSet');
|
||||||
|
}
|
||||||
42
app/templates/passwordInput/passwordInput.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.passwordInput {
|
||||||
|
width: 90%;
|
||||||
|
height: 100px;
|
||||||
|
padding: 10px 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordInput--hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordInput__form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordInput__msg {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordInput__msg--error {
|
||||||
|
color: var(--errorColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--loading {
|
||||||
|
background-image: url('../assets/spinner.svg');
|
||||||
|
background-position: center;
|
||||||
|
background-size: 30px 30px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBtn--password {
|
||||||
|
flex: 0 0 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.passwordInput {
|
||||||
|
flex-direction: column;
|
||||||
|
width: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/templates/popup/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(msg, confirmText, cancelText, confirmCallback) {
|
||||||
|
return html`
|
||||||
|
<div class="popup__wrapper">
|
||||||
|
<div class="popup" onblur=${hide} tabindex="-1">
|
||||||
|
<div class="popup__message">${msg}</div>
|
||||||
|
<div class="popup__action">
|
||||||
|
<span class="popup__no" onclick=${hide}>
|
||||||
|
${cancelText}
|
||||||
|
</span>
|
||||||
|
<span class="popup__yes" onclick=${confirmCallback}>
|
||||||
|
${confirmText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function hide(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const popup = document.querySelector('.popup.popup--show');
|
||||||
|
if (popup) {
|
||||||
|
popup.classList.remove('popup--show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
122
app/templates/popup/popup.css
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
.popup {
|
||||||
|
visibility: hidden;
|
||||||
|
min-width: 204px;
|
||||||
|
min-height: 105px;
|
||||||
|
background-color: var(--pageBGColor);
|
||||||
|
color: var(--textColor);
|
||||||
|
border: 1px solid #d7d7db;
|
||||||
|
padding: 15px 24px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 20px;
|
||||||
|
left: -40px;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
opacity: 0;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 3px 3px 7px rgba(136, 136, 136, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -11px;
|
||||||
|
left: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
display: block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border-radius: 0 0 5px;
|
||||||
|
border-right: 1px solid #d7d7db;
|
||||||
|
border-bottom: 1px solid #d7d7db;
|
||||||
|
border-left: 1px solid #fff;
|
||||||
|
border-top: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__wrapper {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__message {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px #ebebeb solid;
|
||||||
|
color: var(--textColor);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: calc(100% + 48px);
|
||||||
|
margin-left: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__action {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__no {
|
||||||
|
color: #4a4a4a;
|
||||||
|
background-color: #fbfbfb;
|
||||||
|
border: 1px #c1c1c1 solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 25px;
|
||||||
|
font-weight: normal;
|
||||||
|
min-width: 94px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__no:hover {
|
||||||
|
background-color: #efeff1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__yes {
|
||||||
|
color: var(--primaryControlFGColor);
|
||||||
|
background-color: var(--primaryControlBGColor);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 25px;
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 94px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__yes:hover {
|
||||||
|
background-color: var(--primaryControlHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup--show {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 992px), (max-width: 992px) {
|
||||||
|
.popup {
|
||||||
|
left: auto;
|
||||||
|
right: -40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup::after {
|
||||||
|
left: auto;
|
||||||
|
right: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.popup::after {
|
||||||
|
left: 125px;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/templates/progress/index.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const percent = require('../../utils').percent;
|
||||||
|
|
||||||
|
const radius = 73;
|
||||||
|
const oRadius = radius + 10;
|
||||||
|
const oDiameter = oRadius * 2;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
|
module.exports = function(progressRatio, indefinite = false) {
|
||||||
|
const p = indefinite ? 0.2 : progressRatio;
|
||||||
|
const dashOffset = (1 - p) * circumference;
|
||||||
|
const progressPercent = html`
|
||||||
|
<text class="progress__percent" text-anchor="middle" x="50%" y="98">
|
||||||
|
${percent(progressRatio)}
|
||||||
|
</text>`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="progress">
|
||||||
|
<svg
|
||||||
|
width="${oDiameter}"
|
||||||
|
height="${oDiameter}"
|
||||||
|
viewPort="0 0 ${oDiameter} ${oDiameter}"
|
||||||
|
version="1.1">
|
||||||
|
<circle
|
||||||
|
class="progress__bg"
|
||||||
|
r="${radius}"
|
||||||
|
cx="${oRadius}"
|
||||||
|
cy="${oRadius}"
|
||||||
|
fill="transparent"/>
|
||||||
|
<circle
|
||||||
|
class="progress__indefinite ${indefinite ? '' : 'progress--invisible'}"
|
||||||
|
r="${radius}"
|
||||||
|
cx="${oRadius}"
|
||||||
|
cy="${oRadius}"
|
||||||
|
fill="transparent"
|
||||||
|
transform="rotate(-90 ${oRadius} ${oRadius})"
|
||||||
|
stroke-dasharray="${circumference}"
|
||||||
|
stroke-dashoffset="${dashOffset}"/>
|
||||||
|
<circle
|
||||||
|
class="progress__bar ${indefinite ? 'progress--invisible' : ''}"
|
||||||
|
r="${radius}"
|
||||||
|
cx="${oRadius}"
|
||||||
|
cy="${oRadius}"
|
||||||
|
fill="transparent"
|
||||||
|
transform="rotate(-90 ${oRadius} ${oRadius})"
|
||||||
|
stroke-dasharray="${circumference}"
|
||||||
|
stroke-dashoffset="${dashOffset}"/>
|
||||||
|
${indefinite ? '' : progressPercent}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
43
app/templates/progress/progress.css
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
.progress {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress__bg {
|
||||||
|
stroke: #eee;
|
||||||
|
stroke-width: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress__bar {
|
||||||
|
stroke: #3b9dff;
|
||||||
|
stroke-width: 0.75em;
|
||||||
|
transition: stroke-dashoffset 300ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress__indefinite {
|
||||||
|
stroke: #3b9dff;
|
||||||
|
stroke-width: 0.75em;
|
||||||
|
animation: 1s linear infinite spin;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress__percent {
|
||||||
|
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||||
|
font-size: 43.2px;
|
||||||
|
letter-spacing: -0.78px;
|
||||||
|
line-height: 58px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress--invisible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
59
app/templates/selectbox/index.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const number = require('../../utils').number;
|
||||||
|
|
||||||
|
module.exports = function(selected, options, translate, changed) {
|
||||||
|
const id = `select-${Math.random()}`;
|
||||||
|
let x = selected;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="selectbox">
|
||||||
|
<div onclick=${toggle}>
|
||||||
|
<span class="link">${translate(selected)}</span>
|
||||||
|
<svg width="32" height="32">
|
||||||
|
<polygon points="8 18 17 28 26 18" fill="#0094fb"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<ul id="${id}" class="selectbox__options">
|
||||||
|
${options.map(
|
||||||
|
i => html`
|
||||||
|
<li
|
||||||
|
class="selectbox__option"
|
||||||
|
onclick=${choose}
|
||||||
|
data-value="${i}">${number(i)}</li>`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
const ul = document.getElementById(id);
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
ul.classList.remove('selectbox__options--active');
|
||||||
|
body.removeEventListener('click', close);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const ul = document.getElementById(id);
|
||||||
|
if (ul.classList.contains('selectbox__options--active')) {
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
ul.classList.add('selectbox__options--active');
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
body.addEventListener('click', close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function choose(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const target = event.target;
|
||||||
|
const value = +target.dataset.value;
|
||||||
|
target.parentNode.previousSibling.firstElementChild.textContent = translate(
|
||||||
|
value
|
||||||
|
);
|
||||||
|
if (x !== value) {
|
||||||
|
x = value;
|
||||||
|
changed(value);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
36
app/templates/selectbox/selectbox.css
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
.selectbox {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectbox__options {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectbox__options--active {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 40px 0;
|
||||||
|
background-color: var(--pageBGColor);
|
||||||
|
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 1px 2px 4px rgba(12, 12, 13, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectbox__option {
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
font-size: 12pt;
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 60px;
|
||||||
|
border-bottom: 1px solid rgba(12, 12, 13, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectbox__option:hover {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
37
app/templates/setPasswordSection/index.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const passwordInput = require('../passwordInput');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const file = state.storage.getFileById(state.params.id);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="setPasswordSection">
|
||||||
|
<div class="checkbox">
|
||||||
|
<input
|
||||||
|
${file.hasPassword ? 'disabled' : ''}
|
||||||
|
${file.hasPassword || state.passwordSetError ? 'checked' : ''}
|
||||||
|
class="checkbox__input"
|
||||||
|
id="add-password"
|
||||||
|
type="checkbox"
|
||||||
|
autocomplete="off"
|
||||||
|
onchange=${togglePasswordInput}/>
|
||||||
|
<label class="checkbox__label" for="add-password">
|
||||||
|
${state.translate('requirePasswordCheckbox')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
${passwordInput(file, state, emit)}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
function togglePasswordInput(e) {
|
||||||
|
const unlockInput = document.getElementById('password-input');
|
||||||
|
const boxChecked = e.target.checked;
|
||||||
|
document
|
||||||
|
.querySelector('.passwordInput')
|
||||||
|
.classList.toggle('passwordInput--hidden', !boxChecked);
|
||||||
|
if (boxChecked) {
|
||||||
|
unlockInput.focus();
|
||||||
|
} else {
|
||||||
|
unlockInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
68
app/templates/setPasswordSection/setPasswordSection.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.setPasswordSection {
|
||||||
|
padding: 10px 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label {
|
||||||
|
line-height: 23px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--lightTextColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label::before {
|
||||||
|
content: '';
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 5px;
|
||||||
|
float: left;
|
||||||
|
border: 1px solid rgba(12, 12, 13, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:focus + .checkbox__label::before,
|
||||||
|
.checkbox:hover .checkbox__label::before {
|
||||||
|
border: 1px solid var(--primaryControlBGColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:checked + .checkbox__label {
|
||||||
|
color: var(--textColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:checked + .checkbox__label::before {
|
||||||
|
background-image: url('../assets/check-16-blue.svg');
|
||||||
|
background-position: 2px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:disabled + .checkbox__label {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:disabled + .checkbox__label::before {
|
||||||
|
background-image: url('../assets/check-16-blue.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 26px 26px;
|
||||||
|
border: none;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-device-width: 520px), (max-width: 520px) {
|
||||||
|
.setPasswordSection {
|
||||||
|
align-self: center;
|
||||||
|
min-width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label::before {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
173
app/utils.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
const b64 = require('base64-js');
|
||||||
|
|
||||||
|
function arrayToB64(array) {
|
||||||
|
return b64
|
||||||
|
.fromByteArray(array)
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/=/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function b64ToArray(str) {
|
||||||
|
str = (str + '==='.slice((str.length + 3) % 4))
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
return b64.toByteArray(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadShim(polyfill) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const shim = document.createElement('script');
|
||||||
|
shim.src = polyfill;
|
||||||
|
shim.addEventListener('load', () => resolve(true));
|
||||||
|
shim.addEventListener('error', () => resolve(false));
|
||||||
|
document.head.appendChild(shim);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function canHasSend(polyfill) {
|
||||||
|
try {
|
||||||
|
const key = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
length: 128
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||||
|
tagLength: 128
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
new ArrayBuffer(8)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return loadShim(polyfill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFile(id) {
|
||||||
|
return /^[0-9a-fA-F]{10}$/.test(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(str) {
|
||||||
|
const aux = document.createElement('input');
|
||||||
|
aux.setAttribute('value', str);
|
||||||
|
aux.contentEditable = true;
|
||||||
|
aux.readOnly = true;
|
||||||
|
document.body.appendChild(aux);
|
||||||
|
if (navigator.userAgent.match(/iphone|ipad|ipod/i)) {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(aux);
|
||||||
|
const sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
aux.setSelectionRange(0, str.length);
|
||||||
|
} else {
|
||||||
|
aux.select();
|
||||||
|
}
|
||||||
|
const result = document.execCommand('copy');
|
||||||
|
document.body.removeChild(aux);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOCALIZE_NUMBERS = !!(
|
||||||
|
typeof Intl === 'object' &&
|
||||||
|
Intl &&
|
||||||
|
typeof Intl.NumberFormat === 'function' &&
|
||||||
|
typeof navigator === 'object'
|
||||||
|
);
|
||||||
|
|
||||||
|
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
||||||
|
function bytes(num) {
|
||||||
|
if (num < 1) {
|
||||||
|
return '0B';
|
||||||
|
}
|
||||||
|
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
|
||||||
|
const n = Number(num / Math.pow(1000, exponent));
|
||||||
|
let nStr = n.toFixed(1);
|
||||||
|
if (LOCALIZE_NUMBERS) {
|
||||||
|
try {
|
||||||
|
const locale = document.querySelector('html').lang;
|
||||||
|
nStr = n.toLocaleString(locale, {
|
||||||
|
minimumFractionDigits: 1,
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${nStr}${UNITS[exponent]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function percent(ratio) {
|
||||||
|
if (LOCALIZE_NUMBERS) {
|
||||||
|
try {
|
||||||
|
const locale = document.querySelector('html').lang;
|
||||||
|
return ratio.toLocaleString(locale, { style: 'percent' });
|
||||||
|
} catch (e) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${Math.floor(ratio * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function number(n) {
|
||||||
|
if (LOCALIZE_NUMBERS) {
|
||||||
|
const locale = document.querySelector('html').lang;
|
||||||
|
return n.toLocaleString(locale);
|
||||||
|
}
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowedCopy() {
|
||||||
|
const support = !!document.queryCommandSupported;
|
||||||
|
return support ? document.queryCommandSupported('copy') : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(delay = 100) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fadeOut(selector) {
|
||||||
|
const classes = document.querySelector(selector).classList;
|
||||||
|
classes.remove('effect--fadeIn');
|
||||||
|
classes.add('effect--fadeOut');
|
||||||
|
return delay(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLinksInNewTab(links, should = true) {
|
||||||
|
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
||||||
|
if (should) {
|
||||||
|
links.forEach(l => {
|
||||||
|
l.setAttribute('target', '_blank');
|
||||||
|
l.setAttribute('rel', 'noopener noreferrer');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
links.forEach(l => {
|
||||||
|
l.removeAttribute('target');
|
||||||
|
l.removeAttribute('rel');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fadeOut,
|
||||||
|
delay,
|
||||||
|
allowedCopy,
|
||||||
|
bytes,
|
||||||
|
percent,
|
||||||
|
number,
|
||||||
|
copyToClipboard,
|
||||||
|
arrayToB64,
|
||||||
|
b64ToArray,
|
||||||
|
canHasSend,
|
||||||
|
isFile,
|
||||||
|
openLinksInNewTab
|
||||||
|
};
|
||||||
1
assets/check-16-blue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#0A84FF " d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"/></svg>
|
||||||
|
After Width: | Height: | Size: 238 B |
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 287 B |
1
assets/copy-16.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#0A8DFF" d="M14.707 8.293l-3-3A1 1 0 0 0 11 5h-1V4a1 1 0 0 0-.293-.707l-3-3A1 1 0 0 0 6 0H3a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3v3a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V9a1 1 0 0 0-.293-.707zM12.586 9H11V7.414zm-5-5H6V2.414zM6 7v2H3V2h2v2.5a.5.5 0 0 0 .5.5H8a2 2 0 0 0-2 2zm2 7V7h2v2.5a.5.5 0 0 0 .5.5H13v4z"/></svg>
|
||||||
|
After Width: | Height: | Size: 398 B |
BIN
assets/favicon-120.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
assets/favicon-128.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/favicon-144.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/favicon-152.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/favicon-167.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
assets/favicon-180.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/favicon-195.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
assets/favicon-196.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/favicon-228.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/favicon-96.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 649 B After Width: | Height: | Size: 649 B |
1
assets/firefox_logo-only.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 312 KiB After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
1
assets/send_logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="30" height="27" viewBox="0 0 30 27" xmlns="http://www.w3.org/2000/svg"><title>send logo</title><g stroke="#3E3D40" fill="none" fill-rule="evenodd" transform="translate(-0.231,0.11948695)"><path d="M22.364 19.989l-2.153-2.103a2.046 2.046 0 0 0-2.665-.151l3.402 3.323a.531.531 0 0 1 0 .766l-2.466 2.408a.563.563 0 0 1-.784 0l-3.398-3.32a1.932 1.932 0 0 0 .188 2.564l2.153 2.103c.788.77 2.066.77 2.855 0l2.868-2.802a1.94 1.94 0 0 0 0-2.788M8.77 14.745a.534.534 0 0 0 0 .766l3.399 3.32a2.05 2.05 0 0 1-2.625-.184l-2.153-2.102a1.94 1.94 0 0 1 0-2.79l2.869-2.801a2.052 2.052 0 0 1 2.854 0l2.153 2.103c.73.713.775 1.83.154 2.603l-3.401-3.323a.565.565 0 0 0-.784 0L8.77 14.745zm9.464 5.682a.777.777 0 0 1 0 1.118.822.822 0 0 1-1.144 0l-5.6-5.47a.777.777 0 0 1 0-1.118.822.822 0 0 1 1.144 0l5.6 5.47z" stroke-width=".618" fill="#3E3D40"/><path d="M6.065 20.606c-2.913-1.586-3.988-3.656-3.988-6.468 0-2.81 2.265-6.425 5.786-6.289.1.004.55-.006.649 0 .895-3.27 2.508-6.353 6.898-6.353 4.557 0 7.336 3.716 6.75 7.785.08-.005 1.232.17 1.31.186 3.096.644 4.915 3.275 4.915 5.18 0 1.905-.107 3.029-2.023 4.947" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
17
assets/spinner.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
||||||
|
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g transform="translate(1 1)" stroke-width="2">
|
||||||
|
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
|
||||||
|
<path d="M36 18c0-9.94-8.06-18-18-18">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0 18 18"
|
||||||
|
to="360 18 18"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"/>
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 694 B |
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 873 B |
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
10
browserconfig.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square70x70logo src=“favicon-76.png”/>
|
||||||
|
<square150x150logo src="favicon-228.png"/>
|
||||||
|
<TileColor>#0297F8</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
last 2 chrome versions
|
last 2 chrome versions
|
||||||
last 2 firefox versions
|
last 2 firefox versions
|
||||||
firefox esr
|
firefox esr
|
||||||
ie >= 9
|
safari > 9
|
||||||
safari >= 9
|
|
||||||
|
|||||||
61
build/fluent_loader.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// TODO: when node supports 'for await' we can remove babel-polyfill
|
||||||
|
// and use 'fluent' instead of 'fluent/compat' (also below near line 42)
|
||||||
|
require('babel-polyfill');
|
||||||
|
const { MessageContext } = require('fluent/compat');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
function toJSON(map) {
|
||||||
|
return JSON.stringify(Array.from(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge(m1, m2) {
|
||||||
|
const result = new Map(m1);
|
||||||
|
for (const [k, v] of m2) {
|
||||||
|
result.set(k, v);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(source) {
|
||||||
|
const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
|
||||||
|
const result = localeExp.exec(this.resourcePath);
|
||||||
|
const locale = result && result[1];
|
||||||
|
if (!locale) {
|
||||||
|
throw new Error(`couldn't find locale in: ${this.resourcePath}`);
|
||||||
|
}
|
||||||
|
// load default language and "merge" contexts
|
||||||
|
// TODO: make this configurable
|
||||||
|
const en_ftl = fs.readFileSync(
|
||||||
|
require.resolve('../public/locales/en-US/send.ftl'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
const en = new MessageContext('en-US');
|
||||||
|
en.addMessages(en_ftl);
|
||||||
|
// pre-parse the ftl
|
||||||
|
const context = new MessageContext(locale);
|
||||||
|
context.addMessages(source);
|
||||||
|
|
||||||
|
const merged = merge(en._messages, context._messages);
|
||||||
|
return `
|
||||||
|
module.exports = \`
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
require('babel-polyfill');
|
||||||
|
var fluent = require('fluent/compat');
|
||||||
|
}
|
||||||
|
var fluentContext = new fluent.MessageContext('${locale}', {useIsolating: false});
|
||||||
|
fluentContext._messages = new Map(${toJSON(merged)});
|
||||||
|
function translate(id, data) {
|
||||||
|
var msg = fluentContext.getMessage(id);
|
||||||
|
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
||||||
|
msg = msg.attrs.title || msg.attrs.alt
|
||||||
|
}
|
||||||
|
return fluentContext.format(msg, data);
|
||||||
|
}
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
module.exports = translate;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.translate = translate;
|
||||||
|
}
|
||||||
|
\``;
|
||||||
|
};
|
||||||
19
build/generate_asset_map.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function kv(f) {
|
||||||
|
return `"${f}": require('../assets/${f}')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
const files = fs.readdirSync(path.join(__dirname, '..', 'assets'));
|
||||||
|
const code = `module.exports = {
|
||||||
|
"package.json": require('../package.json'),
|
||||||
|
${files.map(kv).join(',\n')}
|
||||||
|
};`;
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
dependencies: files.map(f => require.resolve('../assets/' + f)),
|
||||||
|
cacheable: false
|
||||||
|
};
|
||||||
|
};
|
||||||
22
build/generate_l10n_map.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function kv(d) {
|
||||||
|
return `"${d}": require('../public/locales/${d}/send.ftl')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
const dirs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
|
||||||
|
const code = `
|
||||||
|
module.exports = {
|
||||||
|
translate: function (id, data) { return window.translate(id, data) },
|
||||||
|
${dirs.map(kv).join(',\n')}
|
||||||
|
};`;
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
dependencies: dirs.map(d =>
|
||||||
|
require.resolve(`../public/locales/${d}/send.ftl`)
|
||||||
|
),
|
||||||
|
cacheable: false
|
||||||
|
};
|
||||||
|
};
|
||||||
11
build/package_json_loader.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const commit = require('git-rev-sync').short();
|
||||||
|
|
||||||
|
module.exports = function(source) {
|
||||||
|
const pkg = JSON.parse(source);
|
||||||
|
const version = {
|
||||||
|
commit,
|
||||||
|
source: pkg.homepage,
|
||||||
|
version: process.env.CIRCLE_TAG || `v${pkg.version}`
|
||||||
|
};
|
||||||
|
return `module.exports = '${JSON.stringify(version)}'`;
|
||||||
|
};
|
||||||